Skip to content

Commit df0f152

Browse files
feat(cdn): add custom certificate support (#983)
relates to STACKITCDN-1000
1 parent 813b8c0 commit df0f152

8 files changed

Lines changed: 832 additions & 69 deletions

File tree

docs/data-sources/cdn_custom_domain.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,19 @@ data "stackit_cdn_custom_domain" "example" {
3232
- `name` (String)
3333
- `project_id` (String) STACKIT project ID associated with the distribution
3434

35+
### Optional
36+
37+
- `certificate` (Attributes) The TLS certificate for the custom domain. If omitted, a managed certificate will be used. If the block is specified, a custom certificate is used. (see [below for nested schema](#nestedatt--certificate))
38+
3539
### Read-Only
3640

3741
- `errors` (List of String) List of distribution errors
3842
- `id` (String) Terraform's internal resource identifier. It is structured as "`project_id`,`distribution_id`".
3943
- `status` (String) Status of the distribution
44+
45+
<a id="nestedatt--certificate"></a>
46+
### Nested Schema for `certificate`
47+
48+
Read-Only:
49+
50+
- `version` (Number) A version identifier for the certificate. Required for custom certificates. The certificate will be updated if this field is changed.

docs/resources/cdn_custom_domain.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ resource "stackit_cdn_custom_domain" "example" {
2020
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
2121
distribution_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
2222
name = "https://xxx.xxx"
23+
certificate = {
24+
certificate = "-----BEGIN CERTIFICATE-----\nY2VydGlmaWNhdGVfZGF0YQ==\n-----END CERTIFICATE---"
25+
private_key = "-----BEGIN RSA PRIVATE KEY-----\nY2VydGlmaWNhdGVfZGF0YQ==\n-----END RSA PRIVATE KEY---"
26+
}
2327
}
2428
2529
# Only use the import statement, if you want to import an existing cdn custom domain
@@ -38,8 +42,24 @@ import {
3842
- `name` (String)
3943
- `project_id` (String) STACKIT project ID associated with the distribution
4044

45+
### Optional
46+
47+
- `certificate` (Attributes) The TLS certificate for the custom domain. If omitted, a managed certificate will be used. If the block is specified, a custom certificate is used. (see [below for nested schema](#nestedatt--certificate))
48+
4149
### Read-Only
4250

4351
- `errors` (List of String) List of distribution errors
4452
- `id` (String) Terraform's internal resource identifier. It is structured as "`project_id`,`distribution_id`".
4553
- `status` (String) Status of the distribution
54+
55+
<a id="nestedatt--certificate"></a>
56+
### Nested Schema for `certificate`
57+
58+
Optional:
59+
60+
- `certificate` (String, Sensitive) The PEM-encoded TLS certificate. Required for custom certificates.
61+
- `private_key` (String, Sensitive) The PEM-encoded private key for the certificate. Required for custom certificates. The certificate will be updated if this field is changed.
62+
63+
Read-Only:
64+
65+
- `version` (Number) A version identifier for the certificate. Required for custom certificates. The certificate will be updated if this field is changed.

examples/resources/stackit_cdn_custom_domain/resource.tf

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,14 @@ resource "stackit_cdn_custom_domain" "example" {
22
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
33
distribution_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
44
name = "https://xxx.xxx"
5+
certificate = {
6+
certificate = "-----BEGIN CERTIFICATE-----\nY2VydGlmaWNhdGVfZGF0YQ==\n-----END CERTIFICATE---"
7+
private_key = "-----BEGIN RSA PRIVATE KEY-----\nY2VydGlmaWNhdGVfZGF0YQ==\n-----END RSA PRIVATE KEY---"
8+
}
59
}
610

711
# Only use the import statement, if you want to import an existing cdn custom domain
812
import {
913
to = stackit_cdn_custom_domain.import-example
1014
id = "${var.project_id},${var.distribution_id},${var.custom_domain_name}"
11-
}
15+
}

stackit/internal/services/cdn/cdn_acc_test.go

Lines changed: 83 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@ package cdn_test
22

33
import (
44
"context"
5+
cryptoRand "crypto/rand"
6+
"crypto/rsa"
7+
"crypto/x509"
8+
"crypto/x509/pkix"
9+
"encoding/pem"
510
"fmt"
11+
"math/big"
612
"net"
713
"strings"
814
"testing"
@@ -26,6 +32,7 @@ var instanceResource = map[string]string{
2632
"config_regions_updated": "\"EU\", \"US\", \"ASIA\"",
2733
"blocked_countries": "\"CU\", \"AQ\"", // Do NOT use DE or AT here, because the request might be blocked by bunny at the time of creation - don't lock yourself out
2834
"custom_domain_prefix": uuid.NewString(), // we use a different domain prefix each test run due to inconsistent upstream release of domains, which might impair consecutive test runs
35+
"dns_name": fmt.Sprintf("tf-acc-%s.stackit.gg", strings.Split(uuid.NewString(), "-")[0]),
2936
}
3037

3138
func configResources(regions string) string {
@@ -51,7 +58,7 @@ func configResources(regions string) string {
5158
resource "stackit_dns_zone" "dns_zone" {
5259
project_id = "%s"
5360
name = "cdn_acc_test_zone"
54-
dns_name = "cdntestzone.stackit.gg"
61+
dns_name = "%s"
5562
contact_email = "aa@bb.cc"
5663
type = "primary"
5764
default_ttl = 3600
@@ -64,40 +71,87 @@ func configResources(regions string) string {
6471
records = ["${stackit_cdn_distribution.distribution.domains[0].name}."]
6572
}
6673
`, testutil.CdnProviderConfig(), testutil.ProjectId, instanceResource["config_backend_origin_url"],
67-
regions, instanceResource["blocked_countries"], testutil.ProjectId,
74+
regions, instanceResource["blocked_countries"], testutil.ProjectId, instanceResource["dns_name"],
6875
testutil.ProjectId, instanceResource["custom_domain_prefix"])
6976
}
7077

71-
func configCustomDomainResources(regions string) string {
78+
func configCustomDomainResources(regions, cert, key string) string {
7279
return fmt.Sprintf(`
7380
%s
7481
7582
resource "stackit_cdn_custom_domain" "custom_domain" {
7683
project_id = stackit_cdn_distribution.distribution.project_id
7784
distribution_id = stackit_cdn_distribution.distribution.distribution_id
78-
name = "${stackit_dns_record_set.dns_record.name}.cdntestzone.stackit.gg"
85+
name = "${stackit_dns_record_set.dns_record.name}.${stackit_dns_zone.dns_zone.dns_name}"
86+
certificate = {
87+
certificate = %q
88+
private_key = %q
89+
}
7990
}
80-
`, configResources(regions))
91+
`, configResources(regions), cert, key)
8192
}
8293

83-
func configDatasources(regions string) string {
94+
func configDatasources(regions, cert, key string) string {
8495
return fmt.Sprintf(`
85-
%s
96+
%s
8697
87-
data "stackit_cdn_distribution" "distribution" {
98+
data "stackit_cdn_distribution" "distribution" {
8899
project_id = stackit_cdn_distribution.distribution.project_id
89-
distribution_id = stackit_cdn_distribution.distribution.distribution_id
90-
}
91-
92-
data "stackit_cdn_custom_domain" "custom_domain" {
100+
distribution_id = stackit_cdn_distribution.distribution.distribution_id
101+
}
102+
103+
data "stackit_cdn_custom_domain" "custom_domain" {
93104
project_id = stackit_cdn_custom_domain.custom_domain.project_id
94105
distribution_id = stackit_cdn_custom_domain.custom_domain.distribution_id
95106
name = stackit_cdn_custom_domain.custom_domain.name
96-
}
97-
`, configCustomDomainResources(regions))
107+
108+
}
109+
`, configCustomDomainResources(regions, cert, key))
98110
}
111+
func makeCertAndKey(t *testing.T, organization string) (cert, key []byte) {
112+
privateKey, err := rsa.GenerateKey(cryptoRand.Reader, 2048)
113+
if err != nil {
114+
t.Fatalf("failed to generate key: %s", err.Error())
115+
}
116+
template := x509.Certificate{
117+
SerialNumber: big.NewInt(1),
118+
Issuer: pkix.Name{CommonName: organization},
119+
Subject: pkix.Name{
120+
Organization: []string{organization},
121+
},
122+
NotBefore: time.Now(),
123+
NotAfter: time.Now().Add(time.Hour),
124+
125+
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
126+
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
127+
BasicConstraintsValid: true,
128+
}
129+
cert, err = x509.CreateCertificate(
130+
cryptoRand.Reader,
131+
&template,
132+
&template,
133+
&privateKey.PublicKey,
134+
privateKey,
135+
)
136+
if err != nil {
137+
t.Fatalf("failed to generate cert: %s", err.Error())
138+
}
99139

140+
return pem.EncodeToMemory(&pem.Block{
141+
Type: "CERTIFICATE",
142+
Bytes: cert,
143+
}), pem.EncodeToMemory(&pem.Block{
144+
Type: "RSA PRIVATE KEY",
145+
Bytes: x509.MarshalPKCS1PrivateKey(privateKey),
146+
})
147+
}
100148
func TestAccCDNDistributionResource(t *testing.T) {
149+
fullDomainName := fmt.Sprintf("%s.%s", instanceResource["custom_domain_prefix"], instanceResource["dns_name"])
150+
organization := fmt.Sprintf("organization-%s", uuid.NewString())
151+
cert, key := makeCertAndKey(t, organization)
152+
153+
organization_updated := fmt.Sprintf("organization-updated-%s", uuid.NewString())
154+
cert_updated, key_updated := makeCertAndKey(t, organization_updated)
101155
resource.Test(t, resource.TestCase{
102156
ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories,
103157
CheckDestroy: testAccCheckCDNDistributionDestroy,
@@ -128,16 +182,16 @@ func TestAccCDNDistributionResource(t *testing.T) {
128182
{
129183
Config: configResources(instanceResource["config_regions"]),
130184
Check: func(_ *terraform.State) error {
131-
_, err := blockUntilDomainResolves(instanceResource["custom_domain_prefix"] + ".cdntestzone.stackit.gg")
185+
_, err := blockUntilDomainResolves(fullDomainName)
132186
return err
133187
},
134188
},
135189
// Custom Domain Create
136190
{
137-
Config: configCustomDomainResources(instanceResource["config_regions"]),
191+
Config: configCustomDomainResources(instanceResource["config_regions"], string(cert), string(key)),
138192
Check: resource.ComposeAggregateTestCheckFunc(
139193
resource.TestCheckResourceAttr("stackit_cdn_custom_domain.custom_domain", "status", "ACTIVE"),
140-
resource.TestCheckResourceAttr("stackit_cdn_custom_domain.custom_domain", "name", instanceResource["custom_domain_prefix"]+".cdntestzone.stackit.gg"),
194+
resource.TestCheckResourceAttr("stackit_cdn_custom_domain.custom_domain", "name", fullDomainName),
141195
resource.TestCheckResourceAttrPair("stackit_cdn_distribution.distribution", "distribution_id", "stackit_cdn_custom_domain.custom_domain", "distribution_id"),
142196
resource.TestCheckResourceAttrPair("stackit_cdn_distribution.distribution", "project_id", "stackit_cdn_custom_domain.custom_domain", "project_id"),
143197
),
@@ -181,17 +235,21 @@ func TestAccCDNDistributionResource(t *testing.T) {
181235
},
182236
ImportState: true,
183237
ImportStateVerify: true,
238+
ImportStateVerifyIgnore: []string{
239+
"certificate.certificate",
240+
"certificate.private_key",
241+
},
184242
},
185243
// Data Source
186244
{
187-
Config: configDatasources(instanceResource["config_regions"]),
245+
Config: configDatasources(instanceResource["config_regions"], string(cert), string(key)),
188246
Check: resource.ComposeAggregateTestCheckFunc(
189247
resource.TestCheckResourceAttrSet("data.stackit_cdn_distribution.distribution", "distribution_id"),
190248
resource.TestCheckResourceAttrSet("data.stackit_cdn_distribution.distribution", "created_at"),
191249
resource.TestCheckResourceAttrSet("data.stackit_cdn_distribution.distribution", "updated_at"),
192250
resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "domains.#", "2"),
193251
resource.TestCheckResourceAttrSet("data.stackit_cdn_distribution.distribution", "domains.0.name"),
194-
resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "domains.1.name", instanceResource["custom_domain_prefix"]+".cdntestzone.stackit.gg"),
252+
resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "domains.1.name", fullDomainName),
195253
resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "domains.0.status", "ACTIVE"),
196254
resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "domains.1.status", "ACTIVE"),
197255
resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "domains.0.type", "managed"),
@@ -206,20 +264,21 @@ func TestAccCDNDistributionResource(t *testing.T) {
206264
resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "project_id", testutil.ProjectId),
207265
resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "status", "ACTIVE"),
208266
resource.TestCheckResourceAttr("data.stackit_cdn_custom_domain.custom_domain", "status", "ACTIVE"),
209-
resource.TestCheckResourceAttr("data.stackit_cdn_custom_domain.custom_domain", "name", instanceResource["custom_domain_prefix"]+".cdntestzone.stackit.gg"),
267+
resource.TestCheckResourceAttr("data.stackit_cdn_custom_domain.custom_domain", "certificate.version", "1"),
268+
resource.TestCheckResourceAttr("data.stackit_cdn_custom_domain.custom_domain", "name", fullDomainName),
210269
resource.TestCheckResourceAttrPair("stackit_cdn_distribution.distribution", "distribution_id", "stackit_cdn_custom_domain.custom_domain", "distribution_id"),
211270
),
212271
},
213272
// Update
214273
{
215-
Config: configCustomDomainResources(instanceResource["config_regions_updated"]),
274+
Config: configCustomDomainResources(instanceResource["config_regions_updated"], string(cert_updated), string(key_updated)),
216275
Check: resource.ComposeAggregateTestCheckFunc(
217276
resource.TestCheckResourceAttrSet("stackit_cdn_distribution.distribution", "distribution_id"),
218277
resource.TestCheckResourceAttrSet("stackit_cdn_distribution.distribution", "created_at"),
219278
resource.TestCheckResourceAttrSet("stackit_cdn_distribution.distribution", "updated_at"),
220279
resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "domains.#", "2"),
221280
resource.TestCheckResourceAttrSet("stackit_cdn_distribution.distribution", "domains.0.name"),
222-
resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "domains.1.name", instanceResource["custom_domain_prefix"]+".cdntestzone.stackit.gg"),
281+
resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "domains.1.name", fullDomainName),
223282
resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "domains.0.status", "ACTIVE"),
224283
resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "domains.1.status", "ACTIVE"),
225284
resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "domains.0.type", "managed"),
@@ -235,7 +294,8 @@ func TestAccCDNDistributionResource(t *testing.T) {
235294
resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "project_id", testutil.ProjectId),
236295
resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "status", "ACTIVE"),
237296
resource.TestCheckResourceAttr("stackit_cdn_custom_domain.custom_domain", "status", "ACTIVE"),
238-
resource.TestCheckResourceAttr("stackit_cdn_custom_domain.custom_domain", "name", instanceResource["custom_domain_prefix"]+".cdntestzone.stackit.gg"),
297+
resource.TestCheckResourceAttr("stackit_cdn_custom_domain.custom_domain", "certificate.version", "2"),
298+
resource.TestCheckResourceAttr("stackit_cdn_custom_domain.custom_domain", "name", fullDomainName),
239299
resource.TestCheckResourceAttrPair("stackit_cdn_distribution.distribution", "distribution_id", "stackit_cdn_custom_domain.custom_domain", "distribution_id"),
240300
resource.TestCheckResourceAttrPair("stackit_cdn_distribution.distribution", "project_id", "stackit_cdn_custom_domain.custom_domain", "project_id"),
241301
),

0 commit comments

Comments
 (0)