package cdn_test import ( "context" cryptoRand "crypto/rand" "crypto/rsa" "crypto/x509" "crypto/x509/pkix" "encoding/pem" "fmt" "math/big" "net" "strings" "testing" "time" "github.com/google/uuid" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/terraform" "github.com/stackitcloud/stackit-sdk-go/core/config" "github.com/stackitcloud/stackit-sdk-go/services/cdn" "github.com/stackitcloud/stackit-sdk-go/services/cdn/wait" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil" ) var instanceResource = map[string]string{ "project_id": testutil.ProjectId, "config_backend_type": "http", "config_backend_origin_url": "https://test-backend-1.cdn-dev.runs.onstackit.cloud", "config_regions": "\"EU\", \"US\"", "config_regions_updated": "\"EU\", \"US\", \"ASIA\"", "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 "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 "dns_name": fmt.Sprintf("tf-acc-%s.stackit.gg", strings.Split(uuid.NewString(), "-")[0]), } func configResources(regions string) string { return fmt.Sprintf(` %s resource "stackit_cdn_distribution" "distribution" { project_id = "%s" config = { backend = { type = "http" origin_url = "%s" } regions = [%s] blocked_countries = [%s] optimizer = { enabled = true } } } resource "stackit_dns_zone" "dns_zone" { project_id = "%s" name = "cdn_acc_test_zone" dns_name = "%s" contact_email = "aa@bb.cc" type = "primary" default_ttl = 3600 } resource "stackit_dns_record_set" "dns_record" { project_id = "%s" zone_id = stackit_dns_zone.dns_zone.zone_id name = "%s" type = "CNAME" records = ["${stackit_cdn_distribution.distribution.domains[0].name}."] } `, testutil.CdnProviderConfig(), testutil.ProjectId, instanceResource["config_backend_origin_url"], regions, instanceResource["blocked_countries"], testutil.ProjectId, instanceResource["dns_name"], testutil.ProjectId, instanceResource["custom_domain_prefix"]) } func configCustomDomainResources(regions, cert, key string) string { return fmt.Sprintf(` %s resource "stackit_cdn_custom_domain" "custom_domain" { project_id = stackit_cdn_distribution.distribution.project_id distribution_id = stackit_cdn_distribution.distribution.distribution_id name = "${stackit_dns_record_set.dns_record.name}.${stackit_dns_zone.dns_zone.dns_name}" certificate = { certificate = %q private_key = %q } } `, configResources(regions), cert, key) } func configDatasources(regions, cert, key string) string { return fmt.Sprintf(` %s data "stackit_cdn_distribution" "distribution" { project_id = stackit_cdn_distribution.distribution.project_id distribution_id = stackit_cdn_distribution.distribution.distribution_id } data "stackit_cdn_custom_domain" "custom_domain" { project_id = stackit_cdn_custom_domain.custom_domain.project_id distribution_id = stackit_cdn_custom_domain.custom_domain.distribution_id name = stackit_cdn_custom_domain.custom_domain.name } `, configCustomDomainResources(regions, cert, key)) } func makeCertAndKey(t *testing.T, organization string) (cert, key []byte) { privateKey, err := rsa.GenerateKey(cryptoRand.Reader, 2048) if err != nil { t.Fatalf("failed to generate key: %s", err.Error()) } template := x509.Certificate{ SerialNumber: big.NewInt(1), Issuer: pkix.Name{CommonName: organization}, Subject: pkix.Name{ Organization: []string{organization}, }, NotBefore: time.Now(), NotAfter: time.Now().Add(time.Hour), KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, BasicConstraintsValid: true, } cert, err = x509.CreateCertificate( cryptoRand.Reader, &template, &template, &privateKey.PublicKey, privateKey, ) if err != nil { t.Fatalf("failed to generate cert: %s", err.Error()) } return pem.EncodeToMemory(&pem.Block{ Type: "CERTIFICATE", Bytes: cert, }), pem.EncodeToMemory(&pem.Block{ Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privateKey), }) } func TestAccCDNDistributionResource(t *testing.T) { fullDomainName := fmt.Sprintf("%s.%s", instanceResource["custom_domain_prefix"], instanceResource["dns_name"]) organization := fmt.Sprintf("organization-%s", uuid.NewString()) cert, key := makeCertAndKey(t, organization) organization_updated := fmt.Sprintf("organization-updated-%s", uuid.NewString()) cert_updated, key_updated := makeCertAndKey(t, organization_updated) resource.Test(t, resource.TestCase{ ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, CheckDestroy: testAccCheckCDNDistributionDestroy, Steps: []resource.TestStep{ // Distribution Create { Config: configResources(instanceResource["config_regions"]), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttrSet("stackit_cdn_distribution.distribution", "distribution_id"), resource.TestCheckResourceAttrSet("stackit_cdn_distribution.distribution", "created_at"), resource.TestCheckResourceAttrSet("stackit_cdn_distribution.distribution", "updated_at"), resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "domains.#", "1"), resource.TestCheckResourceAttrSet("stackit_cdn_distribution.distribution", "domains.0.name"), resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "domains.0.type", "managed"), resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "domains.0.status", "ACTIVE"), resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.regions.#", "2"), resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.regions.0", "EU"), resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.regions.1", "US"), resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.blocked_countries.#", "2"), resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.blocked_countries.0", "CU"), resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.blocked_countries.1", "AQ"), resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.optimizer.enabled", "true"), resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "project_id", testutil.ProjectId), resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "status", "ACTIVE"), ), }, // Wait step, that confirms the CNAME record has "propagated" { Config: configResources(instanceResource["config_regions"]), Check: func(_ *terraform.State) error { _, err := blockUntilDomainResolves(fullDomainName) return err }, }, // Custom Domain Create { Config: configCustomDomainResources(instanceResource["config_regions"], string(cert), string(key)), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr("stackit_cdn_custom_domain.custom_domain", "status", "ACTIVE"), resource.TestCheckResourceAttr("stackit_cdn_custom_domain.custom_domain", "name", fullDomainName), resource.TestCheckResourceAttrPair("stackit_cdn_distribution.distribution", "distribution_id", "stackit_cdn_custom_domain.custom_domain", "distribution_id"), resource.TestCheckResourceAttrPair("stackit_cdn_distribution.distribution", "project_id", "stackit_cdn_custom_domain.custom_domain", "project_id"), ), }, // Import { ResourceName: "stackit_cdn_distribution.distribution", ImportStateIdFunc: func(s *terraform.State) (string, error) { r, ok := s.RootModule().Resources["stackit_cdn_distribution.distribution"] if !ok { return "", fmt.Errorf("couldn't find resource stackit_cdn_distribution.distribution") } distributionId, ok := r.Primary.Attributes["distribution_id"] if !ok { return "", fmt.Errorf("couldn't find attribute distribution_id") } return fmt.Sprintf("%s,%s", testutil.ProjectId, distributionId), nil }, ImportState: true, ImportStateVerify: true, ImportStateVerifyIgnore: []string{"domains"}, // we added a domain in the meantime... }, { ResourceName: "stackit_cdn_custom_domain.custom_domain", ImportStateIdFunc: func(s *terraform.State) (string, error) { r, ok := s.RootModule().Resources["stackit_cdn_custom_domain.custom_domain"] if !ok { return "", fmt.Errorf("couldn't find resource stackit_cdn_custom_domain.custom_domain") } distributionId, ok := r.Primary.Attributes["distribution_id"] if !ok { return "", fmt.Errorf("couldn't find attribute distribution_id") } name, ok := r.Primary.Attributes["name"] if !ok { return "", fmt.Errorf("couldn't find attribute name") } return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, distributionId, name), nil }, ImportState: true, ImportStateVerify: true, ImportStateVerifyIgnore: []string{ "certificate.certificate", "certificate.private_key", }, }, // Data Source { Config: configDatasources(instanceResource["config_regions"], string(cert), string(key)), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttrSet("data.stackit_cdn_distribution.distribution", "distribution_id"), resource.TestCheckResourceAttrSet("data.stackit_cdn_distribution.distribution", "created_at"), resource.TestCheckResourceAttrSet("data.stackit_cdn_distribution.distribution", "updated_at"), resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "domains.#", "2"), resource.TestCheckResourceAttrSet("data.stackit_cdn_distribution.distribution", "domains.0.name"), resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "domains.1.name", fullDomainName), resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "domains.0.status", "ACTIVE"), resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "domains.1.status", "ACTIVE"), resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "domains.0.type", "managed"), resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "domains.1.type", "custom"), resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "config.regions.#", "2"), resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "config.regions.0", "EU"), resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "config.regions.1", "US"), resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.blocked_countries.#", "2"), resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.blocked_countries.0", "CU"), resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.blocked_countries.1", "AQ"), resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "config.optimizer.enabled", "true"), resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "project_id", testutil.ProjectId), resource.TestCheckResourceAttr("data.stackit_cdn_distribution.distribution", "status", "ACTIVE"), resource.TestCheckResourceAttr("data.stackit_cdn_custom_domain.custom_domain", "status", "ACTIVE"), resource.TestCheckResourceAttr("data.stackit_cdn_custom_domain.custom_domain", "certificate.version", "1"), resource.TestCheckResourceAttr("data.stackit_cdn_custom_domain.custom_domain", "name", fullDomainName), resource.TestCheckResourceAttrPair("stackit_cdn_distribution.distribution", "distribution_id", "stackit_cdn_custom_domain.custom_domain", "distribution_id"), ), }, // Update { Config: configCustomDomainResources(instanceResource["config_regions_updated"], string(cert_updated), string(key_updated)), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttrSet("stackit_cdn_distribution.distribution", "distribution_id"), resource.TestCheckResourceAttrSet("stackit_cdn_distribution.distribution", "created_at"), resource.TestCheckResourceAttrSet("stackit_cdn_distribution.distribution", "updated_at"), resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "domains.#", "2"), resource.TestCheckResourceAttrSet("stackit_cdn_distribution.distribution", "domains.0.name"), resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "domains.1.name", fullDomainName), resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "domains.0.status", "ACTIVE"), resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "domains.1.status", "ACTIVE"), resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "domains.0.type", "managed"), resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "domains.1.type", "custom"), resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.regions.#", "3"), resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.regions.0", "EU"), resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.regions.1", "US"), resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.regions.2", "ASIA"), resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.blocked_countries.#", "2"), resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.blocked_countries.0", "CU"), resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.blocked_countries.1", "AQ"), resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "config.optimizer.enabled", "true"), resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "project_id", testutil.ProjectId), resource.TestCheckResourceAttr("stackit_cdn_distribution.distribution", "status", "ACTIVE"), resource.TestCheckResourceAttr("stackit_cdn_custom_domain.custom_domain", "status", "ACTIVE"), resource.TestCheckResourceAttr("stackit_cdn_custom_domain.custom_domain", "certificate.version", "2"), resource.TestCheckResourceAttr("stackit_cdn_custom_domain.custom_domain", "name", fullDomainName), resource.TestCheckResourceAttrPair("stackit_cdn_distribution.distribution", "distribution_id", "stackit_cdn_custom_domain.custom_domain", "distribution_id"), resource.TestCheckResourceAttrPair("stackit_cdn_distribution.distribution", "project_id", "stackit_cdn_custom_domain.custom_domain", "project_id"), ), }, }, }) } func testAccCheckCDNDistributionDestroy(s *terraform.State) error { ctx := context.Background() var client *cdn.APIClient var err error if testutil.MongoDBFlexCustomEndpoint == "" { client, err = cdn.NewAPIClient() } else { client, err = cdn.NewAPIClient( config.WithEndpoint(testutil.MongoDBFlexCustomEndpoint), ) } if err != nil { return fmt.Errorf("creating client: %w", err) } distributionsToDestroy := []string{} for _, rs := range s.RootModule().Resources { if rs.Type != "stackit_mongodbflex_instance" { continue } distributionId := strings.Split(rs.Primary.ID, core.Separator)[1] distributionsToDestroy = append(distributionsToDestroy, distributionId) } for _, dist := range distributionsToDestroy { _, err := client.DeleteDistribution(ctx, testutil.ProjectId, dist).Execute() if err != nil { return fmt.Errorf("destroying CDN distribution %s during CheckDestroy: %w", dist, err) } _, err = wait.DeleteDistributionWaitHandler(ctx, client, testutil.ProjectId, dist).WaitWithContext(ctx) if err != nil { return fmt.Errorf("destroying CDN distribution %s during CheckDestroy: waiting for deletion %w", dist, err) } } return nil } const ( recordCheckInterval time.Duration = 3 * time.Second recordCheckAttempts = 100 // wait up to 5 minutes for record to be come available (normally takes less than 2 minutes) ) func blockUntilDomainResolves(domain string) (net.IP, error) { // wait until it becomes ready isReady := func() (net.IP, error) { ips, err := net.LookupIP(domain) if err != nil { return nil, fmt.Errorf("error looking up IP for domain %s: %w", domain, err) } for _, ip := range ips { if ip.String() != "" { return ip, nil } } return nil, fmt.Errorf("no IP for domain: %v", domain) } return retry(recordCheckAttempts, recordCheckInterval, isReady) } func retry[T any](attempts int, sleep time.Duration, f func() (T, error)) (T, error) { var zero T var errOuter error for i := 0; i < attempts; i++ { dist, err := f() if err == nil { return dist, nil } errOuter = err time.Sleep(sleep) } return zero, fmt.Errorf("retry timed out, last error: %w", errOuter) }