diff --git a/client/client.go b/client/client.go index eeaf4f4d..710d3915 100644 --- a/client/client.go +++ b/client/client.go @@ -5,6 +5,7 @@ import ( "time" ) +// Client is an API wrapper, providing a high-level interface to the Vercel API. type Client struct { token string client *http.Client @@ -23,6 +24,7 @@ func (c *Client) http() *http.Client { return c.client } +// New creates a new instace of Client for a given API token. func New(token string) *Client { return &Client{ token: token, diff --git a/client/deployment_create.go b/client/deployment_create.go index 197ab75a..2b42eb51 100644 --- a/client/deployment_create.go +++ b/client/deployment_create.go @@ -13,12 +13,15 @@ import ( "github.com/hashicorp/terraform-plugin-log/tflog" ) +// DeploymentFile is a struct defining the required information about a singular file +// that should be used within a deployment. type DeploymentFile struct { File string `json:"file"` Sha string `json:"sha"` Size int `json:"size"` } +// CreateDeploymentRequest defines the request the Vercel API expects in order to create a deployment. type CreateDeploymentRequest struct { Files []DeploymentFile `json:"files,omitempty"` Functions map[string]interface{} `json:"functions,omitempty"` @@ -34,6 +37,7 @@ type CreateDeploymentRequest struct { Target string `json:"target,omitempty"` } +// DeploymentResponse defines the response the Vercel API returns when a deployment is created or updated. type DeploymentResponse struct { Aliases []string `json:"alias"` AliasError *struct { @@ -66,10 +70,12 @@ type DeploymentResponse struct { URL string `json:"url"` } +// IsComplete is used to determine whether a deployment is still processing, or whether it is fully done. func (dr *DeploymentResponse) IsComplete() bool { return dr.AliasAssigned && dr.AliasError == nil } +// DeploymentLogsURL provides a user friendly URL that links directly to the vercel UI for a particular deployment. func (dr *DeploymentResponse) DeploymentLogsURL(projectID string) string { teamSlug := dr.Creator.Username if dr.Team != nil { @@ -83,6 +89,7 @@ func (dr *DeploymentResponse) DeploymentLogsURL(projectID string) string { ) } +// CheckForError checks through the various failure modes of a deployment to see if any were hit. func (dr *DeploymentResponse) CheckForError(projectID string) error { if dr.ReadyState == "CANCELED" { return fmt.Errorf("deployment canceled") @@ -116,16 +123,20 @@ func (dr *DeploymentResponse) CheckForError(projectID string) error { return nil } +// MissingFilesError is a sentinel error that indicates a deployment could not be created +// because additional files need to be uploaded first. type MissingFilesError struct { Code string `json:"code"` Message string `json:"message"` Missing []string `json:"missing"` } +// Error gives the MissingFilesError a user friendly error message. func (e MissingFilesError) Error() string { return fmt.Sprintf("%s - %s", e.Code, e.Message) } +// CreateDeployment creates a deployment within Vercel. func (c *Client) CreateDeployment(ctx context.Context, request CreateDeploymentRequest, teamID string) (r DeploymentResponse, err error) { request.Name = request.ProjectID // Name is ignored if project is specified request.Build.Environment = request.Environment // Ensure they are both the same, as project environment variables are diff --git a/client/deployment_get.go b/client/deployment_get.go index 8078705a..dff6c90c 100644 --- a/client/deployment_get.go +++ b/client/deployment_get.go @@ -7,6 +7,7 @@ import ( "strings" ) +// 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 teamID != "" { diff --git a/client/environment_variable_upsert.go b/client/environment_variable_upsert.go index 5456c489..ac0451f3 100644 --- a/client/environment_variable_upsert.go +++ b/client/environment_variable_upsert.go @@ -7,8 +7,12 @@ import ( "strings" ) +// UpsertEnvironmentVariableRequest defines the information that needs to be passed to Vercel in order to +// create or update an environment variable. type UpsertEnvironmentVariableRequest EnvironmentVariable +// UpsertEnvironmentVariable will either create a brand new environment variable if one does not exist, or will +// update an existing environment variable to the latest information. func (c *Client) UpsertEnvironmentVariable(ctx context.Context, projectID, teamID string, request UpsertEnvironmentVariableRequest) error { url := fmt.Sprintf("%s/v8/projects/%s/env", c.baseURL, projectID) if teamID != "" { diff --git a/client/environment_variables_delete.go b/client/environment_variables_delete.go index 38daebc4..a4f8bfe2 100644 --- a/client/environment_variables_delete.go +++ b/client/environment_variables_delete.go @@ -7,6 +7,7 @@ import ( "strings" ) +// 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 teamID != "" { diff --git a/client/file_create.go b/client/file_create.go index 8655ba9a..1cc8d5ba 100644 --- a/client/file_create.go +++ b/client/file_create.go @@ -7,6 +7,7 @@ import ( "strings" ) +// CreateFile will upload a file to Vercel so that it can be later used for a Deployment. func (c *Client) CreateFile(ctx context.Context, filename, sha, content string) error { req, err := http.NewRequestWithContext( ctx, diff --git a/client/must_marshal.go b/client/must_marshal.go index 187298e3..27f85dec 100644 --- a/client/must_marshal.go +++ b/client/must_marshal.go @@ -2,6 +2,11 @@ package client import "encoding/json" +// mustMarshal is a helper to remove unnecessary error checking when marshaling a Go +// struct to json. There are only a few instances where marshaling can fail, and they +// are around the shape of the data. e.g. if a struct contains a channel, then it cannot +// be marshaled. As our structs are known ahead of time and are all safe to marshal, +// this simplifies the error checking process. func mustMarshal(v interface{}) []byte { res, _ := json.Marshal(v) return res diff --git a/client/project_create.go b/client/project_create.go index 0470e3bf..33d03f25 100644 --- a/client/project_create.go +++ b/client/project_create.go @@ -7,11 +7,15 @@ import ( "strings" ) +// 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"` @@ -20,6 +24,7 @@ type EnvironmentVariable struct { ID string `json:"id,omitempty"` } +// CreateProjectRequest defines the information necessary to create a project. type CreateProjectRequest struct { Name string `json:"name"` BuildCommand *string `json:"buildCommand"` @@ -33,6 +38,7 @@ type CreateProjectRequest struct { RootDirectory *string `json:"rootDirectory"` } +// 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 teamID != "" { diff --git a/client/project_delete.go b/client/project_delete.go index 9370e149..90490fab 100644 --- a/client/project_delete.go +++ b/client/project_delete.go @@ -7,6 +7,8 @@ import ( "strings" ) +// 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 teamID != "" { diff --git a/client/project_domain_create.go b/client/project_domain_create.go index 68c77364..d68c09ad 100644 --- a/client/project_domain_create.go +++ b/client/project_domain_create.go @@ -7,6 +7,10 @@ import ( "strings" ) +// 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"` @@ -14,6 +18,7 @@ type CreateProjectDomainRequest struct { 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/v8/projects/%s/domains", c.baseURL, projectID) if teamID != "" { diff --git a/client/project_domain_delete.go b/client/project_domain_delete.go index 8dbb7c5a..229a77dd 100644 --- a/client/project_domain_delete.go +++ b/client/project_domain_delete.go @@ -7,6 +7,7 @@ import ( "strings" ) +// 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 teamID != "" { diff --git a/client/project_domain_get.go b/client/project_domain_get.go index 834992c8..439f4616 100644 --- a/client/project_domain_get.go +++ b/client/project_domain_get.go @@ -7,6 +7,8 @@ import ( "strings" ) +// 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"` @@ -15,6 +17,7 @@ type ProjectDomainResponse struct { 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 teamID != "" { diff --git a/client/project_domain_update.go b/client/project_domain_update.go index 749ef6aa..754f47de 100644 --- a/client/project_domain_update.go +++ b/client/project_domain_update.go @@ -7,12 +7,14 @@ import ( "strings" ) +// 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 teamID != "" { diff --git a/client/project_get.go b/client/project_get.go index 4e039c65..e6c35b1f 100644 --- a/client/project_get.go +++ b/client/project_get.go @@ -7,11 +7,14 @@ import ( "strings" ) +// Repository defines the information about a projects git connection. type Repository struct { Type string Repo string } +// 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 @@ -36,6 +39,7 @@ func (r *ProjectResponse) Repository() *Repository { return nil } +// ProjectResponse defines the information vercel returns about a project. type ProjectResponse struct { BuildCommand *string `json:"buildCommand"` DevCommand *string `json:"devCommand"` @@ -61,6 +65,7 @@ type ProjectResponse struct { RootDirectory *string `json:"rootDirectory"` } +// GetProject retrieves information about an existing project from vercel. func (c *Client) GetProject(ctx context.Context, projectID, teamID string) (r ProjectResponse, err error) { url := fmt.Sprintf("%s/v8/projects/%s", c.baseURL, projectID) if teamID != "" { diff --git a/client/project_list.go b/client/project_list.go index 799b556a..c560fd43 100644 --- a/client/project_list.go +++ b/client/project_list.go @@ -7,6 +7,7 @@ import ( "strings" ) +// 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 teamID != "" { diff --git a/client/project_update.go b/client/project_update.go index e548178a..da2c4ce8 100644 --- a/client/project_update.go +++ b/client/project_update.go @@ -7,6 +7,12 @@ import ( "strings" ) +// 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 { Name *string `json:"name,omitempty"` BuildCommand *string `json:"buildCommand"` @@ -18,6 +24,7 @@ type UpdateProjectRequest struct { RootDirectory *string `json:"rootDirectory"` } +// UpdateProject updates an existing projects configuration within vercel. func (c *Client) UpdateProject(ctx context.Context, projectID, teamID string, request UpdateProjectRequest) (r ProjectResponse, err error) { url := fmt.Sprintf("%s/v8/projects/%s", c.baseURL, projectID) if teamID != "" { diff --git a/client/request.go b/client/request.go index b9918b5f..e678b1b6 100644 --- a/client/request.go +++ b/client/request.go @@ -7,6 +7,7 @@ import ( "net/http" ) +// APIError is an error type that exposes additional information about why an API request failed. type APIError struct { Code string `json:"code"` Message string `json:"message"` @@ -14,10 +15,16 @@ type APIError struct { RawMessage []byte } +// Error provides a user friendly error message. func (e APIError) Error() string { return fmt.Sprintf("%s - %s", e.Code, e.Message) } +// doRequest is a helper function for consistently requesting data from vercel. +// This manages: +// - Authorization via the Bearer token +// - Converting error responses into an inspectable type +// - Unmarshaling responses func (c *Client) doRequest(req *http.Request, v interface{}) error { req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", c.token)) resp, err := c.http().Do(req) diff --git a/client/team_create.go b/client/team_create.go index 2b7d5a39..6e72e349 100644 --- a/client/team_create.go +++ b/client/team_create.go @@ -6,15 +6,18 @@ import ( "strings" ) +// TeamCreateRequest defines the information needed to create a team within vercel. type TeamCreateRequest struct { Slug string `json:"slug"` Name string `json:"name"` } +// TeamResponse is the information returned by the vercel api when a team is created. type TeamResponse struct { ID string `json:"id"` } +// CreateTeam creates a team within vercel. func (c *Client) CreateTeam(ctx context.Context, request TeamCreateRequest) (r TeamResponse, err error) { req, err := http.NewRequestWithContext( ctx, diff --git a/client/team_delete.go b/client/team_delete.go index 1fea5808..add2b21d 100644 --- a/client/team_delete.go +++ b/client/team_delete.go @@ -7,6 +7,7 @@ import ( "strings" ) +// DeleteTeam deletes an existing team within vercel. func (c *Client) DeleteTeam(ctx context.Context, teamID string) error { req, err := http.NewRequestWithContext( ctx, diff --git a/client/team_get.go b/client/team_get.go index 81b0b10b..631cf4a1 100644 --- a/client/team_get.go +++ b/client/team_get.go @@ -7,6 +7,7 @@ import ( "strings" ) +// GetTeam returns information about an existing team within vercel. func (c *Client) GetTeam(ctx context.Context, teamID, slug string) (r TeamResponse, err error) { url := c.baseURL + "/v1/teams" if teamID != "" { diff --git a/glob/glob.go b/glob/glob.go index 2f6be510..26f3b183 100644 --- a/glob/glob.go +++ b/glob/glob.go @@ -8,6 +8,8 @@ import ( gitignore "github.com/sabhiram/go-gitignore" ) +// GetPaths is used to find all the files within a directory that do not match a specified +// set of ignore patterns. func GetPaths(basePath string, ignorePatterns []string) ([]string, error) { ignore := gitignore.CompileIgnoreLines(ignorePatterns...) diff --git a/glob/ignores.go b/glob/ignores.go index b4b3d032..63099eb8 100644 --- a/glob/ignores.go +++ b/glob/ignores.go @@ -39,6 +39,8 @@ var defaultIgnores = []string{ ".terraform*", } +// GetIgnores is used to parse a .vercelignore file from a given directory, and +// combine the expected results with a default set of ignored files. func GetIgnores(path string) ([]string, error) { ignoreFilePath := filepath.Join(path, ".vercelignore") ignoreFile, err := os.ReadFile(ignoreFilePath) diff --git a/vercel/data_source_file.go b/vercel/data_source_file.go index 602a0e49..723e2db9 100644 --- a/vercel/data_source_file.go +++ b/vercel/data_source_file.go @@ -14,6 +14,7 @@ import ( type dataSourceFileType struct{} +// GetSchema returns the schema information for a file data source func (r dataSourceFileType) GetSchema(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { return tfsdk.Schema{ Description: ` @@ -41,6 +42,7 @@ This will read a single file, providing metadata for use with a ` + "`vercel_dep }, nil } +// NewDataSource instantiates a new DataSource of this DataSourceType. func (r dataSourceFileType) NewDataSource(ctx context.Context, p tfsdk.Provider) (tfsdk.DataSource, diag.Diagnostics) { return dataSourceFile{ p: *(p.(*provider)), @@ -51,12 +53,15 @@ type dataSourceFile struct { p provider } +// FileData represents the information terraform knows about a File data source type FileData struct { Path types.String `tfsdk:"path"` ID types.String `tfsdk:"id"` File map[string]string `tfsdk:"file"` } +// Read will read a file from the filesytem and provide terraform with information about it. +// It is called by the provider whenever data source values should be read to update state. func (r dataSourceFile) Read(ctx context.Context, req tfsdk.ReadDataSourceRequest, resp *tfsdk.ReadDataSourceResponse) { var config FileData diags := req.Config.Get(ctx, &config) diff --git a/vercel/data_source_project.go b/vercel/data_source_project.go index d28f68f9..85c30184 100644 --- a/vercel/data_source_project.go +++ b/vercel/data_source_project.go @@ -13,6 +13,7 @@ import ( type dataSourceProjectType struct{} +// GetSchema returns the schema information for a project data source func (r dataSourceProjectType) GetSchema(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { return tfsdk.Schema{ Description: ` @@ -137,6 +138,7 @@ For more detailed information, please see the [Vercel documentation](https://ver }, nil } +// NewDataSource instantiates a new DataSource of this DataSourceType. func (r dataSourceProjectType) NewDataSource(ctx context.Context, p tfsdk.Provider) (tfsdk.DataSource, diag.Diagnostics) { return dataSourceProject{ p: *(p.(*provider)), @@ -147,6 +149,9 @@ type dataSourceProject struct { p provider } +// 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. func (r dataSourceProject) Read(ctx context.Context, req tfsdk.ReadDataSourceRequest, resp *tfsdk.ReadDataSourceResponse) { var config Project diags := req.Config.Get(ctx, &config) diff --git a/vercel/data_source_project_directory.go b/vercel/data_source_project_directory.go index 0bc33ac5..a3ee0fcb 100644 --- a/vercel/data_source_project_directory.go +++ b/vercel/data_source_project_directory.go @@ -15,6 +15,7 @@ import ( type dataSourceProjectDirectoryType struct{} +// GetSchema returns the schema information for a project directory data source func (r dataSourceProjectDirectoryType) GetSchema(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { return tfsdk.Schema{ Description: ` @@ -45,6 +46,7 @@ This will recursively read files, providing metadata for use with a ` + "`vercel }, nil } +// NewDataSource instantiates a new DataSource of this DataSourceType. func (r dataSourceProjectDirectoryType) NewDataSource(ctx context.Context, p tfsdk.Provider) (tfsdk.DataSource, diag.Diagnostics) { return dataSourceProjectDirectory{ p: *(p.(*provider)), @@ -55,12 +57,16 @@ type dataSourceProjectDirectory struct { p provider } +// ProjectDirectoryData represents the information terraform knows about a project directory data source type ProjectDirectoryData struct { Path types.String `tfsdk:"path"` ID types.String `tfsdk:"id"` Files map[string]string `tfsdk:"files"` } +// Read will recursively scan a directory looking for any files that do not match a .vercelignore file (if a +// .vercelignore is present). Metadata about all these files will then be made available to terraform. +// It is called by the provider whenever data source values should be read to update state. func (r dataSourceProjectDirectory) Read(ctx context.Context, req tfsdk.ReadDataSourceRequest, resp *tfsdk.ReadDataSourceResponse) { var config ProjectDirectoryData diags := req.Config.Get(ctx, &config) diff --git a/vercel/provider.go b/vercel/provider.go index 735dc405..f55cb5bb 100644 --- a/vercel/provider.go +++ b/vercel/provider.go @@ -16,10 +16,12 @@ type provider struct { client *client.Client } +// New instantiates a new instance of a vercel terraform provider. func New() tfsdk.Provider { return &provider{} } +// GetSchema returns the schema information for the provider configuration itself. func (p *provider) GetSchema(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { return tfsdk.Schema{ Description: ` @@ -39,6 +41,7 @@ Use the navigation to the left to read about the available resources. }, nil } +// GetResources shows the available resources for the vercel provider func (p *provider) GetResources(_ context.Context) (map[string]tfsdk.ResourceType, diag.Diagnostics) { return map[string]tfsdk.ResourceType{ "vercel_deployment": resourceDeploymentType{}, @@ -47,6 +50,7 @@ func (p *provider) GetResources(_ context.Context) (map[string]tfsdk.ResourceTyp }, nil } +// GetDataSources shows the available data sources for the vercel provider func (p *provider) GetDataSources(_ context.Context) (map[string]tfsdk.DataSourceType, diag.Diagnostics) { return map[string]tfsdk.DataSourceType{ "vercel_file": dataSourceFileType{}, @@ -59,8 +63,12 @@ type providerData struct { APIToken types.String `tfsdk:"api_token"` } +// apiTokenRe is a regex for an API access token. We use this to validate that the +// token provided matches the expected format. var apiTokenRe = regexp.MustCompile("[0-9a-zA-Z]{24}") +// Configure takes a provider and applies any configuration. In the context of Vercel +// this allows us to set up an API token. func (p *provider) Configure(ctx context.Context, req tfsdk.ConfigureProviderRequest, resp *tfsdk.ConfigureProviderResponse) { var config providerData diags := req.Config.Get(ctx, &config) diff --git a/vercel/resource_deployment.go b/vercel/resource_deployment.go index f2f8b738..98752f8e 100644 --- a/vercel/resource_deployment.go +++ b/vercel/resource_deployment.go @@ -15,6 +15,7 @@ import ( type resourceDeploymentType struct{} +// GetSchema returns the schema information for a deployment resource. func (r resourceDeploymentType) GetSchema(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { return tfsdk.Schema{ Description: ` @@ -126,6 +127,7 @@ Once the build step has completed successfully, a new, immutable deployment will }, nil } +// NewResource instantiates a new Resource of this ResourceType. func (r resourceDeploymentType) NewResource(_ context.Context, p tfsdk.Provider) (tfsdk.Resource, diag.Diagnostics) { return resourceDeployment{ p: *(p.(*provider)), @@ -136,6 +138,9 @@ type resourceDeployment struct { p provider } +// Create will create a deployment within Vercel. This is done by first attempting to trigger a deployment, seeing what +// files are required, uploading those files, and then attempting to create a deployment again. +// This is called automatically by the provider when a new resource should be created. func (r resourceDeployment) Create(ctx context.Context, req tfsdk.CreateResourceRequest, resp *tfsdk.CreateResourceResponse) { if !r.p.configured { resp.Diagnostics.AddError( @@ -243,6 +248,8 @@ func (r resourceDeployment) Create(ctx context.Context, req tfsdk.CreateResource } } +// Read will read a file from the filesytem and provide terraform with information about it. +// It is called by the provider whenever data source values should be read to update state. func (r resourceDeployment) Read(ctx context.Context, req tfsdk.ReadResourceRequest, resp *tfsdk.ReadResourceResponse) { var state Deployment diags := req.State.Get(ctx, &state) @@ -252,6 +259,11 @@ func (r resourceDeployment) Read(ctx context.Context, req tfsdk.ReadResourceRequ } out, err := r.p.client.GetDeployment(ctx, state.ID.Value, state.TeamID.Value) + var apiErr client.APIError + if err != nil && errors.As(err, &apiErr) && apiErr.StatusCode == 404 { + resp.State.RemoveResource(ctx) + return + } if err != nil { resp.Diagnostics.AddError( "Error reading deployment", @@ -274,15 +286,22 @@ func (r resourceDeployment) Read(ctx context.Context, req tfsdk.ReadResourceRequ } } +// Update is a noop as it is not possible to update an existing deployment. Instead, all +// attributes must be set to force recreation. +// This method has to exist, however, to satisfy the resource interface. func (r resourceDeployment) Update(ctx context.Context, req tfsdk.UpdateResourceRequest, resp *tfsdk.UpdateResourceResponse) { // Nothing to do here - we can't update deployments } +// Delete does nothing other than clear the existing terraform state for a Deployment. +// This is done intentionally, as typically, Vercel users do not continually delete old Deployments. func (r resourceDeployment) Delete(ctx context.Context, req tfsdk.DeleteResourceRequest, resp *tfsdk.DeleteResourceResponse) { tflog.Trace(ctx, "deleted deployment") resp.State.RemoveResource(ctx) } +// ImportState is not implemented as it is not possible to get all the required information for a +// Deployment resource from the vercel API. func (r resourceDeployment) ImportState(ctx context.Context, req tfsdk.ImportResourceStateRequest, resp *tfsdk.ImportResourceStateResponse) { tfsdk.ResourceImportStateNotImplemented(ctx, "", resp) } diff --git a/vercel/resource_deployment_model.go b/vercel/resource_deployment_model.go index adeddc6e..fbc76a11 100644 --- a/vercel/resource_deployment_model.go +++ b/vercel/resource_deployment_model.go @@ -10,6 +10,8 @@ import ( "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"` @@ -18,6 +20,7 @@ type ProjectSettings struct { 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"` @@ -30,6 +33,8 @@ type Deployment struct { URL types.String `tfsdk:"url"` } +// 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.Null { m[name] = nil @@ -39,8 +44,17 @@ func setIfNotUnknown(m map[string]interface{}, v types.String, name string) { } } +// 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 { @@ -63,6 +77,8 @@ func (p *ProjectSettings) toRequest() map[string]interface{} { 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 { return types.String{ Null: t.Null || t.Unknown, @@ -70,6 +86,8 @@ func fillStringNull(t types.String) types.String { } } +// 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 @@ -83,6 +101,9 @@ func (p *ProjectSettings) fillNulls() *ProjectSettings { } } +// 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 (d *Deployment) getFiles() ([]client.DeploymentFile, map[string]client.DeploymentFile, error) { var files []client.DeploymentFile filesBySha := map[string]client.DeploymentFile{} @@ -108,6 +129,9 @@ func (d *Deployment) getFiles() ([]client.DeploymentFile, map[string]client.Depl 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.Bool{Value: false} /* diff --git a/vercel/resource_project.go b/vercel/resource_project.go index 73e3848d..8cfeabe6 100644 --- a/vercel/resource_project.go +++ b/vercel/resource_project.go @@ -16,6 +16,7 @@ import ( type resourceProjectType struct{} +// GetSchema returns the schema information for a deployment resource. func (r resourceProjectType) GetSchema(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { return tfsdk.Schema{ Description: ` @@ -151,6 +152,7 @@ deployments, you may not want to create a Project within the same terraform work }, nil } +// NewResource instantiates a new Resource of this ResourceType. func (r resourceProjectType) NewResource(_ context.Context, p tfsdk.Provider) (tfsdk.Resource, diag.Diagnostics) { return resourceProject{ p: *(p.(*provider)), @@ -161,6 +163,8 @@ type resourceProject struct { p provider } +// 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 resourceProject) Create(ctx context.Context, req tfsdk.CreateResourceRequest, resp *tfsdk.CreateResourceResponse) { if !r.p.configured { resp.Diagnostics.AddError( @@ -196,6 +200,8 @@ func (r resourceProject) Create(ctx context.Context, req tfsdk.CreateResourceReq } } +// Read will read a project from the vercel API and provide terraform with information about it. +// It is called by the provider whenever data source values should be read to update state. func (r resourceProject) Read(ctx context.Context, req tfsdk.ReadResourceRequest, resp *tfsdk.ReadResourceResponse) { var state Project diags := req.State.Get(ctx, &state) @@ -205,6 +211,11 @@ func (r resourceProject) Read(ctx context.Context, req tfsdk.ReadResourceRequest } out, err := r.p.client.GetProject(ctx, state.ID.Value, state.TeamID.Value) + var apiErr client.APIError + if err != nil && errors.As(err, &apiErr) && apiErr.StatusCode == 404 { + resp.State.RemoveResource(ctx) + return + } if err != nil { resp.Diagnostics.AddError( "Error reading project", @@ -227,6 +238,8 @@ func (r resourceProject) Read(ctx context.Context, req tfsdk.ReadResourceRequest } } +// containsEnvVar is a helper function for working out whether a specific environment variable +// is present within a slice. It ensures that all properties of the environment variable match. func containsEnvVar(env []EnvironmentItem, v EnvironmentItem) bool { for _, e := range env { if e.Key == v.Key && @@ -243,6 +256,8 @@ func containsEnvVar(env []EnvironmentItem, v EnvironmentItem) bool { return false } +// diffEnvVars is used to determine the set of environment variables that need to be updated, +// and the set of environment variables that need to be removed. func diffEnvVars(oldVars, newVars []EnvironmentItem) (toUpsert, toRemove []EnvironmentItem) { toRemove = []EnvironmentItem{} toUpsert = []EnvironmentItem{} @@ -259,6 +274,9 @@ func diffEnvVars(oldVars, newVars []EnvironmentItem) (toUpsert, toRemove []Envir return toUpsert, toRemove } +// Update will update a project and it's associated environment variables via the vercel API. +// Environment variables are manually diffed and updated individually. Once the environment +// variables are all updated, the project is updated too. func (r resourceProject) Update(ctx context.Context, req tfsdk.UpdateResourceRequest, resp *tfsdk.UpdateResourceResponse) { var plan Project diags := req.Plan.Get(ctx, &plan) @@ -349,6 +367,8 @@ func (r resourceProject) Update(ctx context.Context, req tfsdk.UpdateResourceReq } } +// Delete a project and any associated environment variables from within terraform. +// Environment variables do not need to be explicitly deleted, as Vercel will automatically prune them. func (r resourceProject) Delete(ctx context.Context, req tfsdk.DeleteResourceRequest, resp *tfsdk.DeleteResourceResponse) { var state Project diags := req.State.Get(ctx, &state) @@ -380,6 +400,8 @@ func (r resourceProject) Delete(ctx context.Context, req tfsdk.DeleteResourceReq resp.State.RemoveResource(ctx) } +// splitID is a helper function for splitting an import ID into the corresponding parts. +// It also validates whether the ID is in a correct format. func splitID(id string) (teamID, _id string, ok bool) { if strings.Contains(id, "/") { attributes := strings.Split(id, "/") @@ -391,6 +413,8 @@ func splitID(id string) (teamID, _id string, ok bool) { return "", id, true } +// ImportState takes an identifier and reads all the project information from the Vercel API. +// Note that environment variables are also read. The results are then stored in terraform state. func (r resourceProject) ImportState(ctx context.Context, req tfsdk.ImportResourceStateRequest, resp *tfsdk.ImportResourceStateResponse) { teamID, projectID, ok := splitID(req.ID) if !ok { diff --git a/vercel/resource_project_domain.go b/vercel/resource_project_domain.go index 6c38e31f..f06d5bbc 100644 --- a/vercel/resource_project_domain.go +++ b/vercel/resource_project_domain.go @@ -15,6 +15,7 @@ import ( type resourceProjectDomainType struct{} +// GetSchema returns the schema information for a deployment resource. func (r resourceProjectDomainType) GetSchema(context.Context) (tfsdk.Schema, diag.Diagnostics) { return tfsdk.Schema{ Description: ` @@ -56,7 +57,7 @@ By default, Project Domains will be automatically applied to any ` + "`productio Optional: true, Type: types.Int64Type, Validators: []tfsdk.AttributeValidator{ - Int64ItemsIn(301, 302, 307, 308), + int64ItemsIn(301, 302, 307, 308), }, }, "git_branch": { @@ -68,6 +69,7 @@ By default, Project Domains will be automatically applied to any ` + "`productio }, nil } +// NewResource instantiates a new Resource of this ResourceType. func (r resourceProjectDomainType) NewResource(_ context.Context, p tfsdk.Provider) (tfsdk.Resource, diag.Diagnostics) { return resourceProjectDomain{ p: *(p.(*provider)), @@ -78,6 +80,8 @@ type resourceProjectDomain struct { p provider } +// Create will create a project domain within Vercel. +// This is called automatically by the provider when a new resource should be created. func (r resourceProjectDomain) Create(ctx context.Context, req tfsdk.CreateResourceRequest, resp *tfsdk.CreateResourceResponse) { if !r.p.configured { resp.Diagnostics.AddError( @@ -123,6 +127,8 @@ func (r resourceProjectDomain) Create(ctx context.Context, req tfsdk.CreateResou } } +// Read will read a project domain from the vercel API and provide terraform with information about it. +// It is called by the provider whenever data source values should be read to update state. func (r resourceProjectDomain) Read(ctx context.Context, req tfsdk.ReadResourceRequest, resp *tfsdk.ReadResourceResponse) { var state ProjectDomain diags := req.State.Get(ctx, &state) @@ -132,6 +138,11 @@ func (r resourceProjectDomain) Read(ctx context.Context, req tfsdk.ReadResourceR } out, err := r.p.client.GetProjectDomain(ctx, state.ProjectID.Value, state.Domain.Value, state.TeamID.Value) + var apiErr client.APIError + if err != nil && errors.As(err, &apiErr) && apiErr.StatusCode == 404 { + resp.State.RemoveResource(ctx) + return + } if err != nil { resp.Diagnostics.AddError( "Error reading project domain", @@ -159,6 +170,7 @@ func (r resourceProjectDomain) Read(ctx context.Context, req tfsdk.ReadResourceR } } +// Update will update a project domain via the vercel API. func (r resourceProjectDomain) Update(ctx context.Context, req tfsdk.UpdateResourceRequest, resp *tfsdk.UpdateResourceResponse) { var plan ProjectDomain diags := req.Plan.Get(ctx, &plan) @@ -208,6 +220,7 @@ func (r resourceProjectDomain) Update(ctx context.Context, req tfsdk.UpdateResou } } +// Delete will remove a project domain via the Vercel API. func (r resourceProjectDomain) Delete(ctx context.Context, req tfsdk.DeleteResourceRequest, resp *tfsdk.DeleteResourceResponse) { var state ProjectDomain diags := req.State.Get(ctx, &state) @@ -244,6 +257,8 @@ func (r resourceProjectDomain) Delete(ctx context.Context, req tfsdk.DeleteResou resp.State.RemoveResource(ctx) } +// splitProjectDomainID is a helper function for splitting an import ID into the corresponding parts. +// It also validates whether the ID is in a correct format. func splitProjectDomainID(id string) (teamID, projectID, domain string, ok bool) { attributes := strings.Split(id, "/") if len(attributes) == 2 { @@ -257,6 +272,8 @@ func splitProjectDomainID(id string) (teamID, projectID, domain string, ok bool) return "", "", "", false } +// ImportState takes an identifier and reads all the project domain information from the Vercel API. +// Note that environment variables are also read. The results are then stored in terraform state. func (r resourceProjectDomain) ImportState(ctx context.Context, req tfsdk.ImportResourceStateRequest, resp *tfsdk.ImportResourceStateResponse) { teamID, projectID, domain, ok := splitProjectDomainID(req.ID) if !ok { diff --git a/vercel/resource_project_domain_model.go b/vercel/resource_project_domain_model.go index cf62c89e..7a626bd1 100644 --- a/vercel/resource_project_domain_model.go +++ b/vercel/resource_project_domain_model.go @@ -5,6 +5,7 @@ import ( "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"` diff --git a/vercel/resource_project_model.go b/vercel/resource_project_model.go index 62f98ddf..7ae54772 100644 --- a/vercel/resource_project_model.go +++ b/vercel/resource_project_model.go @@ -5,6 +5,7 @@ import ( "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"` @@ -80,6 +81,7 @@ func (p *Project) toUpdateProjectRequest(oldName string) client.UpdateProjectReq } } +// EnvironmentItem reflects the state terraform stores internally for a project's environment variable. type EnvironmentItem struct { Target []types.String `tfsdk:"target"` Key types.String `tfsdk:"key"` @@ -101,6 +103,7 @@ func (e *EnvironmentItem) toUpsertEnvironmentVariableRequest() client.UpsertEnvi } } +// 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"` diff --git a/vercel/validator_int64_items_in.go b/vercel/validator_int64_items_in.go index d42814dd..e39814ef 100644 --- a/vercel/validator_int64_items_in.go +++ b/vercel/validator_int64_items_in.go @@ -10,7 +10,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" ) -func Int64ItemsIn(items ...int64) validatorInt64ItemsIn { +func int64ItemsIn(items ...int64) validatorInt64ItemsIn { itemMap := map[int64]struct{}{} for _, i := range items { itemMap[i] = struct{}{}