chore: cleanup alpha branch
This commit is contained in:
parent
c07c81b091
commit
df25ceffd4
374 changed files with 2 additions and 114477 deletions
|
|
@ -1,50 +0,0 @@
|
||||||
package access_token_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
_ "embed"
|
|
||||||
"regexp"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/hashicorp/terraform-plugin-testing/config"
|
|
||||||
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
|
|
||||||
"github.com/hashicorp/terraform-plugin-testing/knownvalue"
|
|
||||||
"github.com/hashicorp/terraform-plugin-testing/statecheck"
|
|
||||||
"github.com/hashicorp/terraform-plugin-testing/tfjsonpath"
|
|
||||||
"github.com/hashicorp/terraform-plugin-testing/tfversion"
|
|
||||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil"
|
|
||||||
)
|
|
||||||
|
|
||||||
//go:embed testdata/ephemeral_resource.tf
|
|
||||||
var ephemeralResourceConfig string
|
|
||||||
|
|
||||||
var testConfigVars = config.Variables{
|
|
||||||
"default_region": config.StringVariable(testutil.Region),
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAccEphemeralAccessToken(t *testing.T) {
|
|
||||||
resource.Test(t, resource.TestCase{
|
|
||||||
TerraformVersionChecks: []tfversion.TerraformVersionCheck{
|
|
||||||
tfversion.SkipBelow(tfversion.Version1_10_0),
|
|
||||||
},
|
|
||||||
ProtoV6ProviderFactories: testutil.TestEphemeralAccProtoV6ProviderFactories,
|
|
||||||
Steps: []resource.TestStep{
|
|
||||||
{
|
|
||||||
Config: ephemeralResourceConfig,
|
|
||||||
ConfigVariables: testConfigVars,
|
|
||||||
ConfigStateChecks: []statecheck.StateCheck{
|
|
||||||
statecheck.ExpectKnownValue(
|
|
||||||
"echo.example",
|
|
||||||
tfjsonpath.New("data").AtMapKey("access_token"),
|
|
||||||
knownvalue.NotNull(),
|
|
||||||
),
|
|
||||||
// JWT access tokens start with "ey" because the first part is base64-encoded JSON that begins with "{".
|
|
||||||
statecheck.ExpectKnownValue(
|
|
||||||
"echo.example",
|
|
||||||
tfjsonpath.New("data").AtMapKey("access_token"),
|
|
||||||
knownvalue.StringRegexp(regexp.MustCompile(`^ey`)),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
@ -1,132 +0,0 @@
|
||||||
package access_token
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/hashicorp/terraform-plugin-framework/ephemeral"
|
|
||||||
"github.com/hashicorp/terraform-plugin-framework/ephemeral/schema"
|
|
||||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
|
||||||
"github.com/stackitcloud/stackit-sdk-go/core/auth"
|
|
||||||
"github.com/stackitcloud/stackit-sdk-go/core/clients"
|
|
||||||
"github.com/stackitcloud/stackit-sdk-go/core/config"
|
|
||||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion"
|
|
||||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
|
|
||||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
_ ephemeral.EphemeralResource = &accessTokenEphemeralResource{}
|
|
||||||
_ ephemeral.EphemeralResourceWithConfigure = &accessTokenEphemeralResource{}
|
|
||||||
)
|
|
||||||
|
|
||||||
func NewAccessTokenEphemeralResource() ephemeral.EphemeralResource {
|
|
||||||
return &accessTokenEphemeralResource{}
|
|
||||||
}
|
|
||||||
|
|
||||||
type accessTokenEphemeralResource struct {
|
|
||||||
keyAuthConfig config.Configuration
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *accessTokenEphemeralResource) Configure(ctx context.Context, req ephemeral.ConfigureRequest, resp *ephemeral.ConfigureResponse) {
|
|
||||||
ephemeralProviderData, ok := conversion.ParseEphemeralProviderData(ctx, req.ProviderData, &resp.Diagnostics)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
features.CheckBetaResourcesEnabled(
|
|
||||||
ctx,
|
|
||||||
&ephemeralProviderData.ProviderData,
|
|
||||||
&resp.Diagnostics,
|
|
||||||
"stackit_access_token", "ephemeral_resource",
|
|
||||||
)
|
|
||||||
if resp.Diagnostics.HasError() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
e.keyAuthConfig = config.Configuration{
|
|
||||||
ServiceAccountKey: ephemeralProviderData.ServiceAccountKey,
|
|
||||||
ServiceAccountKeyPath: ephemeralProviderData.ServiceAccountKeyPath,
|
|
||||||
PrivateKeyPath: ephemeralProviderData.PrivateKey,
|
|
||||||
PrivateKey: ephemeralProviderData.PrivateKeyPath,
|
|
||||||
TokenCustomUrl: ephemeralProviderData.TokenCustomEndpoint,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type ephemeralTokenModel struct {
|
|
||||||
AccessToken types.String `tfsdk:"access_token"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *accessTokenEphemeralResource) Metadata(_ context.Context, req ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) {
|
|
||||||
resp.TypeName = req.ProviderTypeName + "_access_token"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *accessTokenEphemeralResource) Schema(_ context.Context, _ ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) {
|
|
||||||
description := features.AddBetaDescription(
|
|
||||||
fmt.Sprintf(
|
|
||||||
"%s\n\n%s",
|
|
||||||
"Ephemeral resource that generates a short-lived STACKIT access token (JWT) using a service account key. "+
|
|
||||||
"A new token is generated each time the resource is evaluated, and it remains consistent for the duration of a Terraform operation. "+
|
|
||||||
"If a private key is not explicitly provided, the provider attempts to extract it from the service account key instead. "+
|
|
||||||
"Access tokens generated from service account keys expire after 60 minutes.",
|
|
||||||
"~> Service account key credentials must be configured either in the STACKIT provider configuration or via environment variables (see example below). "+
|
|
||||||
"If any other authentication method is configured, this ephemeral resource will fail with an error.",
|
|
||||||
),
|
|
||||||
core.EphemeralResource,
|
|
||||||
)
|
|
||||||
|
|
||||||
resp.Schema = schema.Schema{
|
|
||||||
Description: description,
|
|
||||||
Attributes: map[string]schema.Attribute{
|
|
||||||
"access_token": schema.StringAttribute{
|
|
||||||
Description: "JWT access token for STACKIT API authentication.",
|
|
||||||
Computed: true,
|
|
||||||
Sensitive: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *accessTokenEphemeralResource) Open(ctx context.Context, req ephemeral.OpenRequest, resp *ephemeral.OpenResponse) {
|
|
||||||
var model ephemeralTokenModel
|
|
||||||
|
|
||||||
resp.Diagnostics.Append(req.Config.Get(ctx, &model)...)
|
|
||||||
if resp.Diagnostics.HasError() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
accessToken, err := getAccessToken(&e.keyAuthConfig)
|
|
||||||
if err != nil {
|
|
||||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Access token generation failed", err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
model.AccessToken = types.StringValue(accessToken)
|
|
||||||
resp.Diagnostics.Append(resp.Result.Set(ctx, model)...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// getAccessToken initializes authentication using the provided config and returns an access token via the KeyFlow mechanism.
|
|
||||||
func getAccessToken(keyAuthConfig *config.Configuration) (string, error) {
|
|
||||||
roundTripper, err := auth.KeyAuth(keyAuthConfig)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf(
|
|
||||||
"failed to initialize authentication: %w. "+
|
|
||||||
"Make sure service account credentials are configured either in the provider configuration or via environment variables",
|
|
||||||
err,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Type assert to access token functionality
|
|
||||||
client, ok := roundTripper.(*clients.KeyFlow)
|
|
||||||
if !ok {
|
|
||||||
return "", fmt.Errorf("internal error: expected *clients.KeyFlow, but received a different implementation of http.RoundTripper")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Retrieve the access token
|
|
||||||
accessToken, err := client.GetAccessToken()
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("error obtaining access token: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return accessToken, nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,253 +0,0 @@
|
||||||
package access_token
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/rand"
|
|
||||||
"crypto/rsa"
|
|
||||||
"crypto/x509"
|
|
||||||
_ "embed"
|
|
||||||
"encoding/json"
|
|
||||||
"encoding/pem"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/stackitcloud/stackit-sdk-go/core/clients"
|
|
||||||
"github.com/stackitcloud/stackit-sdk-go/core/config"
|
|
||||||
)
|
|
||||||
|
|
||||||
//go:embed testdata/service_account.json
|
|
||||||
var testServiceAccountKey string
|
|
||||||
|
|
||||||
func startMockTokenServer() *httptest.Server {
|
|
||||||
handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
||||||
resp := clients.TokenResponseBody{
|
|
||||||
AccessToken: "mock_access_token",
|
|
||||||
RefreshToken: "mock_refresh_token",
|
|
||||||
TokenType: "Bearer",
|
|
||||||
ExpiresIn: int(time.Now().Add(time.Hour).Unix()),
|
|
||||||
Scope: "mock_scope",
|
|
||||||
}
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
_ = json.NewEncoder(w).Encode(resp)
|
|
||||||
})
|
|
||||||
return httptest.NewServer(handler)
|
|
||||||
}
|
|
||||||
|
|
||||||
func generatePrivateKey() (string, error) {
|
|
||||||
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
privateKeyPEM := &pem.Block{
|
|
||||||
Type: "RSA PRIVATE KEY",
|
|
||||||
Bytes: x509.MarshalPKCS1PrivateKey(privateKey),
|
|
||||||
}
|
|
||||||
return string(pem.EncodeToMemory(privateKeyPEM)), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func writeTempPEMFile(t *testing.T, pemContent string) string {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
tmpFile, err := os.CreateTemp("", "stackit_test_private_key_*.pem")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := tmpFile.WriteString(pemContent); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := tmpFile.Close(); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Cleanup(func() {
|
|
||||||
_ = os.Remove(tmpFile.Name())
|
|
||||||
})
|
|
||||||
|
|
||||||
return tmpFile.Name()
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetAccessToken(t *testing.T) {
|
|
||||||
mockServer := startMockTokenServer()
|
|
||||||
t.Cleanup(mockServer.Close)
|
|
||||||
|
|
||||||
privateKey, err := generatePrivateKey()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
description string
|
|
||||||
setupEnv func()
|
|
||||||
cleanupEnv func()
|
|
||||||
cfgFactory func() *config.Configuration
|
|
||||||
expectError bool
|
|
||||||
expected string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
description: "should return token when service account key passed by value",
|
|
||||||
cfgFactory: func() *config.Configuration {
|
|
||||||
return &config.Configuration{
|
|
||||||
ServiceAccountKey: testServiceAccountKey,
|
|
||||||
PrivateKey: privateKey,
|
|
||||||
TokenCustomUrl: mockServer.URL,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
expectError: false,
|
|
||||||
expected: "mock_access_token",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "should return token when service account key is loaded from file path",
|
|
||||||
cfgFactory: func() *config.Configuration {
|
|
||||||
return &config.Configuration{
|
|
||||||
ServiceAccountKeyPath: "testdata/service_account.json",
|
|
||||||
PrivateKey: privateKey,
|
|
||||||
TokenCustomUrl: mockServer.URL,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
expectError: false,
|
|
||||||
expected: "mock_access_token",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "should fail when private key is invalid",
|
|
||||||
cfgFactory: func() *config.Configuration {
|
|
||||||
return &config.Configuration{
|
|
||||||
ServiceAccountKey: "invalid-json",
|
|
||||||
PrivateKey: "invalid-PEM",
|
|
||||||
TokenCustomUrl: mockServer.URL,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
expectError: true,
|
|
||||||
expected: "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "should return token when service account key is set via env",
|
|
||||||
setupEnv: func() {
|
|
||||||
_ = os.Setenv("STACKIT_SERVICE_ACCOUNT_KEY", testServiceAccountKey)
|
|
||||||
},
|
|
||||||
cleanupEnv: func() {
|
|
||||||
_ = os.Unsetenv("STACKIT_SERVICE_ACCOUNT_KEY")
|
|
||||||
},
|
|
||||||
cfgFactory: func() *config.Configuration {
|
|
||||||
return &config.Configuration{
|
|
||||||
PrivateKey: privateKey,
|
|
||||||
TokenCustomUrl: mockServer.URL,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
expectError: false,
|
|
||||||
expected: "mock_access_token",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "should return token when service account key path is set via env",
|
|
||||||
setupEnv: func() {
|
|
||||||
_ = os.Setenv("STACKIT_SERVICE_ACCOUNT_KEY_PATH", "testdata/service_account.json")
|
|
||||||
},
|
|
||||||
cleanupEnv: func() {
|
|
||||||
_ = os.Unsetenv("STACKIT_SERVICE_ACCOUNT_KEY_PATH")
|
|
||||||
},
|
|
||||||
cfgFactory: func() *config.Configuration {
|
|
||||||
return &config.Configuration{
|
|
||||||
PrivateKey: privateKey,
|
|
||||||
TokenCustomUrl: mockServer.URL,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
expectError: false,
|
|
||||||
expected: "mock_access_token",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "should return token when private key is set via env",
|
|
||||||
setupEnv: func() {
|
|
||||||
_ = os.Setenv("STACKIT_PRIVATE_KEY", privateKey)
|
|
||||||
},
|
|
||||||
cleanupEnv: func() {
|
|
||||||
_ = os.Unsetenv("STACKIT_PRIVATE_KEY")
|
|
||||||
},
|
|
||||||
cfgFactory: func() *config.Configuration {
|
|
||||||
return &config.Configuration{
|
|
||||||
ServiceAccountKey: testServiceAccountKey,
|
|
||||||
TokenCustomUrl: mockServer.URL,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
expectError: false,
|
|
||||||
expected: "mock_access_token",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "should return token when private key path is set via env",
|
|
||||||
setupEnv: func() {
|
|
||||||
// Write temp file and set env
|
|
||||||
tmpFile := writeTempPEMFile(t, privateKey)
|
|
||||||
_ = os.Setenv("STACKIT_PRIVATE_KEY_PATH", tmpFile)
|
|
||||||
},
|
|
||||||
cleanupEnv: func() {
|
|
||||||
_ = os.Unsetenv("STACKIT_PRIVATE_KEY_PATH")
|
|
||||||
},
|
|
||||||
cfgFactory: func() *config.Configuration {
|
|
||||||
return &config.Configuration{
|
|
||||||
ServiceAccountKey: testServiceAccountKey,
|
|
||||||
TokenCustomUrl: mockServer.URL,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
expectError: false,
|
|
||||||
expected: "mock_access_token",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "should fail when no service account key or private key is set",
|
|
||||||
cfgFactory: func() *config.Configuration {
|
|
||||||
return &config.Configuration{
|
|
||||||
TokenCustomUrl: mockServer.URL,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
expectError: true,
|
|
||||||
expected: "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "should fail when no service account key or private key is set via env",
|
|
||||||
setupEnv: func() {
|
|
||||||
_ = os.Unsetenv("STACKIT_SERVICE_ACCOUNT_KEY")
|
|
||||||
_ = os.Unsetenv("STACKIT_SERVICE_ACCOUNT_KEY_PATH")
|
|
||||||
_ = os.Unsetenv("STACKIT_PRIVATE_KEY")
|
|
||||||
_ = os.Unsetenv("STACKIT_PRIVATE_KEY_PATH")
|
|
||||||
},
|
|
||||||
cleanupEnv: func() {
|
|
||||||
// Restore original environment variables
|
|
||||||
},
|
|
||||||
cfgFactory: func() *config.Configuration {
|
|
||||||
return &config.Configuration{
|
|
||||||
TokenCustomUrl: mockServer.URL,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
expectError: true,
|
|
||||||
expected: "",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.description, func(t *testing.T) {
|
|
||||||
if tt.setupEnv != nil {
|
|
||||||
tt.setupEnv()
|
|
||||||
}
|
|
||||||
if tt.cleanupEnv != nil {
|
|
||||||
defer tt.cleanupEnv()
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg := tt.cfgFactory()
|
|
||||||
|
|
||||||
token, err := getAccessToken(cfg)
|
|
||||||
if tt.expectError {
|
|
||||||
if err == nil {
|
|
||||||
t.Errorf("expected error but got none for test case '%s'", tt.description)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("did not expect error but got: %v for test case '%s'", err, tt.description)
|
|
||||||
}
|
|
||||||
if token != tt.expected {
|
|
||||||
t.Errorf("expected token '%s', got '%s' for test case '%s'", tt.expected, token, tt.description)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
variable "default_region" {}
|
|
||||||
|
|
||||||
provider "stackit" {
|
|
||||||
default_region = var.default_region
|
|
||||||
enable_beta_resources = true
|
|
||||||
}
|
|
||||||
|
|
||||||
ephemeral "stackit_access_token" "example" {}
|
|
||||||
|
|
||||||
provider "echo" {
|
|
||||||
data = ephemeral.stackit_access_token.example
|
|
||||||
}
|
|
||||||
|
|
||||||
resource "echo" "example" {
|
|
||||||
}
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
{
|
|
||||||
"id": "cad1592f-1fe6-4fd1-a6d6-ccef94b01697",
|
|
||||||
"publicKey": "-----BEGIN PUBLIC KEY-----\nABC\n-----END PUBLIC KEY-----",
|
|
||||||
"createdAt": "2025-11-25T15:19:30.689+00:00",
|
|
||||||
"keyType": "USER_MANAGED",
|
|
||||||
"keyOrigin": "GENERATED",
|
|
||||||
"keyAlgorithm": "RSA_2048",
|
|
||||||
"active": true,
|
|
||||||
"credentials": {
|
|
||||||
"kid": "cad1592f-1fe6-4fd1-a6d6-ccef94b01697",
|
|
||||||
"iss": "foo.bar@sa.stackit.cloud",
|
|
||||||
"sub": "cad1592f-1fe6-4fd1-a6d6-ccef94b01697",
|
|
||||||
"aud": "https://stackit-service-account-prod.apps.01.cf.eu01.stackit.cloud",
|
|
||||||
"privateKey": "-----BEGIN PRIVATE KEY-----\nABC\n-----END PRIVATE KEY-----"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,407 +0,0 @@
|
||||||
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, geofencingCountries []string) string {
|
|
||||||
var quotedCountries []string
|
|
||||||
for _, country := range geofencingCountries {
|
|
||||||
quotedCountries = append(quotedCountries, fmt.Sprintf(`%q`, country))
|
|
||||||
}
|
|
||||||
|
|
||||||
geofencingList := strings.Join(quotedCountries, ",")
|
|
||||||
return fmt.Sprintf(`
|
|
||||||
%s
|
|
||||||
|
|
||||||
resource "stackit_cdn_distribution" "distribution" {
|
|
||||||
project_id = "%s"
|
|
||||||
config = {
|
|
||||||
backend = {
|
|
||||||
type = "http"
|
|
||||||
origin_url = "%s"
|
|
||||||
geofencing = {
|
|
||||||
"%s" = [%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"], instanceResource["config_backend_origin_url"], geofencingList,
|
|
||||||
regions, instanceResource["blocked_countries"], testutil.ProjectId, instanceResource["dns_name"],
|
|
||||||
testutil.ProjectId, instanceResource["custom_domain_prefix"])
|
|
||||||
}
|
|
||||||
|
|
||||||
func configCustomDomainResources(regions, cert, key string, geofencingCountries []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, geofencingCountries), cert, key)
|
|
||||||
}
|
|
||||||
|
|
||||||
func configDatasources(regions, cert, key string, geofencingCountries []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, geofencingCountries))
|
|
||||||
}
|
|
||||||
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)
|
|
||||||
geofencing := []string{"DE", "ES"}
|
|
||||||
|
|
||||||
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"], geofencing),
|
|
||||||
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",
|
|
||||||
fmt.Sprintf("config.backend.geofencing.%s.0", instanceResource["config_backend_origin_url"]),
|
|
||||||
"DE",
|
|
||||||
),
|
|
||||||
resource.TestCheckResourceAttr(
|
|
||||||
"stackit_cdn_distribution.distribution",
|
|
||||||
fmt.Sprintf("config.backend.geofencing.%s.1", instanceResource["config_backend_origin_url"]),
|
|
||||||
"ES",
|
|
||||||
),
|
|
||||||
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"], geofencing),
|
|
||||||
Check: func(_ *terraform.State) error {
|
|
||||||
_, err := blockUntilDomainResolves(fullDomainName)
|
|
||||||
return err
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// Custom Domain Create
|
|
||||||
{
|
|
||||||
Config: configCustomDomainResources(instanceResource["config_regions"], string(cert), string(key), geofencing),
|
|
||||||
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), geofencing),
|
|
||||||
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",
|
|
||||||
fmt.Sprintf("config.backend.geofencing.%s.0", instanceResource["config_backend_origin_url"]),
|
|
||||||
"DE",
|
|
||||||
),
|
|
||||||
resource.TestCheckResourceAttr(
|
|
||||||
"data.stackit_cdn_distribution.distribution",
|
|
||||||
fmt.Sprintf("config.backend.geofencing.%s.1", instanceResource["config_backend_origin_url"]),
|
|
||||||
"ES",
|
|
||||||
),
|
|
||||||
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), geofencing),
|
|
||||||
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() != "<nil>" {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
@ -1,236 +0,0 @@
|
||||||
package cdn
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion"
|
|
||||||
cdnUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/cdn/utils"
|
|
||||||
|
|
||||||
"github.com/hashicorp/terraform-plugin-framework/attr"
|
|
||||||
"github.com/hashicorp/terraform-plugin-framework/datasource"
|
|
||||||
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
|
|
||||||
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
|
|
||||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
|
||||||
"github.com/hashicorp/terraform-plugin-log/tflog"
|
|
||||||
"github.com/stackitcloud/stackit-sdk-go/core/oapierror"
|
|
||||||
"github.com/stackitcloud/stackit-sdk-go/services/cdn"
|
|
||||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
|
|
||||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features"
|
|
||||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Ensure the implementation satisfies the expected interfaces.
|
|
||||||
var (
|
|
||||||
_ datasource.DataSource = &customDomainDataSource{}
|
|
||||||
_ datasource.DataSourceWithConfigure = &customDomainDataSource{}
|
|
||||||
)
|
|
||||||
|
|
||||||
var certificateDataSourceTypes = map[string]attr.Type{
|
|
||||||
"version": types.Int64Type,
|
|
||||||
}
|
|
||||||
|
|
||||||
type customDomainDataSource struct {
|
|
||||||
client *cdn.APIClient
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewCustomDomainDataSource() datasource.DataSource {
|
|
||||||
return &customDomainDataSource{}
|
|
||||||
}
|
|
||||||
|
|
||||||
type customDomainDataSourceModel struct {
|
|
||||||
ID types.String `tfsdk:"id"`
|
|
||||||
DistributionId types.String `tfsdk:"distribution_id"`
|
|
||||||
ProjectId types.String `tfsdk:"project_id"`
|
|
||||||
Name types.String `tfsdk:"name"`
|
|
||||||
Status types.String `tfsdk:"status"`
|
|
||||||
Errors types.List `tfsdk:"errors"`
|
|
||||||
Certificate types.Object `tfsdk:"certificate"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *customDomainDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
|
|
||||||
providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
features.CheckBetaResourcesEnabled(ctx, &providerData, &resp.Diagnostics, "stackit_cdn_custom_domain", core.Datasource)
|
|
||||||
if resp.Diagnostics.HasError() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
apiClient := cdnUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics)
|
|
||||||
if resp.Diagnostics.HasError() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
d.client = apiClient
|
|
||||||
tflog.Info(ctx, "CDN client configured")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *customDomainDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
|
|
||||||
resp.TypeName = req.ProviderTypeName + "_cdn_custom_domain"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *customDomainDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
|
|
||||||
resp.Schema = schema.Schema{
|
|
||||||
MarkdownDescription: features.AddBetaDescription("CDN distribution data source schema.", core.Datasource),
|
|
||||||
Description: "CDN distribution data source schema.",
|
|
||||||
Attributes: map[string]schema.Attribute{
|
|
||||||
"id": schema.StringAttribute{
|
|
||||||
Description: customDomainSchemaDescriptions["id"],
|
|
||||||
Computed: true,
|
|
||||||
},
|
|
||||||
"name": schema.StringAttribute{
|
|
||||||
Description: customDomainSchemaDescriptions["name"],
|
|
||||||
Required: true,
|
|
||||||
},
|
|
||||||
"distribution_id": schema.StringAttribute{
|
|
||||||
Description: customDomainSchemaDescriptions["distribution_id"],
|
|
||||||
Required: true,
|
|
||||||
Validators: []validator.String{validate.UUID()},
|
|
||||||
},
|
|
||||||
"project_id": schema.StringAttribute{
|
|
||||||
Description: customDomainSchemaDescriptions["project_id"],
|
|
||||||
Required: true,
|
|
||||||
},
|
|
||||||
"status": schema.StringAttribute{
|
|
||||||
Computed: true,
|
|
||||||
Description: customDomainSchemaDescriptions["status"],
|
|
||||||
},
|
|
||||||
"errors": schema.ListAttribute{
|
|
||||||
ElementType: types.StringType,
|
|
||||||
Computed: true,
|
|
||||||
Description: customDomainSchemaDescriptions["errors"],
|
|
||||||
},
|
|
||||||
"certificate": schema.SingleNestedAttribute{
|
|
||||||
Description: certificateSchemaDescriptions["main"],
|
|
||||||
Optional: true,
|
|
||||||
Attributes: map[string]schema.Attribute{
|
|
||||||
"version": schema.Int64Attribute{
|
|
||||||
Description: certificateSchemaDescriptions["version"],
|
|
||||||
Computed: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *customDomainDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform
|
|
||||||
var model customDomainDataSourceModel // Use the new data source model
|
|
||||||
diags := req.Config.Get(ctx, &model)
|
|
||||||
resp.Diagnostics.Append(diags...)
|
|
||||||
if resp.Diagnostics.HasError() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx = core.InitProviderContext(ctx)
|
|
||||||
|
|
||||||
projectId := model.ProjectId.ValueString()
|
|
||||||
ctx = tflog.SetField(ctx, "project_id", projectId)
|
|
||||||
distributionId := model.DistributionId.ValueString()
|
|
||||||
ctx = tflog.SetField(ctx, "distribution_id", distributionId)
|
|
||||||
name := model.Name.ValueString()
|
|
||||||
ctx = tflog.SetField(ctx, "name", name)
|
|
||||||
|
|
||||||
customDomainResp, err := r.client.GetCustomDomain(ctx, projectId, distributionId, name).Execute()
|
|
||||||
if err != nil {
|
|
||||||
var oapiErr *oapierror.GenericOpenAPIError
|
|
||||||
if errors.As(err, &oapiErr) {
|
|
||||||
if oapiErr.StatusCode == http.StatusNotFound {
|
|
||||||
resp.State.RemoveResource(ctx)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading CDN custom domain", fmt.Sprintf("Calling API: %v", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx = core.LogResponse(ctx)
|
|
||||||
|
|
||||||
// Call the new data source mapping function
|
|
||||||
err = mapCustomDomainDataSourceFields(customDomainResp, &model, projectId, distributionId)
|
|
||||||
if err != nil {
|
|
||||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading CDN custom domain", fmt.Sprintf("Processing API payload: %v", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set refreshed state
|
|
||||||
diags = resp.State.Set(ctx, model)
|
|
||||||
resp.Diagnostics.Append(diags...)
|
|
||||||
if resp.Diagnostics.HasError() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
tflog.Info(ctx, "CDN custom domain read")
|
|
||||||
}
|
|
||||||
|
|
||||||
func mapCustomDomainDataSourceFields(customDomainResponse *cdn.GetCustomDomainResponse, model *customDomainDataSourceModel, projectId, distributionId string) error {
|
|
||||||
if customDomainResponse == nil {
|
|
||||||
return fmt.Errorf("response input is nil")
|
|
||||||
}
|
|
||||||
if model == nil {
|
|
||||||
return fmt.Errorf("model input is nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
if customDomainResponse.CustomDomain == nil {
|
|
||||||
return fmt.Errorf("CustomDomain is missing in response")
|
|
||||||
}
|
|
||||||
|
|
||||||
if customDomainResponse.CustomDomain.Name == nil {
|
|
||||||
return fmt.Errorf("name is missing in response")
|
|
||||||
}
|
|
||||||
if customDomainResponse.CustomDomain.Status == nil {
|
|
||||||
return fmt.Errorf("status missing in response")
|
|
||||||
}
|
|
||||||
|
|
||||||
normalizedCert, err := normalizeCertificate(customDomainResponse.Certificate)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Certificate error in normalizer: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the certificate is managed, the certificate block in the state should be null.
|
|
||||||
if normalizedCert.Type == "managed" {
|
|
||||||
model.Certificate = types.ObjectNull(certificateDataSourceTypes)
|
|
||||||
} else {
|
|
||||||
// For custom certificates, we only care about the version.
|
|
||||||
version := types.Int64Null()
|
|
||||||
if normalizedCert.Version != nil {
|
|
||||||
version = types.Int64Value(*normalizedCert.Version)
|
|
||||||
}
|
|
||||||
|
|
||||||
certificateObj, diags := types.ObjectValue(certificateDataSourceTypes, map[string]attr.Value{
|
|
||||||
"version": version,
|
|
||||||
})
|
|
||||||
if diags.HasError() {
|
|
||||||
return fmt.Errorf("failed to map certificate: %w", core.DiagsToError(diags))
|
|
||||||
}
|
|
||||||
model.Certificate = certificateObj
|
|
||||||
}
|
|
||||||
|
|
||||||
model.ID = types.StringValue(fmt.Sprintf("%s,%s,%s", projectId, distributionId, *customDomainResponse.CustomDomain.Name))
|
|
||||||
model.Status = types.StringValue(string(*customDomainResponse.CustomDomain.Status))
|
|
||||||
|
|
||||||
customDomainErrors := []attr.Value{}
|
|
||||||
if customDomainResponse.CustomDomain.Errors != nil {
|
|
||||||
for _, e := range *customDomainResponse.CustomDomain.Errors {
|
|
||||||
if e.En == nil {
|
|
||||||
return fmt.Errorf("error description missing")
|
|
||||||
}
|
|
||||||
customDomainErrors = append(customDomainErrors, types.StringValue(*e.En))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
modelErrors, diags := types.ListValue(types.StringType, customDomainErrors)
|
|
||||||
if diags.HasError() {
|
|
||||||
return core.DiagsToError(diags)
|
|
||||||
}
|
|
||||||
model.Errors = modelErrors
|
|
||||||
|
|
||||||
// Also map the fields back to the model from the config
|
|
||||||
model.ProjectId = types.StringValue(projectId)
|
|
||||||
model.DistributionId = types.StringValue(distributionId)
|
|
||||||
model.Name = types.StringValue(*customDomainResponse.CustomDomain.Name)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,137 +0,0 @@
|
||||||
package cdn
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/google/go-cmp/cmp"
|
|
||||||
"github.com/hashicorp/terraform-plugin-framework/attr"
|
|
||||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
|
||||||
"github.com/stackitcloud/stackit-sdk-go/services/cdn"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestMapDataSourceFields(t *testing.T) {
|
|
||||||
emtpyErrorsList := types.ListValueMust(types.StringType, []attr.Value{})
|
|
||||||
|
|
||||||
// Expected certificate object when a custom certificate is returned
|
|
||||||
certAttributes := map[string]attr.Value{
|
|
||||||
"version": types.Int64Value(3),
|
|
||||||
}
|
|
||||||
certificateObj, _ := types.ObjectValue(certificateDataSourceTypes, certAttributes)
|
|
||||||
|
|
||||||
// Helper to create expected model instances
|
|
||||||
expectedModel := func(mods ...func(*customDomainDataSourceModel)) *customDomainDataSourceModel {
|
|
||||||
model := &customDomainDataSourceModel{
|
|
||||||
ID: types.StringValue("test-project-id,test-distribution-id,https://testdomain.com"),
|
|
||||||
DistributionId: types.StringValue("test-distribution-id"),
|
|
||||||
ProjectId: types.StringValue("test-project-id"),
|
|
||||||
Name: types.StringValue("https://testdomain.com"),
|
|
||||||
Status: types.StringValue("ACTIVE"),
|
|
||||||
Errors: emtpyErrorsList,
|
|
||||||
Certificate: types.ObjectUnknown(certificateDataSourceTypes),
|
|
||||||
}
|
|
||||||
for _, mod := range mods {
|
|
||||||
mod(model)
|
|
||||||
}
|
|
||||||
return model
|
|
||||||
}
|
|
||||||
|
|
||||||
// API response fixtures for custom and managed certificates
|
|
||||||
customType := "custom"
|
|
||||||
customVersion := int64(3)
|
|
||||||
getRespCustom := cdn.GetCustomDomainResponseGetCertificateAttributeType(&cdn.GetCustomDomainResponseCertificate{
|
|
||||||
GetCustomDomainCustomCertificate: &cdn.GetCustomDomainCustomCertificate{
|
|
||||||
Type: &customType,
|
|
||||||
Version: &customVersion,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
managedType := "managed"
|
|
||||||
getRespManaged := cdn.GetCustomDomainResponseGetCertificateAttributeType(&cdn.GetCustomDomainResponseCertificate{
|
|
||||||
GetCustomDomainManagedCertificate: &cdn.GetCustomDomainManagedCertificate{
|
|
||||||
Type: &managedType,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
// Helper to create API response fixtures
|
|
||||||
customDomainFixture := func(mods ...func(*cdn.GetCustomDomainResponse)) *cdn.GetCustomDomainResponse {
|
|
||||||
distribution := &cdn.CustomDomain{
|
|
||||||
Errors: &[]cdn.StatusError{},
|
|
||||||
Name: cdn.PtrString("https://testdomain.com"),
|
|
||||||
Status: cdn.DOMAINSTATUS_ACTIVE.Ptr(),
|
|
||||||
}
|
|
||||||
customDomainResponse := &cdn.GetCustomDomainResponse{
|
|
||||||
CustomDomain: distribution,
|
|
||||||
Certificate: getRespCustom,
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, mod := range mods {
|
|
||||||
mod(customDomainResponse)
|
|
||||||
}
|
|
||||||
return customDomainResponse
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test cases
|
|
||||||
tests := map[string]struct {
|
|
||||||
Input *cdn.GetCustomDomainResponse
|
|
||||||
Expected *customDomainDataSourceModel
|
|
||||||
IsValid bool
|
|
||||||
}{
|
|
||||||
"happy_path_custom_cert": {
|
|
||||||
Expected: expectedModel(func(m *customDomainDataSourceModel) {
|
|
||||||
m.Certificate = certificateObj
|
|
||||||
}),
|
|
||||||
Input: customDomainFixture(),
|
|
||||||
IsValid: true,
|
|
||||||
},
|
|
||||||
"happy_path_managed_cert": {
|
|
||||||
Expected: expectedModel(func(m *customDomainDataSourceModel) {
|
|
||||||
m.Certificate = types.ObjectNull(certificateDataSourceTypes)
|
|
||||||
}),
|
|
||||||
Input: customDomainFixture(func(gcdr *cdn.GetCustomDomainResponse) {
|
|
||||||
gcdr.Certificate = getRespManaged
|
|
||||||
}),
|
|
||||||
IsValid: true,
|
|
||||||
},
|
|
||||||
"happy_path_status_error": {
|
|
||||||
Expected: expectedModel(func(m *customDomainDataSourceModel) {
|
|
||||||
m.Status = types.StringValue("ERROR")
|
|
||||||
m.Certificate = certificateObj
|
|
||||||
}),
|
|
||||||
Input: customDomainFixture(func(d *cdn.GetCustomDomainResponse) {
|
|
||||||
d.CustomDomain.Status = cdn.DOMAINSTATUS_ERROR.Ptr()
|
|
||||||
}),
|
|
||||||
IsValid: true,
|
|
||||||
},
|
|
||||||
"sad_path_response_nil": {
|
|
||||||
Expected: expectedModel(),
|
|
||||||
Input: nil,
|
|
||||||
IsValid: false,
|
|
||||||
},
|
|
||||||
"sad_path_name_missing": {
|
|
||||||
Expected: expectedModel(),
|
|
||||||
Input: customDomainFixture(func(d *cdn.GetCustomDomainResponse) {
|
|
||||||
d.CustomDomain.Name = nil
|
|
||||||
}),
|
|
||||||
IsValid: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for tn, tc := range tests {
|
|
||||||
t.Run(tn, func(t *testing.T) {
|
|
||||||
model := &customDomainDataSourceModel{}
|
|
||||||
err := mapCustomDomainDataSourceFields(tc.Input, model, "test-project-id", "test-distribution-id")
|
|
||||||
|
|
||||||
if err != nil && tc.IsValid {
|
|
||||||
t.Fatalf("Error mapping fields: %v", err)
|
|
||||||
}
|
|
||||||
if err == nil && !tc.IsValid {
|
|
||||||
t.Fatalf("Should have failed")
|
|
||||||
}
|
|
||||||
if tc.IsValid {
|
|
||||||
diff := cmp.Diff(tc.Expected, model)
|
|
||||||
if diff != "" {
|
|
||||||
t.Fatalf("Mapped model not as expected (-want +got):\n%s", diff)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,534 +0,0 @@
|
||||||
package cdn
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/base64"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils"
|
|
||||||
|
|
||||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion"
|
|
||||||
cdnUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/cdn/utils"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/hashicorp/terraform-plugin-framework/attr"
|
|
||||||
"github.com/hashicorp/terraform-plugin-framework/path"
|
|
||||||
"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/schema/validator"
|
|
||||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
|
||||||
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
|
|
||||||
"github.com/hashicorp/terraform-plugin-log/tflog"
|
|
||||||
"github.com/stackitcloud/stackit-sdk-go/core/oapierror"
|
|
||||||
"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/features"
|
|
||||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Ensure the implementation satisfies the expected interfaces.
|
|
||||||
var (
|
|
||||||
_ resource.Resource = &customDomainResource{}
|
|
||||||
_ resource.ResourceWithConfigure = &customDomainResource{}
|
|
||||||
_ resource.ResourceWithImportState = &customDomainResource{}
|
|
||||||
)
|
|
||||||
var certificateSchemaDescriptions = map[string]string{
|
|
||||||
"main": "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.",
|
|
||||||
"certificate": "The PEM-encoded TLS certificate. Required for custom certificates.",
|
|
||||||
"private_key": "The PEM-encoded private key for the certificate. Required for custom certificates. The certificate will be updated if this field is changed.",
|
|
||||||
"version": "A version identifier for the certificate. Required for custom certificates. The certificate will be updated if this field is changed.",
|
|
||||||
}
|
|
||||||
|
|
||||||
var certificateTypes = map[string]attr.Type{
|
|
||||||
"version": types.Int64Type,
|
|
||||||
"certificate": types.StringType,
|
|
||||||
"private_key": types.StringType,
|
|
||||||
}
|
|
||||||
|
|
||||||
var customDomainSchemaDescriptions = map[string]string{
|
|
||||||
"id": "Terraform's internal resource identifier. It is structured as \"`project_id`,`distribution_id`\".",
|
|
||||||
"distribution_id": "CDN distribution ID",
|
|
||||||
"project_id": "STACKIT project ID associated with the distribution",
|
|
||||||
"status": "Status of the distribution",
|
|
||||||
"errors": "List of distribution errors",
|
|
||||||
}
|
|
||||||
|
|
||||||
type CertificateModel struct {
|
|
||||||
Certificate types.String `tfsdk:"certificate"`
|
|
||||||
PrivateKey types.String `tfsdk:"private_key"`
|
|
||||||
Version types.Int64 `tfsdk:"version"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type CustomDomainModel struct {
|
|
||||||
ID types.String `tfsdk:"id"` // Required by Terraform
|
|
||||||
DistributionId types.String `tfsdk:"distribution_id"` // DistributionID associated with the cdn distribution
|
|
||||||
ProjectId types.String `tfsdk:"project_id"` // ProjectId associated with the cdn distribution
|
|
||||||
Name types.String `tfsdk:"name"` // The custom domain
|
|
||||||
Status types.String `tfsdk:"status"` // The status of the cdn distribution
|
|
||||||
Errors types.List `tfsdk:"errors"` // Any errors that the distribution has
|
|
||||||
Certificate types.Object `tfsdk:"certificate"` // the certificate of the custom domain
|
|
||||||
}
|
|
||||||
|
|
||||||
type customDomainResource struct {
|
|
||||||
client *cdn.APIClient
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewCustomDomainResource() resource.Resource {
|
|
||||||
return &customDomainResource{}
|
|
||||||
}
|
|
||||||
|
|
||||||
type Certificate struct {
|
|
||||||
Type string
|
|
||||||
Version *int64
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *customDomainResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
|
|
||||||
providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
features.CheckBetaResourcesEnabled(ctx, &providerData, &resp.Diagnostics, "stackit_cdn_custom_domain", "resource")
|
|
||||||
if resp.Diagnostics.HasError() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
apiClient := cdnUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics)
|
|
||||||
if resp.Diagnostics.HasError() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
r.client = apiClient
|
|
||||||
tflog.Info(ctx, "CDN client configured")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *customDomainResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
|
|
||||||
resp.TypeName = req.ProviderTypeName + "_cdn_custom_domain"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *customDomainResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
|
|
||||||
resp.Schema = schema.Schema{
|
|
||||||
MarkdownDescription: features.AddBetaDescription("CDN distribution data source schema.", core.Resource),
|
|
||||||
Description: "CDN distribution data source schema.",
|
|
||||||
Attributes: map[string]schema.Attribute{
|
|
||||||
"id": schema.StringAttribute{
|
|
||||||
Description: customDomainSchemaDescriptions["id"],
|
|
||||||
Computed: true,
|
|
||||||
},
|
|
||||||
"name": schema.StringAttribute{
|
|
||||||
Description: customDomainSchemaDescriptions["name"],
|
|
||||||
Required: true,
|
|
||||||
Optional: false,
|
|
||||||
PlanModifiers: []planmodifier.String{
|
|
||||||
stringplanmodifier.UseStateForUnknown(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"distribution_id": schema.StringAttribute{
|
|
||||||
Description: customDomainSchemaDescriptions["distribution_id"],
|
|
||||||
Required: true,
|
|
||||||
Optional: false,
|
|
||||||
Validators: []validator.String{validate.UUID()},
|
|
||||||
PlanModifiers: []planmodifier.String{
|
|
||||||
stringplanmodifier.RequiresReplace(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"project_id": schema.StringAttribute{
|
|
||||||
Description: customDomainSchemaDescriptions["project_id"],
|
|
||||||
Required: true,
|
|
||||||
Optional: false,
|
|
||||||
PlanModifiers: []planmodifier.String{
|
|
||||||
stringplanmodifier.RequiresReplace(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"certificate": schema.SingleNestedAttribute{
|
|
||||||
Description: certificateSchemaDescriptions["main"],
|
|
||||||
Optional: true,
|
|
||||||
Attributes: map[string]schema.Attribute{
|
|
||||||
"certificate": schema.StringAttribute{
|
|
||||||
Description: certificateSchemaDescriptions["certificate"],
|
|
||||||
Optional: true,
|
|
||||||
Sensitive: true,
|
|
||||||
},
|
|
||||||
"private_key": schema.StringAttribute{
|
|
||||||
Description: certificateSchemaDescriptions["private_key"],
|
|
||||||
Optional: true,
|
|
||||||
Sensitive: true,
|
|
||||||
},
|
|
||||||
"version": schema.Int64Attribute{
|
|
||||||
Description: certificateSchemaDescriptions["version"],
|
|
||||||
Computed: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"status": schema.StringAttribute{
|
|
||||||
Computed: true,
|
|
||||||
Description: customDomainSchemaDescriptions["status"],
|
|
||||||
},
|
|
||||||
"errors": schema.ListAttribute{
|
|
||||||
ElementType: types.StringType,
|
|
||||||
Computed: true,
|
|
||||||
Description: customDomainSchemaDescriptions["errors"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *customDomainResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform
|
|
||||||
var model CustomDomainModel
|
|
||||||
diags := req.Plan.Get(ctx, &model)
|
|
||||||
resp.Diagnostics.Append(diags...)
|
|
||||||
if resp.Diagnostics.HasError() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx = core.InitProviderContext(ctx)
|
|
||||||
|
|
||||||
projectId := model.ProjectId.ValueString()
|
|
||||||
ctx = tflog.SetField(ctx, "project_id", projectId)
|
|
||||||
distributionId := model.DistributionId.ValueString()
|
|
||||||
ctx = tflog.SetField(ctx, "distribution_id", distributionId)
|
|
||||||
name := model.Name.ValueString()
|
|
||||||
ctx = tflog.SetField(ctx, "name", name)
|
|
||||||
certificate, err := toCertificatePayload(ctx, &model)
|
|
||||||
if err != nil {
|
|
||||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating CDN custom domain", fmt.Sprintf("Creating API payload: %v", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
payload := cdn.PutCustomDomainPayload{
|
|
||||||
IntentId: cdn.PtrString(uuid.NewString()),
|
|
||||||
Certificate: certificate,
|
|
||||||
}
|
|
||||||
_, err = r.client.PutCustomDomain(ctx, projectId, distributionId, name).PutCustomDomainPayload(payload).Execute()
|
|
||||||
if err != nil {
|
|
||||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating CDN custom domain", fmt.Sprintf("Calling API: %v", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx = core.LogResponse(ctx)
|
|
||||||
|
|
||||||
_, err = wait.CreateCDNCustomDomainWaitHandler(ctx, r.client, projectId, distributionId, name).SetTimeout(5 * time.Minute).WaitWithContext(ctx)
|
|
||||||
if err != nil {
|
|
||||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating CDN custom domain", fmt.Sprintf("Waiting for create: %v", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
respCustomDomain, err := r.client.GetCustomDomainExecute(ctx, projectId, distributionId, name)
|
|
||||||
if err != nil {
|
|
||||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating CDN custom domain", fmt.Sprintf("Calling API: %v", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
err = mapCustomDomainResourceFields(respCustomDomain, &model)
|
|
||||||
if err != nil {
|
|
||||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating CDN custom domain", fmt.Sprintf("Processing API payload: %v", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
diags = resp.State.Set(ctx, model)
|
|
||||||
resp.Diagnostics.Append(diags...)
|
|
||||||
if resp.Diagnostics.HasError() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
tflog.Info(ctx, "CDN custom domain created")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *customDomainResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform
|
|
||||||
var model CustomDomainModel
|
|
||||||
diags := req.State.Get(ctx, &model)
|
|
||||||
resp.Diagnostics.Append(diags...)
|
|
||||||
if resp.Diagnostics.HasError() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx = core.InitProviderContext(ctx)
|
|
||||||
|
|
||||||
projectId := model.ProjectId.ValueString()
|
|
||||||
ctx = tflog.SetField(ctx, "project_id", projectId)
|
|
||||||
distributionId := model.DistributionId.ValueString()
|
|
||||||
ctx = tflog.SetField(ctx, "distribution_id", distributionId)
|
|
||||||
name := model.Name.ValueString()
|
|
||||||
ctx = tflog.SetField(ctx, "name", name)
|
|
||||||
|
|
||||||
customDomainResp, err := r.client.GetCustomDomain(ctx, projectId, distributionId, name).Execute()
|
|
||||||
if err != nil {
|
|
||||||
var oapiErr *oapierror.GenericOpenAPIError
|
|
||||||
// n.b. err is caught here if of type *oapierror.GenericOpenAPIError, which the stackit SDK client returns
|
|
||||||
if errors.As(err, &oapiErr) {
|
|
||||||
if oapiErr.StatusCode == http.StatusNotFound {
|
|
||||||
resp.State.RemoveResource(ctx)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading CDN custom domain", fmt.Sprintf("Calling API: %v", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx = core.LogResponse(ctx)
|
|
||||||
|
|
||||||
err = mapCustomDomainResourceFields(customDomainResp, &model)
|
|
||||||
if err != nil {
|
|
||||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading CDN custom domain", fmt.Sprintf("Processing API payload: %v", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Set refreshed state
|
|
||||||
diags = resp.State.Set(ctx, model)
|
|
||||||
resp.Diagnostics.Append(diags...)
|
|
||||||
if resp.Diagnostics.HasError() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
tflog.Info(ctx, "CDN custom domain read")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *customDomainResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform
|
|
||||||
var model CustomDomainModel
|
|
||||||
diags := req.Plan.Get(ctx, &model)
|
|
||||||
resp.Diagnostics.Append(diags...)
|
|
||||||
if resp.Diagnostics.HasError() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx = core.InitProviderContext(ctx)
|
|
||||||
|
|
||||||
projectId := model.ProjectId.ValueString()
|
|
||||||
ctx = tflog.SetField(ctx, "project_id", projectId)
|
|
||||||
distributionId := model.DistributionId.ValueString()
|
|
||||||
ctx = tflog.SetField(ctx, "distribution_id", distributionId)
|
|
||||||
name := model.Name.ValueString()
|
|
||||||
ctx = tflog.SetField(ctx, "name", name)
|
|
||||||
|
|
||||||
certificate, err := toCertificatePayload(ctx, &model)
|
|
||||||
if err != nil {
|
|
||||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating CDN custom domain", fmt.Sprintf("Creating API payload: %v", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
payload := cdn.PutCustomDomainPayload{
|
|
||||||
IntentId: cdn.PtrString(uuid.NewString()),
|
|
||||||
Certificate: certificate,
|
|
||||||
}
|
|
||||||
_, err = r.client.PutCustomDomain(ctx, projectId, distributionId, name).PutCustomDomainPayload(payload).Execute()
|
|
||||||
if err != nil {
|
|
||||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating CDN custom domain certificate", fmt.Sprintf("Calling API: %v", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx = core.LogResponse(ctx)
|
|
||||||
|
|
||||||
_, err = wait.CreateCDNCustomDomainWaitHandler(ctx, r.client, projectId, distributionId, name).SetTimeout(5 * time.Minute).WaitWithContext(ctx)
|
|
||||||
if err != nil {
|
|
||||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating CDN custom domain certificate", fmt.Sprintf("Waiting for update: %v", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
respCustomDomain, err := r.client.GetCustomDomainExecute(ctx, projectId, distributionId, name)
|
|
||||||
if err != nil {
|
|
||||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating CDN custom domain certificate", fmt.Sprintf("Calling API to read final state: %v", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
err = mapCustomDomainResourceFields(respCustomDomain, &model)
|
|
||||||
if err != nil {
|
|
||||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating CDN custom domain certificate", fmt.Sprintf("Processing API payload: %v", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
diags = resp.State.Set(ctx, model)
|
|
||||||
resp.Diagnostics.Append(diags...)
|
|
||||||
if resp.Diagnostics.HasError() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
tflog.Info(ctx, "CDN custom domain certificate updated")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *customDomainResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform
|
|
||||||
var model CustomDomainModel
|
|
||||||
diags := req.State.Get(ctx, &model)
|
|
||||||
resp.Diagnostics.Append(diags...)
|
|
||||||
if resp.Diagnostics.HasError() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx = core.InitProviderContext(ctx)
|
|
||||||
|
|
||||||
projectId := model.ProjectId.ValueString()
|
|
||||||
ctx = tflog.SetField(ctx, "project_id", projectId)
|
|
||||||
distributionId := model.DistributionId.ValueString()
|
|
||||||
ctx = tflog.SetField(ctx, "distribution_id", distributionId)
|
|
||||||
name := model.Name.ValueString()
|
|
||||||
ctx = tflog.SetField(ctx, "name", name)
|
|
||||||
|
|
||||||
_, err := r.client.DeleteCustomDomain(ctx, projectId, distributionId, name).Execute()
|
|
||||||
if err != nil {
|
|
||||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Delete CDN custom domain", fmt.Sprintf("Delete custom domain: %v", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx = core.LogResponse(ctx)
|
|
||||||
|
|
||||||
_, err = wait.DeleteCDNCustomDomainWaitHandler(ctx, r.client, projectId, distributionId, name).WaitWithContext(ctx)
|
|
||||||
if err != nil {
|
|
||||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Delete CDN custom domain", fmt.Sprintf("Waiting for deletion: %v", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
tflog.Info(ctx, "CDN custom domain deleted")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *customDomainResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
|
|
||||||
idParts := strings.Split(req.ID, core.Separator)
|
|
||||||
|
|
||||||
if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" {
|
|
||||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error importing CDN custom domain", fmt.Sprintf("Expected import identifier on the format: [project_id]%q[distribution_id]%q[custom_domain_name], got %q", core.Separator, core.Separator, req.ID))
|
|
||||||
}
|
|
||||||
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), idParts[0])...)
|
|
||||||
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("distribution_id"), idParts[1])...)
|
|
||||||
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("name"), idParts[2])...)
|
|
||||||
tflog.Info(ctx, "CDN custom domain state imported")
|
|
||||||
}
|
|
||||||
|
|
||||||
func normalizeCertificate(certInput cdn.GetCustomDomainResponseGetCertificateAttributeType) (Certificate, error) {
|
|
||||||
var customCert *cdn.GetCustomDomainCustomCertificate
|
|
||||||
var managedCert *cdn.GetCustomDomainManagedCertificate
|
|
||||||
|
|
||||||
if certInput == nil {
|
|
||||||
return Certificate{}, errors.New("input of type GetCustomDomainResponseCertificate is nil")
|
|
||||||
}
|
|
||||||
customCert = certInput.GetCustomDomainCustomCertificate
|
|
||||||
managedCert = certInput.GetCustomDomainManagedCertificate
|
|
||||||
|
|
||||||
// Now we process the extracted certificates
|
|
||||||
if customCert != nil && customCert.Type != nil && customCert.Version != nil {
|
|
||||||
return Certificate{
|
|
||||||
Type: *customCert.Type,
|
|
||||||
Version: customCert.Version, // Converts from *int64 to int
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if managedCert != nil && managedCert.Type != nil {
|
|
||||||
// The version will be the zero value for int (0), as requested
|
|
||||||
return Certificate{
|
|
||||||
Type: *managedCert.Type,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return Certificate{}, errors.New("certificate structure is empty, neither custom nor managed is set")
|
|
||||||
}
|
|
||||||
|
|
||||||
// toCertificatePayload constructs the certificate part of the payload for the API request.
|
|
||||||
// It defaults to a managed certificate if the certificate block is omitted, otherwise it creates a custom certificate.
|
|
||||||
func toCertificatePayload(ctx context.Context, model *CustomDomainModel) (*cdn.PutCustomDomainPayloadCertificate, error) {
|
|
||||||
// If the certificate block is not specified, default to a managed certificate.
|
|
||||||
if model.Certificate.IsNull() {
|
|
||||||
managedCert := cdn.NewPutCustomDomainManagedCertificate("managed")
|
|
||||||
certPayload := cdn.PutCustomDomainManagedCertificateAsPutCustomDomainPayloadCertificate(managedCert)
|
|
||||||
return &certPayload, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var certModel CertificateModel
|
|
||||||
// Unpack the Terraform object into the temporary struct.
|
|
||||||
respDiags := model.Certificate.As(ctx, &certModel, basetypes.ObjectAsOptions{})
|
|
||||||
if respDiags.HasError() {
|
|
||||||
return nil, fmt.Errorf("invalid certificate or private key: %w", core.DiagsToError(respDiags))
|
|
||||||
}
|
|
||||||
|
|
||||||
if utils.IsUndefined(certModel.Certificate) || utils.IsUndefined(certModel.PrivateKey) {
|
|
||||||
return nil, fmt.Errorf(`"certificate" and "private_key" must be set`)
|
|
||||||
}
|
|
||||||
|
|
||||||
certStr := base64.StdEncoding.EncodeToString([]byte(certModel.Certificate.ValueString()))
|
|
||||||
keyStr := base64.StdEncoding.EncodeToString([]byte(certModel.PrivateKey.ValueString()))
|
|
||||||
|
|
||||||
if certStr == "" || keyStr == "" {
|
|
||||||
return nil, errors.New("invalid certificate or private key. Please check if the string of the public certificate and private key in PEM format")
|
|
||||||
}
|
|
||||||
|
|
||||||
customCert := cdn.NewPutCustomDomainCustomCertificate(
|
|
||||||
certStr,
|
|
||||||
keyStr,
|
|
||||||
"custom",
|
|
||||||
)
|
|
||||||
certPayload := cdn.PutCustomDomainCustomCertificateAsPutCustomDomainPayloadCertificate(customCert)
|
|
||||||
|
|
||||||
return &certPayload, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func mapCustomDomainResourceFields(customDomainResponse *cdn.GetCustomDomainResponse, model *CustomDomainModel) error {
|
|
||||||
if customDomainResponse == nil {
|
|
||||||
return fmt.Errorf("response input is nil")
|
|
||||||
}
|
|
||||||
if model == nil {
|
|
||||||
return fmt.Errorf("model input is nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
if customDomainResponse.CustomDomain == nil {
|
|
||||||
return fmt.Errorf("CustomDomain is missing in response")
|
|
||||||
}
|
|
||||||
if customDomainResponse.CustomDomain.Name == nil {
|
|
||||||
return fmt.Errorf("name is missing in response")
|
|
||||||
}
|
|
||||||
|
|
||||||
if customDomainResponse.CustomDomain.Status == nil {
|
|
||||||
return fmt.Errorf("status missing in response")
|
|
||||||
}
|
|
||||||
normalizedCert, err := normalizeCertificate(customDomainResponse.Certificate)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Certificate error in normalizer: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the certificate is managed, the certificate block in the state should be null.
|
|
||||||
if normalizedCert.Type == "managed" {
|
|
||||||
model.Certificate = types.ObjectNull(certificateTypes)
|
|
||||||
} else {
|
|
||||||
// If the certificate is custom, we need to preserve the user-configured
|
|
||||||
// certificate and private key from the plan/state, and only update the computed version.
|
|
||||||
certAttributes := map[string]attr.Value{
|
|
||||||
"certificate": types.StringNull(), // Default to null
|
|
||||||
"private_key": types.StringNull(), // Default to null
|
|
||||||
"version": types.Int64Null(),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get existing values from the model's certificate object if it exists
|
|
||||||
if !model.Certificate.IsNull() {
|
|
||||||
existingAttrs := model.Certificate.Attributes()
|
|
||||||
if val, ok := existingAttrs["certificate"]; ok {
|
|
||||||
certAttributes["certificate"] = val
|
|
||||||
}
|
|
||||||
if val, ok := existingAttrs["private_key"]; ok {
|
|
||||||
certAttributes["private_key"] = val
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the computed version from the API response
|
|
||||||
if normalizedCert.Version != nil {
|
|
||||||
certAttributes["version"] = types.Int64Value(*normalizedCert.Version)
|
|
||||||
}
|
|
||||||
|
|
||||||
certificateObj, diags := types.ObjectValue(certificateTypes, certAttributes)
|
|
||||||
if diags.HasError() {
|
|
||||||
return fmt.Errorf("failed to map certificate: %w", core.DiagsToError(diags))
|
|
||||||
}
|
|
||||||
model.Certificate = certificateObj
|
|
||||||
}
|
|
||||||
|
|
||||||
model.ID = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), model.DistributionId.ValueString(), *customDomainResponse.CustomDomain.Name)
|
|
||||||
model.Status = types.StringValue(string(*customDomainResponse.CustomDomain.Status))
|
|
||||||
|
|
||||||
customDomainErrors := []attr.Value{}
|
|
||||||
if customDomainResponse.CustomDomain.Errors != nil {
|
|
||||||
for _, e := range *customDomainResponse.CustomDomain.Errors {
|
|
||||||
if e.En == nil {
|
|
||||||
return fmt.Errorf("error description missing")
|
|
||||||
}
|
|
||||||
customDomainErrors = append(customDomainErrors, types.StringValue(*e.En))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
modelErrors, diags := types.ListValue(types.StringType, customDomainErrors)
|
|
||||||
if diags.HasError() {
|
|
||||||
return core.DiagsToError(diags)
|
|
||||||
}
|
|
||||||
model.Errors = modelErrors
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,308 +0,0 @@
|
||||||
package cdn
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
cryptoRand "crypto/rand"
|
|
||||||
"crypto/rsa"
|
|
||||||
"crypto/x509"
|
|
||||||
"crypto/x509/pkix"
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/pem"
|
|
||||||
"fmt"
|
|
||||||
"math/big"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/google/go-cmp/cmp"
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/hashicorp/terraform-plugin-framework/attr"
|
|
||||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
|
||||||
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
|
|
||||||
"github.com/stackitcloud/stackit-sdk-go/services/cdn"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestMapFields(t *testing.T) {
|
|
||||||
// Redefine certificateTypes locally for testing, matching the updated schema
|
|
||||||
certificateTypes := map[string]attr.Type{
|
|
||||||
"version": types.Int64Type,
|
|
||||||
"certificate": types.StringType,
|
|
||||||
"private_key": types.StringType,
|
|
||||||
}
|
|
||||||
|
|
||||||
const dummyCert = "dummy-cert-pem"
|
|
||||||
const dummyKey = "dummy-key-pem"
|
|
||||||
|
|
||||||
emtpyErrorsList := types.ListValueMust(types.StringType, []attr.Value{})
|
|
||||||
|
|
||||||
// Expected object when a custom certificate is returned
|
|
||||||
certAttributes := map[string]attr.Value{
|
|
||||||
"version": types.Int64Value(3),
|
|
||||||
"certificate": types.StringValue(dummyCert),
|
|
||||||
"private_key": types.StringValue(dummyKey),
|
|
||||||
}
|
|
||||||
certificateObj, _ := types.ObjectValue(certificateTypes, certAttributes)
|
|
||||||
|
|
||||||
expectedModel := func(mods ...func(*CustomDomainModel)) *CustomDomainModel {
|
|
||||||
model := &CustomDomainModel{
|
|
||||||
ID: types.StringValue("test-project-id,test-distribution-id,https://testdomain.com"),
|
|
||||||
DistributionId: types.StringValue("test-distribution-id"),
|
|
||||||
ProjectId: types.StringValue("test-project-id"),
|
|
||||||
Status: types.StringValue("ACTIVE"),
|
|
||||||
Errors: emtpyErrorsList,
|
|
||||||
Certificate: types.ObjectUnknown(certificateTypes),
|
|
||||||
}
|
|
||||||
for _, mod := range mods {
|
|
||||||
mod(model)
|
|
||||||
}
|
|
||||||
return model
|
|
||||||
}
|
|
||||||
|
|
||||||
customType := "custom"
|
|
||||||
customVersion := int64(3)
|
|
||||||
getRespCustom := cdn.GetCustomDomainResponseGetCertificateAttributeType(&cdn.GetCustomDomainResponseCertificate{
|
|
||||||
GetCustomDomainCustomCertificate: &cdn.GetCustomDomainCustomCertificate{
|
|
||||||
Type: &customType,
|
|
||||||
Version: &customVersion,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
managedType := "managed"
|
|
||||||
getRespManaged := cdn.GetCustomDomainResponseGetCertificateAttributeType(&cdn.GetCustomDomainResponseCertificate{
|
|
||||||
GetCustomDomainManagedCertificate: &cdn.GetCustomDomainManagedCertificate{
|
|
||||||
Type: &managedType,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
customDomainFixture := func(mods ...func(*cdn.GetCustomDomainResponse)) *cdn.GetCustomDomainResponse {
|
|
||||||
distribution := &cdn.CustomDomain{
|
|
||||||
Errors: &[]cdn.StatusError{},
|
|
||||||
Name: cdn.PtrString("https://testdomain.com"),
|
|
||||||
Status: cdn.DOMAINSTATUS_ACTIVE.Ptr(),
|
|
||||||
}
|
|
||||||
customDomainResponse := &cdn.GetCustomDomainResponse{
|
|
||||||
CustomDomain: distribution,
|
|
||||||
Certificate: getRespCustom,
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, mod := range mods {
|
|
||||||
mod(customDomainResponse)
|
|
||||||
}
|
|
||||||
return customDomainResponse
|
|
||||||
}
|
|
||||||
|
|
||||||
tests := map[string]struct {
|
|
||||||
Input *cdn.GetCustomDomainResponse
|
|
||||||
Certificate interface{}
|
|
||||||
Expected *CustomDomainModel
|
|
||||||
InitialModel *CustomDomainModel
|
|
||||||
IsValid bool
|
|
||||||
SkipInitialNil bool
|
|
||||||
}{
|
|
||||||
"happy_path_custom_cert": {
|
|
||||||
Expected: expectedModel(func(m *CustomDomainModel) {
|
|
||||||
m.Certificate = certificateObj
|
|
||||||
}),
|
|
||||||
Input: customDomainFixture(),
|
|
||||||
IsValid: true,
|
|
||||||
InitialModel: expectedModel(func(m *CustomDomainModel) {
|
|
||||||
m.Certificate = basetypes.NewObjectValueMust(certificateTypes, map[string]attr.Value{
|
|
||||||
"certificate": types.StringValue(dummyCert),
|
|
||||||
"private_key": types.StringValue(dummyKey),
|
|
||||||
"version": types.Int64Null(),
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
"happy_path_managed_cert": {
|
|
||||||
Expected: expectedModel(func(m *CustomDomainModel) {
|
|
||||||
m.Certificate = types.ObjectNull(certificateTypes)
|
|
||||||
}),
|
|
||||||
Input: customDomainFixture(func(gcdr *cdn.GetCustomDomainResponse) {
|
|
||||||
gcdr.Certificate = getRespManaged
|
|
||||||
}),
|
|
||||||
IsValid: true,
|
|
||||||
InitialModel: expectedModel(func(m *CustomDomainModel) { m.Certificate = types.ObjectNull(certificateTypes) }),
|
|
||||||
},
|
|
||||||
"happy_path_status_error": {
|
|
||||||
Expected: expectedModel(func(m *CustomDomainModel) {
|
|
||||||
m.Status = types.StringValue("ERROR")
|
|
||||||
m.Certificate = certificateObj
|
|
||||||
}),
|
|
||||||
Input: customDomainFixture(func(d *cdn.GetCustomDomainResponse) {
|
|
||||||
d.CustomDomain.Status = cdn.DOMAINSTATUS_ERROR.Ptr()
|
|
||||||
}),
|
|
||||||
IsValid: true,
|
|
||||||
InitialModel: expectedModel(func(m *CustomDomainModel) {
|
|
||||||
m.Certificate = basetypes.NewObjectValueMust(certificateTypes, map[string]attr.Value{
|
|
||||||
"certificate": types.StringValue(dummyCert),
|
|
||||||
"private_key": types.StringValue(dummyKey),
|
|
||||||
"version": types.Int64Null(),
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
"sad_path_custom_domain_nil": {
|
|
||||||
Expected: expectedModel(),
|
|
||||||
Input: nil,
|
|
||||||
IsValid: false,
|
|
||||||
InitialModel: &CustomDomainModel{},
|
|
||||||
},
|
|
||||||
"sad_path_name_missing": {
|
|
||||||
Expected: expectedModel(),
|
|
||||||
Input: customDomainFixture(func(d *cdn.GetCustomDomainResponse) {
|
|
||||||
d.CustomDomain.Name = nil
|
|
||||||
}),
|
|
||||||
IsValid: false,
|
|
||||||
InitialModel: &CustomDomainModel{},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for tn, tc := range tests {
|
|
||||||
t.Run(tn, func(t *testing.T) {
|
|
||||||
model := tc.InitialModel
|
|
||||||
model.DistributionId = tc.Expected.DistributionId
|
|
||||||
model.ProjectId = tc.Expected.ProjectId
|
|
||||||
err := mapCustomDomainResourceFields(tc.Input, model)
|
|
||||||
if err != nil && tc.IsValid {
|
|
||||||
t.Fatalf("Error mapping fields: %v", err)
|
|
||||||
}
|
|
||||||
if err == nil && !tc.IsValid {
|
|
||||||
t.Fatalf("Should have failed")
|
|
||||||
}
|
|
||||||
if tc.IsValid {
|
|
||||||
diff := cmp.Diff(tc.Expected, model)
|
|
||||||
if diff != "" {
|
|
||||||
t.Fatalf("Mapped model not as expected (-want +got):\n%s", diff)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 TestToCertificatePayload(t *testing.T) {
|
|
||||||
organization := fmt.Sprintf("organization-%s", uuid.NewString())
|
|
||||||
cert, key := makeCertAndKey(t, organization)
|
|
||||||
certPEM := string(cert)
|
|
||||||
keyPEM := string(key)
|
|
||||||
certBase64 := base64.StdEncoding.EncodeToString(cert)
|
|
||||||
keyBase64 := base64.StdEncoding.EncodeToString(key)
|
|
||||||
|
|
||||||
tests := map[string]struct {
|
|
||||||
model *CustomDomainModel
|
|
||||||
expectedPayload *cdn.PutCustomDomainPayloadCertificate
|
|
||||||
expectErr bool
|
|
||||||
expectedErrMsg string
|
|
||||||
}{
|
|
||||||
"success_managed_when_certificate_block_is_nil": {
|
|
||||||
model: &CustomDomainModel{
|
|
||||||
Certificate: types.ObjectNull(certificateTypes),
|
|
||||||
},
|
|
||||||
expectedPayload: &cdn.PutCustomDomainPayloadCertificate{
|
|
||||||
PutCustomDomainManagedCertificate: cdn.NewPutCustomDomainManagedCertificate("managed"),
|
|
||||||
},
|
|
||||||
expectErr: false,
|
|
||||||
},
|
|
||||||
"success_custom_certificate": {
|
|
||||||
model: &CustomDomainModel{
|
|
||||||
Certificate: basetypes.NewObjectValueMust(
|
|
||||||
certificateTypes,
|
|
||||||
map[string]attr.Value{
|
|
||||||
"version": types.Int64Null(),
|
|
||||||
"certificate": types.StringValue(certPEM),
|
|
||||||
"private_key": types.StringValue(keyPEM),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
},
|
|
||||||
expectedPayload: &cdn.PutCustomDomainPayloadCertificate{
|
|
||||||
PutCustomDomainCustomCertificate: cdn.NewPutCustomDomainCustomCertificate(certBase64, keyBase64, "custom"),
|
|
||||||
},
|
|
||||||
expectErr: false,
|
|
||||||
},
|
|
||||||
"fail_custom_missing_cert_value": {
|
|
||||||
model: &CustomDomainModel{
|
|
||||||
Certificate: basetypes.NewObjectValueMust(
|
|
||||||
certificateTypes,
|
|
||||||
map[string]attr.Value{
|
|
||||||
"version": types.Int64Null(),
|
|
||||||
"certificate": types.StringValue(""), // Empty certificate
|
|
||||||
"private_key": types.StringValue(keyPEM),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
},
|
|
||||||
expectErr: true,
|
|
||||||
expectedErrMsg: "invalid certificate or private key. Please check if the string of the public certificate and private key in PEM format",
|
|
||||||
},
|
|
||||||
|
|
||||||
"success_managed_when_certificate_attributes_are_nil": {
|
|
||||||
model: &CustomDomainModel{
|
|
||||||
Certificate: basetypes.NewObjectValueMust(
|
|
||||||
certificateTypes,
|
|
||||||
map[string]attr.Value{
|
|
||||||
"version": types.Int64Null(),
|
|
||||||
"certificate": types.StringNull(),
|
|
||||||
"private_key": types.StringNull(),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
},
|
|
||||||
expectErr: true,
|
|
||||||
expectedErrMsg: `"certificate" and "private_key" must be set`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for name, tt := range tests {
|
|
||||||
t.Run(name, func(t *testing.T) {
|
|
||||||
payload, err := toCertificatePayload(context.Background(), tt.model)
|
|
||||||
if tt.expectErr {
|
|
||||||
if err == nil {
|
|
||||||
t.Fatalf("expected err, but got none")
|
|
||||||
}
|
|
||||||
if err.Error() != tt.expectedErrMsg {
|
|
||||||
t.Fatalf("expected err '%s', got '%s'", tt.expectedErrMsg, err.Error())
|
|
||||||
}
|
|
||||||
return // Test ends here for failing cases
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("did not expect err, but got: %s", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
if diff := cmp.Diff(tt.expectedPayload, payload); diff != "" {
|
|
||||||
t.Errorf("payload mismatch (-want +got):\n%s", diff)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,214 +0,0 @@
|
||||||
package cdn
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion"
|
|
||||||
cdnUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/cdn/utils"
|
|
||||||
|
|
||||||
"github.com/hashicorp/terraform-plugin-framework/datasource"
|
|
||||||
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
|
|
||||||
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
|
|
||||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
|
||||||
"github.com/hashicorp/terraform-plugin-log/tflog"
|
|
||||||
"github.com/stackitcloud/stackit-sdk-go/services/cdn"
|
|
||||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
|
|
||||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features"
|
|
||||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils"
|
|
||||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate"
|
|
||||||
)
|
|
||||||
|
|
||||||
type distributionDataSource struct {
|
|
||||||
client *cdn.APIClient
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure the implementation satisfies the expected interfaces.
|
|
||||||
var (
|
|
||||||
_ datasource.DataSource = &distributionDataSource{}
|
|
||||||
)
|
|
||||||
|
|
||||||
func NewDistributionDataSource() datasource.DataSource {
|
|
||||||
return &distributionDataSource{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *distributionDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
|
|
||||||
providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
features.CheckBetaResourcesEnabled(ctx, &providerData, &resp.Diagnostics, "stackit_cdn_distribution", "datasource")
|
|
||||||
if resp.Diagnostics.HasError() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
apiClient := cdnUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics)
|
|
||||||
if resp.Diagnostics.HasError() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
d.client = apiClient
|
|
||||||
tflog.Info(ctx, "Service Account client configured")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *distributionDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
|
|
||||||
resp.TypeName = req.ProviderTypeName + "_cdn_distribution"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *distributionDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
|
|
||||||
backendOptions := []string{"http"}
|
|
||||||
resp.Schema = schema.Schema{
|
|
||||||
MarkdownDescription: features.AddBetaDescription("CDN distribution data source schema.", core.Datasource),
|
|
||||||
Description: "CDN distribution data source schema.",
|
|
||||||
Attributes: map[string]schema.Attribute{
|
|
||||||
"id": schema.StringAttribute{
|
|
||||||
Description: schemaDescriptions["id"],
|
|
||||||
Computed: true,
|
|
||||||
},
|
|
||||||
"distribution_id": schema.StringAttribute{
|
|
||||||
Description: schemaDescriptions["project_id"],
|
|
||||||
Required: true,
|
|
||||||
Validators: []validator.String{
|
|
||||||
validate.UUID(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"project_id": schema.StringAttribute{
|
|
||||||
Description: schemaDescriptions["project_id"],
|
|
||||||
Required: true,
|
|
||||||
Validators: []validator.String{
|
|
||||||
validate.UUID(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"status": schema.StringAttribute{
|
|
||||||
Computed: true,
|
|
||||||
Description: schemaDescriptions["status"],
|
|
||||||
},
|
|
||||||
"created_at": schema.StringAttribute{
|
|
||||||
Computed: true,
|
|
||||||
Description: schemaDescriptions["created_at"],
|
|
||||||
},
|
|
||||||
"updated_at": schema.StringAttribute{
|
|
||||||
Computed: true,
|
|
||||||
Description: schemaDescriptions["updated_at"],
|
|
||||||
},
|
|
||||||
"errors": schema.ListAttribute{
|
|
||||||
ElementType: types.StringType,
|
|
||||||
Computed: true,
|
|
||||||
Description: schemaDescriptions["errors"],
|
|
||||||
},
|
|
||||||
"domains": schema.ListNestedAttribute{
|
|
||||||
Computed: true,
|
|
||||||
Description: schemaDescriptions["domains"],
|
|
||||||
NestedObject: schema.NestedAttributeObject{
|
|
||||||
Attributes: map[string]schema.Attribute{
|
|
||||||
"name": schema.StringAttribute{
|
|
||||||
Computed: true,
|
|
||||||
Description: schemaDescriptions["domain_name"],
|
|
||||||
},
|
|
||||||
"status": schema.StringAttribute{
|
|
||||||
Computed: true,
|
|
||||||
Description: schemaDescriptions["domain_status"],
|
|
||||||
},
|
|
||||||
"type": schema.StringAttribute{
|
|
||||||
Computed: true,
|
|
||||||
Description: schemaDescriptions["domain_type"],
|
|
||||||
},
|
|
||||||
"errors": schema.ListAttribute{
|
|
||||||
Computed: true,
|
|
||||||
Description: schemaDescriptions["domain_errors"],
|
|
||||||
ElementType: types.StringType,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"config": schema.SingleNestedAttribute{
|
|
||||||
Computed: true,
|
|
||||||
Description: schemaDescriptions["config"],
|
|
||||||
Attributes: map[string]schema.Attribute{
|
|
||||||
"backend": schema.SingleNestedAttribute{
|
|
||||||
Computed: true,
|
|
||||||
Description: schemaDescriptions["config_backend"],
|
|
||||||
Attributes: map[string]schema.Attribute{
|
|
||||||
"type": schema.StringAttribute{
|
|
||||||
Computed: true,
|
|
||||||
Description: schemaDescriptions["config_backend_type"] + utils.FormatPossibleValues(backendOptions...),
|
|
||||||
},
|
|
||||||
"origin_url": schema.StringAttribute{
|
|
||||||
Computed: true,
|
|
||||||
Description: schemaDescriptions["config_backend_origin_url"],
|
|
||||||
},
|
|
||||||
"origin_request_headers": schema.MapAttribute{
|
|
||||||
Computed: true,
|
|
||||||
Description: schemaDescriptions["config_backend_origin_request_headers"],
|
|
||||||
ElementType: types.StringType,
|
|
||||||
},
|
|
||||||
"geofencing": schema.MapAttribute{
|
|
||||||
Description: "A map of URLs to a list of countries where content is allowed.",
|
|
||||||
Computed: true,
|
|
||||||
ElementType: types.ListType{
|
|
||||||
ElemType: types.StringType,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"regions": schema.ListAttribute{
|
|
||||||
Computed: true,
|
|
||||||
Description: schemaDescriptions["config_regions"],
|
|
||||||
ElementType: types.StringType,
|
|
||||||
},
|
|
||||||
"blocked_countries": schema.ListAttribute{
|
|
||||||
Optional: true,
|
|
||||||
Description: schemaDescriptions["config_blocked_countries"],
|
|
||||||
ElementType: types.StringType,
|
|
||||||
},
|
|
||||||
"optimizer": schema.SingleNestedAttribute{
|
|
||||||
Description: schemaDescriptions["config_optimizer"],
|
|
||||||
Computed: true,
|
|
||||||
Attributes: map[string]schema.Attribute{
|
|
||||||
"enabled": schema.BoolAttribute{
|
|
||||||
Computed: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *distributionDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform
|
|
||||||
var model Model
|
|
||||||
diags := req.Config.Get(ctx, &model)
|
|
||||||
resp.Diagnostics.Append(diags...)
|
|
||||||
if resp.Diagnostics.HasError() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx = core.InitProviderContext(ctx)
|
|
||||||
|
|
||||||
projectId := model.ProjectId.ValueString()
|
|
||||||
distributionId := model.DistributionId.ValueString()
|
|
||||||
distributionResp, err := r.client.GetDistributionExecute(ctx, projectId, distributionId)
|
|
||||||
if err != nil {
|
|
||||||
utils.LogError(
|
|
||||||
ctx,
|
|
||||||
&resp.Diagnostics,
|
|
||||||
err,
|
|
||||||
"Reading CDN distribution",
|
|
||||||
fmt.Sprintf("Unable to access CDN distribution %q.", distributionId),
|
|
||||||
map[int]string{},
|
|
||||||
)
|
|
||||||
resp.State.RemoveResource(ctx)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx = core.LogResponse(ctx)
|
|
||||||
|
|
||||||
err = mapFields(ctx, distributionResp.Distribution, &model)
|
|
||||||
if err != nil {
|
|
||||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading CDN distribution", fmt.Sprintf("Error processing API response: %v", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
diags = resp.State.Set(ctx, &model)
|
|
||||||
resp.Diagnostics.Append(diags...)
|
|
||||||
}
|
|
||||||
|
|
@ -1,940 +0,0 @@
|
||||||
package cdn
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/hashicorp/terraform-plugin-framework-validators/mapvalidator"
|
|
||||||
"github.com/hashicorp/terraform-plugin-framework-validators/objectvalidator"
|
|
||||||
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
|
|
||||||
"github.com/hashicorp/terraform-plugin-framework/attr"
|
|
||||||
"github.com/hashicorp/terraform-plugin-framework/diag"
|
|
||||||
"github.com/hashicorp/terraform-plugin-framework/path"
|
|
||||||
"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/schema/validator"
|
|
||||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
|
||||||
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
|
|
||||||
"github.com/hashicorp/terraform-plugin-log/tflog"
|
|
||||||
"github.com/stackitcloud/stackit-sdk-go/core/oapierror"
|
|
||||||
"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/conversion"
|
|
||||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
|
|
||||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features"
|
|
||||||
cdnUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/cdn/utils"
|
|
||||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils"
|
|
||||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Ensure the implementation satisfies the expected interfaces.
|
|
||||||
var (
|
|
||||||
_ resource.Resource = &distributionResource{}
|
|
||||||
_ resource.ResourceWithConfigure = &distributionResource{}
|
|
||||||
_ resource.ResourceWithImportState = &distributionResource{}
|
|
||||||
)
|
|
||||||
|
|
||||||
var schemaDescriptions = map[string]string{
|
|
||||||
"id": "Terraform's internal resource identifier. It is structured as \"`project_id`,`distribution_id`\".",
|
|
||||||
"distribution_id": "CDN distribution ID",
|
|
||||||
"project_id": "STACKIT project ID associated with the distribution",
|
|
||||||
"status": "Status of the distribution",
|
|
||||||
"created_at": "Time when the distribution was created",
|
|
||||||
"updated_at": "Time when the distribution was last updated",
|
|
||||||
"errors": "List of distribution errors",
|
|
||||||
"domains": "List of configured domains for the distribution",
|
|
||||||
"config": "The distribution configuration",
|
|
||||||
"config_backend": "The configured backend for the distribution",
|
|
||||||
"config_regions": "The configured regions where content will be hosted",
|
|
||||||
"config_backend_type": "The configured backend type. ",
|
|
||||||
"config_optimizer": "Configuration for the Image Optimizer. This is a paid feature that automatically optimizes images to reduce their file size for faster delivery, leading to improved website performance and a better user experience.",
|
|
||||||
"config_backend_origin_url": "The configured backend type for the distribution",
|
|
||||||
"config_backend_origin_request_headers": "The configured origin request headers for the backend",
|
|
||||||
"config_blocked_countries": "The configured countries where distribution of content is blocked",
|
|
||||||
"domain_name": "The name of the domain",
|
|
||||||
"domain_status": "The status of the domain",
|
|
||||||
"domain_type": "The type of the domain. Each distribution has one domain of type \"managed\", and domains of type \"custom\" may be additionally created by the user",
|
|
||||||
"domain_errors": "List of domain errors",
|
|
||||||
}
|
|
||||||
|
|
||||||
type Model struct {
|
|
||||||
ID types.String `tfsdk:"id"` // Required by Terraform
|
|
||||||
DistributionId types.String `tfsdk:"distribution_id"` // DistributionID associated with the cdn distribution
|
|
||||||
ProjectId types.String `tfsdk:"project_id"` // ProjectId associated with the cdn distribution
|
|
||||||
Status types.String `tfsdk:"status"` // The status of the cdn distribution
|
|
||||||
CreatedAt types.String `tfsdk:"created_at"` // When the distribution was created
|
|
||||||
UpdatedAt types.String `tfsdk:"updated_at"` // When the distribution was last updated
|
|
||||||
Errors types.List `tfsdk:"errors"` // Any errors that the distribution has
|
|
||||||
Domains types.List `tfsdk:"domains"` // The domains associated with the distribution
|
|
||||||
Config types.Object `tfsdk:"config"` // the configuration of the distribution
|
|
||||||
}
|
|
||||||
|
|
||||||
type distributionConfig struct {
|
|
||||||
Backend backend `tfsdk:"backend"` // The backend associated with the distribution
|
|
||||||
Regions *[]string `tfsdk:"regions"` // The regions in which data will be cached
|
|
||||||
BlockedCountries *[]string `tfsdk:"blocked_countries"` // The countries for which content will be blocked
|
|
||||||
Optimizer types.Object `tfsdk:"optimizer"` // The optimizer configuration
|
|
||||||
}
|
|
||||||
|
|
||||||
type optimizerConfig struct {
|
|
||||||
Enabled types.Bool `tfsdk:"enabled"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type backend struct {
|
|
||||||
Type string `tfsdk:"type"` // The type of the backend. Currently, only "http" backend is supported
|
|
||||||
OriginURL string `tfsdk:"origin_url"` // The origin URL of the backend
|
|
||||||
OriginRequestHeaders *map[string]string `tfsdk:"origin_request_headers"` // Request headers that should be added by the CDN distribution to incoming requests
|
|
||||||
Geofencing *map[string][]*string `tfsdk:"geofencing"` // The geofencing is an object mapping multiple alternative origins to country codes.
|
|
||||||
}
|
|
||||||
|
|
||||||
var configTypes = map[string]attr.Type{
|
|
||||||
"backend": types.ObjectType{AttrTypes: backendTypes},
|
|
||||||
"regions": types.ListType{ElemType: types.StringType},
|
|
||||||
"blocked_countries": types.ListType{ElemType: types.StringType},
|
|
||||||
"optimizer": types.ObjectType{
|
|
||||||
AttrTypes: optimizerTypes,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
var optimizerTypes = map[string]attr.Type{
|
|
||||||
"enabled": types.BoolType,
|
|
||||||
}
|
|
||||||
|
|
||||||
var geofencingTypes = types.MapType{ElemType: types.ListType{
|
|
||||||
ElemType: types.StringType,
|
|
||||||
}}
|
|
||||||
|
|
||||||
var backendTypes = map[string]attr.Type{
|
|
||||||
"type": types.StringType,
|
|
||||||
"origin_url": types.StringType,
|
|
||||||
"origin_request_headers": types.MapType{ElemType: types.StringType},
|
|
||||||
"geofencing": geofencingTypes,
|
|
||||||
}
|
|
||||||
|
|
||||||
var domainTypes = map[string]attr.Type{
|
|
||||||
"name": types.StringType,
|
|
||||||
"status": types.StringType,
|
|
||||||
"type": types.StringType,
|
|
||||||
"errors": types.ListType{ElemType: types.StringType},
|
|
||||||
}
|
|
||||||
|
|
||||||
type distributionResource struct {
|
|
||||||
client *cdn.APIClient
|
|
||||||
providerData core.ProviderData
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewDistributionResource() resource.Resource {
|
|
||||||
return &distributionResource{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *distributionResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
|
|
||||||
var ok bool
|
|
||||||
r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
features.CheckBetaResourcesEnabled(ctx, &r.providerData, &resp.Diagnostics, "stackit_cdn_distribution", "resource")
|
|
||||||
if resp.Diagnostics.HasError() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
apiClient := cdnUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics)
|
|
||||||
if resp.Diagnostics.HasError() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
r.client = apiClient
|
|
||||||
tflog.Info(ctx, "CDN client configured")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *distributionResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
|
|
||||||
resp.TypeName = req.ProviderTypeName + "_cdn_distribution"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *distributionResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
|
|
||||||
backendOptions := []string{"http"}
|
|
||||||
resp.Schema = schema.Schema{
|
|
||||||
MarkdownDescription: features.AddBetaDescription("CDN distribution data source schema.", core.Resource),
|
|
||||||
Description: "CDN distribution data source schema.",
|
|
||||||
Attributes: map[string]schema.Attribute{
|
|
||||||
"id": schema.StringAttribute{
|
|
||||||
Description: schemaDescriptions["id"],
|
|
||||||
Computed: true,
|
|
||||||
},
|
|
||||||
"distribution_id": schema.StringAttribute{
|
|
||||||
Description: schemaDescriptions["distribution_id"],
|
|
||||||
Computed: true,
|
|
||||||
Validators: []validator.String{validate.UUID()},
|
|
||||||
PlanModifiers: []planmodifier.String{
|
|
||||||
stringplanmodifier.UseStateForUnknown(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"project_id": schema.StringAttribute{
|
|
||||||
Description: schemaDescriptions["project_id"],
|
|
||||||
Required: true,
|
|
||||||
Optional: false,
|
|
||||||
PlanModifiers: []planmodifier.String{
|
|
||||||
stringplanmodifier.RequiresReplace(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"status": schema.StringAttribute{
|
|
||||||
Computed: true,
|
|
||||||
Description: schemaDescriptions["status"],
|
|
||||||
},
|
|
||||||
"created_at": schema.StringAttribute{
|
|
||||||
Computed: true,
|
|
||||||
Description: schemaDescriptions["created_at"],
|
|
||||||
},
|
|
||||||
"updated_at": schema.StringAttribute{
|
|
||||||
Computed: true,
|
|
||||||
Description: schemaDescriptions["updated_at"],
|
|
||||||
},
|
|
||||||
"errors": schema.ListAttribute{
|
|
||||||
ElementType: types.StringType,
|
|
||||||
Computed: true,
|
|
||||||
Description: schemaDescriptions["errors"],
|
|
||||||
},
|
|
||||||
"domains": schema.ListNestedAttribute{
|
|
||||||
Computed: true,
|
|
||||||
Description: schemaDescriptions["domains"],
|
|
||||||
NestedObject: schema.NestedAttributeObject{
|
|
||||||
Attributes: map[string]schema.Attribute{
|
|
||||||
"name": schema.StringAttribute{
|
|
||||||
Computed: true,
|
|
||||||
Description: schemaDescriptions["domain_name"],
|
|
||||||
},
|
|
||||||
"status": schema.StringAttribute{
|
|
||||||
Computed: true,
|
|
||||||
Description: schemaDescriptions["domain_status"],
|
|
||||||
},
|
|
||||||
"type": schema.StringAttribute{
|
|
||||||
Computed: true,
|
|
||||||
Description: schemaDescriptions["domain_type"],
|
|
||||||
},
|
|
||||||
"errors": schema.ListAttribute{
|
|
||||||
Computed: true,
|
|
||||||
Description: schemaDescriptions["domain_errors"],
|
|
||||||
ElementType: types.StringType,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"config": schema.SingleNestedAttribute{
|
|
||||||
Required: true,
|
|
||||||
Description: schemaDescriptions["config"],
|
|
||||||
Attributes: map[string]schema.Attribute{
|
|
||||||
"optimizer": schema.SingleNestedAttribute{
|
|
||||||
Description: schemaDescriptions["config_optimizer"],
|
|
||||||
Optional: true,
|
|
||||||
Computed: true,
|
|
||||||
Attributes: map[string]schema.Attribute{
|
|
||||||
"enabled": schema.BoolAttribute{
|
|
||||||
Optional: true,
|
|
||||||
Computed: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Validators: []validator.Object{
|
|
||||||
objectvalidator.AlsoRequires(path.MatchRelative().AtName("enabled")),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"backend": schema.SingleNestedAttribute{
|
|
||||||
Required: true,
|
|
||||||
Description: schemaDescriptions["config_backend"],
|
|
||||||
Attributes: map[string]schema.Attribute{
|
|
||||||
"type": schema.StringAttribute{
|
|
||||||
Required: true,
|
|
||||||
Description: schemaDescriptions["config_backend_type"] + utils.FormatPossibleValues(backendOptions...),
|
|
||||||
Validators: []validator.String{stringvalidator.OneOf(backendOptions...)},
|
|
||||||
},
|
|
||||||
"origin_url": schema.StringAttribute{
|
|
||||||
Required: true,
|
|
||||||
Description: schemaDescriptions["config_backend_origin_url"],
|
|
||||||
},
|
|
||||||
"origin_request_headers": schema.MapAttribute{
|
|
||||||
Optional: true,
|
|
||||||
Description: schemaDescriptions["config_backend_origin_request_headers"],
|
|
||||||
ElementType: types.StringType,
|
|
||||||
},
|
|
||||||
"geofencing": schema.MapAttribute{
|
|
||||||
Description: "A map of URLs to a list of countries where content is allowed.",
|
|
||||||
Optional: true,
|
|
||||||
ElementType: types.ListType{
|
|
||||||
ElemType: types.StringType,
|
|
||||||
},
|
|
||||||
Validators: []validator.Map{
|
|
||||||
mapvalidator.SizeAtLeast(1),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"regions": schema.ListAttribute{
|
|
||||||
Required: true,
|
|
||||||
Description: schemaDescriptions["config_regions"],
|
|
||||||
ElementType: types.StringType,
|
|
||||||
},
|
|
||||||
"blocked_countries": schema.ListAttribute{
|
|
||||||
Optional: true,
|
|
||||||
Description: schemaDescriptions["config_blocked_countries"],
|
|
||||||
ElementType: types.StringType,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *distributionResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) {
|
|
||||||
var model Model
|
|
||||||
resp.Diagnostics.Append(req.Config.Get(ctx, &model)...)
|
|
||||||
if resp.Diagnostics.HasError() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !utils.IsUndefined(model.Config) {
|
|
||||||
var config distributionConfig
|
|
||||||
if !model.Config.IsNull() {
|
|
||||||
diags := model.Config.As(ctx, &config, basetypes.ObjectAsOptions{})
|
|
||||||
if diags.HasError() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if geofencing := config.Backend.Geofencing; geofencing != nil {
|
|
||||||
for url, region := range *geofencing {
|
|
||||||
if region == nil {
|
|
||||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Invalid geofencing config", fmt.Sprintf("The list of countries for URL %q must not be null.", url))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if len(region) == 0 {
|
|
||||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Invalid geofencing config", fmt.Sprintf("The list of countries for URL %q must not be empty.", url))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, countryPtr := range region {
|
|
||||||
if countryPtr == nil {
|
|
||||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Invalid geofencing config", fmt.Sprintf("Found a null value in the country list for URL %q at index %d.", url, i))
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *distributionResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform
|
|
||||||
var model Model
|
|
||||||
diags := req.Plan.Get(ctx, &model)
|
|
||||||
resp.Diagnostics.Append(diags...)
|
|
||||||
if resp.Diagnostics.HasError() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx = core.InitProviderContext(ctx)
|
|
||||||
|
|
||||||
projectId := model.ProjectId.ValueString()
|
|
||||||
ctx = tflog.SetField(ctx, "project_id", projectId)
|
|
||||||
|
|
||||||
payload, err := toCreatePayload(ctx, &model)
|
|
||||||
if err != nil {
|
|
||||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating CDN distribution", fmt.Sprintf("Creating API payload: %v", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
createResp, err := r.client.CreateDistribution(ctx, projectId).CreateDistributionPayload(*payload).Execute()
|
|
||||||
if err != nil {
|
|
||||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating CDN distribution", fmt.Sprintf("Calling API: %v", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx = core.LogResponse(ctx)
|
|
||||||
|
|
||||||
waitResp, err := wait.CreateDistributionPoolWaitHandler(ctx, r.client, projectId, *createResp.Distribution.Id).SetTimeout(5 * time.Minute).WaitWithContext(ctx)
|
|
||||||
if err != nil {
|
|
||||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating CDN distribution", fmt.Sprintf("Waiting for create: %v", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = mapFields(ctx, waitResp.Distribution, &model)
|
|
||||||
if err != nil {
|
|
||||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating CDN distribution", fmt.Sprintf("Processing API payload: %v", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
diags = resp.State.Set(ctx, model)
|
|
||||||
resp.Diagnostics.Append(diags...)
|
|
||||||
if resp.Diagnostics.HasError() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
tflog.Info(ctx, "CDN distribution created")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *distributionResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform
|
|
||||||
var model Model
|
|
||||||
diags := req.State.Get(ctx, &model)
|
|
||||||
resp.Diagnostics.Append(diags...)
|
|
||||||
if resp.Diagnostics.HasError() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx = core.InitProviderContext(ctx)
|
|
||||||
|
|
||||||
projectId := model.ProjectId.ValueString()
|
|
||||||
distributionId := model.DistributionId.ValueString()
|
|
||||||
ctx = tflog.SetField(ctx, "project_id", projectId)
|
|
||||||
ctx = tflog.SetField(ctx, "distribution_id", distributionId)
|
|
||||||
|
|
||||||
cdnResp, err := r.client.GetDistribution(ctx, projectId, distributionId).Execute()
|
|
||||||
if err != nil {
|
|
||||||
var oapiErr *oapierror.GenericOpenAPIError
|
|
||||||
// n.b. err is caught here if of type *oapierror.GenericOpenAPIError, which the stackit SDK client returns
|
|
||||||
if errors.As(err, &oapiErr) {
|
|
||||||
if oapiErr.StatusCode == http.StatusNotFound {
|
|
||||||
resp.State.RemoveResource(ctx)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading CDN distribution", fmt.Sprintf("Calling API: %v", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx = core.LogResponse(ctx)
|
|
||||||
|
|
||||||
err = mapFields(ctx, cdnResp.Distribution, &model)
|
|
||||||
if err != nil {
|
|
||||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading CDN ditribution", fmt.Sprintf("Processing API payload: %v", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Set refreshed state
|
|
||||||
diags = resp.State.Set(ctx, model)
|
|
||||||
resp.Diagnostics.Append(diags...)
|
|
||||||
if resp.Diagnostics.HasError() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
tflog.Info(ctx, "CDN distribution read")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *distributionResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform
|
|
||||||
var model Model
|
|
||||||
diags := req.Plan.Get(ctx, &model)
|
|
||||||
resp.Diagnostics.Append(diags...)
|
|
||||||
if resp.Diagnostics.HasError() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx = core.InitProviderContext(ctx)
|
|
||||||
|
|
||||||
projectId := model.ProjectId.ValueString()
|
|
||||||
distributionId := model.DistributionId.ValueString()
|
|
||||||
ctx = tflog.SetField(ctx, "project_id", projectId)
|
|
||||||
ctx = tflog.SetField(ctx, "distribution_id", distributionId)
|
|
||||||
|
|
||||||
configModel := distributionConfig{}
|
|
||||||
diags = model.Config.As(ctx, &configModel, basetypes.ObjectAsOptions{
|
|
||||||
UnhandledNullAsEmpty: false,
|
|
||||||
UnhandledUnknownAsEmpty: false,
|
|
||||||
})
|
|
||||||
if diags.HasError() {
|
|
||||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Update CDN distribution", "Error mapping config")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
regions := []cdn.Region{}
|
|
||||||
for _, r := range *configModel.Regions {
|
|
||||||
regionEnum, err := cdn.NewRegionFromValue(r)
|
|
||||||
if err != nil {
|
|
||||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Update CDN distribution", fmt.Sprintf("Map regions: %v", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
regions = append(regions, *regionEnum)
|
|
||||||
}
|
|
||||||
|
|
||||||
// blockedCountries
|
|
||||||
// Use a pointer to a slice to distinguish between an empty list (unblock all) and nil (no change).
|
|
||||||
var blockedCountries *[]string
|
|
||||||
if configModel.BlockedCountries != nil {
|
|
||||||
// Use a temporary slice
|
|
||||||
tempBlockedCountries := []string{}
|
|
||||||
|
|
||||||
for _, blockedCountry := range *configModel.BlockedCountries {
|
|
||||||
validatedBlockedCountry, err := validateCountryCode(blockedCountry)
|
|
||||||
if err != nil {
|
|
||||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Update CDN distribution", fmt.Sprintf("Blocked countries: %v", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
tempBlockedCountries = append(tempBlockedCountries, validatedBlockedCountry)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Point to the populated slice
|
|
||||||
blockedCountries = &tempBlockedCountries
|
|
||||||
}
|
|
||||||
|
|
||||||
geofencingPatch := map[string][]string{}
|
|
||||||
if configModel.Backend.Geofencing != nil {
|
|
||||||
gf := make(map[string][]string)
|
|
||||||
for url, countries := range *configModel.Backend.Geofencing {
|
|
||||||
countryStrings := make([]string, len(countries))
|
|
||||||
for i, countryPtr := range countries {
|
|
||||||
if countryPtr == nil {
|
|
||||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Update CDN distribution", fmt.Sprintf("Geofencing url %q has a null value", url))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
countryStrings[i] = *countryPtr
|
|
||||||
}
|
|
||||||
gf[url] = countryStrings
|
|
||||||
}
|
|
||||||
geofencingPatch = gf
|
|
||||||
}
|
|
||||||
|
|
||||||
configPatch := &cdn.ConfigPatch{
|
|
||||||
Backend: &cdn.ConfigPatchBackend{
|
|
||||||
HttpBackendPatch: &cdn.HttpBackendPatch{
|
|
||||||
OriginRequestHeaders: configModel.Backend.OriginRequestHeaders,
|
|
||||||
OriginUrl: &configModel.Backend.OriginURL,
|
|
||||||
Type: &configModel.Backend.Type,
|
|
||||||
Geofencing: &geofencingPatch, // Use the converted variable
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Regions: ®ions,
|
|
||||||
BlockedCountries: blockedCountries,
|
|
||||||
}
|
|
||||||
|
|
||||||
if !utils.IsUndefined(configModel.Optimizer) {
|
|
||||||
var optimizerModel optimizerConfig
|
|
||||||
|
|
||||||
diags = configModel.Optimizer.As(ctx, &optimizerModel, basetypes.ObjectAsOptions{})
|
|
||||||
if diags.HasError() {
|
|
||||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Update CDN distribution", "Error mapping optimizer config")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
optimizer := cdn.NewOptimizerPatch()
|
|
||||||
if !utils.IsUndefined(optimizerModel.Enabled) {
|
|
||||||
optimizer.SetEnabled(optimizerModel.Enabled.ValueBool())
|
|
||||||
}
|
|
||||||
configPatch.Optimizer = optimizer
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := r.client.PatchDistribution(ctx, projectId, distributionId).PatchDistributionPayload(cdn.PatchDistributionPayload{
|
|
||||||
Config: configPatch,
|
|
||||||
IntentId: cdn.PtrString(uuid.NewString()),
|
|
||||||
}).Execute()
|
|
||||||
if err != nil {
|
|
||||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Update CDN distribution", fmt.Sprintf("Patch distribution: %v", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx = core.LogResponse(ctx)
|
|
||||||
|
|
||||||
waitResp, err := wait.UpdateDistributionWaitHandler(ctx, r.client, projectId, distributionId).WaitWithContext(ctx)
|
|
||||||
if err != nil {
|
|
||||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Update CDN distribution", fmt.Sprintf("Waiting for update: %v", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = mapFields(ctx, waitResp.Distribution, &model)
|
|
||||||
if err != nil {
|
|
||||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Update CDN distribution", fmt.Sprintf("Processing API payload: %v", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
diags = resp.State.Set(ctx, model)
|
|
||||||
resp.Diagnostics.Append(diags...)
|
|
||||||
if resp.Diagnostics.HasError() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
tflog.Info(ctx, "CDN distribution updated")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *distributionResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform
|
|
||||||
var model Model
|
|
||||||
diags := req.State.Get(ctx, &model)
|
|
||||||
resp.Diagnostics.Append(diags...)
|
|
||||||
if resp.Diagnostics.HasError() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx = core.LogResponse(ctx)
|
|
||||||
|
|
||||||
projectId := model.ProjectId.ValueString()
|
|
||||||
distributionId := model.DistributionId.ValueString()
|
|
||||||
ctx = tflog.SetField(ctx, "project_id", projectId)
|
|
||||||
ctx = tflog.SetField(ctx, "distribution_id", distributionId)
|
|
||||||
|
|
||||||
_, err := r.client.DeleteDistribution(ctx, projectId, distributionId).Execute()
|
|
||||||
if err != nil {
|
|
||||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Delete CDN distribution", fmt.Sprintf("Delete distribution: %v", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx = core.LogResponse(ctx)
|
|
||||||
|
|
||||||
_, err = wait.DeleteDistributionWaitHandler(ctx, r.client, projectId, distributionId).WaitWithContext(ctx)
|
|
||||||
if err != nil {
|
|
||||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Delete CDN distribution", fmt.Sprintf("Waiting for deletion: %v", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
tflog.Info(ctx, "CDN distribution deleted")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *distributionResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
|
|
||||||
idParts := strings.Split(req.ID, core.Separator)
|
|
||||||
|
|
||||||
if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" {
|
|
||||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error importing CDN distribution", fmt.Sprintf("Expected import identifier on the format: [project_id]%q[distribution_id], got %q", core.Separator, req.ID))
|
|
||||||
}
|
|
||||||
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), idParts[0])...)
|
|
||||||
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("distribution_id"), idParts[1])...)
|
|
||||||
tflog.Info(ctx, "CDN distribution state imported")
|
|
||||||
}
|
|
||||||
|
|
||||||
func mapFields(ctx context.Context, distribution *cdn.Distribution, model *Model) error {
|
|
||||||
if distribution == nil {
|
|
||||||
return fmt.Errorf("response input is nil")
|
|
||||||
}
|
|
||||||
if model == nil {
|
|
||||||
return fmt.Errorf("model input is nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
if distribution.ProjectId == nil {
|
|
||||||
return fmt.Errorf("Project ID not present")
|
|
||||||
}
|
|
||||||
|
|
||||||
if distribution.Id == nil {
|
|
||||||
return fmt.Errorf("CDN distribution ID not present")
|
|
||||||
}
|
|
||||||
|
|
||||||
if distribution.CreatedAt == nil {
|
|
||||||
return fmt.Errorf("CreatedAt missing in response")
|
|
||||||
}
|
|
||||||
|
|
||||||
if distribution.UpdatedAt == nil {
|
|
||||||
return fmt.Errorf("UpdatedAt missing in response")
|
|
||||||
}
|
|
||||||
|
|
||||||
if distribution.Status == nil {
|
|
||||||
return fmt.Errorf("Status missing in response")
|
|
||||||
}
|
|
||||||
|
|
||||||
model.ID = utils.BuildInternalTerraformId(*distribution.ProjectId, *distribution.Id)
|
|
||||||
model.DistributionId = types.StringValue(*distribution.Id)
|
|
||||||
model.ProjectId = types.StringValue(*distribution.ProjectId)
|
|
||||||
model.Status = types.StringValue(string(distribution.GetStatus()))
|
|
||||||
model.CreatedAt = types.StringValue(distribution.CreatedAt.String())
|
|
||||||
model.UpdatedAt = types.StringValue(distribution.UpdatedAt.String())
|
|
||||||
|
|
||||||
// distributionErrors
|
|
||||||
distributionErrors := []attr.Value{}
|
|
||||||
if distribution.Errors != nil {
|
|
||||||
for _, e := range *distribution.Errors {
|
|
||||||
distributionErrors = append(distributionErrors, types.StringValue(*e.En))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
modelErrors, diags := types.ListValue(types.StringType, distributionErrors)
|
|
||||||
if diags.HasError() {
|
|
||||||
return core.DiagsToError(diags)
|
|
||||||
}
|
|
||||||
model.Errors = modelErrors
|
|
||||||
|
|
||||||
// regions
|
|
||||||
regions := []attr.Value{}
|
|
||||||
for _, r := range *distribution.Config.Regions {
|
|
||||||
regions = append(regions, types.StringValue(string(r)))
|
|
||||||
}
|
|
||||||
modelRegions, diags := types.ListValue(types.StringType, regions)
|
|
||||||
if diags.HasError() {
|
|
||||||
return core.DiagsToError(diags)
|
|
||||||
}
|
|
||||||
|
|
||||||
// blockedCountries
|
|
||||||
var blockedCountries []attr.Value
|
|
||||||
if distribution.Config != nil && distribution.Config.BlockedCountries != nil {
|
|
||||||
for _, c := range *distribution.Config.BlockedCountries {
|
|
||||||
blockedCountries = append(blockedCountries, types.StringValue(string(c)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
modelBlockedCountries, diags := types.ListValue(types.StringType, blockedCountries)
|
|
||||||
if diags.HasError() {
|
|
||||||
return core.DiagsToError(diags)
|
|
||||||
}
|
|
||||||
|
|
||||||
// originRequestHeaders
|
|
||||||
originRequestHeaders := types.MapNull(types.StringType)
|
|
||||||
if origHeaders := distribution.Config.Backend.HttpBackend.OriginRequestHeaders; origHeaders != nil && len(*origHeaders) > 0 {
|
|
||||||
headers := map[string]attr.Value{}
|
|
||||||
for k, v := range *origHeaders {
|
|
||||||
headers[k] = types.StringValue(v)
|
|
||||||
}
|
|
||||||
mappedHeaders, diags := types.MapValue(types.StringType, headers)
|
|
||||||
originRequestHeaders = mappedHeaders
|
|
||||||
if diags.HasError() {
|
|
||||||
return core.DiagsToError(diags)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// geofencing
|
|
||||||
var oldConfig distributionConfig
|
|
||||||
oldGeofencingMap := make(map[string][]*string)
|
|
||||||
if !model.Config.IsNull() {
|
|
||||||
diags = model.Config.As(ctx, &oldConfig, basetypes.ObjectAsOptions{})
|
|
||||||
if diags.HasError() {
|
|
||||||
return core.DiagsToError(diags)
|
|
||||||
}
|
|
||||||
if oldConfig.Backend.Geofencing != nil {
|
|
||||||
oldGeofencingMap = *oldConfig.Backend.Geofencing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
reconciledGeofencingData := make(map[string][]string)
|
|
||||||
if geofencingAPI := distribution.Config.Backend.HttpBackend.Geofencing; geofencingAPI != nil && len(*geofencingAPI) > 0 {
|
|
||||||
newGeofencingMap := *geofencingAPI
|
|
||||||
for url, newCountries := range newGeofencingMap {
|
|
||||||
oldCountriesPtrs := oldGeofencingMap[url]
|
|
||||||
|
|
||||||
oldCountries := utils.ConvertPointerSliceToStringSlice(oldCountriesPtrs)
|
|
||||||
|
|
||||||
reconciledCountries := utils.ReconcileStringSlices(oldCountries, newCountries)
|
|
||||||
reconciledGeofencingData[url] = reconciledCountries
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
geofencingVal := types.MapNull(geofencingTypes.ElemType)
|
|
||||||
if len(reconciledGeofencingData) > 0 {
|
|
||||||
geofencingMapElems := make(map[string]attr.Value)
|
|
||||||
for url, countries := range reconciledGeofencingData {
|
|
||||||
listVal, diags := types.ListValueFrom(ctx, types.StringType, countries)
|
|
||||||
if diags.HasError() {
|
|
||||||
return core.DiagsToError(diags)
|
|
||||||
}
|
|
||||||
geofencingMapElems[url] = listVal
|
|
||||||
}
|
|
||||||
|
|
||||||
var mappedGeofencing basetypes.MapValue
|
|
||||||
mappedGeofencing, diags = types.MapValue(geofencingTypes.ElemType, geofencingMapElems)
|
|
||||||
if diags.HasError() {
|
|
||||||
return core.DiagsToError(diags)
|
|
||||||
}
|
|
||||||
geofencingVal = mappedGeofencing
|
|
||||||
}
|
|
||||||
|
|
||||||
// note that httpbackend is hardcoded here as long as it is the only available backend
|
|
||||||
backend, diags := types.ObjectValue(backendTypes, map[string]attr.Value{
|
|
||||||
"type": types.StringValue(*distribution.Config.Backend.HttpBackend.Type),
|
|
||||||
"origin_url": types.StringValue(*distribution.Config.Backend.HttpBackend.OriginUrl),
|
|
||||||
"origin_request_headers": originRequestHeaders,
|
|
||||||
"geofencing": geofencingVal,
|
|
||||||
})
|
|
||||||
if diags.HasError() {
|
|
||||||
return core.DiagsToError(diags)
|
|
||||||
}
|
|
||||||
|
|
||||||
optimizerVal := types.ObjectNull(optimizerTypes)
|
|
||||||
if o := distribution.Config.Optimizer; o != nil {
|
|
||||||
optimizerEnabled, ok := o.GetEnabledOk()
|
|
||||||
if ok {
|
|
||||||
var diags diag.Diagnostics
|
|
||||||
optimizerVal, diags = types.ObjectValue(optimizerTypes, map[string]attr.Value{
|
|
||||||
"enabled": types.BoolValue(optimizerEnabled),
|
|
||||||
})
|
|
||||||
if diags.HasError() {
|
|
||||||
return core.DiagsToError(diags)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cfg, diags := types.ObjectValue(configTypes, map[string]attr.Value{
|
|
||||||
"backend": backend,
|
|
||||||
"regions": modelRegions,
|
|
||||||
"blocked_countries": modelBlockedCountries,
|
|
||||||
"optimizer": optimizerVal,
|
|
||||||
})
|
|
||||||
if diags.HasError() {
|
|
||||||
return core.DiagsToError(diags)
|
|
||||||
}
|
|
||||||
model.Config = cfg
|
|
||||||
|
|
||||||
domains := []attr.Value{}
|
|
||||||
if distribution.Domains != nil {
|
|
||||||
for _, d := range *distribution.Domains {
|
|
||||||
domainErrors := []attr.Value{}
|
|
||||||
if d.Errors != nil {
|
|
||||||
for _, e := range *d.Errors {
|
|
||||||
if e.En == nil {
|
|
||||||
return fmt.Errorf("error description missing")
|
|
||||||
}
|
|
||||||
domainErrors = append(domainErrors, types.StringValue(*e.En))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
modelDomainErrors, diags := types.ListValue(types.StringType, domainErrors)
|
|
||||||
if diags.HasError() {
|
|
||||||
return core.DiagsToError(diags)
|
|
||||||
}
|
|
||||||
if d.Name == nil || d.Status == nil || d.Type == nil {
|
|
||||||
return fmt.Errorf("domain entry incomplete")
|
|
||||||
}
|
|
||||||
modelDomain, diags := types.ObjectValue(domainTypes, map[string]attr.Value{
|
|
||||||
"name": types.StringValue(*d.Name),
|
|
||||||
"status": types.StringValue(string(*d.Status)),
|
|
||||||
"type": types.StringValue(string(*d.Type)),
|
|
||||||
"errors": modelDomainErrors,
|
|
||||||
})
|
|
||||||
if diags.HasError() {
|
|
||||||
return core.DiagsToError(diags)
|
|
||||||
}
|
|
||||||
|
|
||||||
domains = append(domains, modelDomain)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
modelDomains, diags := types.ListValue(types.ObjectType{AttrTypes: domainTypes}, domains)
|
|
||||||
if diags.HasError() {
|
|
||||||
return core.DiagsToError(diags)
|
|
||||||
}
|
|
||||||
model.Domains = modelDomains
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func toCreatePayload(ctx context.Context, model *Model) (*cdn.CreateDistributionPayload, error) {
|
|
||||||
if model == nil {
|
|
||||||
return nil, fmt.Errorf("missing model")
|
|
||||||
}
|
|
||||||
cfg, err := convertConfig(ctx, model)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var optimizer *cdn.Optimizer
|
|
||||||
if cfg.Optimizer != nil {
|
|
||||||
optimizer = cdn.NewOptimizer(cfg.Optimizer.GetEnabled())
|
|
||||||
}
|
|
||||||
|
|
||||||
payload := &cdn.CreateDistributionPayload{
|
|
||||||
IntentId: cdn.PtrString(uuid.NewString()),
|
|
||||||
OriginUrl: cfg.Backend.HttpBackend.OriginUrl,
|
|
||||||
Regions: cfg.Regions,
|
|
||||||
BlockedCountries: cfg.BlockedCountries,
|
|
||||||
OriginRequestHeaders: cfg.Backend.HttpBackend.OriginRequestHeaders,
|
|
||||||
Geofencing: cfg.Backend.HttpBackend.Geofencing,
|
|
||||||
Optimizer: optimizer,
|
|
||||||
}
|
|
||||||
|
|
||||||
return payload, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func convertConfig(ctx context.Context, model *Model) (*cdn.Config, error) {
|
|
||||||
if model == nil {
|
|
||||||
return nil, errors.New("model cannot be nil")
|
|
||||||
}
|
|
||||||
if model.Config.IsNull() || model.Config.IsUnknown() {
|
|
||||||
return nil, errors.New("config cannot be nil or unknown")
|
|
||||||
}
|
|
||||||
configModel := distributionConfig{}
|
|
||||||
diags := model.Config.As(ctx, &configModel, basetypes.ObjectAsOptions{
|
|
||||||
UnhandledNullAsEmpty: false,
|
|
||||||
UnhandledUnknownAsEmpty: false,
|
|
||||||
})
|
|
||||||
if diags.HasError() {
|
|
||||||
return nil, core.DiagsToError(diags)
|
|
||||||
}
|
|
||||||
|
|
||||||
// regions
|
|
||||||
regions := []cdn.Region{}
|
|
||||||
for _, r := range *configModel.Regions {
|
|
||||||
regionEnum, err := cdn.NewRegionFromValue(r)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
regions = append(regions, *regionEnum)
|
|
||||||
}
|
|
||||||
|
|
||||||
// blockedCountries
|
|
||||||
var blockedCountries []string
|
|
||||||
if configModel.BlockedCountries != nil {
|
|
||||||
for _, blockedCountry := range *configModel.BlockedCountries {
|
|
||||||
validatedBlockedCountry, err := validateCountryCode(blockedCountry)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
blockedCountries = append(blockedCountries, validatedBlockedCountry)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// geofencing
|
|
||||||
geofencing := map[string][]string{}
|
|
||||||
if configModel.Backend.Geofencing != nil {
|
|
||||||
for endpoint, countryCodes := range *configModel.Backend.Geofencing {
|
|
||||||
geofencingCountry := make([]string, len(countryCodes))
|
|
||||||
for i, countryCodePtr := range countryCodes {
|
|
||||||
if countryCodePtr == nil {
|
|
||||||
return nil, fmt.Errorf("geofencing url %q has a null value", endpoint)
|
|
||||||
}
|
|
||||||
validatedCountry, err := validateCountryCode(*countryCodePtr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
geofencingCountry[i] = validatedCountry
|
|
||||||
}
|
|
||||||
geofencing[endpoint] = geofencingCountry
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// originRequestHeaders
|
|
||||||
originRequestHeaders := map[string]string{}
|
|
||||||
if configModel.Backend.OriginRequestHeaders != nil {
|
|
||||||
for k, v := range *configModel.Backend.OriginRequestHeaders {
|
|
||||||
originRequestHeaders[k] = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cdnConfig := &cdn.Config{
|
|
||||||
Backend: &cdn.ConfigBackend{
|
|
||||||
HttpBackend: &cdn.HttpBackend{
|
|
||||||
OriginRequestHeaders: &originRequestHeaders,
|
|
||||||
OriginUrl: &configModel.Backend.OriginURL,
|
|
||||||
Type: &configModel.Backend.Type,
|
|
||||||
Geofencing: &geofencing,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Regions: ®ions,
|
|
||||||
BlockedCountries: &blockedCountries,
|
|
||||||
}
|
|
||||||
|
|
||||||
if !utils.IsUndefined(configModel.Optimizer) {
|
|
||||||
var optimizerModel optimizerConfig
|
|
||||||
diags := configModel.Optimizer.As(ctx, &optimizerModel, basetypes.ObjectAsOptions{})
|
|
||||||
if diags.HasError() {
|
|
||||||
return nil, core.DiagsToError(diags)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !utils.IsUndefined(optimizerModel.Enabled) {
|
|
||||||
cdnConfig.Optimizer = cdn.NewOptimizer(optimizerModel.Enabled.ValueBool())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return cdnConfig, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// validateCountryCode checks for a valid country user input. This is just a quick check
|
|
||||||
// since the API already does a more thorough check.
|
|
||||||
func validateCountryCode(country string) (string, error) {
|
|
||||||
if len(country) != 2 {
|
|
||||||
return "", errors.New("country code must be exactly 2 characters long")
|
|
||||||
}
|
|
||||||
|
|
||||||
upperCountry := strings.ToUpper(country)
|
|
||||||
|
|
||||||
// Check if both characters are alphabetical letters within the ASCII range A-Z.
|
|
||||||
// Yes, we could use the unicode package, but we are only targeting ASCII letters specifically, so
|
|
||||||
// let's omit this dependency.
|
|
||||||
char1 := upperCountry[0]
|
|
||||||
char2 := upperCountry[1]
|
|
||||||
|
|
||||||
if !((char1 >= 'A' && char1 <= 'Z') && (char2 >= 'A' && char2 <= 'Z')) {
|
|
||||||
return "", fmt.Errorf("country code '%s' must consist of two alphabetical letters (A-Z or a-z)", country)
|
|
||||||
}
|
|
||||||
|
|
||||||
return upperCountry, nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,589 +0,0 @@
|
||||||
package cdn
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/google/go-cmp/cmp"
|
|
||||||
"github.com/hashicorp/terraform-plugin-framework/attr"
|
|
||||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
|
||||||
"github.com/stackitcloud/stackit-sdk-go/services/cdn"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestToCreatePayload(t *testing.T) {
|
|
||||||
headers := map[string]attr.Value{
|
|
||||||
"testHeader0": types.StringValue("testHeaderValue0"),
|
|
||||||
"testHeader1": types.StringValue("testHeaderValue1"),
|
|
||||||
}
|
|
||||||
originRequestHeaders := types.MapValueMust(types.StringType, headers)
|
|
||||||
geofencingCountries := types.ListValueMust(types.StringType, []attr.Value{
|
|
||||||
types.StringValue("DE"),
|
|
||||||
types.StringValue("FR"),
|
|
||||||
})
|
|
||||||
geofencing := types.MapValueMust(geofencingTypes.ElemType, map[string]attr.Value{
|
|
||||||
"https://de.mycoolapp.com": geofencingCountries,
|
|
||||||
})
|
|
||||||
backend := types.ObjectValueMust(backendTypes, map[string]attr.Value{
|
|
||||||
"type": types.StringValue("http"),
|
|
||||||
"origin_url": types.StringValue("https://www.mycoolapp.com"),
|
|
||||||
"origin_request_headers": originRequestHeaders,
|
|
||||||
"geofencing": geofencing,
|
|
||||||
})
|
|
||||||
regions := []attr.Value{types.StringValue("EU"), types.StringValue("US")}
|
|
||||||
regionsFixture := types.ListValueMust(types.StringType, regions)
|
|
||||||
blockedCountries := []attr.Value{types.StringValue("XX"), types.StringValue("YY"), types.StringValue("ZZ")}
|
|
||||||
blockedCountriesFixture := types.ListValueMust(types.StringType, blockedCountries)
|
|
||||||
optimizer := types.ObjectValueMust(optimizerTypes, map[string]attr.Value{
|
|
||||||
"enabled": types.BoolValue(true),
|
|
||||||
})
|
|
||||||
config := types.ObjectValueMust(configTypes, map[string]attr.Value{
|
|
||||||
"backend": backend,
|
|
||||||
"regions": regionsFixture,
|
|
||||||
"blocked_countries": blockedCountriesFixture,
|
|
||||||
"optimizer": types.ObjectNull(optimizerTypes),
|
|
||||||
})
|
|
||||||
modelFixture := func(mods ...func(*Model)) *Model {
|
|
||||||
model := &Model{
|
|
||||||
DistributionId: types.StringValue("test-distribution-id"),
|
|
||||||
ProjectId: types.StringValue("test-project-id"),
|
|
||||||
Config: config,
|
|
||||||
}
|
|
||||||
for _, mod := range mods {
|
|
||||||
mod(model)
|
|
||||||
}
|
|
||||||
return model
|
|
||||||
}
|
|
||||||
tests := map[string]struct {
|
|
||||||
Input *Model
|
|
||||||
Expected *cdn.CreateDistributionPayload
|
|
||||||
IsValid bool
|
|
||||||
}{
|
|
||||||
"happy_path": {
|
|
||||||
Input: modelFixture(),
|
|
||||||
Expected: &cdn.CreateDistributionPayload{
|
|
||||||
OriginRequestHeaders: &map[string]string{
|
|
||||||
"testHeader0": "testHeaderValue0",
|
|
||||||
"testHeader1": "testHeaderValue1",
|
|
||||||
},
|
|
||||||
OriginUrl: cdn.PtrString("https://www.mycoolapp.com"),
|
|
||||||
Regions: &[]cdn.Region{"EU", "US"},
|
|
||||||
BlockedCountries: &[]string{"XX", "YY", "ZZ"},
|
|
||||||
Geofencing: &map[string][]string{
|
|
||||||
"https://de.mycoolapp.com": {"DE", "FR"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
IsValid: true,
|
|
||||||
},
|
|
||||||
"happy_path_with_optimizer": {
|
|
||||||
Input: modelFixture(func(m *Model) {
|
|
||||||
m.Config = types.ObjectValueMust(configTypes, map[string]attr.Value{
|
|
||||||
"backend": backend,
|
|
||||||
"regions": regionsFixture,
|
|
||||||
"optimizer": optimizer,
|
|
||||||
"blocked_countries": blockedCountriesFixture,
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
Expected: &cdn.CreateDistributionPayload{
|
|
||||||
OriginRequestHeaders: &map[string]string{
|
|
||||||
"testHeader0": "testHeaderValue0",
|
|
||||||
"testHeader1": "testHeaderValue1",
|
|
||||||
},
|
|
||||||
OriginUrl: cdn.PtrString("https://www.mycoolapp.com"),
|
|
||||||
Regions: &[]cdn.Region{"EU", "US"},
|
|
||||||
Optimizer: cdn.NewOptimizer(true),
|
|
||||||
BlockedCountries: &[]string{"XX", "YY", "ZZ"},
|
|
||||||
Geofencing: &map[string][]string{
|
|
||||||
"https://de.mycoolapp.com": {"DE", "FR"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
IsValid: true,
|
|
||||||
},
|
|
||||||
"sad_path_model_nil": {
|
|
||||||
Input: nil,
|
|
||||||
Expected: nil,
|
|
||||||
IsValid: false,
|
|
||||||
},
|
|
||||||
"sad_path_config_error": {
|
|
||||||
Input: modelFixture(func(m *Model) {
|
|
||||||
m.Config = types.ObjectNull(configTypes)
|
|
||||||
}),
|
|
||||||
Expected: nil,
|
|
||||||
IsValid: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for tn, tc := range tests {
|
|
||||||
t.Run(tn, func(t *testing.T) {
|
|
||||||
res, err := toCreatePayload(context.Background(), tc.Input)
|
|
||||||
if err != nil && tc.IsValid {
|
|
||||||
t.Fatalf("Error converting model to create payload: %v", err)
|
|
||||||
}
|
|
||||||
if err == nil && !tc.IsValid {
|
|
||||||
t.Fatalf("Should have failed")
|
|
||||||
}
|
|
||||||
if tc.IsValid {
|
|
||||||
// set generated ID before diffing
|
|
||||||
tc.Expected.IntentId = res.IntentId
|
|
||||||
|
|
||||||
diff := cmp.Diff(res, tc.Expected)
|
|
||||||
if diff != "" {
|
|
||||||
t.Fatalf("Create Payload not as expected: %s", diff)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConvertConfig(t *testing.T) {
|
|
||||||
headers := map[string]attr.Value{
|
|
||||||
"testHeader0": types.StringValue("testHeaderValue0"),
|
|
||||||
"testHeader1": types.StringValue("testHeaderValue1"),
|
|
||||||
}
|
|
||||||
originRequestHeaders := types.MapValueMust(types.StringType, headers)
|
|
||||||
geofencingCountries := types.ListValueMust(types.StringType, []attr.Value{
|
|
||||||
types.StringValue("DE"),
|
|
||||||
types.StringValue("FR"),
|
|
||||||
})
|
|
||||||
geofencing := types.MapValueMust(geofencingTypes.ElemType, map[string]attr.Value{
|
|
||||||
"https://de.mycoolapp.com": geofencingCountries,
|
|
||||||
})
|
|
||||||
backend := types.ObjectValueMust(backendTypes, map[string]attr.Value{
|
|
||||||
"type": types.StringValue("http"),
|
|
||||||
"origin_url": types.StringValue("https://www.mycoolapp.com"),
|
|
||||||
"origin_request_headers": originRequestHeaders,
|
|
||||||
"geofencing": geofencing,
|
|
||||||
})
|
|
||||||
regions := []attr.Value{types.StringValue("EU"), types.StringValue("US")}
|
|
||||||
regionsFixture := types.ListValueMust(types.StringType, regions)
|
|
||||||
blockedCountries := []attr.Value{types.StringValue("XX"), types.StringValue("YY"), types.StringValue("ZZ")}
|
|
||||||
blockedCountriesFixture := types.ListValueMust(types.StringType, blockedCountries)
|
|
||||||
optimizer := types.ObjectValueMust(optimizerTypes, map[string]attr.Value{"enabled": types.BoolValue(true)})
|
|
||||||
config := types.ObjectValueMust(configTypes, map[string]attr.Value{
|
|
||||||
"backend": backend,
|
|
||||||
"regions": regionsFixture,
|
|
||||||
"optimizer": types.ObjectNull(optimizerTypes),
|
|
||||||
"blocked_countries": blockedCountriesFixture,
|
|
||||||
})
|
|
||||||
modelFixture := func(mods ...func(*Model)) *Model {
|
|
||||||
model := &Model{
|
|
||||||
DistributionId: types.StringValue("test-distribution-id"),
|
|
||||||
ProjectId: types.StringValue("test-project-id"),
|
|
||||||
Config: config,
|
|
||||||
}
|
|
||||||
for _, mod := range mods {
|
|
||||||
mod(model)
|
|
||||||
}
|
|
||||||
return model
|
|
||||||
}
|
|
||||||
tests := map[string]struct {
|
|
||||||
Input *Model
|
|
||||||
Expected *cdn.Config
|
|
||||||
IsValid bool
|
|
||||||
}{
|
|
||||||
"happy_path": {
|
|
||||||
Input: modelFixture(),
|
|
||||||
Expected: &cdn.Config{
|
|
||||||
Backend: &cdn.ConfigBackend{
|
|
||||||
HttpBackend: &cdn.HttpBackend{
|
|
||||||
OriginRequestHeaders: &map[string]string{
|
|
||||||
"testHeader0": "testHeaderValue0",
|
|
||||||
"testHeader1": "testHeaderValue1",
|
|
||||||
},
|
|
||||||
OriginUrl: cdn.PtrString("https://www.mycoolapp.com"),
|
|
||||||
Type: cdn.PtrString("http"),
|
|
||||||
Geofencing: &map[string][]string{
|
|
||||||
"https://de.mycoolapp.com": {"DE", "FR"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Regions: &[]cdn.Region{"EU", "US"},
|
|
||||||
BlockedCountries: &[]string{"XX", "YY", "ZZ"},
|
|
||||||
},
|
|
||||||
IsValid: true,
|
|
||||||
},
|
|
||||||
"happy_path_with_optimizer": {
|
|
||||||
Input: modelFixture(func(m *Model) {
|
|
||||||
m.Config = types.ObjectValueMust(configTypes, map[string]attr.Value{
|
|
||||||
"backend": backend,
|
|
||||||
"regions": regionsFixture,
|
|
||||||
"optimizer": optimizer,
|
|
||||||
"blocked_countries": blockedCountriesFixture,
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
Expected: &cdn.Config{
|
|
||||||
Backend: &cdn.ConfigBackend{
|
|
||||||
HttpBackend: &cdn.HttpBackend{
|
|
||||||
OriginRequestHeaders: &map[string]string{
|
|
||||||
"testHeader0": "testHeaderValue0",
|
|
||||||
"testHeader1": "testHeaderValue1",
|
|
||||||
},
|
|
||||||
OriginUrl: cdn.PtrString("https://www.mycoolapp.com"),
|
|
||||||
Type: cdn.PtrString("http"),
|
|
||||||
Geofencing: &map[string][]string{
|
|
||||||
"https://de.mycoolapp.com": {"DE", "FR"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Regions: &[]cdn.Region{"EU", "US"},
|
|
||||||
Optimizer: cdn.NewOptimizer(true),
|
|
||||||
BlockedCountries: &[]string{"XX", "YY", "ZZ"},
|
|
||||||
},
|
|
||||||
IsValid: true,
|
|
||||||
},
|
|
||||||
"sad_path_model_nil": {
|
|
||||||
Input: nil,
|
|
||||||
Expected: nil,
|
|
||||||
IsValid: false,
|
|
||||||
},
|
|
||||||
"sad_path_config_error": {
|
|
||||||
Input: modelFixture(func(m *Model) {
|
|
||||||
m.Config = types.ObjectNull(configTypes)
|
|
||||||
}),
|
|
||||||
Expected: nil,
|
|
||||||
IsValid: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for tn, tc := range tests {
|
|
||||||
t.Run(tn, func(t *testing.T) {
|
|
||||||
res, err := convertConfig(context.Background(), tc.Input)
|
|
||||||
if err != nil && tc.IsValid {
|
|
||||||
t.Fatalf("Error converting model to create payload: %v", err)
|
|
||||||
}
|
|
||||||
if err == nil && !tc.IsValid {
|
|
||||||
t.Fatalf("Should have failed")
|
|
||||||
}
|
|
||||||
if tc.IsValid {
|
|
||||||
diff := cmp.Diff(res, tc.Expected)
|
|
||||||
if diff != "" {
|
|
||||||
t.Fatalf("Create Payload not as expected: %s", diff)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMapFields(t *testing.T) {
|
|
||||||
createdAt := time.Now()
|
|
||||||
updatedAt := time.Now()
|
|
||||||
headers := map[string]attr.Value{
|
|
||||||
"testHeader0": types.StringValue("testHeaderValue0"),
|
|
||||||
"testHeader1": types.StringValue("testHeaderValue1"),
|
|
||||||
}
|
|
||||||
originRequestHeaders := types.MapValueMust(types.StringType, headers)
|
|
||||||
backend := types.ObjectValueMust(backendTypes, map[string]attr.Value{
|
|
||||||
"type": types.StringValue("http"),
|
|
||||||
"origin_url": types.StringValue("https://www.mycoolapp.com"),
|
|
||||||
"origin_request_headers": originRequestHeaders,
|
|
||||||
"geofencing": types.MapNull(geofencingTypes.ElemType),
|
|
||||||
})
|
|
||||||
regions := []attr.Value{types.StringValue("EU"), types.StringValue("US")}
|
|
||||||
regionsFixture := types.ListValueMust(types.StringType, regions)
|
|
||||||
blockedCountries := []attr.Value{types.StringValue("XX"), types.StringValue("YY"), types.StringValue("ZZ")}
|
|
||||||
blockedCountriesFixture := types.ListValueMust(types.StringType, blockedCountries)
|
|
||||||
geofencingCountries := types.ListValueMust(types.StringType, []attr.Value{types.StringValue("DE"), types.StringValue("BR")})
|
|
||||||
geofencing := types.MapValueMust(geofencingTypes.ElemType, map[string]attr.Value{
|
|
||||||
"test/": geofencingCountries,
|
|
||||||
})
|
|
||||||
geofencingInput := map[string][]string{"test/": {"DE", "BR"}}
|
|
||||||
optimizer := types.ObjectValueMust(optimizerTypes, map[string]attr.Value{
|
|
||||||
"enabled": types.BoolValue(true),
|
|
||||||
})
|
|
||||||
config := types.ObjectValueMust(configTypes, map[string]attr.Value{
|
|
||||||
"backend": backend,
|
|
||||||
"regions": regionsFixture,
|
|
||||||
"blocked_countries": blockedCountriesFixture,
|
|
||||||
"optimizer": types.ObjectNull(optimizerTypes),
|
|
||||||
})
|
|
||||||
|
|
||||||
emtpyErrorsList := types.ListValueMust(types.StringType, []attr.Value{})
|
|
||||||
managedDomain := types.ObjectValueMust(domainTypes, map[string]attr.Value{
|
|
||||||
"name": types.StringValue("test.stackit-cdn.com"),
|
|
||||||
"status": types.StringValue("ACTIVE"),
|
|
||||||
"type": types.StringValue("managed"),
|
|
||||||
"errors": types.ListValueMust(types.StringType, []attr.Value{}),
|
|
||||||
})
|
|
||||||
domains := types.ListValueMust(types.ObjectType{AttrTypes: domainTypes}, []attr.Value{managedDomain})
|
|
||||||
expectedModel := func(mods ...func(*Model)) *Model {
|
|
||||||
model := &Model{
|
|
||||||
ID: types.StringValue("test-project-id,test-distribution-id"),
|
|
||||||
DistributionId: types.StringValue("test-distribution-id"),
|
|
||||||
ProjectId: types.StringValue("test-project-id"),
|
|
||||||
Config: config,
|
|
||||||
Status: types.StringValue("ACTIVE"),
|
|
||||||
CreatedAt: types.StringValue(createdAt.String()),
|
|
||||||
UpdatedAt: types.StringValue(updatedAt.String()),
|
|
||||||
Errors: emtpyErrorsList,
|
|
||||||
Domains: domains,
|
|
||||||
}
|
|
||||||
for _, mod := range mods {
|
|
||||||
mod(model)
|
|
||||||
}
|
|
||||||
return model
|
|
||||||
}
|
|
||||||
distributionFixture := func(mods ...func(*cdn.Distribution)) *cdn.Distribution {
|
|
||||||
distribution := &cdn.Distribution{
|
|
||||||
Config: &cdn.Config{
|
|
||||||
Backend: &cdn.ConfigBackend{
|
|
||||||
HttpBackend: &cdn.HttpBackend{
|
|
||||||
OriginRequestHeaders: &map[string]string{
|
|
||||||
"testHeader0": "testHeaderValue0",
|
|
||||||
"testHeader1": "testHeaderValue1",
|
|
||||||
},
|
|
||||||
OriginUrl: cdn.PtrString("https://www.mycoolapp.com"),
|
|
||||||
Type: cdn.PtrString("http"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Regions: &[]cdn.Region{"EU", "US"},
|
|
||||||
BlockedCountries: &[]string{"XX", "YY", "ZZ"},
|
|
||||||
Optimizer: nil,
|
|
||||||
},
|
|
||||||
CreatedAt: &createdAt,
|
|
||||||
Domains: &[]cdn.Domain{
|
|
||||||
{
|
|
||||||
Name: cdn.PtrString("test.stackit-cdn.com"),
|
|
||||||
Status: cdn.DOMAINSTATUS_ACTIVE.Ptr(),
|
|
||||||
Type: cdn.DOMAINTYPE_MANAGED.Ptr(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Id: cdn.PtrString("test-distribution-id"),
|
|
||||||
ProjectId: cdn.PtrString("test-project-id"),
|
|
||||||
Status: cdn.DISTRIBUTIONSTATUS_ACTIVE.Ptr(),
|
|
||||||
UpdatedAt: &updatedAt,
|
|
||||||
}
|
|
||||||
for _, mod := range mods {
|
|
||||||
mod(distribution)
|
|
||||||
}
|
|
||||||
return distribution
|
|
||||||
}
|
|
||||||
tests := map[string]struct {
|
|
||||||
Input *cdn.Distribution
|
|
||||||
Expected *Model
|
|
||||||
IsValid bool
|
|
||||||
}{
|
|
||||||
"happy_path": {
|
|
||||||
Expected: expectedModel(),
|
|
||||||
Input: distributionFixture(),
|
|
||||||
IsValid: true,
|
|
||||||
},
|
|
||||||
"happy_path_with_optimizer": {
|
|
||||||
Expected: expectedModel(func(m *Model) {
|
|
||||||
m.Config = types.ObjectValueMust(configTypes, map[string]attr.Value{
|
|
||||||
"backend": backend,
|
|
||||||
"regions": regionsFixture,
|
|
||||||
"optimizer": optimizer,
|
|
||||||
"blocked_countries": blockedCountriesFixture,
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
Input: distributionFixture(func(d *cdn.Distribution) {
|
|
||||||
d.Config.Optimizer = &cdn.Optimizer{
|
|
||||||
Enabled: cdn.PtrBool(true),
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
IsValid: true,
|
|
||||||
},
|
|
||||||
"happy_path_with_geofencing": {
|
|
||||||
Expected: expectedModel(func(m *Model) {
|
|
||||||
backendWithGeofencing := types.ObjectValueMust(backendTypes, map[string]attr.Value{
|
|
||||||
"type": types.StringValue("http"),
|
|
||||||
"origin_url": types.StringValue("https://www.mycoolapp.com"),
|
|
||||||
"origin_request_headers": originRequestHeaders,
|
|
||||||
"geofencing": geofencing,
|
|
||||||
})
|
|
||||||
m.Config = types.ObjectValueMust(configTypes, map[string]attr.Value{
|
|
||||||
"backend": backendWithGeofencing,
|
|
||||||
"regions": regionsFixture,
|
|
||||||
"optimizer": types.ObjectNull(optimizerTypes),
|
|
||||||
"blocked_countries": blockedCountriesFixture,
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
Input: distributionFixture(func(d *cdn.Distribution) {
|
|
||||||
d.Config.Backend.HttpBackend.Geofencing = &geofencingInput
|
|
||||||
}),
|
|
||||||
IsValid: true,
|
|
||||||
},
|
|
||||||
"happy_path_status_error": {
|
|
||||||
Expected: expectedModel(func(m *Model) {
|
|
||||||
m.Status = types.StringValue("ERROR")
|
|
||||||
}),
|
|
||||||
Input: distributionFixture(func(d *cdn.Distribution) {
|
|
||||||
d.Status = cdn.DISTRIBUTIONSTATUS_ERROR.Ptr()
|
|
||||||
}),
|
|
||||||
IsValid: true,
|
|
||||||
},
|
|
||||||
"happy_path_custom_domain": {
|
|
||||||
Expected: expectedModel(func(m *Model) {
|
|
||||||
managedDomain := types.ObjectValueMust(domainTypes, map[string]attr.Value{
|
|
||||||
"name": types.StringValue("test.stackit-cdn.com"),
|
|
||||||
"status": types.StringValue("ACTIVE"),
|
|
||||||
"type": types.StringValue("managed"),
|
|
||||||
"errors": types.ListValueMust(types.StringType, []attr.Value{}),
|
|
||||||
})
|
|
||||||
customDomain := types.ObjectValueMust(domainTypes, map[string]attr.Value{
|
|
||||||
"name": types.StringValue("mycoolapp.info"),
|
|
||||||
"status": types.StringValue("ACTIVE"),
|
|
||||||
"type": types.StringValue("custom"),
|
|
||||||
"errors": types.ListValueMust(types.StringType, []attr.Value{}),
|
|
||||||
})
|
|
||||||
domains := types.ListValueMust(types.ObjectType{AttrTypes: domainTypes}, []attr.Value{managedDomain, customDomain})
|
|
||||||
m.Domains = domains
|
|
||||||
}),
|
|
||||||
Input: distributionFixture(func(d *cdn.Distribution) {
|
|
||||||
d.Domains = &[]cdn.Domain{
|
|
||||||
{
|
|
||||||
Name: cdn.PtrString("test.stackit-cdn.com"),
|
|
||||||
Status: cdn.DOMAINSTATUS_ACTIVE.Ptr(),
|
|
||||||
Type: cdn.DOMAINTYPE_MANAGED.Ptr(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: cdn.PtrString("mycoolapp.info"),
|
|
||||||
Status: cdn.DOMAINSTATUS_ACTIVE.Ptr(),
|
|
||||||
Type: cdn.DOMAINTYPE_CUSTOM.Ptr(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
IsValid: true,
|
|
||||||
},
|
|
||||||
"sad_path_distribution_nil": {
|
|
||||||
Expected: nil,
|
|
||||||
Input: nil,
|
|
||||||
IsValid: false,
|
|
||||||
},
|
|
||||||
"sad_path_project_id_missing": {
|
|
||||||
Expected: expectedModel(),
|
|
||||||
Input: distributionFixture(func(d *cdn.Distribution) {
|
|
||||||
d.ProjectId = nil
|
|
||||||
}),
|
|
||||||
IsValid: false,
|
|
||||||
},
|
|
||||||
"sad_path_distribution_id_missing": {
|
|
||||||
Expected: expectedModel(),
|
|
||||||
Input: distributionFixture(func(d *cdn.Distribution) {
|
|
||||||
d.Id = nil
|
|
||||||
}),
|
|
||||||
IsValid: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for tn, tc := range tests {
|
|
||||||
t.Run(tn, func(t *testing.T) {
|
|
||||||
model := &Model{}
|
|
||||||
err := mapFields(context.Background(), tc.Input, model)
|
|
||||||
if err != nil && tc.IsValid {
|
|
||||||
t.Fatalf("Error mapping fields: %v", err)
|
|
||||||
}
|
|
||||||
if err == nil && !tc.IsValid {
|
|
||||||
t.Fatalf("Should have failed")
|
|
||||||
}
|
|
||||||
if tc.IsValid {
|
|
||||||
diff := cmp.Diff(model, tc.Expected)
|
|
||||||
if diff != "" {
|
|
||||||
t.Fatalf("Create Payload not as expected: %s", diff)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestValidateCountryCode tests the validateCountryCode function with a variety of inputs.
|
|
||||||
func TestValidateCountryCode(t *testing.T) {
|
|
||||||
testCases := []struct {
|
|
||||||
name string
|
|
||||||
inputCountry string
|
|
||||||
wantOutput string
|
|
||||||
expectError bool
|
|
||||||
expectedError string
|
|
||||||
}{
|
|
||||||
// Happy Path
|
|
||||||
{
|
|
||||||
name: "Valid lowercase",
|
|
||||||
inputCountry: "us",
|
|
||||||
wantOutput: "US",
|
|
||||||
expectError: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Valid uppercase",
|
|
||||||
inputCountry: "DE",
|
|
||||||
wantOutput: "DE",
|
|
||||||
expectError: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Valid mixed case",
|
|
||||||
inputCountry: "cA",
|
|
||||||
wantOutput: "CA",
|
|
||||||
expectError: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Valid country code FR",
|
|
||||||
inputCountry: "fr",
|
|
||||||
wantOutput: "FR",
|
|
||||||
expectError: false,
|
|
||||||
},
|
|
||||||
|
|
||||||
// Error Scenarios
|
|
||||||
{
|
|
||||||
name: "Invalid length - too short",
|
|
||||||
inputCountry: "a",
|
|
||||||
wantOutput: "",
|
|
||||||
expectError: true,
|
|
||||||
expectedError: "country code must be exactly 2 characters long",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Invalid length - too long",
|
|
||||||
inputCountry: "USA",
|
|
||||||
wantOutput: "",
|
|
||||||
expectError: true,
|
|
||||||
expectedError: "country code must be exactly 2 characters long",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Invalid characters - contains number",
|
|
||||||
inputCountry: "U1",
|
|
||||||
wantOutput: "",
|
|
||||||
expectError: true,
|
|
||||||
expectedError: "country code 'U1' must consist of two alphabetical letters (A-Z or a-z)",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Invalid characters - contains symbol",
|
|
||||||
inputCountry: "D!",
|
|
||||||
wantOutput: "",
|
|
||||||
expectError: true,
|
|
||||||
expectedError: "country code 'D!' must consist of two alphabetical letters (A-Z or a-z)",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Invalid characters - both are numbers",
|
|
||||||
inputCountry: "42",
|
|
||||||
wantOutput: "",
|
|
||||||
expectError: true,
|
|
||||||
expectedError: "country code '42' must consist of two alphabetical letters (A-Z or a-z)",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Empty string",
|
|
||||||
inputCountry: "",
|
|
||||||
wantOutput: "",
|
|
||||||
expectError: true,
|
|
||||||
expectedError: "country code must be exactly 2 characters long",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range testCases {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
gotOutput, err := validateCountryCode(tc.inputCountry)
|
|
||||||
|
|
||||||
if tc.expectError {
|
|
||||||
if err == nil {
|
|
||||||
t.Errorf("expected an error for input '%s', but got none", tc.inputCountry)
|
|
||||||
} else if err.Error() != tc.expectedError {
|
|
||||||
t.Errorf("for input '%s', expected error '%s', but got '%s'", tc.inputCountry, tc.expectedError, err.Error())
|
|
||||||
}
|
|
||||||
if gotOutput != "" {
|
|
||||||
t.Errorf("expected empty string on error, but got '%s'", gotOutput)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("did not expect an error for input '%s', but got: %v", tc.inputCountry, err)
|
|
||||||
}
|
|
||||||
if gotOutput != tc.wantOutput {
|
|
||||||
t.Errorf("for input '%s', expected output '%s', but got '%s'", tc.inputCountry, tc.wantOutput, gotOutput)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
package utils
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/hashicorp/terraform-plugin-framework/diag"
|
|
||||||
"github.com/stackitcloud/stackit-sdk-go/core/config"
|
|
||||||
"github.com/stackitcloud/stackit-sdk-go/services/cdn"
|
|
||||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
|
|
||||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils"
|
|
||||||
)
|
|
||||||
|
|
||||||
func ConfigureClient(ctx context.Context, providerData *core.ProviderData, diags *diag.Diagnostics) *cdn.APIClient {
|
|
||||||
apiClientConfigOptions := []config.ConfigurationOption{
|
|
||||||
config.WithCustomAuth(providerData.RoundTripper),
|
|
||||||
utils.UserAgentConfigOption(providerData.Version),
|
|
||||||
}
|
|
||||||
if providerData.CdnCustomEndpoint != "" {
|
|
||||||
apiClientConfigOptions = append(apiClientConfigOptions, config.WithEndpoint(providerData.CdnCustomEndpoint))
|
|
||||||
}
|
|
||||||
apiClient, err := cdn.NewAPIClient(apiClientConfigOptions...)
|
|
||||||
if err != nil {
|
|
||||||
core.LogAndAddError(ctx, diags, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the resource configuration", err))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return apiClient
|
|
||||||
}
|
|
||||||
|
|
@ -1,93 +0,0 @@
|
||||||
package utils
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"os"
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/hashicorp/terraform-plugin-framework/diag"
|
|
||||||
sdkClients "github.com/stackitcloud/stackit-sdk-go/core/clients"
|
|
||||||
"github.com/stackitcloud/stackit-sdk-go/core/config"
|
|
||||||
"github.com/stackitcloud/stackit-sdk-go/services/cdn"
|
|
||||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
|
|
||||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
testVersion = "1.2.3"
|
|
||||||
testCustomEndpoint = "https://cdn-custom-endpoint.api.stackit.cloud"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestConfigureClient(t *testing.T) {
|
|
||||||
/* mock authentication by setting service account token env variable */
|
|
||||||
os.Clearenv()
|
|
||||||
err := os.Setenv(sdkClients.ServiceAccountToken, "mock-val")
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("error setting env variable: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
type args struct {
|
|
||||||
providerData *core.ProviderData
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
args args
|
|
||||||
wantErr bool
|
|
||||||
expected *cdn.APIClient
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "default endpoint",
|
|
||||||
args: args{
|
|
||||||
providerData: &core.ProviderData{
|
|
||||||
Version: testVersion,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expected: func() *cdn.APIClient {
|
|
||||||
apiClient, err := cdn.NewAPIClient(
|
|
||||||
utils.UserAgentConfigOption(testVersion),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("error configuring client: %v", err)
|
|
||||||
}
|
|
||||||
return apiClient
|
|
||||||
}(),
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "custom endpoint",
|
|
||||||
args: args{
|
|
||||||
providerData: &core.ProviderData{
|
|
||||||
Version: testVersion,
|
|
||||||
CdnCustomEndpoint: testCustomEndpoint,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expected: func() *cdn.APIClient {
|
|
||||||
apiClient, err := cdn.NewAPIClient(
|
|
||||||
utils.UserAgentConfigOption(testVersion),
|
|
||||||
config.WithEndpoint(testCustomEndpoint),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("error configuring client: %v", err)
|
|
||||||
}
|
|
||||||
return apiClient
|
|
||||||
}(),
|
|
||||||
wantErr: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
ctx := context.Background()
|
|
||||||
diags := diag.Diagnostics{}
|
|
||||||
|
|
||||||
actual := ConfigureClient(ctx, tt.args.providerData, &diags)
|
|
||||||
if diags.HasError() != tt.wantErr {
|
|
||||||
t.Errorf("ConfigureClient() error = %v, want %v", diags.HasError(), tt.wantErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !reflect.DeepEqual(actual, tt.expected) {
|
|
||||||
t.Errorf("ConfigureClient() = %v, want %v", actual, tt.expected)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,541 +0,0 @@
|
||||||
package dns_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
_ "embed"
|
|
||||||
"fmt"
|
|
||||||
"maps"
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/hashicorp/terraform-plugin-testing/config"
|
|
||||||
"github.com/hashicorp/terraform-plugin-testing/helper/acctest"
|
|
||||||
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
|
|
||||||
"github.com/hashicorp/terraform-plugin-testing/terraform"
|
|
||||||
core_config "github.com/stackitcloud/stackit-sdk-go/core/config"
|
|
||||||
"github.com/stackitcloud/stackit-sdk-go/core/utils"
|
|
||||||
"github.com/stackitcloud/stackit-sdk-go/services/dns"
|
|
||||||
"github.com/stackitcloud/stackit-sdk-go/services/dns/wait"
|
|
||||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
|
|
||||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
//go:embed testdata/resource-min.tf
|
|
||||||
resourceMinConfig string
|
|
||||||
|
|
||||||
//go:embed testdata/resource-max.tf
|
|
||||||
resourceMaxConfig string
|
|
||||||
)
|
|
||||||
|
|
||||||
var testConfigVarsMin = config.Variables{
|
|
||||||
"project_id": config.StringVariable(testutil.ProjectId),
|
|
||||||
"name": config.StringVariable("tf-acc-" + acctest.RandStringFromCharSet(8, acctest.CharSetAlpha)),
|
|
||||||
"dns_name": config.StringVariable("tf-acc-" + acctest.RandStringFromCharSet(8, acctest.CharSetAlpha) + ".example.home"),
|
|
||||||
"record_name": config.StringVariable("tf-acc-" + acctest.RandStringFromCharSet(8, acctest.CharSetAlpha)),
|
|
||||||
"record_record1": config.StringVariable("1.2.3.4"),
|
|
||||||
"record_type": config.StringVariable("A"),
|
|
||||||
}
|
|
||||||
|
|
||||||
var testConfigVarsMax = config.Variables{
|
|
||||||
"project_id": config.StringVariable(testutil.ProjectId),
|
|
||||||
"name": config.StringVariable("tf-acc-" + acctest.RandStringFromCharSet(8, acctest.CharSetAlpha)),
|
|
||||||
"dns_name": config.StringVariable("tf-acc-" + acctest.RandStringFromCharSet(8, acctest.CharSetAlpha) + ".example.home"),
|
|
||||||
"acl": config.StringVariable("0.0.0.0/0"),
|
|
||||||
"active": config.BoolVariable(true),
|
|
||||||
"contact_email": config.StringVariable("contact@example.com"),
|
|
||||||
"default_ttl": config.IntegerVariable(3600),
|
|
||||||
"description": config.StringVariable("a test description"),
|
|
||||||
"expire_time": config.IntegerVariable(1 * 24 * 60 * 60),
|
|
||||||
"is_reverse_zone": config.BoolVariable(false),
|
|
||||||
// "negative_cache": config.IntegerVariable(128),
|
|
||||||
"primaries": config.ListVariable(config.StringVariable("1.1.1.1")),
|
|
||||||
"refresh_time": config.IntegerVariable(3600),
|
|
||||||
"retry_time": config.IntegerVariable(600),
|
|
||||||
"type": config.StringVariable("primary"),
|
|
||||||
|
|
||||||
"record_name": config.StringVariable("tf-acc-" + acctest.RandStringFromCharSet(8, acctest.CharSetAlpha)),
|
|
||||||
"record_record1": config.StringVariable("1.2.3.4"),
|
|
||||||
"record_active": config.BoolVariable(true),
|
|
||||||
"record_comment": config.StringVariable("a test comment"),
|
|
||||||
"record_ttl": config.IntegerVariable(3600),
|
|
||||||
"record_type": config.StringVariable("A"),
|
|
||||||
}
|
|
||||||
|
|
||||||
func configVarsInvalid(vars config.Variables) config.Variables {
|
|
||||||
tempConfig := maps.Clone(vars)
|
|
||||||
tempConfig["dns_name"] = config.StringVariable("foo")
|
|
||||||
return tempConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
func configVarsMinUpdated() config.Variables {
|
|
||||||
tempConfig := maps.Clone(testConfigVarsMin)
|
|
||||||
tempConfig["record_record1"] = config.StringVariable("1.2.3.5")
|
|
||||||
|
|
||||||
return tempConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
func configVarsMaxUpdated() config.Variables {
|
|
||||||
tempConfig := maps.Clone(testConfigVarsMax)
|
|
||||||
tempConfig["record_record1"] = config.StringVariable("1.2.3.5")
|
|
||||||
return tempConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAccDnsMinResource(t *testing.T) {
|
|
||||||
resource.ParallelTest(t, resource.TestCase{
|
|
||||||
ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories,
|
|
||||||
CheckDestroy: testAccCheckDnsDestroy,
|
|
||||||
Steps: []resource.TestStep{
|
|
||||||
// Creation fail
|
|
||||||
{
|
|
||||||
Config: resourceMinConfig,
|
|
||||||
ConfigVariables: configVarsInvalid(testConfigVarsMin),
|
|
||||||
ExpectError: regexp.MustCompile(`not a valid dns name. Need at least two levels`),
|
|
||||||
},
|
|
||||||
// creation
|
|
||||||
{
|
|
||||||
Config: resourceMinConfig,
|
|
||||||
ConfigVariables: testConfigVarsMin,
|
|
||||||
Check: resource.ComposeAggregateTestCheckFunc(
|
|
||||||
// Zone data
|
|
||||||
resource.TestCheckResourceAttr("stackit_dns_zone.zone", "project_id", testutil.ProjectId),
|
|
||||||
resource.TestCheckResourceAttrSet("stackit_dns_zone.zone", "zone_id"),
|
|
||||||
resource.TestCheckResourceAttrSet("stackit_dns_zone.zone", "state"),
|
|
||||||
|
|
||||||
resource.TestCheckResourceAttrSet("stackit_dns_zone.zone", "primary_name_server"),
|
|
||||||
resource.TestCheckResourceAttrSet("stackit_dns_zone.zone", "serial_number"),
|
|
||||||
resource.TestCheckResourceAttrSet("stackit_dns_zone.zone", "state"),
|
|
||||||
resource.TestCheckResourceAttrSet("stackit_dns_zone.zone", "visibility"),
|
|
||||||
resource.TestCheckResourceAttrSet("stackit_dns_zone.zone", "zone_id"),
|
|
||||||
// Record set data
|
|
||||||
resource.TestCheckResourceAttrPair(
|
|
||||||
"stackit_dns_record_set.record_set", "project_id",
|
|
||||||
"stackit_dns_zone.zone", "project_id",
|
|
||||||
),
|
|
||||||
resource.TestCheckResourceAttrPair(
|
|
||||||
"stackit_dns_record_set.record_set", "zone_id",
|
|
||||||
"stackit_dns_zone.zone", "zone_id",
|
|
||||||
),
|
|
||||||
resource.TestCheckResourceAttrSet("stackit_dns_record_set.record_set", "record_set_id"),
|
|
||||||
resource.TestCheckResourceAttrSet("stackit_dns_record_set.record_set", "name"),
|
|
||||||
resource.TestCheckResourceAttr("stackit_dns_record_set.record_set", "records.#", "1"),
|
|
||||||
resource.TestCheckResourceAttr("stackit_dns_record_set.record_set", "records.0", testutil.ConvertConfigVariable(testConfigVarsMin["record_record1"])),
|
|
||||||
resource.TestCheckResourceAttr("stackit_dns_record_set.record_set", "type", testutil.ConvertConfigVariable(testConfigVarsMin["record_type"])),
|
|
||||||
resource.TestCheckResourceAttrSet("stackit_dns_record_set.record_set", "fqdn"),
|
|
||||||
resource.TestCheckResourceAttrSet("stackit_dns_record_set.record_set", "state"),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
// Data sources
|
|
||||||
{
|
|
||||||
Config: resourceMinConfig,
|
|
||||||
ConfigVariables: testConfigVarsMin,
|
|
||||||
Check: resource.ComposeAggregateTestCheckFunc(
|
|
||||||
// Zone data by zone_id
|
|
||||||
resource.TestCheckResourceAttr("data.stackit_dns_zone.zone", "project_id", testutil.ProjectId),
|
|
||||||
resource.TestCheckResourceAttrPair(
|
|
||||||
"stackit_dns_zone.zone", "zone_id",
|
|
||||||
"data.stackit_dns_zone.zone", "zone_id",
|
|
||||||
),
|
|
||||||
resource.TestCheckResourceAttrPair(
|
|
||||||
"data.stackit_dns_record_set.record_set", "zone_id",
|
|
||||||
"data.stackit_dns_zone.zone", "zone_id",
|
|
||||||
),
|
|
||||||
resource.TestCheckResourceAttrPair(
|
|
||||||
"data.stackit_dns_record_set.record_set", "project_id",
|
|
||||||
"data.stackit_dns_zone.zone", "project_id",
|
|
||||||
),
|
|
||||||
resource.TestCheckResourceAttrPair(
|
|
||||||
"stackit_dns_record_set.record_set", "project_id",
|
|
||||||
"data.stackit_dns_record_set.record_set", "project_id",
|
|
||||||
),
|
|
||||||
|
|
||||||
// Zone data by dns_name
|
|
||||||
resource.TestCheckResourceAttr("data.stackit_dns_zone.zone_name", "project_id", testutil.ProjectId),
|
|
||||||
resource.TestCheckResourceAttrPair(
|
|
||||||
"stackit_dns_zone.zone", "zone_id",
|
|
||||||
"data.stackit_dns_zone.zone_name", "zone_id",
|
|
||||||
),
|
|
||||||
resource.TestCheckResourceAttrPair(
|
|
||||||
"data.stackit_dns_record_set.record_set", "zone_id",
|
|
||||||
"data.stackit_dns_zone.zone_name", "zone_id",
|
|
||||||
),
|
|
||||||
resource.TestCheckResourceAttrPair(
|
|
||||||
"data.stackit_dns_record_set.record_set", "project_id",
|
|
||||||
"data.stackit_dns_zone.zone_name", "project_id",
|
|
||||||
),
|
|
||||||
|
|
||||||
// Record set data
|
|
||||||
resource.TestCheckResourceAttrSet("data.stackit_dns_record_set.record_set", "record_set_id"),
|
|
||||||
resource.TestCheckResourceAttrSet("data.stackit_dns_record_set.record_set", "name"),
|
|
||||||
resource.TestCheckResourceAttr("data.stackit_dns_record_set.record_set", "records.#", "1"),
|
|
||||||
resource.TestCheckResourceAttr("data.stackit_dns_record_set.record_set", "records.0", testutil.ConvertConfigVariable(testConfigVarsMin["record_record1"])),
|
|
||||||
resource.TestCheckResourceAttr("data.stackit_dns_record_set.record_set", "type", testutil.ConvertConfigVariable(testConfigVarsMin["record_type"])),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
// Import
|
|
||||||
{
|
|
||||||
ConfigVariables: testConfigVarsMin,
|
|
||||||
ResourceName: "stackit_dns_zone.zone",
|
|
||||||
ImportStateIdFunc: func(s *terraform.State) (string, error) {
|
|
||||||
r, ok := s.RootModule().Resources["stackit_dns_zone.zone"]
|
|
||||||
if !ok {
|
|
||||||
return "", fmt.Errorf("couldn't find resource stackit_dns_zone.recozonerd_set")
|
|
||||||
}
|
|
||||||
zoneId, ok := r.Primary.Attributes["zone_id"]
|
|
||||||
if !ok {
|
|
||||||
return "", fmt.Errorf("couldn't find attribute zone_id")
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Sprintf("%s,%s", testutil.ProjectId, zoneId), nil
|
|
||||||
},
|
|
||||||
ImportState: true,
|
|
||||||
ImportStateVerify: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ConfigVariables: testConfigVarsMin,
|
|
||||||
ResourceName: "stackit_dns_record_set.record_set",
|
|
||||||
ImportStateIdFunc: func(s *terraform.State) (string, error) {
|
|
||||||
r, ok := s.RootModule().Resources["stackit_dns_record_set.record_set"]
|
|
||||||
if !ok {
|
|
||||||
return "", fmt.Errorf("couldn't find resource stackit_dns_record_set.record_set")
|
|
||||||
}
|
|
||||||
zoneId, ok := r.Primary.Attributes["zone_id"]
|
|
||||||
if !ok {
|
|
||||||
return "", fmt.Errorf("couldn't find attribute zone_id")
|
|
||||||
}
|
|
||||||
recordSetId, ok := r.Primary.Attributes["record_set_id"]
|
|
||||||
if !ok {
|
|
||||||
return "", fmt.Errorf("couldn't find attribute record_set_id")
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, zoneId, recordSetId), nil
|
|
||||||
},
|
|
||||||
ImportState: true,
|
|
||||||
ImportStateVerify: true,
|
|
||||||
// Will be different because of the name vs fqdn problem, but the value is already tested in the datasource acc test
|
|
||||||
ImportStateVerifyIgnore: []string{"name"},
|
|
||||||
},
|
|
||||||
// Update. The zone ttl should not be updated according to the DNS API.
|
|
||||||
{
|
|
||||||
Config: resourceMinConfig,
|
|
||||||
ConfigVariables: configVarsMinUpdated(),
|
|
||||||
Check: resource.ComposeAggregateTestCheckFunc(
|
|
||||||
// Zone data
|
|
||||||
resource.TestCheckResourceAttr("stackit_dns_zone.zone", "project_id", testutil.ProjectId),
|
|
||||||
resource.TestCheckResourceAttrSet("stackit_dns_zone.zone", "zone_id"),
|
|
||||||
resource.TestCheckResourceAttrSet("stackit_dns_zone.zone", "state"),
|
|
||||||
resource.TestCheckResourceAttrSet("stackit_dns_zone.zone", "primary_name_server"),
|
|
||||||
resource.TestCheckResourceAttrSet("stackit_dns_zone.zone", "serial_number"),
|
|
||||||
resource.TestCheckResourceAttrSet("stackit_dns_zone.zone", "state"),
|
|
||||||
resource.TestCheckResourceAttrSet("stackit_dns_zone.zone", "visibility"),
|
|
||||||
resource.TestCheckResourceAttrSet("stackit_dns_zone.zone", "zone_id"),
|
|
||||||
// Record set data
|
|
||||||
resource.TestCheckResourceAttrPair(
|
|
||||||
"stackit_dns_record_set.record_set", "project_id",
|
|
||||||
"stackit_dns_zone.zone", "project_id",
|
|
||||||
),
|
|
||||||
resource.TestCheckResourceAttrPair(
|
|
||||||
"stackit_dns_record_set.record_set", "zone_id",
|
|
||||||
"stackit_dns_zone.zone", "zone_id",
|
|
||||||
),
|
|
||||||
resource.TestCheckResourceAttrSet("stackit_dns_record_set.record_set", "record_set_id"),
|
|
||||||
resource.TestCheckResourceAttr("stackit_dns_record_set.record_set", "name", testutil.ConvertConfigVariable(testConfigVarsMin["record_name"])),
|
|
||||||
resource.TestCheckResourceAttr("stackit_dns_record_set.record_set", "records.#", "1"),
|
|
||||||
resource.TestCheckResourceAttr("stackit_dns_record_set.record_set", "records.0", testutil.ConvertConfigVariable(configVarsMinUpdated()["record_record1"])),
|
|
||||||
resource.TestCheckResourceAttr("stackit_dns_record_set.record_set", "type", testutil.ConvertConfigVariable(testConfigVarsMin["record_type"])),
|
|
||||||
resource.TestCheckResourceAttrSet("stackit_dns_record_set.record_set", "fqdn"),
|
|
||||||
resource.TestCheckResourceAttrSet("stackit_dns_record_set.record_set", "state")),
|
|
||||||
},
|
|
||||||
// Deletion is done by the framework implicitly
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAccDnsMaxResource(t *testing.T) {
|
|
||||||
resource.ParallelTest(t, resource.TestCase{
|
|
||||||
ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories,
|
|
||||||
CheckDestroy: testAccCheckDnsDestroy,
|
|
||||||
Steps: []resource.TestStep{
|
|
||||||
// Creation
|
|
||||||
{
|
|
||||||
Config: resourceMaxConfig,
|
|
||||||
ConfigVariables: testConfigVarsMax,
|
|
||||||
Check: resource.ComposeAggregateTestCheckFunc(
|
|
||||||
// Zone data
|
|
||||||
resource.TestCheckResourceAttr("stackit_dns_zone.zone", "project_id", testutil.ProjectId),
|
|
||||||
resource.TestCheckResourceAttrSet("stackit_dns_zone.zone", "zone_id"),
|
|
||||||
resource.TestCheckResourceAttrSet("stackit_dns_zone.zone", "state"),
|
|
||||||
|
|
||||||
// Record set data
|
|
||||||
resource.TestCheckResourceAttrPair(
|
|
||||||
"stackit_dns_record_set.record_set", "project_id",
|
|
||||||
"stackit_dns_zone.zone", "project_id",
|
|
||||||
),
|
|
||||||
resource.TestCheckResourceAttrPair(
|
|
||||||
"stackit_dns_record_set.record_set", "zone_id",
|
|
||||||
"stackit_dns_zone.zone", "zone_id",
|
|
||||||
),
|
|
||||||
resource.TestCheckResourceAttr("stackit_dns_zone.zone", "acl", testutil.ConvertConfigVariable(testConfigVarsMax["acl"])),
|
|
||||||
resource.TestCheckResourceAttr("stackit_dns_zone.zone", "active", testutil.ConvertConfigVariable(testConfigVarsMax["active"])),
|
|
||||||
resource.TestCheckResourceAttr("stackit_dns_zone.zone", "contact_email", testutil.ConvertConfigVariable(testConfigVarsMax["contact_email"])),
|
|
||||||
resource.TestCheckResourceAttr("stackit_dns_zone.zone", "default_ttl", testutil.ConvertConfigVariable(testConfigVarsMax["default_ttl"])),
|
|
||||||
resource.TestCheckResourceAttr("stackit_dns_zone.zone", "description", testutil.ConvertConfigVariable(testConfigVarsMax["description"])),
|
|
||||||
resource.TestCheckResourceAttr("stackit_dns_zone.zone", "expire_time", testutil.ConvertConfigVariable(testConfigVarsMax["expire_time"])),
|
|
||||||
resource.TestCheckResourceAttr("stackit_dns_zone.zone", "is_reverse_zone", testutil.ConvertConfigVariable(testConfigVarsMax["is_reverse_zone"])),
|
|
||||||
// resource.TestCheckResourceAttrSet("stackit_dns_zone.zone", "negative_cache"),
|
|
||||||
resource.TestCheckResourceAttr("stackit_dns_zone.zone", "primaries.#", "1"),
|
|
||||||
resource.TestCheckResourceAttrSet("stackit_dns_zone.zone", "primaries.0"),
|
|
||||||
resource.TestCheckResourceAttr("stackit_dns_zone.zone", "refresh_time", testutil.ConvertConfigVariable(testConfigVarsMax["refresh_time"])),
|
|
||||||
resource.TestCheckResourceAttr("stackit_dns_zone.zone", "retry_time", testutil.ConvertConfigVariable(testConfigVarsMax["retry_time"])),
|
|
||||||
resource.TestCheckResourceAttr("stackit_dns_zone.zone", "type", testutil.ConvertConfigVariable(testConfigVarsMax["type"])),
|
|
||||||
resource.TestCheckResourceAttrSet("stackit_dns_zone.zone", "primary_name_server"),
|
|
||||||
resource.TestCheckResourceAttrSet("stackit_dns_zone.zone", "serial_number"),
|
|
||||||
resource.TestCheckResourceAttrSet("stackit_dns_zone.zone", "state"),
|
|
||||||
resource.TestCheckResourceAttrSet("stackit_dns_zone.zone", "visibility"),
|
|
||||||
resource.TestCheckResourceAttrSet("stackit_dns_zone.zone", "zone_id"),
|
|
||||||
|
|
||||||
resource.TestCheckResourceAttrSet("stackit_dns_record_set.record_set", "record_set_id"),
|
|
||||||
resource.TestCheckResourceAttr("stackit_dns_record_set.record_set", "name", testutil.ConvertConfigVariable(testConfigVarsMax["record_name"])),
|
|
||||||
resource.TestCheckResourceAttr("stackit_dns_record_set.record_set", "records.#", "1"),
|
|
||||||
resource.TestCheckResourceAttr("stackit_dns_record_set.record_set", "records.0", testutil.ConvertConfigVariable(testConfigVarsMax["record_record1"])),
|
|
||||||
resource.TestCheckResourceAttr("stackit_dns_record_set.record_set", "active", testutil.ConvertConfigVariable(testConfigVarsMax["record_active"])),
|
|
||||||
resource.TestCheckResourceAttr("stackit_dns_record_set.record_set", "comment", testutil.ConvertConfigVariable(testConfigVarsMax["record_comment"])),
|
|
||||||
resource.TestCheckResourceAttr("stackit_dns_record_set.record_set", "ttl", testutil.ConvertConfigVariable(testConfigVarsMax["record_ttl"])),
|
|
||||||
resource.TestCheckResourceAttr("stackit_dns_record_set.record_set", "type", testutil.ConvertConfigVariable(testConfigVarsMax["record_type"])),
|
|
||||||
resource.TestCheckResourceAttrSet("stackit_dns_record_set.record_set", "fqdn"),
|
|
||||||
resource.TestCheckResourceAttrSet("stackit_dns_record_set.record_set", "state"),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
// Data sources
|
|
||||||
{
|
|
||||||
Config: resourceMaxConfig,
|
|
||||||
ConfigVariables: testConfigVarsMax,
|
|
||||||
Check: resource.ComposeAggregateTestCheckFunc(
|
|
||||||
// Zone data by zone_id
|
|
||||||
resource.TestCheckResourceAttr("data.stackit_dns_zone.zone", "project_id", testutil.ProjectId),
|
|
||||||
resource.TestCheckResourceAttrPair(
|
|
||||||
"stackit_dns_zone.zone", "zone_id",
|
|
||||||
"data.stackit_dns_zone.zone", "zone_id",
|
|
||||||
),
|
|
||||||
resource.TestCheckResourceAttrPair(
|
|
||||||
"data.stackit_dns_record_set.record_set", "zone_id",
|
|
||||||
"data.stackit_dns_zone.zone", "zone_id",
|
|
||||||
),
|
|
||||||
resource.TestCheckResourceAttrPair(
|
|
||||||
"data.stackit_dns_record_set.record_set", "project_id",
|
|
||||||
"data.stackit_dns_zone.zone", "project_id",
|
|
||||||
),
|
|
||||||
resource.TestCheckResourceAttrPair(
|
|
||||||
"data.stackit_dns_record_set.record_set", "project_id",
|
|
||||||
"stackit_dns_record_set.record_set", "project_id",
|
|
||||||
),
|
|
||||||
|
|
||||||
resource.TestCheckResourceAttr("data.stackit_dns_zone.zone", "acl", testutil.ConvertConfigVariable(testConfigVarsMax["acl"])),
|
|
||||||
resource.TestCheckResourceAttr("data.stackit_dns_zone.zone", "active", testutil.ConvertConfigVariable(testConfigVarsMax["active"])),
|
|
||||||
resource.TestCheckResourceAttr("data.stackit_dns_zone.zone", "contact_email", testutil.ConvertConfigVariable(testConfigVarsMax["contact_email"])),
|
|
||||||
resource.TestCheckResourceAttr("data.stackit_dns_zone.zone", "default_ttl", testutil.ConvertConfigVariable(testConfigVarsMax["default_ttl"])),
|
|
||||||
resource.TestCheckResourceAttr("data.stackit_dns_zone.zone", "description", testutil.ConvertConfigVariable(testConfigVarsMax["description"])),
|
|
||||||
resource.TestCheckResourceAttr("data.stackit_dns_zone.zone", "expire_time", testutil.ConvertConfigVariable(testConfigVarsMax["expire_time"])),
|
|
||||||
resource.TestCheckResourceAttr("data.stackit_dns_zone.zone", "is_reverse_zone", testutil.ConvertConfigVariable(testConfigVarsMax["is_reverse_zone"])),
|
|
||||||
resource.TestCheckResourceAttr("data.stackit_dns_zone.zone", "primaries.#", "1"),
|
|
||||||
resource.TestCheckResourceAttrSet("data.stackit_dns_zone.zone", "primaries.0"),
|
|
||||||
resource.TestCheckResourceAttr("data.stackit_dns_zone.zone", "refresh_time", testutil.ConvertConfigVariable(testConfigVarsMax["refresh_time"])),
|
|
||||||
resource.TestCheckResourceAttr("data.stackit_dns_zone.zone", "retry_time", testutil.ConvertConfigVariable(testConfigVarsMax["retry_time"])),
|
|
||||||
resource.TestCheckResourceAttr("data.stackit_dns_zone.zone", "type", testutil.ConvertConfigVariable(testConfigVarsMax["type"])),
|
|
||||||
resource.TestCheckResourceAttr("data.stackit_dns_zone.zone", "dns_name", testutil.ConvertConfigVariable(testConfigVarsMax["dns_name"])),
|
|
||||||
resource.TestCheckResourceAttr("data.stackit_dns_zone.zone", "name", testutil.ConvertConfigVariable(testConfigVarsMax["name"])),
|
|
||||||
// resource.TestCheckResourceAttrSet("data.stackit_dns_zone.zone", "negative_cache"),
|
|
||||||
resource.TestCheckResourceAttrSet("data.stackit_dns_zone.zone", "serial_number"),
|
|
||||||
resource.TestCheckResourceAttrSet("data.stackit_dns_zone.zone", "state"),
|
|
||||||
resource.TestCheckResourceAttrSet("data.stackit_dns_zone.zone", "visibility"),
|
|
||||||
|
|
||||||
// Zone data by dns_name
|
|
||||||
resource.TestCheckResourceAttr("data.stackit_dns_zone.zone_name", "project_id", testutil.ProjectId),
|
|
||||||
resource.TestCheckResourceAttrPair(
|
|
||||||
"stackit_dns_zone.zone", "zone_id",
|
|
||||||
"data.stackit_dns_zone.zone_name", "zone_id",
|
|
||||||
),
|
|
||||||
resource.TestCheckResourceAttrPair(
|
|
||||||
"data.stackit_dns_record_set.record_set", "zone_id",
|
|
||||||
"data.stackit_dns_zone.zone_name", "zone_id",
|
|
||||||
),
|
|
||||||
resource.TestCheckResourceAttrPair(
|
|
||||||
"data.stackit_dns_record_set.record_set", "project_id",
|
|
||||||
"data.stackit_dns_zone.zone_name", "project_id",
|
|
||||||
),
|
|
||||||
|
|
||||||
resource.TestCheckResourceAttr("data.stackit_dns_zone.zone_name", "acl", testutil.ConvertConfigVariable(testConfigVarsMax["acl"])),
|
|
||||||
resource.TestCheckResourceAttr("data.stackit_dns_zone.zone_name", "active", testutil.ConvertConfigVariable(testConfigVarsMax["active"])),
|
|
||||||
resource.TestCheckResourceAttr("data.stackit_dns_zone.zone_name", "contact_email", testutil.ConvertConfigVariable(testConfigVarsMax["contact_email"])),
|
|
||||||
resource.TestCheckResourceAttr("data.stackit_dns_zone.zone_name", "default_ttl", testutil.ConvertConfigVariable(testConfigVarsMax["default_ttl"])),
|
|
||||||
resource.TestCheckResourceAttr("data.stackit_dns_zone.zone_name", "description", testutil.ConvertConfigVariable(testConfigVarsMax["description"])),
|
|
||||||
resource.TestCheckResourceAttr("data.stackit_dns_zone.zone_name", "expire_time", testutil.ConvertConfigVariable(testConfigVarsMax["expire_time"])),
|
|
||||||
resource.TestCheckResourceAttr("data.stackit_dns_zone.zone_name", "is_reverse_zone", testutil.ConvertConfigVariable(testConfigVarsMax["is_reverse_zone"])),
|
|
||||||
resource.TestCheckResourceAttr("data.stackit_dns_zone.zone_name", "primaries.#", "1"),
|
|
||||||
resource.TestCheckResourceAttrSet("data.stackit_dns_zone.zone_name", "primaries.0"),
|
|
||||||
resource.TestCheckResourceAttr("data.stackit_dns_zone.zone_name", "refresh_time", testutil.ConvertConfigVariable(testConfigVarsMax["refresh_time"])),
|
|
||||||
resource.TestCheckResourceAttr("data.stackit_dns_zone.zone_name", "retry_time", testutil.ConvertConfigVariable(testConfigVarsMax["retry_time"])),
|
|
||||||
resource.TestCheckResourceAttr("data.stackit_dns_zone.zone_name", "type", testutil.ConvertConfigVariable(testConfigVarsMax["type"])),
|
|
||||||
resource.TestCheckResourceAttr("data.stackit_dns_zone.zone_name", "dns_name", testutil.ConvertConfigVariable(testConfigVarsMax["dns_name"])),
|
|
||||||
resource.TestCheckResourceAttr("data.stackit_dns_zone.zone_name", "name", testutil.ConvertConfigVariable(testConfigVarsMax["name"])),
|
|
||||||
// resource.TestCheckResourceAttrSet("data.stackit_dns_zone.zone_name", "negative_cache"),
|
|
||||||
resource.TestCheckResourceAttrSet("data.stackit_dns_zone.zone_name", "serial_number"),
|
|
||||||
resource.TestCheckResourceAttrSet("data.stackit_dns_zone.zone_name", "state"),
|
|
||||||
resource.TestCheckResourceAttrSet("data.stackit_dns_zone.zone_name", "visibility"),
|
|
||||||
|
|
||||||
// Record set data
|
|
||||||
resource.TestCheckResourceAttrSet("data.stackit_dns_record_set.record_set", "record_set_id"),
|
|
||||||
resource.TestCheckResourceAttrSet("data.stackit_dns_record_set.record_set", "name"),
|
|
||||||
resource.TestCheckResourceAttr("data.stackit_dns_record_set.record_set", "active", testutil.ConvertConfigVariable(testConfigVarsMax["active"])),
|
|
||||||
resource.TestCheckResourceAttrSet("data.stackit_dns_record_set.record_set", "fqdn"),
|
|
||||||
resource.TestCheckResourceAttrSet("data.stackit_dns_record_set.record_set", "state"),
|
|
||||||
resource.TestCheckResourceAttr("data.stackit_dns_record_set.record_set", "records.#", "1"),
|
|
||||||
resource.TestCheckResourceAttr("data.stackit_dns_record_set.record_set", "records.0", testutil.ConvertConfigVariable(testConfigVarsMax["record_record1"])),
|
|
||||||
resource.TestCheckResourceAttr("data.stackit_dns_record_set.record_set", "active", testutil.ConvertConfigVariable(testConfigVarsMax["record_active"])),
|
|
||||||
resource.TestCheckResourceAttr("data.stackit_dns_record_set.record_set", "comment", testutil.ConvertConfigVariable(testConfigVarsMax["record_comment"])),
|
|
||||||
resource.TestCheckResourceAttr("data.stackit_dns_record_set.record_set", "ttl", testutil.ConvertConfigVariable(testConfigVarsMax["record_ttl"])),
|
|
||||||
resource.TestCheckResourceAttr("data.stackit_dns_record_set.record_set", "type", testutil.ConvertConfigVariable(testConfigVarsMax["record_type"])),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
// Import
|
|
||||||
{
|
|
||||||
ConfigVariables: testConfigVarsMax,
|
|
||||||
ResourceName: "stackit_dns_zone.zone",
|
|
||||||
ImportStateIdFunc: func(s *terraform.State) (string, error) {
|
|
||||||
r, ok := s.RootModule().Resources["stackit_dns_zone.zone"]
|
|
||||||
if !ok {
|
|
||||||
return "", fmt.Errorf("couldn't find resource stackit_dns_zone.record_set")
|
|
||||||
}
|
|
||||||
zoneId, ok := r.Primary.Attributes["zone_id"]
|
|
||||||
if !ok {
|
|
||||||
return "", fmt.Errorf("couldn't find attribute zone_id")
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Sprintf("%s,%s", testutil.ProjectId, zoneId), nil
|
|
||||||
},
|
|
||||||
ImportState: true,
|
|
||||||
ImportStateVerify: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ConfigVariables: testConfigVarsMax,
|
|
||||||
ResourceName: "stackit_dns_record_set.record_set",
|
|
||||||
ImportStateIdFunc: func(s *terraform.State) (string, error) {
|
|
||||||
r, ok := s.RootModule().Resources["stackit_dns_record_set.record_set"]
|
|
||||||
if !ok {
|
|
||||||
return "", fmt.Errorf("couldn't find resource stackit_dns_record_set.record_set")
|
|
||||||
}
|
|
||||||
zoneId, ok := r.Primary.Attributes["zone_id"]
|
|
||||||
if !ok {
|
|
||||||
return "", fmt.Errorf("couldn't find attribute zone_id")
|
|
||||||
}
|
|
||||||
recordSetId, ok := r.Primary.Attributes["record_set_id"]
|
|
||||||
if !ok {
|
|
||||||
return "", fmt.Errorf("couldn't find attribute record_set_id")
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, zoneId, recordSetId), nil
|
|
||||||
},
|
|
||||||
ImportState: true,
|
|
||||||
ImportStateVerify: true,
|
|
||||||
// Will be different because of the name vs fqdn problem, but the value is already tested in the datasource acc test
|
|
||||||
ImportStateVerifyIgnore: []string{"name"},
|
|
||||||
},
|
|
||||||
// Update. The zone ttl should not be updated according to the DNS API.
|
|
||||||
{
|
|
||||||
Config: resourceMaxConfig,
|
|
||||||
ConfigVariables: configVarsMaxUpdated(),
|
|
||||||
Check: resource.ComposeAggregateTestCheckFunc(
|
|
||||||
// Zone data
|
|
||||||
resource.TestCheckResourceAttr("stackit_dns_zone.zone", "project_id", testutil.ProjectId),
|
|
||||||
resource.TestCheckResourceAttrSet("stackit_dns_zone.zone", "zone_id"),
|
|
||||||
resource.TestCheckResourceAttrSet("stackit_dns_zone.zone", "state"),
|
|
||||||
resource.TestCheckResourceAttr("stackit_dns_zone.zone", "acl", testutil.ConvertConfigVariable(testConfigVarsMax["acl"])),
|
|
||||||
resource.TestCheckResourceAttr("stackit_dns_zone.zone", "active", testutil.ConvertConfigVariable(testConfigVarsMax["active"])),
|
|
||||||
resource.TestCheckResourceAttr("stackit_dns_zone.zone", "contact_email", testutil.ConvertConfigVariable(testConfigVarsMax["contact_email"])),
|
|
||||||
resource.TestCheckResourceAttr("stackit_dns_zone.zone", "default_ttl", testutil.ConvertConfigVariable(testConfigVarsMax["default_ttl"])),
|
|
||||||
resource.TestCheckResourceAttr("stackit_dns_zone.zone", "description", testutil.ConvertConfigVariable(testConfigVarsMax["description"])),
|
|
||||||
resource.TestCheckResourceAttr("stackit_dns_zone.zone", "expire_time", testutil.ConvertConfigVariable(testConfigVarsMax["expire_time"])),
|
|
||||||
resource.TestCheckResourceAttr("stackit_dns_zone.zone", "is_reverse_zone", testutil.ConvertConfigVariable(testConfigVarsMax["is_reverse_zone"])),
|
|
||||||
// resource.TestCheckResourceAttr("stackit_dns_zone.zone", "negative_cache", testutil.ConvertConfigVariable(testConfigVarsMax["negative_cache"])),
|
|
||||||
resource.TestCheckResourceAttr("stackit_dns_zone.zone", "primaries.#", "1"),
|
|
||||||
resource.TestCheckResourceAttrSet("stackit_dns_zone.zone", "primaries.0"),
|
|
||||||
resource.TestCheckResourceAttr("stackit_dns_zone.zone", "refresh_time", testutil.ConvertConfigVariable(testConfigVarsMax["refresh_time"])),
|
|
||||||
resource.TestCheckResourceAttr("stackit_dns_zone.zone", "retry_time", testutil.ConvertConfigVariable(testConfigVarsMax["retry_time"])),
|
|
||||||
resource.TestCheckResourceAttr("stackit_dns_zone.zone", "type", testutil.ConvertConfigVariable(testConfigVarsMax["type"])),
|
|
||||||
resource.TestCheckResourceAttrSet("stackit_dns_zone.zone", "primary_name_server"),
|
|
||||||
resource.TestCheckResourceAttrSet("stackit_dns_zone.zone", "serial_number"),
|
|
||||||
resource.TestCheckResourceAttrSet("stackit_dns_zone.zone", "state"),
|
|
||||||
resource.TestCheckResourceAttrSet("stackit_dns_zone.zone", "visibility"),
|
|
||||||
resource.TestCheckResourceAttrSet("stackit_dns_zone.zone", "zone_id"),
|
|
||||||
// Record set data
|
|
||||||
resource.TestCheckResourceAttrPair(
|
|
||||||
"stackit_dns_record_set.record_set", "project_id",
|
|
||||||
"stackit_dns_zone.zone", "project_id",
|
|
||||||
),
|
|
||||||
resource.TestCheckResourceAttrPair(
|
|
||||||
"stackit_dns_record_set.record_set", "zone_id",
|
|
||||||
"stackit_dns_zone.zone", "zone_id",
|
|
||||||
),
|
|
||||||
resource.TestCheckResourceAttrSet("stackit_dns_record_set.record_set", "record_set_id"),
|
|
||||||
resource.TestCheckResourceAttr("stackit_dns_record_set.record_set", "name", testutil.ConvertConfigVariable(testConfigVarsMax["record_name"])),
|
|
||||||
resource.TestCheckResourceAttr("stackit_dns_record_set.record_set", "records.#", "1"),
|
|
||||||
resource.TestCheckResourceAttr("stackit_dns_record_set.record_set", "records.0", testutil.ConvertConfigVariable(configVarsMaxUpdated()["record_record1"])),
|
|
||||||
resource.TestCheckResourceAttr("stackit_dns_record_set.record_set", "active", testutil.ConvertConfigVariable(testConfigVarsMax["record_active"])),
|
|
||||||
resource.TestCheckResourceAttr("stackit_dns_record_set.record_set", "comment", testutil.ConvertConfigVariable(testConfigVarsMax["record_comment"])),
|
|
||||||
resource.TestCheckResourceAttr("stackit_dns_record_set.record_set", "ttl", testutil.ConvertConfigVariable(testConfigVarsMax["record_ttl"])),
|
|
||||||
resource.TestCheckResourceAttr("stackit_dns_record_set.record_set", "type", testutil.ConvertConfigVariable(testConfigVarsMax["record_type"])),
|
|
||||||
resource.TestCheckResourceAttrSet("stackit_dns_record_set.record_set", "fqdn"),
|
|
||||||
resource.TestCheckResourceAttrSet("stackit_dns_record_set.record_set", "state")),
|
|
||||||
},
|
|
||||||
// Deletion is done by the framework implicitly
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func testAccCheckDnsDestroy(s *terraform.State) error {
|
|
||||||
ctx := context.Background()
|
|
||||||
var client *dns.APIClient
|
|
||||||
var err error
|
|
||||||
if testutil.DnsCustomEndpoint == "" {
|
|
||||||
client, err = dns.NewAPIClient()
|
|
||||||
} else {
|
|
||||||
client, err = dns.NewAPIClient(
|
|
||||||
core_config.WithEndpoint(testutil.DnsCustomEndpoint),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("creating client: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
zonesToDestroy := []string{}
|
|
||||||
for _, rs := range s.RootModule().Resources {
|
|
||||||
if rs.Type != "stackit_dns_zone" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// zone terraform ID: "[projectId],[zoneId]"
|
|
||||||
zoneId := strings.Split(rs.Primary.ID, core.Separator)[1]
|
|
||||||
zonesToDestroy = append(zonesToDestroy, zoneId)
|
|
||||||
}
|
|
||||||
|
|
||||||
zonesResp, err := client.ListZones(ctx, testutil.ProjectId).ActiveEq(true).Execute()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("getting zonesResp: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
zones := *zonesResp.Zones
|
|
||||||
for i := range zones {
|
|
||||||
id := *zones[i].Id
|
|
||||||
if utils.Contains(zonesToDestroy, id) {
|
|
||||||
_, err := client.DeleteZoneExecute(ctx, testutil.ProjectId, id)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("destroying zone %s during CheckDestroy: %w", *zones[i].Id, err)
|
|
||||||
}
|
|
||||||
_, err = wait.DeleteZoneWaitHandler(ctx, client, testutil.ProjectId, id).WaitWithContext(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("destroying zone %s during CheckDestroy: waiting for deletion %w", *zones[i].Id, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,183 +0,0 @@
|
||||||
package dns
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion"
|
|
||||||
dnsUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/dns/utils"
|
|
||||||
|
|
||||||
"github.com/hashicorp/terraform-plugin-framework/datasource"
|
|
||||||
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
|
|
||||||
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
|
|
||||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
|
||||||
"github.com/hashicorp/terraform-plugin-log/tflog"
|
|
||||||
"github.com/stackitcloud/stackit-sdk-go/services/dns"
|
|
||||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
|
|
||||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils"
|
|
||||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Ensure the implementation satisfies the expected interfaces.
|
|
||||||
var (
|
|
||||||
_ datasource.DataSource = &recordSetDataSource{}
|
|
||||||
)
|
|
||||||
|
|
||||||
// NewRecordSetDataSource NewZoneDataSource is a helper function to simplify the provider implementation.
|
|
||||||
func NewRecordSetDataSource() datasource.DataSource {
|
|
||||||
return &recordSetDataSource{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// recordSetDataSource is the data source implementation.
|
|
||||||
type recordSetDataSource struct {
|
|
||||||
client *dns.APIClient
|
|
||||||
}
|
|
||||||
|
|
||||||
// Metadata returns the data source type name.
|
|
||||||
func (d *recordSetDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
|
|
||||||
resp.TypeName = req.ProviderTypeName + "_dns_record_set"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Configure adds the provider configured client to the data source.
|
|
||||||
func (d *recordSetDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
|
|
||||||
providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
apiClient := dnsUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics)
|
|
||||||
if resp.Diagnostics.HasError() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
d.client = apiClient
|
|
||||||
tflog.Info(ctx, "DNS record set client configured")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Schema defines the schema for the data source.
|
|
||||||
func (d *recordSetDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
|
|
||||||
resp.Schema = schema.Schema{
|
|
||||||
Description: "DNS Record Set Resource schema.",
|
|
||||||
Attributes: map[string]schema.Attribute{
|
|
||||||
"id": schema.StringAttribute{
|
|
||||||
Description: "Terraform's internal data source. ID. It is structured as \"`project_id`,`zone_id`,`record_set_id`\".",
|
|
||||||
Computed: true,
|
|
||||||
},
|
|
||||||
"project_id": schema.StringAttribute{
|
|
||||||
Description: "STACKIT project ID to which the dns record set is associated.",
|
|
||||||
Required: true,
|
|
||||||
Validators: []validator.String{
|
|
||||||
validate.UUID(),
|
|
||||||
validate.NoSeparator(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"zone_id": schema.StringAttribute{
|
|
||||||
Description: "The zone ID to which is dns record set is associated.",
|
|
||||||
Required: true,
|
|
||||||
Validators: []validator.String{
|
|
||||||
validate.UUID(),
|
|
||||||
validate.NoSeparator(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"record_set_id": schema.StringAttribute{
|
|
||||||
Description: "The rr set id.",
|
|
||||||
Required: true,
|
|
||||||
Validators: []validator.String{
|
|
||||||
validate.UUID(),
|
|
||||||
validate.NoSeparator(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"name": schema.StringAttribute{
|
|
||||||
Description: "Name of the record which should be a valid domain according to rfc1035 Section 2.3.4. E.g. `example.com`",
|
|
||||||
Computed: true,
|
|
||||||
},
|
|
||||||
"fqdn": schema.StringAttribute{
|
|
||||||
Description: "Fully qualified domain name (FQDN) of the record set.",
|
|
||||||
Computed: true,
|
|
||||||
},
|
|
||||||
"records": schema.ListAttribute{
|
|
||||||
Description: "Records.",
|
|
||||||
Computed: true,
|
|
||||||
ElementType: types.StringType,
|
|
||||||
},
|
|
||||||
"ttl": schema.Int64Attribute{
|
|
||||||
Description: "Time to live. E.g. 3600",
|
|
||||||
Computed: true,
|
|
||||||
},
|
|
||||||
"type": schema.StringAttribute{
|
|
||||||
Description: "The record set type. E.g. `A` or `CNAME`",
|
|
||||||
Computed: true,
|
|
||||||
},
|
|
||||||
"active": schema.BoolAttribute{
|
|
||||||
Description: "Specifies if the record set is active or not.",
|
|
||||||
Computed: true,
|
|
||||||
},
|
|
||||||
"comment": schema.StringAttribute{
|
|
||||||
Description: "Comment.",
|
|
||||||
Computed: true,
|
|
||||||
},
|
|
||||||
"error": schema.StringAttribute{
|
|
||||||
Description: "Error shows error in case create/update/delete failed.",
|
|
||||||
Computed: true,
|
|
||||||
},
|
|
||||||
"state": schema.StringAttribute{
|
|
||||||
Description: "Record set state.",
|
|
||||||
Computed: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read refreshes the Terraform state with the latest data.
|
|
||||||
func (d *recordSetDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform
|
|
||||||
var model Model
|
|
||||||
diags := req.Config.Get(ctx, &model)
|
|
||||||
resp.Diagnostics.Append(diags...)
|
|
||||||
if resp.Diagnostics.HasError() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx = core.InitProviderContext(ctx)
|
|
||||||
|
|
||||||
projectId := model.ProjectId.ValueString()
|
|
||||||
zoneId := model.ZoneId.ValueString()
|
|
||||||
recordSetId := model.RecordSetId.ValueString()
|
|
||||||
ctx = tflog.SetField(ctx, "project_id", projectId)
|
|
||||||
ctx = tflog.SetField(ctx, "zone_id", zoneId)
|
|
||||||
ctx = tflog.SetField(ctx, "record_set_id", recordSetId)
|
|
||||||
recordSetResp, err := d.client.GetRecordSet(ctx, projectId, zoneId, recordSetId).Execute()
|
|
||||||
if err != nil {
|
|
||||||
utils.LogError(
|
|
||||||
ctx,
|
|
||||||
&resp.Diagnostics,
|
|
||||||
err,
|
|
||||||
"Reading record set",
|
|
||||||
fmt.Sprintf("The record set %q or zone %q does not exist in project %q.", recordSetId, zoneId, projectId),
|
|
||||||
map[int]string{
|
|
||||||
http.StatusForbidden: fmt.Sprintf("Project with ID %q not found or forbidden access", projectId),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
resp.State.RemoveResource(ctx)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx = core.LogResponse(ctx)
|
|
||||||
|
|
||||||
if recordSetResp != nil && recordSetResp.Rrset.State != nil && *recordSetResp.Rrset.State == dns.RECORDSETSTATE_DELETE_SUCCEEDED {
|
|
||||||
resp.State.RemoveResource(ctx)
|
|
||||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading record set", "Record set was deleted successfully")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = mapFields(ctx, recordSetResp, &model)
|
|
||||||
if err != nil {
|
|
||||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading record set", fmt.Sprintf("Processing API payload: %v", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
diags = resp.State.Set(ctx, model)
|
|
||||||
resp.Diagnostics.Append(diags...)
|
|
||||||
if resp.Diagnostics.HasError() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
tflog.Info(ctx, "DNS record set read")
|
|
||||||
}
|
|
||||||
|
|
@ -1,515 +0,0 @@
|
||||||
package dns
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/hashicorp/terraform-plugin-framework-validators/int64validator"
|
|
||||||
"github.com/hashicorp/terraform-plugin-framework-validators/listvalidator"
|
|
||||||
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
|
|
||||||
"github.com/hashicorp/terraform-plugin-framework/resource"
|
|
||||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
|
|
||||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
|
|
||||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
|
|
||||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
|
|
||||||
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
|
|
||||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
|
||||||
"github.com/hashicorp/terraform-plugin-log/tflog"
|
|
||||||
"github.com/stackitcloud/stackit-sdk-go/services/dns"
|
|
||||||
"github.com/stackitcloud/stackit-sdk-go/services/dns/wait"
|
|
||||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion"
|
|
||||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
|
|
||||||
dnsUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/dns/utils"
|
|
||||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils"
|
|
||||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Ensure the implementation satisfies the expected interfaces.
|
|
||||||
var (
|
|
||||||
_ resource.Resource = &recordSetResource{}
|
|
||||||
_ resource.ResourceWithConfigure = &recordSetResource{}
|
|
||||||
_ resource.ResourceWithImportState = &recordSetResource{}
|
|
||||||
)
|
|
||||||
|
|
||||||
type Model struct {
|
|
||||||
Id types.String `tfsdk:"id"` // needed by TF
|
|
||||||
RecordSetId types.String `tfsdk:"record_set_id"`
|
|
||||||
ZoneId types.String `tfsdk:"zone_id"`
|
|
||||||
ProjectId types.String `tfsdk:"project_id"`
|
|
||||||
Active types.Bool `tfsdk:"active"`
|
|
||||||
Comment types.String `tfsdk:"comment"`
|
|
||||||
Name types.String `tfsdk:"name"`
|
|
||||||
Records types.List `tfsdk:"records"`
|
|
||||||
TTL types.Int64 `tfsdk:"ttl"`
|
|
||||||
Type types.String `tfsdk:"type"`
|
|
||||||
Error types.String `tfsdk:"error"`
|
|
||||||
State types.String `tfsdk:"state"`
|
|
||||||
FQDN types.String `tfsdk:"fqdn"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewRecordSetResource is a helper function to simplify the provider implementation.
|
|
||||||
func NewRecordSetResource() resource.Resource {
|
|
||||||
return &recordSetResource{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// recordSetResource is the resource implementation.
|
|
||||||
type recordSetResource struct {
|
|
||||||
client *dns.APIClient
|
|
||||||
}
|
|
||||||
|
|
||||||
// Metadata returns the resource type name.
|
|
||||||
func (r *recordSetResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
|
|
||||||
resp.TypeName = req.ProviderTypeName + "_dns_record_set"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Configure adds the provider configured client to the resource.
|
|
||||||
func (r *recordSetResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
|
|
||||||
providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
apiClient := dnsUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics)
|
|
||||||
if resp.Diagnostics.HasError() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
r.client = apiClient
|
|
||||||
tflog.Info(ctx, "DNS record set client configured")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Schema defines the schema for the resource.
|
|
||||||
func (r *recordSetResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
|
|
||||||
resp.Schema = schema.Schema{
|
|
||||||
Description: "DNS Record Set Resource schema.",
|
|
||||||
Attributes: map[string]schema.Attribute{
|
|
||||||
"id": schema.StringAttribute{
|
|
||||||
Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`zone_id`,`record_set_id`\".",
|
|
||||||
Computed: true,
|
|
||||||
PlanModifiers: []planmodifier.String{
|
|
||||||
stringplanmodifier.UseStateForUnknown(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"project_id": schema.StringAttribute{
|
|
||||||
Description: "STACKIT project ID to which the dns record set is associated.",
|
|
||||||
Required: true,
|
|
||||||
PlanModifiers: []planmodifier.String{
|
|
||||||
stringplanmodifier.RequiresReplace(),
|
|
||||||
},
|
|
||||||
Validators: []validator.String{
|
|
||||||
validate.UUID(),
|
|
||||||
validate.NoSeparator(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"zone_id": schema.StringAttribute{
|
|
||||||
Description: "The zone ID to which is dns record set is associated.",
|
|
||||||
Required: true,
|
|
||||||
PlanModifiers: []planmodifier.String{
|
|
||||||
stringplanmodifier.RequiresReplace(),
|
|
||||||
},
|
|
||||||
Validators: []validator.String{
|
|
||||||
validate.UUID(),
|
|
||||||
validate.NoSeparator(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"record_set_id": schema.StringAttribute{
|
|
||||||
Description: "The rr set id.",
|
|
||||||
Computed: true,
|
|
||||||
PlanModifiers: []planmodifier.String{
|
|
||||||
stringplanmodifier.UseStateForUnknown(),
|
|
||||||
},
|
|
||||||
Validators: []validator.String{
|
|
||||||
validate.UUID(),
|
|
||||||
validate.NoSeparator(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"name": schema.StringAttribute{
|
|
||||||
Description: "Name of the record which should be a valid domain according to rfc1035 Section 2.3.4. E.g. `example.com`",
|
|
||||||
Required: true,
|
|
||||||
Validators: []validator.String{
|
|
||||||
stringvalidator.LengthAtLeast(1),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"fqdn": schema.StringAttribute{
|
|
||||||
Description: "Fully qualified domain name (FQDN) of the record set.",
|
|
||||||
Computed: true,
|
|
||||||
},
|
|
||||||
"records": schema.ListAttribute{
|
|
||||||
Description: "Records.",
|
|
||||||
ElementType: types.StringType,
|
|
||||||
Required: true,
|
|
||||||
Validators: []validator.List{
|
|
||||||
listvalidator.SizeAtLeast(1),
|
|
||||||
listvalidator.UniqueValues(),
|
|
||||||
listvalidator.ValueStringsAre(validate.RecordSet()),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"ttl": schema.Int64Attribute{
|
|
||||||
Description: "Time to live. E.g. 3600",
|
|
||||||
Optional: true,
|
|
||||||
Computed: true,
|
|
||||||
Validators: []validator.Int64{
|
|
||||||
int64validator.AtLeast(60),
|
|
||||||
int64validator.AtMost(99999999),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"type": schema.StringAttribute{
|
|
||||||
Description: "The record set type. E.g. `A` or `CNAME`",
|
|
||||||
Required: true,
|
|
||||||
PlanModifiers: []planmodifier.String{
|
|
||||||
stringplanmodifier.UseStateForUnknown(),
|
|
||||||
stringplanmodifier.RequiresReplace(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"active": schema.BoolAttribute{
|
|
||||||
Description: "Specifies if the record set is active or not. Defaults to `true`",
|
|
||||||
Optional: true,
|
|
||||||
Computed: true,
|
|
||||||
Default: booldefault.StaticBool(true),
|
|
||||||
},
|
|
||||||
"comment": schema.StringAttribute{
|
|
||||||
Description: "Comment.",
|
|
||||||
Optional: true,
|
|
||||||
Computed: true,
|
|
||||||
Validators: []validator.String{
|
|
||||||
stringvalidator.LengthAtMost(255),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"error": schema.StringAttribute{
|
|
||||||
Description: "Error shows error in case create/update/delete failed.",
|
|
||||||
Computed: true,
|
|
||||||
Validators: []validator.String{
|
|
||||||
stringvalidator.LengthAtMost(2000),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"state": schema.StringAttribute{
|
|
||||||
Description: "Record set state.",
|
|
||||||
Computed: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create creates the resource and sets the initial Terraform state.
|
|
||||||
func (r *recordSetResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform
|
|
||||||
// Retrieve values from plan
|
|
||||||
var model Model
|
|
||||||
diags := req.Plan.Get(ctx, &model)
|
|
||||||
resp.Diagnostics.Append(diags...)
|
|
||||||
if resp.Diagnostics.HasError() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx = core.InitProviderContext(ctx)
|
|
||||||
|
|
||||||
projectId := model.ProjectId.ValueString()
|
|
||||||
zoneId := model.ZoneId.ValueString()
|
|
||||||
ctx = tflog.SetField(ctx, "project_id", projectId)
|
|
||||||
ctx = tflog.SetField(ctx, "zone_id", zoneId)
|
|
||||||
|
|
||||||
// Generate API request body from model
|
|
||||||
payload, err := toCreatePayload(&model)
|
|
||||||
if err != nil {
|
|
||||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating record set", fmt.Sprintf("Creating API payload: %v", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Create new recordset
|
|
||||||
recordSetResp, err := r.client.CreateRecordSet(ctx, projectId, zoneId).CreateRecordSetPayload(*payload).Execute()
|
|
||||||
if err != nil || recordSetResp.Rrset == nil || recordSetResp.Rrset.Id == nil {
|
|
||||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating record set", fmt.Sprintf("Calling API: %v", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx = core.LogResponse(ctx)
|
|
||||||
|
|
||||||
// Write id attributes to state before polling via the wait handler - just in case anything goes wrong during the wait handler
|
|
||||||
utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{
|
|
||||||
"project_id": projectId,
|
|
||||||
"zone_id": zoneId,
|
|
||||||
"record_set_id": *recordSetResp.Rrset.Id,
|
|
||||||
})
|
|
||||||
if resp.Diagnostics.HasError() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
waitResp, err := wait.CreateRecordSetWaitHandler(ctx, r.client, projectId, zoneId, *recordSetResp.Rrset.Id).WaitWithContext(ctx)
|
|
||||||
if err != nil {
|
|
||||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating record set", fmt.Sprintf("Instance creation waiting: %v", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Map response body to schema
|
|
||||||
err = mapFields(ctx, waitResp, &model)
|
|
||||||
if err != nil {
|
|
||||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating record set", fmt.Sprintf("Processing API payload: %v", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Set state to fully populated data
|
|
||||||
diags = resp.State.Set(ctx, model)
|
|
||||||
resp.Diagnostics.Append(diags...)
|
|
||||||
if resp.Diagnostics.HasError() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
tflog.Info(ctx, "DNS record set created")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read refreshes the Terraform state with the latest data.
|
|
||||||
func (r *recordSetResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform
|
|
||||||
var model Model
|
|
||||||
diags := req.State.Get(ctx, &model)
|
|
||||||
resp.Diagnostics.Append(diags...)
|
|
||||||
if resp.Diagnostics.HasError() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx = core.InitProviderContext(ctx)
|
|
||||||
|
|
||||||
projectId := model.ProjectId.ValueString()
|
|
||||||
zoneId := model.ZoneId.ValueString()
|
|
||||||
recordSetId := model.RecordSetId.ValueString()
|
|
||||||
ctx = tflog.SetField(ctx, "project_id", projectId)
|
|
||||||
ctx = tflog.SetField(ctx, "zone_id", zoneId)
|
|
||||||
ctx = tflog.SetField(ctx, "record_set_id", recordSetId)
|
|
||||||
|
|
||||||
recordSetResp, err := r.client.GetRecordSet(ctx, projectId, zoneId, recordSetId).Execute()
|
|
||||||
if err != nil {
|
|
||||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading record set", fmt.Sprintf("Calling API: %v", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if recordSetResp != nil && recordSetResp.Rrset.State != nil && *recordSetResp.Rrset.State == dns.RECORDSETSTATE_DELETE_SUCCEEDED {
|
|
||||||
resp.State.RemoveResource(ctx)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx = core.LogResponse(ctx)
|
|
||||||
|
|
||||||
// Map response body to schema
|
|
||||||
err = mapFields(ctx, recordSetResp, &model)
|
|
||||||
if err != nil {
|
|
||||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading record set", fmt.Sprintf("Processing API payload: %v", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set refreshed state
|
|
||||||
diags = resp.State.Set(ctx, model)
|
|
||||||
resp.Diagnostics.Append(diags...)
|
|
||||||
if resp.Diagnostics.HasError() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
tflog.Info(ctx, "DNS record set read")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update updates the resource and sets the updated Terraform state on success.
|
|
||||||
func (r *recordSetResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform
|
|
||||||
// Retrieve values from plan
|
|
||||||
var model Model
|
|
||||||
diags := req.Plan.Get(ctx, &model)
|
|
||||||
resp.Diagnostics.Append(diags...)
|
|
||||||
if resp.Diagnostics.HasError() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx = core.InitProviderContext(ctx)
|
|
||||||
|
|
||||||
projectId := model.ProjectId.ValueString()
|
|
||||||
zoneId := model.ZoneId.ValueString()
|
|
||||||
recordSetId := model.RecordSetId.ValueString()
|
|
||||||
ctx = tflog.SetField(ctx, "project_id", projectId)
|
|
||||||
ctx = tflog.SetField(ctx, "zone_id", zoneId)
|
|
||||||
ctx = tflog.SetField(ctx, "record_set_id", recordSetId)
|
|
||||||
|
|
||||||
// Generate API request body from model
|
|
||||||
payload, err := toUpdatePayload(&model)
|
|
||||||
if err != nil {
|
|
||||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating record set", fmt.Sprintf("Creating API payload: %v", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Update recordset
|
|
||||||
_, err = r.client.PartialUpdateRecordSet(ctx, projectId, zoneId, recordSetId).PartialUpdateRecordSetPayload(*payload).Execute()
|
|
||||||
if err != nil {
|
|
||||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating record set", err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx = core.LogResponse(ctx)
|
|
||||||
|
|
||||||
waitResp, err := wait.PartialUpdateRecordSetWaitHandler(ctx, r.client, projectId, zoneId, recordSetId).WaitWithContext(ctx)
|
|
||||||
if err != nil {
|
|
||||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating record set", fmt.Sprintf("Instance update waiting: %v", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = mapFields(ctx, waitResp, &model)
|
|
||||||
if err != nil {
|
|
||||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating record set", fmt.Sprintf("Processing API payload: %v", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
diags = resp.State.Set(ctx, model)
|
|
||||||
resp.Diagnostics.Append(diags...)
|
|
||||||
if resp.Diagnostics.HasError() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
tflog.Info(ctx, "DNS record set updated")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete deletes the resource and removes the Terraform state on success.
|
|
||||||
func (r *recordSetResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform
|
|
||||||
// Retrieve values from plan
|
|
||||||
var model Model
|
|
||||||
diags := req.State.Get(ctx, &model)
|
|
||||||
resp.Diagnostics.Append(diags...)
|
|
||||||
if resp.Diagnostics.HasError() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx = core.InitProviderContext(ctx)
|
|
||||||
|
|
||||||
projectId := model.ProjectId.ValueString()
|
|
||||||
zoneId := model.ZoneId.ValueString()
|
|
||||||
recordSetId := model.RecordSetId.ValueString()
|
|
||||||
ctx = tflog.SetField(ctx, "project_id", projectId)
|
|
||||||
ctx = tflog.SetField(ctx, "zone_id", zoneId)
|
|
||||||
ctx = tflog.SetField(ctx, "record_set_id", recordSetId)
|
|
||||||
|
|
||||||
// Delete existing record set
|
|
||||||
_, err := r.client.DeleteRecordSet(ctx, projectId, zoneId, recordSetId).Execute()
|
|
||||||
if err != nil {
|
|
||||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting record set", fmt.Sprintf("Calling API: %v", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx = core.LogResponse(ctx)
|
|
||||||
|
|
||||||
_, err = wait.DeleteRecordSetWaitHandler(ctx, r.client, projectId, zoneId, recordSetId).WaitWithContext(ctx)
|
|
||||||
if err != nil {
|
|
||||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting record set", fmt.Sprintf("Instance deletion waiting: %v", err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
tflog.Info(ctx, "DNS record set deleted")
|
|
||||||
}
|
|
||||||
|
|
||||||
// ImportState imports a resource into the Terraform state on success.
|
|
||||||
// The expected format of the resource import identifier is: project_id,zone_id,record_set_id
|
|
||||||
func (r *recordSetResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
|
|
||||||
idParts := strings.Split(req.ID, core.Separator)
|
|
||||||
if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" {
|
|
||||||
core.LogAndAddError(ctx, &resp.Diagnostics,
|
|
||||||
"Error importing record set",
|
|
||||||
fmt.Sprintf("Expected import identifier with format [project_id],[zone_id],[record_set_id], got %q", req.ID),
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]interface{}{
|
|
||||||
"project_id": idParts[0],
|
|
||||||
"zone_id": idParts[1],
|
|
||||||
"record_set_id": idParts[2],
|
|
||||||
})
|
|
||||||
tflog.Info(ctx, "DNS record set state imported")
|
|
||||||
}
|
|
||||||
|
|
||||||
func mapFields(ctx context.Context, recordSetResp *dns.RecordSetResponse, model *Model) error {
|
|
||||||
if recordSetResp == nil || recordSetResp.Rrset == nil {
|
|
||||||
return fmt.Errorf("response input is nil")
|
|
||||||
}
|
|
||||||
if model == nil {
|
|
||||||
return fmt.Errorf("model input is nil")
|
|
||||||
}
|
|
||||||
recordSet := recordSetResp.Rrset
|
|
||||||
|
|
||||||
var recordSetId string
|
|
||||||
if model.RecordSetId.ValueString() != "" {
|
|
||||||
recordSetId = model.RecordSetId.ValueString()
|
|
||||||
} else if recordSet.Id != nil {
|
|
||||||
recordSetId = *recordSet.Id
|
|
||||||
} else {
|
|
||||||
return fmt.Errorf("record set id not present")
|
|
||||||
}
|
|
||||||
|
|
||||||
if recordSet.Records == nil {
|
|
||||||
model.Records = types.ListNull(types.StringType)
|
|
||||||
} else {
|
|
||||||
respRecords := []string{}
|
|
||||||
|
|
||||||
for _, record := range *recordSet.Records {
|
|
||||||
respRecords = append(respRecords, *record.Content)
|
|
||||||
}
|
|
||||||
|
|
||||||
modelRecords, err := utils.ListValuetoStringSlice(model.Records)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
reconciledRecords := utils.ReconcileStringSlices(modelRecords, respRecords)
|
|
||||||
|
|
||||||
recordsTF, diags := types.ListValueFrom(ctx, types.StringType, reconciledRecords)
|
|
||||||
if diags.HasError() {
|
|
||||||
return fmt.Errorf("failed to map records: %w", core.DiagsToError(diags))
|
|
||||||
}
|
|
||||||
|
|
||||||
model.Records = recordsTF
|
|
||||||
}
|
|
||||||
model.Id = utils.BuildInternalTerraformId(
|
|
||||||
model.ProjectId.ValueString(), model.ZoneId.ValueString(), recordSetId,
|
|
||||||
)
|
|
||||||
model.RecordSetId = types.StringPointerValue(recordSet.Id)
|
|
||||||
model.Active = types.BoolPointerValue(recordSet.Active)
|
|
||||||
model.Comment = types.StringPointerValue(recordSet.Comment)
|
|
||||||
model.Error = types.StringPointerValue(recordSet.Error)
|
|
||||||
if model.Name.IsNull() || model.Name.IsUnknown() {
|
|
||||||
model.Name = types.StringPointerValue(recordSet.Name)
|
|
||||||
}
|
|
||||||
model.FQDN = types.StringPointerValue(recordSet.Name)
|
|
||||||
model.State = types.StringValue(string(recordSet.GetState()))
|
|
||||||
model.TTL = types.Int64PointerValue(recordSet.Ttl)
|
|
||||||
model.Type = types.StringValue(string(recordSet.GetType()))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func toCreatePayload(model *Model) (*dns.CreateRecordSetPayload, error) {
|
|
||||||
if model == nil {
|
|
||||||
return nil, fmt.Errorf("nil model")
|
|
||||||
}
|
|
||||||
|
|
||||||
records := []dns.RecordPayload{}
|
|
||||||
for i, record := range model.Records.Elements() {
|
|
||||||
recordString, ok := record.(types.String)
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("expected record at index %d to be of type %T, got %T", i, types.String{}, record)
|
|
||||||
}
|
|
||||||
records = append(records, dns.RecordPayload{
|
|
||||||
Content: conversion.StringValueToPointer(recordString),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return &dns.CreateRecordSetPayload{
|
|
||||||
Comment: conversion.StringValueToPointer(model.Comment),
|
|
||||||
Name: conversion.StringValueToPointer(model.Name),
|
|
||||||
Records: &records,
|
|
||||||
Ttl: conversion.Int64ValueToPointer(model.TTL),
|
|
||||||
Type: dns.CreateRecordSetPayloadGetTypeAttributeType(conversion.StringValueToPointer(model.Type)),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func toUpdatePayload(model *Model) (*dns.PartialUpdateRecordSetPayload, error) {
|
|
||||||
if model == nil {
|
|
||||||
return nil, fmt.Errorf("nil model")
|
|
||||||
}
|
|
||||||
|
|
||||||
records := []dns.RecordPayload{}
|
|
||||||
for i, record := range model.Records.Elements() {
|
|
||||||
recordString, ok := record.(types.String)
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("expected record at index %d to be of type %T, got %T", i, types.String{}, record)
|
|
||||||
}
|
|
||||||
records = append(records, dns.RecordPayload{
|
|
||||||
Content: conversion.StringValueToPointer(recordString),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return &dns.PartialUpdateRecordSetPayload{
|
|
||||||
Comment: conversion.StringValueToPointer(model.Comment),
|
|
||||||
Name: conversion.StringValueToPointer(model.Name),
|
|
||||||
Records: &records,
|
|
||||||
Ttl: conversion.Int64ValueToPointer(model.TTL),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,375 +0,0 @@
|
||||||
package dns
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/google/go-cmp/cmp"
|
|
||||||
"github.com/hashicorp/terraform-plugin-framework/attr"
|
|
||||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
|
||||||
"github.com/stackitcloud/stackit-sdk-go/core/utils"
|
|
||||||
"github.com/stackitcloud/stackit-sdk-go/services/dns"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestMapFields(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
description string
|
|
||||||
state Model
|
|
||||||
input *dns.RecordSetResponse
|
|
||||||
expected Model
|
|
||||||
isValid bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
"default_values",
|
|
||||||
Model{
|
|
||||||
ProjectId: types.StringValue("pid"),
|
|
||||||
ZoneId: types.StringValue("zid"),
|
|
||||||
},
|
|
||||||
&dns.RecordSetResponse{
|
|
||||||
Rrset: &dns.RecordSet{
|
|
||||||
Id: utils.Ptr("rid"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Model{
|
|
||||||
Id: types.StringValue("pid,zid,rid"),
|
|
||||||
RecordSetId: types.StringValue("rid"),
|
|
||||||
ZoneId: types.StringValue("zid"),
|
|
||||||
ProjectId: types.StringValue("pid"),
|
|
||||||
Active: types.BoolNull(),
|
|
||||||
Comment: types.StringNull(),
|
|
||||||
Error: types.StringNull(),
|
|
||||||
Name: types.StringNull(),
|
|
||||||
FQDN: types.StringNull(),
|
|
||||||
Records: types.ListNull(types.StringType),
|
|
||||||
State: types.StringValue(""),
|
|
||||||
TTL: types.Int64Null(),
|
|
||||||
Type: types.StringValue(""),
|
|
||||||
},
|
|
||||||
true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"simple_values",
|
|
||||||
Model{
|
|
||||||
ProjectId: types.StringValue("pid"),
|
|
||||||
ZoneId: types.StringValue("zid"),
|
|
||||||
},
|
|
||||||
&dns.RecordSetResponse{
|
|
||||||
Rrset: &dns.RecordSet{
|
|
||||||
Id: utils.Ptr("rid"),
|
|
||||||
Active: utils.Ptr(true),
|
|
||||||
Comment: utils.Ptr("comment"),
|
|
||||||
Error: utils.Ptr("error"),
|
|
||||||
Name: utils.Ptr("name"),
|
|
||||||
Records: &[]dns.Record{
|
|
||||||
{Content: utils.Ptr("record_1")},
|
|
||||||
{Content: utils.Ptr("record_2")},
|
|
||||||
},
|
|
||||||
State: dns.RECORDSETSTATE_CREATING.Ptr(),
|
|
||||||
Ttl: utils.Ptr(int64(1)),
|
|
||||||
Type: dns.RECORDSETTYPE_A.Ptr(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Model{
|
|
||||||
Id: types.StringValue("pid,zid,rid"),
|
|
||||||
RecordSetId: types.StringValue("rid"),
|
|
||||||
ZoneId: types.StringValue("zid"),
|
|
||||||
ProjectId: types.StringValue("pid"),
|
|
||||||
Active: types.BoolValue(true),
|
|
||||||
Comment: types.StringValue("comment"),
|
|
||||||
Error: types.StringValue("error"),
|
|
||||||
Name: types.StringValue("name"),
|
|
||||||
FQDN: types.StringValue("name"),
|
|
||||||
Records: types.ListValueMust(types.StringType, []attr.Value{
|
|
||||||
types.StringValue("record_1"),
|
|
||||||
types.StringValue("record_2"),
|
|
||||||
}),
|
|
||||||
State: types.StringValue(string(dns.RECORDSETSTATE_CREATING)),
|
|
||||||
TTL: types.Int64Value(1),
|
|
||||||
Type: types.StringValue(string(dns.RECORDSETTYPE_A)),
|
|
||||||
},
|
|
||||||
true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"unordered_records",
|
|
||||||
Model{
|
|
||||||
ProjectId: types.StringValue("pid"),
|
|
||||||
ZoneId: types.StringValue("zid"),
|
|
||||||
Records: types.ListValueMust(types.StringType, []attr.Value{
|
|
||||||
types.StringValue("record_2"),
|
|
||||||
types.StringValue("record_1"),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
&dns.RecordSetResponse{
|
|
||||||
Rrset: &dns.RecordSet{
|
|
||||||
Id: utils.Ptr("rid"),
|
|
||||||
Active: utils.Ptr(true),
|
|
||||||
Comment: utils.Ptr("comment"),
|
|
||||||
Error: utils.Ptr("error"),
|
|
||||||
Name: utils.Ptr("name"),
|
|
||||||
Records: &[]dns.Record{
|
|
||||||
{Content: utils.Ptr("record_1")},
|
|
||||||
{Content: utils.Ptr("record_2")},
|
|
||||||
},
|
|
||||||
State: dns.RECORDSETSTATE_CREATING.Ptr(),
|
|
||||||
Ttl: utils.Ptr(int64(1)),
|
|
||||||
Type: dns.RECORDSETTYPE_A.Ptr(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Model{
|
|
||||||
Id: types.StringValue("pid,zid,rid"),
|
|
||||||
RecordSetId: types.StringValue("rid"),
|
|
||||||
ZoneId: types.StringValue("zid"),
|
|
||||||
ProjectId: types.StringValue("pid"),
|
|
||||||
Active: types.BoolValue(true),
|
|
||||||
Comment: types.StringValue("comment"),
|
|
||||||
Error: types.StringValue("error"),
|
|
||||||
Name: types.StringValue("name"),
|
|
||||||
FQDN: types.StringValue("name"),
|
|
||||||
Records: types.ListValueMust(types.StringType, []attr.Value{
|
|
||||||
types.StringValue("record_2"),
|
|
||||||
types.StringValue("record_1"),
|
|
||||||
}),
|
|
||||||
State: types.StringValue(string(dns.RECORDSETSTATE_CREATING)),
|
|
||||||
TTL: types.Int64Value(1),
|
|
||||||
Type: types.StringValue(string(dns.RECORDSETTYPE_A)),
|
|
||||||
},
|
|
||||||
true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"null_fields_and_int_conversions",
|
|
||||||
Model{
|
|
||||||
ProjectId: types.StringValue("pid"),
|
|
||||||
ZoneId: types.StringValue("zid"),
|
|
||||||
Name: types.StringValue("other-name"),
|
|
||||||
},
|
|
||||||
&dns.RecordSetResponse{
|
|
||||||
Rrset: &dns.RecordSet{
|
|
||||||
Id: utils.Ptr("rid"),
|
|
||||||
Active: nil,
|
|
||||||
Comment: nil,
|
|
||||||
Error: nil,
|
|
||||||
Name: utils.Ptr("name"),
|
|
||||||
Records: nil,
|
|
||||||
State: dns.RECORDSETSTATE_CREATING.Ptr(),
|
|
||||||
Ttl: utils.Ptr(int64(2123456789)),
|
|
||||||
Type: dns.RECORDSETTYPE_A.Ptr(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Model{
|
|
||||||
Id: types.StringValue("pid,zid,rid"),
|
|
||||||
RecordSetId: types.StringValue("rid"),
|
|
||||||
ZoneId: types.StringValue("zid"),
|
|
||||||
ProjectId: types.StringValue("pid"),
|
|
||||||
Active: types.BoolNull(),
|
|
||||||
Comment: types.StringNull(),
|
|
||||||
Error: types.StringNull(),
|
|
||||||
Name: types.StringValue("other-name"),
|
|
||||||
FQDN: types.StringValue("name"),
|
|
||||||
Records: types.ListNull(types.StringType),
|
|
||||||
State: types.StringValue(string(dns.RECORDSETSTATE_CREATING)),
|
|
||||||
TTL: types.Int64Value(2123456789),
|
|
||||||
Type: types.StringValue(string(dns.RECORDSETTYPE_A)),
|
|
||||||
},
|
|
||||||
true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"nil_response",
|
|
||||||
Model{
|
|
||||||
ProjectId: types.StringValue("pid"),
|
|
||||||
ZoneId: types.StringValue("zid"),
|
|
||||||
},
|
|
||||||
nil,
|
|
||||||
Model{},
|
|
||||||
false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"no_resource_id",
|
|
||||||
Model{
|
|
||||||
ProjectId: types.StringValue("pid"),
|
|
||||||
ZoneId: types.StringValue("zid"),
|
|
||||||
},
|
|
||||||
&dns.RecordSetResponse{},
|
|
||||||
Model{},
|
|
||||||
false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.description, func(t *testing.T) {
|
|
||||||
err := mapFields(context.Background(), tt.input, &tt.state)
|
|
||||||
if !tt.isValid && err == nil {
|
|
||||||
t.Fatalf("Should have failed")
|
|
||||||
}
|
|
||||||
if tt.isValid && err != nil {
|
|
||||||
t.Fatalf("Should not have failed: %v", err)
|
|
||||||
}
|
|
||||||
if tt.isValid {
|
|
||||||
diff := cmp.Diff(tt.state, tt.expected)
|
|
||||||
if diff != "" {
|
|
||||||
t.Fatalf("Data does not match: %s", diff)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestToCreatePayload(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
description string
|
|
||||||
input *Model
|
|
||||||
expected *dns.CreateRecordSetPayload
|
|
||||||
isValid bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
"default values",
|
|
||||||
&Model{},
|
|
||||||
&dns.CreateRecordSetPayload{
|
|
||||||
Records: &[]dns.RecordPayload{},
|
|
||||||
},
|
|
||||||
true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"simple_values",
|
|
||||||
&Model{
|
|
||||||
Comment: types.StringValue("comment"),
|
|
||||||
Name: types.StringValue("name"),
|
|
||||||
Records: types.ListValueMust(types.StringType, []attr.Value{
|
|
||||||
types.StringValue("record_1"),
|
|
||||||
types.StringValue("record_2"),
|
|
||||||
}),
|
|
||||||
TTL: types.Int64Value(1),
|
|
||||||
Type: types.StringValue(string(dns.RECORDSETTYPE_A)),
|
|
||||||
},
|
|
||||||
&dns.CreateRecordSetPayload{
|
|
||||||
Comment: utils.Ptr("comment"),
|
|
||||||
Name: utils.Ptr("name"),
|
|
||||||
Records: &[]dns.RecordPayload{
|
|
||||||
{Content: utils.Ptr("record_1")},
|
|
||||||
{Content: utils.Ptr("record_2")},
|
|
||||||
},
|
|
||||||
Ttl: utils.Ptr(int64(1)),
|
|
||||||
Type: dns.CREATERECORDSETPAYLOADTYPE_A.Ptr(),
|
|
||||||
},
|
|
||||||
true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"null_fields_and_int_conversions",
|
|
||||||
&Model{
|
|
||||||
Comment: types.StringNull(),
|
|
||||||
Name: types.StringValue(""),
|
|
||||||
Records: types.ListValueMust(types.StringType, nil),
|
|
||||||
TTL: types.Int64Value(2123456789),
|
|
||||||
Type: types.StringValue(string(dns.RECORDSETTYPE_A)),
|
|
||||||
},
|
|
||||||
&dns.CreateRecordSetPayload{
|
|
||||||
Comment: nil,
|
|
||||||
Name: utils.Ptr(""),
|
|
||||||
Records: &[]dns.RecordPayload{},
|
|
||||||
Ttl: utils.Ptr(int64(2123456789)),
|
|
||||||
Type: dns.CREATERECORDSETPAYLOADTYPE_A.Ptr(),
|
|
||||||
},
|
|
||||||
true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"nil_model",
|
|
||||||
nil,
|
|
||||||
nil,
|
|
||||||
false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.description, func(t *testing.T) {
|
|
||||||
output, err := toCreatePayload(tt.input)
|
|
||||||
if !tt.isValid && err == nil {
|
|
||||||
t.Fatalf("Should have failed")
|
|
||||||
}
|
|
||||||
if tt.isValid && err != nil {
|
|
||||||
t.Fatalf("Should not have failed: %v", err)
|
|
||||||
}
|
|
||||||
if tt.isValid {
|
|
||||||
diff := cmp.Diff(output, tt.expected)
|
|
||||||
if diff != "" {
|
|
||||||
t.Fatalf("Data does not match: %s", diff)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestToUpdatePayload(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
description string
|
|
||||||
input *Model
|
|
||||||
expected *dns.PartialUpdateRecordSetPayload
|
|
||||||
isValid bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
"default_values",
|
|
||||||
&Model{},
|
|
||||||
&dns.PartialUpdateRecordSetPayload{
|
|
||||||
Records: &[]dns.RecordPayload{},
|
|
||||||
},
|
|
||||||
true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"simple_values",
|
|
||||||
&Model{
|
|
||||||
Comment: types.StringValue("comment"),
|
|
||||||
Name: types.StringValue("name"),
|
|
||||||
Records: types.ListValueMust(types.StringType, []attr.Value{
|
|
||||||
types.StringValue("record_1"),
|
|
||||||
types.StringValue("record_2"),
|
|
||||||
}),
|
|
||||||
TTL: types.Int64Value(1),
|
|
||||||
},
|
|
||||||
&dns.PartialUpdateRecordSetPayload{
|
|
||||||
Comment: utils.Ptr("comment"),
|
|
||||||
Name: utils.Ptr("name"),
|
|
||||||
Records: &[]dns.RecordPayload{
|
|
||||||
{Content: utils.Ptr("record_1")},
|
|
||||||
{Content: utils.Ptr("record_2")},
|
|
||||||
},
|
|
||||||
Ttl: utils.Ptr(int64(1)),
|
|
||||||
},
|
|
||||||
true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"null_fields_and_int_conversions",
|
|
||||||
&Model{
|
|
||||||
Comment: types.StringNull(),
|
|
||||||
Name: types.StringValue(""),
|
|
||||||
Records: types.ListValueMust(types.StringType, nil),
|
|
||||||
TTL: types.Int64Value(2123456789),
|
|
||||||
},
|
|
||||||
&dns.PartialUpdateRecordSetPayload{
|
|
||||||
Comment: nil,
|
|
||||||
Name: utils.Ptr(""),
|
|
||||||
Records: &[]dns.RecordPayload{},
|
|
||||||
Ttl: utils.Ptr(int64(2123456789)),
|
|
||||||
},
|
|
||||||
true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"nil_model",
|
|
||||||
nil,
|
|
||||||
nil,
|
|
||||||
false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.description, func(t *testing.T) {
|
|
||||||
output, err := toUpdatePayload(tt.input)
|
|
||||||
if !tt.isValid && err == nil {
|
|
||||||
t.Fatalf("Should have failed")
|
|
||||||
}
|
|
||||||
if tt.isValid && err != nil {
|
|
||||||
t.Fatalf("Should not have failed: %v", err)
|
|
||||||
}
|
|
||||||
if tt.isValid {
|
|
||||||
diff := cmp.Diff(output, tt.expected)
|
|
||||||
if diff != "" {
|
|
||||||
t.Fatalf("Data does not match: %s", diff)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,74 +0,0 @@
|
||||||
variable "project_id" {}
|
|
||||||
variable "name" {}
|
|
||||||
variable "dns_name" {}
|
|
||||||
variable "acl" {}
|
|
||||||
variable "active" {}
|
|
||||||
variable "contact_email" {}
|
|
||||||
variable "default_ttl" {}
|
|
||||||
variable "description" {}
|
|
||||||
variable "expire_time" {}
|
|
||||||
variable "is_reverse_zone" {}
|
|
||||||
# variable "negative_cache" {}
|
|
||||||
variable "primaries" {}
|
|
||||||
variable "refresh_time" {}
|
|
||||||
variable "retry_time" {}
|
|
||||||
variable "type" {}
|
|
||||||
|
|
||||||
variable "record_name" {}
|
|
||||||
variable "record_record1" {}
|
|
||||||
variable "record_active" {}
|
|
||||||
variable "record_comment" {}
|
|
||||||
variable "record_ttl" {}
|
|
||||||
variable "record_type" {}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
resource "stackit_dns_zone" "zone" {
|
|
||||||
project_id = var.project_id
|
|
||||||
name = var.name
|
|
||||||
dns_name = var.dns_name
|
|
||||||
acl = var.acl
|
|
||||||
active = var.active
|
|
||||||
contact_email = var.contact_email
|
|
||||||
default_ttl = var.default_ttl
|
|
||||||
description = var.description
|
|
||||||
expire_time = var.expire_time
|
|
||||||
is_reverse_zone = var.is_reverse_zone
|
|
||||||
# negative_cache = var.negative_cache
|
|
||||||
primaries = var.primaries
|
|
||||||
refresh_time = var.refresh_time
|
|
||||||
retry_time = var.retry_time
|
|
||||||
type = var.type
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
resource "stackit_dns_record_set" "record_set" {
|
|
||||||
project_id = var.project_id
|
|
||||||
zone_id = stackit_dns_zone.zone.zone_id
|
|
||||||
name = var.record_name
|
|
||||||
records = [
|
|
||||||
var.record_record1
|
|
||||||
]
|
|
||||||
|
|
||||||
active = var.record_active
|
|
||||||
comment = var.record_comment
|
|
||||||
ttl = var.record_ttl
|
|
||||||
type = var.record_type
|
|
||||||
}
|
|
||||||
|
|
||||||
data "stackit_dns_zone" "zone" {
|
|
||||||
project_id = var.project_id
|
|
||||||
zone_id = stackit_dns_zone.zone.zone_id
|
|
||||||
}
|
|
||||||
|
|
||||||
data "stackit_dns_zone" "zone_name" {
|
|
||||||
project_id = var.project_id
|
|
||||||
dns_name = stackit_dns_zone.zone.dns_name
|
|
||||||
}
|
|
||||||
|
|
||||||
data "stackit_dns_record_set" "record_set" {
|
|
||||||
project_id = var.project_id
|
|
||||||
zone_id = stackit_dns_zone.zone.zone_id
|
|
||||||
record_set_id = stackit_dns_record_set.record_set.record_set_id
|
|
||||||
}
|
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
variable "project_id" {}
|
|
||||||
variable "name" {}
|
|
||||||
variable "dns_name" {}
|
|
||||||
|
|
||||||
variable "record_name" {}
|
|
||||||
variable "record_record1" {}
|
|
||||||
variable "record_type" {}
|
|
||||||
|
|
||||||
|
|
||||||
resource "stackit_dns_zone" "zone" {
|
|
||||||
project_id = var.project_id
|
|
||||||
name = var.name
|
|
||||||
dns_name = var.dns_name
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
resource "stackit_dns_record_set" "record_set" {
|
|
||||||
project_id = var.project_id
|
|
||||||
zone_id = stackit_dns_zone.zone.zone_id
|
|
||||||
name = var.record_name
|
|
||||||
records = [
|
|
||||||
var.record_record1
|
|
||||||
]
|
|
||||||
type = var.record_type
|
|
||||||
}
|
|
||||||
|
|
||||||
data "stackit_dns_zone" "zone" {
|
|
||||||
project_id = var.project_id
|
|
||||||
zone_id = stackit_dns_zone.zone.zone_id
|
|
||||||
}
|
|
||||||
|
|
||||||
data "stackit_dns_zone" "zone_name" {
|
|
||||||
project_id = var.project_id
|
|
||||||
dns_name = stackit_dns_zone.zone.dns_name
|
|
||||||
}
|
|
||||||
|
|
||||||
data "stackit_dns_record_set" "record_set" {
|
|
||||||
project_id = var.project_id
|
|
||||||
zone_id = stackit_dns_zone.zone.zone_id
|
|
||||||
record_set_id = stackit_dns_record_set.record_set.record_set_id
|
|
||||||
}
|
|
||||||