diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index baf2c7b7..e19d7d0e 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -102,6 +102,7 @@ jobs:
VERCEL_TERRAFORM_TESTING_GITLAB_REPO: "dglsparsons/test"
VERCEL_TERRAFORM_TESTING_BITBUCKET_REPO: "dglsparsons-test/test"
VERCEL_TERRAFORM_TESTING_DOMAIN: "dgls.dev"
+ VERCEL_TERRAFORM_TESTING_ADDITIONAL_USER: ${{ secrets.VERCEL_TERRAFORM_TESTING_ADDITIONAL_USER }}
run: |
go test ./...
diff --git a/client/request.go b/client/request.go
index e5bb3509..50a60241 100644
--- a/client/request.go
+++ b/client/request.go
@@ -111,7 +111,6 @@ func (c *Client) _doRequest(req *http.Request, v interface{}, errorOnNoContent b
defer resp.Body.Close()
responseBody, err := io.ReadAll(resp.Body)
-
if err != nil {
return fmt.Errorf("error reading response body: %w", err)
}
diff --git a/client/team_member.go b/client/team_member.go
new file mode 100644
index 00000000..c5d142a3
--- /dev/null
+++ b/client/team_member.go
@@ -0,0 +1,148 @@
+package client
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/hashicorp/terraform-plugin-log/tflog"
+)
+
+type ProjectRole struct {
+ ProjectID string `json:"projectId"`
+ Role string `json:"role"`
+}
+
+type TeamMemberInviteRequest struct {
+ UserID string `json:"uid,omitempty"`
+ Email string `json:"email,omitempty"`
+ Role string `json:"role,omitempty"`
+ Projects []ProjectRole `json:"projects,omitempty"`
+ AccessGroups []string `json:"accessGroups,omitempty"`
+ TeamID string `json:"-"`
+}
+
+func (c *Client) InviteTeamMember(ctx context.Context, request TeamMemberInviteRequest) error {
+ url := fmt.Sprintf("%s/v1/teams/%s/members", c.baseURL, request.TeamID)
+ tflog.Info(ctx, "inviting user", map[string]interface{}{
+ "url": url,
+ "user": request.UserID,
+ "email": request.Email,
+ "role": request.Role,
+ })
+
+ err := c.doRequest(clientRequest{
+ ctx: ctx,
+ method: "POST",
+ url: url,
+ body: string(mustMarshal(request)),
+ }, nil)
+ return err
+}
+
+type TeamMemberRemoveRequest struct {
+ UserID string
+ TeamID string
+}
+
+func (c *Client) RemoveTeamMember(ctx context.Context, request TeamMemberRemoveRequest) error {
+ url := fmt.Sprintf("%s/v2/teams/%s/members/%s", c.baseURL, request.TeamID, request.UserID)
+ tflog.Info(ctx, "removing user", map[string]interface{}{
+ "url": url,
+ "user": request.UserID,
+ })
+ err := c.doRequest(clientRequest{
+ ctx: ctx,
+ method: "DELETE",
+ url: url,
+ body: "",
+ }, nil)
+ return err
+}
+
+type TeamMemberUpdateRequest struct {
+ UserID string `json:"-"`
+ Role string `json:"role"`
+ TeamID string `json:"-"`
+ Projects []ProjectRole `json:"projects,omitempty"`
+ AccessGroupsToAdd []string `json:"accessGroupsToAdd,omitempty"`
+ AccessGroupsToRemove []string `json:"accessGroupsToRemove,omitempty"`
+}
+
+func (c *Client) UpdateTeamMember(ctx context.Context, request TeamMemberUpdateRequest) error {
+ url := fmt.Sprintf("%s/v1/teams/%s/members/%s", c.baseURL, request.TeamID, request.UserID)
+ tflog.Info(ctx, "updating team member", map[string]interface{}{
+ "url": url,
+ "user": request.UserID,
+ "role": request.Role,
+ })
+ err := c.doRequest(clientRequest{
+ ctx: ctx,
+ method: "PATCH",
+ url: url,
+ body: string(mustMarshal(request)),
+ }, nil)
+ return err
+}
+
+type GetTeamMemberRequest struct {
+ TeamID string
+ UserID string
+}
+
+type TeamMember struct {
+ Confirmed bool `json:"confirmed"`
+ Role string `json:"role"`
+ UserID string `json:"uid"`
+ Projects []ProjectRole `json:"projects"`
+ AccessGroups []struct {
+ ID string `json:"id"`
+ Name string `json:"name"`
+ } `json:"accessGroups"`
+}
+
+func (c *Client) GetTeamMember(ctx context.Context, request GetTeamMemberRequest) (TeamMember, error) {
+ url := fmt.Sprintf("%s/v2/teams/%s/members?limit=1&filterByUserIds=%s", c.baseURL, request.TeamID, request.UserID)
+ tflog.Info(ctx, "getting team member", map[string]interface{}{
+ "url": url,
+ })
+
+ var response struct {
+ Members []TeamMember `json:"members"`
+ }
+ err := c.doRequest(clientRequest{
+ ctx: ctx,
+ method: "GET",
+ url: url,
+ body: "",
+ }, &response)
+ if err != nil {
+ return TeamMember{}, err
+ }
+ if len(response.Members) == 0 {
+ return TeamMember{}, APIError{
+ StatusCode: 404,
+ Message: "Team member not found",
+ Code: "not_found",
+ }
+ }
+
+ // Now look up the projects for the member, but only if we need to.
+ if !response.Members[0].Confirmed {
+ return response.Members[0], nil
+ }
+ url = fmt.Sprintf("%s/v1/teams/%s/members/%s/projects?limit=100", c.baseURL, request.TeamID, request.UserID)
+ var response2 struct {
+ Projects []ProjectRole `json:"projects"`
+ }
+ err = c.doRequest(clientRequest{
+ ctx: ctx,
+ method: "GET",
+ url: url,
+ body: "",
+ }, &response2)
+ if err != nil {
+ return TeamMember{}, err
+ }
+ response.Members[0].Projects = response2.Projects
+ return response.Members[0], err
+}
diff --git a/docs/data-sources/access_group.md b/docs/data-sources/access_group.md
index b3272641..e5daeee4 100644
--- a/docs/data-sources/access_group.md
+++ b/docs/data-sources/access_group.md
@@ -13,7 +13,13 @@ 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).
+## Example Usage
+```terraform
+data "vercel_access_group" "example" {
+ id = "ag_xxxxxxxxxxxxxxxxxxxxxxxxxxxx"
+}
+```
## Schema
diff --git a/docs/data-sources/access_group_project.md b/docs/data-sources/access_group_project.md
index 66fef0f0..66d9bc39 100644
--- a/docs/data-sources/access_group_project.md
+++ b/docs/data-sources/access_group_project.md
@@ -13,7 +13,18 @@ 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).
-
+## Example Usage
+
+```terraform
+data "vercel_project" "example" {
+ name = "my-existing-project"
+}
+
+data "vercel_access_group_project" "example" {
+ access_group_id = "ag_xxxxxxxxxxxxxxxxxxxxxxxxxxxx"
+ project_id = vercel_project.example.id
+}
+```
## Schema
diff --git a/docs/data-sources/project.md b/docs/data-sources/project.md
index 4991a707..7fd4096d 100644
--- a/docs/data-sources/project.md
+++ b/docs/data-sources/project.md
@@ -19,14 +19,9 @@ For more detailed information, please see the [Vercel documentation](https://ver
## Example Usage
```terraform
-data "vercel_project" "foo" {
+data "vercel_project" "example" {
name = "my-existing-project"
}
-
-# Outputs prj_xxxxxx
-output "project_id" {
- value = data.vercel_project.foo.id
-}
```
diff --git a/docs/data-sources/team_member.md b/docs/data-sources/team_member.md
new file mode 100644
index 00000000..966d24db
--- /dev/null
+++ b/docs/data-sources/team_member.md
@@ -0,0 +1,43 @@
+---
+# generated by https://github.com/hashicorp/terraform-plugin-docs
+page_title: "vercel_team_member Data Source - terraform-provider-vercel"
+subcategory: ""
+description: |-
+ Provider a datasource for managing a team member.
+---
+
+# vercel_team_member (Data Source)
+
+Provider a datasource for managing a team member.
+
+## Example Usage
+
+```terraform
+data "vercel_team_member" "example" {
+ user_id = "uuuuuuuuuuuuuuuuuuuuuuuuuu"
+ team_id = "team_xxxxxxxxxxxxxxxxxxxxxxxx"
+}
+```
+
+
+## Schema
+
+### Required
+
+- `team_id` (String) The ID of the existing Vercel Team.
+- `user_id` (String) The ID of the existing Vercel Team Member.
+
+### Read-Only
+
+- `access_groups` (Set of String) If access groups are enabled on the team, and the user is a CONTRIBUTOR, `projects`, `access_groups` or both must be specified. A set of access groups IDs that the user should be granted access to.
+- `id` (String) The ID of this resource.
+- `projects` (Attributes Set) If access groups are enabled on the team, and the user is a CONTRIBUTOR, `projects`, `access_groups` or both must be specified. A set of projects that the user should be granted access to, along with their role in each project. (see [below for nested schema](#nestedatt--projects))
+- `role` (String) The role that the user should have in the project. One of 'MEMBER', 'OWNER', 'VIEWER', 'DEVELOPER', 'BILLING' or 'CONTRIBUTOR'. Depending on your Team's plan, some of these roles may be unavailable.
+
+
+### Nested Schema for `projects`
+
+Required:
+
+- `project_id` (String) The ID of the project that the user should be granted access to.
+- `role` (String) The role that the user should have in the project.
diff --git a/docs/resources/access_group.md b/docs/resources/access_group.md
index d5d5a82e..0821bcbe 100644
--- a/docs/resources/access_group.md
+++ b/docs/resources/access_group.md
@@ -16,7 +16,13 @@ Access Groups provide a way to manage groups of Vercel users across projects on
For more detailed information, please see the [Vercel documentation](https://vercel.com/docs/accounts/team-members-and-roles/access-groups).
+## Example Usage
+```terraform
+resource "vercel_access_group" "example" {
+ name = "example-access-group"
+}
+```
## Schema
@@ -32,3 +38,16 @@ For more detailed information, please see the [Vercel documentation](https://ver
### Read-Only
- `id` (String) The ID of the Access Group.
+
+## Import
+
+Import is supported using the following syntax:
+
+```shell
+# If importing into a personal account, or with a team configured on
+# the provider, simply use the access_group_id.
+terraform import vercel_access_group.example ag_xxxxxxxxxxxxxxxxxxxxxxxxxxxx
+
+# If importing to a team, use the team_id and access_group_id.
+terraform import vercel_access_group.example team_xxxxxxxxxxxxxxxxxxxxxxxx/ag_xxxxxxxxxxxxxxxxxxxxxxxxxxxx
+```
diff --git a/docs/resources/access_group_project.md b/docs/resources/access_group_project.md
index 8117e635..72c801e3 100644
--- a/docs/resources/access_group_project.md
+++ b/docs/resources/access_group_project.md
@@ -16,7 +16,23 @@ An Access Group Project resource defines the relationship between a `vercel_acce
For more detailed information, please see the [Vercel documentation](https://vercel.com/docs/accounts/team-members-and-roles/access-groups).
+## Example Usage
+```terraform
+resource "vercel_project" "example" {
+ name = "example-project"
+}
+
+resource "vercel_access_group" "example" {
+ name = "example-access-group"
+}
+
+resource "vercel_access_group_project" "example" {
+ project_id = vercel_project.example.id
+ access_group_id = vercel_access_group.example.id
+ role = "ADMIN"
+}
+```
## Schema
@@ -30,3 +46,16 @@ For more detailed information, please see the [Vercel documentation](https://ver
### 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.
+
+## Import
+
+Import is supported using the following syntax:
+
+```shell
+# If importing into a personal account, or with a team configured on
+# the provider, use the access_group_id and project_id.
+terraform import vercel_access_group.example ag_xxxxxxxxxxxxxxxxxxxxxxxxxxxx/prj_xxxxxxxxxxxxxxxxxxxxxxxxxxxx
+
+# If importing to a team, use the team_id, access_group_id and project_id.
+terraform import vercel_access_group.example team_xxxxxxxxxxxxxxxxxxxxxxxx/ag_xxxxxxxxxxxxxxxxxxxxxxxxxxxx/prj_xxxxxxxxxxxxxxxxxxxxxxxxxxxx
+```
diff --git a/docs/resources/log_drain.md b/docs/resources/log_drain.md
index 410feb97..0bed54dd 100644
--- a/docs/resources/log_drain.md
+++ b/docs/resources/log_drain.md
@@ -78,10 +78,10 @@ Import is supported using the following syntax:
# If importing into a personal account, or with a team configured on
# the provider, simply use the log_drain_id.
# - log_drain_id can be found by querying the Vercel REST API (https://vercel.com/docs/rest-api/endpoints/logDrains#retrieves-a-list-of-all-the-log-drains).
-terraform import vercel_log_drain.example ecfg_xxxxxxxxxxxxxxxxxxxxxxxxxxxx/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
+terraform import vercel_log_drain.example ld_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# Alternatively, you can import via the team_id and edge_config_id.
# - team_id can be found in the team `settings` tab in the Vercel UI.
# - log_drain_id can be found by querying the Vercel REST API (https://vercel.com/docs/rest-api/endpoints/logDrains#retrieves-a-list-of-all-the-log-drains).
-terraform import vercel_log_drain.example team_xxxxxxxxxxxxxxxxxxxxxxxx/ecfg_xxxxxxxxxxxxxxxxxxxxxxxxxxxx/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
+terraform import vercel_log_drain.example team_xxxxxxxxxxxxxxxxxxxxxxxx/ld_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```
diff --git a/docs/resources/team_member.md b/docs/resources/team_member.md
new file mode 100644
index 00000000..4ac60bf1
--- /dev/null
+++ b/docs/resources/team_member.md
@@ -0,0 +1,52 @@
+---
+# generated by https://github.com/hashicorp/terraform-plugin-docs
+page_title: "vercel_team_member Resource - terraform-provider-vercel"
+subcategory: ""
+description: |-
+ Provider a resource for managing a team member.
+---
+
+# vercel_team_member (Resource)
+
+Provider a resource for managing a team member.
+
+## Example Usage
+
+```terraform
+resource "vercel_team_member" "example" {
+ team_id = "team_xxxxxxxxxxxxxxxxxxxxxxxx"
+ user_id = "uuuuuuuuuuuuuuuuuuuuuuuuuu"
+ role = "MEMBER"
+}
+```
+
+
+## Schema
+
+### Required
+
+- `role` (String) The role that the user should have in the project. One of 'MEMBER', 'OWNER', 'VIEWER', 'DEVELOPER', 'BILLING' or 'CONTRIBUTOR'. Depending on your Team's plan, some of these roles may be unavailable.
+- `team_id` (String) The ID of the existing Vercel Team.
+- `user_id` (String) The ID of the user to add to the team.
+
+### Optional
+
+- `access_groups` (Set of String) If access groups are enabled on the team, and the user is a CONTRIBUTOR, `projects`, `access_groups` or both must be specified. A set of access groups IDs that the user should be granted access to.
+- `projects` (Attributes Set) If access groups are enabled on the team, and the user is a CONTRIBUTOR, `projects`, `access_groups` or both must be specified. A set of projects that the user should be granted access to, along with their role in each project. (see [below for nested schema](#nestedatt--projects))
+
+
+### Nested Schema for `projects`
+
+Required:
+
+- `project_id` (String) The ID of the project that the user should be granted access to.
+- `role` (String) The role that the user should have in the project.
+
+## Import
+
+Import is supported using the following syntax:
+
+```shell
+# To import, use the team_id and user_id.
+terraform import vercel_access_group.example team_xxxxxxxxxxxxxxxxxxxxxxxx/uuuuuuuuuuuuuuuuuuuuuuuuuu
+```
diff --git a/examples/data-sources/vercel_access_group/data-source.tf b/examples/data-sources/vercel_access_group/data-source.tf
new file mode 100644
index 00000000..5b8b4229
--- /dev/null
+++ b/examples/data-sources/vercel_access_group/data-source.tf
@@ -0,0 +1,3 @@
+data "vercel_access_group" "example" {
+ id = "ag_xxxxxxxxxxxxxxxxxxxxxxxxxxxx"
+}
diff --git a/examples/data-sources/vercel_access_group_project/data-source.tf b/examples/data-sources/vercel_access_group_project/data-source.tf
new file mode 100644
index 00000000..2766164c
--- /dev/null
+++ b/examples/data-sources/vercel_access_group_project/data-source.tf
@@ -0,0 +1,8 @@
+data "vercel_project" "example" {
+ name = "my-existing-project"
+}
+
+data "vercel_access_group_project" "example" {
+ access_group_id = "ag_xxxxxxxxxxxxxxxxxxxxxxxxxxxx"
+ project_id = vercel_project.example.id
+}
diff --git a/examples/data-sources/vercel_project/data-source.tf b/examples/data-sources/vercel_project/data-source.tf
index 3ef1e145..958d2bff 100644
--- a/examples/data-sources/vercel_project/data-source.tf
+++ b/examples/data-sources/vercel_project/data-source.tf
@@ -1,8 +1,3 @@
-data "vercel_project" "foo" {
+data "vercel_project" "example" {
name = "my-existing-project"
}
-
-# Outputs prj_xxxxxx
-output "project_id" {
- value = data.vercel_project.foo.id
-}
diff --git a/examples/data-sources/vercel_team_member/data-source.tf b/examples/data-sources/vercel_team_member/data-source.tf
new file mode 100644
index 00000000..296e9d12
--- /dev/null
+++ b/examples/data-sources/vercel_team_member/data-source.tf
@@ -0,0 +1,4 @@
+data "vercel_team_member" "example" {
+ user_id = "uuuuuuuuuuuuuuuuuuuuuuuuuu"
+ team_id = "team_xxxxxxxxxxxxxxxxxxxxxxxx"
+}
diff --git a/examples/resources/vercel_access_group/import.sh b/examples/resources/vercel_access_group/import.sh
new file mode 100644
index 00000000..0654d9fa
--- /dev/null
+++ b/examples/resources/vercel_access_group/import.sh
@@ -0,0 +1,6 @@
+# If importing into a personal account, or with a team configured on
+# the provider, simply use the access_group_id.
+terraform import vercel_access_group.example ag_xxxxxxxxxxxxxxxxxxxxxxxxxxxx
+
+# If importing to a team, use the team_id and access_group_id.
+terraform import vercel_access_group.example team_xxxxxxxxxxxxxxxxxxxxxxxx/ag_xxxxxxxxxxxxxxxxxxxxxxxxxxxx
diff --git a/examples/resources/vercel_access_group/resource.tf b/examples/resources/vercel_access_group/resource.tf
new file mode 100644
index 00000000..2be60c84
--- /dev/null
+++ b/examples/resources/vercel_access_group/resource.tf
@@ -0,0 +1,3 @@
+resource "vercel_access_group" "example" {
+ name = "example-access-group"
+}
diff --git a/examples/resources/vercel_access_group_project/import.sh b/examples/resources/vercel_access_group_project/import.sh
new file mode 100644
index 00000000..c3e94465
--- /dev/null
+++ b/examples/resources/vercel_access_group_project/import.sh
@@ -0,0 +1,6 @@
+# If importing into a personal account, or with a team configured on
+# the provider, use the access_group_id and project_id.
+terraform import vercel_access_group.example ag_xxxxxxxxxxxxxxxxxxxxxxxxxxxx/prj_xxxxxxxxxxxxxxxxxxxxxxxxxxxx
+
+# If importing to a team, use the team_id, access_group_id and project_id.
+terraform import vercel_access_group.example team_xxxxxxxxxxxxxxxxxxxxxxxx/ag_xxxxxxxxxxxxxxxxxxxxxxxxxxxx/prj_xxxxxxxxxxxxxxxxxxxxxxxxxxxx
diff --git a/examples/resources/vercel_access_group_project/resource.tf b/examples/resources/vercel_access_group_project/resource.tf
new file mode 100644
index 00000000..d760ac38
--- /dev/null
+++ b/examples/resources/vercel_access_group_project/resource.tf
@@ -0,0 +1,13 @@
+resource "vercel_project" "example" {
+ name = "example-project"
+}
+
+resource "vercel_access_group" "example" {
+ name = "example-access-group"
+}
+
+resource "vercel_access_group_project" "example" {
+ project_id = vercel_project.example.id
+ access_group_id = vercel_access_group.example.id
+ role = "ADMIN"
+}
diff --git a/examples/resources/vercel_log_drain/import.sh b/examples/resources/vercel_log_drain/import.sh
index bca3b2fc..7bc91948 100644
--- a/examples/resources/vercel_log_drain/import.sh
+++ b/examples/resources/vercel_log_drain/import.sh
@@ -1,9 +1,9 @@
# If importing into a personal account, or with a team configured on
# the provider, simply use the log_drain_id.
# - log_drain_id can be found by querying the Vercel REST API (https://vercel.com/docs/rest-api/endpoints/logDrains#retrieves-a-list-of-all-the-log-drains).
-terraform import vercel_log_drain.example ecfg_xxxxxxxxxxxxxxxxxxxxxxxxxxxx/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
+terraform import vercel_log_drain.example ld_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# Alternatively, you can import via the team_id and edge_config_id.
# - team_id can be found in the team `settings` tab in the Vercel UI.
# - log_drain_id can be found by querying the Vercel REST API (https://vercel.com/docs/rest-api/endpoints/logDrains#retrieves-a-list-of-all-the-log-drains).
-terraform import vercel_log_drain.example team_xxxxxxxxxxxxxxxxxxxxxxxx/ecfg_xxxxxxxxxxxxxxxxxxxxxxxxxxxx/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
+terraform import vercel_log_drain.example team_xxxxxxxxxxxxxxxxxxxxxxxx/ld_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
diff --git a/examples/resources/vercel_team_member/import.sh b/examples/resources/vercel_team_member/import.sh
new file mode 100644
index 00000000..1efe8a13
--- /dev/null
+++ b/examples/resources/vercel_team_member/import.sh
@@ -0,0 +1,2 @@
+# To import, use the team_id and user_id.
+terraform import vercel_access_group.example team_xxxxxxxxxxxxxxxxxxxxxxxx/uuuuuuuuuuuuuuuuuuuuuuuuuu
diff --git a/examples/resources/vercel_team_member/resource.tf b/examples/resources/vercel_team_member/resource.tf
new file mode 100644
index 00000000..aa3cf9f0
--- /dev/null
+++ b/examples/resources/vercel_team_member/resource.tf
@@ -0,0 +1,5 @@
+resource "vercel_team_member" "example" {
+ team_id = "team_xxxxxxxxxxxxxxxxxxxxxxxx"
+ user_id = "uuuuuuuuuuuuuuuuuuuuuuuuuu"
+ role = "MEMBER"
+}
diff --git a/vercel/data_source_access_group_project_test.go b/vercel/data_source_access_group_project_test.go
index 38027947..654479cb 100644
--- a/vercel/data_source_access_group_project_test.go
+++ b/vercel/data_source_access_group_project_test.go
@@ -46,10 +46,10 @@ resource "vercel_access_group_project" "test" {
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
- ]
+ project_id = vercel_project.test.id
+ depends_on = [
+ vercel_access_group_project.test
+ ]
}
`, teamIDConfig(), name)
}
diff --git a/vercel/data_source_team_member.go b/vercel/data_source_team_member.go
new file mode 100644
index 00000000..5cff4811
--- /dev/null
+++ b/vercel/data_source_team_member.go
@@ -0,0 +1,142 @@
+package vercel
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator"
+ "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
+ "github.com/hashicorp/terraform-plugin-framework/datasource"
+ "github.com/hashicorp/terraform-plugin-framework/datasource/schema"
+ "github.com/hashicorp/terraform-plugin-framework/schema/validator"
+ "github.com/hashicorp/terraform-plugin-framework/types"
+ "github.com/vercel/terraform-provider-vercel/v2/client"
+)
+
+// Ensure the implementation satisfies the expected interfaces.
+var (
+ _ datasource.DataSource = &teamMemberDataSource{}
+ _ datasource.DataSourceWithConfigure = &teamMemberDataSource{}
+)
+
+func newTeamMemberDataSource() datasource.DataSource {
+ return &teamMemberDataSource{}
+}
+
+type teamMemberDataSource struct {
+ client *client.Client
+}
+
+func (d *teamMemberDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_team_member"
+}
+
+func (d *teamMemberDataSource) 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 DataSource Configure Type",
+ fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
+ )
+ return
+ }
+
+ d.client = client
+}
+
+func (d *teamMemberDataSource) Schema(_ context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) {
+ resp.Schema = schema.Schema{
+ Description: "Provider a datasource for managing a team member.",
+ Attributes: map[string]schema.Attribute{
+ "id": schema.StringAttribute{
+ Computed: true,
+ },
+ "team_id": schema.StringAttribute{
+ Description: "The ID of the existing Vercel Team.",
+ Required: true,
+ },
+ "user_id": schema.StringAttribute{
+ Description: "The ID of the existing Vercel Team Member.",
+ Required: true,
+ },
+ "role": schema.StringAttribute{
+ Description: "The role that the user should have in the project. One of 'MEMBER', 'OWNER', 'VIEWER', 'DEVELOPER', 'BILLING' or 'CONTRIBUTOR'. Depending on your Team's plan, some of these roles may be unavailable.",
+ Computed: true,
+ Validators: []validator.String{
+ stringvalidator.OneOf("MEMBER", "OWNER", "VIEWER", "DEVELOPER", "BILLING", "CONTRIBUTOR"),
+ },
+ },
+ "projects": schema.SetNestedAttribute{
+ Description: "If access groups are enabled on the team, and the user is a CONTRIBUTOR, `projects`, `access_groups` or both must be specified. A set of projects that the user should be granted access to, along with their role in each project.",
+ Computed: true,
+ NestedObject: schema.NestedAttributeObject{
+ Attributes: map[string]schema.Attribute{
+ "role": schema.StringAttribute{
+ Description: "The role that the user should have in the project.",
+ Required: true,
+ Validators: []validator.String{
+ stringvalidator.OneOf("ADMIN", "PROJECT_VIEWER", "PROJECT_DEVELOPER"),
+ },
+ },
+ "project_id": schema.StringAttribute{
+ Description: "The ID of the project that the user should be granted access to.",
+ Required: true,
+ },
+ },
+ },
+ },
+ "access_groups": schema.SetAttribute{
+ Description: "If access groups are enabled on the team, and the user is a CONTRIBUTOR, `projects`, `access_groups` or both must be specified. A set of access groups IDs that the user should be granted access to.",
+ Computed: true,
+ ElementType: types.StringType,
+ Validators: []validator.Set{
+ setvalidator.SizeAtLeast(1),
+ },
+ },
+ },
+ }
+}
+
+type TeamMemberWithID struct {
+ UserID types.String `tfsdk:"user_id"`
+ TeamID types.String `tfsdk:"team_id"`
+ Role types.String `tfsdk:"role"`
+ Projects types.Set `tfsdk:"projects"`
+ AccessGroups types.Set `tfsdk:"access_groups"`
+ ID types.String `tfsdk:"id"`
+}
+
+func (d *teamMemberDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
+ var config TeamMemberWithID
+ diags := req.Config.Get(ctx, &config)
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ response, err := d.client.GetTeamMember(ctx, client.GetTeamMemberRequest{
+ TeamID: config.TeamID.ValueString(),
+ UserID: config.UserID.ValueString(),
+ })
+ if err != nil {
+ resp.Diagnostics.AddError(
+ "Error reading Team Member",
+ "Could not read Team Member, unexpected error: "+err.Error(),
+ )
+ }
+ teamMember := convertResponseToTeamMember(response, config.TeamID)
+ diags = resp.State.Set(ctx, TeamMemberWithID{
+ UserID: teamMember.UserID,
+ TeamID: teamMember.TeamID,
+ Role: teamMember.Role,
+ Projects: teamMember.Projects,
+ AccessGroups: teamMember.AccessGroups,
+ ID: types.StringValue(fmt.Sprintf("%s/%s", config.TeamID.ValueString(), config.UserID.ValueString())),
+ })
+ resp.Diagnostics.Append(diags...)
+}
diff --git a/vercel/data_source_team_member_test.go b/vercel/data_source_team_member_test.go
new file mode 100644
index 00000000..d55659cb
--- /dev/null
+++ b/vercel/data_source_team_member_test.go
@@ -0,0 +1,40 @@
+package vercel_test
+
+import (
+ "fmt"
+ "testing"
+
+ "github.com/hashicorp/terraform-plugin-testing/helper/resource"
+)
+
+func TestAcc_TeamMemberDataSource(t *testing.T) {
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() { testAccPreCheck(t) },
+ ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
+ Steps: []resource.TestStep{
+ {
+ Config: testAccTeamMemberDataSourceConfig(),
+ Check: resource.ComposeAggregateTestCheckFunc(
+ resource.TestCheckResourceAttrSet("data.vercel_team_member.test", "team_id"),
+ resource.TestCheckResourceAttrSet("data.vercel_team_member.test", "user_id"),
+ resource.TestCheckResourceAttr("data.vercel_team_member.test", "role", "MEMBER"),
+ ),
+ },
+ },
+ })
+}
+
+func testAccTeamMemberDataSourceConfig() string {
+ return fmt.Sprintf(`
+resource "vercel_team_member" "test" {
+ %[1]s
+ user_id = "%s"
+ role = "MEMBER"
+}
+
+data "vercel_team_member" "test" {
+ user_id = vercel_team_member.test.user_id
+ team_id = vercel_team_member.test.team_id
+}
+`, teamIDConfig(), testAdditionalUser())
+}
diff --git a/vercel/provider.go b/vercel/provider.go
index 560d2223..25d166be 100644
--- a/vercel/provider.go
+++ b/vercel/provider.go
@@ -50,12 +50,14 @@ Use the navigation to the left to read about the available resources.
func (p *vercelProvider) Resources(_ context.Context) []func() resource.Resource {
return []func() resource.Resource{
+ newAccessGroupProjectResource,
+ newAccessGroupResource,
newAliasResource,
newAttackChallengeModeResource,
newDNSRecordResource,
newDeploymentResource,
- newEdgeConfigResource,
newEdgeConfigItemResource,
+ newEdgeConfigResource,
newEdgeConfigSchemaResource,
newEdgeConfigTokenResource,
newFirewallConfigResource,
@@ -67,14 +69,15 @@ func (p *vercelProvider) Resources(_ context.Context) []func() resource.Resource
newProjectResource,
newSharedEnvironmentVariableResource,
newTeamConfigResource,
+ newTeamMemberResource,
newWebhookResource,
- newAccessGroupResource,
- newAccessGroupProjectResource,
}
}
func (p *vercelProvider) DataSources(_ context.Context) []func() datasource.DataSource {
return []func() datasource.DataSource{
+ newAccessGroupDataSource,
+ newAccessGroupProjectDataSource,
newAliasDataSource,
newAttackChallengeModeDataSource,
newDeploymentDataSource,
@@ -91,8 +94,7 @@ func (p *vercelProvider) DataSources(_ context.Context) []func() datasource.Data
newProjectDirectoryDataSource,
newSharedEnvironmentVariableDataSource,
newTeamConfigDataSource,
- newAccessGroupDataSource,
- newAccessGroupProjectDataSource,
+ newTeamMemberDataSource,
}
}
diff --git a/vercel/provider_test.go b/vercel/provider_test.go
index 35096c24..fb78a196 100644
--- a/vercel/provider_test.go
+++ b/vercel/provider_test.go
@@ -27,6 +27,7 @@ func testAccPreCheck(t *testing.T) {
mustHaveEnv(t, "VERCEL_TERRAFORM_TESTING_GITLAB_REPO")
mustHaveEnv(t, "VERCEL_TERRAFORM_TESTING_BITBUCKET_REPO")
mustHaveEnv(t, "VERCEL_TERRAFORM_TESTING_DOMAIN")
+ mustHaveEnv(t, "VERCEL_TERRAFORM_TESTING_ADDITIONAL_USER")
}
var tc *client.Client
@@ -65,3 +66,7 @@ func teamIDConfig() string {
func testDomain() string {
return os.Getenv("VERCEL_TERRAFORM_TESTING_DOMAIN")
}
+
+func testAdditionalUser() string {
+ return os.Getenv("VERCEL_TERRAFORM_TESTING_ADDITIONAL_USER")
+}
diff --git a/vercel/resource_access_group_test.go b/vercel/resource_access_group_test.go
index 0b9248b7..a051a506 100644
--- a/vercel/resource_access_group_test.go
+++ b/vercel/resource_access_group_test.go
@@ -112,7 +112,7 @@ func testCheckAccessGroupDoesNotExist(n string) resource.TestCheckFunc {
func testAccResourceAccessGroup(name string) string {
return fmt.Sprintf(`
resource "vercel_access_group" "test" {
- %[1]s
+ %[1]s
name = "test-acc-%[2]s"
}
`, teamIDConfig(), name)
diff --git a/vercel/resource_team_member.go b/vercel/resource_team_member.go
new file mode 100644
index 00000000..0a1c5580
--- /dev/null
+++ b/vercel/resource_team_member.go
@@ -0,0 +1,477 @@
+package vercel
+
+import (
+ "context"
+ "fmt"
+ "os"
+ "time"
+
+ "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/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/setplanmodifier"
+ "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 = &teamMemberResource{}
+ _ resource.ResourceWithConfigure = &teamMemberResource{}
+ _ resource.ResourceWithImportState = &teamMemberResource{}
+)
+
+func newTeamMemberResource() resource.Resource {
+ return &teamMemberResource{}
+}
+
+type teamMemberResource struct {
+ client *client.Client
+}
+
+func (r *teamMemberResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+ resp.TypeName = req.ProviderTypeName + "_team_member"
+}
+
+func (r *teamMemberResource) 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 *teamMemberResource) Schema(_ context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
+ resp.Schema = schema.Schema{
+ Description: "Provider a resource for managing a team member.",
+ Attributes: map[string]schema.Attribute{
+ "team_id": schema.StringAttribute{
+ Description: "The ID of the existing Vercel Team.",
+ Required: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ stringplanmodifier.RequiresReplace(),
+ },
+ },
+ "user_id": schema.StringAttribute{
+ Description: "The ID of the user to add to the team.",
+ Required: true,
+ PlanModifiers: []planmodifier.String{
+ stringplanmodifier.UseStateForUnknown(),
+ stringplanmodifier.RequiresReplace(),
+ },
+ },
+ "role": schema.StringAttribute{
+ Description: "The role that the user should have in the project. One of 'MEMBER', 'OWNER', 'VIEWER', 'DEVELOPER', 'BILLING' or 'CONTRIBUTOR'. Depending on your Team's plan, some of these roles may be unavailable.",
+ Required: true,
+ Validators: []validator.String{
+ stringvalidator.OneOf("MEMBER", "OWNER", "VIEWER", "DEVELOPER", "BILLING", "CONTRIBUTOR"),
+ },
+ },
+ "projects": schema.SetNestedAttribute{
+ Description: "If access groups are enabled on the team, and the user is a CONTRIBUTOR, `projects`, `access_groups` or both must be specified. A set of projects that the user should be granted access to, along with their role in each project.",
+ Optional: true,
+ Computed: true,
+ PlanModifiers: []planmodifier.Set{
+ setplanmodifier.UseStateForUnknown(),
+ },
+ NestedObject: schema.NestedAttributeObject{
+ Attributes: map[string]schema.Attribute{
+ "role": schema.StringAttribute{
+ Description: "The role that the user should have in the project.",
+ Required: true,
+ Validators: []validator.String{
+ stringvalidator.OneOf("ADMIN", "PROJECT_VIEWER", "PROJECT_DEVELOPER"),
+ },
+ },
+ "project_id": schema.StringAttribute{
+ Description: "The ID of the project that the user should be granted access to.",
+ Required: true,
+ },
+ },
+ },
+ },
+ "access_groups": schema.SetAttribute{
+ Description: "If access groups are enabled on the team, and the user is a CONTRIBUTOR, `projects`, `access_groups` or both must be specified. A set of access groups IDs that the user should be granted access to.",
+ Optional: true,
+ Computed: true,
+ ElementType: types.StringType,
+ Validators: []validator.Set{
+ setvalidator.SizeAtLeast(1),
+ },
+ PlanModifiers: []planmodifier.Set{
+ setplanmodifier.UseStateForUnknown(),
+ },
+ },
+ },
+ }
+}
+
+type TeamMember struct {
+ UserID types.String `tfsdk:"user_id"`
+ TeamID types.String `tfsdk:"team_id"`
+ Role types.String `tfsdk:"role"`
+ Projects types.Set `tfsdk:"projects"`
+ AccessGroups types.Set `tfsdk:"access_groups"`
+}
+
+func (t TeamMember) projects(ctx context.Context) ([]TeamMemberProject, diag.Diagnostics) {
+ if t.Projects.IsNull() || t.Projects.IsUnknown() {
+ return nil, nil
+ }
+ var tmps []TeamMemberProject
+ diags := t.Projects.ElementsAs(ctx, &tmps, false)
+ return tmps, diags
+}
+
+func (t TeamMember) accessGroups(ctx context.Context) ([]string, diag.Diagnostics) {
+ if t.AccessGroups.IsNull() || t.AccessGroups.IsUnknown() {
+ return nil, nil
+ }
+ var tmps []string
+ diags := t.AccessGroups.ElementsAs(ctx, &tmps, false)
+ return tmps, diags
+}
+
+type TeamMemberProject struct {
+ Role types.String `tfsdk:"role"`
+ ProjectID types.String `tfsdk:"project_id"`
+}
+
+func (t TeamMember) toInviteTeamMemberRequest(ctx context.Context) (client.TeamMemberInviteRequest, diag.Diagnostics) {
+ tmps, diags := t.projects(ctx)
+ if diags.HasError() {
+ return client.TeamMemberInviteRequest{}, diags
+ }
+
+ var projects []client.ProjectRole
+ for _, p := range tmps {
+ projects = append(projects, client.ProjectRole{
+ ProjectID: p.ProjectID.ValueString(),
+ Role: p.Role.ValueString(),
+ })
+ }
+
+ accessGroups, diags := t.accessGroups(ctx)
+ if diags.HasError() {
+ return client.TeamMemberInviteRequest{}, diags
+ }
+
+ return client.TeamMemberInviteRequest{
+ TeamID: t.TeamID.ValueString(),
+ UserID: t.UserID.ValueString(),
+ Role: t.Role.ValueString(),
+ Projects: projects,
+ AccessGroups: accessGroups,
+ }, diags
+}
+
+func diffAccessGroups(oldAgs, newAgs []string) (toAdd, toRemove []string) {
+ for _, n := range newAgs {
+ if !contains(oldAgs, n) {
+ toAdd = append(toAdd, n)
+ }
+ }
+ for _, n := range oldAgs {
+ if !contains(newAgs, n) {
+ toRemove = append(toRemove, n)
+ }
+ }
+ return
+}
+
+func (t TeamMember) toTeamMemberUpdateRequest(ctx context.Context, state TeamMember) (client.TeamMemberUpdateRequest, diag.Diagnostics) {
+ tmps, diags := t.projects(ctx)
+ if diags.HasError() {
+ return client.TeamMemberUpdateRequest{}, diags
+ }
+
+ var projects []client.ProjectRole
+ for _, p := range tmps {
+ projects = append(projects, client.ProjectRole{
+ ProjectID: p.ProjectID.ValueString(),
+ Role: p.Role.ValueString(),
+ })
+ }
+
+ newAccessGroups, diags := t.accessGroups(ctx)
+ if diags.HasError() {
+ return client.TeamMemberUpdateRequest{}, diags
+ }
+ oldAccessGroups, diags := state.accessGroups(ctx)
+ if diags.HasError() {
+ return client.TeamMemberUpdateRequest{}, diags
+ }
+
+ toAdd, toRemove := diffAccessGroups(oldAccessGroups, newAccessGroups)
+ return client.TeamMemberUpdateRequest{
+ TeamID: t.TeamID.ValueString(),
+ UserID: t.UserID.ValueString(),
+ Role: t.Role.ValueString(),
+ Projects: projects,
+ AccessGroupsToAdd: toAdd,
+ AccessGroupsToRemove: toRemove,
+ }, nil
+}
+
+func (t TeamMember) toTeamMemberRemoveRequest() client.TeamMemberRemoveRequest {
+ return client.TeamMemberRemoveRequest{
+ UserID: t.UserID.ValueString(),
+ TeamID: t.TeamID.ValueString(),
+ }
+}
+
+var projectsElemType = types.ObjectType{
+ AttrTypes: map[string]attr.Type{
+ "role": types.StringType,
+ "project_id": types.StringType,
+ },
+}
+
+func convertResponseToTeamMember(response client.TeamMember, teamID types.String) TeamMember {
+ var projectsAttrs []attr.Value
+ for _, p := range response.Projects {
+ projectsAttrs = append(projectsAttrs, types.ObjectValueMust(
+ map[string]attr.Type{
+ "role": types.StringType,
+ "project_id": types.StringType,
+ },
+ map[string]attr.Value{
+ "role": types.StringValue(p.Role),
+ "project_id": types.StringValue(p.ProjectID),
+ },
+ ))
+ }
+ projects := types.SetValueMust(projectsElemType, projectsAttrs)
+
+ var ags []attr.Value
+ for _, ag := range response.AccessGroups {
+ ags = append(ags, types.StringValue(ag.ID))
+ }
+ accessGroups := types.SetValueMust(types.StringType, ags)
+
+ return TeamMember{
+ UserID: types.StringValue(response.UserID),
+ TeamID: teamID,
+ Role: types.StringValue(response.Role),
+ Projects: projects,
+ AccessGroups: accessGroups,
+ }
+}
+
+func (r *teamMemberResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+ var plan TeamMember
+ diags := req.Plan.Get(ctx, &plan)
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ request, diags := plan.toInviteTeamMemberRequest(ctx)
+ if diags.HasError() {
+ resp.Diagnostics.Append(diags...)
+ return
+ }
+ err := r.client.InviteTeamMember(ctx, request)
+ if err != nil {
+ resp.Diagnostics.AddError(
+ "Error inviting Team Member",
+ "Could not invite Team Member, unexpected error: "+err.Error(),
+ )
+ return
+ }
+
+ tflog.Info(ctx, "invited Team Member", map[string]interface{}{
+ "team_id": plan.TeamID.ValueString(),
+ "user_id": plan.UserID.ValueString(),
+ })
+
+ projects := types.SetNull(projectsElemType)
+ if !plan.Projects.IsUnknown() && !plan.Projects.IsNull() {
+ projects = plan.Projects
+ }
+ ags := types.SetNull(types.StringType)
+ if !plan.AccessGroups.IsUnknown() && !plan.AccessGroups.IsNull() {
+ ags = plan.AccessGroups
+ }
+ diags = resp.State.Set(ctx, TeamMember{
+ TeamID: plan.TeamID,
+ UserID: plan.UserID,
+ Role: plan.Role,
+ Projects: projects,
+ AccessGroups: ags,
+ })
+ resp.Diagnostics.Append(diags...)
+ sleepInTests()
+}
+
+func sleepInTests() {
+ if os.Getenv("TF_ACC") == "true" {
+ // Give a couple of seconds for the user to propagate.
+ // This is horrible, but works for now.
+ time.Sleep(5 * time.Second)
+ }
+}
+
+func (r *teamMemberResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+ var state TeamMember
+ diags := req.State.Get(ctx, &state)
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ response, err := r.client.GetTeamMember(ctx, client.GetTeamMemberRequest{
+ TeamID: state.TeamID.ValueString(),
+ UserID: state.UserID.ValueString(),
+ })
+ if client.NotFound(err) {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ if err != nil {
+ resp.Diagnostics.AddError(
+ "Error reading Team Member",
+ "Could not read Team Member, unexpected error: "+err.Error(),
+ )
+ }
+ teamMember := convertResponseToTeamMember(response, state.TeamID)
+ if !response.Confirmed {
+ // The API doesn't return the projects or access groups for unconfirmed members, so we have to
+ // manually set these fields to whatever was in state.
+ teamMember.Projects = types.SetNull(projectsElemType)
+ if !state.Projects.IsUnknown() && !state.Projects.IsNull() {
+ teamMember.Projects = state.Projects
+ }
+ teamMember.AccessGroups = types.SetNull(types.StringType)
+ if !state.AccessGroups.IsUnknown() && !state.AccessGroups.IsNull() {
+ teamMember.AccessGroups = state.AccessGroups
+ }
+ }
+ diags = resp.State.Set(ctx, teamMember)
+ resp.Diagnostics.Append(diags...)
+}
+
+func (r *teamMemberResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+ var plan TeamMember
+ diags := req.Plan.Get(ctx, &plan)
+ if diags.HasError() {
+ resp.Diagnostics.Append(diags...)
+ return
+ }
+
+ var state TeamMember
+ diags = req.State.Get(ctx, &state)
+ if diags.HasError() {
+ resp.Diagnostics.Append(diags...)
+ return
+ }
+
+ request, diags := plan.toTeamMemberUpdateRequest(ctx, state)
+ if diags.HasError() {
+ resp.Diagnostics.Append(diags...)
+ return
+ }
+ err := r.client.UpdateTeamMember(ctx, request)
+ if err != nil {
+ resp.Diagnostics.AddError(
+ "Error updating Team Member",
+ "Could not update Team Member, unexpected error: "+err.Error(),
+ )
+ return
+ }
+
+ tflog.Info(ctx, "updated Team member", map[string]interface{}{
+ "team_id": request.TeamID,
+ "user_id": request.UserID,
+ })
+
+ projects := types.SetNull(projectsElemType)
+ if !plan.Projects.IsUnknown() && !plan.Projects.IsNull() {
+ projects = plan.Projects
+ }
+ ags := types.SetNull(types.StringType)
+ if !plan.AccessGroups.IsUnknown() && !plan.AccessGroups.IsNull() {
+ ags = plan.AccessGroups
+ }
+ diags = resp.State.Set(ctx, TeamMember{
+ TeamID: plan.TeamID,
+ UserID: plan.UserID,
+ Role: plan.Role,
+ Projects: projects,
+ AccessGroups: ags,
+ })
+ resp.Diagnostics.Append(diags...)
+ sleepInTests()
+}
+
+func (r *teamMemberResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+ var state TeamMember
+ diags := req.State.Get(ctx, &state)
+ resp.Diagnostics.Append(diags...)
+ if resp.Diagnostics.HasError() {
+ return
+ }
+
+ err := r.client.RemoveTeamMember(ctx, state.toTeamMemberRemoveRequest())
+ if client.NotFound(err) {
+ resp.State.RemoveResource(ctx)
+ return
+ }
+ if err != nil {
+ resp.Diagnostics.AddError(
+ "Error removing Team Member",
+ "Could not remove Team Member, unexpected error: "+err.Error(),
+ )
+ }
+
+ resp.State.RemoveResource(ctx)
+ sleepInTests()
+}
+
+// ImportState implements resource.ResourceWithImportState.
+func (r *teamMemberResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
+ teamID, userID, ok := splitInto2(req.ID)
+ if !ok {
+ resp.Diagnostics.AddError(
+ "Error importing Team Member",
+ fmt.Sprintf("Invalid id '%s' specified. should be in format \"team_id/user_id\"", req.ID),
+ )
+ }
+
+ tflog.Info(ctx, "import Team Member", map[string]interface{}{
+ "team_id": teamID,
+ "user_id": userID,
+ })
+
+ response, err := r.client.GetTeamMember(ctx, client.GetTeamMemberRequest{
+ TeamID: teamID,
+ UserID: userID,
+ })
+ if err != nil {
+ resp.Diagnostics.AddError(
+ "Error reading Team Member",
+ "Could not read Team Member, unexpected error: "+err.Error(),
+ )
+ }
+ teamMember := convertResponseToTeamMember(response, types.StringValue(teamID))
+ diags := resp.State.Set(ctx, teamMember)
+ resp.Diagnostics.Append(diags...)
+}
diff --git a/vercel/resource_team_member_test.go b/vercel/resource_team_member_test.go
new file mode 100644
index 00000000..58176b4d
--- /dev/null
+++ b/vercel/resource_team_member_test.go
@@ -0,0 +1,124 @@
+package vercel_test
+
+import (
+ "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"
+)
+
+func getTeamMemberImportID(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", rs.Primary.Attributes["team_id"], rs.Primary.Attributes["user_id"]), nil
+ }
+}
+
+func TestAcc_TeamMemberResource(t *testing.T) {
+ t.Parallel()
+ randomSuffix := acctest.RandString(16)
+ resource.Test(t, resource.TestCase{
+ PreCheck: func() { testAccPreCheck(t) },
+ ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
+ Steps: []resource.TestStep{
+ // Create and Read testing
+ {
+ Config: testAccTeamMemberResourceConfig("MEMBER"),
+ Check: resource.ComposeAggregateTestCheckFunc(
+ resource.TestCheckResourceAttrSet("vercel_team_member.test", "team_id"),
+ resource.TestCheckResourceAttrSet("vercel_team_member.test", "user_id"),
+ resource.TestCheckResourceAttr("vercel_team_member.test", "role", "MEMBER"),
+ ),
+ },
+ // ImportState testing
+ {
+ ResourceName: "vercel_team_member.test",
+ ImportState: true,
+ ImportStateVerify: true,
+ ImportStateIdFunc: getTeamMemberImportID("vercel_team_member.test"),
+ ImportStateVerifyIdentifierAttribute: "user_id",
+ },
+ // Update testing
+ {
+ Config: testAccTeamMemberResourceConfig("VIEWER"),
+ Check: resource.ComposeAggregateTestCheckFunc(
+ resource.TestCheckResourceAttrSet("vercel_team_member.test", "team_id"),
+ resource.TestCheckResourceAttrSet("vercel_team_member.test", "user_id"),
+ resource.TestCheckResourceAttr("vercel_team_member.test", "role", "VIEWER"),
+ ),
+ },
+ // Test with projects
+ {
+ Config: testAccTeamMemberResourceConfigWithProjects(randomSuffix),
+ Check: resource.ComposeAggregateTestCheckFunc(
+ resource.TestCheckResourceAttrSet("vercel_team_member.test_with_projects", "team_id"),
+ resource.TestCheckResourceAttrSet("vercel_team_member.test_with_projects", "user_id"),
+ resource.TestCheckResourceAttr("vercel_team_member.test_with_projects", "role", "CONTRIBUTOR"),
+ resource.TestCheckResourceAttr("vercel_team_member.test_with_projects", "projects.#", "1"),
+ ),
+ },
+ // Test with access groups
+ {
+ Config: testAccTeamMemberResourceConfigWithAccessGroups(randomSuffix),
+ Check: resource.ComposeAggregateTestCheckFunc(
+ resource.TestCheckResourceAttrSet("vercel_team_member.test_with_access_groups", "team_id"),
+ resource.TestCheckResourceAttrSet("vercel_team_member.test_with_access_groups", "user_id"),
+ resource.TestCheckResourceAttr("vercel_team_member.test_with_access_groups", "role", "CONTRIBUTOR"),
+ resource.TestCheckResourceAttr("vercel_team_member.test_with_access_groups", "access_groups.#", "1"),
+ ),
+ },
+ },
+ })
+}
+
+func testAccTeamMemberResourceConfig(role string) string {
+ return fmt.Sprintf(`
+resource "vercel_team_member" "test" {
+ %[1]s
+ user_id = "%s"
+ role = "%s"
+}
+`, teamIDConfig(), testAdditionalUser(), role)
+}
+
+func testAccTeamMemberResourceConfigWithProjects(randomSuffix string) string {
+ return fmt.Sprintf(`
+resource "vercel_project" "test" {
+ name = "test-acc-example-project-%[1]s"
+ %[2]s
+}
+
+resource "vercel_team_member" "test_with_projects" {
+ %[2]s
+ user_id = "%s"
+ role = "CONTRIBUTOR"
+ projects = [{
+ project_id = vercel_project.test.id
+ role = "PROJECT_VIEWER"
+ }]
+}
+`, randomSuffix, teamIDConfig(), testAdditionalUser())
+}
+
+func testAccTeamMemberResourceConfigWithAccessGroups(randomSuffix string) string {
+ return fmt.Sprintf(`
+resource "vercel_access_group" "test" {
+ %[1]s
+ name = "test-acc-access-group-%[3]s"
+}
+
+resource "vercel_team_member" "test_with_access_groups" {
+ %[1]s
+ user_id = "%[2]s"
+ role = "CONTRIBUTOR"
+
+ access_groups = [vercel_access_group.test.id]
+}
+`, teamIDConfig(), testAdditionalUser(), randomSuffix)
+}
diff --git a/vercel/split.go b/vercel/split.go
index 422b7dce..4dccd442 100644
--- a/vercel/split.go
+++ b/vercel/split.go
@@ -2,6 +2,15 @@ package vercel
import "strings"
+func splitInto2(id string) (firstID, secondID string, ok bool) {
+ attributes := strings.Split(id, "/")
+ if len(attributes) != 2 {
+ return "", "", false
+ }
+
+ return attributes[0], attributes[1], true
+}
+
// splitInto2Or3 is a helper function for splitting an import ID into the corresponding parts.
// It also validates whether the ID is in a correct format.
func splitInto2Or3(id string) (teamID, firstID, secondID string, ok bool) {