diff --git a/.changelog/1196.txt b/.changelog/1196.txt
new file mode 100644
index 000000000..e6e1c3c47
--- /dev/null
+++ b/.changelog/1196.txt
@@ -0,0 +1,7 @@
+```release-note:feature
+Add support for sync resource in HCP Vault Secrets
+```
+
+```release-note:improvement
+Allow users to assign one or more syncs with an HCP Vault Secrets App
+```
diff --git a/docs/resources/vault_secrets_app.md b/docs/resources/vault_secrets_app.md
index 042356051..408e895d8 100644
--- a/docs/resources/vault_secrets_app.md
+++ b/docs/resources/vault_secrets_app.md
@@ -29,6 +29,7 @@ resource "hcp_vault_secrets_app" "example" {
- `description` (String) The Vault Secrets app description
- `project_id` (String) The ID of the HCP project where the HCP Vault Secrets app is located.
+- `sync_names` (Set of String) Set of sync names to associate with this app.
### Read-Only
diff --git a/docs/resources/vault_secrets_integration.md b/docs/resources/vault_secrets_integration.md
index 59fa15b0b..3288b03a9 100644
--- a/docs/resources/vault_secrets_integration.md
+++ b/docs/resources/vault_secrets_integration.md
@@ -200,7 +200,7 @@ Read-Only:
Required:
-- `token` (String, Sensitive) Access token used to authenticate against the target GitLab account.
+- `token` (String, Sensitive) Access token used to authenticate against the target GitLab account. This token must have privilege to create CI/CD variables.
diff --git a/docs/resources/vault_secrets_sync.md b/docs/resources/vault_secrets_sync.md
new file mode 100644
index 000000000..ea89f9b80
--- /dev/null
+++ b/docs/resources/vault_secrets_sync.md
@@ -0,0 +1,68 @@
+---
+page_title: "Resource hcp_vault_secrets_sync"
+subcategory: "HCP Vault Secrets"
+description: |-
+ The Vault Secrets sync resource manages an integration.
+---
+
+# hcp_vault_secrets_sync (Resource)
+
+The Vault Secrets sync resource manages an integration.
+
+## Example Usage
+
+```terraform
+resource "hcp_vault_secrets_integration" "example_gitlab_integration" {
+ name = "gitlab-integration"
+ capabilities = ["SYNC"]
+ provider_type = "gitlab"
+ gitlab_access = {
+ token = "myaccesstoken"
+ }
+}
+
+resource "hcp_vault_secrets_sync" "example_gitlab_project_sync" {
+ name = "gitlab-proj-sync"
+ integration_name = hcp_vault_secrets_integration.example_gitlab_integration.name
+ gitlab_config = {
+ scope = "PROJECT"
+ project_id = "123456"
+ }
+}
+```
+
+
+## Schema
+
+### Required
+
+- `integration_name` (String) The Vault Secrets integration name.
+- `name` (String) The Vault Secrets Sync name.
+
+### Optional
+
+- `gitlab_config` (Attributes) Configuration parameters used to determine the sync destination. (see [below for nested schema](#nestedatt--gitlab_config))
+- `project_id` (String) HCP project ID that owns the HCP Vault Secrets integration. Inferred from the provider configuration if omitted.
+
+### Read-Only
+
+- `organization_id` (String) HCP organization ID that owns the HCP Vault Secrets integration.
+- `resource_id` (String) Resource ID used to uniquely identify the sync on the HCP platform.
+
+
+### Nested Schema for `gitlab_config`
+
+Optional:
+
+- `group_id` (String) ID of the group, if the scope is GROUP
+- `project_id` (String) ID of the project, if the scope is PROJECT
+- `scope` (String) The scope to which sync applies. Defaults to GROUP. The valid options are GROUP and PROJECT
+
+## Import
+
+Import is supported using the following syntax:
+
+```shell
+# Vault Secrets Integration can be imported by specifying the name of the integration
+terraform import hcp_vault_secrets_sync.example_gitlab_project_sync gitlab-proj-sync
+```
diff --git a/examples/resources/hcp_vault_secrets_sync/import.sh b/examples/resources/hcp_vault_secrets_sync/import.sh
new file mode 100644
index 000000000..6da504f8d
--- /dev/null
+++ b/examples/resources/hcp_vault_secrets_sync/import.sh
@@ -0,0 +1,2 @@
+# Vault Secrets Integration can be imported by specifying the name of the integration
+terraform import hcp_vault_secrets_sync.example_gitlab_project_sync gitlab-proj-sync
diff --git a/examples/resources/hcp_vault_secrets_sync/resource.tf b/examples/resources/hcp_vault_secrets_sync/resource.tf
new file mode 100644
index 000000000..807372f0c
--- /dev/null
+++ b/examples/resources/hcp_vault_secrets_sync/resource.tf
@@ -0,0 +1,17 @@
+resource "hcp_vault_secrets_integration" "example_gitlab_integration" {
+ name = "gitlab-integration"
+ capabilities = ["SYNC"]
+ provider_type = "gitlab"
+ gitlab_access = {
+ token = "myaccesstoken"
+ }
+}
+
+resource "hcp_vault_secrets_sync" "example_gitlab_project_sync" {
+ name = "gitlab-proj-sync"
+ integration_name = hcp_vault_secrets_integration.example_gitlab_integration.name
+ gitlab_config = {
+ scope = "PROJECT"
+ project_id = "123456"
+ }
+}
diff --git a/internal/provider/provider.go b/internal/provider/provider.go
index 735195a50..2b3c871d5 100644
--- a/internal/provider/provider.go
+++ b/internal/provider/provider.go
@@ -157,6 +157,7 @@ func (p *ProviderFramework) Resources(ctx context.Context) []func() resource.Res
vaultsecrets.NewVaultSecretsIntegrationResource,
vaultsecrets.NewVaultSecretsDynamicSecretResource,
vaultsecrets.NewVaultSecretsRotatingSecretResource,
+ vaultsecrets.NewVaultSecretsSyncResource,
// Vault Secrets Deprecated
vaultsecrets.NewVaultSecretsIntegrationAWSResource,
vaultsecrets.NewVaultSecretsIntegrationAzureResource,
diff --git a/internal/provider/vaultsecrets/resource_vault_secrets_app.go b/internal/provider/vaultsecrets/resource_vault_secrets_app.go
index 794756997..83d29c375 100644
--- a/internal/provider/vaultsecrets/resource_vault_secrets_app.go
+++ b/internal/provider/vaultsecrets/resource_vault_secrets_app.go
@@ -10,7 +10,9 @@ import (
"github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/stable/2023-11-28/client/secret_service"
secretmodels "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/stable/2023-11-28/models"
+ "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
+ "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"
@@ -30,6 +32,9 @@ type App struct {
ProjectID types.String `tfsdk:"project_id"`
OrganizationID types.String `tfsdk:"organization_id"`
ResourceName types.String `tfsdk:"resource_name"`
+ SyncNames types.Set `tfsdk:"sync_names"`
+
+ syncNames []string `tfsdk:"-"`
}
var _ resource.Resource = &resourceVaultSecretsApp{}
@@ -90,7 +95,16 @@ func (r *resourceVaultSecretsApp) Schema(_ context.Context, _ resource.SchemaReq
Computed: true,
Description: "The app's resource name in the format secrets/project//app/.",
},
- },
+ "sync_names": schema.SetAttribute{
+ Description: "Set of sync names to associate with this app.",
+ Optional: true,
+ ElementType: types.StringType,
+ Validators: []validator.Set{
+ setvalidator.ValueStringsAre(
+ slugValidator,
+ ),
+ },
+ }},
}
}
@@ -124,6 +138,7 @@ func (r *resourceVaultSecretsApp) Create(ctx context.Context, req resource.Creat
Body: &secretmodels.SecretServiceCreateAppBody{
Name: app.AppName.ValueString(),
Description: app.Description.ValueString(),
+ SyncNames: app.syncNames,
},
OrganizationID: app.OrganizationID.ValueString(),
ProjectID: app.ProjectID.ValueString(),
@@ -170,6 +185,7 @@ func (r *resourceVaultSecretsApp) Update(ctx context.Context, req resource.Updat
response, err := r.client.VaultSecrets.UpdateApp(&secret_service.UpdateAppParams{
Body: &secretmodels.SecretServiceUpdateAppBody{
Description: app.Description.ValueString(),
+ SyncNames: app.syncNames,
},
Name: app.AppName.ValueString(),
OrganizationID: app.OrganizationID.ValueString(),
@@ -216,9 +232,11 @@ func (a *App) projectID() types.String {
return a.ProjectID
}
-func (a *App) initModel(_ context.Context, orgID, projID string) diag.Diagnostics {
+func (a *App) initModel(ctx context.Context, orgID, projID string) diag.Diagnostics {
a.OrganizationID = types.StringValue(orgID)
a.ProjectID = types.StringValue(projID)
+ a.syncNames = make([]string, 0, len(a.SyncNames.Elements()))
+ a.SyncNames.ElementsAs(ctx, &a.syncNames, false)
return diag.Diagnostics{}
}
@@ -237,5 +255,17 @@ func (a *App) fromModel(_ context.Context, orgID, projID string, model any) diag
a.ID = types.StringValue(appModel.ResourceID)
a.ResourceName = types.StringValue(appModel.ResourceName)
+ var syncs []attr.Value
+ for _, c := range appModel.SyncNames {
+ syncs = append(syncs, types.StringValue(c))
+ }
+
+ if len(syncs) > 0 {
+ a.SyncNames, diags = types.SetValue(types.StringType, syncs)
+ if diags.HasError() {
+ return diags
+ }
+ }
+
return diags
}
diff --git a/internal/provider/vaultsecrets/resource_vault_secrets_app_test.go b/internal/provider/vaultsecrets/resource_vault_secrets_app_test.go
index bc8de8ae7..e93df1222 100644
--- a/internal/provider/vaultsecrets/resource_vault_secrets_app_test.go
+++ b/internal/provider/vaultsecrets/resource_vault_secrets_app_test.go
@@ -17,11 +17,16 @@ import (
)
func TestAccVaultSecretsResourceApp(t *testing.T) {
- appName1 := generateRandomSlug()
- appName2 := generateRandomSlug()
-
- description1 := "my description 1"
- description2 := "my description 2"
+ var (
+ integrationName1 = generateRandomSlug()
+ appName1 = generateRandomSlug()
+ appName2 = generateRandomSlug()
+ description1 = "my description 1"
+ description2 = "my description 2"
+ projSyncName = generateRandomSlug()
+ groupSyncName = generateRandomSlug()
+ gitLabToken = checkRequiredEnvVarOrFail(t, "VAULTSECRETS_GITLAB_ACCESS_TOKEN")
+ )
resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories,
@@ -30,21 +35,58 @@ func TestAccVaultSecretsResourceApp(t *testing.T) {
{
Config: appConfig(appName1, description1),
Check: resource.ComposeTestCheckFunc(
- appCheckFunc(appName1, description1)...,
+ appCheckFunc(appName1, description1, nil)...,
),
},
// Changing an immutable field causes a recreation
{
Config: appConfig(appName2, description1),
Check: resource.ComposeTestCheckFunc(
- appCheckFunc(appName2, description1)...,
+ appCheckFunc(appName2, description1, nil)...,
),
},
// Changing mutable fields causes an update
{
Config: appConfig(appName2, description2),
Check: resource.ComposeTestCheckFunc(
- appCheckFunc(appName2, description2)...,
+ appCheckFunc(appName2, description2, nil)...,
+ ),
+ },
+ // Changing the sync_names causes an update
+ {
+ Config: fmt.Sprintf(`
+ resource "hcp_vault_secrets_integration" "acc_test" {
+ name = %q
+ capabilities = ["SYNC"]
+ provider_type = "gitlab"
+ gitlab_access = {
+ token = %q
+ }
+ }
+ resource "hcp_vault_secrets_sync" "gitlab_proj_sync" {
+ name = %q
+ integration_name = hcp_vault_secrets_integration.acc_test.name
+ gitlab_config = {
+ scope = "PROJECT"
+ project_id = "123456789"
+ }
+ }
+ resource "hcp_vault_secrets_sync" "gitlab_group_sync" {
+ name = %q
+ integration_name = hcp_vault_secrets_integration.acc_test.name
+ gitlab_config = {
+ scope = "GROUP"
+ group_id = "987654321"
+ }
+ }
+ resource "hcp_vault_secrets_app" "acc_test_app" {
+ app_name = %q
+ description = %q
+ sync_names = [hcp_vault_secrets_sync.gitlab_sync.name]
+ }
+ `, integrationName1, gitLabToken, projSyncName, groupSyncName, appName2, description2),
+ Check: resource.ComposeTestCheckFunc(
+ appCheckFunc(appName2, description2, []string{projSyncName, groupSyncName})...,
),
},
// Deleting the app out of band causes a recreation
@@ -63,7 +105,7 @@ func TestAccVaultSecretsResourceApp(t *testing.T) {
},
Config: appConfig(appName2, description2),
Check: resource.ComposeTestCheckFunc(
- appCheckFunc(appName2, description2)...,
+ appCheckFunc(appName2, description2, nil)...,
),
PlanOnly: true,
ExpectNonEmptyPlan: true,
@@ -87,7 +129,7 @@ func TestAccVaultSecretsResourceApp(t *testing.T) {
},
Config: appConfig(appName2, description2),
Check: resource.ComposeTestCheckFunc(
- appCheckFunc(appName2, description2)...,
+ appCheckFunc(appName2, description2, nil)...,
),
ResourceName: "hcp_vault_secrets_app.acc_test_app",
ImportStateId: appName2,
@@ -111,18 +153,26 @@ func appConfig(appName, description string) string {
resource "hcp_vault_secrets_app" "acc_test_app" {
app_name = %q
description = %q
+ sync_names = []
}`, appName, description)
}
-func appCheckFunc(appName, description string) []resource.TestCheckFunc {
- return []resource.TestCheckFunc{
+func appCheckFunc(appName, description string, syncNames []string) []resource.TestCheckFunc {
+ syncsTestFns := make([]resource.TestCheckFunc, 0, len(syncNames))
+ for _, syncName := range syncNames {
+ syncsTestFns = append(syncsTestFns, resource.TestCheckTypeSetElemAttr("hcp_vault_secrets_app.acc_test_app", "sync_names.*", syncName))
+ }
+
+ testFns := []resource.TestCheckFunc{
resource.TestCheckResourceAttrSet("hcp_vault_secrets_app.acc_test_app", "organization_id"),
resource.TestCheckResourceAttrSet("hcp_vault_secrets_app.acc_test_app", "id"),
resource.TestCheckResourceAttrSet("hcp_vault_secrets_app.acc_test_app", "resource_name"),
resource.TestCheckResourceAttr("hcp_vault_secrets_app.acc_test_app", "project_id", os.Getenv("HCP_PROJECT_ID")),
resource.TestCheckResourceAttr("hcp_vault_secrets_app.acc_test_app", "app_name", appName),
- resource.TestCheckResourceAttr("hcp_vault_secrets_app.acc_test_app", "description", description),
- }
+ resource.TestCheckResourceAttr("hcp_vault_secrets_app.acc_test_app", "description", description)}
+
+ testFns = append(testFns, syncsTestFns...)
+ return testFns
}
func appExists(t *testing.T, name string) bool {
diff --git a/internal/provider/vaultsecrets/resource_vault_secrets_integration.go b/internal/provider/vaultsecrets/resource_vault_secrets_integration.go
index 9fdda766d..56f2b0649 100644
--- a/internal/provider/vaultsecrets/resource_vault_secrets_integration.go
+++ b/internal/provider/vaultsecrets/resource_vault_secrets_integration.go
@@ -291,11 +291,14 @@ func (r *resourceVaultSecretsIntegration) Schema(_ context.Context, _ resource.S
Optional: true,
Attributes: map[string]schema.Attribute{
"token": schema.StringAttribute{
- Description: "Access token used to authenticate against the target GitLab account.",
+ Description: "Access token used to authenticate against the target GitLab account. This token must have privilege to create CI/CD variables.",
Required: true,
Sensitive: true,
},
},
+ Validators: []validator.Object{
+ exactlyOneIntegrationTypeFieldsValidator,
+ },
},
}
diff --git a/internal/provider/vaultsecrets/resource_vault_secrets_integration_test.go b/internal/provider/vaultsecrets/resource_vault_secrets_integration_test.go
index 3a2555db3..a7883712a 100644
--- a/internal/provider/vaultsecrets/resource_vault_secrets_integration_test.go
+++ b/internal/provider/vaultsecrets/resource_vault_secrets_integration_test.go
@@ -17,7 +17,7 @@ import (
)
func TestAccVaultSecretsResourceIntegrationGitLab(t *testing.T) {
- accessToken := checkRequiredEnvVarOrFail(t, "GITLAB_ACCESS_TOKEN")
+ accessToken := checkRequiredEnvVarOrFail(t, "VAULTSECRETS_GITLAB_ACCESS_TOKEN")
integrationName1 := generateRandomSlug()
integrationName2 := generateRandomSlug()
diff --git a/internal/provider/vaultsecrets/resource_vault_secrets_sync.go b/internal/provider/vaultsecrets/resource_vault_secrets_sync.go
new file mode 100644
index 000000000..9e043ed20
--- /dev/null
+++ b/internal/provider/vaultsecrets/resource_vault_secrets_sync.go
@@ -0,0 +1,382 @@
+package vaultsecrets
+
+import (
+ "context"
+ "fmt"
+
+ "golang.org/x/exp/maps"
+
+ "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/stable/2023-11-28/client/secret_service"
+ secretmodels "github.com/hashicorp/hcp-sdk-go/clients/cloud-vault-secrets/stable/2023-11-28/models"
+ "github.com/hashicorp/terraform-plugin-framework-validators/objectvalidator"
+ "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
+ "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/planmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+ "github.com/hashicorp/terraform-plugin-framework/schema/validator"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+ "github.com/hashicorp/terraform-plugin-framework/types/basetypes"
+ "github.com/hashicorp/terraform-provider-hcp/internal/clients"
+ "github.com/hashicorp/terraform-provider-hcp/internal/provider/modifiers"
+)
+
+type Sync struct {
+ ResourceID types.String `tfsdk:"resource_id"`
+ Name types.String `tfsdk:"name"`
+ IntegrationName types.String `tfsdk:"integration_name"`
+ OrganizationID types.String `tfsdk:"organization_id"`
+ ProjectID types.String `tfsdk:"project_id"`
+
+ // Destination-specific mutually exclusive fields
+ GitlabConfig types.Object `tfsdk:"gitlab_config"`
+
+ // Inner API-compatible models derived from the Terraform fields
+ gitlabConfig *secretmodels.Secrets20231128SyncConfigGitlab `tfsdk:"-"`
+}
+
+var _ resource.Resource = &resourceVaultSecretsSync{}
+var _ resource.ResourceWithConfigure = &resourceVaultSecretsSync{}
+var _ resource.ResourceWithModifyPlan = &resourceVaultSecretsSync{}
+var _ resource.ResourceWithImportState = &resourceVaultSecretsSync{}
+
+func NewVaultSecretsSyncResource() resource.Resource {
+ return &resourceVaultSecretsSync{}
+}
+
+type resourceVaultSecretsSync struct {
+ client *clients.Client
+}
+
+func (r *resourceVaultSecretsSync) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_vault_secrets_sync"
+}
+
+func (r *resourceVaultSecretsSync) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
+ attributes := map[string]schema.Attribute{
+ "resource_id": schema.StringAttribute{
+ Description: "Resource ID used to uniquely identify the sync on the HCP platform.",
+ Computed: true,
+ },
+ "name": schema.StringAttribute{
+ Description: "The Vault Secrets Sync name.",
+ Required: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.RequiresReplace(),
+ },
+ Validators: []validator.String{
+ slugValidator,
+ },
+ },
+ "integration_name": schema.StringAttribute{
+ Description: "The Vault Secrets integration name.",
+ Required: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.RequiresReplace(),
+ },
+ Validators: []validator.String{
+ slugValidator,
+ },
+ },
+ "gitlab_config": schema.SingleNestedAttribute{
+ Description: "Configuration parameters used to determine the sync destination.",
+ Optional: true,
+ Attributes: map[string]schema.Attribute{
+ "scope": schema.StringAttribute{
+ Description: "The scope to which sync applies. Defaults to GROUP. The valid options are GROUP and PROJECT",
+ Optional: true,
+ Computed: true,
+ Validators: []validator.String{
+ stringvalidator.OneOf("GROUP", "PROJECT"),
+ },
+ },
+ "group_id": schema.StringAttribute{
+ Description: "ID of the group, if the scope is GROUP",
+ Optional: true,
+ Validators: []validator.String{
+ stringvalidator.ConflictsWith(
+ path.MatchRelative().AtParent().AtName("project_id"),
+ ),
+ stringvalidator.AtLeastOneOf(
+ path.MatchRelative().AtParent().AtName("project_id"),
+ ),
+ },
+ },
+ "project_id": schema.StringAttribute{
+ Description: "ID of the project, if the scope is PROJECT",
+ Optional: true,
+ },
+ },
+ Validators: []validator.Object{
+ exactlyOneSyncConfigFieldsValidator,
+ },
+ },
+ }
+
+ maps.Copy(attributes, locationAttributes)
+
+ resp.Schema = schema.Schema{
+ MarkdownDescription: "The Vault Secrets sync resource manages an integration.",
+ Attributes: attributes,
+ }
+}
+
+func (r *resourceVaultSecretsSync) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
+ if req.ProviderData == nil {
+ return
+ }
+ client, ok := req.ProviderData.(*clients.Client)
+ if !ok {
+ resp.Diagnostics.AddError(
+ "Unexpected Data Source Configure Type",
+ fmt.Sprintf("Expected *clients.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
+ )
+ return
+ }
+ r.client = client
+}
+
+func (r *resourceVaultSecretsSync) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
+ modifiers.ModifyPlanForDefaultProjectChange(ctx, r.client.Config.ProjectID, req.State, req.Config, req.Plan, resp)
+}
+
+func (r *resourceVaultSecretsSync) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ resp.Diagnostics.Append(decorateOperation[*Sync](ctx, r.client, &resp.State, req.State.Get, "reading", func(i hvsResource) (any, error) {
+ sync, ok := i.(*Sync)
+ if !ok {
+ return nil, fmt.Errorf("invalid integration type, expected *Sync, got: %T, this is a bug on the provider", i)
+ }
+
+ response, err := r.client.VaultSecrets.GetSync(secret_service.NewGetSyncParamsWithContext(ctx).
+ WithOrganizationID(sync.OrganizationID.ValueString()).
+ WithProjectID(sync.ProjectID.ValueString()).
+ WithName(sync.Name.ValueString()), nil)
+
+ if err != nil && !clients.IsResponseCodeNotFound(err) {
+ return nil, err
+ }
+
+ if response == nil || response.Payload == nil {
+ return nil, nil
+ }
+ return response.Payload.Sync, nil
+ })...)
+}
+
+func (r *resourceVaultSecretsSync) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ resp.Diagnostics.Append(decorateOperation[*Sync](ctx, r.client, &resp.State, req.Plan.Get, "creating", func(i hvsResource) (any, error) {
+ sync, ok := i.(*Sync)
+ if !ok {
+ return nil, fmt.Errorf("invalid resource type, expected *Sync, got: %T, this is a bug on the provider", i)
+ }
+
+ res, err := r.client.VaultSecrets.GetIntegration(secret_service.NewGetIntegrationParamsWithContext(ctx).
+ WithOrganizationID(sync.OrganizationID.ValueString()).
+ WithProjectID(sync.ProjectID.ValueString()).
+ WithName(sync.IntegrationName.ValueString()), nil)
+
+ if err != nil {
+ return nil, err
+ }
+
+ providerType := res.Payload.Integration.Provider
+
+ response, err := r.client.VaultSecrets.CreateSync(&secret_service.CreateSyncParams{
+ Body: &secretmodels.SecretServiceCreateSyncBody{
+ Name: sync.Name.ValueString(),
+ IntegrationName: sync.IntegrationName.ValueString(),
+ Type: providerType,
+ SyncConfigGitlab: sync.gitlabConfig,
+ },
+ OrganizationID: sync.OrganizationID.ValueString(),
+ ProjectID: sync.ProjectID.ValueString(),
+ }, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ return response.Payload.Sync, nil
+ })...)
+}
+
+func (r *resourceVaultSecretsSync) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+}
+
+func (r *resourceVaultSecretsSync) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ resp.Diagnostics.Append(decorateOperation[*Sync](ctx, r.client, &resp.State, req.State.Get, "deleting", func(i hvsResource) (any, error) {
+ sync, ok := i.(*Sync)
+ if !ok {
+ return nil, fmt.Errorf("invalid integration type, expected *Sync, got: %T, this is a bug on the provider", i)
+ }
+
+ _, err := r.client.VaultSecrets.DeleteSync(secret_service.NewDeleteSyncParamsWithContext(ctx).
+ WithOrganizationID(sync.OrganizationID.ValueString()).
+ WithProjectID(sync.ProjectID.ValueString()).
+ WithName(sync.Name.ValueString()), nil)
+ if err != nil {
+ return nil, err
+ }
+ return nil, nil
+ })...)
+}
+
+func (r *resourceVaultSecretsSync) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("organization_id"), r.client.Config.OrganizationID)...)
+ resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), r.client.Config.ProjectID)...)
+ resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("name"), req.ID)...)
+}
+
+var _ hvsResource = &Sync{}
+
+func (s *Sync) projectID() types.String {
+ return s.ProjectID
+}
+
+func (s *Sync) initModel(ctx context.Context, orgID, projID string) diag.Diagnostics {
+ s.OrganizationID = types.StringValue(orgID)
+ s.ProjectID = types.StringValue(projID)
+
+ if !s.GitlabConfig.IsNull() {
+ config := gitlabConfigParams{}
+ diags := s.GitlabConfig.As(ctx, &config, basetypes.ObjectAsOptions{})
+ if diags.HasError() {
+ return diags
+ }
+
+ scopeStr := config.Scope.ValueString()
+ if scopeStr == "" {
+ if !config.GroupID.IsNull() {
+ scopeStr = "GROUP"
+ } else if !config.ProjectID.IsNull() {
+ scopeStr = "PROJECT"
+ } else {
+ return diag.Diagnostics{
+ diag.NewErrorDiagnostic(
+ "Invalid GitLab Configuration",
+ "Either group_id or project_id must be specified",
+ ),
+ }
+ }
+ }
+
+ scope := secretmodels.SyncConfigGitlabScope(scopeStr)
+
+ var groupIDVal, projectIDVal string
+ if scope == secretmodels.SyncConfigGitlabScopeGROUP {
+ if config.GroupID.IsNull() {
+ return diag.Diagnostics{
+ diag.NewErrorDiagnostic(
+ "Invalid GitLab Configuration",
+ "group_id is required when scope is GROUP",
+ ),
+ }
+ }
+ groupIDVal = config.GroupID.ValueString()
+ projectIDVal = ""
+ } else if scope == secretmodels.SyncConfigGitlabScopePROJECT {
+ if config.ProjectID.IsNull() {
+ return diag.Diagnostics{
+ diag.NewErrorDiagnostic(
+ "Invalid GitLab Configuration",
+ "project_id is required when scope is PROJECT",
+ ),
+ }
+ }
+ groupIDVal = ""
+ projectIDVal = config.ProjectID.ValueString()
+ }
+
+ s.gitlabConfig = &secretmodels.Secrets20231128SyncConfigGitlab{
+ Scope: &scope,
+ GroupID: groupIDVal,
+ ProjectID: projectIDVal,
+ Protected: false,
+ Raw: false,
+ }
+ }
+
+ return diag.Diagnostics{}
+}
+
+func (s *Sync) fromModel(ctx context.Context, orgID, projID string, model any) diag.Diagnostics {
+ diags := diag.Diagnostics{}
+
+ syncModel, ok := model.(*secretmodels.Secrets20231128Sync)
+ if !ok {
+ diags.AddError(
+ "Invalid model type, this is a bug on the provider.",
+ fmt.Sprintf("Expected *secretmodels.Secrets20231128Sync, got: %T", model),
+ )
+ return diags
+ }
+
+ s.ResourceID = types.StringValue(syncModel.ResourceID)
+ s.Name = types.StringValue(syncModel.Name)
+ s.IntegrationName = types.StringValue(syncModel.IntegrationName)
+ s.OrganizationID = types.StringValue(orgID)
+ s.ProjectID = types.StringValue(projID)
+
+ if syncModel.SyncConfigGitlab == nil {
+ s.GitlabConfig = types.ObjectNull(map[string]attr.Type{
+ "scope": types.StringType,
+ "group_id": types.StringType,
+ "project_id": types.StringType,
+ })
+
+ return diags
+ }
+
+ scopeVal := types.StringNull()
+ if syncModel.SyncConfigGitlab.Scope != nil {
+ scopeVal = types.StringValue(string(*syncModel.SyncConfigGitlab.Scope))
+ }
+
+ var groupIDVal types.String
+ if syncModel.SyncConfigGitlab.GroupID != "" {
+ groupIDVal = types.StringValue(syncModel.SyncConfigGitlab.GroupID)
+ } else {
+ groupIDVal = types.StringNull()
+ }
+
+ var projectIDVal types.String
+ if syncModel.SyncConfigGitlab.ProjectID != "" {
+ projectIDVal = types.StringValue(syncModel.SyncConfigGitlab.ProjectID)
+ } else {
+ projectIDVal = types.StringNull()
+ }
+
+ s.GitlabConfig, diags = types.ObjectValue(
+ map[string]attr.Type{
+ "scope": types.StringType,
+ "group_id": types.StringType,
+ "project_id": types.StringType,
+ },
+ map[string]attr.Value{
+ "scope": scopeVal,
+ "group_id": groupIDVal,
+ "project_id": projectIDVal,
+ },
+ )
+ if diags.HasError() {
+ return diags
+ }
+
+ return diags
+}
+
+// Validations and types for sync destinations
+
+var exactlyOneSyncConfigFieldsValidator = objectvalidator.ExactlyOneOf(
+ path.Expressions{
+ path.MatchRoot("gitlab_config"),
+ }...,
+)
+
+type gitlabConfigParams struct {
+ Scope types.String `tfsdk:"scope"`
+ GroupID types.String `tfsdk:"group_id"`
+ ProjectID types.String `tfsdk:"project_id"`
+}
diff --git a/internal/provider/vaultsecrets/resource_vault_secrets_sync_test.go b/internal/provider/vaultsecrets/resource_vault_secrets_sync_test.go
new file mode 100644
index 000000000..6e2eab6f1
--- /dev/null
+++ b/internal/provider/vaultsecrets/resource_vault_secrets_sync_test.go
@@ -0,0 +1,51 @@
+package vaultsecrets_test
+
+import (
+ "fmt"
+ "os"
+ "testing"
+
+ "github.com/hashicorp/terraform-plugin-testing/helper/resource"
+ "github.com/hashicorp/terraform-provider-hcp/internal/provider/acctest"
+)
+
+func TestAccVaultSecretsResourceSync(t *testing.T) {
+ syncName := generateRandomSlug()
+ integrationName := generateRandomSlug()
+ gitLabToken := checkRequiredEnvVarOrFail(t, "VAULTSECRETS_GITLAB_ACCESS_TOKEN")
+
+ resource.Test(t, resource.TestCase{
+ ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories,
+ Steps: []resource.TestStep{
+ {
+ Config: syncConfig(integrationName, syncName, gitLabToken),
+ Check: resource.ComposeTestCheckFunc(
+ resource.TestCheckResourceAttrSet("hcp_vault_secrets_sync.acc_test_gitlab_group", "organization_id"),
+ resource.TestCheckResourceAttr("hcp_vault_secrets_sync.acc_test_gitlab_group", "project_id", os.Getenv("HCP_PROJECT_ID")),
+ resource.TestCheckResourceAttr("hcp_vault_secrets_sync.acc_test_gitlab_group", "name", syncName),
+ resource.TestCheckResourceAttr("hcp_vault_secrets_sync.acc_test_gitlab_group", "integration_name", integrationName)),
+ },
+ },
+ })
+}
+
+func syncConfig(integrationName, syncName, accessToken string) string {
+ return fmt.Sprintf(`
+resource "hcp_vault_secrets_integration" "acc_test" {
+ name = %q
+ capabilities = ["SYNC"]
+ provider_type = "gitlab"
+ gitlab_access = {
+ token = %q
+ }
+}
+
+resource "hcp_vault_secrets_sync" "acc_test_gitlab_group" {
+ name = %q
+ integration_name = %q
+ gitlab_config = {
+ scope = "GROUP"
+ group_id = 123456
+ }
+ }`, integrationName, accessToken, syncName, integrationName)
+}
diff --git a/templates/resources/vault_secrets_sync.md.tmpl b/templates/resources/vault_secrets_sync.md.tmpl
new file mode 100644
index 000000000..025dcc1db
--- /dev/null
+++ b/templates/resources/vault_secrets_sync.md.tmpl
@@ -0,0 +1,22 @@
+---
+page_title: "{{.Type}} {{.Name}}"
+subcategory: "HCP Vault Secrets"
+description: |-
+{{ .Description | plainmarkdown | trimspace | prefixlines " " }}
+---
+
+# {{.Name}} ({{.Type}})
+
+{{ .Description | trimspace }}
+
+## Example Usage
+
+{{ tffile "examples/resources/hcp_vault_secrets_sync/resource.tf" }}
+
+{{ .SchemaMarkdown | trimspace }}
+
+## Import
+
+Import is supported using the following syntax:
+
+{{ codefile "shell" "examples/resources/hcp_vault_secrets_sync/import.sh" }}