From 708a3441b004b029d94a144b594180dcbc5e3725 Mon Sep 17 00:00:00 2001 From: Dario Ferrer Date: Tue, 10 May 2022 16:35:31 +0200 Subject: [PATCH 1/5] Aliases: Add aliases deployment --- .gitignore | 1 + client/alias_create.go | 44 ++++++++ client/alias_delete.go | 44 ++++++++ client/alias_get.go | 42 +++++++ docs/resources/alias.md | 34 ++++++ vercel/provider.go | 1 + vercel/resource_alias.go | 196 +++++++++++++++++++++++++++++++++ vercel/resource_alias_model.go | 26 +++++ 8 files changed, 388 insertions(+) create mode 100644 client/alias_create.go create mode 100644 client/alias_delete.go create mode 100644 client/alias_get.go create mode 100644 docs/resources/alias.md create mode 100644 vercel/resource_alias.go create mode 100644 vercel/resource_alias_model.go diff --git a/.gitignore b/.gitignore index ad605acc..badc2056 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ terraform-provider-vercel .task/ Dist/ +.DS_Store diff --git a/client/alias_create.go b/client/alias_create.go new file mode 100644 index 00000000..3fd5f6dc --- /dev/null +++ b/client/alias_create.go @@ -0,0 +1,44 @@ +package client + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/hashicorp/terraform-plugin-log/tflog" +) + +// CreateAliasRequest defines the request the Vercel API expects in order to create an alias. +type CreateAliasRequest struct { + Alias string `json:"alias"` +} + +// CreateAlias creates an alias within Vercel. +func (c *Client) CreateAlias(ctx context.Context, request CreateAliasRequest, deploymentID string, teamID string) (r AliasResponse, err error) { + url := fmt.Sprintf("%s/now/deployments/%s/aliases", c.baseURL, deploymentID) + if teamID != "" { + url = fmt.Sprintf("%s?teamId=%s", url, teamID) + } + payload := string(mustMarshal(request)) + req, err := http.NewRequestWithContext( + ctx, + "POST", + url, + strings.NewReader(payload), + ) + if err != nil { + return r, err + } + + tflog.Trace(ctx, "creating alias", map[string]interface{}{ + "url": url, + "payload": payload, + }) + err = c.doRequest(req, &r) + if err != nil { + return r, err + } + + return r, nil +} diff --git a/client/alias_delete.go b/client/alias_delete.go new file mode 100644 index 00000000..692d2fdb --- /dev/null +++ b/client/alias_delete.go @@ -0,0 +1,44 @@ +package client + +import ( + "context" + "fmt" + "net/http" + + "github.com/hashicorp/terraform-plugin-log/tflog" +) + +// DeleteAliasResponse defines the response the Vercel API returns when an alias is deleted. +type DeleteAliasResponse struct { + Status string `json:"status"` +} + +// DeleteAlias deletes an alias within Vercel. +func (c *Client) DeleteAlias(ctx context.Context, aliasUID string, teamID string) (r DeleteAliasResponse, err error) { + url := fmt.Sprintf("%s/now/aliases/%s", c.baseURL, aliasUID) + req, err := http.NewRequest( + "DELETE", + url, + nil, + ) + if err != nil { + return r, err + } + + // Add query parameters + q := req.URL.Query() + if teamID != "" { + q.Add("teamId", teamID) + } + req.URL.RawQuery = q.Encode() + + tflog.Trace(ctx, "deleting alias", map[string]interface{}{ + "url": url, + }) + err = c.doRequest(req, &r) + if err != nil { + return r, fmt.Errorf("url: %s, err: %s", url, err) + } + + return r, nil +} diff --git a/client/alias_get.go b/client/alias_get.go new file mode 100644 index 00000000..0ed4b0e1 --- /dev/null +++ b/client/alias_get.go @@ -0,0 +1,42 @@ +package client + +import ( + "context" + "fmt" + "net/http" + + "github.com/hashicorp/terraform-plugin-log/tflog" +) + +// AliasResponse defines the response the Vercel API returns for an alias. +type AliasResponse struct { + UID string `json:"uid"` + Alias string `json:"alias"` +} + +// GetAlias retrieves information about an existing alias from vercel. +func (c *Client) GetAlias(ctx context.Context, aliasID, teamID string) (r AliasResponse, err error) { + url := fmt.Sprintf("%s/now/aliases/%s", c.baseURL, aliasID) + if teamID != "" { + url = fmt.Sprintf("%s?teamId=%s", url, teamID) + } + req, err := http.NewRequestWithContext( + ctx, + "GET", + url, + nil, + ) + if err != nil { + return r, err + } + + tflog.Trace(ctx, "getting alias", map[string]interface{}{ + "url": url, + }) + err = c.doRequest(req, &r) + if err != nil { + return r, err + } + + return r, nil +} diff --git a/docs/resources/alias.md b/docs/resources/alias.md new file mode 100644 index 00000000..94ffb248 --- /dev/null +++ b/docs/resources/alias.md @@ -0,0 +1,34 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "vercel_alias Resource - terraform-provider-vercel" +subcategory: "" +description: |- + Provides an alias resource. + An alias allows a deployment to be accessed through a different URL. +--- + +# vercel_alias (Resource) + +Provides an alias resource. + +An alias allows a deployment to be accessed through a different URL. + + + + +## Schema + +### Required + +- `alias` (String) The alias to be set on the deployment. It will become the subdomain of the Vercel project top level domain +- `deployment_id` (String) The deployment id to alias + +### Optional + +- `team_id` (String) The team or scope id + +### Read-Only + +- `uid` (String) + + diff --git a/vercel/provider.go b/vercel/provider.go index f55cb5bb..db6ce977 100644 --- a/vercel/provider.go +++ b/vercel/provider.go @@ -44,6 +44,7 @@ Use the navigation to the left to read about the available resources. // GetResources shows the available resources for the vercel provider func (p *provider) GetResources(_ context.Context) (map[string]tfsdk.ResourceType, diag.Diagnostics) { return map[string]tfsdk.ResourceType{ + "vercel_alias": resourceAliasType{}, "vercel_deployment": resourceDeploymentType{}, "vercel_project": resourceProjectType{}, "vercel_project_domain": resourceProjectDomainType{}, diff --git a/vercel/resource_alias.go b/vercel/resource_alias.go new file mode 100644 index 00000000..4d160004 --- /dev/null +++ b/vercel/resource_alias.go @@ -0,0 +1,196 @@ +package vercel + +import ( + "context" + "errors" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/vercel/terraform-provider-vercel/client" +) + +type resourceAliasType struct{} + +// GetSchema returns the schema information for an alias resource. +func (r resourceAliasType) GetSchema(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { + return tfsdk.Schema{ + Description: ` +Provides an alias resource. + +An alias allows a deployment to be accessed through a different URL.`, + Attributes: map[string]tfsdk.Attribute{ + "alias": { + Description: "The alias to be set on the deployment. It will become the subdomain of the Vercel project top level domain", + Required: true, + PlanModifiers: tfsdk.AttributePlanModifiers{tfsdk.RequiresReplace()}, + Type: types.StringType, + }, + "deployment_id": { + Description: "The deployment id to alias", + Required: true, + PlanModifiers: tfsdk.AttributePlanModifiers{tfsdk.RequiresReplace()}, + Type: types.StringType, + }, + "team_id": { + Description: "The team or scope id", + Optional: true, + PlanModifiers: tfsdk.AttributePlanModifiers{tfsdk.RequiresReplace()}, + Type: types.StringType, + }, + "uid": { + Computed: true, + Type: types.StringType, + }, + }, + }, nil +} + +// NewResource instantiates a new Resource of this ResourceType. +func (r resourceAliasType) NewResource(_ context.Context, p tfsdk.Provider) (tfsdk.Resource, diag.Diagnostics) { + return resourceAlias{ + p: *(p.(*provider)), + }, nil +} + +type resourceAlias struct { + p provider +} + +// Create will create an alias within Vercel. +// This is called automatically by the provider when a new resource should be created. +func (r resourceAlias) Create(ctx context.Context, req tfsdk.CreateResourceRequest, resp *tfsdk.CreateResourceResponse) { + if !r.p.configured { + resp.Diagnostics.AddError( + "Provider not configured", + "The provider hasn't been configured before apply. This leads to weird stuff happening, so we'd prefer if you didn't do that. Thanks!", + ) + return + } + + var plan Alias + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + resp.Diagnostics.AddError( + "Error getting alias plan", + "Error getting alias plan", + ) + return + } + + car := client.CreateAliasRequest{ + Alias: plan.Alias.Value, + } + out, err := r.p.client.CreateAlias(ctx, car, plan.DeploymentId.Value, plan.TeamID.Value) + if err != nil { + resp.Diagnostics.AddError( + "Error creating alias", + "Could not create alias, unexpected error: "+err.Error(), + ) + return + } + + result := convertResponseToAlias(out, plan) + tflog.Trace(ctx, "created alias", map[string]interface{}{ + "team_id": plan.TeamID.Value, + "deployment_id": plan.DeploymentId.Value, + "alias_uid": result.UID.Value, + }) + + diags = resp.State.Set(ctx, result) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Read will read alias information by requesting it from the Vercel API, and will update terraform +// with this information. +func (r resourceAlias) Read(ctx context.Context, req tfsdk.ReadResourceRequest, resp *tfsdk.ReadResourceResponse) { + var state Alias + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + out, err := r.p.client.GetAlias(ctx, state.UID.Value, state.TeamID.Value) + var apiErr client.APIError + if err != nil && errors.As(err, &apiErr) && apiErr.StatusCode == 404 { + resp.State.RemoveResource(ctx) + return + } + if err != nil { + resp.Diagnostics.AddError( + "Error reading alias", + fmt.Sprintf("Could not get alias %s %s, unexpected error: %s", + state.TeamID.Value, + state.UID.Value, + err, + ), + ) + return + } + + result := convertResponseToAlias(out, state) + tflog.Trace(ctx, "read alias", map[string]interface{}{ + "team_id": result.TeamID.Value, + "alias_uid": result.UID.Value, + }) + + diags = resp.State.Set(ctx, result) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Update updates the Alias state. +func (r resourceAlias) Update(ctx context.Context, req tfsdk.UpdateResourceRequest, resp *tfsdk.UpdateResourceResponse) { + var plan Alias + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + resp.Diagnostics.AddError( + "Error getting alias plan", + "Error getting alias plan", + ) + return + } + + var state Alias + diags = req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + +} + +// Delete deletes an Alias. +func (r resourceAlias) Delete(ctx context.Context, req tfsdk.DeleteResourceRequest, resp *tfsdk.DeleteResourceResponse) { + var state Alias + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + _, err := r.p.client.DeleteAlias(ctx, state.UID.Value, state.TeamID.Value) + if err != nil { + resp.Diagnostics.AddError( + "Error deleting alias", + fmt.Sprintf( + "Could not delete alias %s, unexpected error: %s", + state.Alias.Value, + err, + ), + ) + return + } + tflog.Trace(ctx, "deleted alias") + resp.State.RemoveResource(ctx) +} diff --git a/vercel/resource_alias_model.go b/vercel/resource_alias_model.go new file mode 100644 index 00000000..f4ffb819 --- /dev/null +++ b/vercel/resource_alias_model.go @@ -0,0 +1,26 @@ +package vercel + +import ( + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/vercel/terraform-provider-vercel/client" +) + +// Alias represents the terraform state for an alias resource. +type Alias struct { + Alias types.String `tfsdk:"alias"` + UID types.String `tfsdk:"uid"` + DeploymentId types.String `tfsdk:"deployment_id"` + TeamID types.String `tfsdk:"team_id"` +} + +// convertResponseToAlias 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 plan are used. +func convertResponseToAlias(response client.AliasResponse, plan Alias) Alias { + return Alias{ + Alias: plan.Alias, + UID: types.String{Value: response.UID}, + DeploymentId: plan.DeploymentId, + TeamID: plan.TeamID, + } +} From 1406a8f3b19d2c95e8dd97bbe2dfd9f26827b641 Mon Sep 17 00:00:00 2001 From: Dario Ferrer Date: Mon, 16 May 2022 14:00:02 +0200 Subject: [PATCH 2/5] Reflect pollen custom provider in the docs --- docs/index.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/index.md b/docs/index.md index ffe2fd07..88a2257a 100644 --- a/docs/index.md +++ b/docs/index.md @@ -24,8 +24,8 @@ Use the navigation to the left to read about the available resources. terraform { required_providers { vercel = { - source = "vercel/vercel" - version = "~> 0.1" + source = "streetteam/vercel" + version = "~> 0.4" } } } From ebffec14aea7faa0161fc80df97643fd00cfe32c Mon Sep 17 00:00:00 2001 From: Dario Ferrer Date: Tue, 24 May 2022 13:47:05 +0200 Subject: [PATCH 3/5] Alias: API change, now 204 means not found The Vercel API is responding 204 for not found aliases --- client/alias_create.go | 2 +- client/alias_delete.go | 2 +- client/alias_get.go | 6 ++---- client/request.go | 5 +++++ vercel/resource_alias.go | 2 +- 5 files changed, 10 insertions(+), 7 deletions(-) diff --git a/client/alias_create.go b/client/alias_create.go index 3fd5f6dc..c41342b4 100644 --- a/client/alias_create.go +++ b/client/alias_create.go @@ -16,7 +16,7 @@ type CreateAliasRequest struct { // CreateAlias creates an alias within Vercel. func (c *Client) CreateAlias(ctx context.Context, request CreateAliasRequest, deploymentID string, teamID string) (r AliasResponse, err error) { - url := fmt.Sprintf("%s/now/deployments/%s/aliases", c.baseURL, deploymentID) + url := fmt.Sprintf("%s/v2/deployments/%s/aliases", c.baseURL, deploymentID) if teamID != "" { url = fmt.Sprintf("%s?teamId=%s", url, teamID) } diff --git a/client/alias_delete.go b/client/alias_delete.go index 692d2fdb..dd9c7584 100644 --- a/client/alias_delete.go +++ b/client/alias_delete.go @@ -15,7 +15,7 @@ type DeleteAliasResponse struct { // DeleteAlias deletes an alias within Vercel. func (c *Client) DeleteAlias(ctx context.Context, aliasUID string, teamID string) (r DeleteAliasResponse, err error) { - url := fmt.Sprintf("%s/now/aliases/%s", c.baseURL, aliasUID) + url := fmt.Sprintf("%s/v2/aliases/%s", c.baseURL, aliasUID) req, err := http.NewRequest( "DELETE", url, diff --git a/client/alias_get.go b/client/alias_get.go index 0ed4b0e1..5e6c2d10 100644 --- a/client/alias_get.go +++ b/client/alias_get.go @@ -16,7 +16,7 @@ type AliasResponse struct { // GetAlias retrieves information about an existing alias from vercel. func (c *Client) GetAlias(ctx context.Context, aliasID, teamID string) (r AliasResponse, err error) { - url := fmt.Sprintf("%s/now/aliases/%s", c.baseURL, aliasID) + url := fmt.Sprintf("%s/v3/aliases/%s", c.baseURL, aliasID) if teamID != "" { url = fmt.Sprintf("%s?teamId=%s", url, teamID) } @@ -27,9 +27,8 @@ func (c *Client) GetAlias(ctx context.Context, aliasID, teamID string) (r AliasR nil, ) if err != nil { - return r, err + return r, fmt.Errorf("creating request: %s", err) } - tflog.Trace(ctx, "getting alias", map[string]interface{}{ "url": url, }) @@ -37,6 +36,5 @@ func (c *Client) GetAlias(ctx context.Context, aliasID, teamID string) (r AliasR if err != nil { return r, err } - return r, nil } diff --git a/client/request.go b/client/request.go index 1a13f505..a8aa49c7 100644 --- a/client/request.go +++ b/client/request.go @@ -2,6 +2,7 @@ package client import ( "encoding/json" + "errors" "fmt" "io/ioutil" "net/http" @@ -57,6 +58,10 @@ func (c *Client) doRequest(req *http.Request, v interface{}) error { errorResponse.RawMessage = responseBody return errorResponse } + if resp.StatusCode == 204 { + //204 means "no content", we are treating it as an error + return errors.New("no content") + } if v == nil { return nil diff --git a/vercel/resource_alias.go b/vercel/resource_alias.go index 4d160004..ec315b32 100644 --- a/vercel/resource_alias.go +++ b/vercel/resource_alias.go @@ -119,7 +119,7 @@ func (r resourceAlias) Read(ctx context.Context, req tfsdk.ReadResourceRequest, out, err := r.p.client.GetAlias(ctx, state.UID.Value, state.TeamID.Value) var apiErr client.APIError - if err != nil && errors.As(err, &apiErr) && apiErr.StatusCode == 404 { + if err != nil && (errors.As(err, &apiErr) && apiErr.StatusCode == 404) || (err != nil && err.Error() == "no content") { resp.State.RemoveResource(ctx) return } From a1e36723d02801ed9ab769ef2fa407781b0cd14f Mon Sep 17 00:00:00 2001 From: Raymond Butcher Date: Thu, 26 May 2022 14:42:04 +0100 Subject: [PATCH 4/5] Revert docs change --- docs/index.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/index.md b/docs/index.md index 88a2257a..ffe2fd07 100644 --- a/docs/index.md +++ b/docs/index.md @@ -24,8 +24,8 @@ Use the navigation to the left to read about the available resources. terraform { required_providers { vercel = { - source = "streetteam/vercel" - version = "~> 0.4" + source = "vercel/vercel" + version = "~> 0.1" } } } From 7e170ad3c941dd7254427ffc4b25a7084772cfc4 Mon Sep 17 00:00:00 2001 From: Dario Ferrer Date: Fri, 27 May 2022 17:24:17 +0200 Subject: [PATCH 5/5] Alias datasource: Added alias datasource --- client/alias_get.go | 5 +- client/deployment_create.go | 1 + vercel/data_source_alias.go | 95 ++++++++++++++++++++++++++++++++++ vercel/provider.go | 1 + vercel/resource_alias_model.go | 2 +- 5 files changed, 101 insertions(+), 3 deletions(-) create mode 100644 vercel/data_source_alias.go diff --git a/client/alias_get.go b/client/alias_get.go index 5e6c2d10..69646711 100644 --- a/client/alias_get.go +++ b/client/alias_get.go @@ -10,8 +10,9 @@ import ( // AliasResponse defines the response the Vercel API returns for an alias. type AliasResponse struct { - UID string `json:"uid"` - Alias string `json:"alias"` + UID string `json:"uid"` + Alias string `json:"alias"` + DeploymentId string `json:"deploymentId"` } // GetAlias retrieves information about an existing alias from vercel. diff --git a/client/deployment_create.go b/client/deployment_create.go index 143e913b..e0ebaab6 100644 --- a/client/deployment_create.go +++ b/client/deployment_create.go @@ -29,6 +29,7 @@ type gitSource struct { Owner string `json:"owner,omitempty"` Slug string `json:"slug,omitempty"` Ref string `json:"ref"` + SHA string `json:"sha"` } // CreateDeploymentRequest defines the request the Vercel API expects in order to create a deployment. diff --git a/vercel/data_source_alias.go b/vercel/data_source_alias.go new file mode 100644 index 00000000..f1f9f399 --- /dev/null +++ b/vercel/data_source_alias.go @@ -0,0 +1,95 @@ +package vercel + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" +) + +type dataSourceAliasType struct{} + +// GetSchema returns the schema information for an alias data source +func (r dataSourceAliasType) GetSchema(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { + return tfsdk.Schema{ + Description: ` +Provides information about an existing alias within Vercel. + +An alias allows a deployment to be accessed through a different URL. + `, + Attributes: map[string]tfsdk.Attribute{ + "team_id": { + Optional: true, + Type: types.StringType, + PlanModifiers: tfsdk.AttributePlanModifiers{tfsdk.RequiresReplace()}, + Description: "The team ID the alias exists beneath.", + }, + "alias": { + Required: true, + Type: types.StringType, + Description: "The alias or alias ID to be retrieved.", + }, + "deployment_id": { + Computed: true, + Type: types.StringType, + Description: "The deployment ID.", + }, + "uid": { + Computed: true, + Type: types.StringType, + Description: "The unique identifier of the alias.", + }, + }, + }, nil +} + +// NewDataSource instantiates a new DataSource of this DataSourceType. +func (r dataSourceAliasType) NewDataSource(ctx context.Context, p tfsdk.Provider) (tfsdk.DataSource, diag.Diagnostics) { + return dataSourceAlias{ + p: *(p.(*provider)), + }, nil +} + +type dataSourceAlias struct { + p provider +} + +// Read will read the alias 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 (r dataSourceAlias) Read(ctx context.Context, req tfsdk.ReadDataSourceRequest, resp *tfsdk.ReadDataSourceResponse) { + var config Alias + diags := req.Config.Get(ctx, &config) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + out, err := r.p.client.GetAlias(ctx, config.Alias.Value, config.TeamID.Value) + if err != nil { + resp.Diagnostics.AddError( + "Error reading alias", + fmt.Sprintf("Could not read alias %s %s, unexpected error: %s", + config.TeamID.Value, + config.Alias.Value, + err, + ), + ) + return + } + + result := convertResponseToAlias(out, config) + tflog.Trace(ctx, "read project", map[string]interface{}{ + "team_id": result.TeamID.Value, + "alias": result.Alias.Value, + }) + + 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 db6ce977..d0c18ab9 100644 --- a/vercel/provider.go +++ b/vercel/provider.go @@ -57,6 +57,7 @@ func (p *provider) GetDataSources(_ context.Context) (map[string]tfsdk.DataSourc "vercel_file": dataSourceFileType{}, "vercel_project": dataSourceProjectType{}, "vercel_project_directory": dataSourceProjectDirectoryType{}, + "vercel_alias": dataSourceAliasType{}, }, nil } diff --git a/vercel/resource_alias_model.go b/vercel/resource_alias_model.go index f4ffb819..8fb62d1b 100644 --- a/vercel/resource_alias_model.go +++ b/vercel/resource_alias_model.go @@ -20,7 +20,7 @@ func convertResponseToAlias(response client.AliasResponse, plan Alias) Alias { return Alias{ Alias: plan.Alias, UID: types.String{Value: response.UID}, - DeploymentId: plan.DeploymentId, + DeploymentId: types.String{Value: response.DeploymentId}, TeamID: plan.TeamID, } }