diff --git a/go.mod b/go.mod index b7892412e..e4150bf2d 100644 --- a/go.mod +++ b/go.mod @@ -21,6 +21,7 @@ require ( github.com/samber/lo v1.47.0 github.com/stoewer/go-strcase v1.3.0 github.com/stretchr/testify v1.10.0 + github.com/xeipuuv/gojsonschema v1.2.0 golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c golang.org/x/tools v0.26.0 gopkg.in/yaml.v3 v3.0.1 @@ -45,6 +46,8 @@ require ( github.com/rs/zerolog v1.33.0 // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect + github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect + github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.47.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0 // indirect go.opentelemetry.io/otel v1.22.0 // indirect diff --git a/go.sum b/go.sum index 3f4dc326f..2c9ebc79d 100644 --- a/go.sum +++ b/go.sum @@ -586,6 +586,12 @@ github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAh github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= diff --git a/internal/sdkprovider/service/alloydbomni/alloydbomni.go b/internal/sdkprovider/service/alloydbomni/alloydbomni.go index 0ce8d8258..c06b6970f 100644 --- a/internal/sdkprovider/service/alloydbomni/alloydbomni.go +++ b/internal/sdkprovider/service/alloydbomni/alloydbomni.go @@ -9,7 +9,6 @@ import ( "github.com/aiven/go-client-codegen/handler/alloydbomni" "github.com/google/go-cmp/cmp" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" "github.com/aiven/terraform-provider-aiven/internal/common" "github.com/aiven/terraform-provider-aiven/internal/schemautil" @@ -20,11 +19,11 @@ const serviceAccountCredentials = "service_account_credentials" func aivenAlloyDBOmniSchema() map[string]*schema.Schema { s := schemautil.ServiceCommonSchemaWithUserConfig(schemautil.ServiceTypeAlloyDBOmni) s[serviceAccountCredentials] = &schema.Schema{ - Description: "Your [Google service account key](https://cloud.google.com/iam/docs/service-account-creds#key-types) in JSON format.", - Optional: true, - Sensitive: true, - Type: schema.TypeString, - ValidateFunc: validation.StringIsJSON, + Description: "Your [Google service account key](https://cloud.google.com/iam/docs/service-account-creds#key-types) in JSON format.", + Optional: true, + Sensitive: true, + Type: schema.TypeString, + ValidateDiagFunc: validateServiceAccountCredentials, } s[schemautil.ServiceTypeAlloyDBOmni] = &schema.Schema{ Type: schema.TypeList, diff --git a/internal/sdkprovider/service/alloydbomni/alloydbomni_test.go b/internal/sdkprovider/service/alloydbomni/alloydbomni_test.go index ed762d79c..b65b6d9c9 100644 --- a/internal/sdkprovider/service/alloydbomni/alloydbomni_test.go +++ b/internal/sdkprovider/service/alloydbomni/alloydbomni_test.go @@ -1072,28 +1072,57 @@ func TestAccAivenServiceAlloyDBOmni_service_account_credentials(t *testing.T) { project := os.Getenv("AIVEN_PROJECT_NAME") resourceName := "aiven_alloydbomni.foo" serviceName := fmt.Sprintf("test-acc-sr-%s", acc.RandStr()) + + // Service account credentials are managed by its own API + // When Terraform fails to create a service because of this field, + // the whole resource is tainted, and must be replaced + serviceAccountCredentialsInvalid := testAccAivenServiceAlloyDBOmniServiceAccountCredentials( + project, serviceName, + `{ + "private_key": "-----BEGIN PRIVATE KEY--.........----END PRIVATE KEY-----\n", + "client_email": "example@aiven.io", + "client_id": "example_user_id", + "type": "service_account", + "project_id": "example_project_id" + }`, + ) + serviceAccountCredentialsEmpty := testAccAivenServiceAlloyDBOmniServiceAccountCredentials( + project, serviceName, "", + ) + serviceAccountCredentialsValid := testAccAivenServiceAlloyDBOmniServiceAccountCredentials( + project, serviceName, getTestServiceAccountCredentials("foo"), + ) + serviceAccountCredentialsValidModified := testAccAivenServiceAlloyDBOmniServiceAccountCredentials( + project, serviceName, getTestServiceAccountCredentials("bar"), + ) + resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acc.TestAccPreCheck(t) }, ProtoV6ProviderFactories: acc.TestProtoV6ProviderFactories, CheckDestroy: acc.TestAccCheckAivenServiceResourceDestroy, Steps: []resource.TestStep{ + { + // 0. Invalid credentials + Config: serviceAccountCredentialsInvalid, + ExpectError: regexp.MustCompile(`private_key_id is required`), + }, { // 1. No credential initially - Config: testAccAivenServiceAlloyDBOmniServiceAccountCredentials(project, serviceName, ""), + Config: serviceAccountCredentialsEmpty, Check: resource.ComposeTestCheckFunc( resource.TestCheckNoResourceAttr(resourceName, "service_account_credentials"), ), }, { // 2. Credentials are set - Config: testAccAivenServiceAlloyDBOmniServiceAccountCredentials(project, serviceName, "foo"), + Config: serviceAccountCredentialsValid, Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrSet(resourceName, "service_account_credentials"), ), }, { // 3. Updates the credentials - Config: testAccAivenServiceAlloyDBOmniServiceAccountCredentials(project, serviceName, "bar"), + Config: serviceAccountCredentialsValidModified, Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrSet(resourceName, "service_account_credentials"), // Validates they key has been updated @@ -1111,7 +1140,7 @@ func TestAccAivenServiceAlloyDBOmni_service_account_credentials(t *testing.T) { }, { // 4. Removes the credentials - Config: testAccAivenServiceAlloyDBOmniServiceAccountCredentials(project, serviceName, ""), + Config: serviceAccountCredentialsEmpty, Check: resource.ComposeTestCheckFunc( // It looks like TF can't unset an attribute, when it was set. // So I can't use TestCheckNoResourceAttr here. @@ -1131,14 +1160,14 @@ func TestAccAivenServiceAlloyDBOmni_service_account_credentials(t *testing.T) { }, { // 5. Re-applies the credential, so we can check unexpected remote state changes in the next step - Config: testAccAivenServiceAlloyDBOmniServiceAccountCredentials(project, serviceName, "bar"), + Config: serviceAccountCredentialsValidModified, Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrSet(resourceName, "service_account_credentials"), ), }, { // 6. Same config. Modifies the remove state, expects non-empty plan - Config: testAccAivenServiceAlloyDBOmniServiceAccountCredentials(project, serviceName, "bar"), + Config: serviceAccountCredentialsValidModified, PlanOnly: true, ExpectNonEmptyPlan: true, PreConfig: func() { @@ -1158,23 +1187,22 @@ func TestAccAivenServiceAlloyDBOmni_service_account_credentials(t *testing.T) { func getTestServiceAccountCredentials(privateKeyID string) string { return fmt.Sprintf(`{ - "private_key_id": %q, - "private_key": "-----BEGIN PRIVATE KEY--.........----END PRIVATE KEY-----\n", - "client_email": "example@aiven.io", - "client_id": "example_user_id", - "type": "service_account", - "project_id": "example_project_id" -}`, privateKeyID) + "private_key_id": %q, + "private_key": "-----BEGIN PRIVATE KEY--.........----END PRIVATE KEY-----\n", + "client_email": "example@aiven.io", + "client_id": "example_user_id", + "type": "service_account", + "project_id": "example_project_id" + }`, privateKeyID) } -// testAccAivenServiceAlloyDBOmniServiceAccountCredentials adds service_account_credentials when privateKeyID is not empty -func testAccAivenServiceAlloyDBOmniServiceAccountCredentials(project, name, privateKeyID string) string { +func testAccAivenServiceAlloyDBOmniServiceAccountCredentials(project, name, privateKey string) string { var p string - if privateKeyID != "" { + if privateKey != "" { p = fmt.Sprintf(` service_account_credentials = <