diff --git a/client/microfrontend_group.go b/client/microfrontend_group.go new file mode 100644 index 00000000..737c3a64 --- /dev/null +++ b/client/microfrontend_group.go @@ -0,0 +1,187 @@ +package client + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-log/tflog" +) + +type MicrofrontendGroup struct { + ID string `json:"id"` + Name string `json:"name"` + Slug string `json:"slug"` + TeamID string `json:"team_id"` + Projects map[string]MicrofrontendGroupMembership `json:"projects"` + DefaultApp MicrofrontendGroupMembership `json:"defaultApp"` +} + +type MicrofrontendGroupsAPIResponse struct { + Groups []struct { + Group struct { + ID string `json:"id"` + Name string `json:"name"` + Slug string `json:"slug"` + TeamID string `json:"team_id"` + Projects map[string]struct { + IsDefaultApp bool `json:"isDefaultApp"` + DefaultRoute string `json:"defaultRoute"` + RouteObservabilityToThisProject bool `json:"routeObservabilityToThisProject"` + ProjectID string `json:"projectId"` + Enabled bool `json:"enabled"` + } `json:"projects"` + } `json:"group"` + Projects []MicrofrontendGroupMembershipsResponseAPI `json:"projects"` + } `json:"groups"` +} + +func (c *Client) CreateMicrofrontendGroup(ctx context.Context, TeamID string, Name string) (r MicrofrontendGroup, err error) { + if c.teamID(TeamID) == "" { + return r, fmt.Errorf("team_id is required") + } + tflog.Info(ctx, "creating microfrontend group", map[string]interface{}{ + "microfrontend_group_name": Name, + "team_id": c.teamID(TeamID), + }) + url := fmt.Sprintf("%s/teams/%s/microfrontends", c.baseURL, c.teamID(TeamID)) + payload := string(mustMarshal(struct { + NewMicrofrontendsGroupName string `json:"newMicrofrontendsGroupName"` + }{ + NewMicrofrontendsGroupName: Name, + })) + apiResponse := struct { + NewMicrofrontendGroup MicrofrontendGroup `json:"newMicrofrontendsGroup"` + }{} + err = c.doRequest(clientRequest{ + ctx: ctx, + method: "PATCH", + url: url, + body: payload, + }, &apiResponse) + if err != nil { + return r, err + } + return MicrofrontendGroup{ + ID: apiResponse.NewMicrofrontendGroup.ID, + Name: apiResponse.NewMicrofrontendGroup.Name, + Slug: apiResponse.NewMicrofrontendGroup.Slug, + TeamID: c.teamID(TeamID), + }, nil +} + +func (c *Client) UpdateMicrofrontendGroup(ctx context.Context, request MicrofrontendGroup) (r MicrofrontendGroup, err error) { + if c.teamID(request.TeamID) == "" { + return r, fmt.Errorf("team_id is required") + } + url := fmt.Sprintf("%s/teams/%s/microfrontends/%s", c.baseURL, c.teamID(request.TeamID), request.ID) + payload := string(mustMarshal(struct { + Name string `json:"name"` + }{ + Name: request.Name, + })) + tflog.Info(ctx, "updating microfrontend group", map[string]interface{}{ + "url": url, + "payload": payload, + }) + apiResponse := struct { + UpdatedMicrofrontendsGroup MicrofrontendGroup `json:"updatedMicrofrontendsGroup"` + }{} + err = c.doRequest(clientRequest{ + ctx: ctx, + method: "PATCH", + url: url, + body: payload, + }, &apiResponse) + if err != nil { + return r, err + } + return MicrofrontendGroup{ + ID: apiResponse.UpdatedMicrofrontendsGroup.ID, + Name: apiResponse.UpdatedMicrofrontendsGroup.Name, + Slug: apiResponse.UpdatedMicrofrontendsGroup.Slug, + TeamID: c.teamID(request.TeamID), + }, nil +} + +func (c *Client) DeleteMicrofrontendGroup(ctx context.Context, request MicrofrontendGroup) (r struct{}, err error) { + if c.teamID(request.TeamID) == "" { + return r, fmt.Errorf("team_id is required") + } + url := fmt.Sprintf("%s/teams/%s/microfrontends/%s", c.baseURL, c.teamID(request.TeamID), request.ID) + + tflog.Info(ctx, "deleting microfrontend group", map[string]interface{}{ + "url": url, + }) + + err = c.doRequest(clientRequest{ + ctx: ctx, + method: "DELETE", + url: url, + body: "", + }, &r) + return r, err +} + +func (c *Client) GetMicrofrontendGroup(ctx context.Context, microfrontendGroupID string, teamID string) (r MicrofrontendGroup, err error) { + if c.teamID(teamID) == "" { + return r, fmt.Errorf("team_id is required") + } + url := fmt.Sprintf("%s/v1/microfrontends/groups", c.baseURL) + if c.teamID(teamID) != "" { + url = fmt.Sprintf("%s?teamId=%s", url, c.teamID(teamID)) + } + + tflog.Info(ctx, "getting microfrontend group", map[string]interface{}{ + "url": url, + }) + out := MicrofrontendGroupsAPIResponse{} + err = c.doRequest(clientRequest{ + ctx: ctx, + method: "GET", + url: url, + body: "", + }, &out) + + if err != nil { + return r, err + } + + tflog.Info(ctx, "getting microfrontend group", map[string]interface{}{ + "out": out, + }) + + for i := range out.Groups { + if out.Groups[i].Group.ID == microfrontendGroupID { + projects := map[string]MicrofrontendGroupMembership{} + defaultApp := MicrofrontendGroupMembership{} + for _, p := range out.Groups[i].Projects { + projects[p.ID] = MicrofrontendGroupMembership{ + MicrofrontendGroupID: microfrontendGroupID, + ProjectID: p.ID, + TeamID: c.teamID(teamID), + Enabled: p.Microfrontends.Enabled, + IsDefaultApp: p.Microfrontends.IsDefaultApp, + DefaultRoute: p.Microfrontends.DefaultRoute, + RouteObservabilityToThisProject: p.Microfrontends.RouteObservabilityToThisProject, + } + if p.Microfrontends.IsDefaultApp { + defaultApp = projects[p.ID] + } + } + r := MicrofrontendGroup{ + ID: out.Groups[i].Group.ID, + Name: out.Groups[i].Group.Name, + Slug: out.Groups[i].Group.Slug, + TeamID: c.teamID(teamID), + DefaultApp: defaultApp, + Projects: projects, + } + tflog.Info(ctx, "returning microfrontend group", map[string]interface{}{ + "r": r, + }) + return r, nil + } + } + + return r, fmt.Errorf("microfrontend group not found") +} diff --git a/client/microfrontend_group_membership.go b/client/microfrontend_group_membership.go new file mode 100644 index 00000000..91315a55 --- /dev/null +++ b/client/microfrontend_group_membership.go @@ -0,0 +1,137 @@ +package client + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-log/tflog" +) + +type MicrofrontendGroupMembership struct { + MicrofrontendGroupID string `json:"microfrontendsGroupId"` + IsDefaultApp bool `json:"isDefaultApp"` + DefaultRoute string `json:"defaultRoute"` + RouteObservabilityToThisProject bool `json:"routeObservabilityToThisProject"` + ProjectID string `json:"projectId"` + Enabled bool `json:"enabled"` + TeamID string `json:"team_id"` +} + +type MicrofrontendGroupMembershipResponseAPI struct { + GroupIds []string `json:"groupIds"` + Enabled bool `json:"enabled"` + IsDefaultApp bool `json:"isDefaultApp"` + DefaultRoute string `json:"defaultRoute"` + RouteObservabilityToThisProject bool `json:"routeObservabilityToThisProject"` + TeamID string `json:"team_id"` + UpdatedAt int `json:"updatedAt"` +} + +type MicrofrontendGroupMembershipsResponseAPI struct { + ID string `json:"id"` + Microfrontends MicrofrontendGroupMembershipResponseAPI `json:"microfrontends"` +} + +func (c *Client) GetMicrofrontendGroupMembership(ctx context.Context, TeamID string, GroupID string, ProjectID string) (r MicrofrontendGroupMembership, err error) { + tflog.Info(ctx, "getting microfrontend group", map[string]interface{}{ + "project_id": ProjectID, + "group_id": GroupID, + "team_id": c.teamID(TeamID), + }) + group, err := c.GetMicrofrontendGroup(ctx, GroupID, c.teamID(TeamID)) + if err != nil { + return r, err + } + tflog.Info(ctx, "getting microfrontend group membership", map[string]interface{}{ + "project_id": ProjectID, + "group": group, + }) + return group.Projects[ProjectID], nil +} + +func (c *Client) AddOrUpdateMicrofrontendGroupMembership(ctx context.Context, request MicrofrontendGroupMembership) (r MicrofrontendGroupMembership, err error) { + tflog.Info(ctx, "adding / updating microfrontend project to group", map[string]interface{}{ + "is_default_app": request.IsDefaultApp, + "project_id": request.ProjectID, + "group_id": request.MicrofrontendGroupID, + }) + p, err := c.PatchMicrofrontendGroupMembership(ctx, MicrofrontendGroupMembership{ + ProjectID: request.ProjectID, + TeamID: c.teamID(request.TeamID), + Enabled: true, + IsDefaultApp: request.IsDefaultApp, + DefaultRoute: request.DefaultRoute, + RouteObservabilityToThisProject: request.RouteObservabilityToThisProject, + MicrofrontendGroupID: request.MicrofrontendGroupID, + }) + if err != nil { + return r, err + } + return p, nil +} + +func (c *Client) RemoveMicrofrontendGroupMembership(ctx context.Context, request MicrofrontendGroupMembership) (r MicrofrontendGroupMembership, err error) { + tflog.Info(ctx, "removing microfrontend project from group", map[string]interface{}{ + "project_id": request.ProjectID, + "group_id": request.MicrofrontendGroupID, + "team_id": c.teamID(request.TeamID), + }) + p, err := c.PatchMicrofrontendGroupMembership(ctx, MicrofrontendGroupMembership{ + ProjectID: request.ProjectID, + TeamID: c.teamID(request.TeamID), + Enabled: false, + MicrofrontendGroupID: request.MicrofrontendGroupID, + }) + if err != nil { + return r, err + } + return p, nil +} + +func (c *Client) PatchMicrofrontendGroupMembership(ctx context.Context, request MicrofrontendGroupMembership) (r MicrofrontendGroupMembership, err error) { + url := fmt.Sprintf("%s/projects/%s/microfrontends", c.baseURL, request.ProjectID) + payload := string(mustMarshal(MicrofrontendGroupMembership{ + IsDefaultApp: request.IsDefaultApp, + DefaultRoute: request.DefaultRoute, + RouteObservabilityToThisProject: request.RouteObservabilityToThisProject, + ProjectID: request.ProjectID, + Enabled: request.Enabled, + MicrofrontendGroupID: request.MicrofrontendGroupID, + })) + if !request.Enabled { + payload = string(mustMarshal(struct { + ProjectID string `json:"projectId"` + Enabled bool `json:"enabled"` + }{ + ProjectID: request.ProjectID, + Enabled: request.Enabled, + })) + } + if c.teamID(request.TeamID) != "" { + url = fmt.Sprintf("%s?teamId=%s", url, c.teamID(request.TeamID)) + } + + tflog.Info(ctx, "updating microfrontend group membership", map[string]interface{}{ + "url": url, + "payload": payload, + }) + apiResponse := MicrofrontendGroupMembershipsResponseAPI{} + err = c.doRequest(clientRequest{ + ctx: ctx, + method: "PATCH", + url: url, + body: payload, + }, &apiResponse) + if err != nil { + return r, err + } + return MicrofrontendGroupMembership{ + MicrofrontendGroupID: request.MicrofrontendGroupID, + ProjectID: request.ProjectID, + TeamID: c.teamID(request.TeamID), + Enabled: apiResponse.Microfrontends.Enabled, + IsDefaultApp: apiResponse.Microfrontends.IsDefaultApp, + DefaultRoute: apiResponse.Microfrontends.DefaultRoute, + RouteObservabilityToThisProject: apiResponse.Microfrontends.RouteObservabilityToThisProject, + }, nil +} diff --git a/docs/data-sources/microfrontend_group.md b/docs/data-sources/microfrontend_group.md new file mode 100644 index 00000000..8089e51c --- /dev/null +++ b/docs/data-sources/microfrontend_group.md @@ -0,0 +1,47 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "vercel_microfrontend_group Data Source - terraform-provider-vercel" +subcategory: "" +description: |- + Provides information about an existing Microfrontend Group. + A Microfrontend Group is a definition of a microfrontend belonging to a Vercel Team. +--- + +# vercel_microfrontend_group (Data Source) + +Provides information about an existing Microfrontend Group. + +A Microfrontend Group is a definition of a microfrontend belonging to a Vercel Team. + +## Example Usage + +```terraform +data "vercel_microfrontend_group" "example" { + id = "mfe_xxxxxxxxxxxxxxxxxxxxxxxxxxxx" +} +``` + + +## Schema + +### Required + +- `id` (String) A unique identifier for the group of microfrontends. Example: mfe_12HKQaOmR5t5Uy6vdcQsNIiZgHGB + +### Optional + +- `team_id` (String) The team ID to add the project to. Required when configuring a team resource if a default team has not been set in the provider. + +### Read-Only + +- `default_app` (Attributes) The default app for the project. Used as the entry point for the microfrontend. (see [below for nested schema](#nestedatt--default_app)) +- `name` (String) A human readable name for the microfrontends group. +- `slug` (String) A slugified version of the name. + + +### Nested Schema for `default_app` + +Read-Only: + +- `default_route` (String) The default route for the project. Used for the screenshot of deployments. +- `project_id` (String) The ID of the project. diff --git a/docs/data-sources/microfrontend_group_membership.md b/docs/data-sources/microfrontend_group_membership.md new file mode 100644 index 00000000..e2882086 --- /dev/null +++ b/docs/data-sources/microfrontend_group_membership.md @@ -0,0 +1,40 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "vercel_microfrontend_group_membership Data Source - terraform-provider-vercel" +subcategory: "" +description: |- + Provides information about an existing Microfrontend Group Membership. + A Microfrontend Group Membership is a definition of a Vercel Project being a part of a Microfrontend Group. +--- + +# vercel_microfrontend_group_membership (Data Source) + +Provides information about an existing Microfrontend Group Membership. + +A Microfrontend Group Membership is a definition of a Vercel Project being a part of a Microfrontend Group. + +## Example Usage + +```terraform +data "vercel_microfrontend_group_membership" "example" { + project_id = "prj_xxxxxxxxxxxxxxxxxxxxxxxxxxxx" + microfrontend_group_id = "mfe_xxxxxxxxxxxxxxxxxxxxxxxxxxxx" +} +``` + + +## Schema + +### Required + +- `microfrontend_group_id` (String) The ID of the microfrontend group. +- `project_id` (String) The ID of the project. + +### Optional + +- `team_id` (String) The team ID to add the microfrontend group to. Required when configuring a team resource if a default team has not been set in the provider. + +### Read-Only + +- `default_route` (String) The default route for the project. Used for the screenshot of deployments. +- `route_observability_to_this_project` (Boolean) Whether the project is route observability for this project. If dalse, the project will be route observability for all projects to the default project. diff --git a/docs/resources/microfrontend_group.md b/docs/resources/microfrontend_group.md new file mode 100644 index 00000000..d63ecad8 --- /dev/null +++ b/docs/resources/microfrontend_group.md @@ -0,0 +1,84 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "vercel_microfrontend_group Resource - terraform-provider-vercel" +subcategory: "" +description: |- + Provides a Microfrontend Group resource. + A Microfrontend Group is a definition of a microfrontend belonging to a Vercel Team. +--- + +# vercel_microfrontend_group (Resource) + +Provides a Microfrontend Group resource. + +A Microfrontend Group is a definition of a microfrontend belonging to a Vercel Team. + +## Example Usage + +```terraform +data "vercel_project" "parent_mfe_project" { + name = "my parent project" +} + +data "vercel_project" "child_mfe_project" { + name = "my child project" +} + +resource "vercel_microfrontend_group" "example_mfe_group" { + name = "my mfe" + default_app = vercel_project.parent_mfe_project.id +} + +resource "vercel_microfrontend_group_membership" "parent_mfe_project_mfe_membership" { + project_id = vercel_project.parent_mfe_project.id + microfrontend_group_id = vercel_microfrontend_group.example_mfe_group.id +} + +resource "vercel_microfrontend_group_membership" "child_mfe_project_mfe_membership" { + project_id = vercel_project.child_mfe_project.id + microfrontend_group_id = vercel_microfrontend_group.example_mfe_group.id +} +``` + + +## Schema + +### Required + +- `default_app` (Attributes) The default app for the project. Used as the entry point for the microfrontend. (see [below for nested schema](#nestedatt--default_app)) +- `name` (String) A human readable name for the microfrontends group. + +### Optional + +- `team_id` (String) The team ID to add the microfrontend group to. Required when configuring a team resource if a default team has not been set in the provider. + +### Read-Only + +- `id` (String) A unique identifier for the group of microfrontends. Example: mfe_12HKQaOmR5t5Uy6vdcQsNIiZgHGB +- `slug` (String) A slugified version of the name. + + +### Nested Schema for `default_app` + +Required: + +- `project_id` (String) The ID of the project. + +Optional: + +- `default_route` (String) The default route for the project. Used for the screenshot of deployments. + +## 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 record id. +# - the microfrontend ID can be taken from the microfrontend settings page +terraform import vercel_microfrontend_group.example mfe_xxxxxxxxxxxxxxxxxxxxxxxxxxxx + +# Alternatively, you can import via the team_id and microfrontend_id. +# - team_id can be found in the team `settings` tab in the Vercel UI. +# - the microfrontend ID can be taken from the microfrontend settings page +terraform import vercel_microfrontend_group.example team_xxxxxxxxxxxxxxxxxxxxxxxx/mfe_xxxxxxxxxxxxxxxxxxxxxxxxxxxx +``` diff --git a/docs/resources/microfrontend_group_membership.md b/docs/resources/microfrontend_group_membership.md new file mode 100644 index 00000000..db79a184 --- /dev/null +++ b/docs/resources/microfrontend_group_membership.md @@ -0,0 +1,72 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "vercel_microfrontend_group_membership Resource - terraform-provider-vercel" +subcategory: "" +description: |- + Provides a Microfrontend Group Membership resource. + A Microfrontend Group Membership is a definition of a Vercel Project being a part of a Microfrontend Group. +--- + +# vercel_microfrontend_group_membership (Resource) + +Provides a Microfrontend Group Membership resource. + +A Microfrontend Group Membership is a definition of a Vercel Project being a part of a Microfrontend Group. + +## Example Usage + +```terraform +data "vercel_project" "parent_mfe_project" { + name = "my parent project" +} + +data "vercel_project" "child_mfe_project" { + name = "my child project" +} + +resource "vercel_microfrontend_group" "example_mfe_group" { + name = "my mfe" + default_app = vercel_project.parent_mfe_project.id +} + +resource "vercel_microfrontend_group_membership" "parent_mfe_project_mfe_membership" { + project_id = vercel_project.parent_mfe_project.id + microfrontend_group_id = vercel_microfrontend_group.example_mfe_group.id +} + +resource "vercel_microfrontend_group_membership" "child_mfe_project_mfe_membership" { + project_id = vercel_project.child_mfe_project.id + microfrontend_group_id = vercel_microfrontend_group.example_mfe_group.id +} +``` + + +## Schema + +### Required + +- `microfrontend_group_id` (String) The ID of the microfrontend group. +- `project_id` (String) The ID of the project. + +### Optional + +- `default_route` (String) The default route for the project. Used for the screenshot of deployments. +- `route_observability_to_this_project` (Boolean) Whether the project is route observability for this project. If dalse, the project will be route observability for all projects to the default project. +- `team_id` (String) The team ID to add the microfrontend group to. 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, simply use the record id. +# - the microfrontend ID can be taken from the microfrontend settings page +# - the project ID can be taken from the project settings page +terraform import vercel_microfrontend_group_membership.example mfe_xxxxxxxxxxxxxxxxxxxxxxxxxxxx/pid_xxxxxxxxxxxxxxxxxxxxxxxxxxxx + +# Alternatively, you can import via the team_id and microfrontend_id. +# - team_id can be found in the team `settings` tab in the Vercel UI. +# - the microfrontend ID can be taken from the microfrontend settings page +# - the project ID can be taken from the project settings page +terraform import vercel_microfrontend_group_membership.example team_xxxxxxxxxxxxxxxxxxxxxxxx/mfe_xxxxxxxxxxxxxxxxxxxxxxxxxxxx/pid_xxxxxxxxxxxxxxxxxxxxxxxxxxxx +``` diff --git a/examples/data-sources/vercel_microfrontend_group/data-source.tf b/examples/data-sources/vercel_microfrontend_group/data-source.tf new file mode 100644 index 00000000..c0e1275a --- /dev/null +++ b/examples/data-sources/vercel_microfrontend_group/data-source.tf @@ -0,0 +1,4 @@ + +data "vercel_microfrontend_group" "example" { + id = "mfe_xxxxxxxxxxxxxxxxxxxxxxxxxxxx" +} diff --git a/examples/data-sources/vercel_microfrontend_group_membership/data-source.tf b/examples/data-sources/vercel_microfrontend_group_membership/data-source.tf new file mode 100644 index 00000000..f5371ef8 --- /dev/null +++ b/examples/data-sources/vercel_microfrontend_group_membership/data-source.tf @@ -0,0 +1,5 @@ + +data "vercel_microfrontend_group_membership" "example" { + project_id = "prj_xxxxxxxxxxxxxxxxxxxxxxxxxxxx" + microfrontend_group_id = "mfe_xxxxxxxxxxxxxxxxxxxxxxxxxxxx" +} diff --git a/examples/resources/vercel_microfrontend_group/import.sh b/examples/resources/vercel_microfrontend_group/import.sh new file mode 100644 index 00000000..1fc0574e --- /dev/null +++ b/examples/resources/vercel_microfrontend_group/import.sh @@ -0,0 +1,9 @@ +# If importing into a personal account, or with a team configured on the provider, simply use the record id. +# - the microfrontend ID can be taken from the microfrontend settings page +terraform import vercel_microfrontend_group.example mfe_xxxxxxxxxxxxxxxxxxxxxxxxxxxx + +# Alternatively, you can import via the team_id and microfrontend_id. +# - team_id can be found in the team `settings` tab in the Vercel UI. +# - the microfrontend ID can be taken from the microfrontend settings page +terraform import vercel_microfrontend_group.example team_xxxxxxxxxxxxxxxxxxxxxxxx/mfe_xxxxxxxxxxxxxxxxxxxxxxxxxxxx + diff --git a/examples/resources/vercel_microfrontend_group/resource.tf b/examples/resources/vercel_microfrontend_group/resource.tf new file mode 100644 index 00000000..805709ff --- /dev/null +++ b/examples/resources/vercel_microfrontend_group/resource.tf @@ -0,0 +1,22 @@ +data "vercel_project" "parent_mfe_project" { + name = "my parent project" +} + +data "vercel_project" "child_mfe_project" { + name = "my child project" +} + +resource "vercel_microfrontend_group" "example_mfe_group" { + name = "my mfe" + default_app = vercel_project.parent_mfe_project.id +} + +resource "vercel_microfrontend_group_membership" "parent_mfe_project_mfe_membership" { + project_id = vercel_project.parent_mfe_project.id + microfrontend_group_id = vercel_microfrontend_group.example_mfe_group.id +} + +resource "vercel_microfrontend_group_membership" "child_mfe_project_mfe_membership" { + project_id = vercel_project.child_mfe_project.id + microfrontend_group_id = vercel_microfrontend_group.example_mfe_group.id +} diff --git a/examples/resources/vercel_microfrontend_group_membership/import.sh b/examples/resources/vercel_microfrontend_group_membership/import.sh new file mode 100644 index 00000000..04ddbf93 --- /dev/null +++ b/examples/resources/vercel_microfrontend_group_membership/import.sh @@ -0,0 +1,11 @@ +# If importing into a personal account, or with a team configured on the provider, simply use the record id. +# - the microfrontend ID can be taken from the microfrontend settings page +# - the project ID can be taken from the project settings page +terraform import vercel_microfrontend_group_membership.example mfe_xxxxxxxxxxxxxxxxxxxxxxxxxxxx/pid_xxxxxxxxxxxxxxxxxxxxxxxxxxxx + +# Alternatively, you can import via the team_id and microfrontend_id. +# - team_id can be found in the team `settings` tab in the Vercel UI. +# - the microfrontend ID can be taken from the microfrontend settings page +# - the project ID can be taken from the project settings page +terraform import vercel_microfrontend_group_membership.example team_xxxxxxxxxxxxxxxxxxxxxxxx/mfe_xxxxxxxxxxxxxxxxxxxxxxxxxxxx/pid_xxxxxxxxxxxxxxxxxxxxxxxxxxxx + diff --git a/examples/resources/vercel_microfrontend_group_membership/resource.tf b/examples/resources/vercel_microfrontend_group_membership/resource.tf new file mode 100644 index 00000000..805709ff --- /dev/null +++ b/examples/resources/vercel_microfrontend_group_membership/resource.tf @@ -0,0 +1,22 @@ +data "vercel_project" "parent_mfe_project" { + name = "my parent project" +} + +data "vercel_project" "child_mfe_project" { + name = "my child project" +} + +resource "vercel_microfrontend_group" "example_mfe_group" { + name = "my mfe" + default_app = vercel_project.parent_mfe_project.id +} + +resource "vercel_microfrontend_group_membership" "parent_mfe_project_mfe_membership" { + project_id = vercel_project.parent_mfe_project.id + microfrontend_group_id = vercel_microfrontend_group.example_mfe_group.id +} + +resource "vercel_microfrontend_group_membership" "child_mfe_project_mfe_membership" { + project_id = vercel_project.child_mfe_project.id + microfrontend_group_id = vercel_microfrontend_group.example_mfe_group.id +} diff --git a/vercel/data_source_microfrontend_group.go b/vercel/data_source_microfrontend_group.go new file mode 100644 index 00000000..788fe96a --- /dev/null +++ b/vercel/data_source_microfrontend_group.go @@ -0,0 +1,129 @@ +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-log/tflog" + "github.com/vercel/terraform-provider-vercel/v2/client" +) + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ datasource.DataSource = µfrontendGroupDataSource{} + _ datasource.DataSourceWithConfigure = µfrontendGroupDataSource{} +) + +func newMicrofrontendGroupDataSource() datasource.DataSource { + return µfrontendGroupDataSource{} +} + +type microfrontendGroupDataSource struct { + client *client.Client +} + +func (d *microfrontendGroupDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_microfrontend_group" +} + +func (d *microfrontendGroupDataSource) 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 +} + +// Schema returns the schema information for an microfrontendGroup data source +func (r *microfrontendGroupDataSource) Schema(_ context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: ` +Provides information about an existing Microfrontend Group. + +A Microfrontend Group is a definition of a microfrontend belonging to a Vercel Team. +`, + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "A unique identifier for the group of microfrontends. Example: mfe_12HKQaOmR5t5Uy6vdcQsNIiZgHGB", + Required: true, + }, + "name": schema.StringAttribute{ + Description: "A human readable name for the microfrontends group.", + Computed: true, + }, + "slug": schema.StringAttribute{ + Description: "A slugified version of the name.", + Computed: true, + }, + "team_id": schema.StringAttribute{ + Description: "The team ID to add the project to. Required when configuring a team resource if a default team has not been set in the provider.", + Optional: true, + Computed: true, + }, + "default_app": schema.SingleNestedAttribute{ + Description: "The default app for the project. Used as the entry point for the microfrontend.", + Computed: true, + Attributes: map[string]schema.Attribute{ + "project_id": schema.StringAttribute{ + Description: "The ID of the project.", + Computed: true, + }, + "default_route": schema.StringAttribute{ + Description: "The default route for the project. Used for the screenshot of deployments.", + Computed: true, + }, + }, + }, + }, + } +} + +func (d *microfrontendGroupDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + tflog.Info(ctx, "Reading microfrontend group") + var config MicrofrontendGroup + diags := req.Config.Get(ctx, &config) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + out, err := d.client.GetMicrofrontendGroup(ctx, config.ID.ValueString(), config.TeamID.ValueString()) + if client.NotFound(err) { + resp.State.RemoveResource(ctx) + return + } + if err != nil { + resp.Diagnostics.AddError( + "Error reading microfrontend group", + fmt.Sprintf("Could not get microfrontend group %s %s, unexpected error: %s", + config.TeamID.ValueString(), + config.ID.ValueString(), + err, + ), + ) + return + } + + result := convertResponseToMicrofrontendGroup(out) + tflog.Info(ctx, "read microfrontend group", map[string]interface{}{ + "result": result, + }) + + diags = resp.State.Set(ctx, result) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} diff --git a/vercel/data_source_microfrontend_group_membership.go b/vercel/data_source_microfrontend_group_membership.go new file mode 100644 index 00000000..2eca3831 --- /dev/null +++ b/vercel/data_source_microfrontend_group_membership.go @@ -0,0 +1,123 @@ +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-log/tflog" + "github.com/vercel/terraform-provider-vercel/v2/client" +) + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ datasource.DataSource = µfrontendGroupMembershipDataSource{} + _ datasource.DataSourceWithConfigure = µfrontendGroupMembershipDataSource{} +) + +func newMicrofrontendGroupMembershipDataSource() datasource.DataSource { + return µfrontendGroupMembershipDataSource{} +} + +type microfrontendGroupMembershipDataSource struct { + client *client.Client +} + +func (d *microfrontendGroupMembershipDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_microfrontend_group_membership" +} + +func (d *microfrontendGroupMembershipDataSource) 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 +} + +// Schema returns the schema information for an microfrontendGroupMembership data source +func (r *microfrontendGroupMembershipDataSource) Schema(_ context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: ` +Provides information about an existing Microfrontend Group Membership. + +A Microfrontend Group Membership is a definition of a Vercel Project being a part of a Microfrontend Group. +`, + Attributes: map[string]schema.Attribute{ + "project_id": schema.StringAttribute{ + Description: "The ID of the project.", + Required: true, + }, + "microfrontend_group_id": schema.StringAttribute{ + Description: "The ID of the microfrontend group.", + Required: true, + }, + "team_id": schema.StringAttribute{ + Description: "The team ID to add the microfrontend group to. Required when configuring a team resource if a default team has not been set in the provider.", + Optional: true, + Computed: true, + }, + "default_route": schema.StringAttribute{ + Description: "The default route for the project. Used for the screenshot of deployments.", + Computed: true, + }, + "route_observability_to_this_project": schema.BoolAttribute{ + Description: "Whether the project is route observability for this project. If dalse, the project will be route observability for all projects to the default project.", + Computed: true, + }, + }, + } +} + +func (d *microfrontendGroupMembershipDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var config MicrofrontendGroupMembership + diags := req.Config.Get(ctx, &config) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + out, err := d.client.GetMicrofrontendGroupMembership(ctx, config.TeamID.ValueString(), + config.MicrofrontendGroupID.ValueString(), + config.ProjectID.ValueString(), + ) + if client.NotFound(err) { + resp.State.RemoveResource(ctx) + return + } + if err != nil { + resp.Diagnostics.AddError( + "Error reading microfrontend group membership", + fmt.Sprintf("Could not get microfrontend group %s %s, unexpected error: %s", + config.TeamID.ValueString(), + config.MicrofrontendGroupID.ValueString(), + err, + ), + ) + return + } + + result := convertResponseToMicrofrontendGroupMembership(out) + tflog.Info(ctx, "read microfrontend group membership", map[string]interface{}{ + "team_id": result.TeamID.ValueString(), + "group_id": result.MicrofrontendGroupID.ValueString(), + "project_id": result.ProjectID.ValueString(), + }) + + diags = resp.State.Set(ctx, result) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} diff --git a/vercel/data_source_microfrontend_group_test.go b/vercel/data_source_microfrontend_group_test.go new file mode 100644 index 00000000..3c0f75d5 --- /dev/null +++ b/vercel/data_source_microfrontend_group_test.go @@ -0,0 +1,57 @@ +package vercel_test + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestAcc_MicrofrontendGroupDataSource(t *testing.T) { + name := acctest.RandString(16) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(` + resource "vercel_project" "test_project_1" { + name = "test-acc-project-%[1]s" + %[2]s + } + resource "vercel_project" "test_project_2" { + name = "test-acc-project-2-%[1]s" + %[2]s + } + resource "vercel_microfrontend_group" "test_group" { + name = "test-acc-microfrontend-group-%[1]s" + default_app = { + project_id = vercel_project.test_project_1.id + } + %[2]s + } + resource "vercel_microfrontend_group_membership" "test_child" { + project_id = vercel_project.test_project_2.id + microfrontend_group_id = vercel_microfrontend_group.test_group.id + %[2]s + } + data "vercel_microfrontend_group" "test_group" { + id = vercel_microfrontend_group.test_group.id + %[2]s + } + data "vercel_microfrontend_group_membership" "test_child" { + microfrontend_group_id = vercel_microfrontend_group.test_group.id + project_id = vercel_project.test_project_2.id + %[2]s + } + `, name, teamIDConfig()), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("data.vercel_microfrontend_group.test_group", "name", "test-acc-microfrontend-group-"+name), + resource.TestCheckResourceAttrSet("data.vercel_microfrontend_group.test_group", "default_app.project_id"), + resource.TestCheckResourceAttr("data.vercel_microfrontend_group_membership.test_child", "%", "5"), + ), + }, + }, + }) +} diff --git a/vercel/provider.go b/vercel/provider.go index eb0ef20f..41d7c045 100644 --- a/vercel/provider.go +++ b/vercel/provider.go @@ -75,6 +75,8 @@ func (p *vercelProvider) Resources(_ context.Context) []func() resource.Resource newTeamConfigResource, newTeamMemberResource, newWebhookResource, + newMicrofrontendGroupResource, + newMicrofrontendGroupMembershipResource, } } @@ -101,6 +103,8 @@ func (p *vercelProvider) DataSources(_ context.Context) []func() datasource.Data newSharedEnvironmentVariableDataSource, newTeamConfigDataSource, newTeamMemberDataSource, + newMicrofrontendGroupDataSource, + newMicrofrontendGroupMembershipDataSource, } } diff --git a/vercel/provider_test.go b/vercel/provider_test.go index e4c97127..40fe1f89 100644 --- a/vercel/provider_test.go +++ b/vercel/provider_test.go @@ -23,6 +23,7 @@ func mustHaveEnv(t *testing.T, name string) { func testAccPreCheck(t *testing.T) { mustHaveEnv(t, "VERCEL_API_TOKEN") + mustHaveEnv(t, "VERCEL_TERRAFORM_TESTING_TEAM") mustHaveEnv(t, "VERCEL_TERRAFORM_TESTING_GITHUB_REPO") mustHaveEnv(t, "VERCEL_TERRAFORM_TESTING_GITLAB_REPO") mustHaveEnv(t, "VERCEL_TERRAFORM_TESTING_BITBUCKET_REPO") diff --git a/vercel/resource_microfrontend_group.go b/vercel/resource_microfrontend_group.go new file mode 100644 index 00000000..1917beee --- /dev/null +++ b/vercel/resource_microfrontend_group.go @@ -0,0 +1,403 @@ +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/objectplanmodifier" + "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" +) + +var ( + _ resource.Resource = µfrontendGroupResource{} + _ resource.ResourceWithConfigure = µfrontendGroupResource{} + _ resource.ResourceWithImportState = µfrontendGroupResource{} +) + +func newMicrofrontendGroupResource() resource.Resource { + return µfrontendGroupResource{} +} + +type microfrontendGroupResource struct { + client *client.Client +} + +func (r *microfrontendGroupResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_microfrontend_group" +} + +func (r *microfrontendGroupResource) 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 +} + +// Schema returns the schema information for a microfrontendGroup resource. +func (r *microfrontendGroupResource) Schema(_ context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: ` +Provides a Microfrontend Group resource. + +A Microfrontend Group is a definition of a microfrontend belonging to a Vercel Team. +`, + Attributes: map[string]schema.Attribute{ + "name": schema.StringAttribute{ + Description: "A human readable name for the microfrontends group.", + Required: true, + }, + "id": schema.StringAttribute{ + Description: "A unique identifier for the group of microfrontends. Example: mfe_12HKQaOmR5t5Uy6vdcQsNIiZgHGB", + Computed: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, + }, + "slug": schema.StringAttribute{ + Description: "A slugified version of the name.", + Computed: true, + }, + "team_id": schema.StringAttribute{ + Description: "The team ID to add the microfrontend group to. 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()}, + }, + "default_app": schema.SingleNestedAttribute{ + Description: "The default app for the project. Used as the entry point for the microfrontend.", + Required: true, + Attributes: getMicrofrontendGroupMembershipSchema(true), + PlanModifiers: []planmodifier.Object{ + objectplanmodifier.RequiresReplaceIf(func(ctx context.Context, req planmodifier.ObjectRequest, resp *objectplanmodifier.RequiresReplaceIfFuncResponse) { + oldDefaultApp, okOld := req.ConfigValue.ToObjectValue(ctx) + newDefaultApp, okNew := req.PlanValue.ToObjectValue(ctx) + if okOld.HasError() || okNew.HasError() { + return + } + oldValue := oldDefaultApp.Attributes()["project_id"] + newValue := newDefaultApp.Attributes()["project_id"] + + if oldValue != newValue { + resp.RequiresReplace = true + } + }, "The default app for the group has changed.", "The default app for the group has changed."), + }, + }, + }, + } +} + +type MicrofrontendGroupDefaultApp struct { + ProjectID types.String `tfsdk:"project_id"` + DefaultRoute types.String `tfsdk:"default_route"` +} + +type MicrofrontendGroup struct { + TeamID types.String `tfsdk:"team_id"` + ID types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + Slug types.String `tfsdk:"slug"` + DefaultApp *MicrofrontendGroupDefaultApp `tfsdk:"default_app"` +} + +func convertResponseToMicrofrontendGroup(group client.MicrofrontendGroup) MicrofrontendGroup { + return MicrofrontendGroup{ + ID: types.StringValue(group.ID), + Name: types.StringValue(group.Name), + Slug: types.StringValue(group.Slug), + TeamID: types.StringValue(group.TeamID), + DefaultApp: &MicrofrontendGroupDefaultApp{ + ProjectID: types.StringValue(group.DefaultApp.ProjectID), + DefaultRoute: types.StringValue(group.DefaultApp.DefaultRoute), + }, + } +} + +func (r *microfrontendGroupResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan MicrofrontendGroup + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + resp.Diagnostics.AddError( + "Error getting microfrontend group plan", + "Error getting microfrontend group plan", + ) + return + } + + tflog.Info(ctx, "creating microfrontend group", map[string]interface{}{ + "team_id": plan.TeamID.ValueString(), + "name": plan.Name.ValueString(), + }) + + out, err := r.client.CreateMicrofrontendGroup(ctx, plan.TeamID.ValueString(), plan.Name.ValueString()) + if err != nil { + resp.Diagnostics.AddError( + "Error creating microfrontend group", + "Could not create microfrontend group, unexpected error: "+err.Error(), + ) + return + } + + tflog.Info(ctx, "creating default group membership", map[string]interface{}{ + "team_id": plan.TeamID.ValueString(), + "name": plan.Name.ValueString(), + "default_app": plan.DefaultApp.ProjectID.ValueString(), + }) + + default_app, err := r.client.AddOrUpdateMicrofrontendGroupMembership(ctx, client.MicrofrontendGroupMembership{ + ProjectID: plan.DefaultApp.ProjectID.ValueString(), + MicrofrontendGroupID: out.ID, + TeamID: plan.TeamID.ValueString(), + DefaultRoute: plan.DefaultApp.DefaultRoute.ValueString(), + IsDefaultApp: true, + }) + + if err != nil { + resp.Diagnostics.AddError( + "Error creating microfrontend default app group membership", + "Could not create microfrontend default app group membership, unexpected error: "+err.Error(), + ) + return + } + + group := client.MicrofrontendGroup{ + ID: out.ID, + Name: out.Name, + Slug: out.Slug, + TeamID: out.TeamID, + DefaultApp: client.MicrofrontendGroupMembership{ + ProjectID: default_app.ProjectID, + TeamID: default_app.TeamID, + DefaultRoute: default_app.DefaultRoute, + RouteObservabilityToThisProject: default_app.RouteObservabilityToThisProject, + MicrofrontendGroupID: out.ID, + IsDefaultApp: default_app.IsDefaultApp, + }, + Projects: out.Projects, + } + + result := convertResponseToMicrofrontendGroup(group) + tflog.Info(ctx, "created microfrontend group", map[string]interface{}{ + "team_id": result.TeamID.ValueString(), + "group_id": result.ID.ValueString(), + "slug": result.Slug.ValueString(), + "name": result.Name.ValueString(), + "default_app": result.DefaultApp.ProjectID.ValueString(), + }) + + diags = resp.State.Set(ctx, result) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +func (r *microfrontendGroupResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state MicrofrontendGroup + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + out, err := r.client.GetMicrofrontendGroup(ctx, state.ID.ValueString(), state.TeamID.ValueString()) + if client.NotFound(err) { + resp.State.RemoveResource(ctx) + return + } + if err != nil { + resp.Diagnostics.AddError( + "Error reading microfrontend group", + fmt.Sprintf("Could not get microfrontend group %s %s, unexpected error: %s", + state.TeamID.ValueString(), + state.ID.ValueString(), + err, + ), + ) + return + } + + result := convertResponseToMicrofrontendGroup(out) + tflog.Info(ctx, "read microfrontend group", map[string]interface{}{ + "defaultApp": result.DefaultApp.ProjectID.ValueString(), + "team_id": result.TeamID.ValueString(), + "group_id": result.ID.ValueString(), + "slug": result.Slug.ValueString(), + "name": result.Name.ValueString(), + }) + + diags = resp.State.Set(ctx, result) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +func (r *microfrontendGroupResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan MicrofrontendGroup + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + resp.Diagnostics.AddError( + "Error getting microfrontend group plan", + "Error getting microfrontend group plan", + ) + return + } + + var state MicrofrontendGroup + diags = req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + out, err := r.client.UpdateMicrofrontendGroup(ctx, client.MicrofrontendGroup{ + ID: state.ID.ValueString(), + Name: plan.Name.ValueString(), + TeamID: state.TeamID.ValueString(), + }) + if err != nil { + resp.Diagnostics.AddError( + "Error updating microfrontend group", + fmt.Sprintf( + "Could not update microfrontend group %s %s, unexpected error: %s", + state.TeamID.ValueString(), + state.ID.ValueString(), + err, + ), + ) + return + } + + tflog.Info(ctx, "updated microfrontend group", map[string]interface{}{ + "team_id": out.TeamID, + "group_id": out.ID, + "name": out.Name, + "slug": out.Slug, + }) + + result := convertResponseToMicrofrontendGroup(out) + + diags = resp.State.Set(ctx, result) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +func (r *microfrontendGroupResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state MicrofrontendGroup + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + tflog.Info(ctx, "deleting microfrontend default app group membership", map[string]interface{}{ + "group_id": state.ID.ValueString(), + "project_id": state.DefaultApp.ProjectID.ValueString(), + "team_id": state.TeamID.ValueString(), + }) + + _, err := r.client.RemoveMicrofrontendGroupMembership(ctx, client.MicrofrontendGroupMembership{ + MicrofrontendGroupID: state.ID.ValueString(), + TeamID: state.TeamID.ValueString(), + ProjectID: state.DefaultApp.ProjectID.ValueString(), + }) + + if err != nil { + resp.Diagnostics.AddError( + "Error deleting microfrontend default app group membership", + fmt.Sprintf( + "Could not delete microfrontend default app group membership %s %s, unexpected error: %s", + state.ID.ValueString(), + state.DefaultApp.ProjectID.ValueString(), + err, + ), + ) + return + } + + tflog.Info(ctx, "deleting microfrontend group", map[string]interface{}{ + "group_id": state.ID.ValueString(), + }) + + _, err = r.client.DeleteMicrofrontendGroup(ctx, client.MicrofrontendGroup{ + ID: state.ID.ValueString(), + TeamID: state.TeamID.ValueString(), + Slug: state.Slug.ValueString(), + Name: state.Name.ValueString(), + }) + if err != nil { + resp.Diagnostics.AddError( + "Error deleting microfrontend group", + fmt.Sprintf( + "Could not delete microfrontend group %s, unexpected error: %s", + state.ID.ValueString(), + err, + ), + ) + return + } + tflog.Info(ctx, "deleted microfrontendGroup", map[string]any{ + "group_id": state.ID.ValueString(), + }) +} + +func (r *microfrontendGroupResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + teamID, microfrontendID, ok := splitInto1Or2(req.ID) + if !ok { + resp.Diagnostics.AddError( + "Error importing Microfrontend Group", + fmt.Sprintf("Invalid id '%s' specified. should be in format \"team_id/microfrontend_id\" or \"microfrontend_id\"", req.ID), + ) + } + out, err := r.client.GetMicrofrontendGroup(ctx, microfrontendID, teamID) + if client.NotFound(err) { + resp.State.RemoveResource(ctx) + return + } + if err != nil { + resp.Diagnostics.AddError( + "Error importing microfrontend group", + fmt.Sprintf("Could not import microfrontend group %s %s, unexpected error: %s", + teamID, + microfrontendID, + err, + ), + ) + return + } + + result := convertResponseToMicrofrontendGroup(out) + tflog.Info(ctx, "import microfrontend group", map[string]interface{}{ + "defaultApp": result.DefaultApp.ProjectID.ValueString(), + "team_id": result.TeamID.ValueString(), + "group_id": result.ID.ValueString(), + "slug": result.Slug.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_microfrontend_group_membership.go b/vercel/resource_microfrontend_group_membership.go new file mode 100644 index 00000000..0f403374 --- /dev/null +++ b/vercel/resource_microfrontend_group_membership.go @@ -0,0 +1,350 @@ +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/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier" + "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" +) + +var ( + _ resource.Resource = µfrontendGroupMembershipResource{} + _ resource.ResourceWithConfigure = µfrontendGroupMembershipResource{} + _ resource.ResourceWithImportState = µfrontendGroupMembershipResource{} +) + +func newMicrofrontendGroupMembershipResource() resource.Resource { + return µfrontendGroupMembershipResource{} +} + +type microfrontendGroupMembershipResource struct { + client *client.Client +} + +func (r *microfrontendGroupMembershipResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_microfrontend_group_membership" +} + +func (r *microfrontendGroupMembershipResource) 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 getMicrofrontendGroupMembershipSchema(isDefaultApp bool) map[string]schema.Attribute { + res := map[string]schema.Attribute{} + + res["project_id"] = schema.StringAttribute{ + Description: "The ID of the project.", + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + } + res["default_route"] = schema.StringAttribute{ + Description: "The default route for the project. Used for the screenshot of deployments.", + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, + } + + if !isDefaultApp { + res["microfrontend_group_id"] = schema.StringAttribute{ + Description: "The ID of the microfrontend group.", + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + } + res["team_id"] = schema.StringAttribute{ + Description: "The team ID to add the microfrontend group to. 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()}, + } + res["route_observability_to_this_project"] = schema.BoolAttribute{ + Description: "Whether the project is route observability for this project. If dalse, the project will be route observability for all projects to the default project.", + Optional: true, + Computed: true, + Default: booldefault.StaticBool(true), + PlanModifiers: []planmodifier.Bool{boolplanmodifier.UseStateForUnknown()}, + } + } + + return res +} + +// Schema returns the schema information for a microfrontendGroupMembership resource. +func (r *microfrontendGroupMembershipResource) Schema(_ context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: ` +Provides a Microfrontend Group Membership resource. + +A Microfrontend Group Membership is a definition of a Vercel Project being a part of a Microfrontend Group. +`, + Attributes: getMicrofrontendGroupMembershipSchema(false), + } +} + +type MicrofrontendGroupMembership struct { + ProjectID types.String `tfsdk:"project_id"` + MicrofrontendGroupID types.String `tfsdk:"microfrontend_group_id"` + TeamID types.String `tfsdk:"team_id"` + DefaultRoute types.String `tfsdk:"default_route"` + RouteObservabilityToThisProject types.Bool `tfsdk:"route_observability_to_this_project"` +} + +func convertResponseToMicrofrontendGroupMembership(membership client.MicrofrontendGroupMembership) MicrofrontendGroupMembership { + return MicrofrontendGroupMembership{ + ProjectID: types.StringValue(membership.ProjectID), + MicrofrontendGroupID: types.StringValue(membership.MicrofrontendGroupID), + TeamID: types.StringValue(membership.TeamID), + DefaultRoute: types.StringValue(membership.DefaultRoute), + RouteObservabilityToThisProject: types.BoolValue(membership.RouteObservabilityToThisProject), + } +} + +func (r *microfrontendGroupMembershipResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan MicrofrontendGroupMembership + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + resp.Diagnostics.AddError( + "Error getting microfrontend group membership plan", + "Error getting microfrontend group membership plan", + ) + return + } + + tflog.Info(ctx, "creating microfrontend group membership", map[string]interface{}{ + "project_id": plan.ProjectID.ValueString(), + "group_id": plan.MicrofrontendGroupID.ValueString(), + "plan": plan, + }) + + out, err := r.client.AddOrUpdateMicrofrontendGroupMembership(ctx, client.MicrofrontendGroupMembership{ + ProjectID: plan.ProjectID.ValueString(), + MicrofrontendGroupID: plan.MicrofrontendGroupID.ValueString(), + DefaultRoute: plan.DefaultRoute.ValueString(), + RouteObservabilityToThisProject: plan.RouteObservabilityToThisProject.ValueBool(), + TeamID: plan.TeamID.ValueString(), + IsDefaultApp: false, + }) + if err != nil { + resp.Diagnostics.AddError( + "Error creating microfrontend group membership", + "Could not create microfrontend group, unexpected error: "+err.Error(), + ) + return + } + + result := convertResponseToMicrofrontendGroupMembership(out) + tflog.Info(ctx, "created microfrontend group membership", map[string]interface{}{ + "project_id": result.ProjectID.ValueString(), + "group_id": result.MicrofrontendGroupID.ValueString(), + }) + + diags = resp.State.Set(ctx, result) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +func (r *microfrontendGroupMembershipResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state MicrofrontendGroupMembership + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + if state.ProjectID.ValueString() == "" || state.MicrofrontendGroupID.ValueString() == "" { + resp.State.RemoveResource(ctx) + return + } + + out, err := r.client.GetMicrofrontendGroupMembership(ctx, + state.TeamID.ValueString(), + state.MicrofrontendGroupID.ValueString(), + state.ProjectID.ValueString(), + ) + if client.NotFound(err) { + resp.State.RemoveResource(ctx) + return + } + if err != nil { + resp.Diagnostics.AddError( + "Error reading microfrontend group membership", + fmt.Sprintf("Could not get microfrontend group membership %s %s, unexpected error: %s", + state.ProjectID.ValueString(), + state.MicrofrontendGroupID.ValueString(), + err, + ), + ) + return + } + + result := convertResponseToMicrofrontendGroupMembership(out) + tflog.Info(ctx, "read microfrontend group membership", map[string]interface{}{ + "team_id": result.TeamID.ValueString(), + "group_id": result.MicrofrontendGroupID.ValueString(), + "project_id": result.ProjectID.ValueString(), + }) + + diags = resp.State.Set(ctx, result) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +func (r *microfrontendGroupMembershipResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan MicrofrontendGroupMembership + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + resp.Diagnostics.AddError( + "Error getting microfrontend group plan", + "Error getting microfrontend group plan", + ) + return + } + + var state MicrofrontendGroupMembership + diags = req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + out, err := r.client.AddOrUpdateMicrofrontendGroupMembership(ctx, client.MicrofrontendGroupMembership{ + ProjectID: plan.ProjectID.ValueString(), + MicrofrontendGroupID: plan.MicrofrontendGroupID.ValueString(), + DefaultRoute: plan.DefaultRoute.ValueString(), + RouteObservabilityToThisProject: plan.RouteObservabilityToThisProject.ValueBool(), + TeamID: plan.TeamID.ValueString(), + IsDefaultApp: false, + }) + if err != nil { + resp.Diagnostics.AddError( + "Error updating microfrontend group membership", + fmt.Sprintf( + "Could not update microfrontend group membership %s %s %s, unexpected error: %s", + state.TeamID.ValueString(), + state.MicrofrontendGroupID.ValueString(), + state.ProjectID.ValueString(), + err, + ), + ) + return + } + + tflog.Info(ctx, "updated microfrontend group membership", map[string]interface{}{ + "team_id": out.TeamID, + "microfrontend_group_id": out.MicrofrontendGroupID, + "project_id": out.ProjectID, + }) + + result := convertResponseToMicrofrontendGroupMembership(out) + + diags = resp.State.Set(ctx, result) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +func (r *microfrontendGroupMembershipResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state MicrofrontendGroupMembership + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + tflog.Info(ctx, "deleting microfrontend group membership", map[string]interface{}{ + "project_id": state.ProjectID.ValueString(), + "group_id": state.MicrofrontendGroupID.ValueString(), + "team_id": state.TeamID.ValueString(), + }) + + _, err := r.client.RemoveMicrofrontendGroupMembership(ctx, client.MicrofrontendGroupMembership{ + MicrofrontendGroupID: state.MicrofrontendGroupID.ValueString(), + ProjectID: state.ProjectID.ValueString(), + TeamID: state.TeamID.ValueString(), + IsDefaultApp: false, + }) + if err != nil { + resp.Diagnostics.AddError( + "Error deleting microfrontend group membership", + fmt.Sprintf( + "Could not delete microfrontend group membership %s %s, unexpected error: %s", + state.MicrofrontendGroupID.ValueString(), + state.ProjectID.ValueString(), + err, + ), + ) + return + } + tflog.Info(ctx, "deleted microfrontend group membership", map[string]any{ + "group_id": state.MicrofrontendGroupID.ValueString(), + "project_id": state.ProjectID.ValueString(), + }) +} + +func (r *microfrontendGroupMembershipResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + teamID, microfrontendID, projectID, ok := splitInto2Or3(req.ID) + if !ok { + resp.Diagnostics.AddError( + "Error importing Microfrontend Group Membership", + fmt.Sprintf("Invalid id '%s' specified. should be in format \"team_id/microfrontend_id/project_id\" or \"microfrontend_id/project_id\"", req.ID), + ) + } + out, err := r.client.GetMicrofrontendGroupMembership(ctx, teamID, microfrontendID, projectID) + if client.NotFound(err) { + resp.State.RemoveResource(ctx) + return + } + if err != nil { + resp.Diagnostics.AddError( + "Error importing microfrontend group membership", + fmt.Sprintf("Could not import microfrontend group membership %s %s %s, unexpected error: %s", + teamID, + microfrontendID, + projectID, + err, + ), + ) + return + } + + result := convertResponseToMicrofrontendGroupMembership(out) + tflog.Info(ctx, "import microfrontend group", map[string]interface{}{ + "project_id": result.ProjectID.ValueString(), + "group_id": result.MicrofrontendGroupID.ValueString(), + "team_id": result.TeamID.ValueString(), + }) + + diags := resp.State.Set(ctx, result) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} diff --git a/vercel/resource_microfrontend_group_test.go b/vercel/resource_microfrontend_group_test.go new file mode 100644 index 00000000..55ebecd2 --- /dev/null +++ b/vercel/resource_microfrontend_group_test.go @@ -0,0 +1,92 @@ +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" +) + +func testCheckMicrofrontendGroupExists(teamID, 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().GetMicrofrontendGroup(context.TODO(), rs.Primary.ID, teamID) + return err + } +} + +func testCheckMicrofrontendGroupDeleted(n, teamID 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().GetMicrofrontendGroup(context.TODO(), rs.Primary.ID, teamID) + if err == nil { + return fmt.Errorf("expected not_found error, but got no error") + } + if !(err.Error() == "microfrontend group not found") { + return fmt.Errorf("Unexpected error checking for deleted microfrontend group: %s", err) + } + + return nil + } +} + +func TestAcc_MicrofrontendGroupResource(t *testing.T) { + name := acctest.RandString(16) + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + CheckDestroy: testCheckMicrofrontendGroupDeleted("vercel_microfrontend_group.test", testTeam()), + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(` + resource "vercel_project" "test" { + name = "test-acc-project-%[1]s" + %[2]s + } + resource "vercel_project" "test-2" { + name = "test-acc-project-2-%[1]s" + %[2]s + } + resource "vercel_microfrontend_group" "test" { + name = "test-acc-microfrontend-group-%[1]s" + default_app = { + project_id = vercel_project.test.id + } + %[2]s + } + resource "vercel_microfrontend_group_membership" "test-2" { + project_id = vercel_project.test-2.id + microfrontend_group_id = vercel_microfrontend_group.test.id + %[2]s + } + `, name, teamIDConfig()), + Check: resource.ComposeAggregateTestCheckFunc( + testCheckMicrofrontendGroupExists(testTeam(), "vercel_microfrontend_group.test"), + resource.TestCheckResourceAttr("vercel_microfrontend_group.test", "name", "test-acc-microfrontend-group-"+name), + resource.TestCheckResourceAttrSet("vercel_microfrontend_group.test", "id"), + resource.TestCheckResourceAttrSet("vercel_microfrontend_group.test", "default_app.project_id"), + resource.TestCheckResourceAttrSet("vercel_microfrontend_group_membership.test-2", "microfrontend_group_id"), + ), + }, + }, + }) +}