From a848fad6eb20e7eb5a5c491bbb0dd94fcc1aff4b Mon Sep 17 00:00:00 2001 From: Douglas Harcourt Parsons Date: Fri, 16 May 2025 17:23:04 +0100 Subject: [PATCH 1/3] Add a resource for vercel_custom_certificate --- client/custom_certificate.go | 73 ++++++ docs/resources/custom_certificate.md | 44 ++++ .../vercel_custom_certificate/resource.tf | 5 + vercel/provider.go | 11 +- vercel/resource_custom_certificate.go | 216 ++++++++++++++++++ vercel/resource_custom_certificate_test.go | 79 +++++++ 6 files changed, 423 insertions(+), 5 deletions(-) create mode 100644 client/custom_certificate.go create mode 100644 docs/resources/custom_certificate.md create mode 100644 examples/resources/vercel_custom_certificate/resource.tf create mode 100644 vercel/resource_custom_certificate.go create mode 100644 vercel/resource_custom_certificate_test.go diff --git a/client/custom_certificate.go b/client/custom_certificate.go new file mode 100644 index 00000000..4094ce25 --- /dev/null +++ b/client/custom_certificate.go @@ -0,0 +1,73 @@ +package client + +import ( + "context" + "fmt" +) + +type UploadCustomCertificateRequest struct { + TeamID string `json:"-"` + PrivateKey string `json:"key"` + Certificate string `json:"cert"` + CertificateAuthorityCertificate string `json:"ca"` +} + +type CertificateResponse struct { + ID string `json:"id"` +} + +func (c *Client) UploadCustomCertificate(ctx context.Context, request UploadCustomCertificateRequest) (cr CertificateResponse, err error) { + url := fmt.Sprintf("%s/v7/certs", c.baseURL) + if c.TeamID(request.TeamID) != "" { + url = fmt.Sprintf("%s?teamId=%s", url, c.TeamID(request.TeamID)) + } + + payload := string(mustMarshal(request)) + err = c.doRequest(clientRequest{ + ctx: ctx, + method: "PUT", + url: url, + body: payload, + }, &cr) + return cr, err +} + +type GetCustomCertificateRequest struct { + TeamID string `json:"-"` + ID string `json:"-"` +} + +func (c *Client) GetCustomCertificate(ctx context.Context, request GetCustomCertificateRequest) (cr CertificateResponse, err error) { + url := fmt.Sprintf("%s/v7/certs/%s", c.baseURL, request.ID) + if c.TeamID(request.TeamID) != "" { + url = fmt.Sprintf("%s?teamId=%s", url, c.TeamID(request.TeamID)) + } + + err = c.doRequest(clientRequest{ + ctx: ctx, + method: "GET", + url: url, + body: "", + }, &cr) + return cr, err +} + +type DeleteCustomCertificateRequest struct { + TeamID string `json:"-"` + ID string `json:"-"` +} + +func (c *Client) DeleteCustomCertificate(ctx context.Context, request DeleteCustomCertificateRequest) error { + url := fmt.Sprintf("%s/v7/certs/%s", c.baseURL, request.ID) + if c.TeamID(request.TeamID) != "" { + url = fmt.Sprintf("%s?teamId=%s", url, c.TeamID(request.TeamID)) + } + + err := c.doRequest(clientRequest{ + ctx: ctx, + method: "DELETE", + url: url, + body: "", + }, nil) + return err +} diff --git a/docs/resources/custom_certificate.md b/docs/resources/custom_certificate.md new file mode 100644 index 00000000..f4b4c242 --- /dev/null +++ b/docs/resources/custom_certificate.md @@ -0,0 +1,44 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "vercel_custom_certificate Resource - terraform-provider-vercel" +subcategory: "" +description: |- + Provides an Custom Certificate Resource, allowing Custom Certificates to be uploaded to Vercel. + By default, Vercel provides all domains with a custom SSL certificates. However, Enterprise teams can upload their own custom SSL certificate. + For more detailed information, please see the Vercel documentation https://vercel.com/docs/domains/custom-SSL-certificate. +--- + +# vercel_custom_certificate (Resource) + +Provides an Custom Certificate Resource, allowing Custom Certificates to be uploaded to Vercel. + +By default, Vercel provides all domains with a custom SSL certificates. However, Enterprise teams can upload their own custom SSL certificate. + +For more detailed information, please see the [Vercel documentation](https://vercel.com/docs/domains/custom-SSL-certificate). + +## Example Usage + +```terraform +resource "vercel_custom_certificate" "example" { + private_key = file("private.key") + certificate = file("certificate.crt") + certificate_authority_certificate = file("ca.crt") +} +``` + + +## Schema + +### Required + +- `certificate` (String) The certificate itself. Should start with -----BEGIN CERTIFICATE----- and end with -----END CERTIFICATE----- +- `private_key` (String) The private key of the Certificate. Should start with -----BEGIN PRIVATE KEY----- and end with -----END PRIVATE KEY----- + +### Optional + +- `certificate_authority_certificate` (String) The Certificate Authority root certificate such as one of Let's Encrypt's ISRG root certificates. This will be provided by your certificate issuer and is different to the core certificate. This may be included in their download process or available for download on their website. Should start with -----BEGIN CERTIFICATE----- and end with -----END CERTIFICATE----- +- `team_id` (String) The ID of the team the Custom Certificate should exist under. Required when configuring a team resource if a default team has not been set in the provider. + +### Read-Only + +- `id` (String) The ID of the Custom Certificate. diff --git a/examples/resources/vercel_custom_certificate/resource.tf b/examples/resources/vercel_custom_certificate/resource.tf new file mode 100644 index 00000000..22cb8727 --- /dev/null +++ b/examples/resources/vercel_custom_certificate/resource.tf @@ -0,0 +1,5 @@ +resource "vercel_custom_certificate" "example" { + private_key = file("private.key") + certificate = file("certificate.crt") + certificate_authority_certificate = file("ca.crt") +} diff --git a/vercel/provider.go b/vercel/provider.go index 2def273a..ac97d9a7 100644 --- a/vercel/provider.go +++ b/vercel/provider.go @@ -55,30 +55,31 @@ func (p *vercelProvider) Resources(_ context.Context) []func() resource.Resource newAccessGroupResource, newAliasResource, newAttackChallengeModeResource, + newCustomCertificateResource, newCustomEnvironmentResource, - newDNSRecordResource, newDeploymentResource, + newDNSRecordResource, newEdgeConfigItemResource, newEdgeConfigResource, newEdgeConfigSchemaResource, newEdgeConfigTokenResource, - newFirewallConfigResource, newFirewallBypassResource, + newFirewallConfigResource, newIntegrationProjectAccessResource, newLogDrainResource, + newMicrofrontendGroupMembershipResource, + newMicrofrontendGroupResource, newProjectDeploymentRetentionResource, newProjectDomainResource, newProjectEnvironmentVariableResource, newProjectEnvironmentVariablesResource, newProjectMembersResource, newProjectResource, - newSharedEnvironmentVariableResource, newSharedEnvironmentVariableProjectLinkResource, + newSharedEnvironmentVariableResource, newTeamConfigResource, newTeamMemberResource, newWebhookResource, - newMicrofrontendGroupResource, - newMicrofrontendGroupMembershipResource, } } diff --git a/vercel/resource_custom_certificate.go b/vercel/resource_custom_certificate.go new file mode 100644 index 00000000..0a6f6727 --- /dev/null +++ b/vercel/resource_custom_certificate.go @@ -0,0 +1,216 @@ +package vercel + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/vercel/terraform-provider-vercel/v3/client" +) + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ resource.Resource = &customCertificateResource{} + _ resource.ResourceWithConfigure = &customCertificateResource{} +) + +func newCustomCertificateResource() resource.Resource { + return &customCertificateResource{} +} + +type customCertificateResource struct { + client *client.Client +} + +func (r *customCertificateResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_custom_certificate" +} + +func (r *customCertificateResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + // Prevent panic if the provider has not been configured. + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(*client.Client) + if !ok { + resp.Diagnostics.AddError( + "Unexpected Resource Configure Type", + fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return + } + + r.client = client +} + +func (r *customCertificateResource) Schema(_ context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: ` +Provides an Custom Certificate Resource, allowing Custom Certificates to be uploaded to Vercel. + +By default, Vercel provides all domains with a custom SSL certificates. However, Enterprise teams can upload their own custom SSL certificate. + +For more detailed information, please see the [Vercel documentation](https://vercel.com/docs/domains/custom-SSL-certificate). +`, + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "The ID of the Custom Certificate.", + Computed: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()}, + }, + "team_id": schema.StringAttribute{ + Optional: true, + Computed: true, + Description: "The ID of the team the Custom Certificate should exist under. Required when configuring a team resource if a default team has not been set in the provider.", + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplaceIfConfigured(), stringplanmodifier.UseStateForUnknown()}, + }, + "private_key": schema.StringAttribute{ + Description: "The private key of the Certificate. Should start with -----BEGIN PRIVATE KEY----- and end with -----END PRIVATE KEY-----", + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + "certificate": schema.StringAttribute{ + Description: "The certificate itself. Should start with -----BEGIN CERTIFICATE----- and end with -----END CERTIFICATE-----", + Required: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + "certificate_authority_certificate": schema.StringAttribute{ + Description: "The Certificate Authority root certificate such as one of Let's Encrypt's ISRG root certificates. This will be provided by your certificate issuer and is different to the core certificate. This may be included in their download process or available for download on their website. Should start with -----BEGIN CERTIFICATE----- and end with -----END CERTIFICATE-----", + Optional: true, + PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, + }, + }, + } +} + +type CustomCertificate struct { + ID types.String `tfsdk:"id"` + TeamID types.String `tfsdk:"team_id"` + PrivateKey types.String `tfsdk:"private_key"` + Certificate types.String `tfsdk:"certificate"` + CertificateAuthorityCertificate types.String `tfsdk:"certificate_authority_certificate"` +} + +func (r *customCertificateResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan CustomCertificate + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + out, err := r.client.UploadCustomCertificate(ctx, client.UploadCustomCertificateRequest{ + TeamID: plan.TeamID.ValueString(), + PrivateKey: plan.PrivateKey.ValueString(), + Certificate: plan.Certificate.ValueString(), + CertificateAuthorityCertificate: plan.CertificateAuthorityCertificate.ValueString(), + }) + if err != nil { + resp.Diagnostics.AddError( + "Error uploading Custom Certificate", + "Could not upload Custom Certificate, unexpected error: "+err.Error(), + ) + return + } + plan.ID = types.StringValue(out.ID) + plan.TeamID = types.StringValue(r.client.TeamID(plan.TeamID.ValueString())) + + tflog.Info(ctx, "uploaded custom certificate", map[string]any{ + "team_id": plan.TeamID.ValueString(), + "id": out.ID, + }) + + diags = resp.State.Set(ctx, plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +func (r *customCertificateResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state CustomCertificate + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // This is basically a check that it still exists, as the cert itself is immutable. + out, err := r.client.GetCustomCertificate(ctx, client.GetCustomCertificateRequest{ + ID: state.ID.ValueString(), + TeamID: state.TeamID.ValueString(), + }) + if client.NotFound(err) { + resp.State.RemoveResource(ctx) + return + } + if err != nil { + resp.Diagnostics.AddError( + "Error reading Custom Certificate", + fmt.Sprintf("Could not get Custom Certificate %s %s, unexpected error: %s", + state.TeamID.ValueString(), + state.ID.ValueString(), + err, + ), + ) + return + } + + tflog.Info(ctx, "read certificate", map[string]any{ + "team_id": state.TeamID.ValueString(), + "id": out.ID, + }) + + diags = resp.State.Set(ctx, state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +func (r *customCertificateResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + resp.Diagnostics.AddError( + "Updating a Custom Certificate is not supported", + "Updating a Custom Certificate is not supported", + ) +} + +func (r *customCertificateResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state CustomCertificate + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + err := r.client.DeleteCustomCertificate(ctx, client.DeleteCustomCertificateRequest{ + TeamID: state.TeamID.ValueString(), + ID: state.ID.ValueString(), + }) + if client.NotFound(err) { + return + } + if err != nil { + resp.Diagnostics.AddError( + "Error deleting Custom Certificate", + fmt.Sprintf( + "Could not delete Custom Certificate %s %s, unexpected error: %s", + state.TeamID.ValueString(), + state.ID.ValueString(), + err, + ), + ) + return + } + + tflog.Info(ctx, "deleted custom certificate", map[string]any{ + "team_id": state.TeamID.ValueString(), + "id": state.ID.ValueString(), + }) +} diff --git a/vercel/resource_custom_certificate_test.go b/vercel/resource_custom_certificate_test.go new file mode 100644 index 00000000..a6abc5ad --- /dev/null +++ b/vercel/resource_custom_certificate_test.go @@ -0,0 +1,79 @@ +package vercel_test + +import ( + "context" + "fmt" + "os" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/vercel/terraform-provider-vercel/v3/client" +) + +func testCheckCustomCertificateDoesNotExist(testClient *client.Client, teamID string, n string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("no ID is set") + } + + _, err := testClient.GetCustomCertificate(context.TODO(), client.GetCustomCertificateRequest{ + TeamID: teamID, + ID: rs.Primary.ID, + }) + if err == nil { + return fmt.Errorf("expected not_found error, but got no error") + } + if !client.NotFound(err) { + return fmt.Errorf("Unexpected error checking for deleted certificate: %s", err) + } + + return nil + } +} + +func testCert(t *testing.T) string { + v := os.Getenv("VERCEL_TERRAFORM_TESTING_CERTIFICATE") + if v == "" { + t.Fatalf("Missing required environment variable VERCEL_TERRAFORM_TESTING_CERTIFICATE") + } + return v +} + +func testCertKey(t *testing.T) string { + v := os.Getenv("VERCEL_TERRAFORM_TESTING_CERTIFICATE_KEY") + if v == "" { + t.Fatalf("Missing required environment variable VERCEL_TERRAFORM_TESTING_CERTIFICATE_KEY") + } + return v +} + +func TestAcc_CustomCertificateResource(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + CheckDestroy: testCheckCustomCertificateDoesNotExist(testClient(t), testTeam(t), "vercel_custom_certificate.test"), + Steps: []resource.TestStep{ + { + Config: cfg(fmt.Sprintf(` +resource "vercel_custom_certificate" "test" { + private_key = < Date: Mon, 19 May 2025 16:42:31 +0100 Subject: [PATCH 2/3] Bump API version to allow tests to pass --- client/custom_certificate.go | 6 +++--- docs/resources/custom_certificate.md | 10 +++++----- vercel/resource_custom_certificate.go | 10 +++++----- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/client/custom_certificate.go b/client/custom_certificate.go index 4094ce25..0f40c871 100644 --- a/client/custom_certificate.go +++ b/client/custom_certificate.go @@ -17,7 +17,7 @@ type CertificateResponse struct { } func (c *Client) UploadCustomCertificate(ctx context.Context, request UploadCustomCertificateRequest) (cr CertificateResponse, err error) { - url := fmt.Sprintf("%s/v7/certs", c.baseURL) + url := fmt.Sprintf("%s/v8/certs", c.baseURL) if c.TeamID(request.TeamID) != "" { url = fmt.Sprintf("%s?teamId=%s", url, c.TeamID(request.TeamID)) } @@ -38,7 +38,7 @@ type GetCustomCertificateRequest struct { } func (c *Client) GetCustomCertificate(ctx context.Context, request GetCustomCertificateRequest) (cr CertificateResponse, err error) { - url := fmt.Sprintf("%s/v7/certs/%s", c.baseURL, request.ID) + url := fmt.Sprintf("%s/v8/certs/%s", c.baseURL, request.ID) if c.TeamID(request.TeamID) != "" { url = fmt.Sprintf("%s?teamId=%s", url, c.TeamID(request.TeamID)) } @@ -58,7 +58,7 @@ type DeleteCustomCertificateRequest struct { } func (c *Client) DeleteCustomCertificate(ctx context.Context, request DeleteCustomCertificateRequest) error { - url := fmt.Sprintf("%s/v7/certs/%s", c.baseURL, request.ID) + url := fmt.Sprintf("%s/v8/certs/%s", c.baseURL, request.ID) if c.TeamID(request.TeamID) != "" { url = fmt.Sprintf("%s?teamId=%s", url, c.TeamID(request.TeamID)) } diff --git a/docs/resources/custom_certificate.md b/docs/resources/custom_certificate.md index f4b4c242..8eff66c3 100644 --- a/docs/resources/custom_certificate.md +++ b/docs/resources/custom_certificate.md @@ -3,14 +3,14 @@ page_title: "vercel_custom_certificate Resource - terraform-provider-vercel" subcategory: "" description: |- - Provides an Custom Certificate Resource, allowing Custom Certificates to be uploaded to Vercel. + Provides a Custom Certificate Resource, allowing Custom Certificates to be uploaded to Vercel. By default, Vercel provides all domains with a custom SSL certificates. However, Enterprise teams can upload their own custom SSL certificate. For more detailed information, please see the Vercel documentation https://vercel.com/docs/domains/custom-SSL-certificate. --- # vercel_custom_certificate (Resource) -Provides an Custom Certificate Resource, allowing Custom Certificates to be uploaded to Vercel. +Provides a Custom Certificate Resource, allowing Custom Certificates to be uploaded to Vercel. By default, Vercel provides all domains with a custom SSL certificates. However, Enterprise teams can upload their own custom SSL certificate. @@ -31,12 +31,12 @@ resource "vercel_custom_certificate" "example" { ### Required -- `certificate` (String) The certificate itself. Should start with -----BEGIN CERTIFICATE----- and end with -----END CERTIFICATE----- -- `private_key` (String) The private key of the Certificate. Should start with -----BEGIN PRIVATE KEY----- and end with -----END PRIVATE KEY----- +- `certificate` (String) The certificate itself. Should be in PEM format. +- `certificate_authority_certificate` (String) The Certificate Authority root certificate such as one of Let's Encrypt's ISRG root certificates. This will be provided by your certificate issuer and is different to the core certificate. This may be included in their download process or available for download on their website. Should be in PEM format. +- `private_key` (String) The private key of the Certificate. Should be in PEM format. ### Optional -- `certificate_authority_certificate` (String) The Certificate Authority root certificate such as one of Let's Encrypt's ISRG root certificates. This will be provided by your certificate issuer and is different to the core certificate. This may be included in their download process or available for download on their website. Should start with -----BEGIN CERTIFICATE----- and end with -----END CERTIFICATE----- - `team_id` (String) The ID of the team the Custom Certificate should exist under. Required when configuring a team resource if a default team has not been set in the provider. ### Read-Only diff --git a/vercel/resource_custom_certificate.go b/vercel/resource_custom_certificate.go index 0a6f6727..a78e0e4e 100644 --- a/vercel/resource_custom_certificate.go +++ b/vercel/resource_custom_certificate.go @@ -52,7 +52,7 @@ func (r *customCertificateResource) Configure(ctx context.Context, req resource. func (r *customCertificateResource) Schema(_ context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = schema.Schema{ Description: ` -Provides an Custom Certificate Resource, allowing Custom Certificates to be uploaded to Vercel. +Provides a Custom Certificate Resource, allowing Custom Certificates to be uploaded to Vercel. By default, Vercel provides all domains with a custom SSL certificates. However, Enterprise teams can upload their own custom SSL certificate. @@ -71,18 +71,18 @@ For more detailed information, please see the [Vercel documentation](https://ver PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplaceIfConfigured(), stringplanmodifier.UseStateForUnknown()}, }, "private_key": schema.StringAttribute{ - Description: "The private key of the Certificate. Should start with -----BEGIN PRIVATE KEY----- and end with -----END PRIVATE KEY-----", + Description: "The private key of the Certificate. Should be in PEM format.", Required: true, PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, }, "certificate": schema.StringAttribute{ - Description: "The certificate itself. Should start with -----BEGIN CERTIFICATE----- and end with -----END CERTIFICATE-----", + Description: "The certificate itself. Should be in PEM format.", Required: true, PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, }, "certificate_authority_certificate": schema.StringAttribute{ - Description: "The Certificate Authority root certificate such as one of Let's Encrypt's ISRG root certificates. This will be provided by your certificate issuer and is different to the core certificate. This may be included in their download process or available for download on their website. Should start with -----BEGIN CERTIFICATE----- and end with -----END CERTIFICATE-----", - Optional: true, + Description: "The Certificate Authority root certificate such as one of Let's Encrypt's ISRG root certificates. This will be provided by your certificate issuer and is different to the core certificate. This may be included in their download process or available for download on their website. Should be in PEM format.", + Required: true, PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()}, }, }, From c04bf2fe2514416c01b470f94fb62c9d1443811f Mon Sep 17 00:00:00 2001 From: Douglas Harcourt Parsons Date: Tue, 20 May 2025 13:01:53 +0100 Subject: [PATCH 3/3] Add secrets to CI --- .github/workflows/test.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 008e0100..0b574ee1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -113,6 +113,8 @@ jobs: VERCEL_TERRAFORM_TESTING_DOMAIN: "dgls.dev" VERCEL_TERRAFORM_TESTING_ADDITIONAL_USER: ${{ secrets.VERCEL_TERRAFORM_TESTING_ADDITIONAL_USER }} VERCEL_TERRAFORM_TESTING_EXISTING_INTEGRATION: ${{ secrets.VERCEL_TERRAFORM_TESTING_EXISTING_INTEGRATION }} + VERCEL_TERRAFORM_TESTING_CERTIFICATE: ${{ secrets.VERCEL_TERRAFORM_TESTING_CERTIFICATE }} + VERCEL_TERRAFORM_TESTING_CERTIFICATE_KEY: ${{ secrets.VERCEL_TERRAFORM_TESTING_CERTIFICATE_KEY }} run: | go test ./...