diff --git a/docs/resources/app_connection_gcp.md b/docs/resources/app_connection_gcp.md new file mode 100644 index 0000000..810c7f5 --- /dev/null +++ b/docs/resources/app_connection_gcp.md @@ -0,0 +1,68 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "infisical_app_connection_gcp Resource - terraform-provider-infisical" +subcategory: "" +description: |- + Create and manage GCP App Connection +--- + +# infisical_app_connection_gcp (Resource) + +Create and manage GCP App Connection + +## Example Usage + +```terraform +terraform { + required_providers { + infisical = { + # version = + source = "infisical/infisical" + } + } +} + +provider "infisical" { + host = "https://app.infisical.com" # Only required if using self hosted instance of Infisical, default is https://app.infisical.com + auth = { + universal = { + client_id = "" + client_secret = "" + } + } +} + +resource "infisical_app_connection_gcp" "app-connection-gcp" { + name = "gcp-app-connection" + method = "service-account-impersonation" + credentials = { + service_account_email = "service-account-df92581a-0fe9@my-duplicate-project.iam.gserviceaccount.com" + } + description = "I am a test app connection" +} +``` + + +## Schema + +### Required + +- `credentials` (Attributes) The credentials for the GCP App Connection (see [below for nested schema](#nestedatt--credentials)) +- `method` (String) The method used to authenticate with GCP. Possible values are: service-account-impersonation +- `name` (String) The name of the GCP App Connection to create. Must be slug-friendly + +### Optional + +- `description` (String) An optional description for the GCP App Connection. + +### Read-Only + +- `credentials_hash` (String) The hash of the GCP App Connection credentials +- `id` (String) The ID of the app connection + + +### Nested Schema for `credentials` + +Optional: + +- `service_account_email` (String, Sensitive) The service account email to connect with GCP. The service account ID (the part of the email before '@') must be suffixed with the first two sections of your organization ID e.g. service-account-df92581a-0fe9@my-project.iam.gserviceaccount.com. For more details, refer to the documentation here https://infisical.com/docs/integrations/app-connections/gcp#configure-service-account-for-infisical diff --git a/docs/resources/secret_sync_gcp_secret_manager.md b/docs/resources/secret_sync_gcp_secret_manager.md new file mode 100644 index 0000000..164339e --- /dev/null +++ b/docs/resources/secret_sync_gcp_secret_manager.md @@ -0,0 +1,100 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "infisical_secret_sync_gcp_secret_manager Resource - terraform-provider-infisical" +subcategory: "" +description: |- + Create and manage GCP Secret Manager secret syncs +--- + +# infisical_secret_sync_gcp_secret_manager (Resource) + +Create and manage GCP Secret Manager secret syncs + +## Example Usage + +```terraform +terraform { + required_providers { + infisical = { + # version = + source = "infisical/infisical" + } + } +} + +provider "infisical" { + host = "https://app.infisical.com" # Only required if using self hosted instance of Infisical, default is https://app.infisical.com + auth = { + universal = { + client_id = "" + client_secret = "" + } + } +} + +resource "infisical_app_connection_gcp" "app-connection-gcp" { + name = "gcp-app-connect" + method = "service-account-impersonation" + credentials = { + service_account_email = "service-account-df92581a-0fe9@my-duplicate-project.iam.gserviceaccount.com" + } + description = "I am a test app connection" +} + +resource "infisical_secret_sync_gcp_secret_manager" "secret_manager_test" { + name = "gcp-sync-tests" + description = "I am a test secret sync" + project_id = "f4517f4c-8b61-4727-8aef-5ae2807126fb" + environment = "prod" + secret_path = "/" + connection_id = infisical_app_connection_gcp.app-connection-gcp.id + + sync_options = { + initial_sync_behavior = "import-prioritize-destination" + } + destination_config = { + project_id = "my-duplicate-project" + } +} +``` + + +## Schema + +### Required + +- `connection_id` (String) The ID of the GCP Connection to use for syncing. +- `destination_config` (Attributes) The destination configuration for the secret sync. (see [below for nested schema](#nestedatt--destination_config)) +- `environment` (String) The slug of the project environment to sync secrets from. +- `name` (String) The name of the GCP Secret Manager sync to create. Must be slug-friendly. +- `project_id` (String) The ID of the Infisical project to create the sync in. +- `secret_path` (String) The folder path to sync secrets from. +- `sync_options` (Attributes) Parameters to modify how secrets are synced. (see [below for nested schema](#nestedatt--sync_options)) + +### Optional + +- `auto_sync_enabled` (Boolean) Whether secrets should be automatically synced when changes occur at the source location or not. +- `description` (String) An optional description for the GCP Secret Manager sync. + +### Read-Only + +- `id` (String) The ID of the GCP Secret Manager secret sync + + +### Nested Schema for `destination_config` + +Required: + +- `project_id` (String) The ID of the GCP project to sync with + +Optional: + +- `scope` (String) The scope of the sync with GCP Secret Manager. Supported options: global + + + +### Nested Schema for `sync_options` + +Required: + +- `initial_sync_behavior` (String) Specify how Infisical should resolve the initial sync to the GCP Secret Manager destination. Supported options: overwrite-destination, import-prioritize-source, import-prioritize-destination diff --git a/examples/resources/infisical_app_connection_gcp/resource.tf b/examples/resources/infisical_app_connection_gcp/resource.tf index 4bef533..00cd95b 100644 --- a/examples/resources/infisical_app_connection_gcp/resource.tf +++ b/examples/resources/infisical_app_connection_gcp/resource.tf @@ -18,8 +18,10 @@ provider "infisical" { } resource "infisical_app_connection_gcp" "app-connection-gcp" { - name = "gcp-app-connection" - method = "service-account-impersonation" - service_account_email = "service-account-df92581a-0fe9@my-duplicate-project.iam.gserviceaccount.com" - description = "I am a default description" + name = "gcp-app-connection" + method = "service-account-impersonation" + credentials = { + service_account_email = "service-account-df92581a-0fe9@my-duplicate-project.iam.gserviceaccount.com" + } + description = "I am a test app connection" } diff --git a/examples/resources/infisical_secret_sync_gcp_secret_manager/resource.tf b/examples/resources/infisical_secret_sync_gcp_secret_manager/resource.tf index 8965fa5..c37ed41 100644 --- a/examples/resources/infisical_secret_sync_gcp_secret_manager/resource.tf +++ b/examples/resources/infisical_secret_sync_gcp_secret_manager/resource.tf @@ -18,19 +18,26 @@ provider "infisical" { } resource "infisical_app_connection_gcp" "app-connection-gcp" { - name = "gcp-app-connect" - method = "service-account-impersonation" - service_account_email = "service-account-df92581a-0fe9@my-duplicate-project.iam.gserviceaccount.com" - description = "I am a test app connection" + name = "gcp-app-connect" + method = "service-account-impersonation" + credentials = { + service_account_email = "service-account-df92581a-0fe9@my-duplicate-project.iam.gserviceaccount.com" + } + description = "I am a test app connection" } -resource "infisical_secret_sync_gcp_secret_manager" "secret_manager" { - name = "gcp-sync" - environment = "dev" - connection_id = infisical_app_connection_gcp.app-connection-gcp.id - secret_path = "/" - gcp_project_id = "my-duplicate-project" - project_id = "f4517f4c-8b61-4727-8aef-5ae2807126fb" - initial_sync_behavior = "overwrite-destination" - description = "I am a test secret sync" +resource "infisical_secret_sync_gcp_secret_manager" "secret_manager_test" { + name = "gcp-sync-tests" + description = "I am a test secret sync" + project_id = "f4517f4c-8b61-4727-8aef-5ae2807126fb" + environment = "prod" + secret_path = "/" + connection_id = infisical_app_connection_gcp.app-connection-gcp.id + + sync_options = { + initial_sync_behavior = "import-prioritize-destination" + } + destination_config = { + project_id = "my-duplicate-project" + } } diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 0480179..6506e4e 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -7,6 +7,8 @@ import ( infisical "terraform-provider-infisical/internal/client" infisicalDatasource "terraform-provider-infisical/internal/provider/datasource" infisicalResource "terraform-provider-infisical/internal/provider/resource" + appConnectionResource "terraform-provider-infisical/internal/provider/resource/app_connection" + secretSyncResource "terraform-provider-infisical/internal/provider/resource/secret_sync" "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/ephemeral" @@ -260,6 +262,8 @@ func (p *infisicalProvider) Resources(_ context.Context) []func() resource.Resou infisicalResource.NewSecretApprovalPolicyResource, infisicalResource.NewAccessApprovalPolicyResource, infisicalResource.NewProjectSecretImportResource, + appConnectionResource.NewAppConnectionGcpResource, + secretSyncResource.NewSecretSyncGcpSecretManagerResource, } } diff --git a/internal/provider/resource/app_connection/app_connection_gcp.go b/internal/provider/resource/app_connection/app_connection_gcp.go new file mode 100644 index 0000000..7245ad6 --- /dev/null +++ b/internal/provider/resource/app_connection/app_connection_gcp.go @@ -0,0 +1,96 @@ +package resource + +import ( + "context" + infisical "terraform-provider-infisical/internal/client" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "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/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +// AppConnectionGcpCredentialsModel describes the data source data model. +type AppConnectionGcpCredentialsModel struct { + ServiceAccountEmail types.String `tfsdk:"service_account_email"` +} + +func NewAppConnectionGcpResource() resource.Resource { + return &AppConnectionBaseResource{ + App: infisical.AppConnectionAppGCP, + AppConnectionName: "GCP", + ResourceTypeName: "_app_connection_gcp", + AllowedMethods: []string{"service-account-impersonation"}, + CredentialsAttributes: map[string]schema.Attribute{ + "service_account_email": schema.StringAttribute{ + Optional: true, + Description: "The service account email to connect with GCP. The service account ID (the part of the email before '@') must be suffixed with the first two sections of your organization ID e.g. service-account-df92581a-0fe9@my-project.iam.gserviceaccount.com. For more details, refer to the documentation here https://infisical.com/docs/integrations/app-connections/gcp#configure-service-account-for-infisical", + Sensitive: true, + }, + }, + ReadCredentialsForCreateFromPlan: func(ctx context.Context, plan AppConnectionBaseResourceModel) (map[string]interface{}, diag.Diagnostics) { + credentialsConfig := make(map[string]interface{}) + + var credentials AppConnectionGcpCredentialsModel + diags := plan.Credentials.As(ctx, &credentials, basetypes.ObjectAsOptions{}) + if diags.HasError() { + return nil, diags + } + + if credentials.ServiceAccountEmail.IsNull() || credentials.ServiceAccountEmail.ValueString() == "" { + diags.AddError( + "Unable to create GCP app connection", + "Service account email field must be defined", + ) + return nil, diags + } + + credentialsConfig["serviceAccountEmail"] = credentials.ServiceAccountEmail.ValueString() + + return credentialsConfig, diags + }, + ReadCredentialsForUpdateFromPlan: func(ctx context.Context, plan AppConnectionBaseResourceModel, state AppConnectionBaseResourceModel) (map[string]interface{}, diag.Diagnostics) { + credentialsConfig := make(map[string]interface{}) + + var credentialsFromPlan AppConnectionGcpCredentialsModel + diags := plan.Credentials.As(ctx, &credentialsFromPlan, basetypes.ObjectAsOptions{}) + if diags.HasError() { + return nil, diags + } + + var credentialsFromState AppConnectionGcpCredentialsModel + diags = state.Credentials.As(ctx, &credentialsFromState, basetypes.ObjectAsOptions{}) + if diags.HasError() { + return nil, diags + } + + if credentialsFromPlan.ServiceAccountEmail.IsNull() || credentialsFromPlan.ServiceAccountEmail.ValueString() == "" { + diags.AddError( + "Unable to update GCP app connection", + "Service account email field must be defined", + ) + return nil, diags + } + + if credentialsFromState.ServiceAccountEmail.ValueString() != credentialsFromPlan.ServiceAccountEmail.ValueString() { + credentialsConfig["serviceAccountEmail"] = credentialsFromPlan.ServiceAccountEmail.ValueString() + } + + return credentialsConfig, diags + }, + OverwriteCredentialsFields: func(state *AppConnectionBaseResourceModel) diag.Diagnostics { + credentialsConfig := map[string]attr.Value{ + "service_account_email": types.StringNull(), + } + + var diags diag.Diagnostics + state.Credentials, diags = types.ObjectValue(map[string]attr.Type{ + "service_account_email": types.StringType, + }, credentialsConfig) + + return diags + }, + } +} diff --git a/internal/provider/resource/app_connection_gcp.go b/internal/provider/resource/app_connection/base_app_connection.go similarity index 52% rename from internal/provider/resource/app_connection_gcp.go rename to internal/provider/resource/app_connection/base_app_connection.go index 29b2f09..3a93081 100644 --- a/internal/provider/resource/app_connection_gcp.go +++ b/internal/provider/resource/app_connection/base_app_connection.go @@ -3,9 +3,11 @@ package resource import ( "context" "fmt" + "strings" infisical "terraform-provider-infisical/internal/client" infisicalclient "terraform-provider-infisical/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/planmodifier" @@ -13,40 +15,36 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" ) -// Ensure the implementation satisfies the expected interfaces. -var ( - _ resource.Resource = &AppConnectionGcpResource{} -) - -// NewAppConnectionGcpResource is a helper function to simplify the provider implementation. -func NewAppConnectionGcpResource() resource.Resource { - return &AppConnectionGcpResource{} -} - -// AppConnectionGcp is the resource implementation. -type AppConnectionGcpResource struct { - client *infisical.Client +// AppConnectionBaseResource is the resource implementation. +type AppConnectionBaseResource struct { + App infisicalclient.AppConnectionApp // used for identifying secret sync route + ResourceTypeName string // terraform resource name suffix + AppConnectionName string // complete descriptive name of the app connection + client *infisical.Client + AllowedMethods []string + CredentialsAttributes map[string]schema.Attribute + ReadCredentialsForCreateFromPlan func(ctx context.Context, plan AppConnectionBaseResourceModel) (map[string]interface{}, diag.Diagnostics) + ReadCredentialsForUpdateFromPlan func(ctx context.Context, plan AppConnectionBaseResourceModel, state AppConnectionBaseResourceModel) (map[string]interface{}, diag.Diagnostics) + OverwriteCredentialsFields func(state *AppConnectionBaseResourceModel) diag.Diagnostics } -// AppConnectionGcpResourceModel describes the data source data model. -type AppConnectionGcpResourceModel struct { - ID types.String `tfsdk:"id"` - Method types.String `tfsdk:"method"` - ServiceAccountEmail types.String `tfsdk:"service_account_email"` - Name types.String `tfsdk:"name"` - Description types.String `tfsdk:"description"` - CredentialsHash types.String `tfsdk:"credentials_hash"` +type AppConnectionBaseResourceModel struct { + ID types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + Method types.String `tfsdk:"method"` + Description types.String `tfsdk:"description"` + Credentials types.Object `tfsdk:"credentials"` + CredentialsHash types.String `tfsdk:"credentials_hash"` } // Metadata returns the resource type name. -func (r *AppConnectionGcpResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_app_connection_gcp" +func (r *AppConnectionBaseResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + r.ResourceTypeName } -// Schema defines the schema for the resource. -func (r *AppConnectionGcpResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { +func (r *AppConnectionBaseResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = schema.Schema{ - Description: "Create and manage GCP App Connection", + Description: fmt.Sprintf("Create and manage %s App Connection", r.AppConnectionName), Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ Description: "The ID of the app connection", @@ -55,31 +53,31 @@ func (r *AppConnectionGcpResource) Schema(_ context.Context, _ resource.SchemaRe }, "method": schema.StringAttribute{ Required: true, - Description: "The method used to authenticate with GCP. Possible values are: service-account-impersonation", - }, - "service_account_email": schema.StringAttribute{ - Optional: true, - Description: "The service account email to connect with GCP. The service account ID (the part of the email before '@') must be suffixed with the first two sections of your organization ID e.g. service-account-df92581a-0fe9@my-project.iam.gserviceaccount.com. For more details, refer to the documentation here https://infisical.com/docs/integrations/app-connections/gcp#configure-service-account-for-infisical", - Sensitive: true, + Description: fmt.Sprintf("The method used to authenticate with %s. Possible values are: %s", r.AppConnectionName, strings.Join(r.AllowedMethods, ", ")), }, "name": schema.StringAttribute{ Required: true, - Description: "The name of the GCP App Connection to create. Must be slug-friendly", + Description: fmt.Sprintf("The name of the %s App Connection to create. Must be slug-friendly", r.AppConnectionName), }, "description": schema.StringAttribute{ Optional: true, - Description: "An optional description for the GCP App Connection.", + Description: fmt.Sprintf("An optional description for the %s App Connection.", r.AppConnectionName), + }, + "credentials": schema.SingleNestedAttribute{ + Required: true, + Description: fmt.Sprintf("The credentials for the %s App Connection", r.AppConnectionName), + Attributes: r.CredentialsAttributes, }, "credentials_hash": schema.StringAttribute{ Computed: true, - Description: "The hash of the GCP App Connection credentials", + Description: fmt.Sprintf("The hash of the %s App Connection credentials", r.AppConnectionName), }, }, } } // Configure adds the provider configured client to the resource. -func (r *AppConnectionGcpResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { +func (r *AppConnectionBaseResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { if req.ProviderData == nil { return } @@ -99,44 +97,47 @@ func (r *AppConnectionGcpResource) Configure(_ context.Context, req resource.Con } // Create creates the resource and sets the initial Terraform state. -func (r *AppConnectionGcpResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { +func (r *AppConnectionBaseResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { if !r.client.Config.IsMachineIdentityAuth { resp.Diagnostics.AddError( - "Unable to create GCP app connection", + "Unable to create app connection", "Only Machine Identity authentication is supported for this operation", ) return } // Retrieve values from plan - var plan AppConnectionGcpResourceModel + var plan AppConnectionBaseResourceModel diags := req.Plan.Get(ctx, &plan) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } - if plan.Method.ValueString() != string(infisicalclient.AppConnectionGcpMethodServiceAccountImpersonation) { - resp.Diagnostics.AddError( - "Unable to create GCP app connection", - "Invalid value for method field. Possible values are: service-account-impersonation", - ) - return + methodIsValid := false + for _, method := range r.AllowedMethods { + if plan.Method.ValueString() == method { + methodIsValid = true + break + } } - if plan.ServiceAccountEmail.IsNull() || plan.ServiceAccountEmail.ValueString() == "" { + if !methodIsValid { resp.Diagnostics.AddError( - "Unable to create GCP app connection", - "Service account email field must be defined", + "Unable to create app connection", + fmt.Sprintf("Invalid value for method field. Allowed values are: %s", strings.Join(r.AllowedMethods, ", ")), ) return } - credentialsMap := map[string]interface{}{} - credentialsMap["serviceAccountEmail"] = plan.ServiceAccountEmail.ValueString() + credentialsMap, diags := r.ReadCredentialsForCreateFromPlan(ctx, plan) + if diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } appConnection, err := r.client.CreateAppConnection(infisicalclient.CreateAppConnectionRequest{ - App: infisicalclient.AppConnectionAppGCP, + App: r.App, Name: plan.Name.ValueString(), Description: plan.Description.ValueString(), Method: plan.Method.ValueString(), @@ -162,17 +163,17 @@ func (r *AppConnectionGcpResource) Create(ctx context.Context, req resource.Crea } // Read refreshes the Terraform state with the latest data. -func (r *AppConnectionGcpResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { +func (r *AppConnectionBaseResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { if !r.client.Config.IsMachineIdentityAuth { resp.Diagnostics.AddError( - "Unable to read GCP app connection", + "Unable to read app connection", "Only Machine Identity authentication is supported for this operation", ) return } // Get current state - var state AppConnectionGcpResourceModel + var state AppConnectionBaseResourceModel diags := req.State.Get(ctx, &state) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -180,7 +181,7 @@ func (r *AppConnectionGcpResource) Read(ctx context.Context, req resource.ReadRe } appConnection, err := r.client.GetAppConnectionById(infisicalclient.GetAppConnectionByIdRequest{ - App: infisicalclient.AppConnectionAppGCP, + App: r.App, ID: state.ID.ValueString(), }) @@ -190,7 +191,7 @@ func (r *AppConnectionGcpResource) Read(ctx context.Context, req resource.ReadRe return } else { resp.Diagnostics.AddError( - "Error reading GCP app connection", + "Error reading app connection", "Couldn't read app connection, unexpected error: "+err.Error(), ) return @@ -200,10 +201,16 @@ func (r *AppConnectionGcpResource) Read(ctx context.Context, req resource.ReadRe if state.CredentialsHash.ValueString() != appConnection.CredentialsHash { resp.Diagnostics.AddWarning( "App connection credentials conflict", - fmt.Sprintf("The credentials for the GCP app connection with ID %s have been updated outside of Terraform.", state.ID.ValueString()), + fmt.Sprintf("The credentials for the %s App Connection with ID %s have been updated outside of Terraform.", r.AppConnectionName, state.ID.ValueString()), ) - state.ServiceAccountEmail = types.StringNull() + // force TF update + diags = r.OverwriteCredentialsFields(&state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + diags = resp.State.Set(ctx, state) resp.Diagnostics.Append(diags...) return @@ -220,56 +227,55 @@ func (r *AppConnectionGcpResource) Read(ctx context.Context, req resource.ReadRe resp.Diagnostics.Append(diags...) } -// Update updates the resource and sets the updated Terraform state on success. -func (r *AppConnectionGcpResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { - +func (r *AppConnectionBaseResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { if !r.client.Config.IsMachineIdentityAuth { resp.Diagnostics.AddError( - "Unable to update GCP app connection", + "Unable to update app connection", "Only Machine Identity authentication is supported for this operation", ) return } // Retrieve values from plan - var plan AppConnectionGcpResourceModel + var plan AppConnectionBaseResourceModel diags := req.Plan.Get(ctx, &plan) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } - var state AppConnectionGcpResourceModel + var state AppConnectionBaseResourceModel diags = req.State.Get(ctx, &state) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } - if plan.Method.ValueString() != string(infisicalclient.AppConnectionGcpMethodServiceAccountImpersonation) { - resp.Diagnostics.AddError( - "Unable to update GCP app connection", - "Invalid value for method field. Possible values are: service-account-impersonation", - ) - return + methodIsValid := false + for _, method := range r.AllowedMethods { + if plan.Method.ValueString() == method { + methodIsValid = true + break + } } - if plan.ServiceAccountEmail.IsNull() || plan.ServiceAccountEmail.ValueString() == "" { + if !methodIsValid { resp.Diagnostics.AddError( - "Unable to update GCP app connection", - "Service account email field must be defined", + "Unable to create app connection", + fmt.Sprintf("Invalid value for method field. Allowed values are: %s", strings.Join(r.AllowedMethods, ", ")), ) return } - credentialsMap := map[string]interface{}{} - if state.ServiceAccountEmail.ValueString() != plan.ServiceAccountEmail.ValueString() { - credentialsMap["serviceAccountEmail"] = plan.ServiceAccountEmail.ValueString() + credentialsMap, diags := r.ReadCredentialsForUpdateFromPlan(ctx, plan, state) + if diags.HasError() { + resp.Diagnostics.Append(diags...) + return } appConnection, err := r.client.UpdateAppConnection(infisicalclient.UpdateAppConnectionRequest{ ID: state.ID.ValueString(), - App: infisicalclient.AppConnectionAppGCP, + App: r.App, Name: plan.Name.ValueString(), Description: plan.Description.ValueString(), Method: plan.Method.ValueString(), @@ -278,7 +284,7 @@ func (r *AppConnectionGcpResource) Update(ctx context.Context, req resource.Upda if err != nil { resp.Diagnostics.AddError( - "Error updating GCP app connection", + "Error updating app connection", "Couldn't update app connection, unexpected error: "+err.Error(), ) return @@ -293,17 +299,16 @@ func (r *AppConnectionGcpResource) Update(ctx context.Context, req resource.Upda } } -// Delete deletes the resource and removes the Terraform state on success. -func (r *AppConnectionGcpResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { +func (r *AppConnectionBaseResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { if !r.client.Config.IsMachineIdentityAuth { resp.Diagnostics.AddError( - "Unable to delete GCP app connection", + "Unable to delete app connection", "Only Machine Identity authentication is supported for this operation", ) return } - var state AppConnectionGcpResourceModel + var state AppConnectionBaseResourceModel diags := req.State.Get(ctx, &state) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -311,13 +316,13 @@ func (r *AppConnectionGcpResource) Delete(ctx context.Context, req resource.Dele } _, err := r.client.DeleteAppConnection(infisical.DeleteAppConnectionRequest{ - App: infisical.AppConnectionAppGCP, + App: r.App, ID: state.ID.ValueString(), }) if err != nil { resp.Diagnostics.AddError( - "Error deleting GCP app connection", + "Error deleting app connection", "Couldn't delete app connection from Infisical, unexpected error: "+err.Error(), ) } diff --git a/internal/provider/resource/secret_sync_gcp_secret_manager.go b/internal/provider/resource/secret_sync/base_secret_sync.go similarity index 56% rename from internal/provider/resource/secret_sync_gcp_secret_manager.go rename to internal/provider/resource/secret_sync/base_secret_sync.go index a826939..74a616c 100644 --- a/internal/provider/resource/secret_sync_gcp_secret_manager.go +++ b/internal/provider/resource/secret_sync/base_secret_sync.go @@ -6,69 +6,66 @@ import ( infisical "terraform-provider-infisical/internal/client" infisicalclient "terraform-provider-infisical/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/stringdefault" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" "github.com/hashicorp/terraform-plugin-framework/types" ) -const gcpSecretManagerScopeGlobal = "global" - -// Ensure the implementation satisfies the expected interfaces. -var ( - _ resource.Resource = &SecretSyncGcpSecretManagerResource{} -) - -// NewSecretSyncGcpSecretManagerResource is a helper function to simplify the provider implementation. -func NewSecretSyncGcpSecretManagerResource() resource.Resource { - return &SecretSyncGcpSecretManagerResource{} +// SecretSyncBaseResource is the resource implementation. +type SecretSyncBaseResource struct { + App infisicalclient.SecretSyncApp // used for identifying secret sync route + ResourceTypeName string // terraform resource name suffix + SyncName string // complete descriptive name of the secret sync + AppConnection infisicalclient.AppConnectionApp + client *infisical.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 infisicalclient.SecretSync) (types.Object, diag.Diagnostics) } -// SecretSyncGcpSecretManagerResource is the resource implementation. -type SecretSyncGcpSecretManagerResource struct { - client *infisical.Client +type SyncOptionsModel struct { + InitialSyncBehavior types.String `tfsdk:"initial_sync_behavior"` } -// SecretSyncGcpSecretManagerResourceModel describes the data source data model. -type SecretSyncGcpSecretManagerResourceModel 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"` - InitialSyncBehavior types.String `tfsdk:"initial_sync_behavior"` - AutoSyncEnabled types.Bool `tfsdk:"auto_sync_enabled"` - GcpProjectID types.String `tfsdk:"gcp_project_id"` - Scope types.String `tfsdk:"scope"` +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 *SyncOptionsModel `tfsdk:"sync_options"` + AutoSyncEnabled types.Bool `tfsdk:"auto_sync_enabled"` + DestinationConfig types.Object `tfsdk:"destination_config"` } // Metadata returns the resource type name. -func (r *SecretSyncGcpSecretManagerResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_secret_sync_gcp_secret_manager" +func (r *SecretSyncBaseResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + r.ResourceTypeName } -// Schema defines the schema for the resource. -func (r *SecretSyncGcpSecretManagerResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { +func (r *SecretSyncBaseResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = schema.Schema{ - Description: "Create and manage GCP Secret Manager secret syncs", + Description: fmt.Sprintf("Create and manage %s secret syncs", r.SyncName), Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ - Description: "The ID of the GCP Secret Manager secret sync", + 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: "The ID of the GCP Connection to use for syncing.", + Description: fmt.Sprintf("The ID of the %s Connection to use for syncing.", r.AppConnection), }, "name": schema.StringAttribute{ Required: true, - Description: "The name of the GCP Secret Manager sync to create. Must be slug-friendly.", + Description: fmt.Sprintf("The name of the %s sync to create. Must be slug-friendly.", r.SyncName), }, "project_id": schema.StringAttribute{ Required: true, @@ -83,23 +80,9 @@ func (r *SecretSyncGcpSecretManagerResource) Schema(_ context.Context, _ resourc Required: true, Description: "The folder path to sync secrets from.", }, - "gcp_project_id": schema.StringAttribute{ - Required: true, - Description: "The ID of the GCP project to sync with", - }, - "scope": schema.StringAttribute{ - Optional: true, - Description: "The scope of the sync with GCP Secret Manager. Supported options: global", - Default: stringdefault.StaticString("global"), - Computed: true, - }, "description": schema.StringAttribute{ Optional: true, - Description: "An optional description for the GCP Secret Manager sync.", - }, - "initial_sync_behavior": schema.StringAttribute{ - Required: true, - Description: "Specify how Infisical should resolve the initial sync to the GCP Secret Manager destination. Supported options: overwrite-destination, import-prioritize-source, import-prioritize-destination", + Description: fmt.Sprintf("An optional description for the %s sync.", r.SyncName), }, "auto_sync_enabled": schema.BoolAttribute{ Optional: true, @@ -107,12 +90,27 @@ func (r *SecretSyncGcpSecretManagerResource) Schema(_ context.Context, _ resourc Default: booldefault.StaticBool(true), Computed: true, }, + "sync_options": schema.SingleNestedAttribute{ + Required: true, + Description: "Parameters to modify how secrets are synced.", + Attributes: map[string]schema.Attribute{ + "initial_sync_behavior": schema.StringAttribute{ + Required: true, + Description: fmt.Sprintf("Specify how Infisical should resolve the initial sync to the %s destination. Supported options: overwrite-destination, import-prioritize-source, import-prioritize-destination", r.SyncName), + }, + }, + }, + "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 *SecretSyncGcpSecretManagerResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { +func (r *SecretSyncBaseResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { if req.ProviderData == nil { return } @@ -132,37 +130,29 @@ func (r *SecretSyncGcpSecretManagerResource) Configure(_ context.Context, req re } // Create creates the resource and sets the initial Terraform state. -func (r *SecretSyncGcpSecretManagerResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { +func (r *SecretSyncBaseResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { if !r.client.Config.IsMachineIdentityAuth { resp.Diagnostics.AddError( - "Unable to create GCP Secret Manager secret sync", + "Unable to create secret sync", "Only Machine Identity authentication is supported for this operation", ) return } // Retrieve values from plan - var plan SecretSyncGcpSecretManagerResourceModel + var plan SecretSyncBaseResourceModel diags := req.Plan.Get(ctx, &plan) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } - if plan.Scope.ValueString() != gcpSecretManagerScopeGlobal { - resp.Diagnostics.AddError( - "Unable to create GCP secret manager secret sync", - "Invalid value for scope field. Possible values are: global", - ) - return - } - - switch infisical.SecretSyncBehavior(plan.InitialSyncBehavior.ValueString()) { + switch infisical.SecretSyncBehavior(plan.SyncOptions.InitialSyncBehavior.ValueString()) { case infisical.SecretSyncBehaviorOverwriteDestination, infisical.SecretSyncBehaviorPrioritizeDestination, infisical.SecretSyncBehaviorPrioritizeSource: break default: resp.Diagnostics.AddError( - "Unable to create GCP secret manager secret sync", + "Unable to create secret sync", fmt.Sprintf("Invalid value for initial_sync_behavior field. Possible values are: %s, %s, %s", infisical.SecretSyncBehaviorOverwriteDestination, infisical.SecretSyncBehaviorPrioritizeDestination, infisical.SecretSyncBehaviorPrioritizeSource), @@ -170,12 +160,14 @@ func (r *SecretSyncGcpSecretManagerResource) Create(ctx context.Context, req res return } - destinationConfigMap := map[string]interface{}{} - destinationConfigMap["scope"] = plan.Scope.ValueString() - destinationConfigMap["projectId"] = plan.GcpProjectID.ValueString() + destinationConfigMap, diags := r.ReadDestinationConfigForCreateFromPlan(ctx, plan) + if diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } secretSync, err := r.client.CreateSecretSync(infisicalclient.CreateSecretSyncRequest{ - App: infisicalclient.SecretSyncAppGCPSecretManager, + App: r.App, Name: plan.Name.ValueString(), Description: plan.Description.ValueString(), ProjectID: plan.ProjectID.ValueString(), @@ -184,14 +176,14 @@ func (r *SecretSyncGcpSecretManagerResource) Create(ctx context.Context, req res SecretPath: plan.SecretPath.ValueString(), AutoSyncEnabled: plan.AutoSyncEnabled.ValueBool(), SyncOptions: infisicalclient.SecretSyncOptions{ - InitialSyncBehavior: plan.InitialSyncBehavior.ValueString(), + InitialSyncBehavior: plan.SyncOptions.InitialSyncBehavior.ValueString(), }, DestinationConfig: destinationConfigMap, }) if err != nil { resp.Diagnostics.AddError( - "Error creating GCP Secret Manager secret sync", + "Error creating secret sync", "Couldn't create secret sync, unexpected error: "+err.Error(), ) return @@ -207,17 +199,17 @@ func (r *SecretSyncGcpSecretManagerResource) Create(ctx context.Context, req res } // Read refreshes the Terraform state with the latest data. -func (r *SecretSyncGcpSecretManagerResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { +func (r *SecretSyncBaseResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { if !r.client.Config.IsMachineIdentityAuth { resp.Diagnostics.AddError( - "Unable to read GCP Secret Manager secret sync", + "Unable to read secret sync", "Only Machine Identity authentication is supported for this operation", ) return } // Get current state - var state SecretSyncGcpSecretManagerResourceModel + var state SecretSyncBaseResourceModel diags := req.State.Get(ctx, &state) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -225,7 +217,7 @@ func (r *SecretSyncGcpSecretManagerResource) Read(ctx context.Context, req resou } secretSync, err := r.client.GetSecretSyncById(infisicalclient.GetSecretSyncByIdRequest{ - App: infisical.SecretSyncAppGCPSecretManager, + App: r.App, ID: state.ID.ValueString(), }) @@ -235,7 +227,7 @@ func (r *SecretSyncGcpSecretManagerResource) Read(ctx context.Context, req resou return } else { resp.Diagnostics.AddError( - "Error reading GCP Secret Manager secret sync", + "Error reading secret sync", "Couldn't read secret sync, unexpected error: "+err.Error(), ) return @@ -247,65 +239,56 @@ func (r *SecretSyncGcpSecretManagerResource) Read(ctx context.Context, req resou state.ProjectID = types.StringValue(secretSync.ProjectID) state.Environment = types.StringValue(secretSync.Environment.Slug) state.SecretPath = types.StringValue(secretSync.SecretFolder.Path) - state.InitialSyncBehavior = types.StringValue(secretSync.SyncOptions.InitialSyncBehavior) state.AutoSyncEnabled = types.BoolValue(secretSync.AutoSyncEnabled) if !(state.Description.IsNull() && secretSync.Description == "") { state.Description = types.StringValue(secretSync.Description) } - if value, ok := secretSync.DestinationConfig["projectId"].(string); ok { - state.GcpProjectID = types.StringValue(value) + if state.SyncOptions != nil { + state.SyncOptions.InitialSyncBehavior = types.StringValue(secretSync.SyncOptions.InitialSyncBehavior) } - if value, ok := secretSync.DestinationConfig["scope"].(string); ok { - state.Scope = types.StringValue(value) + 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...) } -// Update updates the resource and sets the updated Terraform state on success. -func (r *SecretSyncGcpSecretManagerResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { - +func (r *SecretSyncBaseResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { if !r.client.Config.IsMachineIdentityAuth { resp.Diagnostics.AddError( - "Unable to update GCP Secret Manager secret sync", + "Unable to update secret sync", "Only Machine Identity authentication is supported for this operation", ) return } // Retrieve values from plan - var plan SecretSyncGcpSecretManagerResourceModel + var plan SecretSyncBaseResourceModel diags := req.Plan.Get(ctx, &plan) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } - var state SecretSyncGcpSecretManagerResourceModel + var state SecretSyncBaseResourceModel diags = req.State.Get(ctx, &state) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { return } - if plan.Scope.ValueString() != gcpSecretManagerScopeGlobal { - resp.Diagnostics.AddError( - "Unable to update GCP secret manager secret sync", - "Invalid value for scope field. Possible values are: global", - ) - return - } - - switch infisical.SecretSyncBehavior(plan.InitialSyncBehavior.ValueString()) { + switch infisical.SecretSyncBehavior(plan.SyncOptions.InitialSyncBehavior.ValueString()) { case infisical.SecretSyncBehaviorOverwriteDestination, infisical.SecretSyncBehaviorPrioritizeDestination, infisical.SecretSyncBehaviorPrioritizeSource: break default: resp.Diagnostics.AddError( - "Unable to update GCP secret manager secret sync", + "Unable to update secret sync", fmt.Sprintf("Invalid value for initial_sync_behavior field. Possible values are: %s, %s, %s", infisical.SecretSyncBehaviorOverwriteDestination, infisical.SecretSyncBehaviorPrioritizeDestination, infisical.SecretSyncBehaviorPrioritizeSource), @@ -313,12 +296,14 @@ func (r *SecretSyncGcpSecretManagerResource) Update(ctx context.Context, req res return } - destinationConfigMap := map[string]interface{}{} - destinationConfigMap["scope"] = plan.Scope.ValueString() - destinationConfigMap["projectId"] = plan.GcpProjectID.ValueString() + destinationConfigMap, diags := r.ReadDestinationConfigForUpdateFromPlan(ctx, plan, state) + if diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } _, err := r.client.UpdateSecretSync(infisicalclient.UpdateSecretSyncRequest{ - App: infisicalclient.SecretSyncAppGCPSecretManager, + App: r.App, ID: state.ID.ValueString(), Name: plan.Name.ValueString(), Description: plan.Description.ValueString(), @@ -328,14 +313,14 @@ func (r *SecretSyncGcpSecretManagerResource) Update(ctx context.Context, req res SecretPath: plan.SecretPath.ValueString(), AutoSyncEnabled: plan.AutoSyncEnabled.ValueBool(), SyncOptions: infisicalclient.SecretSyncOptions{ - InitialSyncBehavior: plan.InitialSyncBehavior.ValueString(), + InitialSyncBehavior: plan.SyncOptions.InitialSyncBehavior.ValueString(), }, DestinationConfig: destinationConfigMap, }) if err != nil { resp.Diagnostics.AddError( - "Error updating GCP Secret Manager secret sync", + "Error updating secret sync", "Couldn't update secret sync, unexpected error: "+err.Error(), ) return @@ -348,17 +333,16 @@ func (r *SecretSyncGcpSecretManagerResource) Update(ctx context.Context, req res } } -// Delete deletes the resource and removes the Terraform state on success. -func (r *SecretSyncGcpSecretManagerResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { +func (r *SecretSyncBaseResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { if !r.client.Config.IsMachineIdentityAuth { resp.Diagnostics.AddError( - "Unable to delete GCP Secret Manager secret sync", + "Unable to delete secret sync", "Only Machine Identity authentication is supported for this operation", ) return } - var state SecretSyncGcpSecretManagerResourceModel + var state SecretSyncBaseResourceModel diags := req.State.Get(ctx, &state) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -366,13 +350,13 @@ func (r *SecretSyncGcpSecretManagerResource) Delete(ctx context.Context, req res } _, err := r.client.DeleteSecretSync(infisical.DeleteSecretSyncRequest{ - App: infisical.SecretSyncAppGCPSecretManager, + App: r.App, ID: state.ID.ValueString(), }) if err != nil { resp.Diagnostics.AddError( - "Error deleting GCP Secret Manager secret sync", + "Error deleting secret sync", "Couldn't delete secret sync from Infisical, unexpected error: "+err.Error(), ) } diff --git a/internal/provider/resource/secret_sync/secret_sync_gcp_secret_manager.go b/internal/provider/resource/secret_sync/secret_sync_gcp_secret_manager.go new file mode 100644 index 0000000..3bd52ad --- /dev/null +++ b/internal/provider/resource/secret_sync/secret_sync_gcp_secret_manager.go @@ -0,0 +1,119 @@ +package resource + +import ( + "context" + infisical "terraform-provider-infisical/internal/client" + infisicalclient "terraform-provider-infisical/internal/client" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "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/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" +) + +const gcpSecretManagerScopeGlobal = "global" + +// SecretSyncGcpResourceModel describes the data source data model. +type SecretSyncGcpSecretManagerDestinationConfigModel struct { + ProjectID types.String `tfsdk:"project_id"` + Scope types.String `tfsdk:"scope"` +} + +func NewSecretSyncGcpSecretManagerResource() resource.Resource { + return &SecretSyncBaseResource{ + App: infisical.SecretSyncAppGCPSecretManager, + SyncName: "GCP Secret Manager", + ResourceTypeName: "_secret_sync_gcp_secret_manager", + AppConnection: "GCP", + DestinationConfigAttributes: map[string]schema.Attribute{ + "project_id": schema.StringAttribute{ + Required: true, + Description: "The ID of the GCP project to sync with", + }, + "scope": schema.StringAttribute{ + Optional: true, + Description: "The scope of the sync with GCP Secret Manager. Supported options: global", + Default: stringdefault.StaticString("global"), + Computed: true, + }, + }, + ReadDestinationConfigForCreateFromPlan: func(ctx context.Context, plan SecretSyncBaseResourceModel) (map[string]interface{}, diag.Diagnostics) { + destinationConfig := make(map[string]interface{}) + + var gcpConfig SecretSyncGcpSecretManagerDestinationConfigModel + diags := plan.DestinationConfig.As(ctx, &gcpConfig, basetypes.ObjectAsOptions{}) + if diags.HasError() { + return nil, diags + } + + if gcpConfig.Scope.ValueString() != gcpSecretManagerScopeGlobal { + diags.AddError( + "Unable to create GCP secret manager secret sync", + "Invalid value for scope field. Possible values are: global", + ) + return nil, diags + } + + destinationConfig["scope"] = gcpConfig.Scope.ValueString() + destinationConfig["projectId"] = gcpConfig.ProjectID.ValueString() + + return destinationConfig, diags + }, + ReadDestinationConfigForUpdateFromPlan: func(ctx context.Context, plan SecretSyncBaseResourceModel, _ SecretSyncBaseResourceModel) (map[string]interface{}, diag.Diagnostics) { + destinationConfig := make(map[string]interface{}) + + var gcpConfig SecretSyncGcpSecretManagerDestinationConfigModel + diags := plan.DestinationConfig.As(ctx, &gcpConfig, basetypes.ObjectAsOptions{}) + if diags.HasError() { + return nil, diags + } + + if gcpConfig.Scope.ValueString() != gcpSecretManagerScopeGlobal { + diags.AddError( + "Unable to update GCP secret manager secret sync", + "Invalid value for scope field. Possible values are: global", + ) + return nil, diags + } + + destinationConfig["scope"] = gcpConfig.Scope.ValueString() + destinationConfig["projectId"] = gcpConfig.ProjectID.ValueString() + + return destinationConfig, diags + }, + ReadDestinationConfigFromApi: func(ctx context.Context, secretSync infisicalclient.SecretSync) (types.Object, diag.Diagnostics) { + var diags diag.Diagnostics + + scopeVal, ok := secretSync.DestinationConfig["scope"].(string) + if !ok { + diags.AddError( + "Invalid scope type", + "Expected 'scope' to be a string but got something else", + ) + return types.ObjectNull(map[string]attr.Type{}), diags + } + + projectIdVal, ok := secretSync.DestinationConfig["projectId"].(string) + if !ok { + diags.AddError( + "Invalid projectId type", + "Expected 'projectId' to be a string but got something else", + ) + return types.ObjectNull(map[string]attr.Type{}), diags + } + + destinationConfig := map[string]attr.Value{ + "scope": types.StringValue(scopeVal), + "project_id": types.StringValue(projectIdVal), + } + + return types.ObjectValue(map[string]attr.Type{ + "scope": types.StringType, + "project_id": types.StringType, + }, destinationConfig) + }, + } +}