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

Adds DurationType and uses it for assume role duration #193

Merged
merged 1 commit into from
Sep 29, 2021
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 docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ provider "awscc" {

Optional:

- **duration_seconds** (Number) Number of seconds to restrict the assume role session duration. You can provide a value from 900 seconds (15 minutes) up to the maximum session duration setting for the role.
- **duration** (String) Duration of the assume role session. You can provide a value from 15 minutes up to the maximum session duration setting for the role. A sequence of numbers with a unit suffix, "h" for hour, "m" for minute, and "s" for second. Default value is 1h0m0s
- **external_id** (String) External identifier to use when assuming the role.
- **policy** (String) IAM policy in JSON format to use as a session policy. The effective permissions for the session will be the intersection between this polcy and the role's policies.
- **policy_arns** (List of String) Amazon Resource Names (ARNs) of IAM Policies to use as managed session policies. The effective permissions for the session will be the intersection between these polcy and the role's policies.
Expand Down
35 changes: 19 additions & 16 deletions internal/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"log"
"os"
"strings"
"time"

"github.com/aws/aws-sdk-go-v2/service/cloudcontrol"
"github.com/aws/smithy-go/logging"
Expand All @@ -16,11 +17,13 @@ import (
"github.com/hashicorp/terraform-plugin-framework/types"
tflog "github.com/hashicorp/terraform-plugin-log"
"github.com/hashicorp/terraform-provider-awscc/internal/registry"
cctypes "github.com/hashicorp/terraform-provider-awscc/internal/types"
"github.com/hashicorp/terraform-provider-awscc/internal/validate"
)

const (
defaultMaxRetries = 25
defaultMaxRetries = 25
defaultAssumeRoleDuration = 1 * time.Hour
)

func New() tfsdk.Provider {
Expand Down Expand Up @@ -127,10 +130,12 @@ func (p *AwsCloudControlApiProvider) GetSchema(ctx context.Context) (tfsdk.Schem
},
},

"duration_seconds": {
Type: types.Int64Type,
Description: "Number of seconds to restrict the assume role session duration. You can provide a value from 900 seconds (15 minutes) up to the maximum session duration setting for the role.",
Optional: true,
"duration": {
Type: cctypes.DurationType,
Description: "Duration of the assume role session. You can provide a value from 15 minutes up to the maximum session duration setting for the role. " +
cctypes.DurationType.Description() +
fmt.Sprintf(" Default value is %s", defaultAssumeRoleDuration),
Optional: true,
},

"external_id": {
Expand Down Expand Up @@ -204,20 +209,20 @@ type providerData struct {
}

type assumeRoleData struct {
RoleARN types.String `tfsdk:"role_arn"`
DurationSeconds types.Int64 `tfsdk:"duration_seconds"`
ExternalID types.String `tfsdk:"external_id"`
Policy types.String `tfsdk:"policy"`
PolicyARNs types.List `tfsdk:"policy_arns"`
SessionName types.String `tfsdk:"session_name"`
Tags types.Map `tfsdk:"tags"`
TransitiveTagKeys types.Set `tfsdk:"transitive_tag_keys"`
RoleARN types.String `tfsdk:"role_arn"`
Duration cctypes.Duration `tfsdk:"duration"`
ExternalID types.String `tfsdk:"external_id"`
Policy types.String `tfsdk:"policy"`
PolicyARNs types.List `tfsdk:"policy_arns"`
SessionName types.String `tfsdk:"session_name"`
Tags types.Map `tfsdk:"tags"`
TransitiveTagKeys types.Set `tfsdk:"transitive_tag_keys"`
}

func (a assumeRoleData) Config() *awsbase.AssumeRole {
assumeRole := &awsbase.AssumeRole{
RoleARN: a.RoleARN.Value,
DurationSeconds: int(a.DurationSeconds.Value),
DurationSeconds: int(a.Duration.Value.Seconds()),
ExternalID: a.ExternalID.Value,
Policy: a.Policy.Value,
SessionName: a.SessionName.Value,
Expand Down Expand Up @@ -247,8 +252,6 @@ func (a assumeRoleData) Config() *awsbase.AssumeRole {
return assumeRole
}

// func intValueOrNull(i types.Int64)

func (p *AwsCloudControlApiProvider) Configure(ctx context.Context, request tfsdk.ConfigureProviderRequest, response *tfsdk.ConfigureProviderResponse) {
var config providerData

Expand Down
154 changes: 154 additions & 0 deletions internal/types/duration.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
package types

import (
"context"
"fmt"
"time"

"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-go/tftypes"
)

type durationType uint8

const (
DurationType durationType = iota
)

var (
_ attr.TypeWithValidate = DurationType
)

func (d durationType) TerraformType(_ context.Context) tftypes.Type {
return tftypes.String
}

func (d durationType) ValueFromTerraform(_ context.Context, in tftypes.Value) (attr.Value, error) {
if !in.IsKnown() {
return Duration{Unknown: true}, nil
}
if in.IsNull() {
return Duration{Null: true}, nil
}
var s string
err := in.As(&s)
if err != nil {
return nil, err
}
dur, err := time.ParseDuration(s)
if err != nil {
return nil, err
}
return Duration{Value: dur}, nil
}

// Equal returns true if `o` is also a DurationType.
func (d durationType) Equal(o attr.Type) bool {
_, ok := o.(durationType)
return ok
}

// ApplyTerraform5AttributePathStep applies the given AttributePathStep to the
// type.
func (d durationType) ApplyTerraform5AttributePathStep(step tftypes.AttributePathStep) (interface{}, error) {
return nil, fmt.Errorf("cannot apply AttributePathStep %T to %s", step, d.String())
}

// String returns a human-friendly description of the DurationType.
func (d durationType) String() string {
return "types.DurationType"
}

// Validate implements type validation.
func (d durationType) Validate(ctx context.Context, in tftypes.Value, path *tftypes.AttributePath) diag.Diagnostics {
var diags diag.Diagnostics

if !in.Type().Is(tftypes.String) {
diags.AddAttributeError(
path,
"Duration Type Validation Error",
"An unexpected error was encountered trying to validate an attribute value. This is always an error in the provider. Please report the following to the provider developer:\n\n"+
fmt.Sprintf("Expected String value, received %T with value: %v", in, in),
)
return diags
}

if !in.IsKnown() || in.IsNull() {
return diags
}

var value string
err := in.As(&value)
if err != nil {
diags.AddAttributeError(
path,
"Duration Type Validation Error",
"An unexpected error was encountered trying to validate an attribute value. This is always an error in the provider. Please report the following to the provider developer:\n\n"+
fmt.Sprintf("Cannot convert value to time.Duration: %s", err),
)
return diags
}

_, err = time.ParseDuration(value)
if err != nil {
diags.AddAttributeError(
path,
"Duration Type Validation Error",
fmt.Sprintf("Value %q cannot be parsed as a Duration.", value),
)
return diags
}

return diags
}

func (d durationType) Description() string {
return `A sequence of numbers with a unit suffix, "h" for hour, "m" for minute, and "s" for second.`
}

type Duration struct {
// Unknown will be true if the value is not yet known.
Unknown bool

// Null will be true if the value was not set, or was explicitly set to
// null.
Null bool

// Value contains the set value, as long as Unknown and Null are both
// false.
Value time.Duration
}

// Type returns a DurationType.
func (d Duration) Type(_ context.Context) attr.Type {
return DurationType
}

// ToTerraformValue returns the data contained in the *String as a string. If
// Unknown is true, it returns a tftypes.UnknownValue. If Null is true, it
// returns nil.
func (d Duration) ToTerraformValue(_ context.Context) (interface{}, error) {
if d.Null {
return nil, nil
}
if d.Unknown {
return tftypes.UnknownValue, nil
}
return d.Value, nil
}

// Equal returns true if `other` is a *Duration and has the same value as `d`.
func (d Duration) Equal(other attr.Value) bool {
o, ok := other.(Duration)
if !ok {
return false
}
if d.Unknown != o.Unknown {
return false
}
if d.Null != o.Null {
return false
}
return d.Value == o.Value
}
104 changes: 104 additions & 0 deletions internal/types/duration_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package types

import (
"context"
"testing"
"time"

"github.com/google/go-cmp/cmp"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-go/tftypes"
"github.com/hashicorp/terraform-provider-awscc/internal/tfresource"
)

func TestDurationTypeValueFromTerraform(t *testing.T) {
t.Parallel()

tests := map[string]struct {
val tftypes.Value
expected attr.Value
expectError bool
}{
"null value": {
val: tftypes.NewValue(tftypes.String, nil),
expected: Duration{Null: true},
},
"unknown value": {
val: tftypes.NewValue(tftypes.String, tftypes.UnknownValue),
expected: Duration{Unknown: true},
},
"valid duration": {
val: tftypes.NewValue(tftypes.String, "2h"),
expected: Duration{Value: 2 * time.Hour},
},
"invalid duration": {
val: tftypes.NewValue(tftypes.String, "not ok"),
expectError: true,
},
}

for name, test := range tests {
name, test := name, test
t.Run(name, func(t *testing.T) {
ctx := context.TODO()
val, err := DurationType.ValueFromTerraform(ctx, test.val)

if err == nil && test.expectError {
t.Fatal("expected error, got no error")
}
if err != nil && !test.expectError {
t.Fatalf("got unexpected error: %s", err)
}

if diff := cmp.Diff(val, test.expected); diff != "" {
t.Errorf("unexpected diff (+wanted, -got): %s", diff)
}
})
}
}

func TestDurationTypeValidate(t *testing.T) {
t.Parallel()

type testCase struct {
val tftypes.Value
expectError bool
}
tests := map[string]testCase{
"not a string": {
val: tftypes.NewValue(tftypes.Bool, true),
expectError: true,
},
"unknown string": {
val: tftypes.NewValue(tftypes.String, tftypes.UnknownValue),
},
"null string": {
val: tftypes.NewValue(tftypes.String, nil),
},
"valid string": {
val: tftypes.NewValue(tftypes.String, "2h"),
},
"invalid string": {
val: tftypes.NewValue(tftypes.String, "not ok"),
expectError: true,
},
}

for name, test := range tests {
name, test := name, test
t.Run(name, func(t *testing.T) {
ctx := context.TODO()

attributePath := tftypes.NewAttributePath().WithAttributeName("test")
diags := DurationType.Validate(ctx, test.val, attributePath)

if !diags.HasError() && test.expectError {
t.Fatal("expected error, got no error")
}

if diags.HasError() && !test.expectError {
t.Fatalf("got unexpected error: %s", tfresource.DiagsError(diags))
}
})
}
}