Skip to content

Commit

Permalink
add unit tests
Browse files Browse the repository at this point in the history
  • Loading branch information
liamcervante committed Aug 12, 2024
1 parent 97f64d9 commit 8883d81
Show file tree
Hide file tree
Showing 7 changed files with 346 additions and 73 deletions.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ require (
github.com/go-openapi/validate v0.24.0 // indirect
github.com/go-test/deep v1.1.0 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/go-cmp v0.6.0
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-checkpoint v0.5.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
Expand Down
99 changes: 54 additions & 45 deletions internal/clients/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,57 +121,14 @@ type ClientConfig struct {

// NewClient creates a new Client that is capable of making HCP requests
func NewClient(config ClientConfig) (*Client, error) {
// hasWorkloadIdentityToken is true if the config specified a direct token.
hasWorkloadIdentityToken := config.WorkloadIdentityToken != ""
// hasWorkloadIdentityTokenFile is true if the config specified a path to a
// file that contains the token.
hasWorkloadIdentityTokenFile := config.WorkloadIdentityTokenFile != ""
// hasWorkloadIdentityResource is true if the config specified a resource
// name to authenticate against.
hasWorkloadIdentityResource := config.WorkloadIdentityResourceName != ""

// Overall, we consider workload identity authentication to be enabled if we
// have a token from either source (direct or within a file) and a resource
// name.
hasWorkloadIdentity := (hasWorkloadIdentityToken || hasWorkloadIdentityTokenFile) && hasWorkloadIdentityResource

// Build the HCP Config options
opts := []hcpConfig.HCPConfigOption{hcpConfig.FromEnv()}
if config.ClientID != "" && config.ClientSecret != "" {
opts = append(opts, hcpConfig.WithClientCredentials(config.ClientID, config.ClientSecret))
} else if config.CredentialFile != "" {
opts = append(opts, hcpConfig.WithCredentialFilePath(config.CredentialFile))
} else if hasWorkloadIdentity {
switch {
case hasWorkloadIdentityToken:
// The direct token takes priority over the file path in case both
// are set, so we'll check that first.
cf := &auth.CredentialFile{
Scheme: auth.CredentialFileSchemeWorkload,
Workload: &workload.IdentityProviderConfig{
ProviderResourceName: config.WorkloadIdentityResourceName,
Token: &workload.CredentialTokenSource{
Token: config.WorkloadIdentityToken,
},
},
}
opts = append(opts, hcpConfig.WithCredentialFile(cf))
default:
// If we don't have the token directly, fall back to checking the
// file. We checked earlier that at least one of the two options
// were set, so if the token wasn't set the file information must
// be present.
cf := &auth.CredentialFile{
Scheme: auth.CredentialFileSchemeWorkload,
Workload: &workload.IdentityProviderConfig{
ProviderResourceName: config.WorkloadIdentityResourceName,
File: &workload.FileCredentialSource{
Path: config.WorkloadIdentityTokenFile,
},
},
}
opts = append(opts, hcpConfig.WithCredentialFile(cf))
}
} else if cf := loadCredentialFile(config); cf != nil {
opts = append(opts, hcpConfig.WithCredentialFile(cf))
}

// Create the HCP Config
Expand Down Expand Up @@ -224,6 +181,58 @@ func NewClient(config ClientConfig) (*Client, error) {
return client, nil
}

// loadCredentialFile loads the credential file from the given config. If the
// config does not specify workload identity authentication, this function
// returns nil.
func loadCredentialFile(config ClientConfig) *auth.CredentialFile {
// hasWorkloadIdentityToken is true if the config specified a direct token.
hasWorkloadIdentityToken := config.WorkloadIdentityToken != ""
// hasWorkloadIdentityTokenFile is true if the config specified a path to a
// file that contains the token.
hasWorkloadIdentityTokenFile := config.WorkloadIdentityTokenFile != ""
// hasWorkloadIdentityResource is true if the config specified a resource
// name to authenticate against.
hasWorkloadIdentityResource := config.WorkloadIdentityResourceName != ""

// Overall, we consider workload identity authentication to be enabled if we
// have a token from either source (direct or within a file) and a resource
// name.
hasWorkloadIdentity := (hasWorkloadIdentityToken || hasWorkloadIdentityTokenFile) && hasWorkloadIdentityResource

if !hasWorkloadIdentity {
return nil
}

switch {
case hasWorkloadIdentityToken:
// The direct token takes priority over the file path in case both
// are set, so we'll check that first.
return &auth.CredentialFile{
Scheme: auth.CredentialFileSchemeWorkload,
Workload: &workload.IdentityProviderConfig{
ProviderResourceName: config.WorkloadIdentityResourceName,
Token: &workload.CredentialTokenSource{
Token: config.WorkloadIdentityToken,
},
},
}
default:
// If we don't have the token directly, fall back to checking the
// file. We checked earlier that at least one of the two options
// were set, so if the token wasn't set the file information must
// be present.
return &auth.CredentialFile{
Scheme: auth.CredentialFileSchemeWorkload,
Workload: &workload.IdentityProviderConfig{
ProviderResourceName: config.WorkloadIdentityResourceName,
File: &workload.FileCredentialSource{
Path: config.WorkloadIdentityTokenFile,
},
},
}
}
}

type providerMeta struct {
ModuleName string `cty:"module_name"`
}
Expand Down
81 changes: 81 additions & 0 deletions internal/clients/client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package clients

import (
"testing"

"github.com/google/go-cmp/cmp"
"github.com/hashicorp/hcp-sdk-go/auth"
"github.com/hashicorp/hcp-sdk-go/auth/workload"
)

func Test_loadCredentialFile(t *testing.T) {
tcs := map[string]struct {
config ClientConfig
want *auth.CredentialFile
}{
"empty config": {
config: ClientConfig{},
},
"ignores with only resource": {
config: ClientConfig{
WorkloadIdentityResourceName: "my_resource",
},
},
"loads with file": {
config: ClientConfig{
WorkloadIdentityResourceName: "my_resource",
WorkloadIdentityTokenFile: "/path/to/token/file",
},
want: &auth.CredentialFile{
Scheme: auth.CredentialFileSchemeWorkload,
Workload: &workload.IdentityProviderConfig{
ProviderResourceName: "my_resource",
File: &workload.FileCredentialSource{
Path: "/path/to/token/file",
},
},
},
},
"loads with token": {
config: ClientConfig{
WorkloadIdentityResourceName: "my_resource",
WorkloadIdentityToken: "my_token",
},
want: &auth.CredentialFile{
Scheme: auth.CredentialFileSchemeWorkload,
Workload: &workload.IdentityProviderConfig{
ProviderResourceName: "my_resource",
Token: &workload.CredentialTokenSource{
Token: "my_token",
},
},
},
},
"defaults to token": {
config: ClientConfig{
WorkloadIdentityResourceName: "my_resource",
WorkloadIdentityTokenFile: "/path/to/token/file",
WorkloadIdentityToken: "my_token",
},
want: &auth.CredentialFile{
Scheme: auth.CredentialFileSchemeWorkload,
Workload: &workload.IdentityProviderConfig{
ProviderResourceName: "my_resource",
Token: &workload.CredentialTokenSource{
Token: "my_token",
},
},
},
},
}

for name, tc := range tcs {
t.Run(name, func(t *testing.T) {
got := loadCredentialFile(tc.config)
if cmp.Diff(tc.want, got) != "" {
t.Errorf("mismatch (-want +got): %s", cmp.Diff(tc.want, got))
}
})
}

}
27 changes: 19 additions & 8 deletions internal/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/hashicorp/terraform-plugin-framework-validators/listvalidator"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/provider"
"github.com/hashicorp/terraform-plugin-framework/provider/schema"
Expand Down Expand Up @@ -235,14 +236,10 @@ func (p *ProviderFramework) Configure(ctx context.Context, req provider.Configur
return
}

clientConfig.WorkloadIdentityTokenFile = elements[0].TokenFile.ValueString()
clientConfig.WorkloadIdentityToken = elements[0].Token.ValueString()
clientConfig.WorkloadIdentityResourceName = elements[0].ResourceName.ValueString()

// This should have been validated by the schema, but we'll check it
// here just in case.
if clientConfig.WorkloadIdentityTokenFile == "" && clientConfig.WorkloadIdentityToken == "" {
resp.Diagnostics.AddError("invalid workload_identity", "at least one of `token_file` or `token` must be set")
var diags diag.Diagnostics
clientConfig, diags = readWorkloadIdentity(elements[0], clientConfig)
resp.Diagnostics.Append(diags...)
if diags.HasError() {
return
}
}
Expand Down Expand Up @@ -295,3 +292,17 @@ func (p *ProviderFramework) Configure(ctx context.Context, req provider.Configur
resp.DataSourceData = client
resp.ResourceData = client
}

func readWorkloadIdentity(model WorkloadIdentityFrameworkModel, clientConfig clients.ClientConfig) (clients.ClientConfig, diag.Diagnostics) {
clientConfig.WorkloadIdentityTokenFile = model.TokenFile.ValueString()
clientConfig.WorkloadIdentityToken = model.Token.ValueString()
clientConfig.WorkloadIdentityResourceName = model.ResourceName.ValueString()

// This should have been validated by the schema, but we'll check it
// here just in case.
var diags diag.Diagnostics
if clientConfig.WorkloadIdentityTokenFile == "" && clientConfig.WorkloadIdentityToken == "" {
diags.AddError("invalid workload_identity", "at least one of `token_file` or `token` must be set")
}
return clientConfig, diags
}
74 changes: 74 additions & 0 deletions internal/provider/provider_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package provider

import (
"testing"

"github.com/google/go-cmp/cmp"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"

"github.com/hashicorp/terraform-provider-hcp/internal/clients"
)

func Test_readWorkloadIdentity(t *testing.T) {
tcs := map[string]struct {
model WorkloadIdentityFrameworkModel
want clients.ClientConfig
wantDiags diag.Diagnostics
}{
"missing token and file": {
model: WorkloadIdentityFrameworkModel{
ResourceName: basetypes.NewStringValue("my_resource"),
},
wantDiags: diag.Diagnostics{
diag.NewErrorDiagnostic("invalid workload_identity", "at least one of `token_file` or `token` must be set"),
},
want: clients.ClientConfig{
WorkloadIdentityResourceName: "my_resource",
},
},
"token": {
model: WorkloadIdentityFrameworkModel{
ResourceName: basetypes.NewStringValue("my_resource"),
Token: basetypes.NewStringValue("my_token"),
},
want: clients.ClientConfig{
WorkloadIdentityResourceName: "my_resource",
WorkloadIdentityToken: "my_token",
},
},
"file": {
model: WorkloadIdentityFrameworkModel{
ResourceName: basetypes.NewStringValue("my_resource"),
TokenFile: basetypes.NewStringValue("/path/to/token/file"),
},
want: clients.ClientConfig{
WorkloadIdentityResourceName: "my_resource",
WorkloadIdentityTokenFile: "/path/to/token/file",
},
},
"both": {
model: WorkloadIdentityFrameworkModel{
ResourceName: basetypes.NewStringValue("my_resource"),
Token: basetypes.NewStringValue("my_token"),
TokenFile: basetypes.NewStringValue("/path/to/token/file"),
},
want: clients.ClientConfig{
WorkloadIdentityResourceName: "my_resource",
WorkloadIdentityToken: "my_token",
WorkloadIdentityTokenFile: "/path/to/token/file",
},
},
}
for name, tc := range tcs {
t.Run(name, func(t *testing.T) {
got, gotDiags := readWorkloadIdentity(tc.model, clients.ClientConfig{})
if diff := cmp.Diff(tc.want, got); diff != "" {
t.Errorf("unexpected result (-want +got):\n%s", diff)
}
if diff := cmp.Diff(tc.wantDiags, gotDiags); diff != "" {
t.Errorf("unexpected diagnostics (-want +got):\n%s", diff)
}
})
}
}
50 changes: 31 additions & 19 deletions internal/providersdkv2/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,25 +145,11 @@ func configure(p *schema.Provider) func(context.Context, *schema.ResourceData) (
}

// Read the workload_identity configuration
if v, ok := d.GetOk("workload_identity"); ok && len(v.([]interface{})) == 1 && v.([]interface{})[0] != nil {
wi := v.([]interface{})[0].(map[string]interface{})
if tf, ok := wi["token_file"].(string); ok && tf != "" {
clientConfig.WorkloadIdentityTokenFile = tf
}
if t, ok := wi["token"].(string); ok && t != "" {
clientConfig.WorkloadIdentityToken = t
}
if rn, ok := wi["resource_name"].(string); ok && rn != "" {
clientConfig.WorkloadIdentityResourceName = rn
}

if clientConfig.WorkloadIdentityTokenFile == "" && clientConfig.WorkloadIdentityToken == "" {
diags = append(diags, diag.Diagnostic{
Severity: diag.Error,
Summary: "invalid workload_identity",
Detail: "at least of one of `token_file` or `token` must be set",
AttributePath: cty.GetAttrPath("workload_identity"),
})
if d, ok := d.GetOk("workload_identity"); ok {
var moreDiags diag.Diagnostics
clientConfig, moreDiags = readWorkloadIdentity(d, clientConfig)
diags = append(diags, moreDiags...)
if moreDiags.HasError() {
return nil, diags
}
}
Expand Down Expand Up @@ -216,6 +202,32 @@ func configure(p *schema.Provider) func(context.Context, *schema.ResourceData) (
}
}

func readWorkloadIdentity(v interface{}, clientConfig clients.ClientConfig) (clients.ClientConfig, diag.Diagnostics) {
var diags diag.Diagnostics
if len(v.([]interface{})) == 1 && v.([]interface{})[0] != nil {
wi := v.([]interface{})[0].(map[string]interface{})
if tf, ok := wi["token_file"].(string); ok && tf != "" {
clientConfig.WorkloadIdentityTokenFile = tf
}
if t, ok := wi["token"].(string); ok && t != "" {
clientConfig.WorkloadIdentityToken = t
}
if rn, ok := wi["resource_name"].(string); ok && rn != "" {
clientConfig.WorkloadIdentityResourceName = rn
}

if clientConfig.WorkloadIdentityTokenFile == "" && clientConfig.WorkloadIdentityToken == "" {
diags = append(diags, diag.Diagnostic{
Severity: diag.Error,
Summary: "invalid workload_identity",
Detail: "at least one of `token_file` or `token` must be set",
AttributePath: cty.GetAttrPath("workload_identity"),
})
}
}
return clientConfig, diags
}

// getProjectFromCredentials uses the configured client credentials to
// fetch the associated organization and returns that organization's
// single project.
Expand Down
Loading

0 comments on commit 8883d81

Please sign in to comment.