这是indexloc提供的服务,不要输入任何密码
Skip to content

Flesh out custom certificate resource #328

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
May 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 ./...

Expand Down
73 changes: 73 additions & 0 deletions client/custom_certificate.go
Original file line number Diff line number Diff line change
@@ -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/v8/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/v8/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/v8/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
}
44 changes: 44 additions & 0 deletions docs/resources/custom_certificate.md
Original file line number Diff line number Diff line change
@@ -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 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 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).

## Example Usage

```terraform
resource "vercel_custom_certificate" "example" {
private_key = file("private.key")
certificate = file("certificate.crt")
certificate_authority_certificate = file("ca.crt")
}
```

<!-- schema generated by tfplugindocs -->
## Schema

### Required

- `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

- `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.
5 changes: 5 additions & 0 deletions examples/resources/vercel_custom_certificate/resource.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
resource "vercel_custom_certificate" "example" {
private_key = file("private.key")
certificate = file("certificate.crt")
certificate_authority_certificate = file("ca.crt")
}
11 changes: 6 additions & 5 deletions vercel/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
}

Expand Down
216 changes: 216 additions & 0 deletions vercel/resource_custom_certificate.go
Original file line number Diff line number Diff line change
@@ -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 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).
`,
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 be in PEM format.",
Required: true,
PlanModifiers: []planmodifier.String{stringplanmodifier.RequiresReplace()},
},
"certificate": schema.StringAttribute{
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 be in PEM format.",
Required: 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(),
})
}
Loading