Skip to content

Commit

Permalink
feat(alloydbomni): add service_account_credentials validation
Browse files Browse the repository at this point in the history
  • Loading branch information
byashimov committed Dec 9, 2024
1 parent b6c7377 commit a5400a5
Show file tree
Hide file tree
Showing 6 changed files with 231 additions and 23 deletions.
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
11 changes: 5 additions & 6 deletions internal/sdkprovider/service/alloydbomni/alloydbomni.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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,
Expand Down
62 changes: 45 additions & 17 deletions internal/sdkprovider/service/alloydbomni/alloydbomni_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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": "[email protected]",
"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
Expand All @@ -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.
Expand All @@ -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() {
Expand All @@ -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": "[email protected]",
"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": "[email protected]",
"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 = <<EOF
%s
EOF`, getTestServiceAccountCredentials(privateKeyID))
EOF`, privateKey)
}

return fmt.Sprintf(`
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package alloydbomni

import (
"github.com/hashicorp/go-cty/cty"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/xeipuuv/gojsonschema"
)

var _ schema.SchemaValidateDiagFunc = validateServiceAccountCredentials

func validateServiceAccountCredentials(i interface{}, p cty.Path) diag.Diagnostics {
s, ok := i.(string)
if !ok {
return diag.Errorf("expected type of %q to be string", p)
}

r, err := gojsonschema.Validate(
gojsonschema.NewStringLoader(serviceAccountCredentialsSchema),
gojsonschema.NewStringLoader(s),
)

if err != nil {
return diag.FromErr(err)
}

var diags diag.Diagnostics
for _, e := range r.Errors() {
diags = append(diags, diag.Diagnostic{
Severity: diag.Error,
Summary: e.String(),
AttributePath: p,
})
}
return diags
}

const serviceAccountCredentialsSchema = `{
"title": "Google service account credentials map",
"type": "object",
"properties": {
"type": {
"type": "string",
"title": "Credentials type",
"description": "Always service_account for credentials created in Gcloud console or CLI",
"example": "service_account"
},
"project_id": {
"type": "string",
"title": "Gcloud project id",
"example": "some-my-project"
},
"private_key_id": {
"type": "string",
"title": "Hexadecimal ID number of your private key",
"example": "5fdeb02a11ddf081930ac3ac60bf376a0aef8fad"
},
"private_key": {
"type": "string",
"title": "PEM-encoded private key",
"example": "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n"
},
"client_email": {
"type": "string",
"title": "Email of the service account",
"example": "[email protected]"
},
"client_id": {
"type": "string",
"title": "Numeric client id for this service account",
"example": "103654484443722885992"
},
"auth_uri": {
"type": "string",
"title": "The authentication endpoint of Google",
"example": "https://accounts.google.com/o/oauth2/auth"
},
"token_uri": {
"type": "string",
"title": "The token lease endpoint of Google",
"example": "https://accounts.google.com/o/oauth2/token"
},
"auth_provider_x509_cert_url": {
"type": "string",
"title": "The certificate service of Google",
"example": "https://www.googleapis.com/oauth2/v1/certs"
},
"client_x509_cert_url": {
"type": "string",
"title": "Certificate URL for your service account",
"example": "https://www.googleapis.com/robot/v1/metadata/x509/my-service-account%40some-my-project.iam.gserviceaccount.com"
},
"universe_domain": {
"type": "string",
"title": "The universe domain",
"description": "The universe domain. The default universe domain is googleapis.com."
}
},
"required": [
"private_key_id",
"private_key",
"client_email",
"client_id"
],
"additionalProperties": false
}`
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package alloydbomni

import (
"testing"

"github.com/google/go-cmp/cmp"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/stretchr/testify/assert"
)

func TestValidateServiceAccountCredentials(t *testing.T) {
cases := []struct {
name string
input string
expected diag.Diagnostics
}{
{
name: "valid",
input: `{
"private_key_id": "0",
"private_key": "1",
"client_email": "2",
"client_id": "3"
}`,
expected: nil,
},
{
name: "invalid, empty",
input: `{}`,
expected: diag.Diagnostics{
{Summary: "(root): private_key_id is required"},
{Summary: "(root): private_key is required"},
{Summary: "(root): client_email is required"},
{Summary: "(root): client_id is required"},
},
},
{
name: "missing private_key_id",
input: `{
"private_key": "1",
"client_email": "2",
"client_id": "3"
}`,
expected: diag.Diagnostics{{Summary: "(root): private_key_id is required"}},
},
{
name: "invalid type client_id",
input: `{
"private_key_id": "0",
"private_key": "1",
"client_email": "2",
"client_id": 3
}`,
expected: diag.Diagnostics{{Summary: "client_id: Invalid type. Expected: string, given: integer"}},
},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
actual := validateServiceAccountCredentials(tc.input, nil)
if diff := cmp.Diff(tc.expected, actual); diff != "" {
assert.Empty(t, diff)
}
})
}
}

0 comments on commit a5400a5

Please sign in to comment.