From eec53e01d39f9d87077865747cb271522d914f81 Mon Sep 17 00:00:00 2001 From: Kit Foster Date: Tue, 18 Feb 2025 14:39:47 +0100 Subject: [PATCH 01/12] Add microfrontend_group resource and data_source --- client/microfrontend_group.go | 192 ++++++++++++++++ docs/data-sources/microfrontend_group.md | 34 +++ docs/resources/microfrontend_group.md | 34 +++ vercel/data_source_microfrontend_group.go | 138 +++++++++++ vercel/provider.go | 2 + vercel/resource_microfrontend_group.go | 267 ++++++++++++++++++++++ 6 files changed, 667 insertions(+) create mode 100644 client/microfrontend_group.go create mode 100644 docs/data-sources/microfrontend_group.md create mode 100644 docs/resources/microfrontend_group.md create mode 100644 vercel/data_source_microfrontend_group.go create mode 100644 vercel/resource_microfrontend_group.go diff --git a/client/microfrontend_group.go b/client/microfrontend_group.go new file mode 100644 index 00000000..ec84377a --- /dev/null +++ b/client/microfrontend_group.go @@ -0,0 +1,192 @@ +package client + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-log/tflog" +) + +type CreateMicrofrontendGroupRequestAPI struct { + NewMicrofrontendsGroupName string `json:"newMicrofrontendsGroupName"` +} + +// CreateMicrofrontendGroupRequest defines the request the Vercel API expects in order to create a microfrontend group. +type CreateMicrofrontendGroupRequest struct { + Name string `json:"name"` + TeamID string `json:"team_id"` +} + +// MicrofrontendGroupResponse defines the response the Vercel API returns when a microfrontend group is created or updated. +type NewMicrofrontendGroupResponseAPI struct { + NewMicrofrontendGroup MicrofrontendGroupResponse `json:"newMicrofrontendsGroup"` +} + +type MicrofrontendGroupResponse struct { + ID string `json:"id"` + Name string `json:"name"` + Slug string `json:"slug"` + TeamID string `json:"team_id"` +} + +// CreateMicrofrontendGroup creates a microfrontend group within Vercel. +func (c *Client) CreateMicrofrontendGroup(ctx context.Context, request CreateMicrofrontendGroupRequest) (r MicrofrontendGroupResponse, err error) { + url := fmt.Sprintf("%s/teams/%s/microfrontends", c.baseURL, c.teamID(request.TeamID)) + payload := string(mustMarshal(CreateMicrofrontendGroupRequestAPI{ + NewMicrofrontendsGroupName: request.Name, + })) + + tflog.Info(ctx, "creating microfrontend group", map[string]interface{}{ + "url": url, + "payload": payload, + }) + apiResponse := NewMicrofrontendGroupResponseAPI{} + err = c.doRequest(clientRequest{ + ctx: ctx, + method: "PATCH", + url: url, + body: payload, + }, &apiResponse) + if err != nil { + return r, err + } + return MicrofrontendGroupResponse{ + ID: apiResponse.NewMicrofrontendGroup.ID, + Name: apiResponse.NewMicrofrontendGroup.Name, + Slug: apiResponse.NewMicrofrontendGroup.Slug, + TeamID: c.teamID(request.TeamID), + }, nil +} + +type UpdateMicrofrontendGroupRequestAPI struct { + Name string `json:"name"` +} +type UpdateMicrofrontendGroupRequest struct { + ID string `json:"id"` + Name string `json:"name"` + TeamID string `json:"team_id"` +} + +type UpdateMicrofrontendGroupResponseAPI struct { + UpdatedMicrofrontendsGroup MicrofrontendGroupResponseInner `json:"updatedMicrofrontendsGroup"` +} + +// UpdateMicrofrontendGroup updates a microfrontend group within Vercel. +func (c *Client) UpdateMicrofrontendGroup(ctx context.Context, request UpdateMicrofrontendGroupRequest) (r MicrofrontendGroupResponse, err error) { + url := fmt.Sprintf("%s/teams/%s/microfrontends/%s", c.baseURL, c.teamID(request.TeamID), request.ID) + payload := string(mustMarshal(UpdateMicrofrontendGroupRequestAPI{ + Name: request.Name, + })) + + tflog.Info(ctx, "updating microfrontend group", map[string]interface{}{ + "url": url, + "payload": payload, + }) + apiResponse := UpdateMicrofrontendGroupResponseAPI{} + err = c.doRequest(clientRequest{ + ctx: ctx, + method: "PATCH", + url: url, + body: payload, + }, &apiResponse) + if err != nil { + return r, err + } + return MicrofrontendGroupResponse{ + ID: apiResponse.UpdatedMicrofrontendsGroup.ID, + Name: apiResponse.UpdatedMicrofrontendsGroup.Name, + Slug: apiResponse.UpdatedMicrofrontendsGroup.Slug, + TeamID: c.teamID(request.TeamID), + }, nil +} + +// MicrofrontendGroupResponse defines the response the Vercel API returns when a microfrontend group is deleted. +type DeleteMicrofrontendGroupResponse struct{} + +// DeleteMicrofrontendGroup deletes a microfrontend group within Vercel. +func (c *Client) DeleteMicrofrontendGroup(ctx context.Context, microfrontendGroupID string, teamID string) (r DeleteMicrofrontendGroupResponse, err error) { + url := fmt.Sprintf("%s/teams/%s/microfrontends/%s", c.baseURL, c.teamID(teamID), microfrontendGroupID) + + 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 +} + +type ProjectMicrofrontend struct { + Enabled bool `json:"enabled"` + GroupIds []string `json:"groupIds"` + IsDefaultApp bool `json:"isDefaultApp"` + UpdatedAt string `json:"updatedAt"` +} + +type MicrofrontendProjectResponse struct { + ID string `json:"id"` + Name string `json:"name"` + Framework string `json:"framework"` + Microfrontends ProjectMicrofrontend `json:"microfrontends"` +} + +type MicrofrontendGroupResponseInner struct { + ID string `json:"id"` + Name string `json:"name"` + Slug string `json:"slug"` +} + +type MicrofrontendGroupsResponseInner struct { + Group MicrofrontendGroupResponseInner `json:"group"` + Projects []ProjectResponse `json:"projects"` +} + +type MicrofrontendGroupsResponse struct { + Groups []MicrofrontendGroupsResponseInner `json:"groups"` +} + +// GetMicrofrontendGroups retrieves information from Vercel about existing Microfrontend Groups. +func (c *Client) GetMicrofrontendGroups(ctx context.Context, teamID string) (r MicrofrontendGroupsResponse, err error) { + 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, + }) + err = c.doRequest(clientRequest{ + ctx: ctx, + method: "GET", + url: url, + body: "", + }, &r) + return r, err +} + +// GetMicrofrontendGroup retrieves information from Vercel about an existing MicrofrontendGroup. +func (c *Client) GetMicrofrontendGroup(ctx context.Context, microfrontendGroupID string, teamID string) (r MicrofrontendGroupResponse, err error) { + res, err := c.GetMicrofrontendGroups(ctx, teamID) + + if err != nil { + return r, err + } + + fmt.Print(res) + + for i := range res.Groups { + if res.Groups[i].Group.ID == microfrontendGroupID { + return MicrofrontendGroupResponse{ + ID: res.Groups[i].Group.ID, + Name: res.Groups[i].Group.Name, + Slug: res.Groups[i].Group.Slug, + TeamID: teamID, + }, nil + } + } + + return r, fmt.Errorf("microfrontend group not found") +} diff --git a/docs/data-sources/microfrontend_group.md b/docs/data-sources/microfrontend_group.md new file mode 100644 index 00000000..5b95e2cb --- /dev/null +++ b/docs/data-sources/microfrontend_group.md @@ -0,0 +1,34 @@ +--- +# 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. + Projects are added to a Microfrontend Group. +--- + +# 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. +Projects are added to a Microfrontend Group. + + + + +## 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 + +- `name` (String) A human readable name for the microfrontends group. +- `slug` (String) A slugified version of the name. diff --git a/docs/resources/microfrontend_group.md b/docs/resources/microfrontend_group.md new file mode 100644 index 00000000..0f9296a6 --- /dev/null +++ b/docs/resources/microfrontend_group.md @@ -0,0 +1,34 @@ +--- +# 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. + Projects are added to a Microfrontend Group. +--- + +# vercel_microfrontend_group (Resource) + +Provides a Microfrontend Group resource. + +A Microfrontend Group is a definition of a microfrontend belonging to a Vercel Team. +Projects are added to a Microfrontend Group. + + + + +## Schema + +### Required + +- `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. diff --git a/vercel/data_source_microfrontend_group.go b/vercel/data_source_microfrontend_group.go new file mode 100644 index 00000000..a21de4c0 --- /dev/null +++ b/vercel/data_source_microfrontend_group.go @@ -0,0 +1,138 @@ +package vercel + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/vercel/terraform-provider-vercel/v2/client" +) + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ datasource.DataSource = µ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. +Projects are added to a Microfrontend Group. +`, + 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, + }, + }, + } +} + +type MicrofrontendGroupDataSource struct { + ID types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + Slug types.String `tfsdk:"slug"` + TeamID types.String `tfsdk:"team_id"` +} + +func convertResponseToMicrofrontendGroupDataSource(in client.MicrofrontendGroupResponse) MicrofrontendGroupDataSource { + return MicrofrontendGroupDataSource{ + ID: types.StringValue(in.ID), + Name: types.StringValue(in.Name), + Slug: types.StringValue(in.Slug), + TeamID: types.StringValue(in.TeamID), + } +} + +// Read will read the microfrontendGroup information by requesting it from the Vercel API, and will update terraform +// with this information. +// It is called by the provider whenever data source values should be read to update state. +func (d *microfrontendGroupDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var config MicrofrontendGroupDataSource + 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 microfrontendGroup", + fmt.Sprintf("Could not get microfrontend group %s %s, unexpected error: %s", + config.TeamID.ValueString(), + config.ID.ValueString(), + err, + ), + ) + return + } + + result := convertResponseToMicrofrontendGroupDataSource(out) + tflog.Info(ctx, "read microfrontendGroup", map[string]interface{}{ + "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/provider.go b/vercel/provider.go index eb0ef20f..a2f12e40 100644 --- a/vercel/provider.go +++ b/vercel/provider.go @@ -75,6 +75,7 @@ func (p *vercelProvider) Resources(_ context.Context) []func() resource.Resource newTeamConfigResource, newTeamMemberResource, newWebhookResource, + newMicrofrontendGroupResource, } } @@ -101,6 +102,7 @@ func (p *vercelProvider) DataSources(_ context.Context) []func() datasource.Data newSharedEnvironmentVariableDataSource, newTeamConfigDataSource, newTeamMemberDataSource, + newMicrofrontendGroupDataSource, } } diff --git a/vercel/resource_microfrontend_group.go b/vercel/resource_microfrontend_group.go new file mode 100644 index 00000000..9e11b70e --- /dev/null +++ b/vercel/resource_microfrontend_group.go @@ -0,0 +1,267 @@ +package vercel + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/vercel/terraform-provider-vercel/v2/client" +) + +var ( + _ resource.Resource = µfrontendGroupResource{} + _ resource.ResourceWithConfigure = µ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. +Projects are added to a Microfrontend Group. +`, + 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, + }, + "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()}, + }, + }, + } +} + +// MicrofrontendGroup represents the terraform state for a microfrontendGroup resource. +type MicrofrontendGroup struct { + TeamID types.String `tfsdk:"team_id"` + ID types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + Slug types.String `tfsdk:"slug"` +} + +// convertResponseToMicrofrontendGroup is used to populate terraform state based on an API response. +// Where possible, values from the API response are used to populate state. If not possible, +// values from the existing microfrontendGroup state are used. +func convertResponseToMicrofrontendGroup(response client.MicrofrontendGroupResponse) MicrofrontendGroup { + return MicrofrontendGroup{ + ID: types.StringValue(response.ID), + Name: types.StringValue(response.Name), + Slug: types.StringValue(response.Slug), + TeamID: types.StringValue(response.TeamID), + } +} + +// Create will create a microfrontendGroup within Vercel. This is done by first attempting to trigger a microfrontendGroup, seeing what +// files are required, uploading those files, and then attempting to create a microfrontendGroup again. +// This is called automatically by the provider when a new resource should be created. +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 microfrontendGroup plan", + "Error getting microfrontendGroup plan", + ) + return + } + + cdr := client.CreateMicrofrontendGroupRequest{ + Name: plan.Name.ValueString(), + TeamID: plan.TeamID.ValueString(), + } + + out, err := r.client.CreateMicrofrontendGroup(ctx, cdr) + if err != nil { + resp.Diagnostics.AddError( + "Error creating microfrontend group", + "Could not create microfrontend group, unexpected error: "+err.Error(), + ) + return + } + + result := convertResponseToMicrofrontendGroup(out) + tflog.Info(ctx, "created microfrontendGroup", map[string]interface{}{ + "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 + } +} + +// Read will read a file from the filesytem and provide terraform with information about it. +// It is called by the provider whenever data source values should be read to update state. +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 microfrontendGroup", + fmt.Sprintf("Could not get microfrontendGroup %s %s, unexpected error: %s", + state.TeamID.ValueString(), + state.ID.ValueString(), + err, + ), + ) + return + } + + result := convertResponseToMicrofrontendGroup(out) + tflog.Info(ctx, "read microfrontendGroup", map[string]interface{}{ + "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 + } +} + +// Update updates the microfrontendGroup state. +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 microfrontendGroup plan", + "Error getting microfrontendGroup 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.UpdateMicrofrontendGroupRequest{ + 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 + } +} + +// Delete deletes a MicrofrontendGroup. +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 + } + + _, err := r.client.DeleteMicrofrontendGroup(ctx, state.ID.ValueString(), state.TeamID.ValueString()) + if err != nil { + resp.Diagnostics.AddError( + "Error deleting microfrontendGroup", + fmt.Sprintf( + "Could not delete microfrontendGroup %s, unexpected error: %s", + state.ID.ValueString(), + err, + ), + ) + return + } + tflog.Info(ctx, "deleted microfrontendGroup", map[string]any{ + "group_id": state.ID.ValueString(), + }) +} From 5392042182454380dbfb5c4156604ba43b3418aa Mon Sep 17 00:00:00 2001 From: Kit Foster Date: Wed, 19 Feb 2025 19:44:47 +0100 Subject: [PATCH 02/12] Add microfrontend_group projects --- client/microfrontend_group.go | 148 +++++--------- client/microfrontend_project.go | 114 +++++++++++ docs/data-sources/microfrontend_group.md | 14 +- docs/resources/microfrontend_group.md | 38 +++- vercel/data_source_microfrontend_group.go | 48 ++--- vercel/resource_microfrontend_group.go | 223 ++++++++++++++++++---- 6 files changed, 426 insertions(+), 159 deletions(-) create mode 100644 client/microfrontend_project.go diff --git a/client/microfrontend_group.go b/client/microfrontend_group.go index ec84377a..298700b7 100644 --- a/client/microfrontend_group.go +++ b/client/microfrontend_group.go @@ -7,40 +7,36 @@ import ( "github.com/hashicorp/terraform-plugin-log/tflog" ) -type CreateMicrofrontendGroupRequestAPI struct { - NewMicrofrontendsGroupName string `json:"newMicrofrontendsGroupName"` +type MicrofrontendGroup struct { + ID string `json:"id"` + Name string `json:"name"` + Slug string `json:"slug"` + TeamID string `json:"team_id"` + Projects map[string]MicrofrontendProject `json:"projects"` } -// CreateMicrofrontendGroupRequest defines the request the Vercel API expects in order to create a microfrontend group. -type CreateMicrofrontendGroupRequest struct { - Name string `json:"name"` - TeamID string `json:"team_id"` +type MicrofrontendGroupsAPI struct { + Group MicrofrontendGroup `json:"group"` + Projects []MicrofrontendProjectsResponseAPI `json:"projects"` } -// MicrofrontendGroupResponse defines the response the Vercel API returns when a microfrontend group is created or updated. -type NewMicrofrontendGroupResponseAPI struct { - NewMicrofrontendGroup MicrofrontendGroupResponse `json:"newMicrofrontendsGroup"` +type MicrofrontendGroupsAPIResponse struct { + Groups []MicrofrontendGroupsAPI `json:"groups"` } -type MicrofrontendGroupResponse struct { - ID string `json:"id"` - Name string `json:"name"` - Slug string `json:"slug"` - TeamID string `json:"team_id"` -} - -// CreateMicrofrontendGroup creates a microfrontend group within Vercel. -func (c *Client) CreateMicrofrontendGroup(ctx context.Context, request CreateMicrofrontendGroupRequest) (r MicrofrontendGroupResponse, err error) { +func (c *Client) CreateMicrofrontendGroup(ctx context.Context, request MicrofrontendGroup) (r MicrofrontendGroup, err error) { + tflog.Info(ctx, "creating microfrontend group", map[string]interface{}{ + "microfrontend_group_name": request.Name, + }) url := fmt.Sprintf("%s/teams/%s/microfrontends", c.baseURL, c.teamID(request.TeamID)) - payload := string(mustMarshal(CreateMicrofrontendGroupRequestAPI{ + payload := string(mustMarshal(struct { + NewMicrofrontendsGroupName string `json:"newMicrofrontendsGroupName"` + }{ NewMicrofrontendsGroupName: request.Name, })) - - tflog.Info(ctx, "creating microfrontend group", map[string]interface{}{ - "url": url, - "payload": payload, - }) - apiResponse := NewMicrofrontendGroupResponseAPI{} + apiResponse := struct { + NewMicrofrontendGroup MicrofrontendGroup `json:"newMicrofrontendsGroup"` + }{} err = c.doRequest(clientRequest{ ctx: ctx, method: "PATCH", @@ -50,7 +46,7 @@ func (c *Client) CreateMicrofrontendGroup(ctx context.Context, request CreateMic if err != nil { return r, err } - return MicrofrontendGroupResponse{ + return MicrofrontendGroup{ ID: apiResponse.NewMicrofrontendGroup.ID, Name: apiResponse.NewMicrofrontendGroup.Name, Slug: apiResponse.NewMicrofrontendGroup.Slug, @@ -58,31 +54,20 @@ func (c *Client) CreateMicrofrontendGroup(ctx context.Context, request CreateMic }, nil } -type UpdateMicrofrontendGroupRequestAPI struct { - Name string `json:"name"` -} -type UpdateMicrofrontendGroupRequest struct { - ID string `json:"id"` - Name string `json:"name"` - TeamID string `json:"team_id"` -} - -type UpdateMicrofrontendGroupResponseAPI struct { - UpdatedMicrofrontendsGroup MicrofrontendGroupResponseInner `json:"updatedMicrofrontendsGroup"` -} - -// UpdateMicrofrontendGroup updates a microfrontend group within Vercel. -func (c *Client) UpdateMicrofrontendGroup(ctx context.Context, request UpdateMicrofrontendGroupRequest) (r MicrofrontendGroupResponse, err error) { +func (c *Client) UpdateMicrofrontendGroup(ctx context.Context, request MicrofrontendGroup) (r MicrofrontendGroup, err error) { url := fmt.Sprintf("%s/teams/%s/microfrontends/%s", c.baseURL, c.teamID(request.TeamID), request.ID) - payload := string(mustMarshal(UpdateMicrofrontendGroupRequestAPI{ + 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 := UpdateMicrofrontendGroupResponseAPI{} + apiResponse := struct { + UpdatedMicrofrontendsGroup MicrofrontendGroup `json:"updatedMicrofrontendsGroup"` + }{} err = c.doRequest(clientRequest{ ctx: ctx, method: "PATCH", @@ -92,7 +77,7 @@ func (c *Client) UpdateMicrofrontendGroup(ctx context.Context, request UpdateMic if err != nil { return r, err } - return MicrofrontendGroupResponse{ + return MicrofrontendGroup{ ID: apiResponse.UpdatedMicrofrontendsGroup.ID, Name: apiResponse.UpdatedMicrofrontendsGroup.Name, Slug: apiResponse.UpdatedMicrofrontendsGroup.Slug, @@ -100,16 +85,13 @@ func (c *Client) UpdateMicrofrontendGroup(ctx context.Context, request UpdateMic }, nil } -// MicrofrontendGroupResponse defines the response the Vercel API returns when a microfrontend group is deleted. -type DeleteMicrofrontendGroupResponse struct{} - -// DeleteMicrofrontendGroup deletes a microfrontend group within Vercel. -func (c *Client) DeleteMicrofrontendGroup(ctx context.Context, microfrontendGroupID string, teamID string) (r DeleteMicrofrontendGroupResponse, err error) { - url := fmt.Sprintf("%s/teams/%s/microfrontends/%s", c.baseURL, c.teamID(teamID), microfrontendGroupID) +func (c *Client) DeleteMicrofrontendGroup(ctx context.Context, request MicrofrontendGroup) (r struct{}, err error) { + 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", @@ -119,37 +101,7 @@ func (c *Client) DeleteMicrofrontendGroup(ctx context.Context, microfrontendGrou return r, err } -type ProjectMicrofrontend struct { - Enabled bool `json:"enabled"` - GroupIds []string `json:"groupIds"` - IsDefaultApp bool `json:"isDefaultApp"` - UpdatedAt string `json:"updatedAt"` -} - -type MicrofrontendProjectResponse struct { - ID string `json:"id"` - Name string `json:"name"` - Framework string `json:"framework"` - Microfrontends ProjectMicrofrontend `json:"microfrontends"` -} - -type MicrofrontendGroupResponseInner struct { - ID string `json:"id"` - Name string `json:"name"` - Slug string `json:"slug"` -} - -type MicrofrontendGroupsResponseInner struct { - Group MicrofrontendGroupResponseInner `json:"group"` - Projects []ProjectResponse `json:"projects"` -} - -type MicrofrontendGroupsResponse struct { - Groups []MicrofrontendGroupsResponseInner `json:"groups"` -} - -// GetMicrofrontendGroups retrieves information from Vercel about existing Microfrontend Groups. -func (c *Client) GetMicrofrontendGroups(ctx context.Context, teamID string) (r MicrofrontendGroupsResponse, err error) { +func (c *Client) GetMicrofrontendGroup(ctx context.Context, microfrontendGroupID string, teamID string) (r MicrofrontendGroup, err error) { url := fmt.Sprintf("%s/v1/microfrontends/groups", c.baseURL) if c.teamID(teamID) != "" { url = fmt.Sprintf("%s?teamId=%s", url, c.teamID(teamID)) @@ -158,32 +110,38 @@ func (c *Client) GetMicrofrontendGroups(ctx context.Context, teamID string) (r M tflog.Info(ctx, "getting microfrontend group", map[string]interface{}{ "url": url, }) + res := MicrofrontendGroupsAPIResponse{} err = c.doRequest(clientRequest{ ctx: ctx, method: "GET", url: url, body: "", - }, &r) - return r, err -} - -// GetMicrofrontendGroup retrieves information from Vercel about an existing MicrofrontendGroup. -func (c *Client) GetMicrofrontendGroup(ctx context.Context, microfrontendGroupID string, teamID string) (r MicrofrontendGroupResponse, err error) { - res, err := c.GetMicrofrontendGroups(ctx, teamID) + }, &res) if err != nil { return r, err } - fmt.Print(res) + tflog.Info(ctx, "getting microfrontend group", map[string]interface{}{ + "res": res, + }) for i := range res.Groups { if res.Groups[i].Group.ID == microfrontendGroupID { - return MicrofrontendGroupResponse{ - ID: res.Groups[i].Group.ID, - Name: res.Groups[i].Group.Name, - Slug: res.Groups[i].Group.Slug, - TeamID: teamID, + projects := map[string]MicrofrontendProject{} + for _, p := range res.Groups[i].Projects { + projects[p.ID] = MicrofrontendProject{ + IsDefaultApp: p.Microfrontends.IsDefaultApp, + DefaultRoute: p.Microfrontends.DefaultRoute, + RouteObservabilityToThisProject: p.Microfrontends.RouteObservabilityToThisProject, + } + } + return MicrofrontendGroup{ + ID: res.Groups[i].Group.ID, + Name: res.Groups[i].Group.Name, + Slug: res.Groups[i].Group.Slug, + TeamID: teamID, + Projects: projects, }, nil } } diff --git a/client/microfrontend_project.go b/client/microfrontend_project.go new file mode 100644 index 00000000..86ac3544 --- /dev/null +++ b/client/microfrontend_project.go @@ -0,0 +1,114 @@ +package client + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-log/tflog" +) + +type MicrofrontendProject 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 MicrofrontendProjectResponseAPI 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 MicrofrontendProjectsResponseAPI struct { + ID string `json:"id"` + Microfrontends MicrofrontendProjectResponseAPI `json:"microfrontends"` +} + +func (c *Client) AddOrUpdateMicrofrontendProject(ctx context.Context, request MicrofrontendProject) (r MicrofrontendProject, err error) { + tflog.Info(ctx, "adding / updating microfrontend project to group", map[string]interface{}{ + "project_id": request.ProjectID, + "group_id": request.MicrofrontendGroupID, + }) + p, err := c.PatchMicrofrontendProject(ctx, MicrofrontendProject{ + 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) RemoveMicrofrontendProject(ctx context.Context, request MicrofrontendProject) (r MicrofrontendProject, err error) { + tflog.Info(ctx, "removing microfrontend project from group", map[string]interface{}{ + "project_id": request.ProjectID, + "group_id": request.MicrofrontendGroupID, + }) + p, err := c.PatchMicrofrontendProject(ctx, MicrofrontendProject{ + 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) PatchMicrofrontendProject(ctx context.Context, request MicrofrontendProject) (r MicrofrontendProject, err error) { + url := fmt.Sprintf("%s/projects/%s/microfrontends", c.baseURL, request.ProjectID) + payload := string(mustMarshal(MicrofrontendProject{ + 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, "creating microfrontend group", map[string]interface{}{ + "url": url, + "payload": payload, + }) + apiResponse := MicrofrontendProjectsResponseAPI{} + err = c.doRequest(clientRequest{ + ctx: ctx, + method: "PATCH", + url: url, + body: payload, + }, &apiResponse) + if err != nil { + return r, err + } + return MicrofrontendProject{ + 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 index 5b95e2cb..81215a62 100644 --- a/docs/data-sources/microfrontend_group.md +++ b/docs/data-sources/microfrontend_group.md @@ -5,15 +5,13 @@ subcategory: "" description: |- Provides information about an existing Microfrontend Group. A Microfrontend Group is a definition of a microfrontend belonging to a Vercel Team. - Projects are added to a Microfrontend Group. --- # 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. -Projects are added to a Microfrontend Group. +A Microfrontend Group is a definition of a microfrontend belonging to a Vercel Team. @@ -31,4 +29,14 @@ Projects are added to a Microfrontend Group. ### Read-Only - `name` (String) A human readable name for the microfrontends group. +- `projects` (Attributes Map) A map of project ids to project configuration that belong to the microfrontend group. (see [below for nested schema](#nestedatt--projects)) - `slug` (String) A slugified version of the name. + + +### Nested Schema for `projects` + +Optional: + +- `default_route` (String) The default route for the project. Used for the screenshot of deployments. +- `is_default_app` (Boolean) Whether the project is the default app for the microfrontend group. Microfrontend groups must have exactly one default app. +- `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 index 0f9296a6..f45902f4 100644 --- a/docs/resources/microfrontend_group.md +++ b/docs/resources/microfrontend_group.md @@ -5,7 +5,18 @@ subcategory: "" description: |- Provides a Microfrontend Group resource. A Microfrontend Group is a definition of a microfrontend belonging to a Vercel Team. - Projects are added to a Microfrontend Group. + Example: + resource "vercel_microfrontend_group" "my-microfrontend-group" { + name = "microfrontend test" + projects = { + (vercel_project.my-parent-project.id) = { + is_default_app = true + } + (vercel_project.my-child-project.id) = { + is_default_app = false + } + } + } --- # vercel_microfrontend_group (Resource) @@ -13,7 +24,20 @@ description: |- Provides a Microfrontend Group resource. A Microfrontend Group is a definition of a microfrontend belonging to a Vercel Team. -Projects are added to a Microfrontend Group. + +Example: + +resource "vercel_microfrontend_group" "my-microfrontend-group" { + name = "microfrontend test" + projects = { + (vercel_project.my-parent-project.id) = { + is_default_app = true + } + (vercel_project.my-child-project.id) = { + is_default_app = false + } + } +} @@ -23,6 +47,7 @@ Projects are added to a Microfrontend Group. ### Required - `name` (String) A human readable name for the microfrontends group. +- `projects` (Attributes Map) A map of project ids to project configuration that belong to the microfrontend group. (see [below for nested schema](#nestedatt--projects)) ### Optional @@ -32,3 +57,12 @@ Projects are added to a Microfrontend Group. - `id` (String) A unique identifier for the group of microfrontends. Example: mfe_12HKQaOmR5t5Uy6vdcQsNIiZgHGB - `slug` (String) A slugified version of the name. + + +### Nested Schema for `projects` + +Optional: + +- `default_route` (String) The default route for the project. Used for the screenshot of deployments. +- `is_default_app` (Boolean) Whether the project is the default app for the microfrontend group. Microfrontend groups must have exactly one default app. +- `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/vercel/data_source_microfrontend_group.go b/vercel/data_source_microfrontend_group.go index a21de4c0..02f9716c 100644 --- a/vercel/data_source_microfrontend_group.go +++ b/vercel/data_source_microfrontend_group.go @@ -6,7 +6,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/vercel/terraform-provider-vercel/v2/client" ) @@ -54,7 +53,6 @@ func (r *microfrontendGroupDataSource) Schema(_ context.Context, req datasource. Provides information about an existing Microfrontend Group. A Microfrontend Group is a definition of a microfrontend belonging to a Vercel Team. -Projects are added to a Microfrontend Group. `, Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ @@ -74,31 +72,35 @@ Projects are added to a Microfrontend Group. Optional: true, Computed: true, }, + "projects": schema.MapNestedAttribute{ + Description: "A map of project ids to project configuration that belong to the microfrontend group.", + Computed: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "is_default_app": schema.BoolAttribute{ + Description: "Whether the project is the default app for the microfrontend group. Microfrontend groups must have exactly one default app.", + Optional: true, + Computed: true, + }, + "default_route": schema.StringAttribute{ + Description: "The default route for the project. Used for the screenshot of deployments.", + Optional: true, + 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.", + Optional: true, + Computed: true, + }, + }, + }, + }, }, } } -type MicrofrontendGroupDataSource struct { - ID types.String `tfsdk:"id"` - Name types.String `tfsdk:"name"` - Slug types.String `tfsdk:"slug"` - TeamID types.String `tfsdk:"team_id"` -} - -func convertResponseToMicrofrontendGroupDataSource(in client.MicrofrontendGroupResponse) MicrofrontendGroupDataSource { - return MicrofrontendGroupDataSource{ - ID: types.StringValue(in.ID), - Name: types.StringValue(in.Name), - Slug: types.StringValue(in.Slug), - TeamID: types.StringValue(in.TeamID), - } -} - -// Read will read the microfrontendGroup information by requesting it from the Vercel API, and will update terraform -// with this information. -// It is called by the provider whenever data source values should be read to update state. func (d *microfrontendGroupDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { - var config MicrofrontendGroupDataSource + var config MicrofrontendGroup diags := req.Config.Get(ctx, &config) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -122,7 +124,7 @@ func (d *microfrontendGroupDataSource) Read(ctx context.Context, req datasource. return } - result := convertResponseToMicrofrontendGroupDataSource(out) + result := convertResponseToMicrofrontendGroup(out, out.Projects) tflog.Info(ctx, "read microfrontendGroup", map[string]interface{}{ "team_id": result.TeamID.ValueString(), "group_id": result.ID.ValueString(), diff --git a/vercel/resource_microfrontend_group.go b/vercel/resource_microfrontend_group.go index 9e11b70e..e5934695 100644 --- a/vercel/resource_microfrontend_group.go +++ b/vercel/resource_microfrontend_group.go @@ -4,10 +4,17 @@ import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework-validators/boolvalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/mapvalidator" + "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/mapplanmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/vercel/terraform-provider-vercel/v2/client" @@ -55,7 +62,20 @@ func (r *microfrontendGroupResource) Schema(_ context.Context, req resource.Sche Provides a Microfrontend Group resource. A Microfrontend Group is a definition of a microfrontend belonging to a Vercel Team. -Projects are added to a Microfrontend Group. + +Example: + +resource "vercel_microfrontend_group" "my-microfrontend-group" { + name = "microfrontend test" + projects = { + (vercel_project.my-parent-project.id) = { + is_default_app = true + } + (vercel_project.my-child-project.id) = { + is_default_app = false + } + } +} `, Attributes: map[string]schema.Attribute{ "name": schema.StringAttribute{ @@ -63,8 +83,9 @@ Projects are added to a Microfrontend Group. Required: true, }, "id": schema.StringAttribute{ - Description: "A unique identifier for the group of microfrontends. Example: mfe_12HKQaOmR5t5Uy6vdcQsNIiZgHGB", - Computed: true, + 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.", @@ -76,33 +97,74 @@ Projects are added to a Microfrontend Group. Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplaceIfConfigured(), stringplanmodifier.UseStateForUnknown()}, }, + "projects": schema.MapNestedAttribute{ + Description: "A map of project ids to project configuration that belong to the microfrontend group.", + Required: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "is_default_app": schema.BoolAttribute{ + Description: "Whether the project is the default app for the microfrontend group. Microfrontend groups must have exactly one default app.", + Optional: true, + Computed: true, + Validators: []validator.Bool{boolvalidator.ExactlyOneOf(path.Expressions{path.MatchRelative()}...)}, + PlanModifiers: []planmodifier.Bool{boolplanmodifier.UseStateForUnknown()}, + }, + "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()}, + }, + "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()}, + }, + }, + }, + Validators: []validator.Map{ + mapvalidator.SizeAtLeast(1), + }, + PlanModifiers: []planmodifier.Map{mapplanmodifier.UseStateForUnknown()}, + }, }, } } -// MicrofrontendGroup represents the terraform state for a microfrontendGroup resource. +type MicrofrontendProject struct { + IsDefaultApp types.Bool `tfsdk:"is_default_app"` + DefaultRoute types.String `tfsdk:"default_route"` + RouteObservabilityToThisProject types.Bool `tfsdk:"route_observability_to_this_project"` +} + type MicrofrontendGroup struct { - TeamID types.String `tfsdk:"team_id"` - ID types.String `tfsdk:"id"` - Name types.String `tfsdk:"name"` - Slug types.String `tfsdk:"slug"` + TeamID types.String `tfsdk:"team_id"` + ID types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + Slug types.String `tfsdk:"slug"` + Projects map[string]MicrofrontendProject `tfsdk:"projects"` } -// convertResponseToMicrofrontendGroup is used to populate terraform state based on an API response. -// Where possible, values from the API response are used to populate state. If not possible, -// values from the existing microfrontendGroup state are used. -func convertResponseToMicrofrontendGroup(response client.MicrofrontendGroupResponse) MicrofrontendGroup { +func convertResponseToMicrofrontendGroup(group client.MicrofrontendGroup, projects map[string]client.MicrofrontendProject) MicrofrontendGroup { + projectResponse := map[string]MicrofrontendProject{} + for projectID, p := range projects { + projectResponse[projectID] = MicrofrontendProject{ + IsDefaultApp: types.BoolValue(p.IsDefaultApp), + DefaultRoute: types.StringValue(p.DefaultRoute), + RouteObservabilityToThisProject: types.BoolValue(p.RouteObservabilityToThisProject), + } + } return MicrofrontendGroup{ - ID: types.StringValue(response.ID), - Name: types.StringValue(response.Name), - Slug: types.StringValue(response.Slug), - TeamID: types.StringValue(response.TeamID), + ID: types.StringValue(group.ID), + Name: types.StringValue(group.Name), + Slug: types.StringValue(group.Slug), + TeamID: types.StringValue(group.TeamID), + Projects: projectResponse, } } -// Create will create a microfrontendGroup within Vercel. This is done by first attempting to trigger a microfrontendGroup, seeing what -// files are required, uploading those files, and then attempting to create a microfrontendGroup again. -// This is called automatically by the provider when a new resource should be created. func (r *microfrontendGroupResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { var plan MicrofrontendGroup diags := req.Plan.Get(ctx, &plan) @@ -115,12 +177,12 @@ func (r *microfrontendGroupResource) Create(ctx context.Context, req resource.Cr return } - cdr := client.CreateMicrofrontendGroupRequest{ + cdr := client.MicrofrontendGroup{ Name: plan.Name.ValueString(), TeamID: plan.TeamID.ValueString(), } - out, err := r.client.CreateMicrofrontendGroup(ctx, cdr) + groupResponse, err := r.client.CreateMicrofrontendGroup(ctx, cdr) if err != nil { resp.Diagnostics.AddError( "Error creating microfrontend group", @@ -129,8 +191,29 @@ func (r *microfrontendGroupResource) Create(ctx context.Context, req resource.Cr return } - result := convertResponseToMicrofrontendGroup(out) - tflog.Info(ctx, "created microfrontendGroup", map[string]interface{}{ + projectResponse := map[string]client.MicrofrontendProject{} + for projectID, project := range plan.Projects { + p, err := r.client.AddOrUpdateMicrofrontendProject(ctx, client.MicrofrontendProject{ + IsDefaultApp: project.IsDefaultApp.ValueBool(), + DefaultRoute: project.DefaultRoute.ValueString(), + RouteObservabilityToThisProject: project.RouteObservabilityToThisProject.ValueBool(), + MicrofrontendGroupID: groupResponse.ID, + TeamID: plan.TeamID.ValueString(), + ProjectID: projectID, + }) + + if err != nil { + resp.Diagnostics.AddError( + "Error creating microfrontend project", + "Could not create microfrontend project, unexpected error: "+err.Error(), + ) + return + } + projectResponse[projectID] = p + } + + result := convertResponseToMicrofrontendGroup(groupResponse, projectResponse) + tflog.Info(ctx, "created microfrontend group", map[string]interface{}{ "team_id": result.TeamID.ValueString(), "group_id": result.ID.ValueString(), "slug": result.Slug.ValueString(), @@ -144,8 +227,6 @@ func (r *microfrontendGroupResource) Create(ctx context.Context, req resource.Cr } } -// Read will read a file from the filesytem and provide terraform with information about it. -// It is called by the provider whenever data source values should be read to update state. func (r *microfrontendGroupResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { var state MicrofrontendGroup diags := req.State.Get(ctx, &state) @@ -161,8 +242,8 @@ func (r *microfrontendGroupResource) Read(ctx context.Context, req resource.Read } if err != nil { resp.Diagnostics.AddError( - "Error reading microfrontendGroup", - fmt.Sprintf("Could not get microfrontendGroup %s %s, unexpected error: %s", + "Error reading microfrontend group", + fmt.Sprintf("Could not get microfrontend group %s %s, unexpected error: %s", state.TeamID.ValueString(), state.ID.ValueString(), err, @@ -171,8 +252,8 @@ func (r *microfrontendGroupResource) Read(ctx context.Context, req resource.Read return } - result := convertResponseToMicrofrontendGroup(out) - tflog.Info(ctx, "read microfrontendGroup", map[string]interface{}{ + result := convertResponseToMicrofrontendGroup(out, out.Projects) + tflog.Info(ctx, "read microfrontend group", map[string]interface{}{ "team_id": result.TeamID.ValueString(), "group_id": result.ID.ValueString(), "slug": result.Slug.ValueString(), @@ -186,15 +267,14 @@ func (r *microfrontendGroupResource) Read(ctx context.Context, req resource.Read } } -// Update updates the microfrontendGroup state. 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 microfrontendGroup plan", - "Error getting microfrontendGroup plan", + "Error getting microfrontend group plan", + "Error getting microfrontend group plan", ) return } @@ -206,7 +286,54 @@ func (r *microfrontendGroupResource) Update(ctx context.Context, req resource.Up return } - out, err := r.client.UpdateMicrofrontendGroup(ctx, client.UpdateMicrofrontendGroupRequest{ + for projectID, project := range state.Projects { + _, exists := plan.Projects[projectID] + if !exists { + tflog.Info(ctx, "removing microfrontend project", map[string]interface{}{ + "project_id": projectID, + }) + _, err := r.client.RemoveMicrofrontendProject(ctx, client.MicrofrontendProject{ + ProjectID: projectID, + IsDefaultApp: project.IsDefaultApp.ValueBool(), + DefaultRoute: project.DefaultRoute.ValueString(), + RouteObservabilityToThisProject: project.RouteObservabilityToThisProject.ValueBool(), + MicrofrontendGroupID: state.ID.ValueString(), + TeamID: state.TeamID.ValueString(), + }) + if err != nil { + resp.Diagnostics.AddError( + "Error removing microfrontend project "+projectID, + "Could not remove microfrontend project, unexpected error: "+err.Error(), + ) + return + } + } + } + + projects := map[string]client.MicrofrontendProject{} + for projectID, project := range plan.Projects { + tflog.Info(ctx, "adding / updating microfrontend project", map[string]interface{}{ + "project_id": projectID, + }) + updatedProject, err := r.client.AddOrUpdateMicrofrontendProject(ctx, client.MicrofrontendProject{ + ProjectID: projectID, + IsDefaultApp: project.IsDefaultApp.ValueBool(), + DefaultRoute: project.DefaultRoute.ValueString(), + RouteObservabilityToThisProject: project.RouteObservabilityToThisProject.ValueBool(), + MicrofrontendGroupID: state.ID.ValueString(), + TeamID: state.TeamID.ValueString(), + }) + if err != nil { + resp.Diagnostics.AddError( + "Error adding microfrontend project "+projectID, + "Could not add microfrontend project, unexpected error: "+err.Error(), + ) + return + } + projects[projectID] = updatedProject + } + + out, err := r.client.UpdateMicrofrontendGroup(ctx, client.MicrofrontendGroup{ ID: state.ID.ValueString(), Name: plan.Name.ValueString(), TeamID: state.TeamID.ValueString(), @@ -231,7 +358,7 @@ func (r *microfrontendGroupResource) Update(ctx context.Context, req resource.Up "slug": out.Slug, }) - result := convertResponseToMicrofrontendGroup(out) + result := convertResponseToMicrofrontendGroup(out, projects) diags = resp.State.Set(ctx, result) resp.Diagnostics.Append(diags...) @@ -240,7 +367,6 @@ func (r *microfrontendGroupResource) Update(ctx context.Context, req resource.Up } } -// Delete deletes a MicrofrontendGroup. func (r *microfrontendGroupResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { var state MicrofrontendGroup diags := req.State.Get(ctx, &state) @@ -249,7 +375,32 @@ func (r *microfrontendGroupResource) Delete(ctx context.Context, req resource.De return } - _, err := r.client.DeleteMicrofrontendGroup(ctx, state.ID.ValueString(), state.TeamID.ValueString()) + for projectID, project := range state.Projects { + tflog.Info(ctx, "removing microfrontend project", map[string]interface{}{ + "project_id": projectID, + }) + _, err := r.client.RemoveMicrofrontendProject(ctx, client.MicrofrontendProject{ + ProjectID: projectID, + IsDefaultApp: project.IsDefaultApp.ValueBool(), + DefaultRoute: project.DefaultRoute.ValueString(), + RouteObservabilityToThisProject: project.RouteObservabilityToThisProject.ValueBool(), + MicrofrontendGroupID: state.ID.ValueString(), + TeamID: state.TeamID.ValueString(), + }) + if err != nil { + resp.Diagnostics.AddError( + "Error removing microfrontend project "+projectID, + "Could not remove microfrontend project, unexpected error: "+err.Error(), + ) + return + } + } + _, 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 microfrontendGroup", From ad8019a15a1472ebcc3d550db3e6f78bddab12a8 Mon Sep 17 00:00:00 2001 From: Kit Foster Date: Thu, 20 Feb 2025 12:55:26 +0100 Subject: [PATCH 03/12] Add unit test for microfrontend_group --- client/microfrontend_group.go | 13 ++ docs/resources/microfrontend_group.md | 2 +- vercel/provider_test.go | 1 + vercel/resource_microfrontend_group.go | 18 ++- vercel/resource_microfrontend_group_test.go | 144 ++++++++++++++++++++ 5 files changed, 172 insertions(+), 6 deletions(-) create mode 100644 vercel/resource_microfrontend_group_test.go diff --git a/client/microfrontend_group.go b/client/microfrontend_group.go index 298700b7..2da60df7 100644 --- a/client/microfrontend_group.go +++ b/client/microfrontend_group.go @@ -25,8 +25,12 @@ type MicrofrontendGroupsAPIResponse struct { } func (c *Client) CreateMicrofrontendGroup(ctx context.Context, request MicrofrontendGroup) (r MicrofrontendGroup, err error) { + if c.teamID(request.TeamID) == "" { + return r, fmt.Errorf("team_id is required") + } tflog.Info(ctx, "creating microfrontend group", map[string]interface{}{ "microfrontend_group_name": request.Name, + "team_id": c.teamID(request.TeamID), }) url := fmt.Sprintf("%s/teams/%s/microfrontends", c.baseURL, c.teamID(request.TeamID)) payload := string(mustMarshal(struct { @@ -55,6 +59,9 @@ func (c *Client) CreateMicrofrontendGroup(ctx context.Context, request Microfron } 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"` @@ -86,6 +93,9 @@ func (c *Client) UpdateMicrofrontendGroup(ctx context.Context, request Microfron } 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{}{ @@ -102,6 +112,9 @@ func (c *Client) DeleteMicrofrontendGroup(ctx context.Context, request Microfron } 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)) diff --git a/docs/resources/microfrontend_group.md b/docs/resources/microfrontend_group.md index f45902f4..53f8c12e 100644 --- a/docs/resources/microfrontend_group.md +++ b/docs/resources/microfrontend_group.md @@ -64,5 +64,5 @@ resource "vercel_microfrontend_group" "my-microfrontend-group" { Optional: - `default_route` (String) The default route for the project. Used for the screenshot of deployments. -- `is_default_app` (Boolean) Whether the project is the default app for the microfrontend group. Microfrontend groups must have exactly one default app. +- `is_default_app` (Boolean) Whether the project is the default app for the microfrontend group. Microfrontend groups must have exactly one default app. (Omit false values) - `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/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 index e5934695..addf72ff 100644 --- a/vercel/resource_microfrontend_group.go +++ b/vercel/resource_microfrontend_group.go @@ -103,11 +103,15 @@ resource "vercel_microfrontend_group" "my-microfrontend-group" { NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "is_default_app": schema.BoolAttribute{ - Description: "Whether the project is the default app for the microfrontend group. Microfrontend groups must have exactly one default app.", + Description: "Whether the project is the default app for the microfrontend group. Microfrontend groups must have exactly one default app. (Omit false values)", Optional: true, Computed: true, - Validators: []validator.Bool{boolvalidator.ExactlyOneOf(path.Expressions{path.MatchRelative()}...)}, PlanModifiers: []planmodifier.Bool{boolplanmodifier.UseStateForUnknown()}, + Validators: []validator.Bool{ + boolvalidator.ExactlyOneOf( + path.MatchRoot("projects").AtAnyMapKey().AtName("is_default_app"), + ), + }, }, "default_route": schema.StringAttribute{ Description: "The default route for the project. Used for the screenshot of deployments.", @@ -124,9 +128,7 @@ resource "vercel_microfrontend_group" "my-microfrontend-group" { }, }, }, - Validators: []validator.Map{ - mapvalidator.SizeAtLeast(1), - }, + Validators: []validator.Map{mapvalidator.SizeAtLeast(1)}, PlanModifiers: []planmodifier.Map{mapplanmodifier.UseStateForUnknown()}, }, }, @@ -177,6 +179,12 @@ func (r *microfrontendGroupResource) Create(ctx context.Context, req resource.Cr return } + tflog.Info(ctx, "creating microfrontend group", map[string]interface{}{ + "team_id": plan.TeamID.ValueString(), + "name": plan.Name.ValueString(), + "plan": plan, + }) + cdr := client.MicrofrontendGroup{ Name: plan.Name.ValueString(), TeamID: plan.TeamID.ValueString(), diff --git a/vercel/resource_microfrontend_group_test.go b/vercel/resource_microfrontend_group_test.go new file mode 100644 index 00000000..136c5244 --- /dev/null +++ b/vercel/resource_microfrontend_group_test.go @@ -0,0 +1,144 @@ +package vercel_test + +import ( + "context" + "fmt" + "regexp" + "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: ` + resource "vercel_microfrontend_group" "test" { + name = "foo" + } + `, + ExpectError: regexp.MustCompile(`The argument "projects" is required, but no definition was found.`), + }, + { + 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 = "foo-%[1]s" + %[2]s + projects = { + (vercel_project.test.id) = {} + (vercel_project.test-2.id) = {} + } + } + `, name, teamIDConfig()), + ExpectError: regexp.MustCompile(`Invalid Attribute Combination`), + }, + { + 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 = "foo-%[1]s" + %[2]s + projects = { + (vercel_project.test.id) = { + is_default_app = true + } + (vercel_project.test-2.id) = { + is_default_app = true + } + } + } + `, name, teamIDConfig()), + ExpectError: regexp.MustCompile(`Invalid Attribute Combination`), + }, + { + 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" + %[2]s + projects = { + (vercel_project.test.id) = { + is_default_app = true + } + (vercel_project.test-2.id) = {} + } + } + `, 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.TestCheckResourceAttr("vercel_microfrontend_group.test", "projects.%", "2"), + ), + }, + }, + }) +} From ba79b98c16dd8e0382a32ce7838fa39c1cc27433 Mon Sep 17 00:00:00 2001 From: Kit Foster Date: Fri, 21 Feb 2025 10:33:20 +0100 Subject: [PATCH 04/12] Replace microfrontend group if default app is updated --- vercel/resource_microfrontend_group.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vercel/resource_microfrontend_group.go b/vercel/resource_microfrontend_group.go index addf72ff..f262a455 100644 --- a/vercel/resource_microfrontend_group.go +++ b/vercel/resource_microfrontend_group.go @@ -106,7 +106,7 @@ resource "vercel_microfrontend_group" "my-microfrontend-group" { Description: "Whether the project is the default app for the microfrontend group. Microfrontend groups must have exactly one default app. (Omit false values)", Optional: true, Computed: true, - PlanModifiers: []planmodifier.Bool{boolplanmodifier.UseStateForUnknown()}, + PlanModifiers: []planmodifier.Bool{boolplanmodifier.UseStateForUnknown(), boolplanmodifier.RequiresReplace()}, Validators: []validator.Bool{ boolvalidator.ExactlyOneOf( path.MatchRoot("projects").AtAnyMapKey().AtName("is_default_app"), From 1c89ff9ab5a6998ccaf3c3b63b18a345e9fce9af Mon Sep 17 00:00:00 2001 From: Kit Foster Date: Thu, 27 Feb 2025 15:57:15 +0100 Subject: [PATCH 05/12] Add microfrontend_group_membership --- client/microfrontend_group.go | 46 ++- ...t.go => microfrontend_group_membership.go} | 61 +++- docs/data-sources/microfrontend_group.md | 11 +- .../microfrontend_group_membership.md | 33 ++ docs/resources/microfrontend_group.md | 58 ++-- .../microfrontend_group_membership.md | 55 +++ .../vercel_microfrontend_group/resource.tf | 22 ++ .../resource.tf | 22 ++ vercel/data_source_microfrontend_group.go | 25 +- ...a_source_microfrontend_group_membership.go | 124 +++++++ vercel/provider.go | 2 + vercel/resource_microfrontend_group.go | 245 +++++-------- ...resource_microfrontend_group_membership.go | 325 ++++++++++++++++++ 13 files changed, 763 insertions(+), 266 deletions(-) rename client/{microfrontend_project.go => microfrontend_group_membership.go} (56%) create mode 100644 docs/data-sources/microfrontend_group_membership.md create mode 100644 docs/resources/microfrontend_group_membership.md create mode 100644 examples/resources/vercel_microfrontend_group/resource.tf create mode 100644 examples/resources/vercel_microfrontend_group_membership/resource.tf create mode 100644 vercel/data_source_microfrontend_group_membership.go create mode 100644 vercel/resource_microfrontend_group_membership.go diff --git a/client/microfrontend_group.go b/client/microfrontend_group.go index 2da60df7..8b2bf20f 100644 --- a/client/microfrontend_group.go +++ b/client/microfrontend_group.go @@ -8,16 +8,17 @@ import ( ) type MicrofrontendGroup struct { - ID string `json:"id"` - Name string `json:"name"` - Slug string `json:"slug"` - TeamID string `json:"team_id"` - Projects map[string]MicrofrontendProject `json:"projects"` + ID string `json:"id"` + Name string `json:"name"` + Slug string `json:"slug"` + TeamID string `json:"team_id"` + Projects map[string]MicrofrontendGroupMembership `json:"projects"` + DefaultApp string `json:"defaultApp"` } type MicrofrontendGroupsAPI struct { - Group MicrofrontendGroup `json:"group"` - Projects []MicrofrontendProjectsResponseAPI `json:"projects"` + Group MicrofrontendGroup `json:"group"` + Projects []MicrofrontendGroupMembershipsResponseAPI `json:"projects"` } type MicrofrontendGroupsAPIResponse struct { @@ -141,21 +142,34 @@ func (c *Client) GetMicrofrontendGroup(ctx context.Context, microfrontendGroupID for i := range res.Groups { if res.Groups[i].Group.ID == microfrontendGroupID { - projects := map[string]MicrofrontendProject{} + projects := map[string]MicrofrontendGroupMembership{} + defaultApp := "" for _, p := range res.Groups[i].Projects { - projects[p.ID] = MicrofrontendProject{ + 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 = p.ID + } + } + res := MicrofrontendGroup{ + ID: res.Groups[i].Group.ID, + Name: res.Groups[i].Group.Name, + Slug: res.Groups[i].Group.Slug, + TeamID: teamID, + DefaultApp: defaultApp, + Projects: projects, } - return MicrofrontendGroup{ - ID: res.Groups[i].Group.ID, - Name: res.Groups[i].Group.Name, - Slug: res.Groups[i].Group.Slug, - TeamID: teamID, - Projects: projects, - }, nil + tflog.Info(ctx, "returning microfrontend group", map[string]interface{}{ + "res": res, + }) + return res, nil } } diff --git a/client/microfrontend_project.go b/client/microfrontend_group_membership.go similarity index 56% rename from client/microfrontend_project.go rename to client/microfrontend_group_membership.go index 86ac3544..d635f8ff 100644 --- a/client/microfrontend_project.go +++ b/client/microfrontend_group_membership.go @@ -7,7 +7,7 @@ import ( "github.com/hashicorp/terraform-plugin-log/tflog" ) -type MicrofrontendProject struct { +type MicrofrontendGroupMembership struct { MicrofrontendGroupID string `json:"microfrontendsGroupId"` IsDefaultApp bool `json:"isDefaultApp"` DefaultRoute string `json:"defaultRoute"` @@ -17,7 +17,7 @@ type MicrofrontendProject struct { TeamID string `json:"team_id"` } -type MicrofrontendProjectResponseAPI struct { +type MicrofrontendGroupMembershipResponseAPI struct { GroupIds []string `json:"groupIds"` Enabled bool `json:"enabled"` IsDefaultApp bool `json:"isDefaultApp"` @@ -27,21 +27,40 @@ type MicrofrontendProjectResponseAPI struct { UpdatedAt int `json:"updatedAt"` } -type MicrofrontendProjectsResponseAPI struct { - ID string `json:"id"` - Microfrontends MicrofrontendProjectResponseAPI `json:"microfrontends"` +type MicrofrontendGroupMembershipsResponseAPI struct { + ID string `json:"id"` + Microfrontends MicrofrontendGroupMembershipResponseAPI `json:"microfrontends"` } -func (c *Client) AddOrUpdateMicrofrontendProject(ctx context.Context, request MicrofrontendProject) (r MicrofrontendProject, err error) { - tflog.Info(ctx, "adding / updating microfrontend project to group", map[string]interface{}{ +func (c *Client) GetMicrofrontendGroupMembership(ctx context.Context, request MicrofrontendGroupMembership) (r MicrofrontendGroupMembership, err error) { + tflog.Info(ctx, "getting microfrontend group", map[string]interface{}{ "project_id": request.ProjectID, "group_id": request.MicrofrontendGroupID, + "team_id": c.teamID(request.TeamID), + }) + group, err := c.GetMicrofrontendGroup(ctx, request.MicrofrontendGroupID, c.teamID(request.TeamID)) + if err != nil { + return r, err + } + tflog.Info(ctx, "getting microfrontend group membership", map[string]interface{}{ + "project_id": request.ProjectID, + "group": group, + }) + return group.Projects[request.ProjectID], nil +} + +func (c *Client) AddOrUpdateMicrofrontendGroupMembership(ctx context.Context, request MicrofrontendGroupMembership, group MicrofrontendGroup) (r MicrofrontendGroupMembership, err error) { + isDefaultApp := group.DefaultApp == request.ProjectID + tflog.Info(ctx, "adding / updating microfrontend project to group", map[string]interface{}{ + "is_default_app": isDefaultApp, + "project_id": request.ProjectID, + "group_id": request.MicrofrontendGroupID, }) - p, err := c.PatchMicrofrontendProject(ctx, MicrofrontendProject{ + p, err := c.PatchMicrofrontendGroupMembership(ctx, MicrofrontendGroupMembership{ ProjectID: request.ProjectID, TeamID: c.teamID(request.TeamID), Enabled: true, - IsDefaultApp: request.IsDefaultApp, + IsDefaultApp: isDefaultApp, DefaultRoute: request.DefaultRoute, RouteObservabilityToThisProject: request.RouteObservabilityToThisProject, MicrofrontendGroupID: request.MicrofrontendGroupID, @@ -52,12 +71,18 @@ func (c *Client) AddOrUpdateMicrofrontendProject(ctx context.Context, request Mi return p, nil } -func (c *Client) RemoveMicrofrontendProject(ctx context.Context, request MicrofrontendProject) (r MicrofrontendProject, err error) { +func (c *Client) RemoveMicrofrontendGroupMembership(ctx context.Context, request MicrofrontendGroupMembership, canDeleteDefaultApp bool) (r MicrofrontendGroupMembership, err error) { + if request.IsDefaultApp && !canDeleteDefaultApp { + // Only delete the default app relationship if the entire group is being deleted + return r, nil + } + 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.PatchMicrofrontendProject(ctx, MicrofrontendProject{ + p, err := c.PatchMicrofrontendGroupMembership(ctx, MicrofrontendGroupMembership{ ProjectID: request.ProjectID, TeamID: c.teamID(request.TeamID), Enabled: false, @@ -69,9 +94,9 @@ func (c *Client) RemoveMicrofrontendProject(ctx context.Context, request Microfr return p, nil } -func (c *Client) PatchMicrofrontendProject(ctx context.Context, request MicrofrontendProject) (r MicrofrontendProject, err error) { +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(MicrofrontendProject{ + payload := string(mustMarshal(MicrofrontendGroupMembership{ IsDefaultApp: request.IsDefaultApp, DefaultRoute: request.DefaultRoute, RouteObservabilityToThisProject: request.RouteObservabilityToThisProject, @@ -92,11 +117,11 @@ func (c *Client) PatchMicrofrontendProject(ctx context.Context, request Microfro url = fmt.Sprintf("%s?teamId=%s", url, c.teamID(request.TeamID)) } - tflog.Info(ctx, "creating microfrontend group", map[string]interface{}{ + tflog.Info(ctx, "updating microfrontend group membership", map[string]interface{}{ "url": url, "payload": payload, }) - apiResponse := MicrofrontendProjectsResponseAPI{} + apiResponse := MicrofrontendGroupMembershipsResponseAPI{} err = c.doRequest(clientRequest{ ctx: ctx, method: "PATCH", @@ -106,7 +131,11 @@ func (c *Client) PatchMicrofrontendProject(ctx context.Context, request Microfro if err != nil { return r, err } - return MicrofrontendProject{ + 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, diff --git a/docs/data-sources/microfrontend_group.md b/docs/data-sources/microfrontend_group.md index 81215a62..9fe8b29a 100644 --- a/docs/data-sources/microfrontend_group.md +++ b/docs/data-sources/microfrontend_group.md @@ -28,15 +28,6 @@ A Microfrontend Group is a definition of a microfrontend belonging to a Vercel T ### Read-Only +- `default_app` (String) The default app for the project. Used as the entry point for the microfrontend. - `name` (String) A human readable name for the microfrontends group. -- `projects` (Attributes Map) A map of project ids to project configuration that belong to the microfrontend group. (see [below for nested schema](#nestedatt--projects)) - `slug` (String) A slugified version of the name. - - -### Nested Schema for `projects` - -Optional: - -- `default_route` (String) The default route for the project. Used for the screenshot of deployments. -- `is_default_app` (Boolean) Whether the project is the default app for the microfrontend group. Microfrontend groups must have exactly one default app. -- `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/data-sources/microfrontend_group_membership.md b/docs/data-sources/microfrontend_group_membership.md new file mode 100644 index 00000000..4b779d5a --- /dev/null +++ b/docs/data-sources/microfrontend_group_membership.md @@ -0,0 +1,33 @@ +--- +# 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. + + + + +## 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 index 53f8c12e..1a516a86 100644 --- a/docs/resources/microfrontend_group.md +++ b/docs/resources/microfrontend_group.md @@ -5,49 +5,48 @@ subcategory: "" description: |- Provides a Microfrontend Group resource. A Microfrontend Group is a definition of a microfrontend belonging to a Vercel Team. - Example: - resource "vercel_microfrontend_group" "my-microfrontend-group" { - name = "microfrontend test" - projects = { - (vercel_project.my-parent-project.id) = { - is_default_app = true - } - (vercel_project.my-child-project.id) = { - is_default_app = false - } - } - } --- # vercel_microfrontend_group (Resource) Provides a Microfrontend Group resource. -A Microfrontend Group is a definition of a microfrontend belonging to a Vercel Team. +A Microfrontend Group is a definition of a microfrontend belonging to a Vercel Team. -Example: +## Example Usage -resource "vercel_microfrontend_group" "my-microfrontend-group" { - name = "microfrontend test" - projects = { - (vercel_project.my-parent-project.id) = { - is_default_app = true - } - (vercel_project.my-child-project.id) = { - is_default_app = false - } - } +```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` (String) The default app for the project. Used as the entry point for the microfrontend. - `name` (String) A human readable name for the microfrontends group. -- `projects` (Attributes Map) A map of project ids to project configuration that belong to the microfrontend group. (see [below for nested schema](#nestedatt--projects)) ### Optional @@ -57,12 +56,3 @@ resource "vercel_microfrontend_group" "my-microfrontend-group" { - `id` (String) A unique identifier for the group of microfrontends. Example: mfe_12HKQaOmR5t5Uy6vdcQsNIiZgHGB - `slug` (String) A slugified version of the name. - - -### Nested Schema for `projects` - -Optional: - -- `default_route` (String) The default route for the project. Used for the screenshot of deployments. -- `is_default_app` (Boolean) Whether the project is the default app for the microfrontend group. Microfrontend groups must have exactly one default app. (Omit false values) -- `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_membership.md b/docs/resources/microfrontend_group_membership.md new file mode 100644 index 00000000..d3e7ed2c --- /dev/null +++ b/docs/resources/microfrontend_group_membership.md @@ -0,0 +1,55 @@ +--- +# 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. diff --git a/examples/resources/vercel_microfrontend_group/resource.tf b/examples/resources/vercel_microfrontend_group/resource.tf new file mode 100644 index 00000000..66af13ef --- /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/resource.tf b/examples/resources/vercel_microfrontend_group_membership/resource.tf new file mode 100644 index 00000000..66af13ef --- /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 index 02f9716c..4e2d6aaf 100644 --- a/vercel/data_source_microfrontend_group.go +++ b/vercel/data_source_microfrontend_group.go @@ -72,28 +72,9 @@ A Microfrontend Group is a definition of a microfrontend belonging to a Vercel T Optional: true, Computed: true, }, - "projects": schema.MapNestedAttribute{ - Description: "A map of project ids to project configuration that belong to the microfrontend group.", + "default_app": schema.StringAttribute{ + Description: "The default app for the project. Used as the entry point for the microfrontend.", Computed: true, - NestedObject: schema.NestedAttributeObject{ - Attributes: map[string]schema.Attribute{ - "is_default_app": schema.BoolAttribute{ - Description: "Whether the project is the default app for the microfrontend group. Microfrontend groups must have exactly one default app.", - Optional: true, - Computed: true, - }, - "default_route": schema.StringAttribute{ - Description: "The default route for the project. Used for the screenshot of deployments.", - Optional: true, - 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.", - Optional: true, - Computed: true, - }, - }, - }, }, }, } @@ -124,7 +105,7 @@ func (d *microfrontendGroupDataSource) Read(ctx context.Context, req datasource. return } - result := convertResponseToMicrofrontendGroup(out, out.Projects) + result := convertResponseToMicrofrontendGroup(out) tflog.Info(ctx, "read microfrontendGroup", map[string]interface{}{ "team_id": result.TeamID.ValueString(), "group_id": result.ID.ValueString(), diff --git a/vercel/data_source_microfrontend_group_membership.go b/vercel/data_source_microfrontend_group_membership.go new file mode 100644 index 00000000..7249a1d2 --- /dev/null +++ b/vercel/data_source_microfrontend_group_membership.go @@ -0,0 +1,124 @@ +package vercel + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-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, client.MicrofrontendGroupMembership{ + ProjectID: config.ProjectID.ValueString(), + MicrofrontendGroupID: config.MicrofrontendGroupID.ValueString(), + TeamID: config.TeamID.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/provider.go b/vercel/provider.go index a2f12e40..41d7c045 100644 --- a/vercel/provider.go +++ b/vercel/provider.go @@ -76,6 +76,7 @@ func (p *vercelProvider) Resources(_ context.Context) []func() resource.Resource newTeamMemberResource, newWebhookResource, newMicrofrontendGroupResource, + newMicrofrontendGroupMembershipResource, } } @@ -103,6 +104,7 @@ func (p *vercelProvider) DataSources(_ context.Context) []func() datasource.Data newTeamConfigDataSource, newTeamMemberDataSource, newMicrofrontendGroupDataSource, + newMicrofrontendGroupMembershipDataSource, } } diff --git a/vercel/resource_microfrontend_group.go b/vercel/resource_microfrontend_group.go index f262a455..8d0311b1 100644 --- a/vercel/resource_microfrontend_group.go +++ b/vercel/resource_microfrontend_group.go @@ -4,17 +4,10 @@ import ( "context" "fmt" - "github.com/hashicorp/terraform-plugin-framework-validators/boolvalidator" - "github.com/hashicorp/terraform-plugin-framework-validators/mapvalidator" - "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/mapplanmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/vercel/terraform-provider-vercel/v2/client" @@ -62,20 +55,6 @@ func (r *microfrontendGroupResource) Schema(_ context.Context, req resource.Sche Provides a Microfrontend Group resource. A Microfrontend Group is a definition of a microfrontend belonging to a Vercel Team. - -Example: - -resource "vercel_microfrontend_group" "my-microfrontend-group" { - name = "microfrontend test" - projects = { - (vercel_project.my-parent-project.id) = { - is_default_app = true - } - (vercel_project.my-child-project.id) = { - is_default_app = false - } - } -} `, Attributes: map[string]schema.Attribute{ "name": schema.StringAttribute{ @@ -97,73 +76,30 @@ resource "vercel_microfrontend_group" "my-microfrontend-group" { Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplaceIfConfigured(), stringplanmodifier.UseStateForUnknown()}, }, - "projects": schema.MapNestedAttribute{ - Description: "A map of project ids to project configuration that belong to the microfrontend group.", - Required: true, - NestedObject: schema.NestedAttributeObject{ - Attributes: map[string]schema.Attribute{ - "is_default_app": schema.BoolAttribute{ - Description: "Whether the project is the default app for the microfrontend group. Microfrontend groups must have exactly one default app. (Omit false values)", - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.Bool{boolplanmodifier.UseStateForUnknown(), boolplanmodifier.RequiresReplace()}, - Validators: []validator.Bool{ - boolvalidator.ExactlyOneOf( - path.MatchRoot("projects").AtAnyMapKey().AtName("is_default_app"), - ), - }, - }, - "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()}, - }, - "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()}, - }, - }, - }, - Validators: []validator.Map{mapvalidator.SizeAtLeast(1)}, - PlanModifiers: []planmodifier.Map{mapplanmodifier.UseStateForUnknown()}, + "default_app": schema.StringAttribute{ + Description: "The default app for the project. Used as the entry point for the microfrontend.", + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, }, }, } } -type MicrofrontendProject struct { - IsDefaultApp types.Bool `tfsdk:"is_default_app"` - DefaultRoute types.String `tfsdk:"default_route"` - RouteObservabilityToThisProject types.Bool `tfsdk:"route_observability_to_this_project"` -} - type MicrofrontendGroup struct { - TeamID types.String `tfsdk:"team_id"` - ID types.String `tfsdk:"id"` - Name types.String `tfsdk:"name"` - Slug types.String `tfsdk:"slug"` - Projects map[string]MicrofrontendProject `tfsdk:"projects"` + TeamID types.String `tfsdk:"team_id"` + ID types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + Slug types.String `tfsdk:"slug"` + DefaultApp types.String `tfsdk:"default_app"` } -func convertResponseToMicrofrontendGroup(group client.MicrofrontendGroup, projects map[string]client.MicrofrontendProject) MicrofrontendGroup { - projectResponse := map[string]MicrofrontendProject{} - for projectID, p := range projects { - projectResponse[projectID] = MicrofrontendProject{ - IsDefaultApp: types.BoolValue(p.IsDefaultApp), - DefaultRoute: types.StringValue(p.DefaultRoute), - RouteObservabilityToThisProject: types.BoolValue(p.RouteObservabilityToThisProject), - } - } +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), - Projects: projectResponse, + ID: types.StringValue(group.ID), + Name: types.StringValue(group.Name), + Slug: types.StringValue(group.Slug), + TeamID: types.StringValue(group.TeamID), + DefaultApp: types.StringValue(group.DefaultApp), } } @@ -182,7 +118,6 @@ func (r *microfrontendGroupResource) Create(ctx context.Context, req resource.Cr tflog.Info(ctx, "creating microfrontend group", map[string]interface{}{ "team_id": plan.TeamID.ValueString(), "name": plan.Name.ValueString(), - "plan": plan, }) cdr := client.MicrofrontendGroup{ @@ -190,7 +125,7 @@ func (r *microfrontendGroupResource) Create(ctx context.Context, req resource.Cr TeamID: plan.TeamID.ValueString(), } - groupResponse, err := r.client.CreateMicrofrontendGroup(ctx, cdr) + out, err := r.client.CreateMicrofrontendGroup(ctx, cdr) if err != nil { resp.Diagnostics.AddError( "Error creating microfrontend group", @@ -199,33 +134,43 @@ func (r *microfrontendGroupResource) Create(ctx context.Context, req resource.Cr return } - projectResponse := map[string]client.MicrofrontendProject{} - for projectID, project := range plan.Projects { - p, err := r.client.AddOrUpdateMicrofrontendProject(ctx, client.MicrofrontendProject{ - IsDefaultApp: project.IsDefaultApp.ValueBool(), - DefaultRoute: project.DefaultRoute.ValueString(), - RouteObservabilityToThisProject: project.RouteObservabilityToThisProject.ValueBool(), - MicrofrontendGroupID: groupResponse.ID, - TeamID: plan.TeamID.ValueString(), - ProjectID: projectID, - }) + tflog.Info(ctx, "creating default group membership", map[string]interface{}{ + "team_id": plan.TeamID.ValueString(), + "name": plan.Name.ValueString(), + "default_app": plan.DefaultApp.ValueString(), + }) - if err != nil { - resp.Diagnostics.AddError( - "Error creating microfrontend project", - "Could not create microfrontend project, unexpected error: "+err.Error(), - ) - return - } - projectResponse[projectID] = p + group := client.MicrofrontendGroup{ + ID: out.ID, + Name: out.Name, + Slug: out.Slug, + TeamID: out.TeamID, + DefaultApp: plan.DefaultApp.ValueString(), + Projects: out.Projects, } - result := convertResponseToMicrofrontendGroup(groupResponse, projectResponse) + _, err = r.client.AddOrUpdateMicrofrontendGroupMembership(ctx, client.MicrofrontendGroupMembership{ + ProjectID: plan.DefaultApp.ValueString(), + MicrofrontendGroupID: out.ID, + TeamID: plan.TeamID.ValueString(), + IsDefaultApp: true, + }, group) + + 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 + } + + 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(), + "team_id": result.TeamID.ValueString(), + "group_id": result.ID.ValueString(), + "slug": result.Slug.ValueString(), + "name": result.Name.ValueString(), + "default_app": result.DefaultApp.ValueString(), }) diags = resp.State.Set(ctx, result) @@ -260,7 +205,7 @@ func (r *microfrontendGroupResource) Read(ctx context.Context, req resource.Read return } - result := convertResponseToMicrofrontendGroup(out, out.Projects) + result := convertResponseToMicrofrontendGroup(out) tflog.Info(ctx, "read microfrontend group", map[string]interface{}{ "team_id": result.TeamID.ValueString(), "group_id": result.ID.ValueString(), @@ -294,53 +239,6 @@ func (r *microfrontendGroupResource) Update(ctx context.Context, req resource.Up return } - for projectID, project := range state.Projects { - _, exists := plan.Projects[projectID] - if !exists { - tflog.Info(ctx, "removing microfrontend project", map[string]interface{}{ - "project_id": projectID, - }) - _, err := r.client.RemoveMicrofrontendProject(ctx, client.MicrofrontendProject{ - ProjectID: projectID, - IsDefaultApp: project.IsDefaultApp.ValueBool(), - DefaultRoute: project.DefaultRoute.ValueString(), - RouteObservabilityToThisProject: project.RouteObservabilityToThisProject.ValueBool(), - MicrofrontendGroupID: state.ID.ValueString(), - TeamID: state.TeamID.ValueString(), - }) - if err != nil { - resp.Diagnostics.AddError( - "Error removing microfrontend project "+projectID, - "Could not remove microfrontend project, unexpected error: "+err.Error(), - ) - return - } - } - } - - projects := map[string]client.MicrofrontendProject{} - for projectID, project := range plan.Projects { - tflog.Info(ctx, "adding / updating microfrontend project", map[string]interface{}{ - "project_id": projectID, - }) - updatedProject, err := r.client.AddOrUpdateMicrofrontendProject(ctx, client.MicrofrontendProject{ - ProjectID: projectID, - IsDefaultApp: project.IsDefaultApp.ValueBool(), - DefaultRoute: project.DefaultRoute.ValueString(), - RouteObservabilityToThisProject: project.RouteObservabilityToThisProject.ValueBool(), - MicrofrontendGroupID: state.ID.ValueString(), - TeamID: state.TeamID.ValueString(), - }) - if err != nil { - resp.Diagnostics.AddError( - "Error adding microfrontend project "+projectID, - "Could not add microfrontend project, unexpected error: "+err.Error(), - ) - return - } - projects[projectID] = updatedProject - } - out, err := r.client.UpdateMicrofrontendGroup(ctx, client.MicrofrontendGroup{ ID: state.ID.ValueString(), Name: plan.Name.ValueString(), @@ -366,7 +264,7 @@ func (r *microfrontendGroupResource) Update(ctx context.Context, req resource.Up "slug": out.Slug, }) - result := convertResponseToMicrofrontendGroup(out, projects) + result := convertResponseToMicrofrontendGroup(out) diags = resp.State.Set(ctx, result) resp.Diagnostics.Append(diags...) @@ -383,26 +281,37 @@ func (r *microfrontendGroupResource) Delete(ctx context.Context, req resource.De return } - for projectID, project := range state.Projects { - tflog.Info(ctx, "removing microfrontend project", map[string]interface{}{ - "project_id": projectID, - }) - _, err := r.client.RemoveMicrofrontendProject(ctx, client.MicrofrontendProject{ - ProjectID: projectID, - IsDefaultApp: project.IsDefaultApp.ValueBool(), - DefaultRoute: project.DefaultRoute.ValueString(), - RouteObservabilityToThisProject: project.RouteObservabilityToThisProject.ValueBool(), - MicrofrontendGroupID: state.ID.ValueString(), - TeamID: state.TeamID.ValueString(), + if state.DefaultApp.ValueString() != "" { + tflog.Info(ctx, "deleting microfrontend default app group membership", map[string]interface{}{ + "group_id": state.ID.ValueString(), + "project_id": state.DefaultApp.ValueString(), + "team_id": state.TeamID.ValueString(), }) + + _, err := r.client.RemoveMicrofrontendGroupMembership(ctx, client.MicrofrontendGroupMembership{ + MicrofrontendGroupID: state.ID.ValueString(), + TeamID: state.TeamID.ValueString(), + ProjectID: state.DefaultApp.ValueString(), + }, true) + if err != nil { resp.Diagnostics.AddError( - "Error removing microfrontend project "+projectID, - "Could not remove microfrontend project, unexpected error: "+err.Error(), + "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.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(), @@ -411,9 +320,9 @@ func (r *microfrontendGroupResource) Delete(ctx context.Context, req resource.De }) if err != nil { resp.Diagnostics.AddError( - "Error deleting microfrontendGroup", + "Error deleting microfrontend group", fmt.Sprintf( - "Could not delete microfrontendGroup %s, unexpected error: %s", + "Could not delete microfrontend group %s, unexpected error: %s", state.ID.ValueString(), err, ), diff --git a/vercel/resource_microfrontend_group_membership.go b/vercel/resource_microfrontend_group_membership.go new file mode 100644 index 00000000..7bcf9316 --- /dev/null +++ b/vercel/resource_microfrontend_group_membership.go @@ -0,0 +1,325 @@ +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{} +) + +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 +} + +// 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: map[string]schema.Attribute{ + "project_id": schema.StringAttribute{ + Description: "The ID of the project.", + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + "microfrontend_group_id": schema.StringAttribute{ + Description: "The ID of the microfrontend group.", + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + "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_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()}, + }, + "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()}, + }, + }, + } +} + +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, + }) + + cdr := client.MicrofrontendGroupMembership{ + ProjectID: plan.ProjectID.ValueString(), + MicrofrontendGroupID: plan.MicrofrontendGroupID.ValueString(), + DefaultRoute: plan.DefaultRoute.ValueString(), + RouteObservabilityToThisProject: plan.RouteObservabilityToThisProject.ValueBool(), + } + + group, err := r.client.GetMicrofrontendGroup(ctx, plan.MicrofrontendGroupID.ValueString(), plan.TeamID.ValueString()) + + if err != nil { + resp.Diagnostics.AddError( + "Error getting microfrontend group", + "Could not get microfrontend group, unexpected error: "+err.Error(), + ) + return + } + + out, err := r.client.AddOrUpdateMicrofrontendGroupMembership(ctx, cdr, group) + 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 + } + + out, err := r.client.GetMicrofrontendGroupMembership(ctx, client.MicrofrontendGroupMembership{ + ProjectID: state.ProjectID.ValueString(), + MicrofrontendGroupID: state.MicrofrontendGroupID.ValueString(), + TeamID: state.TeamID.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 + } + + cdr := client.MicrofrontendGroupMembership{ + ProjectID: plan.ProjectID.ValueString(), + MicrofrontendGroupID: plan.MicrofrontendGroupID.ValueString(), + DefaultRoute: plan.DefaultRoute.ValueString(), + RouteObservabilityToThisProject: plan.RouteObservabilityToThisProject.ValueBool(), + } + + group, err := r.client.GetMicrofrontendGroup(ctx, plan.MicrofrontendGroupID.ValueString(), plan.TeamID.ValueString()) + + if err != nil { + resp.Diagnostics.AddError( + "Error getting microfrontend group", + "Could not get microfrontend group, unexpected error: "+err.Error(), + ) + return + } + + out, err := r.client.AddOrUpdateMicrofrontendGroupMembership(ctx, cdr, group) + 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(), + }) + + group, err := r.client.GetMicrofrontendGroup(ctx, state.MicrofrontendGroupID.ValueString(), state.TeamID.ValueString()) + + if err != nil { + resp.Diagnostics.AddError( + "Error getting microfrontend group", + "Could not get microfrontend group, unexpected error: "+err.Error(), + ) + return + } + + _, err = r.client.RemoveMicrofrontendGroupMembership(ctx, client.MicrofrontendGroupMembership{ + MicrofrontendGroupID: state.MicrofrontendGroupID.ValueString(), + ProjectID: state.ProjectID.ValueString(), + TeamID: state.TeamID.ValueString(), + IsDefaultApp: group.DefaultApp == state.ProjectID.ValueString(), + }, 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(), + }) +} From b9aefbe0f0cad185c10d126ec2f73da897205a89 Mon Sep 17 00:00:00 2001 From: Kit Foster Date: Thu, 27 Feb 2025 16:10:11 +0100 Subject: [PATCH 06/12] Unit tests --- client/microfrontend_group.go | 2 +- ...resource_microfrontend_group_membership.go | 2 + vercel/resource_microfrontend_group_test.go | 75 ++++--------------- 3 files changed, 16 insertions(+), 63 deletions(-) diff --git a/client/microfrontend_group.go b/client/microfrontend_group.go index 8b2bf20f..5b0ce50c 100644 --- a/client/microfrontend_group.go +++ b/client/microfrontend_group.go @@ -162,7 +162,7 @@ func (c *Client) GetMicrofrontendGroup(ctx context.Context, microfrontendGroupID ID: res.Groups[i].Group.ID, Name: res.Groups[i].Group.Name, Slug: res.Groups[i].Group.Slug, - TeamID: teamID, + TeamID: c.teamID(teamID), DefaultApp: defaultApp, Projects: projects, } diff --git a/vercel/resource_microfrontend_group_membership.go b/vercel/resource_microfrontend_group_membership.go index 7bcf9316..f4e0583c 100644 --- a/vercel/resource_microfrontend_group_membership.go +++ b/vercel/resource_microfrontend_group_membership.go @@ -133,6 +133,7 @@ func (r *microfrontendGroupMembershipResource) Create(ctx context.Context, req r MicrofrontendGroupID: plan.MicrofrontendGroupID.ValueString(), DefaultRoute: plan.DefaultRoute.ValueString(), RouteObservabilityToThisProject: plan.RouteObservabilityToThisProject.ValueBool(), + TeamID: plan.TeamID.ValueString(), } group, err := r.client.GetMicrofrontendGroup(ctx, plan.MicrofrontendGroupID.ValueString(), plan.TeamID.ValueString()) @@ -234,6 +235,7 @@ func (r *microfrontendGroupMembershipResource) Update(ctx context.Context, req r MicrofrontendGroupID: plan.MicrofrontendGroupID.ValueString(), DefaultRoute: plan.DefaultRoute.ValueString(), RouteObservabilityToThisProject: plan.RouteObservabilityToThisProject.ValueBool(), + TeamID: plan.TeamID.ValueString(), } group, err := r.client.GetMicrofrontendGroup(ctx, plan.MicrofrontendGroupID.ValueString(), plan.TeamID.ValueString()) diff --git a/vercel/resource_microfrontend_group_test.go b/vercel/resource_microfrontend_group_test.go index 136c5244..0f1180e5 100644 --- a/vercel/resource_microfrontend_group_test.go +++ b/vercel/resource_microfrontend_group_test.go @@ -3,7 +3,6 @@ package vercel_test import ( "context" "fmt" - "regexp" "testing" "github.com/hashicorp/terraform-plugin-testing/helper/acctest" @@ -57,60 +56,6 @@ func TestAcc_MicrofrontendGroupResource(t *testing.T) { ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, CheckDestroy: testCheckMicrofrontendGroupDeleted("vercel_microfrontend_group.test", testTeam()), Steps: []resource.TestStep{ - { - Config: ` - resource "vercel_microfrontend_group" "test" { - name = "foo" - } - `, - ExpectError: regexp.MustCompile(`The argument "projects" is required, but no definition was found.`), - }, - { - 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 = "foo-%[1]s" - %[2]s - projects = { - (vercel_project.test.id) = {} - (vercel_project.test-2.id) = {} - } - } - `, name, teamIDConfig()), - ExpectError: regexp.MustCompile(`Invalid Attribute Combination`), - }, - { - 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 = "foo-%[1]s" - %[2]s - projects = { - (vercel_project.test.id) = { - is_default_app = true - } - (vercel_project.test-2.id) = { - is_default_app = true - } - } - } - `, name, teamIDConfig()), - ExpectError: regexp.MustCompile(`Invalid Attribute Combination`), - }, { Config: fmt.Sprintf(` resource "vercel_project" "test" { @@ -123,20 +68,26 @@ func TestAcc_MicrofrontendGroupResource(t *testing.T) { } resource "vercel_microfrontend_group" "test" { name = "test-acc-microfrontend-group-%[1]s" + default_app = vercel_project.test.id + %[2]s + } + resource "vercel_microfrontend_group_membership" "test" { + project_id = vercel_project.test.id + microfrontend_group_id = vercel_microfrontend_group.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 - projects = { - (vercel_project.test.id) = { - is_default_app = true - } - (vercel_project.test-2.id) = {} - } } `, 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.TestCheckResourceAttr("vercel_microfrontend_group.test", "projects.%", "2"), + resource.TestCheckResourceAttrSet("vercel_microfrontend_group_membership.test", "project_id"), + resource.TestCheckResourceAttrSet("vercel_microfrontend_group_membership.test-2", "microfrontend_group_id"), ), }, }, From 4d5b84cddddadc42fc646b4bbf6132ea6cb38552 Mon Sep 17 00:00:00 2001 From: Kit Foster Date: Mon, 3 Mar 2025 10:56:38 +0100 Subject: [PATCH 07/12] Add default app to group resource --- client/microfrontend_group.go | 20 +-- client/microfrontend_group_membership.go | 14 +- docs/resources/microfrontend_group.md | 13 +- vercel/resource_microfrontend_group.go | 149 ++++++++++-------- ...resource_microfrontend_group_membership.go | 121 ++++++-------- 5 files changed, 162 insertions(+), 155 deletions(-) diff --git a/client/microfrontend_group.go b/client/microfrontend_group.go index 5b0ce50c..ff93e022 100644 --- a/client/microfrontend_group.go +++ b/client/microfrontend_group.go @@ -13,7 +13,7 @@ type MicrofrontendGroup struct { Slug string `json:"slug"` TeamID string `json:"team_id"` Projects map[string]MicrofrontendGroupMembership `json:"projects"` - DefaultApp string `json:"defaultApp"` + DefaultApp MicrofrontendGroupMembership `json:"defaultApp"` } type MicrofrontendGroupsAPI struct { @@ -25,19 +25,19 @@ type MicrofrontendGroupsAPIResponse struct { Groups []MicrofrontendGroupsAPI `json:"groups"` } -func (c *Client) CreateMicrofrontendGroup(ctx context.Context, request MicrofrontendGroup) (r MicrofrontendGroup, err error) { - if c.teamID(request.TeamID) == "" { +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": request.Name, - "team_id": c.teamID(request.TeamID), + "microfrontend_group_name": Name, + "team_id": c.teamID(TeamID), }) - url := fmt.Sprintf("%s/teams/%s/microfrontends", c.baseURL, c.teamID(request.TeamID)) + url := fmt.Sprintf("%s/teams/%s/microfrontends", c.baseURL, c.teamID(TeamID)) payload := string(mustMarshal(struct { NewMicrofrontendsGroupName string `json:"newMicrofrontendsGroupName"` }{ - NewMicrofrontendsGroupName: request.Name, + NewMicrofrontendsGroupName: Name, })) apiResponse := struct { NewMicrofrontendGroup MicrofrontendGroup `json:"newMicrofrontendsGroup"` @@ -55,7 +55,7 @@ func (c *Client) CreateMicrofrontendGroup(ctx context.Context, request Microfron ID: apiResponse.NewMicrofrontendGroup.ID, Name: apiResponse.NewMicrofrontendGroup.Name, Slug: apiResponse.NewMicrofrontendGroup.Slug, - TeamID: c.teamID(request.TeamID), + TeamID: c.teamID(TeamID), }, nil } @@ -143,7 +143,7 @@ func (c *Client) GetMicrofrontendGroup(ctx context.Context, microfrontendGroupID for i := range res.Groups { if res.Groups[i].Group.ID == microfrontendGroupID { projects := map[string]MicrofrontendGroupMembership{} - defaultApp := "" + defaultApp := MicrofrontendGroupMembership{} for _, p := range res.Groups[i].Projects { projects[p.ID] = MicrofrontendGroupMembership{ MicrofrontendGroupID: microfrontendGroupID, @@ -155,7 +155,7 @@ func (c *Client) GetMicrofrontendGroup(ctx context.Context, microfrontendGroupID RouteObservabilityToThisProject: p.Microfrontends.RouteObservabilityToThisProject, } if p.Microfrontends.IsDefaultApp { - defaultApp = p.ID + defaultApp = projects[p.ID] } } res := MicrofrontendGroup{ diff --git a/client/microfrontend_group_membership.go b/client/microfrontend_group_membership.go index d635f8ff..cab74248 100644 --- a/client/microfrontend_group_membership.go +++ b/client/microfrontend_group_membership.go @@ -49,10 +49,9 @@ func (c *Client) GetMicrofrontendGroupMembership(ctx context.Context, request Mi return group.Projects[request.ProjectID], nil } -func (c *Client) AddOrUpdateMicrofrontendGroupMembership(ctx context.Context, request MicrofrontendGroupMembership, group MicrofrontendGroup) (r MicrofrontendGroupMembership, err error) { - isDefaultApp := group.DefaultApp == request.ProjectID +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": isDefaultApp, + "is_default_app": request.IsDefaultApp, "project_id": request.ProjectID, "group_id": request.MicrofrontendGroupID, }) @@ -60,7 +59,7 @@ func (c *Client) AddOrUpdateMicrofrontendGroupMembership(ctx context.Context, re ProjectID: request.ProjectID, TeamID: c.teamID(request.TeamID), Enabled: true, - IsDefaultApp: isDefaultApp, + IsDefaultApp: request.IsDefaultApp, DefaultRoute: request.DefaultRoute, RouteObservabilityToThisProject: request.RouteObservabilityToThisProject, MicrofrontendGroupID: request.MicrofrontendGroupID, @@ -71,12 +70,7 @@ func (c *Client) AddOrUpdateMicrofrontendGroupMembership(ctx context.Context, re return p, nil } -func (c *Client) RemoveMicrofrontendGroupMembership(ctx context.Context, request MicrofrontendGroupMembership, canDeleteDefaultApp bool) (r MicrofrontendGroupMembership, err error) { - if request.IsDefaultApp && !canDeleteDefaultApp { - // Only delete the default app relationship if the entire group is being deleted - return r, 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, diff --git a/docs/resources/microfrontend_group.md b/docs/resources/microfrontend_group.md index 1a516a86..7f4c0ea2 100644 --- a/docs/resources/microfrontend_group.md +++ b/docs/resources/microfrontend_group.md @@ -45,7 +45,7 @@ resource "vercel_microfrontend_group_membership" "child-mfe-project-mfe-membersh ### Required -- `default_app` (String) The default app for the project. Used as the entry point for the microfrontend. +- `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 @@ -56,3 +56,14 @@ resource "vercel_microfrontend_group_membership" "child-mfe-project-mfe-membersh - `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. diff --git a/vercel/resource_microfrontend_group.go b/vercel/resource_microfrontend_group.go index 8d0311b1..d9b3a39d 100644 --- a/vercel/resource_microfrontend_group.go +++ b/vercel/resource_microfrontend_group.go @@ -6,6 +6,7 @@ import ( "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" @@ -76,30 +77,53 @@ A Microfrontend Group is a definition of a microfrontend belonging to a Vercel T Computed: true, PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplaceIfConfigured(), stringplanmodifier.UseStateForUnknown()}, }, - "default_app": schema.StringAttribute{ - Description: "The default app for the project. Used as the entry point for the microfrontend.", - Required: true, - PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + "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 types.String `tfsdk:"default_app"` + 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: types.StringValue(group.DefaultApp), + 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), + }, } } @@ -109,8 +133,8 @@ func (r *microfrontendGroupResource) Create(ctx context.Context, req resource.Cr resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { resp.Diagnostics.AddError( - "Error getting microfrontendGroup plan", - "Error getting microfrontendGroup plan", + "Error getting microfrontend group plan", + "Error getting microfrontend group plan", ) return } @@ -120,12 +144,7 @@ func (r *microfrontendGroupResource) Create(ctx context.Context, req resource.Cr "name": plan.Name.ValueString(), }) - cdr := client.MicrofrontendGroup{ - Name: plan.Name.ValueString(), - TeamID: plan.TeamID.ValueString(), - } - - out, err := r.client.CreateMicrofrontendGroup(ctx, cdr) + out, err := r.client.CreateMicrofrontendGroup(ctx, plan.TeamID.ValueString(), plan.Name.ValueString()) if err != nil { resp.Diagnostics.AddError( "Error creating microfrontend group", @@ -137,24 +156,16 @@ func (r *microfrontendGroupResource) Create(ctx context.Context, req resource.Cr tflog.Info(ctx, "creating default group membership", map[string]interface{}{ "team_id": plan.TeamID.ValueString(), "name": plan.Name.ValueString(), - "default_app": plan.DefaultApp.ValueString(), + "default_app": plan.DefaultApp.ProjectID.ValueString(), }) - group := client.MicrofrontendGroup{ - ID: out.ID, - Name: out.Name, - Slug: out.Slug, - TeamID: out.TeamID, - DefaultApp: plan.DefaultApp.ValueString(), - Projects: out.Projects, - } - - _, err = r.client.AddOrUpdateMicrofrontendGroupMembership(ctx, client.MicrofrontendGroupMembership{ - ProjectID: plan.DefaultApp.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, - }, group) + }) if err != nil { resp.Diagnostics.AddError( @@ -164,13 +175,29 @@ func (r *microfrontendGroupResource) Create(ctx context.Context, req resource.Cr 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.ValueString(), + "default_app": result.DefaultApp.ProjectID.ValueString(), }) diags = resp.State.Set(ctx, result) @@ -281,38 +308,36 @@ func (r *microfrontendGroupResource) Delete(ctx context.Context, req resource.De return } - if state.DefaultApp.ValueString() != "" { - tflog.Info(ctx, "deleting microfrontend default app group membership", map[string]interface{}{ - "group_id": state.ID.ValueString(), - "project_id": state.DefaultApp.ValueString(), - "team_id": state.TeamID.ValueString(), - }) - - _, err := r.client.RemoveMicrofrontendGroupMembership(ctx, client.MicrofrontendGroupMembership{ - MicrofrontendGroupID: state.ID.ValueString(), - TeamID: state.TeamID.ValueString(), - ProjectID: state.DefaultApp.ValueString(), - }, true) - - 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.ValueString(), - err, - ), - ) - 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{ + _, err = r.client.DeleteMicrofrontendGroup(ctx, client.MicrofrontendGroup{ ID: state.ID.ValueString(), TeamID: state.TeamID.ValueString(), Slug: state.Slug.ValueString(), diff --git a/vercel/resource_microfrontend_group_membership.go b/vercel/resource_microfrontend_group_membership.go index f4e0583c..061f9644 100644 --- a/vercel/resource_microfrontend_group_membership.go +++ b/vercel/resource_microfrontend_group_membership.go @@ -50,6 +50,45 @@ func (r *microfrontendGroupMembershipResource) Configure(ctx context.Context, re 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{ @@ -58,37 +97,7 @@ 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: map[string]schema.Attribute{ - "project_id": schema.StringAttribute{ - Description: "The ID of the project.", - Required: true, - PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, - }, - "microfrontend_group_id": schema.StringAttribute{ - Description: "The ID of the microfrontend group.", - Required: true, - PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, - }, - "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_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()}, - }, - "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()}, - }, - }, + Attributes: getMicrofrontendGroupMembershipSchema(false), } } @@ -128,25 +137,14 @@ func (r *microfrontendGroupMembershipResource) Create(ctx context.Context, req r "plan": plan, }) - cdr := client.MicrofrontendGroupMembership{ + 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(), - } - - group, err := r.client.GetMicrofrontendGroup(ctx, plan.MicrofrontendGroupID.ValueString(), plan.TeamID.ValueString()) - - if err != nil { - resp.Diagnostics.AddError( - "Error getting microfrontend group", - "Could not get microfrontend group, unexpected error: "+err.Error(), - ) - return - } - - out, err := r.client.AddOrUpdateMicrofrontendGroupMembership(ctx, cdr, group) + IsDefaultApp: false, + }) if err != nil { resp.Diagnostics.AddError( "Error creating microfrontend group membership", @@ -230,25 +228,14 @@ func (r *microfrontendGroupMembershipResource) Update(ctx context.Context, req r return } - cdr := client.MicrofrontendGroupMembership{ + 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(), - } - - group, err := r.client.GetMicrofrontendGroup(ctx, plan.MicrofrontendGroupID.ValueString(), plan.TeamID.ValueString()) - - if err != nil { - resp.Diagnostics.AddError( - "Error getting microfrontend group", - "Could not get microfrontend group, unexpected error: "+err.Error(), - ) - return - } - - out, err := r.client.AddOrUpdateMicrofrontendGroupMembership(ctx, cdr, group) + IsDefaultApp: false, + }) if err != nil { resp.Diagnostics.AddError( "Error updating microfrontend group membership", @@ -292,22 +279,12 @@ func (r *microfrontendGroupMembershipResource) Delete(ctx context.Context, req r "team_id": state.TeamID.ValueString(), }) - group, err := r.client.GetMicrofrontendGroup(ctx, state.MicrofrontendGroupID.ValueString(), state.TeamID.ValueString()) - - if err != nil { - resp.Diagnostics.AddError( - "Error getting microfrontend group", - "Could not get microfrontend group, unexpected error: "+err.Error(), - ) - return - } - - _, err = r.client.RemoveMicrofrontendGroupMembership(ctx, client.MicrofrontendGroupMembership{ + _, err := r.client.RemoveMicrofrontendGroupMembership(ctx, client.MicrofrontendGroupMembership{ MicrofrontendGroupID: state.MicrofrontendGroupID.ValueString(), ProjectID: state.ProjectID.ValueString(), TeamID: state.TeamID.ValueString(), - IsDefaultApp: group.DefaultApp == state.ProjectID.ValueString(), - }, false) + IsDefaultApp: false, + }) if err != nil { resp.Diagnostics.AddError( "Error deleting microfrontend group membership", From e13578af0cc11877c4f2b5dbc0b6aa8866e4f109 Mon Sep 17 00:00:00 2001 From: Kit Foster Date: Mon, 3 Mar 2025 14:49:53 +0100 Subject: [PATCH 08/12] Update data source --- client/microfrontend_group.go | 46 +++++++++------ docs/data-sources/microfrontend_group.md | 10 +++- vercel/data_source_microfrontend_group.go | 22 +++++--- .../data_source_microfrontend_group_test.go | 56 +++++++++++++++++++ vercel/resource_microfrontend_group.go | 21 +++---- vercel/resource_microfrontend_group_test.go | 11 ++-- 6 files changed, 123 insertions(+), 43 deletions(-) create mode 100644 vercel/data_source_microfrontend_group_test.go diff --git a/client/microfrontend_group.go b/client/microfrontend_group.go index ff93e022..737c3a64 100644 --- a/client/microfrontend_group.go +++ b/client/microfrontend_group.go @@ -16,13 +16,23 @@ type MicrofrontendGroup struct { DefaultApp MicrofrontendGroupMembership `json:"defaultApp"` } -type MicrofrontendGroupsAPI struct { - Group MicrofrontendGroup `json:"group"` - Projects []MicrofrontendGroupMembershipsResponseAPI `json:"projects"` -} - type MicrofrontendGroupsAPIResponse struct { - Groups []MicrofrontendGroupsAPI `json:"groups"` + 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) { @@ -124,27 +134,27 @@ func (c *Client) GetMicrofrontendGroup(ctx context.Context, microfrontendGroupID tflog.Info(ctx, "getting microfrontend group", map[string]interface{}{ "url": url, }) - res := MicrofrontendGroupsAPIResponse{} + out := MicrofrontendGroupsAPIResponse{} err = c.doRequest(clientRequest{ ctx: ctx, method: "GET", url: url, body: "", - }, &res) + }, &out) if err != nil { return r, err } tflog.Info(ctx, "getting microfrontend group", map[string]interface{}{ - "res": res, + "out": out, }) - for i := range res.Groups { - if res.Groups[i].Group.ID == microfrontendGroupID { + for i := range out.Groups { + if out.Groups[i].Group.ID == microfrontendGroupID { projects := map[string]MicrofrontendGroupMembership{} defaultApp := MicrofrontendGroupMembership{} - for _, p := range res.Groups[i].Projects { + for _, p := range out.Groups[i].Projects { projects[p.ID] = MicrofrontendGroupMembership{ MicrofrontendGroupID: microfrontendGroupID, ProjectID: p.ID, @@ -158,18 +168,18 @@ func (c *Client) GetMicrofrontendGroup(ctx context.Context, microfrontendGroupID defaultApp = projects[p.ID] } } - res := MicrofrontendGroup{ - ID: res.Groups[i].Group.ID, - Name: res.Groups[i].Group.Name, - Slug: res.Groups[i].Group.Slug, + 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{}{ - "res": res, + "r": r, }) - return res, nil + return r, nil } } diff --git a/docs/data-sources/microfrontend_group.md b/docs/data-sources/microfrontend_group.md index 9fe8b29a..d72ea605 100644 --- a/docs/data-sources/microfrontend_group.md +++ b/docs/data-sources/microfrontend_group.md @@ -28,6 +28,14 @@ A Microfrontend Group is a definition of a microfrontend belonging to a Vercel T ### Read-Only -- `default_app` (String) The default app for the project. Used as the entry point for the microfrontend. +- `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/vercel/data_source_microfrontend_group.go b/vercel/data_source_microfrontend_group.go index 4e2d6aaf..788fe96a 100644 --- a/vercel/data_source_microfrontend_group.go +++ b/vercel/data_source_microfrontend_group.go @@ -72,15 +72,26 @@ A Microfrontend Group is a definition of a microfrontend belonging to a Vercel T Optional: true, Computed: true, }, - "default_app": schema.StringAttribute{ + "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...) @@ -95,7 +106,7 @@ func (d *microfrontendGroupDataSource) Read(ctx context.Context, req datasource. } if err != nil { resp.Diagnostics.AddError( - "Error reading microfrontendGroup", + "Error reading microfrontend group", fmt.Sprintf("Could not get microfrontend group %s %s, unexpected error: %s", config.TeamID.ValueString(), config.ID.ValueString(), @@ -106,11 +117,8 @@ func (d *microfrontendGroupDataSource) Read(ctx context.Context, req datasource. } result := convertResponseToMicrofrontendGroup(out) - tflog.Info(ctx, "read microfrontendGroup", map[string]interface{}{ - "team_id": result.TeamID.ValueString(), - "group_id": result.ID.ValueString(), - "slug": result.Slug.ValueString(), - "name": result.Name.ValueString(), + tflog.Info(ctx, "read microfrontend group", map[string]interface{}{ + "result": result, }) diags = resp.State.Set(ctx, result) diff --git a/vercel/data_source_microfrontend_group_test.go b/vercel/data_source_microfrontend_group_test.go new file mode 100644 index 00000000..c30136c0 --- /dev/null +++ b/vercel/data_source_microfrontend_group_test.go @@ -0,0 +1,56 @@ +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" { + 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 + } + data "vercel_microfrontend_group" "test" { + id = vercel_microfrontend_group.test.id + %[2]s + } + data "vercel_microfrontend_group_membership" "test-2" { + microfrontend_group_id = vercel_microfrontend_group.test.id + project_id = vercel_microfrontend_group_membership.test-2.project_id + %[2]s + } + `, name, teamIDConfig()), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("data.vercel_microfrontend_group.test", "name", "long-term-test"), + resource.TestCheckResourceAttr("data.vercel_microfrontend_group.test", "id", "mfe_z5wEafgq19cbB92CAQV7fZgUXAdp"), + ), + }, + }, + }) +} diff --git a/vercel/resource_microfrontend_group.go b/vercel/resource_microfrontend_group.go index d9b3a39d..9e4c118c 100644 --- a/vercel/resource_microfrontend_group.go +++ b/vercel/resource_microfrontend_group.go @@ -107,11 +107,11 @@ type MicrofrontendGroupDefaultApp struct { } 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"` + 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 { @@ -120,7 +120,7 @@ func convertResponseToMicrofrontendGroup(group client.MicrofrontendGroup) Microf Name: types.StringValue(group.Name), Slug: types.StringValue(group.Slug), TeamID: types.StringValue(group.TeamID), - DefaultApp: MicrofrontendGroupDefaultApp{ + DefaultApp: &MicrofrontendGroupDefaultApp{ ProjectID: types.StringValue(group.DefaultApp.ProjectID), DefaultRoute: types.StringValue(group.DefaultApp.DefaultRoute), }, @@ -234,10 +234,11 @@ func (r *microfrontendGroupResource) Read(ctx context.Context, req resource.Read result := convertResponseToMicrofrontendGroup(out) tflog.Info(ctx, "read microfrontend group", map[string]interface{}{ - "team_id": result.TeamID.ValueString(), - "group_id": result.ID.ValueString(), - "slug": result.Slug.ValueString(), - "name": result.Name.ValueString(), + "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) diff --git a/vercel/resource_microfrontend_group_test.go b/vercel/resource_microfrontend_group_test.go index 0f1180e5..225379e1 100644 --- a/vercel/resource_microfrontend_group_test.go +++ b/vercel/resource_microfrontend_group_test.go @@ -68,12 +68,9 @@ func TestAcc_MicrofrontendGroupResource(t *testing.T) { } resource "vercel_microfrontend_group" "test" { name = "test-acc-microfrontend-group-%[1]s" - default_app = vercel_project.test.id - %[2]s - } - resource "vercel_microfrontend_group_membership" "test" { - project_id = vercel_project.test.id - microfrontend_group_id = vercel_microfrontend_group.test.id + default_app = { + project_id = vercel_project.test.id + } %[2]s } resource "vercel_microfrontend_group_membership" "test-2" { @@ -86,7 +83,7 @@ func TestAcc_MicrofrontendGroupResource(t *testing.T) { 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_membership.test", "project_id"), + resource.TestCheckResourceAttrSet("vercel_microfrontend_group.test.default_app", "project_id"), resource.TestCheckResourceAttrSet("vercel_microfrontend_group_membership.test-2", "microfrontend_group_id"), ), }, From 65bf91a7819d6d0ef4875c130d971a6b416b8c87 Mon Sep 17 00:00:00 2001 From: Kit Foster Date: Mon, 3 Mar 2025 16:18:03 +0100 Subject: [PATCH 09/12] Support import --- client/microfrontend_group_membership.go | 14 ++--- docs/resources/microfrontend_group.md | 15 +++++ .../microfrontend_group_membership.md | 17 ++++++ .../vercel_microfrontend_group/import.sh | 9 +++ .../import.sh | 11 ++++ ...a_source_microfrontend_group_membership.go | 9 ++- .../data_source_microfrontend_group_test.go | 29 ++++----- vercel/resource_microfrontend_group.go | 58 +++++++++++++++--- ...resource_microfrontend_group_membership.go | 60 ++++++++++++++++--- 9 files changed, 181 insertions(+), 41 deletions(-) create mode 100644 examples/resources/vercel_microfrontend_group/import.sh create mode 100644 examples/resources/vercel_microfrontend_group_membership/import.sh diff --git a/client/microfrontend_group_membership.go b/client/microfrontend_group_membership.go index cab74248..91315a55 100644 --- a/client/microfrontend_group_membership.go +++ b/client/microfrontend_group_membership.go @@ -32,21 +32,21 @@ type MicrofrontendGroupMembershipsResponseAPI struct { Microfrontends MicrofrontendGroupMembershipResponseAPI `json:"microfrontends"` } -func (c *Client) GetMicrofrontendGroupMembership(ctx context.Context, request MicrofrontendGroupMembership) (r MicrofrontendGroupMembership, err error) { +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": request.ProjectID, - "group_id": request.MicrofrontendGroupID, - "team_id": c.teamID(request.TeamID), + "project_id": ProjectID, + "group_id": GroupID, + "team_id": c.teamID(TeamID), }) - group, err := c.GetMicrofrontendGroup(ctx, request.MicrofrontendGroupID, c.teamID(request.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": request.ProjectID, + "project_id": ProjectID, "group": group, }) - return group.Projects[request.ProjectID], nil + return group.Projects[ProjectID], nil } func (c *Client) AddOrUpdateMicrofrontendGroupMembership(ctx context.Context, request MicrofrontendGroupMembership) (r MicrofrontendGroupMembership, err error) { diff --git a/docs/resources/microfrontend_group.md b/docs/resources/microfrontend_group.md index 7f4c0ea2..d2acf9cf 100644 --- a/docs/resources/microfrontend_group.md +++ b/docs/resources/microfrontend_group.md @@ -67,3 +67,18 @@ Required: 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 index d3e7ed2c..99eb42e0 100644 --- a/docs/resources/microfrontend_group_membership.md +++ b/docs/resources/microfrontend_group_membership.md @@ -53,3 +53,20 @@ resource "vercel_microfrontend_group_membership" "child-mfe-project-mfe-membersh - `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/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_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/vercel/data_source_microfrontend_group_membership.go b/vercel/data_source_microfrontend_group_membership.go index 7249a1d2..2eca3831 100644 --- a/vercel/data_source_microfrontend_group_membership.go +++ b/vercel/data_source_microfrontend_group_membership.go @@ -88,11 +88,10 @@ func (d *microfrontendGroupMembershipDataSource) Read(ctx context.Context, req d return } - out, err := d.client.GetMicrofrontendGroupMembership(ctx, client.MicrofrontendGroupMembership{ - ProjectID: config.ProjectID.ValueString(), - MicrofrontendGroupID: config.MicrofrontendGroupID.ValueString(), - TeamID: config.TeamID.ValueString(), - }) + out, err := d.client.GetMicrofrontendGroupMembership(ctx, config.TeamID.ValueString(), + config.MicrofrontendGroupID.ValueString(), + config.ProjectID.ValueString(), + ) if client.NotFound(err) { resp.State.RemoveResource(ctx) return diff --git a/vercel/data_source_microfrontend_group_test.go b/vercel/data_source_microfrontend_group_test.go index c30136c0..b1a76868 100644 --- a/vercel/data_source_microfrontend_group_test.go +++ b/vercel/data_source_microfrontend_group_test.go @@ -16,39 +16,40 @@ func TestAcc_MicrofrontendGroupDataSource(t *testing.T) { Steps: []resource.TestStep{ { Config: fmt.Sprintf(` - resource "vercel_project" "test" { + resource "vercel_project" "test-project-1" { name = "test-acc-project-%[1]s" %[2]s } - resource "vercel_project" "test-2" { + resource "vercel_project" "test-project-2" { name = "test-acc-project-2-%[1]s" %[2]s } - resource "vercel_microfrontend_group" "test" { + resource "vercel_microfrontend_group" "test-group" { name = "test-acc-microfrontend-group-%[1]s" default_app = { - project_id = vercel_project.test.id + project_id = vercel_project.test-project-1.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 + 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" { - id = vercel_microfrontend_group.test.id + data "vercel_microfrontend_group" "test-group" { + id = vercel_microfrontend_group.test-group.id %[2]s } - data "vercel_microfrontend_group_membership" "test-2" { - microfrontend_group_id = vercel_microfrontend_group.test.id - project_id = vercel_microfrontend_group_membership.test-2.project_id + 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", "name", "long-term-test"), - resource.TestCheckResourceAttr("data.vercel_microfrontend_group.test", "id", "mfe_z5wEafgq19cbB92CAQV7fZgUXAdp"), + resource.TestCheckResourceAttr("data.vercel_microfrontend_group.test-group", "name", "long-term-test"), + resource.TestCheckResourceAttrSet("data.vercel_microfrontend_group.test-group.default_app", "project_id"), + resource.TestCheckResourceAttrSet("data.vercel_microfrontend_group_membership.test-child", "project_id"), ), }, }, diff --git a/vercel/resource_microfrontend_group.go b/vercel/resource_microfrontend_group.go index 9e4c118c..55de3921 100644 --- a/vercel/resource_microfrontend_group.go +++ b/vercel/resource_microfrontend_group.go @@ -15,8 +15,9 @@ import ( ) var ( - _ resource.Resource = µfrontendGroupResource{} - _ resource.ResourceWithConfigure = µfrontendGroupResource{} + _ resource.Resource = µfrontendGroupResource{} + _ resource.ResourceWithConfigure = µfrontendGroupResource{} + _ resource.ResourceWithImportState = µfrontendGroupResource{} ) func newMicrofrontendGroupResource() resource.Resource { @@ -107,11 +108,11 @@ type MicrofrontendGroupDefaultApp struct { } 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"` + 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 { @@ -120,7 +121,7 @@ func convertResponseToMicrofrontendGroup(group client.MicrofrontendGroup) Microf Name: types.StringValue(group.Name), Slug: types.StringValue(group.Slug), TeamID: types.StringValue(group.TeamID), - DefaultApp: &MicrofrontendGroupDefaultApp{ + DefaultApp: MicrofrontendGroupDefaultApp{ ProjectID: types.StringValue(group.DefaultApp.ProjectID), DefaultRoute: types.StringValue(group.DefaultApp.DefaultRoute), }, @@ -359,3 +360,44 @@ func (r *microfrontendGroupResource) Delete(ctx context.Context, req resource.De "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 index 061f9644..0f403374 100644 --- a/vercel/resource_microfrontend_group_membership.go +++ b/vercel/resource_microfrontend_group_membership.go @@ -16,8 +16,9 @@ import ( ) var ( - _ resource.Resource = µfrontendGroupMembershipResource{} - _ resource.ResourceWithConfigure = µfrontendGroupMembershipResource{} + _ resource.Resource = µfrontendGroupMembershipResource{} + _ resource.ResourceWithConfigure = µfrontendGroupMembershipResource{} + _ resource.ResourceWithImportState = µfrontendGroupMembershipResource{} ) func newMicrofrontendGroupMembershipResource() resource.Resource { @@ -174,11 +175,16 @@ func (r *microfrontendGroupMembershipResource) Read(ctx context.Context, req res return } - out, err := r.client.GetMicrofrontendGroupMembership(ctx, client.MicrofrontendGroupMembership{ - ProjectID: state.ProjectID.ValueString(), - MicrofrontendGroupID: state.MicrofrontendGroupID.ValueString(), - TeamID: state.TeamID.ValueString(), - }) + 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 @@ -302,3 +308,43 @@ func (r *microfrontendGroupMembershipResource) Delete(ctx context.Context, req r "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 + } +} From 18f9b370f3c26b75037bbc8275391ba7cf07629c Mon Sep 17 00:00:00 2001 From: Kit Foster Date: Mon, 3 Mar 2025 17:14:58 +0100 Subject: [PATCH 10/12] Update microfrontend data source --- vercel/resource_microfrontend_group.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/vercel/resource_microfrontend_group.go b/vercel/resource_microfrontend_group.go index 55de3921..1917beee 100644 --- a/vercel/resource_microfrontend_group.go +++ b/vercel/resource_microfrontend_group.go @@ -108,11 +108,11 @@ type MicrofrontendGroupDefaultApp struct { } 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"` + 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 { @@ -121,7 +121,7 @@ func convertResponseToMicrofrontendGroup(group client.MicrofrontendGroup) Microf Name: types.StringValue(group.Name), Slug: types.StringValue(group.Slug), TeamID: types.StringValue(group.TeamID), - DefaultApp: MicrofrontendGroupDefaultApp{ + DefaultApp: &MicrofrontendGroupDefaultApp{ ProjectID: types.StringValue(group.DefaultApp.ProjectID), DefaultRoute: types.StringValue(group.DefaultApp.DefaultRoute), }, From 927bb92440d1e7770b432ee7dc22c7c717eea0b1 Mon Sep 17 00:00:00 2001 From: Kit Foster Date: Mon, 3 Mar 2025 17:19:28 +0100 Subject: [PATCH 11/12] Add data example --- docs/data-sources/microfrontend_group.md | 6 ++++++ .../microfrontend_group_membership.md | 9 ++++++++- docs/resources/microfrontend_group.md | 20 +++++++++---------- .../microfrontend_group_membership.md | 20 +++++++++---------- .../vercel_microfrontend_group/data-source.tf | 4 ++++ .../data-source.tf | 5 +++++ .../vercel_microfrontend_group/resource.tf | 20 +++++++++---------- .../resource.tf | 20 +++++++++---------- 8 files changed, 63 insertions(+), 41 deletions(-) create mode 100644 examples/data-sources/vercel_microfrontend_group/data-source.tf create mode 100644 examples/data-sources/vercel_microfrontend_group_membership/data-source.tf diff --git a/docs/data-sources/microfrontend_group.md b/docs/data-sources/microfrontend_group.md index d72ea605..8089e51c 100644 --- a/docs/data-sources/microfrontend_group.md +++ b/docs/data-sources/microfrontend_group.md @@ -13,7 +13,13 @@ 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 diff --git a/docs/data-sources/microfrontend_group_membership.md b/docs/data-sources/microfrontend_group_membership.md index 4b779d5a..e2882086 100644 --- a/docs/data-sources/microfrontend_group_membership.md +++ b/docs/data-sources/microfrontend_group_membership.md @@ -13,7 +13,14 @@ 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 diff --git a/docs/resources/microfrontend_group.md b/docs/resources/microfrontend_group.md index d2acf9cf..d63ecad8 100644 --- a/docs/resources/microfrontend_group.md +++ b/docs/resources/microfrontend_group.md @@ -16,27 +16,27 @@ A Microfrontend Group is a definition of a microfrontend belonging to a Vercel T ## Example Usage ```terraform -data "vercel_project" "parent-mfe-project" { +data "vercel_project" "parent_mfe_project" { name = "my parent project" } -data "vercel_project" "child-mfe-project" { +data "vercel_project" "child_mfe_project" { name = "my child project" } -resource "vercel_microfrontend_group" "example-mfe-group" { +resource "vercel_microfrontend_group" "example_mfe_group" { name = "my mfe" - default_app = vercel_project.parent-mfe-project.id + 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" "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 +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/docs/resources/microfrontend_group_membership.md b/docs/resources/microfrontend_group_membership.md index 99eb42e0..db79a184 100644 --- a/docs/resources/microfrontend_group_membership.md +++ b/docs/resources/microfrontend_group_membership.md @@ -16,27 +16,27 @@ A Microfrontend Group Membership is a definition of a Vercel Project being a par ## Example Usage ```terraform -data "vercel_project" "parent-mfe-project" { +data "vercel_project" "parent_mfe_project" { name = "my parent project" } -data "vercel_project" "child-mfe-project" { +data "vercel_project" "child_mfe_project" { name = "my child project" } -resource "vercel_microfrontend_group" "example-mfe-group" { +resource "vercel_microfrontend_group" "example_mfe_group" { name = "my mfe" - default_app = vercel_project.parent-mfe-project.id + 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" "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 +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/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/resource.tf b/examples/resources/vercel_microfrontend_group/resource.tf index 66af13ef..805709ff 100644 --- a/examples/resources/vercel_microfrontend_group/resource.tf +++ b/examples/resources/vercel_microfrontend_group/resource.tf @@ -1,22 +1,22 @@ -data "vercel_project" "parent-mfe-project" { +data "vercel_project" "parent_mfe_project" { name = "my parent project" } -data "vercel_project" "child-mfe-project" { +data "vercel_project" "child_mfe_project" { name = "my child project" } -resource "vercel_microfrontend_group" "example-mfe-group" { +resource "vercel_microfrontend_group" "example_mfe_group" { name = "my mfe" - default_app = vercel_project.parent-mfe-project.id + 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" "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 +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/resource.tf b/examples/resources/vercel_microfrontend_group_membership/resource.tf index 66af13ef..805709ff 100644 --- a/examples/resources/vercel_microfrontend_group_membership/resource.tf +++ b/examples/resources/vercel_microfrontend_group_membership/resource.tf @@ -1,22 +1,22 @@ -data "vercel_project" "parent-mfe-project" { +data "vercel_project" "parent_mfe_project" { name = "my parent project" } -data "vercel_project" "child-mfe-project" { +data "vercel_project" "child_mfe_project" { name = "my child project" } -resource "vercel_microfrontend_group" "example-mfe-group" { +resource "vercel_microfrontend_group" "example_mfe_group" { name = "my mfe" - default_app = vercel_project.parent-mfe-project.id + 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" "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 +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 } From a79a6bf7507efdd719bd1b2c90b2a967757181cb Mon Sep 17 00:00:00 2001 From: Kit Foster Date: Mon, 3 Mar 2025 19:51:54 +0100 Subject: [PATCH 12/12] Update tests --- .../data_source_microfrontend_group_test.go | 30 +++++++++---------- vercel/resource_microfrontend_group_test.go | 2 +- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/vercel/data_source_microfrontend_group_test.go b/vercel/data_source_microfrontend_group_test.go index b1a76868..3c0f75d5 100644 --- a/vercel/data_source_microfrontend_group_test.go +++ b/vercel/data_source_microfrontend_group_test.go @@ -16,40 +16,40 @@ func TestAcc_MicrofrontendGroupDataSource(t *testing.T) { Steps: []resource.TestStep{ { Config: fmt.Sprintf(` - resource "vercel_project" "test-project-1" { + resource "vercel_project" "test_project_1" { name = "test-acc-project-%[1]s" %[2]s } - resource "vercel_project" "test-project-2" { + resource "vercel_project" "test_project_2" { name = "test-acc-project-2-%[1]s" %[2]s } - resource "vercel_microfrontend_group" "test-group" { + resource "vercel_microfrontend_group" "test_group" { name = "test-acc-microfrontend-group-%[1]s" default_app = { - project_id = vercel_project.test-project-1.id + 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 + 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 + 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 + 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", "long-term-test"), - resource.TestCheckResourceAttrSet("data.vercel_microfrontend_group.test-group.default_app", "project_id"), - resource.TestCheckResourceAttrSet("data.vercel_microfrontend_group_membership.test-child", "project_id"), + 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/resource_microfrontend_group_test.go b/vercel/resource_microfrontend_group_test.go index 225379e1..55ebecd2 100644 --- a/vercel/resource_microfrontend_group_test.go +++ b/vercel/resource_microfrontend_group_test.go @@ -83,7 +83,7 @@ func TestAcc_MicrofrontendGroupResource(t *testing.T) { 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.test", "default_app.project_id"), resource.TestCheckResourceAttrSet("vercel_microfrontend_group_membership.test-2", "microfrontend_group_id"), ), },