diff --git a/provider/provider_yaml_test.go b/provider/provider_yaml_test.go index eaba19490ae..ac53193443d 100644 --- a/provider/provider_yaml_test.go +++ b/provider/provider_yaml_test.go @@ -753,6 +753,17 @@ func TestAccDefaultTagsWithImport(t *testing.T) { } } +func TestAssumeRoleSessionTags(t *testing.T) { + t.Parallel() + ptest := pulumiTest(t, filepath.Join("test-programs", "assume-role-session-tags"), opttest.SkipInstall()) + result := ptest.Up(t) + t.Logf("STDOUT: %v", result.StdOut) + t.Logf("STDERR: %v", result.StdErr) + + require.Contains(t, result.Outputs, "bucketArn") + assert.NotEmpty(t, result.Outputs["bucketArn"].Value.(string)) +} + // testTagsPulumiLifecycle tests the complete lifecycle of a pulumi program // Scenarios that this tests: // 1. `Up` with both provider `defaultTags`/`ignoreTags` and resource level `tags` diff --git a/provider/resources.go b/provider/resources.go index ef9c27ef7d0..25fd4c7ad18 100644 --- a/provider/resources.go +++ b/provider/resources.go @@ -579,6 +579,26 @@ func arrayValue(vars resource.PropertyMap, prop resource.PropertyKey, envs []str return vals } +func extractTags(vars resource.PropertyMap, prop resource.PropertyKey) map[string]string { + val, ok := vars[prop] + + if !ok || !val.IsObject() { + return nil + } + + tagProp := val.ObjectValue() + tags := make(map[string]string, len(tagProp)) + + for k, v := range tagProp { + if !v.IsString() { + continue + } + tags[string(k)] = v.StringValue() + } + + return tags +} + // returns a pointer so we can distinguish between a zero value and a missing value func durationFromConfig(vars resource.PropertyMap, prop resource.PropertyKey) (*time.Duration, error) { val, ok := vars[prop] @@ -630,6 +650,7 @@ func validateCredentials(vars resource.PropertyMap, c shim.ResourceConfig) error SessionName: stringValue(details.ObjectValue(), "sessionName", []string{}), SourceIdentity: stringValue(details.ObjectValue(), "sourceIdentity", []string{}), TransitiveTagKeys: arrayValue(details.ObjectValue(), "transitiveTagKeys", []string{}), + Tags: extractTags(details.ObjectValue(), "tags"), } duration, err := durationFromConfig(details.ObjectValue(), "durationSeconds") if err != nil { diff --git a/provider/resources_test.go b/provider/resources_test.go index 3cfea066214..2a76cc43203 100644 --- a/provider/resources_test.go +++ b/provider/resources_test.go @@ -66,3 +66,68 @@ func TestHasOptionalOrRequiredNamePropertyOptimized(t *testing.T) { } } } +func TestExtractTags(t *testing.T) { + tests := []struct { + name string + vars resource.PropertyMap + prop resource.PropertyKey + expected map[string]string + }{ + { + name: "valid tags", + vars: resource.PropertyMap{ + "tags": resource.NewObjectProperty(resource.PropertyMap{ + "Name": resource.NewStringProperty("example"), + "Env": resource.NewStringProperty("production"), + }), + }, + prop: "tags", + expected: map[string]string{ + "Name": "example", + "Env": "production", + }, + }, + { + name: "no tags", + vars: resource.PropertyMap{ + "tags": resource.NewObjectProperty(resource.PropertyMap{}), + }, + prop: "tags", + expected: map[string]string{}, + }, + { + name: "non-string tags", + vars: resource.PropertyMap{ + "tags": resource.NewObjectProperty(resource.PropertyMap{ + "Name": resource.NewStringProperty("example"), + "Count": resource.NewNumberProperty(1), + }), + }, + prop: "tags", + expected: map[string]string{ + "Name": "example", + }, + }, + { + name: "missing tags property", + vars: resource.PropertyMap{}, + prop: "tags", + expected: nil, + }, + { + name: "tags property is not an object", + vars: resource.PropertyMap{ + "tags": resource.NewStringProperty("not-an-object"), + }, + prop: "tags", + expected: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual := extractTags(tt.vars, tt.prop) + assert.Equal(t, tt.expected, actual) + }) + } +} diff --git a/provider/test-programs/assume-role-session-tags/Pulumi.yaml b/provider/test-programs/assume-role-session-tags/Pulumi.yaml new file mode 100644 index 00000000000..35638470080 --- /dev/null +++ b/provider/test-programs/assume-role-session-tags/Pulumi.yaml @@ -0,0 +1,75 @@ +name: assume-role-session-tags +runtime: yaml + +variables: + awsAccount: + fn::invoke: + function: aws:getCallerIdentity + currentRole: + fn::invoke: + function: aws:iam:getSessionContext + arguments: + arn: ${awsAccount.arn} + +resources: + bootstrapProvider: + type: pulumi:providers:aws + + iamRole: + type: aws:iam:Role + properties: + assumeRolePolicy: + fn::toJSON: + Version: "2012-10-17" + Statement: + - Action: + - "sts:AssumeRole" + - "sts:TagSession" + Effect: "Allow" + Principal: + AWS: ${currentRole.issuerArn} + Condition: + StringEquals: + "aws:RequestTag/Repository": + - "my-org/my-repo" + inlinePolicies: + - name: "inline-policy" + policy: + fn::toJSON: + Version: "2012-10-17" + Statement: + - Action: + - "s3:*" + Effect: "Allow" + Resource: "*" + options: + provider: ${bootstrapProvider} + + # IAM has a delay in propagating the new role, so we need to wait for it to be available + # AWS is aiming for P99 below 2s so 6s should be enough + wait6s: + type: time:Sleep + properties: + createDuration: 6s + + provider: + type: pulumi:providers:aws + properties: + assumeRole: + roleArn: ${iamRole.arn} + sessionName: "session-tagging-test" + tags: + Repository: "my-org/my-repo" + options: + dependsOn: + - ${wait6s} + + myTestBucket: + type: aws:s3:Bucket + options: + provider: ${provider} + +outputs: + bucketArn: ${myTestBucket.arn} + roleId: ${iamRole.id} + roleARN: ${iamRole.arn}