├── .gitignore ├── terraform-registry-manifest.json ├── main.go ├── .goreleaser.yml ├── go.mod ├── README.md ├── internal └── provider │ ├── provider.go │ ├── certificate_data_source.go │ └── certificate_resource.go └── go.sum /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | -------------------------------------------------------------------------------- /terraform-registry-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "metadata": { 4 | "protocol_versions": ["6.0"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "log" 7 | 8 | "github.com/hashicorp/terraform-plugin-framework/providerserver" 9 | "github.com/envato/origin-certificate-provider/internal/provider" 10 | ) 11 | 12 | var version = "dev" 13 | 14 | func main() { 15 | var debug bool 16 | flag.BoolVar(&debug, "debug", false, "set to true to run the provider with support for debuggers like delve") 17 | flag.Parse() 18 | 19 | opts := providerserver.ServeOpts{ 20 | Address: "registry.terraform.io/envato/cfcert", 21 | Debug: debug, 22 | } 23 | 24 | err := providerserver.Serve(context.Background(), provider.New(version), opts) 25 | if err != nil { 26 | log.Fatal(err.Error()) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | # Visit https://goreleaser.com for documentation on how to customize this 2 | # behavior. 3 | version: 2 4 | before: 5 | hooks: 6 | # this is just an example and not a requirement for provider building/publishing 7 | - go mod tidy 8 | builds: 9 | - env: 10 | # goreleaser does not work with CGO, it could also complicate 11 | # usage by users in CI/CD systems like HCP Terraform where 12 | # they are unable to install libraries. 13 | - CGO_ENABLED=0 14 | mod_timestamp: '{{ .CommitTimestamp }}' 15 | flags: 16 | - -trimpath 17 | ldflags: 18 | - '-s -w -X main.version={{.Version}} -X main.commit={{.Commit}}' 19 | goos: 20 | - freebsd 21 | - windows 22 | - linux 23 | - darwin 24 | goarch: 25 | - amd64 26 | - '386' 27 | - arm 28 | - arm64 29 | ignore: 30 | - goos: darwin 31 | goarch: '386' 32 | binary: '{{ .ProjectName }}_v{{ .Version }}' 33 | archives: 34 | - format: zip 35 | name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}' 36 | checksum: 37 | extra_files: 38 | - glob: 'terraform-registry-manifest.json' 39 | name_template: '{{ .ProjectName }}_{{ .Version }}_manifest.json' 40 | name_template: '{{ .ProjectName }}_{{ .Version }}_SHA256SUMS' 41 | algorithm: sha256 42 | signs: 43 | - artifacts: checksum 44 | args: 45 | # if you are using this in a GitHub action or some other automated pipeline, you 46 | # need to pass the batch flag to indicate its not interactive. 47 | - "--batch" 48 | - "--local-user" 49 | - "{{ .Env.GPG_FINGERPRINT }}" # set this environment variable for your signing key 50 | - "--output" 51 | - "${signature}" 52 | - "--detach-sign" 53 | - "${artifact}" 54 | release: 55 | extra_files: 56 | - glob: 'terraform-registry-manifest.json' 57 | name_template: '{{ .ProjectName }}_{{ .Version }}_manifest.json' 58 | # If you want to manually examine the release before its live, uncomment this line: 59 | # draft: true 60 | changelog: 61 | disable: true 62 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/envato/origin-certificate-provider 2 | 3 | go 1.22.0 4 | 5 | require ( 6 | github.com/aws/aws-sdk-go-v2 v1.32.6 7 | github.com/aws/aws-sdk-go-v2/config v1.28.6 8 | github.com/aws/aws-sdk-go-v2/service/acm v1.30.6 9 | github.com/hashicorp/terraform-plugin-framework v1.13.0 10 | ) 11 | 12 | require ( 13 | github.com/aws/aws-sdk-go-v2/credentials v1.17.47 // indirect 14 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.21 // indirect 15 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.25 // indirect 16 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.25 // indirect 17 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect 18 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 // indirect 19 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.6 // indirect 20 | github.com/aws/aws-sdk-go-v2/service/sso v1.24.7 // indirect 21 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.6 // indirect 22 | github.com/aws/aws-sdk-go-v2/service/sts v1.33.2 // indirect 23 | github.com/aws/smithy-go v1.22.1 // indirect 24 | github.com/fatih/color v1.13.0 // indirect 25 | github.com/golang/protobuf v1.5.4 // indirect 26 | github.com/hashicorp/go-hclog v1.5.0 // indirect 27 | github.com/hashicorp/go-plugin v1.6.2 // indirect 28 | github.com/hashicorp/go-uuid v1.0.3 // indirect 29 | github.com/hashicorp/terraform-plugin-go v0.25.0 // indirect 30 | github.com/hashicorp/terraform-plugin-log v0.9.0 // indirect 31 | github.com/hashicorp/terraform-registry-address v0.2.3 // indirect 32 | github.com/hashicorp/terraform-svchost v0.1.1 // indirect 33 | github.com/hashicorp/yamux v0.1.1 // indirect 34 | github.com/jmespath/go-jmespath v0.4.0 // indirect 35 | github.com/mattn/go-colorable v0.1.12 // indirect 36 | github.com/mattn/go-isatty v0.0.17 // indirect 37 | github.com/mitchellh/go-testing-interface v1.14.1 // indirect 38 | github.com/oklog/run v1.0.0 // indirect 39 | github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect 40 | github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect 41 | golang.org/x/net v0.28.0 // indirect 42 | golang.org/x/sys v0.24.0 // indirect 43 | golang.org/x/text v0.17.0 // indirect 44 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect 45 | google.golang.org/grpc v1.67.1 // indirect 46 | google.golang.org/protobuf v1.35.1 // indirect 47 | ) 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cloudflare Origin Certificate Terraform Provider 2 | 3 | A Terraform provider for managing Cloudflare Origin Certificates imported into AWS ACM. 4 | 5 | ## Overview 6 | 7 | This provider automates the process of: 8 | 1. Generating an EC P-256 private key 9 | 2. Creating a CSR for a domain 10 | 3. Requesting a Cloudflare Origin Certificate via their API 11 | 4. Importing the certificate into AWS ACM 12 | 13 | If an existing certificate for the domain already exists in ACM, it will be reused instead of creating a new one. 14 | 15 | ## Requirements 16 | 17 | - Go 1.21+ (for building) 18 | - Terraform 1.0+ 19 | - AWS credentials configured 20 | - Cloudflare API token with Origin CA permissions 21 | 22 | ## Building 23 | 24 | ```bash 25 | cd certificate-provider 26 | go mod tidy 27 | go build -o terraform-provider-cfcert 28 | ``` 29 | 30 | ## Installation 31 | 32 | For local development, add to your `~/.terraformrc`: 33 | 34 | ```hcl 35 | provider_installation { 36 | dev_overrides { 37 | "envato/cfcert" = "/path/to/certificate-provider" 38 | } 39 | direct {} 40 | } 41 | ``` 42 | 43 | ## Usage 44 | 45 | ### Provider Configuration 46 | 47 | ```hcl 48 | provider "cfcert" { 49 | region = "ap-southeast-2" # Optional, defaults to AWS_REGION 50 | cloudflare_api_token = "your-api-token" # Optional, defaults to CLOUDFLARE_API_TOKEN 51 | } 52 | ``` 53 | 54 | ### Resource: `cfcert_origin_certificate` 55 | 56 | Creates or imports a Cloudflare Origin Certificate into AWS ACM. 57 | 58 | ```hcl 59 | resource "cfcert_origin_certificate" "example" { 60 | domain_name = "example.com" 61 | } 62 | 63 | output "certificate_arn" { 64 | value = cfcert_origin_certificate.example.certificate_arn 65 | } 66 | ``` 67 | 68 | #### Arguments 69 | 70 | - `domain_name` - (Required) The domain name for the certificate. Changing this forces a new resource. 71 | 72 | #### Attributes 73 | 74 | - `certificate_arn` - The ARN of the ACM certificate. 75 | - `id` - Same as `certificate_arn`. 76 | 77 | ### Data Source: `cfcert_origin_certificate` 78 | 79 | Look up an existing certificate by domain name. 80 | 81 | ```hcl 82 | data "cfcert_origin_certificate" "example" { 83 | domain_name = "example.com" 84 | } 85 | 86 | output "certificate_arn" { 87 | value = data.cfcert_origin_certificate.example.certificate_arn 88 | } 89 | ``` 90 | 91 | #### Arguments 92 | 93 | - `domain_name` - (Required) The domain name to search for. 94 | 95 | #### Attributes 96 | 97 | - `certificate_arn` - The ARN of the ACM certificate. 98 | - `id` - Same as `certificate_arn`. 99 | 100 | ## Environment Variables 101 | 102 | - `AWS_REGION` - AWS region (can be overridden by provider config) 103 | - `CLOUDFLARE_API_TOKEN` - Cloudflare API token (can be overridden by provider config) 104 | - Standard AWS credential environment variables (`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, etc.) 105 | 106 | ## Notes 107 | 108 | - The resource will reuse an existing certificate if one with the same domain name already exists in ACM (with `EC_prime256v1` key type and ISSUED status) 109 | - Certificates are requested with a 15-year (5475 days) validity period from Cloudflare 110 | - Deleting the resource will delete the certificate from ACM 111 | -------------------------------------------------------------------------------- /internal/provider/provider.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "context" 5 | "os" 6 | 7 | "github.com/aws/aws-sdk-go-v2/config" 8 | "github.com/aws/aws-sdk-go-v2/service/acm" 9 | "github.com/hashicorp/terraform-plugin-framework/datasource" 10 | "github.com/hashicorp/terraform-plugin-framework/provider" 11 | "github.com/hashicorp/terraform-plugin-framework/provider/schema" 12 | "github.com/hashicorp/terraform-plugin-framework/resource" 13 | "github.com/hashicorp/terraform-plugin-framework/types" 14 | ) 15 | 16 | var _ provider.Provider = &CertificateProvider{} 17 | 18 | type CertificateProvider struct { 19 | version string 20 | } 21 | 22 | type CertificateProviderModel struct { 23 | Region types.String `tfsdk:"region"` 24 | CloudflareAPIToken types.String `tfsdk:"cloudflare_api_token"` 25 | } 26 | 27 | type ProviderClients struct { 28 | ACMClient *acm.Client 29 | CloudflareAPIToken string 30 | Region string 31 | } 32 | 33 | func New(version string) func() provider.Provider { 34 | return func() provider.Provider { 35 | return &CertificateProvider{ 36 | version: version, 37 | } 38 | } 39 | } 40 | 41 | func (p *CertificateProvider) Metadata(ctx context.Context, req provider.MetadataRequest, resp *provider.MetadataResponse) { 42 | resp.TypeName = "cfcert" 43 | resp.Version = p.version 44 | } 45 | 46 | func (p *CertificateProvider) Schema(ctx context.Context, req provider.SchemaRequest, resp *provider.SchemaResponse) { 47 | resp.Schema = schema.Schema{ 48 | Description: "Provider for managing Cloudflare Origin Certificates imported into AWS ACM.", 49 | Attributes: map[string]schema.Attribute{ 50 | "region": schema.StringAttribute{ 51 | Description: "AWS region. Can also be set via AWS_REGION environment variable.", 52 | Optional: true, 53 | }, 54 | "cloudflare_api_token": schema.StringAttribute{ 55 | Description: "Cloudflare API token with Origin CA permissions. Can also be set via CLOUDFLARE_API_TOKEN environment variable.", 56 | Optional: true, 57 | Sensitive: true, 58 | }, 59 | }, 60 | } 61 | } 62 | 63 | func (p *CertificateProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) { 64 | var data CertificateProviderModel 65 | resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) 66 | if resp.Diagnostics.HasError() { 67 | return 68 | } 69 | 70 | region := os.Getenv("AWS_REGION") 71 | if !data.Region.IsNull() && data.Region.ValueString() != "" { 72 | region = data.Region.ValueString() 73 | } 74 | 75 | cloudflareToken := os.Getenv("CLOUDFLARE_API_TOKEN") 76 | if !data.CloudflareAPIToken.IsNull() && data.CloudflareAPIToken.ValueString() != "" { 77 | cloudflareToken = data.CloudflareAPIToken.ValueString() 78 | } 79 | 80 | if region == "" { 81 | resp.Diagnostics.AddError( 82 | "Missing AWS Region", 83 | "AWS region must be set via the region attribute or AWS_REGION environment variable.", 84 | ) 85 | } 86 | 87 | if cloudflareToken == "" { 88 | resp.Diagnostics.AddError( 89 | "Missing Cloudflare API Token", 90 | "Cloudflare API token must be set via the cloudflare_api_token attribute or CLOUDFLARE_API_TOKEN environment variable.", 91 | ) 92 | } 93 | 94 | if resp.Diagnostics.HasError() { 95 | return 96 | } 97 | 98 | cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(region)) 99 | if err != nil { 100 | resp.Diagnostics.AddError( 101 | "Unable to Create AWS Config", 102 | "An error occurred while creating the AWS configuration: "+err.Error(), 103 | ) 104 | return 105 | } 106 | 107 | clients := &ProviderClients{ 108 | ACMClient: acm.NewFromConfig(cfg), 109 | CloudflareAPIToken: cloudflareToken, 110 | Region: region, 111 | } 112 | 113 | resp.DataSourceData = clients 114 | resp.ResourceData = clients 115 | } 116 | 117 | func (p *CertificateProvider) Resources(ctx context.Context) []func() resource.Resource { 118 | return []func() resource.Resource{ 119 | NewCertificateResource, 120 | } 121 | } 122 | 123 | func (p *CertificateProvider) DataSources(ctx context.Context) []func() datasource.DataSource { 124 | return []func() datasource.DataSource{ 125 | NewCertificateDataSource, 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /internal/provider/certificate_data_source.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/aws/aws-sdk-go-v2/aws" 8 | "github.com/aws/aws-sdk-go-v2/service/acm" 9 | "github.com/aws/aws-sdk-go-v2/service/acm/types" 10 | "github.com/hashicorp/terraform-plugin-framework/datasource" 11 | "github.com/hashicorp/terraform-plugin-framework/datasource/schema" 12 | tfTypes "github.com/hashicorp/terraform-plugin-framework/types" 13 | ) 14 | 15 | var _ datasource.DataSource = &CertificateDataSource{} 16 | var _ datasource.DataSourceWithConfigure = &CertificateDataSource{} 17 | 18 | type CertificateDataSource struct { 19 | clients *ProviderClients 20 | } 21 | 22 | type CertificateDataSourceModel struct { 23 | DomainName tfTypes.String `tfsdk:"domain_name"` 24 | CertificateArn tfTypes.String `tfsdk:"certificate_arn"` 25 | ID tfTypes.String `tfsdk:"id"` 26 | } 27 | 28 | func NewCertificateDataSource() datasource.DataSource { 29 | return &CertificateDataSource{} 30 | } 31 | 32 | func (d *CertificateDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { 33 | resp.TypeName = req.ProviderTypeName + "_origin_certificate" 34 | } 35 | 36 | func (d *CertificateDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { 37 | resp.Schema = schema.Schema{ 38 | Description: "Look up an existing Cloudflare Origin Certificate in AWS ACM by domain name.", 39 | Attributes: map[string]schema.Attribute{ 40 | "domain_name": schema.StringAttribute{ 41 | Description: "The domain name to search for.", 42 | Required: true, 43 | }, 44 | "certificate_arn": schema.StringAttribute{ 45 | Description: "The ARN of the ACM certificate, if found.", 46 | Computed: true, 47 | }, 48 | "id": schema.StringAttribute{ 49 | Description: "Data source identifier.", 50 | Computed: true, 51 | }, 52 | }, 53 | } 54 | } 55 | 56 | func (d *CertificateDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { 57 | if req.ProviderData == nil { 58 | return 59 | } 60 | clients, ok := req.ProviderData.(*ProviderClients) 61 | if !ok { 62 | resp.Diagnostics.AddError( 63 | "Unexpected Data Source Configure Type", 64 | fmt.Sprintf("Expected *ProviderClients, got: %T", req.ProviderData), 65 | ) 66 | return 67 | } 68 | d.clients = clients 69 | } 70 | 71 | func (d *CertificateDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { 72 | var data CertificateDataSourceModel 73 | resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) 74 | if resp.Diagnostics.HasError() { 75 | return 76 | } 77 | 78 | domainName := data.DomainName.ValueString() 79 | 80 | arn, err := d.findExistingCertificate(ctx, domainName) 81 | if err != nil { 82 | resp.Diagnostics.AddError("Failed to search for certificates", err.Error()) 83 | return 84 | } 85 | 86 | if arn == "" { 87 | resp.Diagnostics.AddError( 88 | "Certificate Not Found", 89 | fmt.Sprintf("No issued EC_prime256v1 certificate found for domain: %s", domainName), 90 | ) 91 | return 92 | } 93 | 94 | data.CertificateArn = tfTypes.StringValue(arn) 95 | data.ID = tfTypes.StringValue(arn) 96 | 97 | resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) 98 | } 99 | 100 | func (d *CertificateDataSource) findExistingCertificate(ctx context.Context, domainName string) (string, error) { 101 | paginator := acm.NewListCertificatesPaginator(d.clients.ACMClient, &acm.ListCertificatesInput{ 102 | CertificateStatuses: []types.CertificateStatus{types.CertificateStatusIssued}, 103 | Includes: &types.Filters{ 104 | KeyTypes: []types.KeyAlgorithm{types.KeyAlgorithmEcPrime256v1}, 105 | }, 106 | SortBy: types.SortByCreatedAt, 107 | SortOrder: types.SortOrderDescending, 108 | }) 109 | 110 | for paginator.HasMorePages() { 111 | page, err := paginator.NextPage(ctx) 112 | if err != nil { 113 | return "", err 114 | } 115 | for _, cert := range page.CertificateSummaryList { 116 | if aws.ToString(cert.DomainName) == domainName { 117 | return aws.ToString(cert.CertificateArn), nil 118 | } 119 | } 120 | } 121 | return "", nil 122 | } 123 | -------------------------------------------------------------------------------- /internal/provider/certificate_resource.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "crypto/ecdsa" 7 | "crypto/elliptic" 8 | "crypto/rand" 9 | "crypto/x509" 10 | "crypto/x509/pkix" 11 | "encoding/json" 12 | "encoding/pem" 13 | "fmt" 14 | "io" 15 | "net/http" 16 | 17 | "github.com/aws/aws-sdk-go-v2/aws" 18 | "github.com/aws/aws-sdk-go-v2/service/acm" 19 | "github.com/aws/aws-sdk-go-v2/service/acm/types" 20 | "github.com/hashicorp/terraform-plugin-framework/resource" 21 | "github.com/hashicorp/terraform-plugin-framework/resource/schema" 22 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" 23 | "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" 24 | tfTypes "github.com/hashicorp/terraform-plugin-framework/types" 25 | ) 26 | 27 | var _ resource.Resource = &CertificateResource{} 28 | var _ resource.ResourceWithConfigure = &CertificateResource{} 29 | 30 | type CertificateResource struct { 31 | clients *ProviderClients 32 | } 33 | 34 | type CertificateResourceModel struct { 35 | DomainName tfTypes.String `tfsdk:"domain_name"` 36 | CertificateArn tfTypes.String `tfsdk:"certificate_arn"` 37 | ID tfTypes.String `tfsdk:"id"` 38 | } 39 | 40 | func NewCertificateResource() resource.Resource { 41 | return &CertificateResource{} 42 | } 43 | 44 | func (r *CertificateResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { 45 | resp.TypeName = req.ProviderTypeName + "_origin_certificate" 46 | } 47 | 48 | func (r *CertificateResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { 49 | resp.Schema = schema.Schema{ 50 | Description: "Manages a Cloudflare Origin Certificate imported into AWS ACM.", 51 | Attributes: map[string]schema.Attribute{ 52 | "domain_name": schema.StringAttribute{ 53 | Description: "The domain name for the certificate.", 54 | Required: true, 55 | PlanModifiers: []planmodifier.String{ 56 | stringplanmodifier.RequiresReplace(), 57 | }, 58 | }, 59 | "certificate_arn": schema.StringAttribute{ 60 | Description: "The ARN of the ACM certificate.", 61 | Computed: true, 62 | }, 63 | "id": schema.StringAttribute{ 64 | Description: "Resource identifier (same as certificate_arn).", 65 | Computed: true, 66 | }, 67 | }, 68 | } 69 | } 70 | 71 | func (r *CertificateResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { 72 | if req.ProviderData == nil { 73 | return 74 | } 75 | clients, ok := req.ProviderData.(*ProviderClients) 76 | if !ok { 77 | resp.Diagnostics.AddError( 78 | "Unexpected Resource Configure Type", 79 | fmt.Sprintf("Expected *ProviderClients, got: %T", req.ProviderData), 80 | ) 81 | return 82 | } 83 | r.clients = clients 84 | } 85 | 86 | func (r *CertificateResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { 87 | var data CertificateResourceModel 88 | resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) 89 | if resp.Diagnostics.HasError() { 90 | return 91 | } 92 | 93 | domainName := data.DomainName.ValueString() 94 | 95 | existingArn, err := r.findExistingCertificate(ctx, domainName) 96 | if err != nil { 97 | resp.Diagnostics.AddError("Failed to check existing certificates", err.Error()) 98 | return 99 | } 100 | 101 | if existingArn != "" { 102 | data.CertificateArn = tfTypes.StringValue(existingArn) 103 | data.ID = tfTypes.StringValue(existingArn) 104 | resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) 105 | return 106 | } 107 | 108 | privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 109 | if err != nil { 110 | resp.Diagnostics.AddError("Failed to generate private key", err.Error()) 111 | return 112 | } 113 | 114 | csrTemplate := x509.CertificateRequest{ 115 | Subject: pkix.Name{CommonName: domainName}, 116 | DNSNames: []string{domainName}, 117 | } 118 | csrDER, err := x509.CreateCertificateRequest(rand.Reader, &csrTemplate, privateKey) 119 | if err != nil { 120 | resp.Diagnostics.AddError("Failed to create CSR", err.Error()) 121 | return 122 | } 123 | 124 | csrPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csrDER}) 125 | 126 | certPEM, err := r.requestCloudflareOriginCert(domainName, string(csrPEM)) 127 | if err != nil { 128 | resp.Diagnostics.AddError("Failed to request Cloudflare Origin Certificate", err.Error()) 129 | return 130 | } 131 | 132 | keyDER, err := x509.MarshalECPrivateKey(privateKey) 133 | if err != nil { 134 | resp.Diagnostics.AddError("Failed to marshal private key", err.Error()) 135 | return 136 | } 137 | keyPEM := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: keyDER}) 138 | 139 | importOutput, err := r.clients.ACMClient.ImportCertificate(ctx, &acm.ImportCertificateInput{ 140 | Certificate: []byte(certPEM), 141 | PrivateKey: keyPEM, 142 | }) 143 | if err != nil { 144 | resp.Diagnostics.AddError("Failed to import certificate to ACM", err.Error()) 145 | return 146 | } 147 | 148 | arn := aws.ToString(importOutput.CertificateArn) 149 | data.CertificateArn = tfTypes.StringValue(arn) 150 | data.ID = tfTypes.StringValue(arn) 151 | 152 | resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) 153 | } 154 | 155 | func (r *CertificateResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { 156 | var data CertificateResourceModel 157 | resp.Diagnostics.Append(req.State.Get(ctx, &data)...) 158 | if resp.Diagnostics.HasError() { 159 | return 160 | } 161 | 162 | arn := data.CertificateArn.ValueString() 163 | if arn == "" { 164 | resp.State.RemoveResource(ctx) 165 | return 166 | } 167 | 168 | _, err := r.clients.ACMClient.DescribeCertificate(ctx, &acm.DescribeCertificateInput{ 169 | CertificateArn: aws.String(arn), 170 | }) 171 | if err != nil { 172 | resp.State.RemoveResource(ctx) 173 | return 174 | } 175 | 176 | resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) 177 | } 178 | 179 | func (r *CertificateResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { 180 | // domain_name forces replacement, so Update is a no-op 181 | var data CertificateResourceModel 182 | resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) 183 | if resp.Diagnostics.HasError() { 184 | return 185 | } 186 | resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) 187 | } 188 | 189 | func (r *CertificateResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { 190 | var data CertificateResourceModel 191 | resp.Diagnostics.Append(req.State.Get(ctx, &data)...) 192 | if resp.Diagnostics.HasError() { 193 | return 194 | } 195 | 196 | arn := data.CertificateArn.ValueString() 197 | if arn == "" { 198 | return 199 | } 200 | 201 | _, err := r.clients.ACMClient.DeleteCertificate(ctx, &acm.DeleteCertificateInput{ 202 | CertificateArn: aws.String(arn), 203 | }) 204 | if err != nil { 205 | resp.Diagnostics.AddError("Failed to delete certificate", err.Error()) 206 | return 207 | } 208 | } 209 | 210 | func (r *CertificateResource) findExistingCertificate(ctx context.Context, domainName string) (string, error) { 211 | paginator := acm.NewListCertificatesPaginator(r.clients.ACMClient, &acm.ListCertificatesInput{ 212 | CertificateStatuses: []types.CertificateStatus{types.CertificateStatusIssued}, 213 | Includes: &types.Filters{ 214 | KeyTypes: []types.KeyAlgorithm{types.KeyAlgorithmEcPrime256v1}, 215 | }, 216 | SortBy: types.SortByCreatedAt, 217 | SortOrder: types.SortOrderDescending, 218 | }) 219 | 220 | for paginator.HasMorePages() { 221 | page, err := paginator.NextPage(ctx) 222 | if err != nil { 223 | return "", err 224 | } 225 | for _, cert := range page.CertificateSummaryList { 226 | if aws.ToString(cert.DomainName) == domainName { 227 | return aws.ToString(cert.CertificateArn), nil 228 | } 229 | } 230 | } 231 | return "", nil 232 | } 233 | 234 | type cloudflareOriginCertRequest struct { 235 | CSR string `json:"csr"` 236 | Hostnames []string `json:"hostnames"` 237 | RequestType string `json:"request_type"` 238 | RequestedValidity int `json:"requested_validity"` 239 | } 240 | 241 | type cloudflareOriginCertResponse struct { 242 | Success bool `json:"success"` 243 | Result struct { 244 | Certificate string `json:"certificate"` 245 | } `json:"result"` 246 | Errors []struct { 247 | Message string `json:"message"` 248 | } `json:"errors"` 249 | } 250 | 251 | func (r *CertificateResource) requestCloudflareOriginCert(domainName, csrPEM string) (string, error) { 252 | reqBody := cloudflareOriginCertRequest{ 253 | CSR: csrPEM, 254 | Hostnames: []string{domainName}, 255 | RequestType: "origin-ecc", 256 | RequestedValidity: 5475, 257 | } 258 | 259 | jsonBody, err := json.Marshal(reqBody) 260 | if err != nil { 261 | return "", fmt.Errorf("failed to marshal request: %w", err) 262 | } 263 | 264 | httpReq, err := http.NewRequest("POST", "https://api.cloudflare.com/client/v4/certificates", bytes.NewReader(jsonBody)) 265 | if err != nil { 266 | return "", fmt.Errorf("failed to create request: %w", err) 267 | } 268 | 269 | httpReq.Header.Set("Content-Type", "application/json") 270 | httpReq.Header.Set("Authorization", "Bearer "+r.clients.CloudflareAPIToken) 271 | 272 | client := &http.Client{} 273 | httpResp, err := client.Do(httpReq) 274 | if err != nil { 275 | return "", fmt.Errorf("failed to send request: %w", err) 276 | } 277 | defer httpResp.Body.Close() 278 | 279 | body, err := io.ReadAll(httpResp.Body) 280 | if err != nil { 281 | return "", fmt.Errorf("failed to read response: %w", err) 282 | } 283 | 284 | var cfResp cloudflareOriginCertResponse 285 | if err := json.Unmarshal(body, &cfResp); err != nil { 286 | return "", fmt.Errorf("failed to parse response: %w", err) 287 | } 288 | 289 | if !cfResp.Success { 290 | errMsg := "unknown error" 291 | if len(cfResp.Errors) > 0 { 292 | errMsg = cfResp.Errors[0].Message 293 | } 294 | return "", fmt.Errorf("cloudflare API error: %s", errMsg) 295 | } 296 | 297 | return cfResp.Result.Certificate, nil 298 | } 299 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/aws/aws-sdk-go-v2 v1.32.6 h1:7BokKRgRPuGmKkFMhEg/jSul+tB9VvXhcViILtfG8b4= 2 | github.com/aws/aws-sdk-go-v2 v1.32.6/go.mod h1:P5WJBrYqqbWVaOxgH0X/FYYD47/nooaPOZPlQdmiN2U= 3 | github.com/aws/aws-sdk-go-v2/config v1.28.6 h1:D89IKtGrs/I3QXOLNTH93NJYtDhm8SYa9Q5CsPShmyo= 4 | github.com/aws/aws-sdk-go-v2/config v1.28.6/go.mod h1:GDzxJ5wyyFSCoLkS+UhGB0dArhb9mI+Co4dHtoTxbko= 5 | github.com/aws/aws-sdk-go-v2/credentials v1.17.47 h1:48bA+3/fCdi2yAwVt+3COvmatZ6jUDNkDTIsqDiMUdw= 6 | github.com/aws/aws-sdk-go-v2/credentials v1.17.47/go.mod h1:+KdckOejLW3Ks3b0E3b5rHsr2f9yuORBum0WPnE5o5w= 7 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.21 h1:AmoU1pziydclFT/xRV+xXE/Vb8fttJCLRPv8oAkprc0= 8 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.21/go.mod h1:AjUdLYe4Tgs6kpH4Bv7uMZo7pottoyHMn4eTcIcneaY= 9 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.25 h1:s/fF4+yDQDoElYhfIVvSNyeCydfbuTKzhxSXDXCPasU= 10 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.25/go.mod h1:IgPfDv5jqFIzQSNbUEMoitNooSMXjRSDkhXv8jiROvU= 11 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.25 h1:ZntTCl5EsYnhN/IygQEUugpdwbhdkom9uHcbCftiGgA= 12 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.25/go.mod h1:DBdPrgeocww+CSl1C8cEV8PN1mHMBhuCDLpXezyvWkE= 13 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ= 14 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc= 15 | github.com/aws/aws-sdk-go-v2/service/acm v1.30.6 h1:fDg0RlN30Xf/yYzEUL/WXqhmgFsjVb/I3230oCfyI5w= 16 | github.com/aws/aws-sdk-go-v2/service/acm v1.30.6/go.mod h1:zRR6jE3v/TcbfO8C2P+H0Z+kShiKKVaVyoIl8NQRjyg= 17 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 h1:iXtILhvDxB6kPvEXgsDhGaZCSC6LQET5ZHSdJozeI0Y= 18 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1/go.mod h1:9nu0fVANtYiAePIBh2/pFUSwtJ402hLnp854CNoDOeE= 19 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.6 h1:50+XsN70RS7dwJ2CkVNXzj7U2L1HKP8nqTd3XWEXBN4= 20 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.6/go.mod h1:WqgLmwY7so32kG01zD8CPTJWVWM+TzJoOVHwTg4aPug= 21 | github.com/aws/aws-sdk-go-v2/service/sso v1.24.7 h1:rLnYAfXQ3YAccocshIH5mzNNwZBkBo+bP6EhIxak6Hw= 22 | github.com/aws/aws-sdk-go-v2/service/sso v1.24.7/go.mod h1:ZHtuQJ6t9A/+YDuxOLnbryAmITtr8UysSny3qcyvJTc= 23 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.6 h1:JnhTZR3PiYDNKlXy50/pNeix9aGMo6lLpXwJ1mw8MD4= 24 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.6/go.mod h1:URronUEGfXZN1VpdktPSD1EkAL9mfrV+2F4sjH38qOY= 25 | github.com/aws/aws-sdk-go-v2/service/sts v1.33.2 h1:s4074ZO1Hk8qv65GqNXqDjmkf4HSQqJukaLuuW0TpDA= 26 | github.com/aws/aws-sdk-go-v2/service/sts v1.33.2/go.mod h1:mVggCnIWoM09jP71Wh+ea7+5gAp53q+49wDFs1SW5z8= 27 | github.com/aws/smithy-go v1.22.1 h1:/HPHZQ0g7f4eUeK6HKglFz8uwVfZKgoI25rb/J+dnro= 28 | github.com/aws/smithy-go v1.22.1/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= 29 | github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA= 30 | github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8= 31 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 32 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 33 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 34 | github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= 35 | github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= 36 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 37 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 38 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 39 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 40 | github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c= 41 | github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= 42 | github.com/hashicorp/go-plugin v1.6.2 h1:zdGAEd0V1lCaU0u+MxWQhtSDQmahpkwOun8U8EiRVog= 43 | github.com/hashicorp/go-plugin v1.6.2/go.mod h1:CkgLQ5CZqNmdL9U9JzM532t8ZiYQ35+pj3b1FD37R0Q= 44 | github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= 45 | github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 46 | github.com/hashicorp/terraform-plugin-framework v1.13.0 h1:8OTG4+oZUfKgnfTdPTJwZ532Bh2BobF4H+yBiYJ/scw= 47 | github.com/hashicorp/terraform-plugin-framework v1.13.0/go.mod h1:j64rwMGpgM3NYXTKuxrCnyubQb/4VKldEKlcG8cvmjU= 48 | github.com/hashicorp/terraform-plugin-go v0.25.0 h1:oi13cx7xXA6QciMcpcFi/rwA974rdTxjqEhXJjbAyks= 49 | github.com/hashicorp/terraform-plugin-go v0.25.0/go.mod h1:+SYagMYadJP86Kvn+TGeV+ofr/R3g4/If0O5sO96MVw= 50 | github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= 51 | github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow= 52 | github.com/hashicorp/terraform-registry-address v0.2.3 h1:2TAiKJ1A3MAkZlH1YI/aTVcLZRu7JseiXNRHbOAyoTI= 53 | github.com/hashicorp/terraform-registry-address v0.2.3/go.mod h1:lFHA76T8jfQteVfT7caREqguFrW3c4MFSPhZB7HHgUM= 54 | github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S52uzrw4x0jKQ= 55 | github.com/hashicorp/terraform-svchost v0.1.1/go.mod h1:mNsjQfZyf/Jhz35v6/0LWcv26+X7JPS+buii2c9/ctc= 56 | github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= 57 | github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= 58 | github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c= 59 | github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo= 60 | github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= 61 | github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= 62 | github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= 63 | github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= 64 | github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 65 | github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= 66 | github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= 67 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 68 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 69 | github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= 70 | github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 71 | github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= 72 | github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= 73 | github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= 74 | github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= 75 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 76 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 77 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 78 | github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= 79 | github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= 80 | github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 81 | github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= 82 | github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= 83 | github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= 84 | github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= 85 | golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= 86 | golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= 87 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 88 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 89 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 90 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 91 | golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 92 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 93 | golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= 94 | golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 95 | golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= 96 | golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= 97 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 h1:e7S5W7MGGLaSu8j3YjdezkZ+m1/Nm0uRVRMEMGk26Xs= 98 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= 99 | google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= 100 | google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= 101 | google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= 102 | google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 103 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 104 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 105 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 106 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 107 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 108 | --------------------------------------------------------------------------------