mirror of
https://github.com/hanzokms/terraform-provider.git
synced 2026-10-30 20:29:45 +00:00
487 lines
17 KiB
Go
487 lines
17 KiB
Go
package resource
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
kmsclient "github.com/hanzokms/terraform-provider/internal/client"
|
|
|
|
"github.com/hashicorp/terraform-plugin-framework/diag"
|
|
"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/planmodifier"
|
|
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
|
|
"github.com/hashicorp/terraform-plugin-framework/types"
|
|
)
|
|
|
|
// SecretSyncBaseResource is the resource implementation.
|
|
type SecretSyncBaseResource struct {
|
|
App kmsclient.SecretSyncApp // used for identifying secret sync route
|
|
ResourceTypeName string // terraform resource name suffix
|
|
SyncName string // complete descriptive name of the secret sync
|
|
AppConnection kmsclient.AppConnectionApp
|
|
CanImportSecrets bool // whether this sync destination supports importing secrets from the destination
|
|
client *kmsclient.Client
|
|
DestinationConfigAttributes map[string]schema.Attribute
|
|
ReadDestinationConfigForCreateFromPlan func(ctx context.Context, plan SecretSyncBaseResourceModel) (map[string]interface{}, diag.Diagnostics)
|
|
ReadDestinationConfigForUpdateFromPlan func(ctx context.Context, plan SecretSyncBaseResourceModel, state SecretSyncBaseResourceModel) (map[string]interface{}, diag.Diagnostics)
|
|
ReadDestinationConfigFromApi func(ctx context.Context, secretSync kmsclient.SecretSync) (types.Object, diag.Diagnostics)
|
|
|
|
SyncOptionsAttributes map[string]schema.Attribute
|
|
ReadSyncOptionsForCreateFromPlan func(ctx context.Context, plan SecretSyncBaseResourceModel) (map[string]interface{}, diag.Diagnostics)
|
|
ReadSyncOptionsForUpdateFromPlan func(ctx context.Context, plan SecretSyncBaseResourceModel, state SecretSyncBaseResourceModel) (map[string]interface{}, diag.Diagnostics)
|
|
ReadSyncOptionsFromApi func(ctx context.Context, secretSync kmsclient.SecretSync) (types.Object, diag.Diagnostics)
|
|
}
|
|
|
|
type SecretSyncBaseResourceModel struct {
|
|
ID types.String `tfsdk:"id"`
|
|
ConnectionID types.String `tfsdk:"connection_id"`
|
|
Name types.String `tfsdk:"name"`
|
|
ProjectID types.String `tfsdk:"project_id"`
|
|
Description types.String `tfsdk:"description"`
|
|
Environment types.String `tfsdk:"environment"`
|
|
SecretPath types.String `tfsdk:"secret_path"`
|
|
SyncOptions types.Object `tfsdk:"sync_options"`
|
|
AutoSyncEnabled types.Bool `tfsdk:"auto_sync_enabled"`
|
|
DestinationConfig types.Object `tfsdk:"destination_config"`
|
|
}
|
|
|
|
// Metadata returns the resource type name.
|
|
func (r *SecretSyncBaseResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
|
|
resp.TypeName = req.ProviderTypeName + r.ResourceTypeName
|
|
}
|
|
|
|
func (r *SecretSyncBaseResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
|
|
resp.Schema = schema.Schema{
|
|
Description: fmt.Sprintf("Create and manage %s secret syncs", r.SyncName),
|
|
Attributes: map[string]schema.Attribute{
|
|
"id": schema.StringAttribute{
|
|
Description: fmt.Sprintf("The ID of the %s secret sync", r.SyncName),
|
|
Computed: true,
|
|
PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()},
|
|
},
|
|
"connection_id": schema.StringAttribute{
|
|
Required: true,
|
|
Description: fmt.Sprintf("The ID of the %s Connection to use for syncing.", r.AppConnection),
|
|
},
|
|
"name": schema.StringAttribute{
|
|
Required: true,
|
|
Description: fmt.Sprintf("The name of the %s sync to create. Must be slug-friendly.", r.SyncName),
|
|
},
|
|
"project_id": schema.StringAttribute{
|
|
Required: true,
|
|
Description: "The ID of the Kms project to create the sync in.",
|
|
PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()},
|
|
},
|
|
"environment": schema.StringAttribute{
|
|
Required: true,
|
|
Description: "The slug of the project environment to sync secrets from.",
|
|
},
|
|
"secret_path": schema.StringAttribute{
|
|
Required: true,
|
|
Description: "The folder path to sync secrets from.",
|
|
},
|
|
"description": schema.StringAttribute{
|
|
Optional: true,
|
|
Description: fmt.Sprintf("An optional description for the %s sync.", r.SyncName),
|
|
},
|
|
"auto_sync_enabled": schema.BoolAttribute{
|
|
Optional: true,
|
|
Description: "Whether secrets should be automatically synced when changes occur at the source location or not.",
|
|
Default: booldefault.StaticBool(true),
|
|
Computed: true,
|
|
},
|
|
"sync_options": schema.SingleNestedAttribute{
|
|
Required: true,
|
|
Description: "Parameters to modify how secrets are synced.",
|
|
Attributes: r.SyncOptionsAttributes,
|
|
},
|
|
"destination_config": schema.SingleNestedAttribute{
|
|
Required: true,
|
|
Description: "The destination configuration for the secret sync.",
|
|
Attributes: r.DestinationConfigAttributes,
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// Configure adds the provider configured client to the resource.
|
|
func (r *SecretSyncBaseResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
|
|
if req.ProviderData == nil {
|
|
return
|
|
}
|
|
|
|
client, ok := req.ProviderData.(*kmsclient.Client)
|
|
|
|
if !ok {
|
|
resp.Diagnostics.AddError(
|
|
"Unexpected Resource Source Configure Type",
|
|
fmt.Sprintf("Expected *http.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
|
|
)
|
|
|
|
return
|
|
}
|
|
|
|
r.client = client
|
|
}
|
|
|
|
func (r *SecretSyncBaseResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
|
|
if req.Plan.Raw.IsNull() || r.client == nil || !r.client.Config.IsMachineIdentityAuth {
|
|
return
|
|
}
|
|
|
|
var plan SecretSyncBaseResourceModel
|
|
if diags := req.Plan.Get(ctx, &plan); diags.HasError() {
|
|
return
|
|
}
|
|
|
|
r.addOverwriteDestinationWarnings(ctx, plan, resp)
|
|
|
|
if !req.State.Raw.IsNull() {
|
|
var state SecretSyncBaseResourceModel
|
|
if diags := req.State.Get(ctx, &state); !diags.HasError() {
|
|
if plan.DestinationConfig.Equal(state.DestinationConfig) {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
var destinationConfigMap map[string]interface{}
|
|
var configDiags diag.Diagnostics
|
|
var excludeSyncId string
|
|
|
|
if req.State.Raw.IsNull() {
|
|
destinationConfigMap, configDiags = r.ReadDestinationConfigForCreateFromPlan(ctx, plan)
|
|
} else {
|
|
var state SecretSyncBaseResourceModel
|
|
if diags := req.State.Get(ctx, &state); diags.HasError() {
|
|
return
|
|
}
|
|
destinationConfigMap, configDiags = r.ReadDestinationConfigForUpdateFromPlan(ctx, plan, state)
|
|
if !state.ID.IsNull() && !state.ID.IsUnknown() {
|
|
excludeSyncId = state.ID.ValueString()
|
|
}
|
|
}
|
|
|
|
if configDiags.HasError() {
|
|
return
|
|
}
|
|
|
|
checkRequest := kmsclient.CheckDuplicateDestinationRequest{
|
|
App: r.App,
|
|
DestinationConfig: destinationConfigMap,
|
|
ProjectID: plan.ProjectID.ValueString(),
|
|
ExcludeSyncId: excludeSyncId,
|
|
}
|
|
|
|
duplicateCheck, err := r.client.CheckDuplicateDestination(checkRequest)
|
|
if err != nil {
|
|
resp.Diagnostics.AddWarning(
|
|
"Unable to check for duplicate destinations",
|
|
"Warning: Could not verify if this destination already exists. This may result in conflicts. Error: "+err.Error(),
|
|
)
|
|
} else if duplicateCheck.HasDuplicate {
|
|
resp.Diagnostics.AddWarning(
|
|
"Duplicate destination configuration detected",
|
|
"A secret sync with the same destination configuration already exists in this project. Proceeding with this operation may result in conflicts or unexpected behavior.",
|
|
)
|
|
}
|
|
}
|
|
|
|
// addOverwriteDestinationWarnings adds warnings when using overwrite destination initial sync behavior.
|
|
func (r *SecretSyncBaseResource) addOverwriteDestinationWarnings(ctx context.Context, plan SecretSyncBaseResourceModel, resp *resource.ModifyPlanResponse) {
|
|
syncOptions, diags := r.ReadSyncOptionsForCreateFromPlan(ctx, plan)
|
|
if diags.HasError() {
|
|
return
|
|
}
|
|
|
|
initialSyncBehavior, _ := syncOptions["initialSyncBehavior"].(string)
|
|
disableSecretDeletion, _ := syncOptions["disableSecretDeletion"].(bool)
|
|
|
|
if disableSecretDeletion {
|
|
return
|
|
}
|
|
|
|
if !r.CanImportSecrets {
|
|
resp.Diagnostics.AddWarning(
|
|
fmt.Sprintf("Existing secrets from %s may be removed", r.SyncName),
|
|
fmt.Sprintf("Secrets not present in Kms will be removed from the destination. Consider adding a key_schema or setting disable_secret_deletion to true if you do not want existing secrets to be removed from %s.", r.SyncName),
|
|
)
|
|
} else if initialSyncBehavior == string(kmsclient.SecretSyncBehaviorOverwriteDestination) {
|
|
// Destination supports import but user chose overwrite - warn about secret deletion with import suggestion
|
|
resp.Diagnostics.AddWarning(
|
|
"Overwrite destination will remove secrets not present in Kms",
|
|
fmt.Sprintf("Secrets not present in Kms will be removed from the destination. If you have secrets in %s that you do not want deleted, consider setting initial_sync_behavior to import-prioritize-source or import-prioritize-destination. Alternatively, configure a key_schema or set disable_secret_deletion to true to have Kms ignore these secrets.", r.SyncName),
|
|
)
|
|
}
|
|
}
|
|
|
|
// Create creates the resource and sets the initial Terraform state.
|
|
func (r *SecretSyncBaseResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
|
|
if !r.client.Config.IsMachineIdentityAuth {
|
|
resp.Diagnostics.AddError(
|
|
"Unable to create secret sync",
|
|
"Only Machine Identity authentication is supported for this operation",
|
|
)
|
|
return
|
|
}
|
|
|
|
// Retrieve values from plan
|
|
var plan SecretSyncBaseResourceModel
|
|
diags := req.Plan.Get(ctx, &plan)
|
|
resp.Diagnostics.Append(diags...)
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
|
|
syncOptions, diags := r.ReadSyncOptionsForCreateFromPlan(ctx, plan)
|
|
resp.Diagnostics.Append(diags...)
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
|
|
initialSyncBehavior, ok := syncOptions["initialSyncBehavior"].(string)
|
|
if !ok {
|
|
initialSyncBehavior = ""
|
|
}
|
|
|
|
if initialSyncBehavior == "" {
|
|
resp.Diagnostics.AddError(
|
|
"Unable to create secret sync",
|
|
"Failed to parse initial_sync_behavior field",
|
|
)
|
|
return
|
|
}
|
|
|
|
switch kmsclient.SecretSyncBehavior(initialSyncBehavior) {
|
|
case kmsclient.SecretSyncBehaviorOverwriteDestination, kmsclient.SecretSyncBehaviorPrioritizeDestination, kmsclient.SecretSyncBehaviorPrioritizeSource:
|
|
break
|
|
default:
|
|
resp.Diagnostics.AddError(
|
|
"Unable to create secret sync",
|
|
fmt.Sprintf("Invalid value for initial_sync_behavior field. Possible values are: %s, %s, %s",
|
|
kmsclient.SecretSyncBehaviorOverwriteDestination, kmsclient.SecretSyncBehaviorPrioritizeDestination,
|
|
kmsclient.SecretSyncBehaviorPrioritizeSource),
|
|
)
|
|
return
|
|
}
|
|
|
|
destinationConfigMap, diags := r.ReadDestinationConfigForCreateFromPlan(ctx, plan)
|
|
if diags.HasError() {
|
|
resp.Diagnostics.Append(diags...)
|
|
return
|
|
}
|
|
|
|
secretSync, err := r.client.CreateSecretSync(kmsclient.CreateSecretSyncRequest{
|
|
App: r.App,
|
|
Name: plan.Name.ValueString(),
|
|
Description: plan.Description.ValueString(),
|
|
ProjectID: plan.ProjectID.ValueString(),
|
|
ConnectionID: plan.ConnectionID.ValueString(),
|
|
Environment: plan.Environment.ValueString(),
|
|
SecretPath: plan.SecretPath.ValueString(),
|
|
AutoSyncEnabled: plan.AutoSyncEnabled.ValueBool(),
|
|
SyncOptions: syncOptions,
|
|
DestinationConfig: destinationConfigMap,
|
|
})
|
|
|
|
if err != nil {
|
|
resp.Diagnostics.AddError(
|
|
"Error creating secret sync",
|
|
"Couldn't create secret sync, unexpected error: "+err.Error(),
|
|
)
|
|
return
|
|
}
|
|
|
|
plan.ID = types.StringValue(secretSync.ID)
|
|
|
|
diags = resp.State.Set(ctx, plan)
|
|
resp.Diagnostics.Append(diags...)
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
}
|
|
|
|
// Read refreshes the Terraform state with the latest data.
|
|
func (r *SecretSyncBaseResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
|
|
if !r.client.Config.IsMachineIdentityAuth {
|
|
resp.Diagnostics.AddError(
|
|
"Unable to read secret sync",
|
|
"Only Machine Identity authentication is supported for this operation",
|
|
)
|
|
return
|
|
}
|
|
|
|
// Get current state
|
|
var state SecretSyncBaseResourceModel
|
|
diags := req.State.Get(ctx, &state)
|
|
resp.Diagnostics.Append(diags...)
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
|
|
secretSync, err := r.client.GetSecretSyncById(kmsclient.GetSecretSyncByIdRequest{
|
|
App: r.App,
|
|
ID: state.ID.ValueString(),
|
|
})
|
|
|
|
if err != nil {
|
|
if err == kmsclient.ErrNotFound {
|
|
resp.State.RemoveResource(ctx)
|
|
return
|
|
} else {
|
|
resp.Diagnostics.AddError(
|
|
"Error reading secret sync",
|
|
"Couldn't read secret sync, unexpected error: "+err.Error(),
|
|
)
|
|
return
|
|
}
|
|
}
|
|
|
|
state.ConnectionID = types.StringValue(secretSync.Connection.ConnectionID)
|
|
state.Name = types.StringValue(secretSync.Name)
|
|
state.ProjectID = types.StringValue(secretSync.ProjectID)
|
|
state.Environment = types.StringValue(secretSync.Environment.Slug)
|
|
state.SecretPath = types.StringValue(secretSync.SecretFolder.Path)
|
|
state.AutoSyncEnabled = types.BoolValue(secretSync.AutoSyncEnabled)
|
|
|
|
if !(state.Description.IsNull() && secretSync.Description == "") {
|
|
state.Description = types.StringValue(secretSync.Description)
|
|
}
|
|
|
|
state.SyncOptions, diags = r.ReadSyncOptionsFromApi(ctx, secretSync)
|
|
resp.Diagnostics.Append(diags...)
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
|
|
state.DestinationConfig, diags = r.ReadDestinationConfigFromApi(ctx, secretSync)
|
|
resp.Diagnostics.Append(diags...)
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
|
|
diags = resp.State.Set(ctx, state)
|
|
resp.Diagnostics.Append(diags...)
|
|
}
|
|
|
|
func (r *SecretSyncBaseResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
|
|
if !r.client.Config.IsMachineIdentityAuth {
|
|
resp.Diagnostics.AddError(
|
|
"Unable to update secret sync",
|
|
"Only Machine Identity authentication is supported for this operation",
|
|
)
|
|
return
|
|
}
|
|
|
|
// Retrieve values from plan
|
|
var plan SecretSyncBaseResourceModel
|
|
diags := req.Plan.Get(ctx, &plan)
|
|
resp.Diagnostics.Append(diags...)
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
|
|
var state SecretSyncBaseResourceModel
|
|
diags = req.State.Get(ctx, &state)
|
|
resp.Diagnostics.Append(diags...)
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
|
|
secretSyncPlan, diags := r.ReadSyncOptionsForUpdateFromPlan(ctx, plan, state)
|
|
resp.Diagnostics.Append(diags...)
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
|
|
initialSyncBehavior, ok := secretSyncPlan["initialSyncBehavior"].(string)
|
|
if !ok {
|
|
initialSyncBehavior = ""
|
|
}
|
|
|
|
if initialSyncBehavior == "" {
|
|
resp.Diagnostics.AddError(
|
|
"Unable to update secret sync",
|
|
"Failed to parse initial_sync_behavior field",
|
|
)
|
|
return
|
|
}
|
|
|
|
switch kmsclient.SecretSyncBehavior(initialSyncBehavior) {
|
|
case kmsclient.SecretSyncBehaviorOverwriteDestination, kmsclient.SecretSyncBehaviorPrioritizeDestination, kmsclient.SecretSyncBehaviorPrioritizeSource:
|
|
break
|
|
default:
|
|
resp.Diagnostics.AddError(
|
|
"Unable to update secret sync",
|
|
fmt.Sprintf("Invalid value for initial_sync_behavior field. Possible values are: %s, %s, %s",
|
|
kmsclient.SecretSyncBehaviorOverwriteDestination, kmsclient.SecretSyncBehaviorPrioritizeDestination,
|
|
kmsclient.SecretSyncBehaviorPrioritizeSource),
|
|
)
|
|
return
|
|
}
|
|
|
|
destinationConfigMap, diags := r.ReadDestinationConfigForUpdateFromPlan(ctx, plan, state)
|
|
if diags.HasError() {
|
|
resp.Diagnostics.Append(diags...)
|
|
return
|
|
}
|
|
|
|
_, err := r.client.UpdateSecretSync(kmsclient.UpdateSecretSyncRequest{
|
|
App: r.App,
|
|
ID: state.ID.ValueString(),
|
|
Name: plan.Name.ValueString(),
|
|
Description: plan.Description.ValueString(),
|
|
ProjectID: plan.ProjectID.ValueString(),
|
|
ConnectionID: plan.ConnectionID.ValueString(),
|
|
Environment: plan.Environment.ValueString(),
|
|
SecretPath: plan.SecretPath.ValueString(),
|
|
AutoSyncEnabled: plan.AutoSyncEnabled.ValueBool(),
|
|
SyncOptions: secretSyncPlan,
|
|
DestinationConfig: destinationConfigMap,
|
|
})
|
|
|
|
if err != nil {
|
|
resp.Diagnostics.AddError(
|
|
"Error updating secret sync",
|
|
"Couldn't update secret sync, unexpected error: "+err.Error(),
|
|
)
|
|
return
|
|
}
|
|
|
|
diags = resp.State.Set(ctx, plan)
|
|
resp.Diagnostics.Append(diags...)
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
}
|
|
|
|
func (r *SecretSyncBaseResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
|
|
if !r.client.Config.IsMachineIdentityAuth {
|
|
resp.Diagnostics.AddError(
|
|
"Unable to delete secret sync",
|
|
"Only Machine Identity authentication is supported for this operation",
|
|
)
|
|
return
|
|
}
|
|
|
|
var state SecretSyncBaseResourceModel
|
|
diags := req.State.Get(ctx, &state)
|
|
resp.Diagnostics.Append(diags...)
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
|
|
_, err := r.client.DeleteSecretSync(kmsclient.DeleteSecretSyncRequest{
|
|
App: r.App,
|
|
ID: state.ID.ValueString(),
|
|
})
|
|
|
|
if err != nil {
|
|
resp.Diagnostics.AddError(
|
|
"Error deleting secret sync",
|
|
"Couldn't delete secret sync from Kms, unexpected error: "+err.Error(),
|
|
)
|
|
}
|
|
}
|