From 29afd69e975318dc17abc8a6188c1ca178a60238 Mon Sep 17 00:00:00 2001 From: Douglas Harcourt Parsons Date: Fri, 19 Apr 2024 16:34:56 +0100 Subject: [PATCH] Move to using fewer files across the codebase This is no code changes, purely moving code around --- client/alias.go | 102 ++++ client/alias_create.go | 51 -- client/alias_delete.go | 32 -- client/alias_get.go | 35 -- .../{deployment_create.go => deployment.go} | 45 ++ client/deployment_delete.go | 33 -- client/deployment_get.go | 28 - client/dns_record.go | 162 ++++++ client/dns_record_create.go | 49 -- client/dns_record_delete.go | 21 - client/dns_record_get.go | 37 -- client/dns_record_list.go | 31 -- client/dns_record_update.go | 48 -- client/environment_variable.go | 168 ++++++ client/environment_variable_create.go | 72 --- client/environment_variable_update.go | 43 -- client/environment_variables_delete.go | 25 - client/environment_variables_get.go | 52 -- client/{file_create.go => file.go} | 0 client/project.go | 325 ++++++++++++ client/project_create.go | 73 --- client/project_delete.go | 26 - client/project_domain.go | 119 +++++ client/project_domain_create.go | 41 -- client/project_domain_delete.go | 26 - client/project_domain_get.go | 39 -- client/project_domain_update.go | 37 -- client/project_get.go | 126 ----- client/project_list.go | 33 -- client/project_update.go | 65 --- client/project_update_production_branch.go | 42 -- client/shared_environment_variable.go | 179 +++++++ client/shared_environment_variable_create.go | 66 --- client/shared_environment_variable_delete.go | 32 -- client/shared_environment_variable_get.go | 27 - client/shared_environment_variable_list.go | 32 -- client/shared_environment_variable_update.go | 54 -- client/{team_create.go => team.go} | 29 ++ client/team_delete.go | 22 - client/team_get.go | 23 - vercel/data_source_project.go | 56 ++ vercel/data_source_project_model.go | 64 --- vercel/resource_alias.go | 21 + vercel/resource_alias_model.go | 26 - vercel/resource_deployment.go | 208 ++++++++ vercel/resource_deployment_model.go | 218 -------- vercel/resource_dns_record.go | 141 ++++++ vercel/resource_dns_record_model.go | 149 ------ vercel/resource_project.go | 468 +++++++++++++++++ vercel/resource_project_domain.go | 41 ++ vercel/resource_project_domain_model.go | 46 -- .../resource_project_environment_variable.go | 89 ++++ ...urce_project_environment_variable_model.go | 95 ---- vercel/resource_project_model.go | 478 ------------------ .../resource_shared_environment_variable.go | 113 +++++ ...ource_shared_environment_variable_model.go | 121 ----- 56 files changed, 2266 insertions(+), 2518 deletions(-) create mode 100644 client/alias.go delete mode 100644 client/alias_create.go delete mode 100644 client/alias_delete.go delete mode 100644 client/alias_get.go rename client/{deployment_create.go => deployment.go} (85%) delete mode 100644 client/deployment_delete.go delete mode 100644 client/deployment_get.go create mode 100644 client/dns_record.go delete mode 100644 client/dns_record_create.go delete mode 100644 client/dns_record_delete.go delete mode 100644 client/dns_record_get.go delete mode 100644 client/dns_record_list.go delete mode 100644 client/dns_record_update.go create mode 100644 client/environment_variable.go delete mode 100644 client/environment_variable_create.go delete mode 100644 client/environment_variable_update.go delete mode 100644 client/environment_variables_delete.go delete mode 100644 client/environment_variables_get.go rename client/{file_create.go => file.go} (100%) create mode 100644 client/project.go delete mode 100644 client/project_create.go delete mode 100644 client/project_delete.go create mode 100644 client/project_domain.go delete mode 100644 client/project_domain_create.go delete mode 100644 client/project_domain_delete.go delete mode 100644 client/project_domain_get.go delete mode 100644 client/project_domain_update.go delete mode 100644 client/project_get.go delete mode 100644 client/project_list.go delete mode 100644 client/project_update.go delete mode 100644 client/project_update_production_branch.go create mode 100644 client/shared_environment_variable.go delete mode 100644 client/shared_environment_variable_create.go delete mode 100644 client/shared_environment_variable_delete.go delete mode 100644 client/shared_environment_variable_get.go delete mode 100644 client/shared_environment_variable_list.go delete mode 100644 client/shared_environment_variable_update.go rename client/{team_create.go => team.go} (52%) delete mode 100644 client/team_delete.go delete mode 100644 client/team_get.go delete mode 100644 vercel/data_source_project_model.go delete mode 100644 vercel/resource_alias_model.go delete mode 100644 vercel/resource_deployment_model.go delete mode 100644 vercel/resource_dns_record_model.go delete mode 100644 vercel/resource_project_domain_model.go delete mode 100644 vercel/resource_project_environment_variable_model.go delete mode 100644 vercel/resource_project_model.go delete mode 100644 vercel/resource_shared_environment_variable_model.go diff --git a/client/alias.go b/client/alias.go new file mode 100644 index 00000000..8b41f5d0 --- /dev/null +++ b/client/alias.go @@ -0,0 +1,102 @@ +package client + +import ( + "context" + "fmt" + + "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"` +} + +// The create Alias endpoint does not return the full AliasResponse, only the UID and Alias. +type createAliasResponse struct { + UID string `json:"uid"` + Alias string `json:"alias"` + TeamID string `json:"-"` +} + +// 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/v2/deployments/%s/aliases", c.baseURL, deploymentID) + if c.teamID(teamID) != "" { + url = fmt.Sprintf("%s?teamId=%s", url, c.teamID(teamID)) + } + payload := string(mustMarshal(request)) + + tflog.Info(ctx, "creating alias", map[string]interface{}{ + "url": url, + "payload": payload, + }) + var aliasResponse createAliasResponse + err = c.doRequest(clientRequest{ + ctx: ctx, + method: "POST", + url: url, + body: payload, + }, &aliasResponse) + if err != nil { + return r, err + } + + return AliasResponse{ + UID: aliasResponse.UID, + Alias: aliasResponse.Alias, + DeploymentID: deploymentID, + TeamID: c.teamID(teamID), + }, nil +} + +// 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/v2/aliases/%s", c.baseURL, aliasUID) + if c.teamID(teamID) != "" { + url = fmt.Sprintf("%s?teamId=%s", url, c.teamID(teamID)) + } + + tflog.Info(ctx, "deleting alias", map[string]interface{}{ + "url": url, + }) + err = c.doRequest(clientRequest{ + ctx: ctx, + method: "DELETE", + url: url, + body: "", + }, &r) + return r, err +} + +// AliasResponse defines the response the Vercel API returns for an alias. +type AliasResponse struct { + UID string `json:"uid"` + Alias string `json:"alias"` + DeploymentID string `json:"deploymentId"` + TeamID string `json:"-"` +} + +// GetAlias retrieves information about an existing alias from vercel. +func (c *Client) GetAlias(ctx context.Context, alias, teamID string) (r AliasResponse, err error) { + url := fmt.Sprintf("%s/v4/aliases/%s", c.baseURL, alias) + if c.teamID(teamID) != "" { + url = fmt.Sprintf("%s?teamId=%s", url, c.teamID(teamID)) + } + tflog.Info(ctx, "getting alias", map[string]interface{}{ + "url": url, + }) + err = c.doRequest(clientRequest{ + ctx: ctx, + method: "GET", + url: url, + body: "", + }, &r) + r.TeamID = c.teamID(teamID) + return r, err +} diff --git a/client/alias_create.go b/client/alias_create.go deleted file mode 100644 index 319cc7da..00000000 --- a/client/alias_create.go +++ /dev/null @@ -1,51 +0,0 @@ -package client - -import ( - "context" - "fmt" - - "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"` -} - -// The create Alias endpoint does not return the full AliasResponse, only the UID and Alias. -type createAliasResponse struct { - UID string `json:"uid"` - Alias string `json:"alias"` - TeamID string `json:"-"` -} - -// 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/v2/deployments/%s/aliases", c.baseURL, deploymentID) - if c.teamID(teamID) != "" { - url = fmt.Sprintf("%s?teamId=%s", url, c.teamID(teamID)) - } - payload := string(mustMarshal(request)) - - tflog.Info(ctx, "creating alias", map[string]interface{}{ - "url": url, - "payload": payload, - }) - var aliasResponse createAliasResponse - err = c.doRequest(clientRequest{ - ctx: ctx, - method: "POST", - url: url, - body: payload, - }, &aliasResponse) - if err != nil { - return r, err - } - - return AliasResponse{ - UID: aliasResponse.UID, - Alias: aliasResponse.Alias, - DeploymentID: deploymentID, - TeamID: c.teamID(teamID), - }, nil -} diff --git a/client/alias_delete.go b/client/alias_delete.go deleted file mode 100644 index d30d2b5c..00000000 --- a/client/alias_delete.go +++ /dev/null @@ -1,32 +0,0 @@ -package client - -import ( - "context" - "fmt" - - "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/v2/aliases/%s", c.baseURL, aliasUID) - if c.teamID(teamID) != "" { - url = fmt.Sprintf("%s?teamId=%s", url, c.teamID(teamID)) - } - - tflog.Info(ctx, "deleting alias", map[string]interface{}{ - "url": url, - }) - err = c.doRequest(clientRequest{ - ctx: ctx, - method: "DELETE", - url: url, - body: "", - }, &r) - return r, err -} diff --git a/client/alias_get.go b/client/alias_get.go deleted file mode 100644 index 77cd99e1..00000000 --- a/client/alias_get.go +++ /dev/null @@ -1,35 +0,0 @@ -package client - -import ( - "context" - "fmt" - - "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"` - DeploymentID string `json:"deploymentId"` - TeamID string `json:"-"` -} - -// GetAlias retrieves information about an existing alias from vercel. -func (c *Client) GetAlias(ctx context.Context, alias, teamID string) (r AliasResponse, err error) { - url := fmt.Sprintf("%s/v4/aliases/%s", c.baseURL, alias) - if c.teamID(teamID) != "" { - url = fmt.Sprintf("%s?teamId=%s", url, c.teamID(teamID)) - } - tflog.Info(ctx, "getting alias", map[string]interface{}{ - "url": url, - }) - err = c.doRequest(clientRequest{ - ctx: ctx, - method: "GET", - url: url, - body: "", - }, &r) - r.TeamID = c.teamID(teamID) - return r, err -} diff --git a/client/deployment_create.go b/client/deployment.go similarity index 85% rename from client/deployment_create.go rename to client/deployment.go index 5909cdea..bcecd34c 100644 --- a/client/deployment_create.go +++ b/client/deployment.go @@ -249,3 +249,48 @@ func (c *Client) CreateDeployment(ctx context.Context, request CreateDeploymentR return r, nil } + +// DeleteDeploymentResponse defines the response the Vercel API returns when a deployment is deleted. +type DeleteDeploymentResponse struct { + State string `json:"state"` + UID string `json:"uid"` +} + +// DeleteDeployment deletes a deployment within Vercel. +func (c *Client) DeleteDeployment(ctx context.Context, deploymentID string, teamID string) (r DeleteDeploymentResponse, err error) { + url := fmt.Sprintf("%s/v13/deployments/%s", c.baseURL, deploymentID) + if c.teamID(teamID) != "" { + url = fmt.Sprintf("%s?teamId=%s", url, c.teamID(teamID)) + } + + tflog.Info(ctx, "deleting deployment", map[string]interface{}{ + "url": url, + }) + err = c.doRequest(clientRequest{ + ctx: ctx, + method: "DELETE", + url: url, + body: "", + }, &r) + return r, err +} + +// GetDeployment retrieves information from Vercel about an existing Deployment. +func (c *Client) GetDeployment(ctx context.Context, deploymentID, teamID string) (r DeploymentResponse, err error) { + url := fmt.Sprintf("%s/v13/deployments/%s", c.baseURL, deploymentID) + if c.teamID(teamID) != "" { + url = fmt.Sprintf("%s?teamId=%s", url, c.teamID(teamID)) + } + + tflog.Info(ctx, "getting deployment", map[string]interface{}{ + "url": url, + }) + err = c.doRequest(clientRequest{ + ctx: ctx, + method: "GET", + url: url, + body: "", + }, &r) + r.TeamID = c.teamID(teamID) + return r, err +} diff --git a/client/deployment_delete.go b/client/deployment_delete.go deleted file mode 100644 index cc8a8690..00000000 --- a/client/deployment_delete.go +++ /dev/null @@ -1,33 +0,0 @@ -package client - -import ( - "context" - "fmt" - - "github.com/hashicorp/terraform-plugin-log/tflog" -) - -// DeleteDeploymentResponse defines the response the Vercel API returns when a deployment is deleted. -type DeleteDeploymentResponse struct { - State string `json:"state"` - UID string `json:"uid"` -} - -// DeleteDeployment deletes a deployment within Vercel. -func (c *Client) DeleteDeployment(ctx context.Context, deploymentID string, teamID string) (r DeleteDeploymentResponse, err error) { - url := fmt.Sprintf("%s/v13/deployments/%s", c.baseURL, deploymentID) - if c.teamID(teamID) != "" { - url = fmt.Sprintf("%s?teamId=%s", url, c.teamID(teamID)) - } - - tflog.Info(ctx, "deleting deployment", map[string]interface{}{ - "url": url, - }) - err = c.doRequest(clientRequest{ - ctx: ctx, - method: "DELETE", - url: url, - body: "", - }, &r) - return r, err -} diff --git a/client/deployment_get.go b/client/deployment_get.go deleted file mode 100644 index 25a3ebb2..00000000 --- a/client/deployment_get.go +++ /dev/null @@ -1,28 +0,0 @@ -package client - -import ( - "context" - "fmt" - - "github.com/hashicorp/terraform-plugin-log/tflog" -) - -// GetDeployment retrieves information from Vercel about an existing Deployment. -func (c *Client) GetDeployment(ctx context.Context, deploymentID, teamID string) (r DeploymentResponse, err error) { - url := fmt.Sprintf("%s/v13/deployments/%s", c.baseURL, deploymentID) - if c.teamID(teamID) != "" { - url = fmt.Sprintf("%s?teamId=%s", url, c.teamID(teamID)) - } - - tflog.Info(ctx, "getting deployment", map[string]interface{}{ - "url": url, - }) - err = c.doRequest(clientRequest{ - ctx: ctx, - method: "GET", - url: url, - body: "", - }, &r) - r.TeamID = c.teamID(teamID) - return r, err -} diff --git a/client/dns_record.go b/client/dns_record.go new file mode 100644 index 00000000..b572d808 --- /dev/null +++ b/client/dns_record.go @@ -0,0 +1,162 @@ +package client + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-log/tflog" +) + +// SRV defines the metata required for creating an SRV type DNS Record. +type SRV struct { + Port int64 `json:"port"` + Priority int64 `json:"priority"` + Target string `json:"target"` + Weight int64 `json:"weight"` +} + +// CreateDNSRecordRequest defines the information necessary to create a DNS record within Vercel. +type CreateDNSRecordRequest struct { + Domain string `json:"-"` + MXPriority int64 `json:"mxPriority,omitempty"` + Name string `json:"name"` + SRV *SRV `json:"srv,omitempty"` + TTL int64 `json:"ttl,omitempty"` + Type string `json:"type"` + Value string `json:"value,omitempty"` + Comment string `json:"comment"` +} + +// CreateDNSRecord creates a DNS record for a specified domain name within Vercel. +func (c *Client) CreateDNSRecord(ctx context.Context, teamID string, request CreateDNSRecordRequest) (r DNSRecord, err error) { + url := fmt.Sprintf("%s/v4/domains/%s/records", c.baseURL, request.Domain) + if c.teamID(teamID) != "" { + url = fmt.Sprintf("%s?teamId=%s", url, c.teamID(teamID)) + } + + var response struct { + RecordID string `json:"uid"` + } + err = c.doRequest(clientRequest{ + ctx: ctx, + method: "POST", + url: url, + body: string(mustMarshal(request)), + }, &response) + if err != nil { + return r, err + } + + return c.GetDNSRecord(ctx, response.RecordID, teamID) +} + +// DeleteDNSRecord removes a DNS domain from Vercel. +func (c *Client) DeleteDNSRecord(ctx context.Context, domain, recordID, teamID string) error { + url := fmt.Sprintf("%s/v2/domains/%s/records/%s", c.baseURL, domain, recordID) + if c.teamID(teamID) != "" { + url = fmt.Sprintf("%s?teamId=%s", url, c.teamID(teamID)) + } + + return c.doRequest(clientRequest{ + ctx: ctx, + method: "DELETE", + url: url, + body: "", + }, nil) +} + +// DNSRecord is the information Vercel surfaces about a DNS record associated with a particular domain. +type DNSRecord struct { + Creator string `json:"creator"` + Domain string `json:"domain"` + ID string `json:"id"` + TeamID string `json:"-"` + Name string `json:"name"` + TTL int64 `json:"ttl"` + Value string `json:"value"` + RecordType string `json:"recordType"` + Priority int64 `json:"priority"` + Comment string `json:"comment"` +} + +// GetDNSRecord retrieves information about a DNS domain from Vercel. +func (c *Client) GetDNSRecord(ctx context.Context, recordID, teamID string) (r DNSRecord, err error) { + url := fmt.Sprintf("%s/domains/records/%s", c.baseURL, recordID) + if c.teamID(teamID) != "" { + url = fmt.Sprintf("%s?teamId=%s", url, c.teamID(teamID)) + } + + err = c.doRequest(clientRequest{ + ctx: ctx, + method: "GET", + url: url, + body: "", + }, &r) + r.TeamID = c.teamID(teamID) + return r, err +} + +// ListDNSRecords is a test helper for listing DNS records that exist for a given domain. +// We limit this to 100, as this is the largest limit allowed by the API. +// This is only used by the sweeper script, so this is safe to do so, but converting +// into a production ready function would require some refactoring. +func (c *Client) ListDNSRecords(ctx context.Context, domain, teamID string) (r []DNSRecord, err error) { + url := fmt.Sprintf("%s/v4/domains/%s/records?limit=100", c.baseURL, domain) + if c.teamID(teamID) != "" { + url = fmt.Sprintf("%s&teamId=%s", url, c.teamID(teamID)) + } + + dr := struct { + Records []DNSRecord `json:"records"` + }{} + err = c.doRequest(clientRequest{ + ctx: ctx, + method: "GET", + url: url, + body: "", + }, &dr) + for _, record := range dr.Records { + record.TeamID = c.teamID(teamID) + } + return dr.Records, err +} + +// SRVUpdate defines the updatable fields within an SRV block of a DNS record. +type SRVUpdate struct { + Port *int64 `json:"port"` + Priority *int64 `json:"priority"` + Target *string `json:"target"` + Weight *int64 `json:"weight"` +} + +// UpdateDNSRecordRequest defines the structure of the request body for updating a DNS record. +type UpdateDNSRecordRequest struct { + MXPriority *int64 `json:"mxPriority,omitempty"` + Name *string `json:"name,omitempty"` + SRV *SRVUpdate `json:"srv,omitempty"` + TTL *int64 `json:"ttl,omitempty"` + Value *string `json:"value,omitempty"` + Comment string `json:"comment"` +} + +// UpdateDNSRecord updates a DNS record for a specified domain name within Vercel. +func (c *Client) UpdateDNSRecord(ctx context.Context, teamID, recordID string, request UpdateDNSRecordRequest) (r DNSRecord, err error) { + url := fmt.Sprintf("%s/v4/domains/records/%s", c.baseURL, recordID) + if c.teamID(teamID) != "" { + url = fmt.Sprintf("%s?teamId=%s", url, c.teamID(teamID)) + } + + payload := string(mustMarshal(request)) + tflog.Info(ctx, "updating DNS record", map[string]interface{}{ + "url": url, + "payload": payload, + }) + err = c.doRequest(clientRequest{ + ctx: ctx, + method: "PATCH", + url: url, + body: payload, + }, &r) + r.TeamID = c.teamID(teamID) + return r, err +} diff --git a/client/dns_record_create.go b/client/dns_record_create.go deleted file mode 100644 index 81650b7f..00000000 --- a/client/dns_record_create.go +++ /dev/null @@ -1,49 +0,0 @@ -package client - -import ( - "context" - "fmt" -) - -// SRV defines the metata required for creating an SRV type DNS Record. -type SRV struct { - Port int64 `json:"port"` - Priority int64 `json:"priority"` - Target string `json:"target"` - Weight int64 `json:"weight"` -} - -// CreateDNSRecordRequest defines the information necessary to create a DNS record within Vercel. -type CreateDNSRecordRequest struct { - Domain string `json:"-"` - MXPriority int64 `json:"mxPriority,omitempty"` - Name string `json:"name"` - SRV *SRV `json:"srv,omitempty"` - TTL int64 `json:"ttl,omitempty"` - Type string `json:"type"` - Value string `json:"value,omitempty"` - Comment string `json:"comment"` -} - -// CreateDNSRecord creates a DNS record for a specified domain name within Vercel. -func (c *Client) CreateDNSRecord(ctx context.Context, teamID string, request CreateDNSRecordRequest) (r DNSRecord, err error) { - url := fmt.Sprintf("%s/v4/domains/%s/records", c.baseURL, request.Domain) - if c.teamID(teamID) != "" { - url = fmt.Sprintf("%s?teamId=%s", url, c.teamID(teamID)) - } - - var response struct { - RecordID string `json:"uid"` - } - err = c.doRequest(clientRequest{ - ctx: ctx, - method: "POST", - url: url, - body: string(mustMarshal(request)), - }, &response) - if err != nil { - return r, err - } - - return c.GetDNSRecord(ctx, response.RecordID, teamID) -} diff --git a/client/dns_record_delete.go b/client/dns_record_delete.go deleted file mode 100644 index 8b1dd24e..00000000 --- a/client/dns_record_delete.go +++ /dev/null @@ -1,21 +0,0 @@ -package client - -import ( - "context" - "fmt" -) - -// DeleteDNSRecord removes a DNS domain from Vercel. -func (c *Client) DeleteDNSRecord(ctx context.Context, domain, recordID, teamID string) error { - url := fmt.Sprintf("%s/v2/domains/%s/records/%s", c.baseURL, domain, recordID) - if c.teamID(teamID) != "" { - url = fmt.Sprintf("%s?teamId=%s", url, c.teamID(teamID)) - } - - return c.doRequest(clientRequest{ - ctx: ctx, - method: "DELETE", - url: url, - body: "", - }, nil) -} diff --git a/client/dns_record_get.go b/client/dns_record_get.go deleted file mode 100644 index ae08b9d4..00000000 --- a/client/dns_record_get.go +++ /dev/null @@ -1,37 +0,0 @@ -package client - -import ( - "context" - "fmt" -) - -// DNSRecord is the information Vercel surfaces about a DNS record associated with a particular domain. -type DNSRecord struct { - Creator string `json:"creator"` - Domain string `json:"domain"` - ID string `json:"id"` - TeamID string `json:"-"` - Name string `json:"name"` - TTL int64 `json:"ttl"` - Value string `json:"value"` - RecordType string `json:"recordType"` - Priority int64 `json:"priority"` - Comment string `json:"comment"` -} - -// GetDNSRecord retrieves information about a DNS domain from Vercel. -func (c *Client) GetDNSRecord(ctx context.Context, recordID, teamID string) (r DNSRecord, err error) { - url := fmt.Sprintf("%s/domains/records/%s", c.baseURL, recordID) - if c.teamID(teamID) != "" { - url = fmt.Sprintf("%s?teamId=%s", url, c.teamID(teamID)) - } - - err = c.doRequest(clientRequest{ - ctx: ctx, - method: "GET", - url: url, - body: "", - }, &r) - r.TeamID = c.teamID(teamID) - return r, err -} diff --git a/client/dns_record_list.go b/client/dns_record_list.go deleted file mode 100644 index ac94d61c..00000000 --- a/client/dns_record_list.go +++ /dev/null @@ -1,31 +0,0 @@ -package client - -import ( - "context" - "fmt" -) - -// ListDNSRecords is a test helper for listing DNS records that exist for a given domain. -// We limit this to 100, as this is the largest limit allowed by the API. -// This is only used by the sweeper script, so this is safe to do so, but converting -// into a production ready function would require some refactoring. -func (c *Client) ListDNSRecords(ctx context.Context, domain, teamID string) (r []DNSRecord, err error) { - url := fmt.Sprintf("%s/v4/domains/%s/records?limit=100", c.baseURL, domain) - if c.teamID(teamID) != "" { - url = fmt.Sprintf("%s&teamId=%s", url, c.teamID(teamID)) - } - - dr := struct { - Records []DNSRecord `json:"records"` - }{} - err = c.doRequest(clientRequest{ - ctx: ctx, - method: "GET", - url: url, - body: "", - }, &dr) - for _, record := range dr.Records { - record.TeamID = c.teamID(teamID) - } - return dr.Records, err -} diff --git a/client/dns_record_update.go b/client/dns_record_update.go deleted file mode 100644 index 83ca6c00..00000000 --- a/client/dns_record_update.go +++ /dev/null @@ -1,48 +0,0 @@ -package client - -import ( - "context" - "fmt" - - "github.com/hashicorp/terraform-plugin-log/tflog" -) - -// SRVUpdate defines the updatable fields within an SRV block of a DNS record. -type SRVUpdate struct { - Port *int64 `json:"port"` - Priority *int64 `json:"priority"` - Target *string `json:"target"` - Weight *int64 `json:"weight"` -} - -// UpdateDNSRecordRequest defines the structure of the request body for updating a DNS record. -type UpdateDNSRecordRequest struct { - MXPriority *int64 `json:"mxPriority,omitempty"` - Name *string `json:"name,omitempty"` - SRV *SRVUpdate `json:"srv,omitempty"` - TTL *int64 `json:"ttl,omitempty"` - Value *string `json:"value,omitempty"` - Comment string `json:"comment"` -} - -// UpdateDNSRecord updates a DNS record for a specified domain name within Vercel. -func (c *Client) UpdateDNSRecord(ctx context.Context, teamID, recordID string, request UpdateDNSRecordRequest) (r DNSRecord, err error) { - url := fmt.Sprintf("%s/v4/domains/records/%s", c.baseURL, recordID) - if c.teamID(teamID) != "" { - url = fmt.Sprintf("%s?teamId=%s", url, c.teamID(teamID)) - } - - payload := string(mustMarshal(request)) - tflog.Info(ctx, "updating DNS record", map[string]interface{}{ - "url": url, - "payload": payload, - }) - err = c.doRequest(clientRequest{ - ctx: ctx, - method: "PATCH", - url: url, - body: payload, - }, &r) - r.TeamID = c.teamID(teamID) - return r, err -} diff --git a/client/environment_variable.go b/client/environment_variable.go new file mode 100644 index 00000000..87cc476a --- /dev/null +++ b/client/environment_variable.go @@ -0,0 +1,168 @@ +package client + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-log/tflog" +) + +// CreateEnvironmentVariableRequest defines the information that needs to be passed to Vercel in order to +// create an environment variable. +type EnvironmentVariableRequest struct { + Key string `json:"key"` + Value string `json:"value"` + Target []string `json:"target"` + GitBranch *string `json:"gitBranch,omitempty"` + Type string `json:"type"` +} + +type CreateEnvironmentVariableRequest struct { + EnvironmentVariable EnvironmentVariableRequest + ProjectID string + TeamID string +} + +// CreateEnvironmentVariable will create a brand new environment variable if one does not exist. +func (c *Client) CreateEnvironmentVariable(ctx context.Context, request CreateEnvironmentVariableRequest) (e EnvironmentVariable, err error) { + url := fmt.Sprintf("%s/v9/projects/%s/env", c.baseURL, request.ProjectID) + if c.teamID(request.TeamID) != "" { + url = fmt.Sprintf("%s?teamId=%s", url, c.teamID(request.TeamID)) + } + payload := string(mustMarshal(request.EnvironmentVariable)) + + tflog.Info(ctx, "creating environment variable", map[string]interface{}{ + "url": url, + "payload": payload, + }) + err = c.doRequest(clientRequest{ + ctx: ctx, + method: "POST", + url: url, + body: payload, + }, &e) + // The API response returns an encrypted environment variable, but we want to return the decrypted version. + e.Value = request.EnvironmentVariable.Value + e.TeamID = c.teamID(request.TeamID) + return e, err +} + +type CreateEnvironmentVariablesRequest struct { + EnvironmentVariables []EnvironmentVariableRequest + ProjectID string + TeamID string +} + +func (c *Client) CreateEnvironmentVariables(ctx context.Context, request CreateEnvironmentVariablesRequest) error { + url := fmt.Sprintf("%s/v10/projects/%s/env", c.baseURL, request.ProjectID) + if c.teamID(request.TeamID) != "" { + url = fmt.Sprintf("%s?teamId=%s", url, c.teamID(request.TeamID)) + } + payload := string(mustMarshal(request.EnvironmentVariables)) + tflog.Info(ctx, "creating environment variables", map[string]interface{}{ + "url": url, + "payload": payload, + }) + return c.doRequest(clientRequest{ + ctx: ctx, + method: "POST", + url: url, + body: payload, + }, nil) +} + +// UpdateEnvironmentVariableRequest defines the information that needs to be passed to Vercel in order to +// update an environment variable. +type UpdateEnvironmentVariableRequest struct { + Value string `json:"value"` + Target []string `json:"target"` + GitBranch *string `json:"gitBranch,omitempty"` + Type string `json:"type"` + ProjectID string `json:"-"` + TeamID string `json:"-"` + EnvID string `json:"-"` +} + +// UpdateEnvironmentVariable will update an existing environment variable to the latest information. +func (c *Client) UpdateEnvironmentVariable(ctx context.Context, request UpdateEnvironmentVariableRequest) (e EnvironmentVariable, err error) { + url := fmt.Sprintf("%s/v9/projects/%s/env/%s", c.baseURL, request.ProjectID, request.EnvID) + if c.teamID(request.TeamID) != "" { + url = fmt.Sprintf("%s?teamId=%s", url, c.teamID(request.TeamID)) + } + payload := string(mustMarshal(request)) + tflog.Info(ctx, "updating environment variable", map[string]interface{}{ + "url": url, + "payload": payload, + }) + err = c.doRequest(clientRequest{ + ctx: ctx, + method: "PATCH", + url: url, + body: payload, + }, &e) + // The API response returns an encrypted environment variable, but we want to return the decrypted version. + e.Value = request.Value + e.TeamID = c.teamID(request.TeamID) + return e, err +} + +// DeleteEnvironmentVariable will remove an environment variable from Vercel. +func (c *Client) DeleteEnvironmentVariable(ctx context.Context, projectID, teamID, variableID string) error { + url := fmt.Sprintf("%s/v8/projects/%s/env/%s", c.baseURL, projectID, variableID) + if c.teamID(teamID) != "" { + url = fmt.Sprintf("%s?teamId=%s", url, c.teamID(teamID)) + } + tflog.Info(ctx, "deleting environment variable", map[string]interface{}{ + "url": url, + }) + return c.doRequest(clientRequest{ + ctx: ctx, + method: "DELETE", + url: url, + body: "", + }, nil) +} + +func (c *Client) getEnvironmentVariables(ctx context.Context, projectID, teamID string) ([]EnvironmentVariable, error) { + url := fmt.Sprintf("%s/v8/projects/%s/env?decrypt=true", c.baseURL, projectID) + if c.teamID(teamID) != "" { + url = fmt.Sprintf("%s&teamId=%s", url, c.teamID(teamID)) + } + + envResponse := struct { + Env []EnvironmentVariable `json:"envs"` + }{} + tflog.Info(ctx, "getting environment variables", map[string]interface{}{ + "url": url, + }) + err := c.doRequest(clientRequest{ + ctx: ctx, + method: "GET", + url: url, + body: "", + }, &envResponse) + for _, env := range envResponse.Env { + env.TeamID = c.teamID(teamID) + } + return envResponse.Env, err +} + +// GetEnvironmentVariable gets a singluar environment variable from Vercel based on its ID. +func (c *Client) GetEnvironmentVariable(ctx context.Context, projectID, teamID, envID string) (e EnvironmentVariable, err error) { + url := fmt.Sprintf("%s/v1/projects/%s/env/%s", c.baseURL, projectID, envID) + if c.teamID(teamID) != "" { + url = fmt.Sprintf("%s?teamId=%s", url, c.teamID(teamID)) + } + + tflog.Info(ctx, "getting environment variable", map[string]interface{}{ + "url": url, + }) + err = c.doRequest(clientRequest{ + ctx: ctx, + method: "GET", + url: url, + body: "", + }, &e) + e.TeamID = c.teamID(teamID) + return e, err +} diff --git a/client/environment_variable_create.go b/client/environment_variable_create.go deleted file mode 100644 index c30f80fc..00000000 --- a/client/environment_variable_create.go +++ /dev/null @@ -1,72 +0,0 @@ -package client - -import ( - "context" - "fmt" - - "github.com/hashicorp/terraform-plugin-log/tflog" -) - -// CreateEnvironmentVariableRequest defines the information that needs to be passed to Vercel in order to -// create an environment variable. -type EnvironmentVariableRequest struct { - Key string `json:"key"` - Value string `json:"value"` - Target []string `json:"target"` - GitBranch *string `json:"gitBranch,omitempty"` - Type string `json:"type"` -} - -type CreateEnvironmentVariableRequest struct { - EnvironmentVariable EnvironmentVariableRequest - ProjectID string - TeamID string -} - -// CreateEnvironmentVariable will create a brand new environment variable if one does not exist. -func (c *Client) CreateEnvironmentVariable(ctx context.Context, request CreateEnvironmentVariableRequest) (e EnvironmentVariable, err error) { - url := fmt.Sprintf("%s/v9/projects/%s/env", c.baseURL, request.ProjectID) - if c.teamID(request.TeamID) != "" { - url = fmt.Sprintf("%s?teamId=%s", url, c.teamID(request.TeamID)) - } - payload := string(mustMarshal(request.EnvironmentVariable)) - - tflog.Info(ctx, "creating environment variable", map[string]interface{}{ - "url": url, - "payload": payload, - }) - err = c.doRequest(clientRequest{ - ctx: ctx, - method: "POST", - url: url, - body: payload, - }, &e) - // The API response returns an encrypted environment variable, but we want to return the decrypted version. - e.Value = request.EnvironmentVariable.Value - e.TeamID = c.teamID(request.TeamID) - return e, err -} - -type CreateEnvironmentVariablesRequest struct { - EnvironmentVariables []EnvironmentVariableRequest - ProjectID string - TeamID string -} - -func (c *Client) CreateEnvironmentVariables(ctx context.Context, request CreateEnvironmentVariablesRequest) error { - url := fmt.Sprintf("%s/v10/projects/%s/env", c.baseURL, request.ProjectID) - if c.teamID(request.TeamID) != "" { - url = fmt.Sprintf("%s?teamId=%s", url, c.teamID(request.TeamID)) - } - payload := string(mustMarshal(request.EnvironmentVariables)) - tflog.Info(ctx, "creating environment variables", map[string]interface{}{ - "url": url, - "payload": payload, - }) - return c.doRequest(clientRequest{ - ctx: ctx, - method: "POST", - url: url, - body: payload, - }, nil) -} diff --git a/client/environment_variable_update.go b/client/environment_variable_update.go deleted file mode 100644 index bce4908a..00000000 --- a/client/environment_variable_update.go +++ /dev/null @@ -1,43 +0,0 @@ -package client - -import ( - "context" - "fmt" - - "github.com/hashicorp/terraform-plugin-log/tflog" -) - -// UpdateEnvironmentVariableRequest defines the information that needs to be passed to Vercel in order to -// update an environment variable. -type UpdateEnvironmentVariableRequest struct { - Value string `json:"value"` - Target []string `json:"target"` - GitBranch *string `json:"gitBranch,omitempty"` - Type string `json:"type"` - ProjectID string `json:"-"` - TeamID string `json:"-"` - EnvID string `json:"-"` -} - -// UpdateEnvironmentVariable will update an existing environment variable to the latest information. -func (c *Client) UpdateEnvironmentVariable(ctx context.Context, request UpdateEnvironmentVariableRequest) (e EnvironmentVariable, err error) { - url := fmt.Sprintf("%s/v9/projects/%s/env/%s", c.baseURL, request.ProjectID, request.EnvID) - if c.teamID(request.TeamID) != "" { - url = fmt.Sprintf("%s?teamId=%s", url, c.teamID(request.TeamID)) - } - payload := string(mustMarshal(request)) - tflog.Info(ctx, "updating environment variable", map[string]interface{}{ - "url": url, - "payload": payload, - }) - err = c.doRequest(clientRequest{ - ctx: ctx, - method: "PATCH", - url: url, - body: payload, - }, &e) - // The API response returns an encrypted environment variable, but we want to return the decrypted version. - e.Value = request.Value - e.TeamID = c.teamID(request.TeamID) - return e, err -} diff --git a/client/environment_variables_delete.go b/client/environment_variables_delete.go deleted file mode 100644 index fe059dbf..00000000 --- a/client/environment_variables_delete.go +++ /dev/null @@ -1,25 +0,0 @@ -package client - -import ( - "context" - "fmt" - - "github.com/hashicorp/terraform-plugin-log/tflog" -) - -// DeleteEnvironmentVariable will remove an environment variable from Vercel. -func (c *Client) DeleteEnvironmentVariable(ctx context.Context, projectID, teamID, variableID string) error { - url := fmt.Sprintf("%s/v8/projects/%s/env/%s", c.baseURL, projectID, variableID) - if c.teamID(teamID) != "" { - url = fmt.Sprintf("%s?teamId=%s", url, c.teamID(teamID)) - } - tflog.Info(ctx, "deleting environment variable", map[string]interface{}{ - "url": url, - }) - return c.doRequest(clientRequest{ - ctx: ctx, - method: "DELETE", - url: url, - body: "", - }, nil) -} diff --git a/client/environment_variables_get.go b/client/environment_variables_get.go deleted file mode 100644 index 6d5c857b..00000000 --- a/client/environment_variables_get.go +++ /dev/null @@ -1,52 +0,0 @@ -package client - -import ( - "context" - "fmt" - - "github.com/hashicorp/terraform-plugin-log/tflog" -) - -func (c *Client) getEnvironmentVariables(ctx context.Context, projectID, teamID string) ([]EnvironmentVariable, error) { - url := fmt.Sprintf("%s/v8/projects/%s/env?decrypt=true", c.baseURL, projectID) - if c.teamID(teamID) != "" { - url = fmt.Sprintf("%s&teamId=%s", url, c.teamID(teamID)) - } - - envResponse := struct { - Env []EnvironmentVariable `json:"envs"` - }{} - tflog.Info(ctx, "getting environment variables", map[string]interface{}{ - "url": url, - }) - err := c.doRequest(clientRequest{ - ctx: ctx, - method: "GET", - url: url, - body: "", - }, &envResponse) - for _, env := range envResponse.Env { - env.TeamID = c.teamID(teamID) - } - return envResponse.Env, err -} - -// GetEnvironmentVariable gets a singluar environment variable from Vercel based on its ID. -func (c *Client) GetEnvironmentVariable(ctx context.Context, projectID, teamID, envID string) (e EnvironmentVariable, err error) { - url := fmt.Sprintf("%s/v1/projects/%s/env/%s", c.baseURL, projectID, envID) - if c.teamID(teamID) != "" { - url = fmt.Sprintf("%s?teamId=%s", url, c.teamID(teamID)) - } - - tflog.Info(ctx, "getting environment variable", map[string]interface{}{ - "url": url, - }) - err = c.doRequest(clientRequest{ - ctx: ctx, - method: "GET", - url: url, - body: "", - }, &e) - e.TeamID = c.teamID(teamID) - return e, err -} diff --git a/client/file_create.go b/client/file.go similarity index 100% rename from client/file_create.go rename to client/file.go diff --git a/client/project.go b/client/project.go new file mode 100644 index 00000000..1dd4e591 --- /dev/null +++ b/client/project.go @@ -0,0 +1,325 @@ +package client + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/terraform-plugin-log/tflog" +) + +// GitRepository is the information Vercel requires and surfaces about which git provider and repository +// a project is linked with. +type GitRepository struct { + Type string `json:"type"` + Repo string `json:"repo"` +} + +// EnvironmentVariable defines the information Vercel requires and surfaces about an environment variable +// that is associated with a project. +type EnvironmentVariable struct { + Key string `json:"key"` + Value string `json:"value"` + Target []string `json:"target"` + GitBranch *string `json:"gitBranch,omitempty"` + Type string `json:"type"` + ID string `json:"id,omitempty"` + TeamID string `json:"-"` +} + +// CreateProjectRequest defines the information necessary to create a project. +type CreateProjectRequest struct { + BuildCommand *string `json:"buildCommand"` + CommandForIgnoringBuildStep *string `json:"commandForIgnoringBuildStep,omitempty"` + DevCommand *string `json:"devCommand"` + EnvironmentVariables []EnvironmentVariable `json:"environmentVariables"` + Framework *string `json:"framework"` + GitRepository *GitRepository `json:"gitRepository,omitempty"` + InstallCommand *string `json:"installCommand"` + Name string `json:"name"` + OutputDirectory *string `json:"outputDirectory"` + PublicSource *bool `json:"publicSource"` + RootDirectory *string `json:"rootDirectory"` + ServerlessFunctionRegion *string `json:"serverlessFunctionRegion,omitempty"` +} + +// CreateProject will create a project within Vercel. +func (c *Client) CreateProject(ctx context.Context, teamID string, request CreateProjectRequest) (r ProjectResponse, err error) { + url := fmt.Sprintf("%s/v8/projects", c.baseURL) + if c.teamID(teamID) != "" { + url = fmt.Sprintf("%s?teamId=%s", url, c.teamID(teamID)) + } + + payload := string(mustMarshal(request)) + tflog.Info(ctx, "creating project", map[string]interface{}{ + "url": url, + "payload": payload, + }) + err = c.doRequest(clientRequest{ + ctx: ctx, + method: "POST", + url: url, + body: payload, + }, &r) + if err != nil { + return r, err + } + env, err := c.getEnvironmentVariables(ctx, r.ID, teamID) + if err != nil { + return r, fmt.Errorf("error getting environment variables: %w", err) + } + r.EnvironmentVariables = env + r.TeamID = c.teamID(teamID) + return r, err +} + +// DeleteProject deletes a project within Vercel. Note that there is no need to explicitly +// remove every environment variable, as these cease to exist when a project is removed. +func (c *Client) DeleteProject(ctx context.Context, projectID, teamID string) error { + url := fmt.Sprintf("%s/v8/projects/%s", c.baseURL, projectID) + if c.teamID(teamID) != "" { + url = fmt.Sprintf("%s?teamId=%s", url, c.teamID(teamID)) + } + tflog.Info(ctx, "deleting project", map[string]interface{}{ + "url": url, + }) + return c.doRequest(clientRequest{ + ctx: ctx, + method: "DELETE", + url: url, + body: "", + }, nil) +} + +// Repository defines the information about a projects git connection. +type Repository struct { + Type string + Repo string + ProductionBranch *string +} + +// getRepoNameFromURL is a helper method to extract the repo name from a GitLab URL. +// This is necessary as GitLab doesn't return the repository slug in the API response, +// Because this information isn't present, the only way to obtain it is to parse the URL. +func getRepoNameFromURL(url string) string { + url = strings.TrimSuffix(url, ".git") + urlParts := strings.Split(url, "/") + + return urlParts[len(urlParts)-1] +} + +// Repository is a helper method to convert the ProjectResponse Repository information into a more +// digestible format. +func (r *ProjectResponse) Repository() *Repository { + if r.Link == nil { + return nil + } + switch r.Link.Type { + case "github": + return &Repository{ + Type: "github", + Repo: fmt.Sprintf("%s/%s", r.Link.Org, r.Link.Repo), + ProductionBranch: r.Link.ProductionBranch, + } + case "gitlab": + return &Repository{ + Type: "gitlab", + Repo: fmt.Sprintf("%s/%s", r.Link.ProjectNamespace, getRepoNameFromURL(r.Link.ProjectURL)), + ProductionBranch: r.Link.ProductionBranch, + } + case "bitbucket": + return &Repository{ + Type: "bitbucket", + Repo: fmt.Sprintf("%s/%s", r.Link.Owner, r.Link.Slug), + ProductionBranch: r.Link.ProductionBranch, + } + } + return nil +} + +// ProjectResponse defines the information Vercel returns about a project. +type ProjectResponse struct { + BuildCommand *string `json:"buildCommand"` + CommandForIgnoringBuildStep *string `json:"commandForIgnoringBuildStep"` + DevCommand *string `json:"devCommand"` + EnvironmentVariables []EnvironmentVariable `json:"env"` + Framework *string `json:"framework"` + ID string `json:"id"` + TeamID string `json:"-"` + InstallCommand *string `json:"installCommand"` + Link *struct { + Type string `json:"type"` + // github + Org string `json:"org"` + Repo string `json:"repo"` + // bitbucket + Owner string `json:"owner"` + Slug string `json:"slug"` + // gitlab + ProjectNamespace string `json:"projectNamespace"` + ProjectURL string `json:"projectUrl"` + ProjectID int64 `json:"projectId,string"` + // production branch + ProductionBranch *string `json:"productionBranch"` + } `json:"link"` + Name string `json:"name"` + OutputDirectory *string `json:"outputDirectory"` + PublicSource *bool `json:"publicSource"` + RootDirectory *string `json:"rootDirectory"` + ServerlessFunctionRegion *string `json:"serverlessFunctionRegion"` + VercelAuthentication *VercelAuthentication `json:"ssoProtection"` + PasswordProtection *PasswordProtection `json:"passwordProtection"` + TrustedIps *TrustedIps `json:"trustedIps"` + ProtectionBypass map[string]ProtectionBypass `json:"protectionBypass"` + AutoExposeSystemEnvVars *bool `json:"autoExposeSystemEnvs"` +} + +// GetProject retrieves information about an existing project from Vercel. +func (c *Client) GetProject(ctx context.Context, projectID, teamID string, shouldFetchEnvironmentVariables bool) (r ProjectResponse, err error) { + url := fmt.Sprintf("%s/v10/projects/%s", c.baseURL, projectID) + if c.teamID(teamID) != "" { + url = fmt.Sprintf("%s?teamId=%s", url, c.teamID(teamID)) + } + tflog.Info(ctx, "getting project", map[string]interface{}{ + "url": url, + "shouldFetchEnvironment": shouldFetchEnvironmentVariables, + }) + err = c.doRequest(clientRequest{ + ctx: ctx, + method: "GET", + url: url, + body: "", + }, &r) + if err != nil { + return r, fmt.Errorf("unable to get project: %w", err) + } + + if shouldFetchEnvironmentVariables { + r.EnvironmentVariables, err = c.getEnvironmentVariables(ctx, projectID, teamID) + if err != nil { + return r, fmt.Errorf("error getting environment variables for project: %w", err) + } + } else { + // The get project endpoint returns environment variables, but returns them fully + // encrypted. This isn't useful, so we just remove them. + r.EnvironmentVariables = nil + } + r.TeamID = c.teamID(teamID) + return r, err +} + +// ListProjects lists the top 100 projects (no pagination) from within Vercel. +func (c *Client) ListProjects(ctx context.Context, teamID string) (r []ProjectResponse, err error) { + url := fmt.Sprintf("%s/v8/projects?limit=100", c.baseURL) + if c.teamID(teamID) != "" { + url = fmt.Sprintf("%s&teamId=%s", url, c.teamID(teamID)) + } + + pr := struct { + Projects []ProjectResponse `json:"projects"` + }{} + tflog.Info(ctx, "listing projects", map[string]interface{}{ + "url": url, + }) + err = c.doRequest(clientRequest{ + ctx: ctx, + method: "GET", + url: url, + body: "", + }, &pr) + for _, p := range pr.Projects { + p.TeamID = c.teamID(teamID) + } + return pr.Projects, err +} + +// UpdateProjectRequest defines the possible fields that can be updated within a vercel project. +// note that the values are all pointers, with many containing `omitempty` for serialisation. +// This is because the Vercel API behaves in the following manner: +// - a provided field will be updated +// - setting the field to an empty value (e.g. "") will remove the setting for that field. +// - omitting the value entirely from the request will _not_ update the field. +type UpdateProjectRequest struct { + BuildCommand *string `json:"buildCommand"` + CommandForIgnoringBuildStep *string `json:"commandForIgnoringBuildStep"` + DevCommand *string `json:"devCommand"` + Framework *string `json:"framework"` + InstallCommand *string `json:"installCommand"` + Name *string `json:"name,omitempty"` + OutputDirectory *string `json:"outputDirectory"` + PublicSource *bool `json:"publicSource"` + RootDirectory *string `json:"rootDirectory"` + ServerlessFunctionRegion *string `json:"serverlessFunctionRegion"` + VercelAuthentication *VercelAuthentication `json:"ssoProtection"` + PasswordProtection *PasswordProtectionWithPassword `json:"passwordProtection"` + TrustedIps *TrustedIps `json:"trustedIps"` + AutoExposeSystemEnvVars *bool `json:"autoExposeSystemEnvs,omitempty"` +} + +// UpdateProject updates an existing projects configuration within Vercel. +func (c *Client) UpdateProject(ctx context.Context, projectID, teamID string, request UpdateProjectRequest, shouldFetchEnvironmentVariables bool) (r ProjectResponse, err error) { + url := fmt.Sprintf("%s/v9/projects/%s", c.baseURL, projectID) + if c.teamID(teamID) != "" { + url = fmt.Sprintf("%s?teamId=%s", url, c.teamID(teamID)) + } + payload := string(mustMarshal(request)) + tflog.Info(ctx, "updating project", map[string]interface{}{ + "url": url, + "payload": payload, + "shouldFetchEnvironmentVariables": shouldFetchEnvironmentVariables, + }) + err = c.doRequest(clientRequest{ + ctx: ctx, + method: "PATCH", + url: url, + body: payload, + }, &r) + if err != nil { + return r, err + } + if shouldFetchEnvironmentVariables { + r.EnvironmentVariables, err = c.getEnvironmentVariables(ctx, r.ID, teamID) + if err != nil { + return r, fmt.Errorf("error getting environment variables for project: %w", err) + } + } else { + r.EnvironmentVariables = nil + } + + r.TeamID = c.teamID(teamID) + return r, err +} + +type UpdateProductionBranchRequest struct { + TeamID string `json:"-"` + ProjectID string `json:"-"` + Branch string `json:"branch"` +} + +func (c *Client) UpdateProductionBranch(ctx context.Context, request UpdateProductionBranchRequest) (r ProjectResponse, err error) { + url := fmt.Sprintf("%s/v9/projects/%s/branch", c.baseURL, request.ProjectID) + if c.teamID(request.TeamID) != "" { + url = fmt.Sprintf("%s?teamId=%s", url, c.teamID(request.TeamID)) + } + payload := string(mustMarshal(request)) + tflog.Info(ctx, "updating project production branch", map[string]interface{}{ + "url": url, + "payload": payload, + }) + err = c.doRequest(clientRequest{ + ctx: ctx, + method: "PATCH", + url: url, + body: payload, + }, &r) + if err != nil { + return r, err + } + env, err := c.getEnvironmentVariables(ctx, r.ID, request.TeamID) + if err != nil { + return r, fmt.Errorf("error getting environment variables: %w", err) + } + r.EnvironmentVariables = env + r.TeamID = c.teamID(c.teamID(request.TeamID)) + return r, err +} diff --git a/client/project_create.go b/client/project_create.go deleted file mode 100644 index b0c118cd..00000000 --- a/client/project_create.go +++ /dev/null @@ -1,73 +0,0 @@ -package client - -import ( - "context" - "fmt" - - "github.com/hashicorp/terraform-plugin-log/tflog" -) - -// GitRepository is the information Vercel requires and surfaces about which git provider and repository -// a project is linked with. -type GitRepository struct { - Type string `json:"type"` - Repo string `json:"repo"` -} - -// EnvironmentVariable defines the information Vercel requires and surfaces about an environment variable -// that is associated with a project. -type EnvironmentVariable struct { - Key string `json:"key"` - Value string `json:"value"` - Target []string `json:"target"` - GitBranch *string `json:"gitBranch,omitempty"` - Type string `json:"type"` - ID string `json:"id,omitempty"` - TeamID string `json:"-"` -} - -// CreateProjectRequest defines the information necessary to create a project. -type CreateProjectRequest struct { - BuildCommand *string `json:"buildCommand"` - CommandForIgnoringBuildStep *string `json:"commandForIgnoringBuildStep,omitempty"` - DevCommand *string `json:"devCommand"` - EnvironmentVariables []EnvironmentVariable `json:"environmentVariables"` - Framework *string `json:"framework"` - GitRepository *GitRepository `json:"gitRepository,omitempty"` - InstallCommand *string `json:"installCommand"` - Name string `json:"name"` - OutputDirectory *string `json:"outputDirectory"` - PublicSource *bool `json:"publicSource"` - RootDirectory *string `json:"rootDirectory"` - ServerlessFunctionRegion *string `json:"serverlessFunctionRegion,omitempty"` -} - -// CreateProject will create a project within Vercel. -func (c *Client) CreateProject(ctx context.Context, teamID string, request CreateProjectRequest) (r ProjectResponse, err error) { - url := fmt.Sprintf("%s/v8/projects", c.baseURL) - if c.teamID(teamID) != "" { - url = fmt.Sprintf("%s?teamId=%s", url, c.teamID(teamID)) - } - - payload := string(mustMarshal(request)) - tflog.Info(ctx, "creating project", map[string]interface{}{ - "url": url, - "payload": payload, - }) - err = c.doRequest(clientRequest{ - ctx: ctx, - method: "POST", - url: url, - body: payload, - }, &r) - if err != nil { - return r, err - } - env, err := c.getEnvironmentVariables(ctx, r.ID, teamID) - if err != nil { - return r, fmt.Errorf("error getting environment variables: %w", err) - } - r.EnvironmentVariables = env - r.TeamID = c.teamID(teamID) - return r, err -} diff --git a/client/project_delete.go b/client/project_delete.go deleted file mode 100644 index a05ee668..00000000 --- a/client/project_delete.go +++ /dev/null @@ -1,26 +0,0 @@ -package client - -import ( - "context" - "fmt" - - "github.com/hashicorp/terraform-plugin-log/tflog" -) - -// DeleteProject deletes a project within Vercel. Note that there is no need to explicitly -// remove every environment variable, as these cease to exist when a project is removed. -func (c *Client) DeleteProject(ctx context.Context, projectID, teamID string) error { - url := fmt.Sprintf("%s/v8/projects/%s", c.baseURL, projectID) - if c.teamID(teamID) != "" { - url = fmt.Sprintf("%s?teamId=%s", url, c.teamID(teamID)) - } - tflog.Info(ctx, "deleting project", map[string]interface{}{ - "url": url, - }) - return c.doRequest(clientRequest{ - ctx: ctx, - method: "DELETE", - url: url, - body: "", - }, nil) -} diff --git a/client/project_domain.go b/client/project_domain.go new file mode 100644 index 00000000..e6e8df97 --- /dev/null +++ b/client/project_domain.go @@ -0,0 +1,119 @@ +package client + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-log/tflog" +) + +// CreateProjectDomainRequest defines the information necessary to create a project domain. +// A project domain is an association of a specific domain name to a project. These are typically +// used to assign a domain name to any production deployments, but can also be used to configure +// redirects, or to give specific git branches a domain name. +type CreateProjectDomainRequest struct { + Name string `json:"name"` + GitBranch string `json:"gitBranch,omitempty"` + Redirect string `json:"redirect,omitempty"` + RedirectStatusCode int64 `json:"redirectStatusCode,omitempty"` +} + +// CreateProjectDomain creates a project domain within Vercel. +func (c *Client) CreateProjectDomain(ctx context.Context, projectID, teamID string, request CreateProjectDomainRequest) (r ProjectDomainResponse, err error) { + url := fmt.Sprintf("%s/v10/projects/%s/domains", c.baseURL, projectID) + if c.teamID(teamID) != "" { + url = fmt.Sprintf("%s?teamId=%s", url, c.teamID(teamID)) + } + + payload := string(mustMarshal(request)) + tflog.Info(ctx, "creating project domain", map[string]interface{}{ + "url": url, + "payload": payload, + }) + err = c.doRequest(clientRequest{ + ctx: ctx, + method: "POST", + url: url, + body: payload, + }, &r) + r.TeamID = c.teamID(teamID) + return r, err +} + +// DeleteProjectDomain removes any association of a domain name with a Vercel project. +func (c *Client) DeleteProjectDomain(ctx context.Context, projectID, domain, teamID string) error { + url := fmt.Sprintf("%s/v8/projects/%s/domains/%s", c.baseURL, projectID, domain) + if c.teamID(teamID) != "" { + url = fmt.Sprintf("%s?teamId=%s", url, c.teamID(teamID)) + } + + tflog.Info(ctx, "deleting project domain", map[string]interface{}{ + "url": url, + }) + return c.doRequest(clientRequest{ + ctx: ctx, + method: "DELETE", + url: url, + body: "", + }, nil) +} + +// ProjectDomainResponse defines the information that Vercel exposes about a domain that is +// associated with a vercel project. +type ProjectDomainResponse struct { + Name string `json:"name"` + ProjectID string `json:"projectId"` + TeamID string `json:"-"` + Redirect *string `json:"redirect"` + RedirectStatusCode *int64 `json:"redirectStatusCode"` + GitBranch *string `json:"gitBranch"` +} + +// GetProjectDomain retrieves information about a project domain from Vercel. +func (c *Client) GetProjectDomain(ctx context.Context, projectID, domain, teamID string) (r ProjectDomainResponse, err error) { + url := fmt.Sprintf("%s/v8/projects/%s/domains/%s", c.baseURL, projectID, domain) + if c.teamID(teamID) != "" { + url = fmt.Sprintf("%s?teamId=%s", url, c.teamID(teamID)) + } + + tflog.Info(ctx, "getting project domain", map[string]interface{}{ + "url": url, + }) + err = c.doRequest(clientRequest{ + ctx: ctx, + method: "GET", + url: url, + body: "", + }, &r) + r.TeamID = c.teamID(teamID) + return r, err +} + +// UpdateProjectDomainRequest defines the information necessary to update a project domain. +type UpdateProjectDomainRequest struct { + GitBranch *string `json:"gitBranch"` + Redirect *string `json:"redirect"` + RedirectStatusCode *int64 `json:"redirectStatusCode"` +} + +// UpdateProjectDomain updates an existing project domain within Vercel. +func (c *Client) UpdateProjectDomain(ctx context.Context, projectID, domain, teamID string, request UpdateProjectDomainRequest) (r ProjectDomainResponse, err error) { + url := fmt.Sprintf("%s/v8/projects/%s/domains/%s", c.baseURL, projectID, domain) + if c.teamID(teamID) != "" { + url = fmt.Sprintf("%s?teamId=%s", url, c.teamID(teamID)) + } + + payload := string(mustMarshal(request)) + tflog.Info(ctx, "updating project domain", map[string]interface{}{ + "url": url, + "payload": payload, + }) + err = c.doRequest(clientRequest{ + ctx: ctx, + method: "PATCH", + url: url, + body: payload, + }, &r) + r.TeamID = c.teamID(teamID) + return r, err +} diff --git a/client/project_domain_create.go b/client/project_domain_create.go deleted file mode 100644 index 5c584798..00000000 --- a/client/project_domain_create.go +++ /dev/null @@ -1,41 +0,0 @@ -package client - -import ( - "context" - "fmt" - - "github.com/hashicorp/terraform-plugin-log/tflog" -) - -// CreateProjectDomainRequest defines the information necessary to create a project domain. -// A project domain is an association of a specific domain name to a project. These are typically -// used to assign a domain name to any production deployments, but can also be used to configure -// redirects, or to give specific git branches a domain name. -type CreateProjectDomainRequest struct { - Name string `json:"name"` - GitBranch string `json:"gitBranch,omitempty"` - Redirect string `json:"redirect,omitempty"` - RedirectStatusCode int64 `json:"redirectStatusCode,omitempty"` -} - -// CreateProjectDomain creates a project domain within Vercel. -func (c *Client) CreateProjectDomain(ctx context.Context, projectID, teamID string, request CreateProjectDomainRequest) (r ProjectDomainResponse, err error) { - url := fmt.Sprintf("%s/v10/projects/%s/domains", c.baseURL, projectID) - if c.teamID(teamID) != "" { - url = fmt.Sprintf("%s?teamId=%s", url, c.teamID(teamID)) - } - - payload := string(mustMarshal(request)) - tflog.Info(ctx, "creating project domain", map[string]interface{}{ - "url": url, - "payload": payload, - }) - err = c.doRequest(clientRequest{ - ctx: ctx, - method: "POST", - url: url, - body: payload, - }, &r) - r.TeamID = c.teamID(teamID) - return r, err -} diff --git a/client/project_domain_delete.go b/client/project_domain_delete.go deleted file mode 100644 index 4f86f247..00000000 --- a/client/project_domain_delete.go +++ /dev/null @@ -1,26 +0,0 @@ -package client - -import ( - "context" - "fmt" - - "github.com/hashicorp/terraform-plugin-log/tflog" -) - -// DeleteProjectDomain removes any association of a domain name with a Vercel project. -func (c *Client) DeleteProjectDomain(ctx context.Context, projectID, domain, teamID string) error { - url := fmt.Sprintf("%s/v8/projects/%s/domains/%s", c.baseURL, projectID, domain) - if c.teamID(teamID) != "" { - url = fmt.Sprintf("%s?teamId=%s", url, c.teamID(teamID)) - } - - tflog.Info(ctx, "deleting project domain", map[string]interface{}{ - "url": url, - }) - return c.doRequest(clientRequest{ - ctx: ctx, - method: "DELETE", - url: url, - body: "", - }, nil) -} diff --git a/client/project_domain_get.go b/client/project_domain_get.go deleted file mode 100644 index 4c3be136..00000000 --- a/client/project_domain_get.go +++ /dev/null @@ -1,39 +0,0 @@ -package client - -import ( - "context" - "fmt" - - "github.com/hashicorp/terraform-plugin-log/tflog" -) - -// ProjectDomainResponse defines the information that Vercel exposes about a domain that is -// associated with a vercel project. -type ProjectDomainResponse struct { - Name string `json:"name"` - ProjectID string `json:"projectId"` - TeamID string `json:"-"` - Redirect *string `json:"redirect"` - RedirectStatusCode *int64 `json:"redirectStatusCode"` - GitBranch *string `json:"gitBranch"` -} - -// GetProjectDomain retrieves information about a project domain from Vercel. -func (c *Client) GetProjectDomain(ctx context.Context, projectID, domain, teamID string) (r ProjectDomainResponse, err error) { - url := fmt.Sprintf("%s/v8/projects/%s/domains/%s", c.baseURL, projectID, domain) - if c.teamID(teamID) != "" { - url = fmt.Sprintf("%s?teamId=%s", url, c.teamID(teamID)) - } - - tflog.Info(ctx, "getting project domain", map[string]interface{}{ - "url": url, - }) - err = c.doRequest(clientRequest{ - ctx: ctx, - method: "GET", - url: url, - body: "", - }, &r) - r.TeamID = c.teamID(teamID) - return r, err -} diff --git a/client/project_domain_update.go b/client/project_domain_update.go deleted file mode 100644 index c7022e7f..00000000 --- a/client/project_domain_update.go +++ /dev/null @@ -1,37 +0,0 @@ -package client - -import ( - "context" - "fmt" - - "github.com/hashicorp/terraform-plugin-log/tflog" -) - -// UpdateProjectDomainRequest defines the information necessary to update a project domain. -type UpdateProjectDomainRequest struct { - GitBranch *string `json:"gitBranch"` - Redirect *string `json:"redirect"` - RedirectStatusCode *int64 `json:"redirectStatusCode"` -} - -// UpdateProjectDomain updates an existing project domain within Vercel. -func (c *Client) UpdateProjectDomain(ctx context.Context, projectID, domain, teamID string, request UpdateProjectDomainRequest) (r ProjectDomainResponse, err error) { - url := fmt.Sprintf("%s/v8/projects/%s/domains/%s", c.baseURL, projectID, domain) - if c.teamID(teamID) != "" { - url = fmt.Sprintf("%s?teamId=%s", url, c.teamID(teamID)) - } - - payload := string(mustMarshal(request)) - tflog.Info(ctx, "updating project domain", map[string]interface{}{ - "url": url, - "payload": payload, - }) - err = c.doRequest(clientRequest{ - ctx: ctx, - method: "PATCH", - url: url, - body: payload, - }, &r) - r.TeamID = c.teamID(teamID) - return r, err -} diff --git a/client/project_get.go b/client/project_get.go deleted file mode 100644 index d50e2b22..00000000 --- a/client/project_get.go +++ /dev/null @@ -1,126 +0,0 @@ -package client - -import ( - "context" - "fmt" - "strings" - - "github.com/hashicorp/terraform-plugin-log/tflog" -) - -// Repository defines the information about a projects git connection. -type Repository struct { - Type string - Repo string - ProductionBranch *string -} - -// getRepoNameFromURL is a helper method to extract the repo name from a GitLab URL. -// This is necessary as GitLab doesn't return the repository slug in the API response, -// Because this information isn't present, the only way to obtain it is to parse the URL. -func getRepoNameFromURL(url string) string { - url = strings.TrimSuffix(url, ".git") - urlParts := strings.Split(url, "/") - - return urlParts[len(urlParts)-1] -} - -// Repository is a helper method to convert the ProjectResponse Repository information into a more -// digestible format. -func (r *ProjectResponse) Repository() *Repository { - if r.Link == nil { - return nil - } - switch r.Link.Type { - case "github": - return &Repository{ - Type: "github", - Repo: fmt.Sprintf("%s/%s", r.Link.Org, r.Link.Repo), - ProductionBranch: r.Link.ProductionBranch, - } - case "gitlab": - return &Repository{ - Type: "gitlab", - Repo: fmt.Sprintf("%s/%s", r.Link.ProjectNamespace, getRepoNameFromURL(r.Link.ProjectURL)), - ProductionBranch: r.Link.ProductionBranch, - } - case "bitbucket": - return &Repository{ - Type: "bitbucket", - Repo: fmt.Sprintf("%s/%s", r.Link.Owner, r.Link.Slug), - ProductionBranch: r.Link.ProductionBranch, - } - } - return nil -} - -// ProjectResponse defines the information Vercel returns about a project. -type ProjectResponse struct { - BuildCommand *string `json:"buildCommand"` - CommandForIgnoringBuildStep *string `json:"commandForIgnoringBuildStep"` - DevCommand *string `json:"devCommand"` - EnvironmentVariables []EnvironmentVariable `json:"env"` - Framework *string `json:"framework"` - ID string `json:"id"` - TeamID string `json:"-"` - InstallCommand *string `json:"installCommand"` - Link *struct { - Type string `json:"type"` - // github - Org string `json:"org"` - Repo string `json:"repo"` - // bitbucket - Owner string `json:"owner"` - Slug string `json:"slug"` - // gitlab - ProjectNamespace string `json:"projectNamespace"` - ProjectURL string `json:"projectUrl"` - ProjectID int64 `json:"projectId,string"` - // production branch - ProductionBranch *string `json:"productionBranch"` - } `json:"link"` - Name string `json:"name"` - OutputDirectory *string `json:"outputDirectory"` - PublicSource *bool `json:"publicSource"` - RootDirectory *string `json:"rootDirectory"` - ServerlessFunctionRegion *string `json:"serverlessFunctionRegion"` - VercelAuthentication *VercelAuthentication `json:"ssoProtection"` - PasswordProtection *PasswordProtection `json:"passwordProtection"` - TrustedIps *TrustedIps `json:"trustedIps"` - ProtectionBypass map[string]ProtectionBypass `json:"protectionBypass"` - AutoExposeSystemEnvVars *bool `json:"autoExposeSystemEnvs"` -} - -// GetProject retrieves information about an existing project from Vercel. -func (c *Client) GetProject(ctx context.Context, projectID, teamID string, shouldFetchEnvironmentVariables bool) (r ProjectResponse, err error) { - url := fmt.Sprintf("%s/v10/projects/%s", c.baseURL, projectID) - if c.teamID(teamID) != "" { - url = fmt.Sprintf("%s?teamId=%s", url, c.teamID(teamID)) - } - tflog.Info(ctx, "getting project", map[string]interface{}{ - "url": url, - "shouldFetchEnvironment": shouldFetchEnvironmentVariables, - }) - err = c.doRequest(clientRequest{ - ctx: ctx, - method: "GET", - url: url, - body: "", - }, &r) - if err != nil { - return r, fmt.Errorf("unable to get project: %w", err) - } - - if shouldFetchEnvironmentVariables { - r.EnvironmentVariables, err = c.getEnvironmentVariables(ctx, projectID, teamID) - if err != nil { - return r, fmt.Errorf("error getting environment variables for project: %w", err) - } - } else { - // The get project endpoint returns environment variables, but returns them fully - // encrypted. This isn't useful, so we just remove them. - r.EnvironmentVariables = nil - } - r.TeamID = c.teamID(teamID) - return r, err -} diff --git a/client/project_list.go b/client/project_list.go deleted file mode 100644 index 2adfc036..00000000 --- a/client/project_list.go +++ /dev/null @@ -1,33 +0,0 @@ -package client - -import ( - "context" - "fmt" - - "github.com/hashicorp/terraform-plugin-log/tflog" -) - -// ListProjects lists the top 100 projects (no pagination) from within Vercel. -func (c *Client) ListProjects(ctx context.Context, teamID string) (r []ProjectResponse, err error) { - url := fmt.Sprintf("%s/v8/projects?limit=100", c.baseURL) - if c.teamID(teamID) != "" { - url = fmt.Sprintf("%s&teamId=%s", url, c.teamID(teamID)) - } - - pr := struct { - Projects []ProjectResponse `json:"projects"` - }{} - tflog.Info(ctx, "listing projects", map[string]interface{}{ - "url": url, - }) - err = c.doRequest(clientRequest{ - ctx: ctx, - method: "GET", - url: url, - body: "", - }, &pr) - for _, p := range pr.Projects { - p.TeamID = c.teamID(teamID) - } - return pr.Projects, err -} diff --git a/client/project_update.go b/client/project_update.go deleted file mode 100644 index 4e45d95c..00000000 --- a/client/project_update.go +++ /dev/null @@ -1,65 +0,0 @@ -package client - -import ( - "context" - "fmt" - - "github.com/hashicorp/terraform-plugin-log/tflog" -) - -// UpdateProjectRequest defines the possible fields that can be updated within a vercel project. -// note that the values are all pointers, with many containing `omitempty` for serialisation. -// This is because the Vercel API behaves in the following manner: -// - a provided field will be updated -// - setting the field to an empty value (e.g. "") will remove the setting for that field. -// - omitting the value entirely from the request will _not_ update the field. -type UpdateProjectRequest struct { - BuildCommand *string `json:"buildCommand"` - CommandForIgnoringBuildStep *string `json:"commandForIgnoringBuildStep"` - DevCommand *string `json:"devCommand"` - Framework *string `json:"framework"` - InstallCommand *string `json:"installCommand"` - Name *string `json:"name,omitempty"` - OutputDirectory *string `json:"outputDirectory"` - PublicSource *bool `json:"publicSource"` - RootDirectory *string `json:"rootDirectory"` - ServerlessFunctionRegion *string `json:"serverlessFunctionRegion"` - VercelAuthentication *VercelAuthentication `json:"ssoProtection"` - PasswordProtection *PasswordProtectionWithPassword `json:"passwordProtection"` - TrustedIps *TrustedIps `json:"trustedIps"` - AutoExposeSystemEnvVars *bool `json:"autoExposeSystemEnvs,omitempty"` -} - -// UpdateProject updates an existing projects configuration within Vercel. -func (c *Client) UpdateProject(ctx context.Context, projectID, teamID string, request UpdateProjectRequest, shouldFetchEnvironmentVariables bool) (r ProjectResponse, err error) { - url := fmt.Sprintf("%s/v9/projects/%s", c.baseURL, projectID) - if c.teamID(teamID) != "" { - url = fmt.Sprintf("%s?teamId=%s", url, c.teamID(teamID)) - } - payload := string(mustMarshal(request)) - tflog.Info(ctx, "updating project", map[string]interface{}{ - "url": url, - "payload": payload, - "shouldFetchEnvironmentVariables": shouldFetchEnvironmentVariables, - }) - err = c.doRequest(clientRequest{ - ctx: ctx, - method: "PATCH", - url: url, - body: payload, - }, &r) - if err != nil { - return r, err - } - if shouldFetchEnvironmentVariables { - r.EnvironmentVariables, err = c.getEnvironmentVariables(ctx, r.ID, teamID) - if err != nil { - return r, fmt.Errorf("error getting environment variables for project: %w", err) - } - } else { - r.EnvironmentVariables = nil - } - - r.TeamID = c.teamID(teamID) - return r, err -} diff --git a/client/project_update_production_branch.go b/client/project_update_production_branch.go deleted file mode 100644 index a009e765..00000000 --- a/client/project_update_production_branch.go +++ /dev/null @@ -1,42 +0,0 @@ -package client - -import ( - "context" - "fmt" - - "github.com/hashicorp/terraform-plugin-log/tflog" -) - -type UpdateProductionBranchRequest struct { - TeamID string `json:"-"` - ProjectID string `json:"-"` - Branch string `json:"branch"` -} - -func (c *Client) UpdateProductionBranch(ctx context.Context, request UpdateProductionBranchRequest) (r ProjectResponse, err error) { - url := fmt.Sprintf("%s/v9/projects/%s/branch", c.baseURL, request.ProjectID) - if c.teamID(request.TeamID) != "" { - url = fmt.Sprintf("%s?teamId=%s", url, c.teamID(request.TeamID)) - } - payload := string(mustMarshal(request)) - tflog.Info(ctx, "updating project production branch", map[string]interface{}{ - "url": url, - "payload": payload, - }) - err = c.doRequest(clientRequest{ - ctx: ctx, - method: "PATCH", - url: url, - body: payload, - }, &r) - if err != nil { - return r, err - } - env, err := c.getEnvironmentVariables(ctx, r.ID, request.TeamID) - if err != nil { - return r, fmt.Errorf("error getting environment variables: %w", err) - } - r.EnvironmentVariables = env - r.TeamID = c.teamID(c.teamID(request.TeamID)) - return r, err -} diff --git a/client/shared_environment_variable.go b/client/shared_environment_variable.go new file mode 100644 index 00000000..069b9a6c --- /dev/null +++ b/client/shared_environment_variable.go @@ -0,0 +1,179 @@ +package client + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-log/tflog" +) + +type SharedEnvironmentVariableResponse struct { + Key string `json:"key"` + TeamID string `json:"ownerId"` + ID string `json:"id,omitempty"` + Value string `json:"value"` + Type string `json:"type"` + Target []string `json:"target"` + ProjectIDs []string `json:"projectId"` +} + +type SharedEnvVarRequest struct { + Key string `json:"key"` + Value string `json:"value"` +} + +type SharedEnvironmentVariableRequest struct { + Type string `json:"type"` + ProjectIDs []string `json:"projectId"` + Target []string `json:"target"` + EnvironmentVariables []SharedEnvVarRequest `json:"evs"` +} + +type CreateSharedEnvironmentVariableRequest struct { + EnvironmentVariable SharedEnvironmentVariableRequest + TeamID string +} + +// CreateSharedEnvironmentVariable will create a brand new shared environment variable if one does not exist. +func (c *Client) CreateSharedEnvironmentVariable(ctx context.Context, request CreateSharedEnvironmentVariableRequest) (e SharedEnvironmentVariableResponse, err error) { + url := fmt.Sprintf("%s/v1/env", c.baseURL) + if c.teamID(request.TeamID) != "" { + url = fmt.Sprintf("%s?teamId=%s", url, c.teamID(request.TeamID)) + } + payload := string(mustMarshal(request.EnvironmentVariable)) + tflog.Info(ctx, "creating shared environment variable", map[string]interface{}{ + "url": url, + "payload": payload, + }) + var response struct { + Created []SharedEnvironmentVariableResponse `json:"created"` + } + err = c.doRequest(clientRequest{ + ctx: ctx, + method: "POST", + url: url, + body: payload, + }, &response) + if err != nil { + return e, err + } + if len(response.Created) != 1 { + return e, fmt.Errorf("expected 1 environment variable to be created, got %d", len(response.Created)) + } + // Override the value, as it returns the encrypted value. + response.Created[0].Value = request.EnvironmentVariable.EnvironmentVariables[0].Value + return response.Created[0], err +} + +// DeleteSharedEnvironmentVariable will remove a shared environment variable from Vercel. +func (c *Client) DeleteSharedEnvironmentVariable(ctx context.Context, teamID, variableID string) error { + url := fmt.Sprintf("%s/v1/env", c.baseURL) + if c.teamID(teamID) != "" { + url = fmt.Sprintf("%s?teamId=%s", url, c.teamID(teamID)) + } + payload := string(mustMarshal(struct { + IDs []string `json:"ids"` + }{ + IDs: []string{ + variableID, + }, + })) + tflog.Info(ctx, "deleting shared environment variable", map[string]interface{}{ + "url": url, + }) + return c.doRequest(clientRequest{ + ctx: ctx, + method: "DELETE", + url: url, + body: payload, + }, nil) +} + +func (c *Client) GetSharedEnvironmentVariable(ctx context.Context, teamID, envID string) (e SharedEnvironmentVariableResponse, err error) { + url := fmt.Sprintf("%s/v1/env/%s", c.baseURL, envID) + if c.teamID(teamID) != "" { + url = fmt.Sprintf("%s?teamId=%s", url, c.teamID(teamID)) + } + + tflog.Info(ctx, "getting shared environment variable", map[string]interface{}{ + "url": url, + }) + err = c.doRequest(clientRequest{ + ctx: ctx, + method: "GET", + url: url, + body: "", + }, &e) + e.TeamID = c.teamID(teamID) + return e, err +} + +func (c *Client) ListSharedEnvironmentVariables(ctx context.Context, teamID string) ([]SharedEnvironmentVariableResponse, error) { + url := fmt.Sprintf("%s/v1/env/all", c.baseURL) + if c.teamID(teamID) != "" { + url = fmt.Sprintf("%s?teamId=%s", url, c.teamID(teamID)) + } + + tflog.Info(ctx, "listing shared environment variables", map[string]interface{}{ + "url": url, + }) + res := struct { + Data []SharedEnvironmentVariableResponse `json:"data"` + }{} + err := c.doRequest(clientRequest{ + ctx: ctx, + method: "GET", + url: url, + body: "", + }, &res) + for _, v := range res.Data { + v.TeamID = c.teamID(teamID) + } + return res.Data, err +} + +type UpdateSharedEnvironmentVariableRequest struct { + Value string `json:"value"` + Type string `json:"type"` + ProjectIDs []string `json:"projectId"` + Target []string `json:"target"` + TeamID string `json:"-"` + EnvID string `json:"-"` +} + +func (c *Client) UpdateSharedEnvironmentVariable(ctx context.Context, request UpdateSharedEnvironmentVariableRequest) (e SharedEnvironmentVariableResponse, err error) { + url := fmt.Sprintf("%s/v1/env", c.baseURL) + if c.teamID(request.TeamID) != "" { + url = fmt.Sprintf("%s?teamId=%s", url, c.teamID(request.TeamID)) + } + payload := string(mustMarshal(struct { + Updates map[string]UpdateSharedEnvironmentVariableRequest `json:"updates"` + }{ + Updates: map[string]UpdateSharedEnvironmentVariableRequest{ + request.EnvID: request, + }, + })) + + tflog.Info(ctx, "updating shared environment variable", map[string]interface{}{ + "url": url, + "payload": payload, + }) + var response struct { + Updated []SharedEnvironmentVariableResponse `json:"updated"` + } + err = c.doRequest(clientRequest{ + ctx: ctx, + method: "PATCH", + url: url, + body: payload, + }, &response) + if err != nil { + return e, err + } + if len(response.Updated) != 1 { + return e, fmt.Errorf("expected 1 environment variable to be created, got %d", len(response.Updated)) + } + // Override the value, as it returns the encrypted value. + response.Updated[0].Value = request.Value + return response.Updated[0], err +} diff --git a/client/shared_environment_variable_create.go b/client/shared_environment_variable_create.go deleted file mode 100644 index 2b678788..00000000 --- a/client/shared_environment_variable_create.go +++ /dev/null @@ -1,66 +0,0 @@ -package client - -import ( - "context" - "fmt" - - "github.com/hashicorp/terraform-plugin-log/tflog" -) - -type SharedEnvironmentVariableResponse struct { - Key string `json:"key"` - TeamID string `json:"ownerId"` - ID string `json:"id,omitempty"` - Value string `json:"value"` - Type string `json:"type"` - Target []string `json:"target"` - ProjectIDs []string `json:"projectId"` -} - -type SharedEnvVarRequest struct { - Key string `json:"key"` - Value string `json:"value"` -} - -type SharedEnvironmentVariableRequest struct { - Type string `json:"type"` - ProjectIDs []string `json:"projectId"` - Target []string `json:"target"` - EnvironmentVariables []SharedEnvVarRequest `json:"evs"` -} - -type CreateSharedEnvironmentVariableRequest struct { - EnvironmentVariable SharedEnvironmentVariableRequest - TeamID string -} - -// CreateSharedEnvironmentVariable will create a brand new shared environment variable if one does not exist. -func (c *Client) CreateSharedEnvironmentVariable(ctx context.Context, request CreateSharedEnvironmentVariableRequest) (e SharedEnvironmentVariableResponse, err error) { - url := fmt.Sprintf("%s/v1/env", c.baseURL) - if c.teamID(request.TeamID) != "" { - url = fmt.Sprintf("%s?teamId=%s", url, c.teamID(request.TeamID)) - } - payload := string(mustMarshal(request.EnvironmentVariable)) - tflog.Info(ctx, "creating shared environment variable", map[string]interface{}{ - "url": url, - "payload": payload, - }) - var response struct { - Created []SharedEnvironmentVariableResponse `json:"created"` - } - err = c.doRequest(clientRequest{ - ctx: ctx, - method: "POST", - url: url, - body: payload, - }, &response) - if err != nil { - return e, err - } - if len(response.Created) != 1 { - return e, fmt.Errorf("expected 1 environment variable to be created, got %d", len(response.Created)) - } - // Override the value, as it returns the encrypted value. - response.Created[0].Value = request.EnvironmentVariable.EnvironmentVariables[0].Value - return response.Created[0], err -} diff --git a/client/shared_environment_variable_delete.go b/client/shared_environment_variable_delete.go deleted file mode 100644 index 671dc485..00000000 --- a/client/shared_environment_variable_delete.go +++ /dev/null @@ -1,32 +0,0 @@ -package client - -import ( - "context" - "fmt" - - "github.com/hashicorp/terraform-plugin-log/tflog" -) - -// DeleteSharedEnvironmentVariable will remove a shared environment variable from Vercel. -func (c *Client) DeleteSharedEnvironmentVariable(ctx context.Context, teamID, variableID string) error { - url := fmt.Sprintf("%s/v1/env", c.baseURL) - if c.teamID(teamID) != "" { - url = fmt.Sprintf("%s?teamId=%s", url, c.teamID(teamID)) - } - payload := string(mustMarshal(struct { - IDs []string `json:"ids"` - }{ - IDs: []string{ - variableID, - }, - })) - tflog.Info(ctx, "deleting shared environment variable", map[string]interface{}{ - "url": url, - }) - return c.doRequest(clientRequest{ - ctx: ctx, - method: "DELETE", - url: url, - body: payload, - }, nil) -} diff --git a/client/shared_environment_variable_get.go b/client/shared_environment_variable_get.go deleted file mode 100644 index 6b7ceaaa..00000000 --- a/client/shared_environment_variable_get.go +++ /dev/null @@ -1,27 +0,0 @@ -package client - -import ( - "context" - "fmt" - - "github.com/hashicorp/terraform-plugin-log/tflog" -) - -func (c *Client) GetSharedEnvironmentVariable(ctx context.Context, teamID, envID string) (e SharedEnvironmentVariableResponse, err error) { - url := fmt.Sprintf("%s/v1/env/%s", c.baseURL, envID) - if c.teamID(teamID) != "" { - url = fmt.Sprintf("%s?teamId=%s", url, c.teamID(teamID)) - } - - tflog.Info(ctx, "getting shared environment variable", map[string]interface{}{ - "url": url, - }) - err = c.doRequest(clientRequest{ - ctx: ctx, - method: "GET", - url: url, - body: "", - }, &e) - e.TeamID = c.teamID(teamID) - return e, err -} diff --git a/client/shared_environment_variable_list.go b/client/shared_environment_variable_list.go deleted file mode 100644 index 3c99c12f..00000000 --- a/client/shared_environment_variable_list.go +++ /dev/null @@ -1,32 +0,0 @@ -package client - -import ( - "context" - "fmt" - - "github.com/hashicorp/terraform-plugin-log/tflog" -) - -func (c *Client) ListSharedEnvironmentVariables(ctx context.Context, teamID string) ([]SharedEnvironmentVariableResponse, error) { - url := fmt.Sprintf("%s/v1/env/all", c.baseURL) - if c.teamID(teamID) != "" { - url = fmt.Sprintf("%s?teamId=%s", url, c.teamID(teamID)) - } - - tflog.Info(ctx, "listing shared environment variables", map[string]interface{}{ - "url": url, - }) - res := struct { - Data []SharedEnvironmentVariableResponse `json:"data"` - }{} - err := c.doRequest(clientRequest{ - ctx: ctx, - method: "GET", - url: url, - body: "", - }, &res) - for _, v := range res.Data { - v.TeamID = c.teamID(teamID) - } - return res.Data, err -} diff --git a/client/shared_environment_variable_update.go b/client/shared_environment_variable_update.go deleted file mode 100644 index e1f2313a..00000000 --- a/client/shared_environment_variable_update.go +++ /dev/null @@ -1,54 +0,0 @@ -package client - -import ( - "context" - "fmt" - - "github.com/hashicorp/terraform-plugin-log/tflog" -) - -type UpdateSharedEnvironmentVariableRequest struct { - Value string `json:"value"` - Type string `json:"type"` - ProjectIDs []string `json:"projectId"` - Target []string `json:"target"` - TeamID string `json:"-"` - EnvID string `json:"-"` -} - -func (c *Client) UpdateSharedEnvironmentVariable(ctx context.Context, request UpdateSharedEnvironmentVariableRequest) (e SharedEnvironmentVariableResponse, err error) { - url := fmt.Sprintf("%s/v1/env", c.baseURL) - if c.teamID(request.TeamID) != "" { - url = fmt.Sprintf("%s?teamId=%s", url, c.teamID(request.TeamID)) - } - payload := string(mustMarshal(struct { - Updates map[string]UpdateSharedEnvironmentVariableRequest `json:"updates"` - }{ - Updates: map[string]UpdateSharedEnvironmentVariableRequest{ - request.EnvID: request, - }, - })) - - tflog.Info(ctx, "updating shared environment variable", map[string]interface{}{ - "url": url, - "payload": payload, - }) - var response struct { - Updated []SharedEnvironmentVariableResponse `json:"updated"` - } - err = c.doRequest(clientRequest{ - ctx: ctx, - method: "PATCH", - url: url, - body: payload, - }, &response) - if err != nil { - return e, err - } - if len(response.Updated) != 1 { - return e, fmt.Errorf("expected 1 environment variable to be created, got %d", len(response.Updated)) - } - // Override the value, as it returns the encrypted value. - response.Updated[0].Value = request.Value - return response.Updated[0], err -} diff --git a/client/team_create.go b/client/team.go similarity index 52% rename from client/team_create.go rename to client/team.go index b9a63768..9ec61326 100644 --- a/client/team_create.go +++ b/client/team.go @@ -35,3 +35,32 @@ func (c *Client) CreateTeam(ctx context.Context, request TeamCreateRequest) (r T }, &r) return r, err } + +// DeleteTeam deletes an existing team within vercel. +func (c *Client) DeleteTeam(ctx context.Context, teamID string) error { + url := fmt.Sprintf("%s/v1/teams/%s", c.baseURL, teamID) + tflog.Info(ctx, "deleting team", map[string]interface{}{ + "url": url, + }) + return c.doRequest(clientRequest{ + ctx: ctx, + method: "DELETE", + url: url, + body: "", + }, nil) +} + +// GetTeam returns information about an existing team within vercel. +func (c *Client) GetTeam(ctx context.Context, idOrSlug string) (r TeamResponse, err error) { + url := fmt.Sprintf("%s/v2/teams/%s", c.baseURL, idOrSlug) + tflog.Info(ctx, "getting team", map[string]interface{}{ + "url": url, + }) + err = c.doRequest(clientRequest{ + ctx: ctx, + method: "GET", + url: url, + body: "", + }, &r) + return r, err +} diff --git a/client/team_delete.go b/client/team_delete.go deleted file mode 100644 index f7a1f9b5..00000000 --- a/client/team_delete.go +++ /dev/null @@ -1,22 +0,0 @@ -package client - -import ( - "context" - "fmt" - - "github.com/hashicorp/terraform-plugin-log/tflog" -) - -// DeleteTeam deletes an existing team within vercel. -func (c *Client) DeleteTeam(ctx context.Context, teamID string) error { - url := fmt.Sprintf("%s/v1/teams/%s", c.baseURL, teamID) - tflog.Info(ctx, "deleting team", map[string]interface{}{ - "url": url, - }) - return c.doRequest(clientRequest{ - ctx: ctx, - method: "DELETE", - url: url, - body: "", - }, nil) -} diff --git a/client/team_get.go b/client/team_get.go deleted file mode 100644 index d169303e..00000000 --- a/client/team_get.go +++ /dev/null @@ -1,23 +0,0 @@ -package client - -import ( - "context" - "fmt" - - "github.com/hashicorp/terraform-plugin-log/tflog" -) - -// GetTeam returns information about an existing team within vercel. -func (c *Client) GetTeam(ctx context.Context, idOrSlug string) (r TeamResponse, err error) { - url := fmt.Sprintf("%s/v2/teams/%s", c.baseURL, idOrSlug) - tflog.Info(ctx, "getting team", map[string]interface{}{ - "url": url, - }) - err = c.doRequest(clientRequest{ - ctx: ctx, - method: "GET", - url: url, - body: "", - }, &r) - return r, err -} diff --git a/vercel/data_source_project.go b/vercel/data_source_project.go index 0b7f69e6..9bee75cc 100644 --- a/vercel/data_source_project.go +++ b/vercel/data_source_project.go @@ -221,6 +221,62 @@ For more detailed information, please see the [Vercel documentation](https://ver } } +// Project reflects the state terraform stores internally for a project. +type ProjectDataSource struct { + BuildCommand types.String `tfsdk:"build_command"` + DevCommand types.String `tfsdk:"dev_command"` + Environment types.Set `tfsdk:"environment"` + Framework types.String `tfsdk:"framework"` + GitRepository *GitRepository `tfsdk:"git_repository"` + ID types.String `tfsdk:"id"` + IgnoreCommand types.String `tfsdk:"ignore_command"` + InstallCommand types.String `tfsdk:"install_command"` + Name types.String `tfsdk:"name"` + OutputDirectory types.String `tfsdk:"output_directory"` + PublicSource types.Bool `tfsdk:"public_source"` + RootDirectory types.String `tfsdk:"root_directory"` + ServerlessFunctionRegion types.String `tfsdk:"serverless_function_region"` + TeamID types.String `tfsdk:"team_id"` + VercelAuthentication *VercelAuthentication `tfsdk:"vercel_authentication"` + PasswordProtection *PasswordProtection `tfsdk:"password_protection"` + TrustedIps *TrustedIps `tfsdk:"trusted_ips"` + AutoExposeSystemEnvVars types.Bool `tfsdk:"automatically_expose_system_environment_variables"` +} + +func convertResponseToProjectDataSource(ctx context.Context, response client.ProjectResponse, plan Project) (ProjectDataSource, error) { + project, err := convertResponseToProject(ctx, response, plan) + if err != nil { + return ProjectDataSource{}, err + } + + var pp *PasswordProtection + if project.PasswordProtection != nil { + pp = &PasswordProtection{ + DeploymentType: project.PasswordProtection.DeploymentType, + } + } + return ProjectDataSource{ + BuildCommand: project.BuildCommand, + DevCommand: project.DevCommand, + Environment: project.Environment, + Framework: project.Framework, + GitRepository: project.GitRepository, + ID: project.ID, + IgnoreCommand: project.IgnoreCommand, + InstallCommand: project.InstallCommand, + Name: project.Name, + OutputDirectory: project.OutputDirectory, + PublicSource: project.PublicSource, + RootDirectory: project.RootDirectory, + ServerlessFunctionRegion: project.ServerlessFunctionRegion, + TeamID: project.TeamID, + VercelAuthentication: project.VercelAuthentication, + PasswordProtection: pp, + TrustedIps: project.TrustedIps, + AutoExposeSystemEnvVars: fromBoolPointer(response.AutoExposeSystemEnvVars), + }, nil +} + // Read will read project 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. diff --git a/vercel/data_source_project_model.go b/vercel/data_source_project_model.go deleted file mode 100644 index d4741b12..00000000 --- a/vercel/data_source_project_model.go +++ /dev/null @@ -1,64 +0,0 @@ -package vercel - -import ( - "context" - - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/vercel/terraform-provider-vercel/client" -) - -// Project reflects the state terraform stores internally for a project. -type ProjectDataSource struct { - BuildCommand types.String `tfsdk:"build_command"` - DevCommand types.String `tfsdk:"dev_command"` - Environment types.Set `tfsdk:"environment"` - Framework types.String `tfsdk:"framework"` - GitRepository *GitRepository `tfsdk:"git_repository"` - ID types.String `tfsdk:"id"` - IgnoreCommand types.String `tfsdk:"ignore_command"` - InstallCommand types.String `tfsdk:"install_command"` - Name types.String `tfsdk:"name"` - OutputDirectory types.String `tfsdk:"output_directory"` - PublicSource types.Bool `tfsdk:"public_source"` - RootDirectory types.String `tfsdk:"root_directory"` - ServerlessFunctionRegion types.String `tfsdk:"serverless_function_region"` - TeamID types.String `tfsdk:"team_id"` - VercelAuthentication *VercelAuthentication `tfsdk:"vercel_authentication"` - PasswordProtection *PasswordProtection `tfsdk:"password_protection"` - TrustedIps *TrustedIps `tfsdk:"trusted_ips"` - AutoExposeSystemEnvVars types.Bool `tfsdk:"automatically_expose_system_environment_variables"` -} - -func convertResponseToProjectDataSource(ctx context.Context, response client.ProjectResponse, plan Project) (ProjectDataSource, error) { - project, err := convertResponseToProject(ctx, response, plan) - if err != nil { - return ProjectDataSource{}, err - } - - var pp *PasswordProtection - if project.PasswordProtection != nil { - pp = &PasswordProtection{ - DeploymentType: project.PasswordProtection.DeploymentType, - } - } - return ProjectDataSource{ - BuildCommand: project.BuildCommand, - DevCommand: project.DevCommand, - Environment: project.Environment, - Framework: project.Framework, - GitRepository: project.GitRepository, - ID: project.ID, - IgnoreCommand: project.IgnoreCommand, - InstallCommand: project.InstallCommand, - Name: project.Name, - OutputDirectory: project.OutputDirectory, - PublicSource: project.PublicSource, - RootDirectory: project.RootDirectory, - ServerlessFunctionRegion: project.ServerlessFunctionRegion, - TeamID: project.TeamID, - VercelAuthentication: project.VercelAuthentication, - PasswordProtection: pp, - TrustedIps: project.TrustedIps, - AutoExposeSystemEnvVars: fromBoolPointer(response.AutoExposeSystemEnvVars), - }, nil -} diff --git a/vercel/resource_alias.go b/vercel/resource_alias.go index 79e9d18b..041cbb7f 100644 --- a/vercel/resource_alias.go +++ b/vercel/resource_alias.go @@ -8,6 +8,7 @@ import ( "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/client" ) @@ -79,6 +80,26 @@ An Alias allows a ` + "`vercel_deployment` to be accessed through a different UR } } +// Alias represents the terraform state for an alias resource. +type Alias struct { + Alias types.String `tfsdk:"alias"` + ID types.String `tfsdk:"id"` + 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, + ID: types.StringValue(response.UID), + DeploymentID: types.StringValue(response.DeploymentID), + TeamID: toTeamID(response.TeamID), + } +} + // Create will create an alias within Vercel. // This is called automatically by the provider when a new resource should be created. func (r *aliasResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { diff --git a/vercel/resource_alias_model.go b/vercel/resource_alias_model.go deleted file mode 100644 index 30a6e3e3..00000000 --- a/vercel/resource_alias_model.go +++ /dev/null @@ -1,26 +0,0 @@ -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"` - ID types.String `tfsdk:"id"` - 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, - ID: types.StringValue(response.UID), - DeploymentID: types.StringValue(response.DeploymentID), - TeamID: toTeamID(response.TeamID), - } -} diff --git a/vercel/resource_deployment.go b/vercel/resource_deployment.go index d6428ebc..8d7a53de 100644 --- a/vercel/resource_deployment.go +++ b/vercel/resource_deployment.go @@ -6,8 +6,10 @@ import ( "fmt" "os" "path/filepath" + "strconv" "strings" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier" @@ -177,6 +179,212 @@ terraform to your Deployment. } } +// ProjectSettings represents the terraform state for a nested deployment -> project_settings +// block. These are overrides specific to a single deployment. +type ProjectSettings struct { + BuildCommand types.String `tfsdk:"build_command"` + Framework types.String `tfsdk:"framework"` + InstallCommand types.String `tfsdk:"install_command"` + OutputDirectory types.String `tfsdk:"output_directory"` + RootDirectory types.String `tfsdk:"root_directory"` +} + +// Deployment represents the terraform state for a deployment resource. +type Deployment struct { + Domains types.List `tfsdk:"domains"` + Environment types.Map `tfsdk:"environment"` + Files types.Map `tfsdk:"files"` + ID types.String `tfsdk:"id"` + Production types.Bool `tfsdk:"production"` + ProjectID types.String `tfsdk:"project_id"` + PathPrefix types.String `tfsdk:"path_prefix"` + ProjectSettings *ProjectSettings `tfsdk:"project_settings"` + TeamID types.String `tfsdk:"team_id"` + URL types.String `tfsdk:"url"` + DeleteOnDestroy types.Bool `tfsdk:"delete_on_destroy"` + Ref types.String `tfsdk:"ref"` +} + +// setIfNotUnknown is a helper function to set a value in a map if it is not unknown. +// Null values are set as nil, and actual values are set directly. +func setIfNotUnknown(m map[string]interface{}, v types.String, name string) { + if v.IsNull() { + m[name] = nil + } + if v.ValueString() != "" { + m[name] = toPtr(v.ValueString()) + } +} + +// toRequest takes a set of ProjectSettings and converts them into the required +// format for a CreateDeploymentRequest. +func (p *ProjectSettings) toRequest() map[string]interface{} { + res := map[string]interface{}{ + /* Source files outside the root directory are required + * for a monorepo style codebase. This allows a root_directory + * to be set, but enables navigating upwards into a parent workspace. + * + * Surprisngly, even though this is the default setting for a project, + * it has to be explicitly passed for each request. + */ + "sourceFilesOutsideRootDirectory": true, + } + if p == nil { + return res + } + + setIfNotUnknown(res, p.BuildCommand, "buildCommand") + setIfNotUnknown(res, p.Framework, "framework") + setIfNotUnknown(res, p.InstallCommand, "installCommand") + setIfNotUnknown(res, p.OutputDirectory, "outputDirectory") + + if p.RootDirectory.IsNull() { + res["rootDirectory"] = nil + } + + return res +} + +// fillStringNull is used to populate unknown resource values within state. Unknown values +// are coerced into null values. Explicitly set values are left unchanged. +func fillStringNull(t types.String) types.String { + if t.IsNull() || t.IsUnknown() { + return types.StringNull() + } + return types.StringValue(t.ValueString()) +} + +// fillNulls takes a ProjectSettings and ensures that none of the values are unknown. +// Any unknown values are instead converted to nulls. +func (p *ProjectSettings) fillNulls() *ProjectSettings { + if p == nil { + return nil + } + return &ProjectSettings{ + BuildCommand: fillStringNull(p.BuildCommand), + Framework: fillStringNull(p.Framework), + InstallCommand: fillStringNull(p.InstallCommand), + OutputDirectory: fillStringNull(p.OutputDirectory), + RootDirectory: fillStringNull(p.RootDirectory), + } +} + +/* + * The files uploaded to Vercel need to have some minor adjustments: + * - Legacy behaviour was that any upward navigation ("../") was stripped from the + * start of a file path. + * - Newer behaviour introduced a `path_prefix` that could be specified, that would + * control what part of a relative path to files should be removed prior to uploading + * into Vercel. + * - We want to support this regardless of path separator, the simplest way to do + * this is to ensure all paths are converted to forward slashes, and settings should + * be specified using forward slashes. + * See https://github.com/vercel/terraform-provider-vercel/issues/14#issuecomment-1103973603 + * for additional context on the first two points. + */ +func normaliseFilename(filename string, pathPrefix types.String) string { + filename = filepath.ToSlash(filename) + if pathPrefix.IsUnknown() || pathPrefix.IsNull() { + for strings.HasPrefix(filename, "../") { + return strings.TrimPrefix(filename, "../") + } + } + + return strings.TrimPrefix(filename, filepath.ToSlash(pathPrefix.ValueString())) +} + +// getFiles is a helper for turning the terraform deployment state into a set of client.DeploymentFile +// structs, ready to hit the API with. It also returns a map of files by sha, which is used to quickly +// look up any missing SHAs from the create deployment resposnse. +func getFiles(unparsedFiles map[string]string, pathPrefix types.String) ([]client.DeploymentFile, map[string]client.DeploymentFile, error) { + var files []client.DeploymentFile + filesBySha := map[string]client.DeploymentFile{} + + for filename, rawSizeAndSha := range unparsedFiles { + sizeSha := strings.Split(rawSizeAndSha, "~") + if len(sizeSha) != 2 { + return nil, nil, fmt.Errorf("expected file to have format `filename: size~sha`, but could not parse") + } + size, err := strconv.Atoi(sizeSha[0]) + if err != nil { + return nil, nil, fmt.Errorf("unable to parse file size: %w", err) + } + sha := sizeSha[1] + + file := client.DeploymentFile{ + File: normaliseFilename(filename, pathPrefix), + Sha: sha, + Size: size, + } + files = append(files, file) + + /* The API can return a set of missing files. When this happens, we want the path name + * complete with the original, untrimmed prefix. This also needs to use the hosts + * path separator. This is so we can read the file. + */ + filesBySha[sha] = client.DeploymentFile{ + File: filename, + Sha: sha, + Size: size, + } + } + return files, filesBySha, nil +} + +// convertResponseToDeployment 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 deployment state are used. +func convertResponseToDeployment(response client.DeploymentResponse, plan Deployment) Deployment { + production := types.BoolValue(false) + /* + * TODO - the first deployment to a new project is currently _always_ a + * production deployment, even if you ask it to be a preview deployment. + * In order to terraform complaining about an inconsistent output, we should only set + * the state back if it matches what we expect. The third part of this + * conditional ensures this, but can be removed if the behaviour is changed. + * see: + * https://github.com/vercel/customer-issues/issues/178#issuecomment-1012062345 and + * https://vercel.slack.com/archives/C01A2M9R8RZ/p1639594164360300 + * for more context. + */ + if response.Target != nil && *response.Target == "production" && (plan.Production.ValueBool() || plan.Production.IsUnknown()) { + production = types.BoolValue(true) + } + + var domains []attr.Value + for _, a := range response.Aliases { + domains = append(domains, types.StringValue(a)) + } + + if plan.Environment.IsUnknown() || plan.Environment.IsNull() { + plan.Environment = types.MapNull(types.StringType) + } + + if plan.Files.IsUnknown() || plan.Files.IsNull() { + plan.Files = types.MapNull(types.StringType) + } + + ref := types.StringNull() + if response.GitSource.Ref != "" { + ref = types.StringValue(response.GitSource.Ref) + } + + return Deployment{ + Domains: types.ListValueMust(types.StringType, domains), + TeamID: toTeamID(response.TeamID), + Environment: plan.Environment, + ProjectID: types.StringValue(response.ProjectID), + ID: types.StringValue(response.ID), + URL: types.StringValue(response.URL), + Production: production, + Files: plan.Files, + PathPrefix: fillStringNull(plan.PathPrefix), + ProjectSettings: plan.ProjectSettings.fillNulls(), + DeleteOnDestroy: plan.DeleteOnDestroy, + Ref: ref, + } +} + // ValidateConfig allows additional validation (specifically cross-field validation) to be added. func (r *deploymentResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) { var config Deployment diff --git a/vercel/resource_deployment_model.go b/vercel/resource_deployment_model.go deleted file mode 100644 index ee7e4efa..00000000 --- a/vercel/resource_deployment_model.go +++ /dev/null @@ -1,218 +0,0 @@ -package vercel - -import ( - "fmt" - "path/filepath" - "strconv" - "strings" - - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/vercel/terraform-provider-vercel/client" -) - -// ProjectSettings represents the terraform state for a nested deployment -> project_settings -// block. These are overrides specific to a single deployment. -type ProjectSettings struct { - BuildCommand types.String `tfsdk:"build_command"` - Framework types.String `tfsdk:"framework"` - InstallCommand types.String `tfsdk:"install_command"` - OutputDirectory types.String `tfsdk:"output_directory"` - RootDirectory types.String `tfsdk:"root_directory"` -} - -// Deployment represents the terraform state for a deployment resource. -type Deployment struct { - Domains types.List `tfsdk:"domains"` - Environment types.Map `tfsdk:"environment"` - Files types.Map `tfsdk:"files"` - ID types.String `tfsdk:"id"` - Production types.Bool `tfsdk:"production"` - ProjectID types.String `tfsdk:"project_id"` - PathPrefix types.String `tfsdk:"path_prefix"` - ProjectSettings *ProjectSettings `tfsdk:"project_settings"` - TeamID types.String `tfsdk:"team_id"` - URL types.String `tfsdk:"url"` - DeleteOnDestroy types.Bool `tfsdk:"delete_on_destroy"` - Ref types.String `tfsdk:"ref"` -} - -// setIfNotUnknown is a helper function to set a value in a map if it is not unknown. -// Null values are set as nil, and actual values are set directly. -func setIfNotUnknown(m map[string]interface{}, v types.String, name string) { - if v.IsNull() { - m[name] = nil - } - if v.ValueString() != "" { - m[name] = toPtr(v.ValueString()) - } -} - -// toRequest takes a set of ProjectSettings and converts them into the required -// format for a CreateDeploymentRequest. -func (p *ProjectSettings) toRequest() map[string]interface{} { - res := map[string]interface{}{ - /* Source files outside the root directory are required - * for a monorepo style codebase. This allows a root_directory - * to be set, but enables navigating upwards into a parent workspace. - * - * Surprisngly, even though this is the default setting for a project, - * it has to be explicitly passed for each request. - */ - "sourceFilesOutsideRootDirectory": true, - } - if p == nil { - return res - } - - setIfNotUnknown(res, p.BuildCommand, "buildCommand") - setIfNotUnknown(res, p.Framework, "framework") - setIfNotUnknown(res, p.InstallCommand, "installCommand") - setIfNotUnknown(res, p.OutputDirectory, "outputDirectory") - - if p.RootDirectory.IsNull() { - res["rootDirectory"] = nil - } - - return res -} - -// fillStringNull is used to populate unknown resource values within state. Unknown values -// are coerced into null values. Explicitly set values are left unchanged. -func fillStringNull(t types.String) types.String { - if t.IsNull() || t.IsUnknown() { - return types.StringNull() - } - return types.StringValue(t.ValueString()) -} - -// fillNulls takes a ProjectSettings and ensures that none of the values are unknown. -// Any unknown values are instead converted to nulls. -func (p *ProjectSettings) fillNulls() *ProjectSettings { - if p == nil { - return nil - } - return &ProjectSettings{ - BuildCommand: fillStringNull(p.BuildCommand), - Framework: fillStringNull(p.Framework), - InstallCommand: fillStringNull(p.InstallCommand), - OutputDirectory: fillStringNull(p.OutputDirectory), - RootDirectory: fillStringNull(p.RootDirectory), - } -} - -/* - * The files uploaded to Vercel need to have some minor adjustments: - * - Legacy behaviour was that any upward navigation ("../") was stripped from the - * start of a file path. - * - Newer behaviour introduced a `path_prefix` that could be specified, that would - * control what part of a relative path to files should be removed prior to uploading - * into Vercel. - * - We want to support this regardless of path separator, the simplest way to do - * this is to ensure all paths are converted to forward slashes, and settings should - * be specified using forward slashes. - * See https://github.com/vercel/terraform-provider-vercel/issues/14#issuecomment-1103973603 - * for additional context on the first two points. - */ -func normaliseFilename(filename string, pathPrefix types.String) string { - filename = filepath.ToSlash(filename) - if pathPrefix.IsUnknown() || pathPrefix.IsNull() { - for strings.HasPrefix(filename, "../") { - return strings.TrimPrefix(filename, "../") - } - } - - return strings.TrimPrefix(filename, filepath.ToSlash(pathPrefix.ValueString())) -} - -// getFiles is a helper for turning the terraform deployment state into a set of client.DeploymentFile -// structs, ready to hit the API with. It also returns a map of files by sha, which is used to quickly -// look up any missing SHAs from the create deployment resposnse. -func getFiles(unparsedFiles map[string]string, pathPrefix types.String) ([]client.DeploymentFile, map[string]client.DeploymentFile, error) { - var files []client.DeploymentFile - filesBySha := map[string]client.DeploymentFile{} - - for filename, rawSizeAndSha := range unparsedFiles { - sizeSha := strings.Split(rawSizeAndSha, "~") - if len(sizeSha) != 2 { - return nil, nil, fmt.Errorf("expected file to have format `filename: size~sha`, but could not parse") - } - size, err := strconv.Atoi(sizeSha[0]) - if err != nil { - return nil, nil, fmt.Errorf("unable to parse file size: %w", err) - } - sha := sizeSha[1] - - file := client.DeploymentFile{ - File: normaliseFilename(filename, pathPrefix), - Sha: sha, - Size: size, - } - files = append(files, file) - - /* The API can return a set of missing files. When this happens, we want the path name - * complete with the original, untrimmed prefix. This also needs to use the hosts - * path separator. This is so we can read the file. - */ - filesBySha[sha] = client.DeploymentFile{ - File: filename, - Sha: sha, - Size: size, - } - } - return files, filesBySha, nil -} - -// convertResponseToDeployment 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 deployment state are used. -func convertResponseToDeployment(response client.DeploymentResponse, plan Deployment) Deployment { - production := types.BoolValue(false) - /* - * TODO - the first deployment to a new project is currently _always_ a - * production deployment, even if you ask it to be a preview deployment. - * In order to terraform complaining about an inconsistent output, we should only set - * the state back if it matches what we expect. The third part of this - * conditional ensures this, but can be removed if the behaviour is changed. - * see: - * https://github.com/vercel/customer-issues/issues/178#issuecomment-1012062345 and - * https://vercel.slack.com/archives/C01A2M9R8RZ/p1639594164360300 - * for more context. - */ - if response.Target != nil && *response.Target == "production" && (plan.Production.ValueBool() || plan.Production.IsUnknown()) { - production = types.BoolValue(true) - } - - var domains []attr.Value - for _, a := range response.Aliases { - domains = append(domains, types.StringValue(a)) - } - - if plan.Environment.IsUnknown() || plan.Environment.IsNull() { - plan.Environment = types.MapNull(types.StringType) - } - - if plan.Files.IsUnknown() || plan.Files.IsNull() { - plan.Files = types.MapNull(types.StringType) - } - - ref := types.StringNull() - if response.GitSource.Ref != "" { - ref = types.StringValue(response.GitSource.Ref) - } - - return Deployment{ - Domains: types.ListValueMust(types.StringType, domains), - TeamID: toTeamID(response.TeamID), - Environment: plan.Environment, - ProjectID: types.StringValue(response.ProjectID), - ID: types.StringValue(response.ID), - URL: types.StringValue(response.URL), - Production: production, - Files: plan.Files, - PathPrefix: fillStringNull(plan.PathPrefix), - ProjectSettings: plan.ProjectSettings.fillNulls(), - DeleteOnDestroy: plan.DeleteOnDestroy, - Ref: ref, - } -} diff --git a/vercel/resource_dns_record.go b/vercel/resource_dns_record.go index 64b0e0ec..ab0761fe 100644 --- a/vercel/resource_dns_record.go +++ b/vercel/resource_dns_record.go @@ -3,6 +3,8 @@ package vercel import ( "context" "fmt" + "strconv" + "strings" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" @@ -157,6 +159,145 @@ For more detailed information, please see the [Vercel documentation](https://ver } } +// SRV reflect the state terraform stores internally for a nested SRV Record. +type SRV struct { + Port types.Int64 `tfsdk:"port"` + Priority types.Int64 `tfsdk:"priority"` + Target types.String `tfsdk:"target"` + Weight types.Int64 `tfsdk:"weight"` +} + +// DNSRecord reflects the state terraform stores internally for a DNS Record. +type DNSRecord struct { + ID types.String `tfsdk:"id"` + Domain types.String `tfsdk:"domain"` + MXPriority types.Int64 `tfsdk:"mx_priority"` + Name types.String `tfsdk:"name"` + SRV *SRV `tfsdk:"srv"` + TTL types.Int64 `tfsdk:"ttl"` + TeamID types.String `tfsdk:"team_id"` + Type types.String `tfsdk:"type"` + Value types.String `tfsdk:"value"` + Comment types.String `tfsdk:"comment"` +} + +func (d DNSRecord) toCreateDNSRecordRequest() client.CreateDNSRecordRequest { + var srv *client.SRV = nil + if d.Type.ValueString() == "SRV" { + srv = &client.SRV{ + Port: d.SRV.Port.ValueInt64(), + Priority: d.SRV.Priority.ValueInt64(), + Target: d.SRV.Target.ValueString(), + Weight: d.SRV.Weight.ValueInt64(), + } + } + + return client.CreateDNSRecordRequest{ + Domain: d.Domain.ValueString(), + MXPriority: d.MXPriority.ValueInt64(), + Name: d.Name.ValueString(), + TTL: d.TTL.ValueInt64(), + Type: d.Type.ValueString(), + Value: d.Value.ValueString(), + SRV: srv, + Comment: d.Comment.ValueString(), + } +} + +func (d DNSRecord) toUpdateRequest() client.UpdateDNSRecordRequest { + var srv *client.SRVUpdate = nil + if d.SRV != nil { + srv = &client.SRVUpdate{ + Port: toPtr(d.SRV.Port.ValueInt64()), + Priority: toPtr(d.SRV.Priority.ValueInt64()), + Target: toPtr(d.SRV.Target.ValueString()), + Weight: toPtr(d.SRV.Weight.ValueInt64()), + } + } + return client.UpdateDNSRecordRequest{ + MXPriority: toInt64Pointer(d.MXPriority), + Name: toPtr(d.Name.ValueString()), + SRV: srv, + TTL: toInt64Pointer(d.TTL), + Value: toStrPointer(d.Value), + Comment: d.Comment.ValueString(), + } +} + +func convertResponseToDNSRecord(r client.DNSRecord, value types.String, srv *SRV) (record DNSRecord, err error) { + record = DNSRecord{ + Domain: types.StringValue(r.Domain), + ID: types.StringValue(r.ID), + MXPriority: types.Int64Null(), + Name: types.StringValue(r.Name), + TTL: types.Int64Value(r.TTL), + TeamID: toTeamID(r.TeamID), + Type: types.StringValue(r.RecordType), + Comment: types.StringValue(r.Comment), + } + + if r.RecordType == "SRV" { + // The returned 'Value' field is comprised of the various parts of the SRV block. + // So instead, we want to parse the SRV block back out. + split := strings.Split(r.Value, " ") + if len(split) != 4 && len(split) != 3 { + return record, fmt.Errorf("expected a 3 or 4 part value '{priority} {weight} {port} {target}', but got %s", r.Value) + } + priority, err := strconv.Atoi(split[0]) + if err != nil { + return record, fmt.Errorf("expected SRV record weight to be an int, but got %s", split[0]) + } + weight, err := strconv.Atoi(split[1]) + if err != nil { + return record, fmt.Errorf("expected SRV record port to be an int, but got %s", split[1]) + } + port, err := strconv.Atoi(split[2]) + if err != nil { + return record, fmt.Errorf("expected SRV record port to be an int, but got %s", split[1]) + } + target := "" + if len(split) == 4 { + target = split[3] + } + record.SRV = &SRV{ + Weight: types.Int64Value(int64(weight)), + Port: types.Int64Value(int64(port)), + Priority: types.Int64Value(int64(priority)), + Target: types.StringValue(target), + } + // SRV records have no value + record.Value = types.StringNull() + if srv != nil && fmt.Sprintf("%s.", srv.Target.ValueString()) == record.SRV.Target.ValueString() { + record.SRV.Target = srv.Target + } + return record, nil + } + + if r.RecordType == "MX" { + split := strings.Split(r.Value, " ") + if len(split) != 2 { + return record, fmt.Errorf("expected a 2 part value '{priority} {value}', but got %s", r.Value) + } + priority, err := strconv.Atoi(split[0]) + if err != nil { + return record, fmt.Errorf("expected MX priority to be an int, but got %s", split[0]) + } + + record.MXPriority = types.Int64Value(int64(priority)) + record.Value = types.StringValue(split[1]) + if split[1] == fmt.Sprintf("%s.", value.ValueString()) { + record.Value = value + } + return record, nil + } + + record.Value = types.StringValue(r.Value) + if r.Value == fmt.Sprintf("%s.", value.ValueString()) { + record.Value = value + } + return record, nil +} + // ValidateConfig validates the Resource configuration. func (r *dnsRecordResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) { var config DNSRecord diff --git a/vercel/resource_dns_record_model.go b/vercel/resource_dns_record_model.go deleted file mode 100644 index 1cf2b291..00000000 --- a/vercel/resource_dns_record_model.go +++ /dev/null @@ -1,149 +0,0 @@ -package vercel - -import ( - "fmt" - "strconv" - "strings" - - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/vercel/terraform-provider-vercel/client" -) - -// SRV reflect the state terraform stores internally for a nested SRV Record. -type SRV struct { - Port types.Int64 `tfsdk:"port"` - Priority types.Int64 `tfsdk:"priority"` - Target types.String `tfsdk:"target"` - Weight types.Int64 `tfsdk:"weight"` -} - -// DNSRecord reflects the state terraform stores internally for a DNS Record. -type DNSRecord struct { - ID types.String `tfsdk:"id"` - Domain types.String `tfsdk:"domain"` - MXPriority types.Int64 `tfsdk:"mx_priority"` - Name types.String `tfsdk:"name"` - SRV *SRV `tfsdk:"srv"` - TTL types.Int64 `tfsdk:"ttl"` - TeamID types.String `tfsdk:"team_id"` - Type types.String `tfsdk:"type"` - Value types.String `tfsdk:"value"` - Comment types.String `tfsdk:"comment"` -} - -func (d DNSRecord) toCreateDNSRecordRequest() client.CreateDNSRecordRequest { - var srv *client.SRV = nil - if d.Type.ValueString() == "SRV" { - srv = &client.SRV{ - Port: d.SRV.Port.ValueInt64(), - Priority: d.SRV.Priority.ValueInt64(), - Target: d.SRV.Target.ValueString(), - Weight: d.SRV.Weight.ValueInt64(), - } - } - - return client.CreateDNSRecordRequest{ - Domain: d.Domain.ValueString(), - MXPriority: d.MXPriority.ValueInt64(), - Name: d.Name.ValueString(), - TTL: d.TTL.ValueInt64(), - Type: d.Type.ValueString(), - Value: d.Value.ValueString(), - SRV: srv, - Comment: d.Comment.ValueString(), - } -} - -func (d DNSRecord) toUpdateRequest() client.UpdateDNSRecordRequest { - var srv *client.SRVUpdate = nil - if d.SRV != nil { - srv = &client.SRVUpdate{ - Port: toPtr(d.SRV.Port.ValueInt64()), - Priority: toPtr(d.SRV.Priority.ValueInt64()), - Target: toPtr(d.SRV.Target.ValueString()), - Weight: toPtr(d.SRV.Weight.ValueInt64()), - } - } - return client.UpdateDNSRecordRequest{ - MXPriority: toInt64Pointer(d.MXPriority), - Name: toPtr(d.Name.ValueString()), - SRV: srv, - TTL: toInt64Pointer(d.TTL), - Value: toStrPointer(d.Value), - Comment: d.Comment.ValueString(), - } -} - -func convertResponseToDNSRecord(r client.DNSRecord, value types.String, srv *SRV) (record DNSRecord, err error) { - record = DNSRecord{ - Domain: types.StringValue(r.Domain), - ID: types.StringValue(r.ID), - MXPriority: types.Int64Null(), - Name: types.StringValue(r.Name), - TTL: types.Int64Value(r.TTL), - TeamID: toTeamID(r.TeamID), - Type: types.StringValue(r.RecordType), - Comment: types.StringValue(r.Comment), - } - - if r.RecordType == "SRV" { - // The returned 'Value' field is comprised of the various parts of the SRV block. - // So instead, we want to parse the SRV block back out. - split := strings.Split(r.Value, " ") - if len(split) != 4 && len(split) != 3 { - return record, fmt.Errorf("expected a 3 or 4 part value '{priority} {weight} {port} {target}', but got %s", r.Value) - } - priority, err := strconv.Atoi(split[0]) - if err != nil { - return record, fmt.Errorf("expected SRV record weight to be an int, but got %s", split[0]) - } - weight, err := strconv.Atoi(split[1]) - if err != nil { - return record, fmt.Errorf("expected SRV record port to be an int, but got %s", split[1]) - } - port, err := strconv.Atoi(split[2]) - if err != nil { - return record, fmt.Errorf("expected SRV record port to be an int, but got %s", split[1]) - } - target := "" - if len(split) == 4 { - target = split[3] - } - record.SRV = &SRV{ - Weight: types.Int64Value(int64(weight)), - Port: types.Int64Value(int64(port)), - Priority: types.Int64Value(int64(priority)), - Target: types.StringValue(target), - } - // SRV records have no value - record.Value = types.StringNull() - if srv != nil && fmt.Sprintf("%s.", srv.Target.ValueString()) == record.SRV.Target.ValueString() { - record.SRV.Target = srv.Target - } - return record, nil - } - - if r.RecordType == "MX" { - split := strings.Split(r.Value, " ") - if len(split) != 2 { - return record, fmt.Errorf("expected a 2 part value '{priority} {value}', but got %s", r.Value) - } - priority, err := strconv.Atoi(split[0]) - if err != nil { - return record, fmt.Errorf("expected MX priority to be an int, but got %s", split[0]) - } - - record.MXPriority = types.Int64Value(int64(priority)) - record.Value = types.StringValue(split[1]) - if split[1] == fmt.Sprintf("%s.", value.ValueString()) { - record.Value = value - } - return record, nil - } - - record.Value = types.StringValue(r.Value) - if r.Value == fmt.Sprintf("%s.", value.ValueString()) { - record.Value = value - } - return record, nil -} diff --git a/vercel/resource_project.go b/vercel/resource_project.go index 04f22bcf..b8f4bcf9 100644 --- a/vercel/resource_project.go +++ b/vercel/resource_project.go @@ -306,6 +306,474 @@ At this time you cannot use a Vercel Project resource with in-line ` + "`environ } } +// Project reflects the state terraform stores internally for a project. +type Project struct { + BuildCommand types.String `tfsdk:"build_command"` + DevCommand types.String `tfsdk:"dev_command"` + Environment types.Set `tfsdk:"environment"` + Framework types.String `tfsdk:"framework"` + GitRepository *GitRepository `tfsdk:"git_repository"` + ID types.String `tfsdk:"id"` + IgnoreCommand types.String `tfsdk:"ignore_command"` + InstallCommand types.String `tfsdk:"install_command"` + Name types.String `tfsdk:"name"` + OutputDirectory types.String `tfsdk:"output_directory"` + PublicSource types.Bool `tfsdk:"public_source"` + RootDirectory types.String `tfsdk:"root_directory"` + ServerlessFunctionRegion types.String `tfsdk:"serverless_function_region"` + TeamID types.String `tfsdk:"team_id"` + VercelAuthentication *VercelAuthentication `tfsdk:"vercel_authentication"` + PasswordProtection *PasswordProtectionWithPassword `tfsdk:"password_protection"` + TrustedIps *TrustedIps `tfsdk:"trusted_ips"` + ProtectionBypassForAutomation types.Bool `tfsdk:"protection_bypass_for_automation"` + ProtectionBypassForAutomationSecret types.String `tfsdk:"protection_bypass_for_automation_secret"` + AutoExposeSystemEnvVars types.Bool `tfsdk:"automatically_expose_system_environment_variables"` +} + +var nullProject = Project{ + /* As this is read only, none of these fields are specified - so treat them all as Null */ + BuildCommand: types.StringNull(), + DevCommand: types.StringNull(), + InstallCommand: types.StringNull(), + OutputDirectory: types.StringNull(), + PublicSource: types.BoolNull(), + Environment: types.SetNull(envVariableElemType), +} + +func (p *Project) environment(ctx context.Context) ([]EnvironmentItem, error) { + if p.Environment.IsNull() { + return nil, nil + } + + var vars []EnvironmentItem + err := p.Environment.ElementsAs(ctx, &vars, true) + if err != nil { + return nil, fmt.Errorf("error reading project environment variables: %s", err) + } + return vars, nil +} + +func parseEnvironment(vars []EnvironmentItem) []client.EnvironmentVariable { + out := []client.EnvironmentVariable{} + for _, e := range vars { + target := []string{} + for _, t := range e.Target { + target = append(target, t.ValueString()) + } + + var envVariableType string + + if e.Sensitive.ValueBool() { + envVariableType = "sensitive" + } else { + envVariableType = "encrypted" + } + + out = append(out, client.EnvironmentVariable{ + Key: e.Key.ValueString(), + Value: e.Value.ValueString(), + Target: target, + GitBranch: toStrPointer(e.GitBranch), + Type: envVariableType, + ID: e.ID.ValueString(), + }) + } + return out +} + +func (p *Project) toCreateProjectRequest(envs []EnvironmentItem) client.CreateProjectRequest { + return client.CreateProjectRequest{ + BuildCommand: toStrPointer(p.BuildCommand), + CommandForIgnoringBuildStep: toStrPointer(p.IgnoreCommand), + DevCommand: toStrPointer(p.DevCommand), + EnvironmentVariables: parseEnvironment(envs), + Framework: toStrPointer(p.Framework), + GitRepository: p.GitRepository.toCreateProjectRequest(), + InstallCommand: toStrPointer(p.InstallCommand), + Name: p.Name.ValueString(), + OutputDirectory: toStrPointer(p.OutputDirectory), + PublicSource: toBoolPointer(p.PublicSource), + RootDirectory: toStrPointer(p.RootDirectory), + ServerlessFunctionRegion: toStrPointer(p.ServerlessFunctionRegion), + } +} + +func (p *Project) toUpdateProjectRequest(oldName string) client.UpdateProjectRequest { + var name *string = nil + if oldName != p.Name.ValueString() { + n := p.Name.ValueString() + name = &n + } + return client.UpdateProjectRequest{ + BuildCommand: toStrPointer(p.BuildCommand), + CommandForIgnoringBuildStep: toStrPointer(p.IgnoreCommand), + DevCommand: toStrPointer(p.DevCommand), + Framework: toStrPointer(p.Framework), + InstallCommand: toStrPointer(p.InstallCommand), + Name: name, + OutputDirectory: toStrPointer(p.OutputDirectory), + PublicSource: toBoolPointer(p.PublicSource), + RootDirectory: toStrPointer(p.RootDirectory), + ServerlessFunctionRegion: toStrPointer(p.ServerlessFunctionRegion), + PasswordProtection: p.PasswordProtection.toUpdateProjectRequest(), + VercelAuthentication: p.VercelAuthentication.toUpdateProjectRequest(), + TrustedIps: p.TrustedIps.toUpdateProjectRequest(), + AutoExposeSystemEnvVars: toBoolPointer(p.AutoExposeSystemEnvVars), + } +} + +// EnvironmentItem reflects the state terraform stores internally for a project's environment variable. +type EnvironmentItem struct { + Target []types.String `tfsdk:"target"` + GitBranch types.String `tfsdk:"git_branch"` + Key types.String `tfsdk:"key"` + Value types.String `tfsdk:"value"` + ID types.String `tfsdk:"id"` + Sensitive types.Bool `tfsdk:"sensitive"` +} + +func (e *EnvironmentItem) toEnvironmentVariableRequest() client.EnvironmentVariableRequest { + target := []string{} + for _, t := range e.Target { + target = append(target, t.ValueString()) + } + + var envVariableType string + + if e.Sensitive.ValueBool() { + envVariableType = "sensitive" + } else { + envVariableType = "encrypted" + } + + return client.EnvironmentVariableRequest{ + Key: e.Key.ValueString(), + Value: e.Value.ValueString(), + Target: target, + GitBranch: toStrPointer(e.GitBranch), + Type: envVariableType, + } +} + +// GitRepository reflects the state terraform stores internally for a nested git_repository block on a project resource. +type GitRepository struct { + Type types.String `tfsdk:"type"` + Repo types.String `tfsdk:"repo"` + ProductionBranch types.String `tfsdk:"production_branch"` +} + +func (g *GitRepository) toCreateProjectRequest() *client.GitRepository { + if g == nil { + return nil + } + return &client.GitRepository{ + Type: g.Type.ValueString(), + Repo: g.Repo.ValueString(), + } +} + +func toApiDeploymentProtectionType(dt types.String) string { + switch dt { + case types.StringValue("standard_protection"): + return "prod_deployment_urls_and_all_previews" + case types.StringValue("all_deployments"): + return "all" + case types.StringValue("only_preview_deployments"): + return "preview" + case types.StringValue("only_production_deployments"): + return "production" + default: + return dt.ValueString() + } +} + +func fromApiDeploymentProtectionType(dt string) types.String { + switch dt { + case "prod_deployment_urls_and_all_previews": + return types.StringValue("standard_protection") + case "all": + return types.StringValue("all_deployments") + case "preview": + return types.StringValue("only_preview_deployments") + case "production": + return types.StringValue("only_production_deployments") + default: + return types.StringValue(dt) + } +} + +func (v *VercelAuthentication) toUpdateProjectRequest() *client.VercelAuthentication { + if v == nil { + return nil + } + + return &client.VercelAuthentication{ + DeploymentType: toApiDeploymentProtectionType(v.DeploymentType), + } +} + +func (p *PasswordProtectionWithPassword) toUpdateProjectRequest() *client.PasswordProtectionWithPassword { + if p == nil { + return nil + } + + return &client.PasswordProtectionWithPassword{ + DeploymentType: toApiDeploymentProtectionType(p.DeploymentType), + Password: p.Password.ValueString(), + } +} + +func toApiTrustedIpProtectionMode(dt types.String) string { + switch dt { + case types.StringValue("trusted_ip_required"): + return "additional" + case types.StringValue("trusted_ip_optional"): + return "exclusive" + default: + return dt.ValueString() + } +} + +func fromApiTrustedIpProtectionMode(dt string) types.String { + switch dt { + case "additional": + return types.StringValue("trusted_ip_required") + case "exclusive": + return types.StringValue("trusted_ip_optional") + default: + return types.StringValue(dt) + } +} + +func (t *TrustedIps) toUpdateProjectRequest() *client.TrustedIps { + if t == nil { + return nil + } + + var addresses = []client.TrustedIpAddress{} + for _, address := range t.Addresses { + addresses = append(addresses, client.TrustedIpAddress{ + Value: address.Value.ValueString(), + Note: address.Note.ValueString(), + }) + } + + return &client.TrustedIps{ + Addresses: addresses, + DeploymentType: toApiDeploymentProtectionType(t.DeploymentType), + ProtectionMode: toApiTrustedIpProtectionMode(t.ProtectionMode), + } +} + +/* +* In the Vercel API the following fields are coerced to null during project creation + +* This causes an issue when they are specified, but falsy, as the +* terraform configuration explicitly sets a value for them, but the Vercel +* API returns a different value. This causes an inconsistent plan error. + +* We avoid this issue by choosing to use values from the terraform state, +* but only if they are _explicitly stated_ *and* they are _falsy_ values +* *and* the response value was null. This is important as drift detection +* would fail to work if the value was always selected, so this is as stringent +* as possible to allow drift-detection in the majority of scenarios. + +* This is implemented in the below uncoerceString and uncoerceBool functions. + */ +type projectCoercedFields struct { + BuildCommand types.String + DevCommand types.String + InstallCommand types.String + OutputDirectory types.String + PublicSource types.Bool +} + +func (p *Project) coercedFields() projectCoercedFields { + return projectCoercedFields{ + BuildCommand: p.BuildCommand, + DevCommand: p.DevCommand, + InstallCommand: p.InstallCommand, + OutputDirectory: p.OutputDirectory, + PublicSource: p.PublicSource, + } +} + +func uncoerceString(plan, res types.String) types.String { + if plan.ValueString() == "" && !plan.IsNull() && res.IsNull() { + return plan + } + return res +} +func uncoerceBool(plan, res types.Bool) types.Bool { + if !plan.ValueBool() && !plan.IsNull() && res.IsNull() { + return plan + } + return res +} + +var envVariableElemType = types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "key": types.StringType, + "value": types.StringType, + "target": types.SetType{ + ElemType: types.StringType, + }, + "git_branch": types.StringType, + "id": types.StringType, + "sensitive": types.BoolType, + }, +} + +func hasSameTarget(p EnvironmentItem, target []string) bool { + if len(p.Target) != len(target) { + return false + } + for _, t := range p.Target { + v := t.ValueString() + if !contains(target, v) { + return false + } + } + return true +} + +func convertResponseToProject(ctx context.Context, response client.ProjectResponse, plan Project) (Project, error) { + fields := plan.coercedFields() + + var gr *GitRepository + if repo := response.Repository(); repo != nil { + gr = &GitRepository{ + Type: types.StringValue(repo.Type), + Repo: types.StringValue(repo.Repo), + ProductionBranch: types.StringNull(), + } + if repo.ProductionBranch != nil { + gr.ProductionBranch = types.StringValue(*repo.ProductionBranch) + } + } + + var pp *PasswordProtectionWithPassword + if response.PasswordProtection != nil { + pass := types.StringValue("") + if plan.PasswordProtection != nil { + pass = plan.PasswordProtection.Password + } + pp = &PasswordProtectionWithPassword{ + Password: pass, + DeploymentType: fromApiDeploymentProtectionType(response.PasswordProtection.DeploymentType), + } + } + + var va = &VercelAuthentication{ + DeploymentType: types.StringValue("none"), + } + if response.VercelAuthentication != nil { + va = &VercelAuthentication{ + DeploymentType: fromApiDeploymentProtectionType(response.VercelAuthentication.DeploymentType), + } + } + + var tip *TrustedIps + if response.TrustedIps != nil { + var addresses []TrustedIpAddress + for _, address := range response.TrustedIps.Addresses { + addresses = append(addresses, TrustedIpAddress{ + Value: types.StringValue(address.Value), + Note: types.StringValue(address.Note), + }) + } + tip = &TrustedIps{ + DeploymentType: fromApiDeploymentProtectionType(response.TrustedIps.DeploymentType), + Addresses: addresses, + ProtectionMode: fromApiTrustedIpProtectionMode(response.TrustedIps.ProtectionMode), + } + } + + var env []attr.Value + for _, e := range response.EnvironmentVariables { + target := []attr.Value{} + for _, t := range e.Target { + target = append(target, types.StringValue(t)) + } + value := types.StringValue(e.Value) + if e.Type == "sensitive" { + value = types.StringNull() + environment, err := plan.environment(ctx) + if err != nil { + return Project{}, fmt.Errorf("error reading project environment variables: %s", err) + } + for _, p := range environment { + if p.Sensitive.ValueBool() && p.Key.ValueString() == e.Key && hasSameTarget(p, e.Target) { + value = p.Value + break + } + } + } + + env = append(env, types.ObjectValueMust( + map[string]attr.Type{ + "key": types.StringType, + "value": types.StringType, + "target": types.SetType{ + ElemType: types.StringType, + }, + "git_branch": types.StringType, + "id": types.StringType, + "sensitive": types.BoolType, + }, + map[string]attr.Value{ + "key": types.StringValue(e.Key), + "value": value, + "target": types.SetValueMust(types.StringType, target), + "git_branch": fromStringPointer(e.GitBranch), + "id": types.StringValue(e.ID), + "sensitive": types.BoolValue(e.Type == "sensitive"), + }, + )) + } + + protectionBypassSecret := types.StringNull() + protectionBypass := types.BoolNull() + for k, v := range response.ProtectionBypass { + if v.Scope == "automation-bypass" { + protectionBypass = types.BoolValue(true) + protectionBypassSecret = types.StringValue(k) + break + } + } + if !plan.ProtectionBypassForAutomation.IsNull() && !plan.ProtectionBypassForAutomation.ValueBool() { + protectionBypass = types.BoolValue(false) + } + + environmentEntry := types.SetValueMust(envVariableElemType, env) + if len(response.EnvironmentVariables) == 0 && plan.Environment.IsNull() { + environmentEntry = types.SetNull(envVariableElemType) + } + + return Project{ + BuildCommand: uncoerceString(fields.BuildCommand, fromStringPointer(response.BuildCommand)), + DevCommand: uncoerceString(fields.DevCommand, fromStringPointer(response.DevCommand)), + Environment: environmentEntry, + Framework: fromStringPointer(response.Framework), + GitRepository: gr, + ID: types.StringValue(response.ID), + IgnoreCommand: fromStringPointer(response.CommandForIgnoringBuildStep), + InstallCommand: uncoerceString(fields.InstallCommand, fromStringPointer(response.InstallCommand)), + Name: types.StringValue(response.Name), + OutputDirectory: uncoerceString(fields.OutputDirectory, fromStringPointer(response.OutputDirectory)), + PublicSource: uncoerceBool(fields.PublicSource, fromBoolPointer(response.PublicSource)), + RootDirectory: fromStringPointer(response.RootDirectory), + ServerlessFunctionRegion: fromStringPointer(response.ServerlessFunctionRegion), + TeamID: toTeamID(response.TeamID), + PasswordProtection: pp, + VercelAuthentication: va, + TrustedIps: tip, + ProtectionBypassForAutomation: protectionBypass, + ProtectionBypassForAutomationSecret: protectionBypassSecret, + AutoExposeSystemEnvVars: fromBoolPointer(response.AutoExposeSystemEnvVars), + }, nil +} + // Create will create a project within Vercel by calling the Vercel API. // This is called automatically by the provider when a new resource should be created. func (r *projectResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { diff --git a/vercel/resource_project_domain.go b/vercel/resource_project_domain.go index 41573c89..488dba83 100644 --- a/vercel/resource_project_domain.go +++ b/vercel/resource_project_domain.go @@ -9,6 +9,7 @@ import ( "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/client" ) @@ -97,6 +98,46 @@ By default, Project Domains will be automatically applied to any ` + "`productio } } +// ProjectDomain reflects the state terraform stores internally for a project domain. +type ProjectDomain struct { + Domain types.String `tfsdk:"domain"` + GitBranch types.String `tfsdk:"git_branch"` + ID types.String `tfsdk:"id"` + ProjectID types.String `tfsdk:"project_id"` + Redirect types.String `tfsdk:"redirect"` + RedirectStatusCode types.Int64 `tfsdk:"redirect_status_code"` + TeamID types.String `tfsdk:"team_id"` +} + +func convertResponseToProjectDomain(response client.ProjectDomainResponse) ProjectDomain { + return ProjectDomain{ + Domain: types.StringValue(response.Name), + GitBranch: fromStringPointer(response.GitBranch), + ID: types.StringValue(response.Name), + ProjectID: types.StringValue(response.ProjectID), + Redirect: fromStringPointer(response.Redirect), + RedirectStatusCode: fromInt64Pointer(response.RedirectStatusCode), + TeamID: toTeamID(response.TeamID), + } +} + +func (p *ProjectDomain) toCreateRequest() client.CreateProjectDomainRequest { + return client.CreateProjectDomainRequest{ + GitBranch: p.GitBranch.ValueString(), + Name: p.Domain.ValueString(), + Redirect: p.Redirect.ValueString(), + RedirectStatusCode: p.RedirectStatusCode.ValueInt64(), + } +} + +func (p *ProjectDomain) toUpdateRequest() client.UpdateProjectDomainRequest { + return client.UpdateProjectDomainRequest{ + GitBranch: toStrPointer(p.GitBranch), + Redirect: toStrPointer(p.Redirect), + RedirectStatusCode: toInt64Pointer(p.RedirectStatusCode), + } +} + // Create will create a project domain within Vercel. // This is called automatically by the provider when a new resource should be created. func (r *projectDomainResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { diff --git a/vercel/resource_project_domain_model.go b/vercel/resource_project_domain_model.go deleted file mode 100644 index 7c7a8348..00000000 --- a/vercel/resource_project_domain_model.go +++ /dev/null @@ -1,46 +0,0 @@ -package vercel - -import ( - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/vercel/terraform-provider-vercel/client" -) - -// ProjectDomain reflects the state terraform stores internally for a project domain. -type ProjectDomain struct { - Domain types.String `tfsdk:"domain"` - GitBranch types.String `tfsdk:"git_branch"` - ID types.String `tfsdk:"id"` - ProjectID types.String `tfsdk:"project_id"` - Redirect types.String `tfsdk:"redirect"` - RedirectStatusCode types.Int64 `tfsdk:"redirect_status_code"` - TeamID types.String `tfsdk:"team_id"` -} - -func convertResponseToProjectDomain(response client.ProjectDomainResponse) ProjectDomain { - return ProjectDomain{ - Domain: types.StringValue(response.Name), - GitBranch: fromStringPointer(response.GitBranch), - ID: types.StringValue(response.Name), - ProjectID: types.StringValue(response.ProjectID), - Redirect: fromStringPointer(response.Redirect), - RedirectStatusCode: fromInt64Pointer(response.RedirectStatusCode), - TeamID: toTeamID(response.TeamID), - } -} - -func (p *ProjectDomain) toCreateRequest() client.CreateProjectDomainRequest { - return client.CreateProjectDomainRequest{ - GitBranch: p.GitBranch.ValueString(), - Name: p.Domain.ValueString(), - Redirect: p.Redirect.ValueString(), - RedirectStatusCode: p.RedirectStatusCode.ValueInt64(), - } -} - -func (p *ProjectDomain) toUpdateRequest() client.UpdateProjectDomainRequest { - return client.UpdateProjectDomainRequest{ - GitBranch: toStrPointer(p.GitBranch), - Redirect: toStrPointer(p.Redirect), - RedirectStatusCode: toInt64Pointer(p.RedirectStatusCode), - } -} diff --git a/vercel/resource_project_environment_variable.go b/vercel/resource_project_environment_variable.go index 3a54c469..4a63295a 100644 --- a/vercel/resource_project_environment_variable.go +++ b/vercel/resource_project_environment_variable.go @@ -114,6 +114,95 @@ At this time you cannot use a Vercel Project resource with in-line ` + "`environ } } +// ProjectEnvironmentVariable reflects the state terraform stores internally for a project environment variable. +type ProjectEnvironmentVariable struct { + Target []types.String `tfsdk:"target"` + GitBranch types.String `tfsdk:"git_branch"` + Key types.String `tfsdk:"key"` + Value types.String `tfsdk:"value"` + TeamID types.String `tfsdk:"team_id"` + ProjectID types.String `tfsdk:"project_id"` + ID types.String `tfsdk:"id"` + Sensitive types.Bool `tfsdk:"sensitive"` +} + +func (e *ProjectEnvironmentVariable) toCreateEnvironmentVariableRequest() client.CreateEnvironmentVariableRequest { + target := []string{} + for _, t := range e.Target { + target = append(target, t.ValueString()) + } + var envVariableType string + + if e.Sensitive.ValueBool() { + envVariableType = "sensitive" + } else { + envVariableType = "encrypted" + } + + return client.CreateEnvironmentVariableRequest{ + EnvironmentVariable: client.EnvironmentVariableRequest{ + Key: e.Key.ValueString(), + Value: e.Value.ValueString(), + Target: target, + GitBranch: toStrPointer(e.GitBranch), + Type: envVariableType, + }, + ProjectID: e.ProjectID.ValueString(), + TeamID: e.TeamID.ValueString(), + } +} + +func (e *ProjectEnvironmentVariable) toUpdateEnvironmentVariableRequest() client.UpdateEnvironmentVariableRequest { + target := []string{} + for _, t := range e.Target { + target = append(target, t.ValueString()) + } + + var envVariableType string + + if e.Sensitive.ValueBool() { + envVariableType = "sensitive" + } else { + envVariableType = "encrypted" + } + + return client.UpdateEnvironmentVariableRequest{ + Value: e.Value.ValueString(), + Target: target, + GitBranch: toStrPointer(e.GitBranch), + Type: envVariableType, + ProjectID: e.ProjectID.ValueString(), + TeamID: e.TeamID.ValueString(), + EnvID: e.ID.ValueString(), + } +} + +// convertResponseToProjectEnvironmentVariable 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 convertResponseToProjectEnvironmentVariable(response client.EnvironmentVariable, projectID types.String, v types.String) ProjectEnvironmentVariable { + target := []types.String{} + for _, t := range response.Target { + target = append(target, types.StringValue(t)) + } + + value := types.StringValue(response.Value) + if response.Type == "sensitive" { + value = v + } + + return ProjectEnvironmentVariable{ + Target: target, + GitBranch: fromStringPointer(response.GitBranch), + Key: types.StringValue(response.Key), + Value: value, + TeamID: toTeamID(response.TeamID), + ProjectID: projectID, + ID: types.StringValue(response.ID), + Sensitive: types.BoolValue(response.Type == "sensitive"), + } +} + // Create will create a new project environment variable for a Vercel project. // This is called automatically by the provider when a new resource should be created. func (r *projectEnvironmentVariableResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { diff --git a/vercel/resource_project_environment_variable_model.go b/vercel/resource_project_environment_variable_model.go deleted file mode 100644 index ebe65bdd..00000000 --- a/vercel/resource_project_environment_variable_model.go +++ /dev/null @@ -1,95 +0,0 @@ -package vercel - -import ( - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/vercel/terraform-provider-vercel/client" -) - -// ProjectEnvironmentVariable reflects the state terraform stores internally for a project environment variable. -type ProjectEnvironmentVariable struct { - Target []types.String `tfsdk:"target"` - GitBranch types.String `tfsdk:"git_branch"` - Key types.String `tfsdk:"key"` - Value types.String `tfsdk:"value"` - TeamID types.String `tfsdk:"team_id"` - ProjectID types.String `tfsdk:"project_id"` - ID types.String `tfsdk:"id"` - Sensitive types.Bool `tfsdk:"sensitive"` -} - -func (e *ProjectEnvironmentVariable) toCreateEnvironmentVariableRequest() client.CreateEnvironmentVariableRequest { - target := []string{} - for _, t := range e.Target { - target = append(target, t.ValueString()) - } - var envVariableType string - - if e.Sensitive.ValueBool() { - envVariableType = "sensitive" - } else { - envVariableType = "encrypted" - } - - return client.CreateEnvironmentVariableRequest{ - EnvironmentVariable: client.EnvironmentVariableRequest{ - Key: e.Key.ValueString(), - Value: e.Value.ValueString(), - Target: target, - GitBranch: toStrPointer(e.GitBranch), - Type: envVariableType, - }, - ProjectID: e.ProjectID.ValueString(), - TeamID: e.TeamID.ValueString(), - } -} - -func (e *ProjectEnvironmentVariable) toUpdateEnvironmentVariableRequest() client.UpdateEnvironmentVariableRequest { - target := []string{} - for _, t := range e.Target { - target = append(target, t.ValueString()) - } - - var envVariableType string - - if e.Sensitive.ValueBool() { - envVariableType = "sensitive" - } else { - envVariableType = "encrypted" - } - - return client.UpdateEnvironmentVariableRequest{ - Value: e.Value.ValueString(), - Target: target, - GitBranch: toStrPointer(e.GitBranch), - Type: envVariableType, - ProjectID: e.ProjectID.ValueString(), - TeamID: e.TeamID.ValueString(), - EnvID: e.ID.ValueString(), - } -} - -// convertResponseToProjectEnvironmentVariable 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 convertResponseToProjectEnvironmentVariable(response client.EnvironmentVariable, projectID types.String, v types.String) ProjectEnvironmentVariable { - target := []types.String{} - for _, t := range response.Target { - target = append(target, types.StringValue(t)) - } - - value := types.StringValue(response.Value) - if response.Type == "sensitive" { - value = v - } - - return ProjectEnvironmentVariable{ - Target: target, - GitBranch: fromStringPointer(response.GitBranch), - Key: types.StringValue(response.Key), - Value: value, - TeamID: toTeamID(response.TeamID), - ProjectID: projectID, - ID: types.StringValue(response.ID), - Sensitive: types.BoolValue(response.Type == "sensitive"), - } -} diff --git a/vercel/resource_project_model.go b/vercel/resource_project_model.go deleted file mode 100644 index 8cd0c9b6..00000000 --- a/vercel/resource_project_model.go +++ /dev/null @@ -1,478 +0,0 @@ -package vercel - -import ( - "context" - "fmt" - - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/vercel/terraform-provider-vercel/client" -) - -// Project reflects the state terraform stores internally for a project. -type Project struct { - BuildCommand types.String `tfsdk:"build_command"` - DevCommand types.String `tfsdk:"dev_command"` - Environment types.Set `tfsdk:"environment"` - Framework types.String `tfsdk:"framework"` - GitRepository *GitRepository `tfsdk:"git_repository"` - ID types.String `tfsdk:"id"` - IgnoreCommand types.String `tfsdk:"ignore_command"` - InstallCommand types.String `tfsdk:"install_command"` - Name types.String `tfsdk:"name"` - OutputDirectory types.String `tfsdk:"output_directory"` - PublicSource types.Bool `tfsdk:"public_source"` - RootDirectory types.String `tfsdk:"root_directory"` - ServerlessFunctionRegion types.String `tfsdk:"serverless_function_region"` - TeamID types.String `tfsdk:"team_id"` - VercelAuthentication *VercelAuthentication `tfsdk:"vercel_authentication"` - PasswordProtection *PasswordProtectionWithPassword `tfsdk:"password_protection"` - TrustedIps *TrustedIps `tfsdk:"trusted_ips"` - ProtectionBypassForAutomation types.Bool `tfsdk:"protection_bypass_for_automation"` - ProtectionBypassForAutomationSecret types.String `tfsdk:"protection_bypass_for_automation_secret"` - AutoExposeSystemEnvVars types.Bool `tfsdk:"automatically_expose_system_environment_variables"` -} - -var nullProject = Project{ - /* As this is read only, none of these fields are specified - so treat them all as Null */ - BuildCommand: types.StringNull(), - DevCommand: types.StringNull(), - InstallCommand: types.StringNull(), - OutputDirectory: types.StringNull(), - PublicSource: types.BoolNull(), - Environment: types.SetNull(envVariableElemType), -} - -func (p *Project) environment(ctx context.Context) ([]EnvironmentItem, error) { - if p.Environment.IsNull() { - return nil, nil - } - - var vars []EnvironmentItem - err := p.Environment.ElementsAs(ctx, &vars, true) - if err != nil { - return nil, fmt.Errorf("error reading project environment variables: %s", err) - } - return vars, nil -} - -func parseEnvironment(vars []EnvironmentItem) []client.EnvironmentVariable { - out := []client.EnvironmentVariable{} - for _, e := range vars { - target := []string{} - for _, t := range e.Target { - target = append(target, t.ValueString()) - } - - var envVariableType string - - if e.Sensitive.ValueBool() { - envVariableType = "sensitive" - } else { - envVariableType = "encrypted" - } - - out = append(out, client.EnvironmentVariable{ - Key: e.Key.ValueString(), - Value: e.Value.ValueString(), - Target: target, - GitBranch: toStrPointer(e.GitBranch), - Type: envVariableType, - ID: e.ID.ValueString(), - }) - } - return out -} - -func (p *Project) toCreateProjectRequest(envs []EnvironmentItem) client.CreateProjectRequest { - return client.CreateProjectRequest{ - BuildCommand: toStrPointer(p.BuildCommand), - CommandForIgnoringBuildStep: toStrPointer(p.IgnoreCommand), - DevCommand: toStrPointer(p.DevCommand), - EnvironmentVariables: parseEnvironment(envs), - Framework: toStrPointer(p.Framework), - GitRepository: p.GitRepository.toCreateProjectRequest(), - InstallCommand: toStrPointer(p.InstallCommand), - Name: p.Name.ValueString(), - OutputDirectory: toStrPointer(p.OutputDirectory), - PublicSource: toBoolPointer(p.PublicSource), - RootDirectory: toStrPointer(p.RootDirectory), - ServerlessFunctionRegion: toStrPointer(p.ServerlessFunctionRegion), - } -} - -func (p *Project) toUpdateProjectRequest(oldName string) client.UpdateProjectRequest { - var name *string = nil - if oldName != p.Name.ValueString() { - n := p.Name.ValueString() - name = &n - } - return client.UpdateProjectRequest{ - BuildCommand: toStrPointer(p.BuildCommand), - CommandForIgnoringBuildStep: toStrPointer(p.IgnoreCommand), - DevCommand: toStrPointer(p.DevCommand), - Framework: toStrPointer(p.Framework), - InstallCommand: toStrPointer(p.InstallCommand), - Name: name, - OutputDirectory: toStrPointer(p.OutputDirectory), - PublicSource: toBoolPointer(p.PublicSource), - RootDirectory: toStrPointer(p.RootDirectory), - ServerlessFunctionRegion: toStrPointer(p.ServerlessFunctionRegion), - PasswordProtection: p.PasswordProtection.toUpdateProjectRequest(), - VercelAuthentication: p.VercelAuthentication.toUpdateProjectRequest(), - TrustedIps: p.TrustedIps.toUpdateProjectRequest(), - AutoExposeSystemEnvVars: toBoolPointer(p.AutoExposeSystemEnvVars), - } -} - -// EnvironmentItem reflects the state terraform stores internally for a project's environment variable. -type EnvironmentItem struct { - Target []types.String `tfsdk:"target"` - GitBranch types.String `tfsdk:"git_branch"` - Key types.String `tfsdk:"key"` - Value types.String `tfsdk:"value"` - ID types.String `tfsdk:"id"` - Sensitive types.Bool `tfsdk:"sensitive"` -} - -func (e *EnvironmentItem) toEnvironmentVariableRequest() client.EnvironmentVariableRequest { - target := []string{} - for _, t := range e.Target { - target = append(target, t.ValueString()) - } - - var envVariableType string - - if e.Sensitive.ValueBool() { - envVariableType = "sensitive" - } else { - envVariableType = "encrypted" - } - - return client.EnvironmentVariableRequest{ - Key: e.Key.ValueString(), - Value: e.Value.ValueString(), - Target: target, - GitBranch: toStrPointer(e.GitBranch), - Type: envVariableType, - } -} - -// GitRepository reflects the state terraform stores internally for a nested git_repository block on a project resource. -type GitRepository struct { - Type types.String `tfsdk:"type"` - Repo types.String `tfsdk:"repo"` - ProductionBranch types.String `tfsdk:"production_branch"` -} - -func (g *GitRepository) toCreateProjectRequest() *client.GitRepository { - if g == nil { - return nil - } - return &client.GitRepository{ - Type: g.Type.ValueString(), - Repo: g.Repo.ValueString(), - } -} - -func toApiDeploymentProtectionType(dt types.String) string { - switch dt { - case types.StringValue("standard_protection"): - return "prod_deployment_urls_and_all_previews" - case types.StringValue("all_deployments"): - return "all" - case types.StringValue("only_preview_deployments"): - return "preview" - case types.StringValue("only_production_deployments"): - return "production" - default: - return dt.ValueString() - } -} - -func fromApiDeploymentProtectionType(dt string) types.String { - switch dt { - case "prod_deployment_urls_and_all_previews": - return types.StringValue("standard_protection") - case "all": - return types.StringValue("all_deployments") - case "preview": - return types.StringValue("only_preview_deployments") - case "production": - return types.StringValue("only_production_deployments") - default: - return types.StringValue(dt) - } -} - -func (v *VercelAuthentication) toUpdateProjectRequest() *client.VercelAuthentication { - if v == nil { - return nil - } - - return &client.VercelAuthentication{ - DeploymentType: toApiDeploymentProtectionType(v.DeploymentType), - } -} - -func (p *PasswordProtectionWithPassword) toUpdateProjectRequest() *client.PasswordProtectionWithPassword { - if p == nil { - return nil - } - - return &client.PasswordProtectionWithPassword{ - DeploymentType: toApiDeploymentProtectionType(p.DeploymentType), - Password: p.Password.ValueString(), - } -} - -func toApiTrustedIpProtectionMode(dt types.String) string { - switch dt { - case types.StringValue("trusted_ip_required"): - return "additional" - case types.StringValue("trusted_ip_optional"): - return "exclusive" - default: - return dt.ValueString() - } -} - -func fromApiTrustedIpProtectionMode(dt string) types.String { - switch dt { - case "additional": - return types.StringValue("trusted_ip_required") - case "exclusive": - return types.StringValue("trusted_ip_optional") - default: - return types.StringValue(dt) - } -} - -func (t *TrustedIps) toUpdateProjectRequest() *client.TrustedIps { - if t == nil { - return nil - } - - var addresses = []client.TrustedIpAddress{} - for _, address := range t.Addresses { - addresses = append(addresses, client.TrustedIpAddress{ - Value: address.Value.ValueString(), - Note: address.Note.ValueString(), - }) - } - - return &client.TrustedIps{ - Addresses: addresses, - DeploymentType: toApiDeploymentProtectionType(t.DeploymentType), - ProtectionMode: toApiTrustedIpProtectionMode(t.ProtectionMode), - } -} - -/* -* In the Vercel API the following fields are coerced to null during project creation - -* This causes an issue when they are specified, but falsy, as the -* terraform configuration explicitly sets a value for them, but the Vercel -* API returns a different value. This causes an inconsistent plan error. - -* We avoid this issue by choosing to use values from the terraform state, -* but only if they are _explicitly stated_ *and* they are _falsy_ values -* *and* the response value was null. This is important as drift detection -* would fail to work if the value was always selected, so this is as stringent -* as possible to allow drift-detection in the majority of scenarios. - -* This is implemented in the below uncoerceString and uncoerceBool functions. - */ -type projectCoercedFields struct { - BuildCommand types.String - DevCommand types.String - InstallCommand types.String - OutputDirectory types.String - PublicSource types.Bool -} - -func (p *Project) coercedFields() projectCoercedFields { - return projectCoercedFields{ - BuildCommand: p.BuildCommand, - DevCommand: p.DevCommand, - InstallCommand: p.InstallCommand, - OutputDirectory: p.OutputDirectory, - PublicSource: p.PublicSource, - } -} - -func uncoerceString(plan, res types.String) types.String { - if plan.ValueString() == "" && !plan.IsNull() && res.IsNull() { - return plan - } - return res -} -func uncoerceBool(plan, res types.Bool) types.Bool { - if !plan.ValueBool() && !plan.IsNull() && res.IsNull() { - return plan - } - return res -} - -var envVariableElemType = types.ObjectType{ - AttrTypes: map[string]attr.Type{ - "key": types.StringType, - "value": types.StringType, - "target": types.SetType{ - ElemType: types.StringType, - }, - "git_branch": types.StringType, - "id": types.StringType, - "sensitive": types.BoolType, - }, -} - -func hasSameTarget(p EnvironmentItem, target []string) bool { - if len(p.Target) != len(target) { - return false - } - for _, t := range p.Target { - v := t.ValueString() - if !contains(target, v) { - return false - } - } - return true -} - -func convertResponseToProject(ctx context.Context, response client.ProjectResponse, plan Project) (Project, error) { - fields := plan.coercedFields() - - var gr *GitRepository - if repo := response.Repository(); repo != nil { - gr = &GitRepository{ - Type: types.StringValue(repo.Type), - Repo: types.StringValue(repo.Repo), - ProductionBranch: types.StringNull(), - } - if repo.ProductionBranch != nil { - gr.ProductionBranch = types.StringValue(*repo.ProductionBranch) - } - } - - var pp *PasswordProtectionWithPassword - if response.PasswordProtection != nil { - pass := types.StringValue("") - if plan.PasswordProtection != nil { - pass = plan.PasswordProtection.Password - } - pp = &PasswordProtectionWithPassword{ - Password: pass, - DeploymentType: fromApiDeploymentProtectionType(response.PasswordProtection.DeploymentType), - } - } - - var va = &VercelAuthentication{ - DeploymentType: types.StringValue("none"), - } - if response.VercelAuthentication != nil { - va = &VercelAuthentication{ - DeploymentType: fromApiDeploymentProtectionType(response.VercelAuthentication.DeploymentType), - } - } - - var tip *TrustedIps - if response.TrustedIps != nil { - var addresses []TrustedIpAddress - for _, address := range response.TrustedIps.Addresses { - addresses = append(addresses, TrustedIpAddress{ - Value: types.StringValue(address.Value), - Note: types.StringValue(address.Note), - }) - } - tip = &TrustedIps{ - DeploymentType: fromApiDeploymentProtectionType(response.TrustedIps.DeploymentType), - Addresses: addresses, - ProtectionMode: fromApiTrustedIpProtectionMode(response.TrustedIps.ProtectionMode), - } - } - - var env []attr.Value - for _, e := range response.EnvironmentVariables { - target := []attr.Value{} - for _, t := range e.Target { - target = append(target, types.StringValue(t)) - } - value := types.StringValue(e.Value) - if e.Type == "sensitive" { - value = types.StringNull() - environment, err := plan.environment(ctx) - if err != nil { - return Project{}, fmt.Errorf("error reading project environment variables: %s", err) - } - for _, p := range environment { - if p.Sensitive.ValueBool() && p.Key.ValueString() == e.Key && hasSameTarget(p, e.Target) { - value = p.Value - break - } - } - } - - env = append(env, types.ObjectValueMust( - map[string]attr.Type{ - "key": types.StringType, - "value": types.StringType, - "target": types.SetType{ - ElemType: types.StringType, - }, - "git_branch": types.StringType, - "id": types.StringType, - "sensitive": types.BoolType, - }, - map[string]attr.Value{ - "key": types.StringValue(e.Key), - "value": value, - "target": types.SetValueMust(types.StringType, target), - "git_branch": fromStringPointer(e.GitBranch), - "id": types.StringValue(e.ID), - "sensitive": types.BoolValue(e.Type == "sensitive"), - }, - )) - } - - protectionBypassSecret := types.StringNull() - protectionBypass := types.BoolNull() - for k, v := range response.ProtectionBypass { - if v.Scope == "automation-bypass" { - protectionBypass = types.BoolValue(true) - protectionBypassSecret = types.StringValue(k) - break - } - } - if !plan.ProtectionBypassForAutomation.IsNull() && !plan.ProtectionBypassForAutomation.ValueBool() { - protectionBypass = types.BoolValue(false) - } - - environmentEntry := types.SetValueMust(envVariableElemType, env) - if len(response.EnvironmentVariables) == 0 && plan.Environment.IsNull() { - environmentEntry = types.SetNull(envVariableElemType) - } - - return Project{ - BuildCommand: uncoerceString(fields.BuildCommand, fromStringPointer(response.BuildCommand)), - DevCommand: uncoerceString(fields.DevCommand, fromStringPointer(response.DevCommand)), - Environment: environmentEntry, - Framework: fromStringPointer(response.Framework), - GitRepository: gr, - ID: types.StringValue(response.ID), - IgnoreCommand: fromStringPointer(response.CommandForIgnoringBuildStep), - InstallCommand: uncoerceString(fields.InstallCommand, fromStringPointer(response.InstallCommand)), - Name: types.StringValue(response.Name), - OutputDirectory: uncoerceString(fields.OutputDirectory, fromStringPointer(response.OutputDirectory)), - PublicSource: uncoerceBool(fields.PublicSource, fromBoolPointer(response.PublicSource)), - RootDirectory: fromStringPointer(response.RootDirectory), - ServerlessFunctionRegion: fromStringPointer(response.ServerlessFunctionRegion), - TeamID: toTeamID(response.TeamID), - PasswordProtection: pp, - VercelAuthentication: va, - TrustedIps: tip, - ProtectionBypassForAutomation: protectionBypass, - ProtectionBypassForAutomationSecret: protectionBypassSecret, - AutoExposeSystemEnvVars: fromBoolPointer(response.AutoExposeSystemEnvVars), - }, nil -} diff --git a/vercel/resource_shared_environment_variable.go b/vercel/resource_shared_environment_variable.go index 03425ffa..1606cb62 100644 --- a/vercel/resource_shared_environment_variable.go +++ b/vercel/resource_shared_environment_variable.go @@ -5,6 +5,8 @@ import ( "fmt" "strings" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier" @@ -107,6 +109,117 @@ For more detailed information, please see the [Vercel documentation](https://ver } } +// SharedEnvironmentVariable reflects the state terraform stores internally for a project environment variable. +type SharedEnvironmentVariable struct { + Target types.Set `tfsdk:"target"` + Key types.String `tfsdk:"key"` + Value types.String `tfsdk:"value"` + TeamID types.String `tfsdk:"team_id"` + ProjectIDs types.Set `tfsdk:"project_ids"` + ID types.String `tfsdk:"id"` + Sensitive types.Bool `tfsdk:"sensitive"` +} + +func (e *SharedEnvironmentVariable) toCreateSharedEnvironmentVariableRequest(ctx context.Context, diags diag.Diagnostics) (req client.CreateSharedEnvironmentVariableRequest, ok bool) { + var target []string + ds := e.Target.ElementsAs(ctx, &target, false) + diags = append(diags, ds...) + if diags.HasError() { + return req, false + } + + var projectIDs []string + ds = e.ProjectIDs.ElementsAs(ctx, &projectIDs, false) + diags = append(diags, ds...) + if diags.HasError() { + return req, false + } + + var envVariableType string + + if e.Sensitive.ValueBool() { + envVariableType = "sensitive" + } else { + envVariableType = "encrypted" + } + + return client.CreateSharedEnvironmentVariableRequest{ + EnvironmentVariable: client.SharedEnvironmentVariableRequest{ + Target: target, + Type: envVariableType, + ProjectIDs: projectIDs, + EnvironmentVariables: []client.SharedEnvVarRequest{ + { + Key: e.Key.ValueString(), + Value: e.Value.ValueString(), + }, + }, + }, + TeamID: e.TeamID.ValueString(), + }, true +} + +func (e *SharedEnvironmentVariable) toUpdateSharedEnvironmentVariableRequest(ctx context.Context, diags diag.Diagnostics) (req client.UpdateSharedEnvironmentVariableRequest, ok bool) { + var target []string + ds := e.Target.ElementsAs(ctx, &target, false) + diags = append(diags, ds...) + if diags.HasError() { + return req, false + } + + var projectIDs []string + ds = e.ProjectIDs.ElementsAs(ctx, &projectIDs, false) + diags = append(diags, ds...) + if diags.HasError() { + return req, false + } + var envVariableType string + + if e.Sensitive.ValueBool() { + envVariableType = "sensitive" + } else { + envVariableType = "encrypted" + } + return client.UpdateSharedEnvironmentVariableRequest{ + Value: e.Value.ValueString(), + Target: target, + Type: envVariableType, + TeamID: e.TeamID.ValueString(), + EnvID: e.ID.ValueString(), + ProjectIDs: projectIDs, + }, true +} + +// convertResponseToSharedEnvironmentVariable 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 convertResponseToSharedEnvironmentVariable(response client.SharedEnvironmentVariableResponse, v types.String) SharedEnvironmentVariable { + target := []attr.Value{} + for _, t := range response.Target { + target = append(target, types.StringValue(t)) + } + + projectIDs := []attr.Value{} + for _, t := range response.ProjectIDs { + projectIDs = append(projectIDs, types.StringValue(t)) + } + + value := types.StringValue(response.Value) + if response.Type == "sensitive" { + value = v + } + + return SharedEnvironmentVariable{ + Target: types.SetValueMust(types.StringType, target), + Key: types.StringValue(response.Key), + Value: value, + ProjectIDs: types.SetValueMust(types.StringType, projectIDs), + TeamID: toTeamID(response.TeamID), + ID: types.StringValue(response.ID), + Sensitive: types.BoolValue(response.Type == "sensitive"), + } +} + // Create will create a new shared environment variable. // This is called automatically by the provider when a new resource should be created. func (r *sharedEnvironmentVariableResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { diff --git a/vercel/resource_shared_environment_variable_model.go b/vercel/resource_shared_environment_variable_model.go deleted file mode 100644 index 4e429199..00000000 --- a/vercel/resource_shared_environment_variable_model.go +++ /dev/null @@ -1,121 +0,0 @@ -package vercel - -import ( - "context" - - "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/vercel/terraform-provider-vercel/client" -) - -// SharedEnvironmentVariable reflects the state terraform stores internally for a project environment variable. -type SharedEnvironmentVariable struct { - Target types.Set `tfsdk:"target"` - Key types.String `tfsdk:"key"` - Value types.String `tfsdk:"value"` - TeamID types.String `tfsdk:"team_id"` - ProjectIDs types.Set `tfsdk:"project_ids"` - ID types.String `tfsdk:"id"` - Sensitive types.Bool `tfsdk:"sensitive"` -} - -func (e *SharedEnvironmentVariable) toCreateSharedEnvironmentVariableRequest(ctx context.Context, diags diag.Diagnostics) (req client.CreateSharedEnvironmentVariableRequest, ok bool) { - var target []string - ds := e.Target.ElementsAs(ctx, &target, false) - diags = append(diags, ds...) - if diags.HasError() { - return req, false - } - - var projectIDs []string - ds = e.ProjectIDs.ElementsAs(ctx, &projectIDs, false) - diags = append(diags, ds...) - if diags.HasError() { - return req, false - } - - var envVariableType string - - if e.Sensitive.ValueBool() { - envVariableType = "sensitive" - } else { - envVariableType = "encrypted" - } - - return client.CreateSharedEnvironmentVariableRequest{ - EnvironmentVariable: client.SharedEnvironmentVariableRequest{ - Target: target, - Type: envVariableType, - ProjectIDs: projectIDs, - EnvironmentVariables: []client.SharedEnvVarRequest{ - { - Key: e.Key.ValueString(), - Value: e.Value.ValueString(), - }, - }, - }, - TeamID: e.TeamID.ValueString(), - }, true -} - -func (e *SharedEnvironmentVariable) toUpdateSharedEnvironmentVariableRequest(ctx context.Context, diags diag.Diagnostics) (req client.UpdateSharedEnvironmentVariableRequest, ok bool) { - var target []string - ds := e.Target.ElementsAs(ctx, &target, false) - diags = append(diags, ds...) - if diags.HasError() { - return req, false - } - - var projectIDs []string - ds = e.ProjectIDs.ElementsAs(ctx, &projectIDs, false) - diags = append(diags, ds...) - if diags.HasError() { - return req, false - } - var envVariableType string - - if e.Sensitive.ValueBool() { - envVariableType = "sensitive" - } else { - envVariableType = "encrypted" - } - return client.UpdateSharedEnvironmentVariableRequest{ - Value: e.Value.ValueString(), - Target: target, - Type: envVariableType, - TeamID: e.TeamID.ValueString(), - EnvID: e.ID.ValueString(), - ProjectIDs: projectIDs, - }, true -} - -// convertResponseToSharedEnvironmentVariable 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 convertResponseToSharedEnvironmentVariable(response client.SharedEnvironmentVariableResponse, v types.String) SharedEnvironmentVariable { - target := []attr.Value{} - for _, t := range response.Target { - target = append(target, types.StringValue(t)) - } - - projectIDs := []attr.Value{} - for _, t := range response.ProjectIDs { - projectIDs = append(projectIDs, types.StringValue(t)) - } - - value := types.StringValue(response.Value) - if response.Type == "sensitive" { - value = v - } - - return SharedEnvironmentVariable{ - Target: types.SetValueMust(types.StringType, target), - Key: types.StringValue(response.Key), - Value: value, - ProjectIDs: types.SetValueMust(types.StringType, projectIDs), - TeamID: toTeamID(response.TeamID), - ID: types.StringValue(response.ID), - Sensitive: types.BoolValue(response.Type == "sensitive"), - } -}