mirror of
https://github.com/hanzokms/terraform-provider.git
synced 2026-10-30 20:29:45 +00:00
786 lines
28 KiB
Go
786 lines
28 KiB
Go
package resource
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
kmsclient "github.com/hanzokms/terraform-provider/internal/client"
|
|
"reflect"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/hashicorp/terraform-plugin-framework/attr"
|
|
"github.com/hashicorp/terraform-plugin-framework/diag"
|
|
"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/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
|
|
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.StringAttribute{
|
|
Required: true,
|
|
Description: buildJSONSchemaDescription(
|
|
"Parameters to modify how secrets are synced.",
|
|
r.SyncOptionsAttributes,
|
|
),
|
|
},
|
|
|
|
"destination_config": schema.StringAttribute{
|
|
Required: true,
|
|
Description: buildJSONSchemaDescription(
|
|
"The destination configuration for the secret sync.",
|
|
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
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
var syncOptions map[string]interface{}
|
|
var destinationConfigMap map[string]interface{}
|
|
|
|
// first we parse sync_options and destination_config from the plan
|
|
var syncOptsJSON types.String
|
|
diags := req.Plan.GetAttribute(ctx, path.Root("sync_options"), &syncOptsJSON)
|
|
resp.Diagnostics.Append(diags...)
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
|
|
if err := json.Unmarshal([]byte(syncOptsJSON.ValueString()), &syncOptions); err != nil {
|
|
resp.Diagnostics.AddError("Invalid JSON", fmt.Sprintf("Failed to parse sync_options: %s", err.Error()))
|
|
return
|
|
}
|
|
// transform sync_options since we're not calling a function for it
|
|
syncOptions = transformMapKeys(syncOptions)
|
|
|
|
var destConfigJSON types.String
|
|
diags = req.Plan.GetAttribute(ctx, path.Root("destination_config"), &destConfigJSON)
|
|
resp.Diagnostics.Append(diags...)
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
|
|
var tempDestConfigMap map[string]interface{}
|
|
if err := json.Unmarshal([]byte(destConfigJSON.ValueString()), &tempDestConfigMap); err != nil {
|
|
resp.Diagnostics.AddError("Invalid JSON", fmt.Sprintf("Failed to parse destination_config: %s", err.Error()))
|
|
return
|
|
}
|
|
|
|
// convert map to types.Object (with snake_case keys because this is the convention we follow for all map attributes across all secret syncs)
|
|
destConfigObj, d := mapToTypesObject(tempDestConfigMap, r.DestinationConfigAttributes)
|
|
resp.Diagnostics.Append(d...)
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
|
|
// create temporary plan model that we pass into the ReadDestinationConfigForCreateFromPlan function for the validation
|
|
tempPlan := SecretSyncBaseResourceModel{
|
|
DestinationConfig: destConfigObj,
|
|
SyncOptions: types.ObjectNull(nil),
|
|
}
|
|
|
|
// This function validates with snake_case keys and transforms to API format (camelCase)
|
|
destinationConfigMap, diags = r.ReadDestinationConfigForCreateFromPlan(ctx, tempPlan)
|
|
resp.Diagnostics.Append(diags...)
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
|
|
// get the rest of the plan fields (common to both modes)
|
|
// we have to get them individually because if we try to read the whole plan at once, we'll get type errors when trying to read the sync_options and destination_config attributes. there's no pretty way to do this
|
|
var planName types.String
|
|
var planDescription types.String
|
|
var planProjectID types.String
|
|
var planConnectionID types.String
|
|
var planEnvironment types.String
|
|
var planSecretPath types.String
|
|
var planAutoSyncEnabled types.Bool
|
|
|
|
req.Plan.GetAttribute(ctx, path.Root("name"), &planName)
|
|
req.Plan.GetAttribute(ctx, path.Root("description"), &planDescription)
|
|
req.Plan.GetAttribute(ctx, path.Root("project_id"), &planProjectID)
|
|
req.Plan.GetAttribute(ctx, path.Root("connection_id"), &planConnectionID)
|
|
req.Plan.GetAttribute(ctx, path.Root("environment"), &planEnvironment)
|
|
req.Plan.GetAttribute(ctx, path.Root("secret_path"), &planSecretPath)
|
|
req.Plan.GetAttribute(ctx, path.Root("auto_sync_enabled"), &planAutoSyncEnabled)
|
|
|
|
// Validation
|
|
initialSyncBehavior, ok := syncOptions["initialSyncBehavior"].(string)
|
|
if !ok || 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
|
|
}
|
|
|
|
secretSync, err := r.client.CreateSecretSync(kmsclient.CreateSecretSyncRequest{
|
|
App: r.App,
|
|
Name: planName.ValueString(),
|
|
Description: planDescription.ValueString(),
|
|
ProjectID: planProjectID.ValueString(),
|
|
ConnectionID: planConnectionID.ValueString(),
|
|
Environment: planEnvironment.ValueString(),
|
|
SecretPath: planSecretPath.ValueString(),
|
|
AutoSyncEnabled: planAutoSyncEnabled.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
|
|
}
|
|
|
|
// set the state
|
|
var plan SecretSyncBaseResourceModel
|
|
plan.ID = types.StringValue(secretSync.ID)
|
|
plan.Name = planName
|
|
plan.Description = planDescription
|
|
plan.ProjectID = planProjectID
|
|
plan.ConnectionID = planConnectionID
|
|
plan.Environment = planEnvironment
|
|
plan.SecretPath = planSecretPath
|
|
plan.AutoSyncEnabled = planAutoSyncEnabled
|
|
|
|
// get the original json strings from plan
|
|
var syncOptsJSONOriginal types.String
|
|
req.Plan.GetAttribute(ctx, path.Root("sync_options"), &syncOptsJSONOriginal)
|
|
resp.State.SetAttribute(ctx, path.Root("sync_options"), syncOptsJSONOriginal.ValueString())
|
|
|
|
var destConfigJSONOriginal types.String
|
|
req.Plan.GetAttribute(ctx, path.Root("destination_config"), &destConfigJSONOriginal)
|
|
resp.State.SetAttribute(ctx, path.Root("destination_config"), destConfigJSONOriginal.ValueString())
|
|
|
|
plan.SyncOptions = types.ObjectNull(nil)
|
|
plan.DestinationConfig = types.ObjectNull(nil)
|
|
|
|
// set other fields (again no pretty way to do this)
|
|
resp.State.SetAttribute(ctx, path.Root("id"), plan.ID)
|
|
resp.State.SetAttribute(ctx, path.Root("name"), plan.Name)
|
|
resp.State.SetAttribute(ctx, path.Root("description"), plan.Description)
|
|
resp.State.SetAttribute(ctx, path.Root("project_id"), plan.ProjectID)
|
|
resp.State.SetAttribute(ctx, path.Root("connection_id"), plan.ConnectionID)
|
|
resp.State.SetAttribute(ctx, path.Root("environment"), plan.Environment)
|
|
resp.State.SetAttribute(ctx, path.Root("secret_path"), plan.SecretPath)
|
|
resp.State.SetAttribute(ctx, path.Root("auto_sync_enabled"), plan.AutoSyncEnabled)
|
|
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
var stateID types.String
|
|
diags := req.State.GetAttribute(ctx, path.Root("id"), &stateID)
|
|
resp.Diagnostics.Append(diags...)
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
|
|
// Crossplane will attempt to call the Read() function even before the resource is created.
|
|
// When that happens, the state ID will be empty, which means it will try to call "api/v1/secret-syncs/${APP}/${stateId}" <- This will fail.
|
|
// It will fail because ${stateId} is empty. This makes it call the LIST secret syncs endpoint, which requires a project ID.
|
|
// Finally, crossplane will fail with a zod validation error, because it's hitting the wrong endpoint!
|
|
if stateID.ValueString() == "" {
|
|
resp.State.RemoveResource(ctx)
|
|
return
|
|
}
|
|
|
|
secretSync, err := r.client.GetSecretSyncById(kmsclient.GetSecretSyncByIdRequest{
|
|
App: r.App,
|
|
ID: stateID.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
|
|
}
|
|
}
|
|
|
|
// get current state values to preserve structure
|
|
var currentSyncOpts types.String
|
|
var currentDestConfig types.String
|
|
req.State.GetAttribute(ctx, path.Root("sync_options"), ¤tSyncOpts)
|
|
req.State.GetAttribute(ctx, path.Root("destination_config"), ¤tDestConfig)
|
|
|
|
var currentSyncOptsMap map[string]interface{}
|
|
if err := json.Unmarshal([]byte(currentSyncOpts.ValueString()), ¤tSyncOptsMap); err != nil {
|
|
resp.Diagnostics.AddError("Invalid JSON", fmt.Sprintf("Failed to parse sync_options: %s", err.Error()))
|
|
return
|
|
}
|
|
|
|
var currentDestConfigMap map[string]interface{}
|
|
if err := json.Unmarshal([]byte(currentDestConfig.ValueString()), ¤tDestConfigMap); err != nil {
|
|
resp.Diagnostics.AddError("Invalid JSON", fmt.Sprintf("Failed to parse destination_config: %s", err.Error()))
|
|
return
|
|
}
|
|
|
|
syncOptsObj, d := r.ReadSyncOptionsFromApi(ctx, secretSync)
|
|
resp.Diagnostics.Append(d...)
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
|
|
destConfigObj, d := r.ReadDestinationConfigFromApi(ctx, secretSync)
|
|
resp.Diagnostics.Append(d...)
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
|
|
// update only the keys that exist in current state
|
|
for k := range currentSyncOptsMap {
|
|
if attr, ok := syncOptsObj.Attributes()[k]; ok {
|
|
currentSyncOptsMap[k] = extractAttrValue(attr)
|
|
}
|
|
}
|
|
|
|
for k := range currentDestConfigMap {
|
|
if attr, ok := destConfigObj.Attributes()[k]; ok {
|
|
currentDestConfigMap[k] = extractAttrValue(attr)
|
|
}
|
|
}
|
|
|
|
syncOptsJSON, err := json.Marshal(currentSyncOptsMap)
|
|
if err != nil {
|
|
resp.Diagnostics.AddError("Error marshalling sync options", "Couldn't marshal sync options, unexpected error: "+err.Error())
|
|
return
|
|
}
|
|
destConfigJSON, err := json.Marshal(currentDestConfigMap)
|
|
if err != nil {
|
|
resp.Diagnostics.AddError("Error marshalling destination config", "Couldn't marshal destination config, unexpected error: "+err.Error())
|
|
return
|
|
}
|
|
|
|
finalSyncOpts := string(syncOptsJSON)
|
|
if areJSONEquivalent(currentSyncOpts.ValueString(), finalSyncOpts) {
|
|
finalSyncOpts = currentSyncOpts.ValueString() // Keep original formatting
|
|
}
|
|
|
|
finalDestConfig := string(destConfigJSON)
|
|
if areJSONEquivalent(currentDestConfig.ValueString(), finalDestConfig) {
|
|
finalDestConfig = currentDestConfig.ValueString() // Keep original formatting
|
|
}
|
|
|
|
// Set all attributes
|
|
resp.State.SetAttribute(ctx, path.Root("id"), types.StringValue(secretSync.ID))
|
|
resp.State.SetAttribute(ctx, path.Root("connection_id"), types.StringValue(secretSync.Connection.ConnectionID))
|
|
resp.State.SetAttribute(ctx, path.Root("name"), types.StringValue(secretSync.Name))
|
|
resp.State.SetAttribute(ctx, path.Root("project_id"), types.StringValue(secretSync.ProjectID))
|
|
resp.State.SetAttribute(ctx, path.Root("environment"), types.StringValue(secretSync.Environment.Slug))
|
|
resp.State.SetAttribute(ctx, path.Root("secret_path"), types.StringValue(secretSync.SecretFolder.Path))
|
|
resp.State.SetAttribute(ctx, path.Root("auto_sync_enabled"), types.BoolValue(secretSync.AutoSyncEnabled))
|
|
resp.State.SetAttribute(ctx, path.Root("description"), types.StringValue(secretSync.Description))
|
|
resp.State.SetAttribute(ctx, path.Root("sync_options"), finalSyncOpts)
|
|
resp.State.SetAttribute(ctx, path.Root("destination_config"), finalDestConfig)
|
|
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
var secretSyncPlan map[string]interface{}
|
|
var destinationConfigMap map[string]interface{}
|
|
var stateID types.String
|
|
|
|
diags := req.State.GetAttribute(ctx, path.Root("id"), &stateID)
|
|
resp.Diagnostics.Append(diags...)
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
|
|
// parse sync_options and transform keys (snake to camel for API calls)
|
|
var syncOptsJSON types.String
|
|
diags = req.Plan.GetAttribute(ctx, path.Root("sync_options"), &syncOptsJSON)
|
|
resp.Diagnostics.Append(diags...)
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
if err := json.Unmarshal([]byte(syncOptsJSON.ValueString()), &secretSyncPlan); err != nil {
|
|
resp.Diagnostics.AddError("Invalid JSON", fmt.Sprintf("Failed to parse sync_options: %s", err.Error()))
|
|
return
|
|
}
|
|
secretSyncPlan = transformMapKeys(secretSyncPlan)
|
|
|
|
var destConfigJSON types.String
|
|
diags = req.Plan.GetAttribute(ctx, path.Root("destination_config"), &destConfigJSON)
|
|
resp.Diagnostics.Append(diags...)
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
|
|
var tempDestConfigMap map[string]interface{}
|
|
if err := json.Unmarshal([]byte(destConfigJSON.ValueString()), &tempDestConfigMap); err != nil {
|
|
resp.Diagnostics.AddError("Invalid JSON", fmt.Sprintf("Failed to parse destination_config: %s", err.Error()))
|
|
return
|
|
}
|
|
// we gotta keep the snake_case keys for any potential validation like in the github secret sync
|
|
destConfigObj, d := mapToTypesObject(tempDestConfigMap, r.DestinationConfigAttributes)
|
|
resp.Diagnostics.Append(d...)
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
|
|
var stateDestConfigJSON types.String
|
|
diags = req.State.GetAttribute(ctx, path.Root("destination_config"), &stateDestConfigJSON)
|
|
resp.Diagnostics.Append(diags...)
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
|
|
var tempStateDestConfigMap map[string]interface{}
|
|
if err := json.Unmarshal([]byte(stateDestConfigJSON.ValueString()), &tempStateDestConfigMap); err != nil {
|
|
resp.Diagnostics.AddError("Invalid JSON", fmt.Sprintf("Failed to parse state destination_config: %s", err.Error()))
|
|
return
|
|
}
|
|
|
|
stateDestConfigObj, d := mapToTypesObject(tempStateDestConfigMap, r.DestinationConfigAttributes)
|
|
resp.Diagnostics.Append(d...)
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
|
|
tempPlan := SecretSyncBaseResourceModel{
|
|
DestinationConfig: destConfigObj,
|
|
SyncOptions: types.ObjectNull(nil),
|
|
}
|
|
|
|
tempState := SecretSyncBaseResourceModel{
|
|
DestinationConfig: stateDestConfigObj,
|
|
SyncOptions: types.ObjectNull(nil),
|
|
}
|
|
|
|
destinationConfigMap, diags = r.ReadDestinationConfigForUpdateFromPlan(ctx, tempPlan, tempState)
|
|
resp.Diagnostics.Append(diags...)
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
|
|
// Validation
|
|
initialSyncBehavior, ok := secretSyncPlan["initialSyncBehavior"].(string)
|
|
if !ok || 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
|
|
}
|
|
|
|
// Get other plan fields
|
|
var planName, planDescription, planProjectID, planConnectionID, planEnvironment, planSecretPath types.String
|
|
var planAutoSyncEnabled types.Bool
|
|
|
|
req.Plan.GetAttribute(ctx, path.Root("name"), &planName)
|
|
req.Plan.GetAttribute(ctx, path.Root("description"), &planDescription)
|
|
req.Plan.GetAttribute(ctx, path.Root("project_id"), &planProjectID)
|
|
req.Plan.GetAttribute(ctx, path.Root("connection_id"), &planConnectionID)
|
|
req.Plan.GetAttribute(ctx, path.Root("environment"), &planEnvironment)
|
|
req.Plan.GetAttribute(ctx, path.Root("secret_path"), &planSecretPath)
|
|
req.Plan.GetAttribute(ctx, path.Root("auto_sync_enabled"), &planAutoSyncEnabled)
|
|
|
|
_, err := r.client.UpdateSecretSync(kmsclient.UpdateSecretSyncRequest{
|
|
App: r.App,
|
|
ID: stateID.ValueString(),
|
|
Name: planName.ValueString(),
|
|
Description: planDescription.ValueString(),
|
|
ProjectID: planProjectID.ValueString(),
|
|
ConnectionID: planConnectionID.ValueString(),
|
|
Environment: planEnvironment.ValueString(),
|
|
SecretPath: planSecretPath.ValueString(),
|
|
AutoSyncEnabled: planAutoSyncEnabled.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
|
|
}
|
|
|
|
// get original JSON strings from plan to preserve formatting
|
|
var syncOptsJSONOriginal types.String
|
|
var destConfigJSONOriginal types.String
|
|
req.Plan.GetAttribute(ctx, path.Root("sync_options"), &syncOptsJSONOriginal)
|
|
req.Plan.GetAttribute(ctx, path.Root("destination_config"), &destConfigJSONOriginal)
|
|
|
|
resp.State.SetAttribute(ctx, path.Root("id"), stateID)
|
|
resp.State.SetAttribute(ctx, path.Root("name"), planName)
|
|
resp.State.SetAttribute(ctx, path.Root("description"), planDescription)
|
|
resp.State.SetAttribute(ctx, path.Root("project_id"), planProjectID)
|
|
resp.State.SetAttribute(ctx, path.Root("connection_id"), planConnectionID)
|
|
resp.State.SetAttribute(ctx, path.Root("environment"), planEnvironment)
|
|
resp.State.SetAttribute(ctx, path.Root("secret_path"), planSecretPath)
|
|
resp.State.SetAttribute(ctx, path.Root("auto_sync_enabled"), planAutoSyncEnabled)
|
|
resp.State.SetAttribute(ctx, path.Root("sync_options"), syncOptsJSON.ValueString())
|
|
resp.State.SetAttribute(ctx, path.Root("destination_config"), destConfigJSON.ValueString())
|
|
}
|
|
|
|
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 stateID types.String
|
|
diags := req.State.GetAttribute(ctx, path.Root("id"), &stateID)
|
|
resp.Diagnostics.Append(diags...)
|
|
if resp.Diagnostics.HasError() {
|
|
return
|
|
}
|
|
|
|
_, err := r.client.DeleteSecretSync(kmsclient.DeleteSecretSyncRequest{
|
|
App: r.App,
|
|
ID: stateID.ValueString(),
|
|
})
|
|
|
|
if err != nil {
|
|
resp.Diagnostics.AddError(
|
|
"Error deleting secret sync",
|
|
"Couldn't delete secret sync from Kms, unexpected error: "+err.Error(),
|
|
)
|
|
}
|
|
}
|
|
|
|
func snakeToCamel(s string) string {
|
|
parts := strings.Split(s, "_")
|
|
for i := 1; i < len(parts); i++ {
|
|
if len(parts[i]) > 0 {
|
|
parts[i] = strings.ToUpper(parts[i][:1]) + parts[i][1:]
|
|
}
|
|
}
|
|
return strings.Join(parts, "")
|
|
}
|
|
|
|
func transformMapKeys(m map[string]interface{}) map[string]interface{} {
|
|
result := make(map[string]interface{})
|
|
for k, v := range m {
|
|
camelKey := snakeToCamel(k)
|
|
|
|
// incase of nested maps we recursively transform the keys
|
|
if nestedMap, ok := v.(map[string]interface{}); ok {
|
|
result[camelKey] = transformMapKeys(nestedMap)
|
|
} else {
|
|
result[camelKey] = v
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func extractAttrValue(v attr.Value) interface{} {
|
|
if v.IsNull() || v.IsUnknown() {
|
|
return nil
|
|
}
|
|
|
|
switch val := v.(type) {
|
|
case types.String:
|
|
return val.ValueString()
|
|
case types.Bool:
|
|
return val.ValueBool()
|
|
case types.Int64:
|
|
return val.ValueInt64()
|
|
case types.Float64:
|
|
return val.ValueFloat64()
|
|
case types.List:
|
|
result := make([]interface{}, 0)
|
|
for _, elem := range val.Elements() {
|
|
result = append(result, extractAttrValue(elem))
|
|
}
|
|
return result
|
|
case types.Object:
|
|
result := make(map[string]interface{})
|
|
for k, elem := range val.Attributes() {
|
|
result[k] = extractAttrValue(elem)
|
|
}
|
|
return result
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func areJSONEquivalent(json1, json2 string) bool {
|
|
var obj1, obj2 interface{}
|
|
|
|
if err := json.Unmarshal([]byte(json1), &obj1); err != nil {
|
|
return false
|
|
}
|
|
if err := json.Unmarshal([]byte(json2), &obj2); err != nil {
|
|
return false
|
|
}
|
|
|
|
return reflect.DeepEqual(obj1, obj2)
|
|
}
|
|
|
|
// helper for converting map[string] to types.Object.
|
|
func mapToTypesObject(m map[string]interface{}, attrTypes map[string]schema.Attribute) (types.Object, diag.Diagnostics) {
|
|
var diags diag.Diagnostics
|
|
|
|
attrTypeMap := make(map[string]attr.Type)
|
|
attrValues := make(map[string]attr.Value)
|
|
|
|
for key, schemaAttr := range attrTypes {
|
|
// Determine the type from schema
|
|
switch a := schemaAttr.(type) {
|
|
case schema.StringAttribute:
|
|
attrTypeMap[key] = types.StringType
|
|
if val, ok := m[key].(string); ok {
|
|
attrValues[key] = types.StringValue(val)
|
|
} else {
|
|
attrValues[key] = types.StringNull()
|
|
}
|
|
case schema.BoolAttribute:
|
|
attrTypeMap[key] = types.BoolType
|
|
if val, ok := m[key].(bool); ok {
|
|
attrValues[key] = types.BoolValue(val)
|
|
} else {
|
|
attrValues[key] = types.BoolNull()
|
|
}
|
|
case schema.Int64Attribute:
|
|
attrTypeMap[key] = types.Int64Type
|
|
if val, ok := m[key].(float64); ok {
|
|
attrValues[key] = types.Int64Value(int64(val))
|
|
} else {
|
|
attrValues[key] = types.Int64Null()
|
|
}
|
|
case schema.ListAttribute:
|
|
attrTypeMap[key] = types.ListType{ElemType: a.ElementType}
|
|
if val, ok := m[key].([]interface{}); ok {
|
|
elemValues := make([]attr.Value, len(val))
|
|
for i, v := range val {
|
|
if a.ElementType == types.Int64Type {
|
|
if fVal, ok := v.(float64); ok {
|
|
elemValues[i] = types.Int64Value(int64(fVal))
|
|
}
|
|
}
|
|
}
|
|
listVal, d := types.ListValue(a.ElementType, elemValues)
|
|
diags.Append(d...)
|
|
attrValues[key] = listVal
|
|
} else {
|
|
attrValues[key] = types.ListNull(a.ElementType)
|
|
}
|
|
}
|
|
}
|
|
|
|
obj, d := types.ObjectValue(attrTypeMap, attrValues)
|
|
diags.Append(d...)
|
|
return obj, diags
|
|
}
|
|
|
|
func buildJSONSchemaDescription(baseDesc string, attrs map[string]schema.Attribute) string {
|
|
var fields []string
|
|
|
|
for name, attr := range attrs {
|
|
var fieldDesc string
|
|
var required string
|
|
|
|
switch a := attr.(type) {
|
|
case schema.StringAttribute:
|
|
if a.Required {
|
|
required = "required"
|
|
} else {
|
|
required = "optional"
|
|
}
|
|
fieldDesc = fmt.Sprintf(" - `%s` (%s): %s", name, required, a.Description)
|
|
|
|
case schema.BoolAttribute:
|
|
if a.Required {
|
|
required = "required"
|
|
} else {
|
|
required = "optional"
|
|
}
|
|
fieldDesc = fmt.Sprintf(" - `%s` (%s): %s", name, required, a.Description)
|
|
|
|
case schema.Int64Attribute:
|
|
if a.Required {
|
|
required = "required"
|
|
} else {
|
|
required = "optional"
|
|
}
|
|
fieldDesc = fmt.Sprintf(" - `%s` (%s): %s", name, required, a.Description)
|
|
|
|
case schema.ListAttribute:
|
|
if a.Required {
|
|
required = "required"
|
|
} else {
|
|
required = "optional"
|
|
}
|
|
fieldDesc = fmt.Sprintf(" - `%s` (%s): %s", name, required, a.Description)
|
|
}
|
|
|
|
if fieldDesc != "" {
|
|
fields = append(fields, fieldDesc)
|
|
}
|
|
}
|
|
|
|
// Sort alphabetically for consistency
|
|
sort.Strings(fields)
|
|
|
|
return fmt.Sprintf("%s Must be a JSON string with the following structure:\n\n%s",
|
|
baseDesc, strings.Join(fields, "\n"))
|
|
}
|