Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Phase 2] Include Auth Login blocks in multiplexed Provider configuration #2097

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ require (
github.com/hashicorp/terraform-plugin-framework v1.4.1
github.com/hashicorp/terraform-plugin-framework-validators v0.12.0
github.com/hashicorp/terraform-plugin-go v0.19.0
github.com/hashicorp/terraform-plugin-log v0.9.0
github.com/hashicorp/terraform-plugin-mux v0.12.0
github.com/hashicorp/terraform-plugin-sdk/v2 v2.29.0
github.com/hashicorp/vault v1.11.3
Expand Down Expand Up @@ -134,7 +135,6 @@ require (
github.com/hashicorp/serf v0.9.7 // indirect
github.com/hashicorp/terraform-exec v0.19.0 // indirect
github.com/hashicorp/terraform-json v0.17.1 // indirect
github.com/hashicorp/terraform-plugin-log v0.9.0 // indirect
github.com/hashicorp/terraform-registry-address v0.2.2 // indirect
github.com/hashicorp/terraform-svchost v0.1.1 // indirect
github.com/hashicorp/yamux v0.1.1 // indirect
Expand Down
3 changes: 3 additions & 0 deletions internal/framework/validators/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Terraform Plugin Framework Validators

This package contains custom Terraform Plugin Framework [validators](https://developer.hashicorp.com/terraform/plugin/framework/validation).
49 changes: 49 additions & 0 deletions internal/framework/validators/gcp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package validators

import (
"context"
"os"

"github.com/hashicorp/terraform-plugin-framework/schema/validator"
googleoauth "golang.org/x/oauth2/google"
)

// Credentials Validator
var _ validator.String = credentialsValidator{}

// credentialsValidator validates that a string Attribute's is valid JSON credentials.
type credentialsValidator struct{}

// Description describes the validation in plain text formatting.
func (v credentialsValidator) Description(_ context.Context) string {
return "value must be a path to valid JSON credentials or valid, raw, JSON credentials"
}

// MarkdownDescription describes the validation in Markdown formatting.
func (v credentialsValidator) MarkdownDescription(ctx context.Context) string {
return v.Description(ctx)
}

// ValidateString performs the validation.
func (v credentialsValidator) ValidateString(ctx context.Context, request validator.StringRequest, response *validator.StringResponse) {
if request.ConfigValue.IsNull() || request.ConfigValue.IsUnknown() {
return
}

value := request.ConfigValue.ValueString()

// if this is a path and we can stat it, assume it's ok
if _, err := os.Stat(value); err == nil {
return
}
if _, err := googleoauth.CredentialsFromJSON(context.Background(), []byte(value)); err != nil {
response.Diagnostics.AddError("JSON credentials are not valid", err.Error())
}
}

func GCPCredentialsValidator() validator.String {
return credentialsValidator{}
}
83 changes: 83 additions & 0 deletions internal/framework/validators/gcp_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package validators

import (
"context"
"io/ioutil"
"testing"

"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"
)

const testFakeCredentialsPath = "./test-fixtures/fake_account.json"

func TestFrameworkProvider_CredentialsValidator(t *testing.T) {
cases := map[string]struct {
ConfigValue func(t *testing.T) types.String
ExpectedWarningCount int
ExpectedErrorCount int
}{
"configuring credentials as a path to a credentials JSON file is valid": {
ConfigValue: func(t *testing.T) types.String {
return types.StringValue(testFakeCredentialsPath) // Path to a test fixture
},
},
"configuring credentials as a path to a non-existant file is NOT valid": {
ConfigValue: func(t *testing.T) types.String {
return types.StringValue("./this/path/doesnt/exist.json") // Doesn't exist
},
ExpectedErrorCount: 1,
},
"configuring credentials as a credentials JSON string is valid": {
ConfigValue: func(t *testing.T) types.String {
contents, err := ioutil.ReadFile(testFakeCredentialsPath)
if err != nil {
t.Fatalf("Unexpected error: %s", err)
}
stringContents := string(contents)
return types.StringValue(stringContents)
},
},
"configuring credentials as an empty string is not valid": {
ConfigValue: func(t *testing.T) types.String {
return types.StringValue("")
},
ExpectedErrorCount: 1,
},
"leaving credentials unconfigured is valid": {
ConfigValue: func(t *testing.T) types.String {
return types.StringNull()
},
},
}

for tn, tc := range cases {
t.Run(tn, func(t *testing.T) {
// Arrange
req := validator.StringRequest{
ConfigValue: tc.ConfigValue(t),
}

resp := validator.StringResponse{
Diagnostics: diag.Diagnostics{},
}

cv := GCPCredentialsValidator()

// Act
cv.ValidateString(context.Background(), req, &resp)

// Assert
if resp.Diagnostics.WarningsCount() > tc.ExpectedWarningCount {
t.Errorf("Expected %d warnings, got %d", tc.ExpectedWarningCount, resp.Diagnostics.WarningsCount())
}
if resp.Diagnostics.ErrorsCount() > tc.ExpectedErrorCount {
t.Errorf("Expected %d errors, got %d", tc.ExpectedErrorCount, resp.Diagnostics.ErrorsCount())
}
})
}
}
7 changes: 7 additions & 0 deletions internal/framework/validators/test-fixtures/fake_account.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"private_key_id": "foo",
"private_key": "bar",
"client_email": "[email protected]",
"client_id": "[email protected]",
"type": "service_account"
}
36 changes: 17 additions & 19 deletions internal/provider/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import (
type (
loginSchemaFunc func(string) *schema.Schema
getSchemaResource func(string) *schema.Resource
validateFunc func(data *schema.ResourceData) error
validateFunc func(data *schema.ResourceData, params map[string]interface{}) error
authLoginFunc func(*schema.ResourceData) (AuthLogin, error)
)

Expand Down Expand Up @@ -54,11 +54,6 @@ type authLoginRegistry struct {
// Register field for loginFunc and schemaFunc. A field can only be registered
// once.
func (r *authLoginRegistry) Register(field string, loginFunc authLoginFunc, schemaFunc loginSchemaFunc) error {
// TODO(JM): remove this after testing
// for now we only register auth_login_token_file
if field != consts.FieldAuthLoginTokenFile {
return nil
}
e := &authLoginEntry{
field: field,
loginFunc: loginFunc,
Expand Down Expand Up @@ -143,15 +138,15 @@ func (l *AuthLoginCommon) Init(d *schema.ResourceData, authField string, validat
return err
}

l.mount = path
l.params = params

for _, vf := range validators {
if err := vf(d); err != nil {
if err := vf(d, params); err != nil {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updating the signature for validateFunc and passing in the params here allows us to maintain existing behavior for l.params=nil returned when validation fails. This way we don't have to update any existing tests, since we only update l.params once all validators have passed

return err
}
}

l.mount = path
l.params = params

return l.validate()
}

Expand Down Expand Up @@ -278,34 +273,37 @@ func (l *AuthLoginCommon) init(d *schema.ResourceData) (string, map[string]inter

type authDefault struct {
field string
envVar string
envVars []string
defaultVal string
}

type authDefaults []authDefault

func (l *AuthLoginCommon) setDefaultFields(d *schema.ResourceData, defaults authDefaults) error {
func (l *AuthLoginCommon) setDefaultFields(d *schema.ResourceData, defaults authDefaults, params map[string]interface{}) error {
for _, f := range defaults {
if _, ok := l.getOk(d, f.field); !ok {

// if field is unset in the config, check env
defaultValue := os.Getenv(f.envVar)
if defaultValue == "" {
defaultValue = f.defaultVal
for _, envVar := range f.envVars {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This allows us to pass multiple options for the default environment variable and take the first value we find. Similar to what MultiEnvDefaultFunc is achieving here

defaultValue := os.Getenv(envVar)
if defaultValue == "" {
defaultValue = f.defaultVal
}
params[f.field] = defaultValue
// found a value, no need to check other options
break
}
l.params[f.field] = defaultValue
}
}

return nil
}

func (l *AuthLoginCommon) checkRequiredFields(d *schema.ResourceData, required ...string) error {
func (l *AuthLoginCommon) checkRequiredFields(d *schema.ResourceData, params map[string]interface{}, required ...string) error {
var missing []string
for _, f := range required {
if data, ok := l.getOk(d, f); !ok {
// if the field was unset in the config
if l.params[f] == data {
if params[f] == data {
missing = append(missing, f)
}
}
Expand Down
Loading