diff --git a/client/access_group.go b/client/access_group.go new file mode 100644 index 00000000..7ea8ce2f --- /dev/null +++ b/client/access_group.go @@ -0,0 +1,131 @@ +package client + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-log/tflog" +) + +type AccessGroup struct { + ID string `json:"accessGroupId"` + TeamID string `json:"teamId"` + Name string `json:"name"` +} + +type GetAccessGroupRequest struct { + AccessGroupID string + TeamID string +} + +func (c *Client) GetAccessGroup(ctx context.Context, req GetAccessGroupRequest) (r AccessGroup, err error) { + url := fmt.Sprintf("%s/v1/access-groups/%s", c.baseURL, req.AccessGroupID) + if c.teamID(req.TeamID) != "" { + url = fmt.Sprintf("%s?teamId=%s", url, c.teamID(req.TeamID)) + } + tflog.Info(ctx, "getting access group", map[string]interface{}{ + "url": url, + }) + err = c.doRequest(clientRequest{ + ctx: ctx, + method: "GET", + url: url, + body: "", + }, &r) + if err != nil { + return r, fmt.Errorf("unable to get access group: %w", err) + } + + r.TeamID = c.teamID(req.TeamID) + return r, err +} + +type CreateAccessGroupRequest struct { + TeamID string + Name string +} + +func (c *Client) CreateAccessGroup(ctx context.Context, req CreateAccessGroupRequest) (r AccessGroup, err error) { + url := fmt.Sprintf("%s/v1/access-groups", c.baseURL) + if c.teamID(req.TeamID) != "" { + url = fmt.Sprintf("%s?teamId=%s", url, c.teamID(req.TeamID)) + } + payload := string(mustMarshal( + struct { + Name string `json:"name"` + }{ + Name: req.Name, + }, + )) + tflog.Info(ctx, "creating access group", map[string]interface{}{ + "url": url, + "payload": payload, + }) + err = c.doRequest(clientRequest{ + ctx: ctx, + method: "POST", + url: url, + body: payload, + }, &r) + if err != nil { + return r, err + } + r.TeamID = c.teamID(req.TeamID) + return r, err +} + +type UpdateAccessGroupRequest struct { + AccessGroupID string + TeamID string + Name string +} + +func (c *Client) UpdateAccessGroup(ctx context.Context, req UpdateAccessGroupRequest) (r AccessGroup, err error) { + url := fmt.Sprintf("%s/v1/access-groups/%s", c.baseURL, req.AccessGroupID) + if c.teamID(req.TeamID) != "" { + url = fmt.Sprintf("%s?teamId=%s", url, c.teamID(req.TeamID)) + } + payload := string(mustMarshal( + struct { + Name string `json:"name"` + }{ + Name: req.Name, + }, + )) + tflog.Info(ctx, "updating access group", map[string]interface{}{ + "url": url, + "payload": payload, + }) + err = c.doRequest(clientRequest{ + ctx: ctx, + method: "POST", + url: url, + body: payload, + }, &r) + if err != nil { + return r, err + } + r.TeamID = c.teamID(req.TeamID) + return r, err +} + +type DeleteAccessGroupRequest struct { + AccessGroupID string + TeamID string +} + +func (c *Client) DeleteAccessGroup(ctx context.Context, req DeleteAccessGroupRequest) error { + url := fmt.Sprintf("%s/v1/access-groups/%s", c.baseURL, req.AccessGroupID) + if c.teamID(req.TeamID) != "" { + url = fmt.Sprintf("%s?teamId=%s", url, c.teamID(req.TeamID)) + } + tflog.Info(ctx, "deleting access group", map[string]interface{}{ + "url": url, + }) + return c.doRequest(clientRequest{ + ctx: ctx, + method: "DELETE", + url: url, + body: "", + }, nil) +} diff --git a/client/access_group_project.go b/client/access_group_project.go new file mode 100644 index 00000000..74e04b09 --- /dev/null +++ b/client/access_group_project.go @@ -0,0 +1,138 @@ +package client + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-log/tflog" +) + +type AccessGroupProject struct { + TeamID string `json:"teamId"` + AccessGroupID string `json:"accessGroupId"` + ProjectID string `json:"projectId"` + Role string `json:"role"` +} + +type CreateAccessGroupProjectRequest struct { + TeamID string + AccessGroupID string + ProjectID string + Role string +} + +func (c *Client) CreateAccessGroupProject(ctx context.Context, req CreateAccessGroupProjectRequest) (r AccessGroupProject, err error) { + url := fmt.Sprintf("%s/v1/access-groups/%s/projects", c.baseURL, req.AccessGroupID) + if c.teamID(req.TeamID) != "" { + url = fmt.Sprintf("%s?teamId=%s", url, c.teamID(req.TeamID)) + } + payload := string(mustMarshal( + struct { + Role string `json:"role"` + ProjectID string `json:"projectId"` + }{ + Role: req.Role, + ProjectID: req.ProjectID, + }, + )) + tflog.Info(ctx, "creating access group project", map[string]interface{}{ + "url": url, + "payload": payload, + }) + err = c.doRequest(clientRequest{ + ctx: ctx, + method: "POST", + url: url, + body: payload, + }, &r) + if err != nil { + return r, err + } + r.TeamID = c.teamID(req.TeamID) + return r, err +} + +type GetAccessGroupProjectRequest struct { + TeamID string + AccessGroupID string + ProjectID string +} + +func (c *Client) GetAccessGroupProject(ctx context.Context, req GetAccessGroupProjectRequest) (r AccessGroupProject, err error) { + url := fmt.Sprintf("%s/v1/access-groups/%s/projects/%s", c.baseURL, req.AccessGroupID, req.ProjectID) + if c.teamID(req.TeamID) != "" { + url = fmt.Sprintf("%s?teamId=%s", url, c.teamID(req.TeamID)) + } + tflog.Info(ctx, "getting access group project", map[string]interface{}{ + "url": url, + }) + err = c.doRequest(clientRequest{ + ctx: ctx, + method: "GET", + url: url, + }, &r) + + if err != nil { + return r, fmt.Errorf("unable to get access group project: %w", err) + } + + return r, err +} + +type UpdateAccessGroupProjectRequest struct { + TeamID string + AccessGroupID string + ProjectID string + Role string +} + +func (c *Client) UpdateAccessGroupProject(ctx context.Context, req UpdateAccessGroupProjectRequest) (r AccessGroupProject, err error) { + url := fmt.Sprintf("%s/v1/access-groups/%s/projects/%s", c.baseURL, req.AccessGroupID, req.ProjectID) + if c.teamID(req.TeamID) != "" { + url = fmt.Sprintf("%s?teamId=%s", url, c.teamID(req.TeamID)) + } + payload := string(mustMarshal( + struct { + Role string `json:"role"` + }{ + Role: req.Role, + }, + )) + tflog.Info(ctx, "updating access group project", map[string]interface{}{ + "url": url, + "payload": payload, + }) + err = c.doRequest(clientRequest{ + ctx: ctx, + method: "PATCH", + url: url, + body: payload, + }, &r) + if err != nil { + return r, err + } + r.TeamID = c.teamID(req.TeamID) + return r, err +} + +type DeleteAccessGroupProjectRequest struct { + TeamID string + AccessGroupID string + ProjectID string +} + +func (c *Client) DeleteAccessGroupProject(ctx context.Context, req DeleteAccessGroupProjectRequest) error { + url := fmt.Sprintf("%s/v1/access-groups/%s/projects/%s", c.baseURL, req.AccessGroupID, req.ProjectID) + if c.teamID(req.TeamID) != "" { + url = fmt.Sprintf("%s?teamId=%s", url, c.teamID(req.TeamID)) + } + tflog.Info(ctx, "deleting access group project", map[string]interface{}{ + "url": url, + }) + return c.doRequest(clientRequest{ + ctx: ctx, + method: "DELETE", + url: url, + body: "", + }, nil) +} diff --git a/docs/data-sources/access_group.md b/docs/data-sources/access_group.md new file mode 100644 index 00000000..b3272641 --- /dev/null +++ b/docs/data-sources/access_group.md @@ -0,0 +1,31 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "vercel_access_group Data Source - terraform-provider-vercel" +subcategory: "" +description: |- + Provides information about an existing Access Group. + For more detailed information, please see the Vercel documentation https://vercel.com/docs/accounts/team-members-and-roles/access-groups. +--- + +# vercel_access_group (Data Source) + +Provides information about an existing Access Group. + +For more detailed information, please see the [Vercel documentation](https://vercel.com/docs/accounts/team-members-and-roles/access-groups). + + + + +## Schema + +### Required + +- `id` (String) The Access Group ID to be retrieved. + +### Optional + +- `team_id` (String) The ID of the team the Access Group should exist under. Required when configuring a team resource if a default team has not been set in the provider. + +### Read-Only + +- `name` (String) The name of the Access Group. diff --git a/docs/data-sources/access_group_project.md b/docs/data-sources/access_group_project.md new file mode 100644 index 00000000..66fef0f0 --- /dev/null +++ b/docs/data-sources/access_group_project.md @@ -0,0 +1,32 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "vercel_access_group_project Data Source - terraform-provider-vercel" +subcategory: "" +description: |- + Provides information about an existing Access Group Project Assignment. + For more detailed information, please see the Vercel documentation https://vercel.com/docs/accounts/team-members-and-roles/access-groups. +--- + +# vercel_access_group_project (Data Source) + +Provides information about an existing Access Group Project Assignment. + +For more detailed information, please see the [Vercel documentation](https://vercel.com/docs/accounts/team-members-and-roles/access-groups). + + + + +## Schema + +### Required + +- `access_group_id` (String) The Access Group ID. +- `project_id` (String) The Project ID. + +### Optional + +- `team_id` (String) The ID of the team the Access Group Project should exist under. Required when configuring a team resource if a default team has not been set in the provider. + +### Read-Only + +- `role` (String) The Access Group Project Role. diff --git a/docs/resources/access_group.md b/docs/resources/access_group.md new file mode 100644 index 00000000..d5d5a82e --- /dev/null +++ b/docs/resources/access_group.md @@ -0,0 +1,34 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "vercel_access_group Resource - terraform-provider-vercel" +subcategory: "" +description: |- + Provides an Access Group Resource. + Access Groups provide a way to manage groups of Vercel users across projects on your team. They are a set of project role assignations, a combination of Vercel users and the projects they work on. + For more detailed information, please see the Vercel documentation https://vercel.com/docs/accounts/team-members-and-roles/access-groups. +--- + +# vercel_access_group (Resource) + +Provides an Access Group Resource. + +Access Groups provide a way to manage groups of Vercel users across projects on your team. They are a set of project role assignations, a combination of Vercel users and the projects they work on. + +For more detailed information, please see the [Vercel documentation](https://vercel.com/docs/accounts/team-members-and-roles/access-groups). + + + + +## Schema + +### Required + +- `name` (String) The name of the Access Group + +### Optional + +- `team_id` (String) The ID of the team the Access Group should exist under. Required when configuring a team resource if a default team has not been set in the provider. + +### Read-Only + +- `id` (String) The ID of the Access Group. diff --git a/docs/resources/access_group_project.md b/docs/resources/access_group_project.md new file mode 100644 index 00000000..8117e635 --- /dev/null +++ b/docs/resources/access_group_project.md @@ -0,0 +1,32 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "vercel_access_group_project Resource - terraform-provider-vercel" +subcategory: "" +description: |- + Provides an Access Group Project Resource. + An Access Group Project resource defines the relationship between a vercel_access_group and a vercel_project. + For more detailed information, please see the Vercel documentation https://vercel.com/docs/accounts/team-members-and-roles/access-groups. +--- + +# vercel_access_group_project (Resource) + +Provides an Access Group Project Resource. + +An Access Group Project resource defines the relationship between a `vercel_access_group` and a `vercel_project`. + +For more detailed information, please see the [Vercel documentation](https://vercel.com/docs/accounts/team-members-and-roles/access-groups). + + + + +## Schema + +### Required + +- `access_group_id` (String) The ID of the Access Group. +- `project_id` (String) The Project ID to assign to the access group. +- `role` (String) The project role to assign to the access group. Must be either `ADMIN`, `PROJECT_DEVELOPER`, or `PROJECT_VIEWER`. + +### Optional + +- `team_id` (String) The ID of the team the access group project should exist under. Required when configuring a team resource if a default team has not been set in the provider. diff --git a/go.mod b/go.mod index af6d1093..38ba2695 100644 --- a/go.mod +++ b/go.mod @@ -45,6 +45,7 @@ require ( github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/oklog/run v1.1.0 // indirect + github.com/vercel/terraform-provider-vercel v1.14.1 github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect diff --git a/go.sum b/go.sum index 09e5b437..f4517c11 100644 --- a/go.sum +++ b/go.sum @@ -140,6 +140,8 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/vercel/terraform-provider-vercel v1.14.1 h1:ghAjFkMMzka4XuoBYdu1OXM/K7FQEj8wUd+xMPPOGrg= +github.com/vercel/terraform-provider-vercel v1.14.1/go.mod h1:AdFCiUD0XP8XOi6tnhaCh7I0vyq2TAPmI+GcIp3+7SI= github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI= github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= diff --git a/vercel/data_source_access_group.go b/vercel/data_source_access_group.go new file mode 100644 index 00000000..713bfcc4 --- /dev/null +++ b/vercel/data_source_access_group.go @@ -0,0 +1,116 @@ +package vercel + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/vercel/terraform-provider-vercel/v2/client" +) + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ datasource.DataSource = &accessGroupDataSource{} + _ datasource.DataSourceWithConfigure = &accessGroupDataSource{} +) + +func newAccessGroupDataSource() datasource.DataSource { + return &accessGroupDataSource{} +} + +type accessGroupDataSource struct { + client *client.Client +} + +func (d *accessGroupDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_access_group" +} + +func (d *accessGroupDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + // Prevent panic if the provider has not been configured. + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(*client.Client) + if !ok { + resp.Diagnostics.AddError( + "Unexpected Data Source Configure Type", + fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return + } + + d.client = client +} + +func (r *accessGroupDataSource) Schema(_ context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: ` +Provides information about an existing Access Group. + +For more detailed information, please see the [Vercel documentation](https://vercel.com/docs/accounts/team-members-and-roles/access-groups). +`, + Attributes: map[string]schema.Attribute{ + "team_id": schema.StringAttribute{ + Optional: true, + Computed: true, + Description: "The ID of the team the Access Group should exist under. Required when configuring a team resource if a default team has not been set in the provider.", + }, + "id": schema.StringAttribute{ + Required: true, + Description: "The Access Group ID to be retrieved.", + }, + "name": schema.StringAttribute{ + Computed: true, + Description: "The name of the Access Group.", + }, + }, + } +} + +func (d *accessGroupDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var state AccessGroup + diags := req.Config.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + out, err := d.client.GetAccessGroup(ctx, client.GetAccessGroupRequest{ + AccessGroupID: state.ID.ValueString(), + TeamID: state.TeamID.ValueString(), + }) + + if err != nil { + resp.Diagnostics.AddError( + "Error reading Access Group", + fmt.Sprintf("Could not get Access Group %s %s, unexpected error: %s", + state.TeamID.ValueString(), + state.ID.ValueString(), + err, + ), + ) + return + } + + result := AccessGroup{ + ID: types.StringValue(out.ID), + TeamID: types.StringValue(out.TeamID), + Name: types.StringValue(out.Name), + } + tflog.Info(ctx, "read Access Group", map[string]interface{}{ + "team_id": result.TeamID.ValueString(), + "access_group_id": result.ID.ValueString(), + "name": result.Name.ValueString(), + }) + + diags = resp.State.Set(ctx, result) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} diff --git a/vercel/data_source_access_group_project.go b/vercel/data_source_access_group_project.go new file mode 100644 index 00000000..fbdb76d7 --- /dev/null +++ b/vercel/data_source_access_group_project.go @@ -0,0 +1,124 @@ +package vercel + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/vercel/terraform-provider-vercel/v2/client" +) + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ datasource.DataSource = &accessGroupProjectDataSource{} + _ datasource.DataSourceWithConfigure = &accessGroupProjectDataSource{} +) + +func newAccessGroupProjectDataSource() datasource.DataSource { + return &accessGroupProjectDataSource{} +} + +type accessGroupProjectDataSource struct { + client *client.Client +} + +func (d *accessGroupProjectDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_access_group_project" +} + +func (d *accessGroupProjectDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + // Prevent panic if the provider has not been configured. + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(*client.Client) + if !ok { + resp.Diagnostics.AddError( + "Unexpected Data Source Configure Type", + fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return + } + + d.client = client +} + +func (r *accessGroupProjectDataSource) Schema(_ context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: ` +Provides information about an existing Access Group Project Assignment. + +For more detailed information, please see the [Vercel documentation](https://vercel.com/docs/accounts/team-members-and-roles/access-groups). +`, + Attributes: map[string]schema.Attribute{ + "team_id": schema.StringAttribute{ + Optional: true, + Computed: true, + Description: "The ID of the team the Access Group Project should exist under. Required when configuring a team resource if a default team has not been set in the provider.", + }, + "access_group_id": schema.StringAttribute{ + Required: true, + Description: "The Access Group ID.", + }, + "project_id": schema.StringAttribute{ + Required: true, + Description: "The Project ID.", + }, + "role": schema.StringAttribute{ + Computed: true, + Description: "The Access Group Project Role.", + }, + }, + } +} + +func (d *accessGroupProjectDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var state AccessGroupProject + diags := req.Config.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + out, err := d.client.GetAccessGroupProject(ctx, client.GetAccessGroupProjectRequest{ + TeamID: state.TeamID.ValueString(), + AccessGroupID: state.AccessGroupID.ValueString(), + ProjectID: state.ProjectID.ValueString(), + }) + + if err != nil { + resp.Diagnostics.AddError( + "Error reading Access Group Project", + fmt.Sprintf("Could not get Access Group Project %s %s %s, unexpected error: %s", + state.TeamID.ValueString(), + state.AccessGroupID.ValueString(), + state.ProjectID.ValueString(), + err, + ), + ) + return + } + + result := AccessGroupProject{ + TeamID: types.StringValue(out.TeamID), + AccessGroupID: types.StringValue(out.AccessGroupID), + ProjectID: types.StringValue(out.ProjectID), + Role: types.StringValue(out.Role), + } + tflog.Info(ctx, "read Access Group Project", map[string]interface{}{ + "team_id": result.TeamID.ValueString(), + "access_group_id": result.AccessGroupID.ValueString(), + "project_id": result.ProjectID.ValueString(), + "role": result.Role.ValueString(), + }) + + diags = resp.State.Set(ctx, result) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} diff --git a/vercel/data_source_access_group_project_test.go b/vercel/data_source_access_group_project_test.go new file mode 100644 index 00000000..38027947 --- /dev/null +++ b/vercel/data_source_access_group_project_test.go @@ -0,0 +1,55 @@ +package vercel_test + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestAcc_AccessGroupProjectDataSource(t *testing.T) { + name := acctest.RandString(16) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: testAccAccessGroupProjectDataSource(name), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("data.vercel_access_group_project.test", "role", "ADMIN"), + ), + }, + }, + }) +} + +func testAccAccessGroupProjectDataSource(name string) string { + return fmt.Sprintf(` +resource "vercel_project" "test" { + %[1]s + name = "test-acc-%[2]s" +} + +resource "vercel_access_group" "test" { + %[1]s + name = "test-acc-%[2]s" +} + +resource "vercel_access_group_project" "test" { + %[1]s + access_group_id = vercel_access_group.test.id + project_id = vercel_project.test.id + role = "ADMIN" +} + +data "vercel_access_group_project" "test" { + %[1]s + access_group_id = vercel_access_group.test.id + project_id = vercel_project.test.id + depends_on = [ + vercel_access_group_project.test + ] +} +`, teamIDConfig(), name) +} diff --git a/vercel/data_source_access_group_test.go b/vercel/data_source_access_group_test.go new file mode 100644 index 00000000..1787a344 --- /dev/null +++ b/vercel/data_source_access_group_test.go @@ -0,0 +1,39 @@ +package vercel_test + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestAcc_AccessGroupDataSource(t *testing.T) { + name := acctest.RandString(16) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: testAccAccessGroupDataSource(name), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("data.vercel_access_group.test", "name", "test-acc-"+name), + ), + }, + }, + }) +} + +func testAccAccessGroupDataSource(name string) string { + return fmt.Sprintf(` +resource "vercel_access_group" "test" { + name = "test-acc-%[2]s" + %[1]s +} + +data "vercel_access_group" "test" { + id = vercel_access_group.test.id + %[1]s +} +`, teamIDConfig(), name) +} diff --git a/vercel/provider.go b/vercel/provider.go index 18db6be5..560d2223 100644 --- a/vercel/provider.go +++ b/vercel/provider.go @@ -68,6 +68,8 @@ func (p *vercelProvider) Resources(_ context.Context) []func() resource.Resource newSharedEnvironmentVariableResource, newTeamConfigResource, newWebhookResource, + newAccessGroupResource, + newAccessGroupProjectResource, } } @@ -89,6 +91,8 @@ func (p *vercelProvider) DataSources(_ context.Context) []func() datasource.Data newProjectDirectoryDataSource, newSharedEnvironmentVariableDataSource, newTeamConfigDataSource, + newAccessGroupDataSource, + newAccessGroupProjectDataSource, } } diff --git a/vercel/resource_access_group.go b/vercel/resource_access_group.go new file mode 100644 index 00000000..c989b855 --- /dev/null +++ b/vercel/resource_access_group.go @@ -0,0 +1,308 @@ +package vercel + +import ( + "context" + "fmt" + + "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/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/vercel/terraform-provider-vercel/v2/client" +) + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ resource.Resource = &accessGroupResource{} + _ resource.ResourceWithConfigure = &accessGroupResource{} + _ resource.ResourceWithImportState = &accessGroupResource{} +) + +func newAccessGroupResource() resource.Resource { + return &accessGroupResource{} +} + +type accessGroupResource struct { + client *client.Client +} + +func (r *accessGroupResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_access_group" +} + +func (r *accessGroupResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + // Prevent panic if the provider has not been configured. + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(*client.Client) + if !ok { + resp.Diagnostics.AddError( + "Unexpected Resource Configure Type", + fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return + } + + r.client = client +} + +func (r *accessGroupResource) Schema(_ context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: ` +Provides an Access Group Resource. + +Access Groups provide a way to manage groups of Vercel users across projects on your team. They are a set of project role assignations, a combination of Vercel users and the projects they work on. + +For more detailed information, please see the [Vercel documentation](https://vercel.com/docs/accounts/team-members-and-roles/access-groups). +`, + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "The ID of the Access Group.", + Computed: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, + }, + "team_id": schema.StringAttribute{ + Description: "The ID of the team the Access Group should exist under. Required when configuring a team resource if a default team has not been set in the provider.", + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplaceIfConfigured(), stringplanmodifier.UseStateForUnknown()}, + }, + "name": schema.StringAttribute{ + Description: "The name of the Access Group", + Required: true, + }, + }, + } +} + +type AccessGroup struct { + ID types.String `tfsdk:"id"` + TeamID types.String `tfsdk:"team_id"` + Name types.String `tfsdk:"name"` +} + +func (r *accessGroupResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan AccessGroup + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + out, err := r.client.CreateAccessGroup(ctx, client.CreateAccessGroupRequest{ + TeamID: plan.TeamID.ValueString(), + Name: plan.Name.ValueString(), + }) + + if err != nil { + resp.Diagnostics.AddError( + "Error creating Access Group", + "Could not create Access Group, unexpected error: "+err.Error(), + ) + return + } + result := AccessGroup{ + ID: types.StringValue(out.ID), + Name: types.StringValue(out.Name), + TeamID: types.StringValue(out.TeamID), + } + + tflog.Info(ctx, "created Access Group", map[string]interface{}{ + "team_id": result.TeamID.ValueString(), + "id": result.ID.ValueString(), + }) + + diags = resp.State.Set(ctx, result) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +func (r *accessGroupResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state AccessGroup + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + out, err := r.client.GetAccessGroup(ctx, client.GetAccessGroupRequest{ + AccessGroupID: state.ID.ValueString(), + TeamID: state.TeamID.ValueString(), + }) + + if client.NotFound(err) { + resp.State.RemoveResource(ctx) + return + } + + if err != nil { + resp.Diagnostics.AddError( + "Error reading Access Group", + fmt.Sprintf("Could not get Access Group %s %s, unexpected error: %s", + state.TeamID.ValueString(), + state.ID.ValueString(), + err, + ), + ) + return + } + + result := AccessGroup{ + ID: types.StringValue(out.ID), + TeamID: toTeamID(out.TeamID), + Name: types.StringValue(out.Name), + } + + tflog.Info(ctx, "read Access Group", map[string]interface{}{ + "team_id": result.TeamID.ValueString(), + "id": result.ID.ValueString(), + "name": result.Name.ValueString(), + }) + + diags = resp.State.Set(ctx, result) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +func (r *accessGroupResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan AccessGroup + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + out, err := r.client.UpdateAccessGroup(ctx, client.UpdateAccessGroupRequest{ + TeamID: plan.TeamID.ValueString(), + AccessGroupID: plan.ID.ValueString(), + Name: plan.Name.ValueString(), + }) + + if client.NotFound(err) { + resp.State.RemoveResource(ctx) + return + } + + if err != nil { + resp.Diagnostics.AddError( + "Error updating Acccess Group", + fmt.Sprintf("Could not update Access Group %s %s, unexpected error: %s", + plan.TeamID.ValueString(), + plan.ID.ValueString(), + err, + ), + ) + return + } + + result := AccessGroup{ + ID: types.StringValue(out.ID), + TeamID: types.StringValue(out.TeamID), + Name: types.StringValue(out.Name), + } + + tflog.Trace(ctx, "update Access Group", map[string]interface{}{ + "team_id": result.TeamID.ValueString(), + "id": result.ID.ValueString(), + }) + + diags = resp.State.Set(ctx, result) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +func (r *accessGroupResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state AccessGroup + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + err := r.client.DeleteAccessGroup(ctx, client.DeleteAccessGroupRequest{ + TeamID: state.TeamID.ValueString(), + AccessGroupID: state.ID.ValueString(), + }) + + if client.NotFound(err) { + return + } + + if err != nil { + resp.Diagnostics.AddError( + "Error deleting Access Group", + fmt.Sprintf( + "Could not delete Access Group %s %s, unexpected error: %s", + state.TeamID.ValueString(), + state.ID.ValueString(), + err, + ), + ) + return + } + + tflog.Info(ctx, "deleted Access Group", map[string]interface{}{ + "team_id": state.TeamID.ValueString(), + "id": state.ID.ValueString(), + }) +} + +func (r *accessGroupResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + teamID, id, ok := splitInto1Or2(req.ID) + if !ok { + resp.Diagnostics.AddError( + "Error importing Access Group", + fmt.Sprintf("Invalid id '%s' specified. should be in format \"team_id/access_group_id\" or \"access_group_id\"", req.ID), + ) + } + + out, err := r.client.GetAccessGroup(ctx, client.GetAccessGroupRequest{ + TeamID: teamID, + AccessGroupID: id, + }) + + if client.NotFound(err) { + resp.State.RemoveResource(ctx) + return + } + + if err != nil { + resp.Diagnostics.AddError( + "Error reading Access Group", + fmt.Sprintf("Could not get Accesss Group %s %s, unexpected error: %s", + teamID, + id, + err, + ), + ) + return + } + + result := AccessGroup{ + ID: types.StringValue(out.ID), + TeamID: toTeamID(out.TeamID), + Name: types.StringValue(out.Name), + } + + tflog.Info(ctx, "import Access Group", map[string]interface{}{ + "team_id": result.TeamID.ValueString(), + "id": result.ID.ValueString(), + "name": result.Name.ValueString(), + }) + + diags := resp.State.Set(ctx, result) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} diff --git a/vercel/resource_access_group_project.go b/vercel/resource_access_group_project.go new file mode 100644 index 00000000..ac35e386 --- /dev/null +++ b/vercel/resource_access_group_project.go @@ -0,0 +1,339 @@ +package vercel + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "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-log/tflog" + "github.com/vercel/terraform-provider-vercel/v2/client" +) + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ resource.Resource = &accessGroupProjectResource{} + _ resource.ResourceWithConfigure = &accessGroupProjectResource{} + _ resource.ResourceWithImportState = &accessGroupProjectResource{} +) + +func newAccessGroupProjectResource() resource.Resource { + return &accessGroupProjectResource{} +} + +type accessGroupProjectResource struct { + client *client.Client +} + +func (r *accessGroupProjectResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_access_group_project" +} + +func (r *accessGroupProjectResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + // Prevent panic if the provider has not been configured. + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(*client.Client) + if !ok { + resp.Diagnostics.AddError( + "Unexpected Resource Configure Type", + fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return + } + + r.client = client +} + +func (r *accessGroupProjectResource) Schema(_ context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: ` +Provides an Access Group Project Resource. + +An Access Group Project resource defines the relationship between a ` + "`vercel_access_group` and a `vercel_project`." + ` + +For more detailed information, please see the [Vercel documentation](https://vercel.com/docs/accounts/team-members-and-roles/access-groups). +`, + Attributes: map[string]schema.Attribute{ + "access_group_id": schema.StringAttribute{ + Description: "The ID of the Access Group.", + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + "team_id": schema.StringAttribute{ + Optional: true, + Computed: true, + Description: "The ID of the team the access group project should exist under. Required when configuring a team resource if a default team has not been set in the provider.", + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplaceIfConfigured(), stringplanmodifier.UseStateForUnknown()}, + }, + "project_id": schema.StringAttribute{ + Required: true, + Description: "The Project ID to assign to the access group.", + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + "role": schema.StringAttribute{ + Description: "The project role to assign to the access group. Must be either `ADMIN`, `PROJECT_DEVELOPER`, or `PROJECT_VIEWER`.", + Required: true, + Validators: []validator.String{ + stringvalidator.OneOf("ADMIN", "PROJECT_DEVELOPER", "PROJECT_VIEWER"), + }, + }, + }, + } +} + +type AccessGroupProject struct { + AccessGroupID types.String `tfsdk:"access_group_id"` + TeamID types.String `tfsdk:"team_id"` + ProjectID types.String `tfsdk:"project_id"` + Role types.String `tfsdk:"role"` +} + +func (r *accessGroupProjectResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan AccessGroupProject + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + out, err := r.client.CreateAccessGroupProject(ctx, client.CreateAccessGroupProjectRequest{ + TeamID: plan.TeamID.ValueString(), + AccessGroupID: plan.AccessGroupID.ValueString(), + ProjectID: plan.ProjectID.ValueString(), + Role: plan.Role.ValueString(), + }) + + if err != nil { + resp.Diagnostics.AddError( + "Error creating Access Group Project", + fmt.Sprintf("Could not create Access Group Project %s %s %s, unexpected error: %s", + plan.TeamID.ValueString(), + plan.AccessGroupID.ValueString(), + plan.ProjectID.ValueString(), + err, + ), + ) + return + } + + result := AccessGroupProject{ + TeamID: types.StringValue(out.TeamID), + AccessGroupID: types.StringValue(out.AccessGroupID), + ProjectID: types.StringValue(out.ProjectID), + Role: types.StringValue(out.Role), + } + + tflog.Info(ctx, "created Access Group", map[string]interface{}{ + "team_id": result.TeamID.ValueString(), + "access_group_id": result.AccessGroupID.ValueString(), + "project_id": result.ProjectID.ValueString(), + "role": result.Role.ValueString(), + }) + + diags = resp.State.Set(ctx, result) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +func (r *accessGroupProjectResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state AccessGroupProject + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + out, err := r.client.GetAccessGroupProject(ctx, client.GetAccessGroupProjectRequest{ + AccessGroupID: state.AccessGroupID.ValueString(), + TeamID: state.TeamID.ValueString(), + ProjectID: state.ProjectID.ValueString(), + }) + if client.NotFound(err) { + resp.State.RemoveResource(ctx) + return + } + if err != nil { + resp.Diagnostics.AddError( + "Error reading Access Group Project", + fmt.Sprintf("Could not get Access Group Project %s %s %s, unexpected error: %s", + state.TeamID.ValueString(), + state.AccessGroupID.ValueString(), + state.ProjectID.ValueString(), + err, + ), + ) + return + } + + result := AccessGroupProject{ + TeamID: types.StringValue(out.TeamID), + AccessGroupID: types.StringValue(out.AccessGroupID), + ProjectID: types.StringValue(out.ProjectID), + Role: types.StringValue(out.Role), + } + tflog.Info(ctx, "read Access Group Project", map[string]interface{}{ + "team_id": state.TeamID.ValueString(), + "access_group_id": state.AccessGroupID.ValueString(), + "project_id": state.ProjectID.ValueString(), + "role": state.Role.ValueString(), + }) + + diags = resp.State.Set(ctx, result) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +func (r *accessGroupProjectResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan AccessGroupProject + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + out, err := r.client.UpdateAccessGroupProject(ctx, client.UpdateAccessGroupProjectRequest{ + TeamID: plan.TeamID.ValueString(), + AccessGroupID: plan.AccessGroupID.ValueString(), + ProjectID: plan.ProjectID.ValueString(), + Role: plan.Role.ValueString(), + }) + + if err != nil { + resp.Diagnostics.AddError( + "Error updating Access Group Project", + fmt.Sprintf("Could not create Access Group Project %s %s %s, unexpected error: %s", + plan.TeamID.ValueString(), + plan.AccessGroupID.ValueString(), + plan.ProjectID.ValueString(), + err, + ), + ) + return + } + + result := AccessGroupProject{ + TeamID: types.StringValue(out.TeamID), + AccessGroupID: types.StringValue(out.AccessGroupID), + ProjectID: types.StringValue(out.ProjectID), + Role: types.StringValue(out.Role), + } + + tflog.Info(ctx, "updated Access Group Project", map[string]interface{}{ + "team_id": result.TeamID.ValueString(), + "access_group_id": result.AccessGroupID.ValueString(), + "project_id": result.ProjectID.ValueString(), + "role": result.Role.ValueString(), + }) + + diags = resp.State.Set(ctx, result) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +func (r *accessGroupProjectResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state AccessGroupProject + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + err := r.client.DeleteAccessGroupProject(ctx, client.DeleteAccessGroupProjectRequest{ + TeamID: state.TeamID.ValueString(), + AccessGroupID: state.AccessGroupID.ValueString(), + ProjectID: state.ProjectID.ValueString(), + }) + + if client.NotFound(err) { + return + } + + if err != nil { + resp.Diagnostics.AddError( + "Error deleting Access Group Project", + fmt.Sprintf( + "Could not delete Access Group %s %s %s, unexpected error: %s", + state.TeamID.ValueString(), + state.AccessGroupID.ValueString(), + state.ProjectID.ValueString(), + err, + ), + ) + return + } + + tflog.Info(ctx, "deleted Access Group", map[string]interface{}{ + "team_id": state.TeamID.ValueString(), + "access_group_id": state.AccessGroupID.ValueString(), + "project_id": state.ProjectID.ValueString(), + "role": state.Role.ValueString(), + }) +} + +func (r *accessGroupProjectResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + teamID, accessGroupID, projectID, ok := splitInto2Or3(req.ID) + if !ok { + resp.Diagnostics.AddError( + "Error importing Access Group", + fmt.Sprintf("Invalid id '%s' specified. should be in format \"team_id/access_group_id/project_id\" or \"access_group_id/project_id\"", req.ID), + ) + } + + out, err := r.client.GetAccessGroupProject(ctx, client.GetAccessGroupProjectRequest{ + TeamID: teamID, + AccessGroupID: accessGroupID, + ProjectID: projectID, + }) + + if client.NotFound(err) { + resp.State.RemoveResource(ctx) + return + } + + if err != nil { + resp.Diagnostics.AddError( + "Error reading Access Group Project", + fmt.Sprintf("Could not get Accesss Group %s %s %s, unexpected error: %s", + teamID, + accessGroupID, + projectID, + err, + ), + ) + return + } + + result := AccessGroupProject{ + TeamID: types.StringValue(out.TeamID), + AccessGroupID: types.StringValue(out.AccessGroupID), + ProjectID: types.StringValue(out.ProjectID), + Role: types.StringValue(out.Role), + } + + tflog.Info(ctx, "import Access Group Project", map[string]interface{}{ + "team_id": result.TeamID.ValueString(), + "access_group_id": result.AccessGroupID.ValueString(), + "project_id": result.ProjectID.ValueString(), + "role": result.Role.ValueString(), + }) + + diags := resp.State.Set(ctx, result) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} diff --git a/vercel/resource_access_group_project_test.go b/vercel/resource_access_group_project_test.go new file mode 100644 index 00000000..b50279bd --- /dev/null +++ b/vercel/resource_access_group_project_test.go @@ -0,0 +1,144 @@ +package vercel_test + +import ( + "context" + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/vercel/terraform-provider-vercel/v2/client" +) + +func TestAcc_AccessGroupProjectResource(t *testing.T) { + name := acctest.RandString(16) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + CheckDestroy: testAccAccessGroupProjectDoesNotExist("vercel_access_group_project.test"), + Steps: []resource.TestStep{ + { + Config: testAccResourceAccessGroupProject(name), + Check: resource.ComposeAggregateTestCheckFunc( + testCheckAccessGroupProjectExists("vercel_access_group_project.test"), + resource.TestCheckResourceAttrSet("vercel_access_group_project.test", "access_group_id"), + resource.TestCheckResourceAttrSet("vercel_access_group_project.test", "project_id"), + resource.TestCheckResourceAttr("vercel_access_group_project.test", "role", "ADMIN"), + ), + }, + { + ResourceName: "vercel_access_group_project.test", + ImportState: true, + ImportStateIdFunc: getAccessGroupProjectImportID("vercel_access_group_project.test"), + }, + { + Config: testAccResourceAccessGroupProjectUpdated(name), + Check: resource.ComposeAggregateTestCheckFunc( + testCheckAccessGroupProjectExists("vercel_access_group_project.test"), + resource.TestCheckResourceAttrSet("vercel_access_group_project.test", "project_id"), + resource.TestCheckResourceAttrSet("vercel_access_group_project.test", "access_group_id"), + resource.TestCheckResourceAttr("vercel_access_group_project.test", "role", "PROJECT_DEVELOPER"), + ), + }, + }, + }) +} + +func getAccessGroupProjectImportID(n string) resource.ImportStateIdFunc { + return func(s *terraform.State) (string, error) { + rs, ok := s.RootModule().Resources[n] + if !ok { + return "", fmt.Errorf("not found: %s", n) + } + + return fmt.Sprintf( + "%s/%s/%s", + rs.Primary.Attributes["team_id"], + rs.Primary.Attributes["access_group_id"], + rs.Primary.Attributes["project_id"], + ), nil + } +} + +func testCheckAccessGroupProjectExists(n string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("not found: %s", n) + } + + _, err := testClient().GetAccessGroupProject(context.TODO(), client.GetAccessGroupProjectRequest{ + TeamID: testTeam(), + AccessGroupID: rs.Primary.Attributes["access_group_id"], + ProjectID: rs.Primary.Attributes["project_id"], + }) + return err + } +} + +func testAccAccessGroupProjectDoesNotExist(n string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("not found: %s", n) + } + + _, err := testClient().GetAccessGroupProject(context.TODO(), client.GetAccessGroupProjectRequest{ + TeamID: testTeam(), + AccessGroupID: rs.Primary.Attributes["access_group_id"], + ProjectID: rs.Primary.Attributes["project_id"], + }) + if err == nil { + return fmt.Errorf("expected not_found error, but got no error") + } + if !client.NotFound(err) { + return fmt.Errorf("Unexpected error checking for deleted access group: %s", err) + } + + return nil + } +} + +func testAccResourceAccessGroupProject(name string) string { + return fmt.Sprintf(` +resource "vercel_project" "test" { + %[1]s + name = "test-acc-%[2]s" +} + +resource "vercel_access_group" "test" { + %[1]s + name = "test-acc-%[2]s" +} + +resource "vercel_access_group_project" "test" { + %[1]s + project_id = vercel_project.test.id + access_group_id = vercel_access_group.test.id + role = "ADMIN" +} +`, teamIDConfig(), name) +} + +func testAccResourceAccessGroupProjectUpdated(name string) string { + return fmt.Sprintf(` +resource "vercel_project" "test" { + %[1]s + name = "test-acc-%[2]s" +} + +resource "vercel_access_group" "test" { + %[1]s + name = "test-acc-%[2]s" +} + +resource "vercel_access_group_project" "test" { + %[1]s + project_id = vercel_project.test.id + access_group_id = vercel_access_group.test.id + role = "PROJECT_DEVELOPER" +} +`, teamIDConfig(), name) +} diff --git a/vercel/resource_access_group_test.go b/vercel/resource_access_group_test.go new file mode 100644 index 00000000..0b9248b7 --- /dev/null +++ b/vercel/resource_access_group_test.go @@ -0,0 +1,128 @@ +package vercel_test + +import ( + "context" + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/vercel/terraform-provider-vercel/v2/client" +) + +func TestAcc_AccessGroupResource(t *testing.T) { + name := acctest.RandString(16) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + CheckDestroy: resource.ComposeAggregateTestCheckFunc( + testCheckAccessGroupDoesNotExist("vercel_access_group.test"), + ), + Steps: []resource.TestStep{ + { + Config: testAccResourceAccessGroup(name), + Check: resource.ComposeAggregateTestCheckFunc( + testCheckAccessGroupExists("vercel_access_group.test"), + resource.TestCheckResourceAttrSet("vercel_access_group.test", "id"), + resource.TestCheckResourceAttr("vercel_access_group.test", "name", fmt.Sprintf("test-acc-%s", name)), + ), + }, + { + ResourceName: "vercel_access_group.test", + ImportState: true, + ImportStateIdFunc: getAccessGroupImportID("vercel_access_group.test"), + }, + { + Config: testAccResourceAccessGroupUpdated(name), + Check: resource.ComposeAggregateTestCheckFunc( + testCheckAccessGroupExists("vercel_access_group.test"), + resource.TestCheckResourceAttrSet("vercel_access_group.test", "id"), + resource.TestCheckResourceAttr("vercel_access_group.test", "name", fmt.Sprintf("test-acc-%s-updated", name)), + ), + }, + }, + }) +} + +func getAccessGroupImportID(n string) resource.ImportStateIdFunc { + return func(s *terraform.State) (string, error) { + rs, ok := s.RootModule().Resources[n] + if !ok { + return "", fmt.Errorf("not found: %s", n) + } + + if rs.Primary.ID == "" { + return "", fmt.Errorf("no ID is set") + } + + return fmt.Sprintf( + "%s/%s", + rs.Primary.Attributes["team_id"], + rs.Primary.Attributes["access_group_id"], + ), nil + } +} + +func testCheckAccessGroupExists(n string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("no ID is set") + } + + _, err := testClient().GetAccessGroup(context.TODO(), client.GetAccessGroupRequest{ + TeamID: testTeam(), + AccessGroupID: rs.Primary.ID, + }) + return err + } +} + +func testCheckAccessGroupDoesNotExist(n string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("no ID is set") + } + + _, err := testClient().GetAccessGroup(context.TODO(), client.GetAccessGroupRequest{ + TeamID: testTeam(), + AccessGroupID: rs.Primary.ID, + }) + if err == nil { + return fmt.Errorf("expected not_found error, but got no error") + } + if !client.NotFound(err) { + return fmt.Errorf("Unexpected error checking for deleted access group: %s", err) + } + + return nil + } +} + +func testAccResourceAccessGroup(name string) string { + return fmt.Sprintf(` +resource "vercel_access_group" "test" { + %[1]s + name = "test-acc-%[2]s" +} +`, teamIDConfig(), name) +} + +func testAccResourceAccessGroupUpdated(name string) string { + return fmt.Sprintf(` +resource "vercel_access_group" "test" { + %[1]s + name = "test-acc-%[2]s-updated" +} +`, teamIDConfig(), name) +}