diff --git a/CHANGELOG.md b/CHANGELOG.md index f393201ab..bf5922418 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## Unreleased FEATURES: +* d/tfe_outputs: Add `nonsensitive_values` attribute to expose current non-sensitive outputs of a given workspace ([#711](https://github.com/hashicorp/terraform-provider-tfe/pull/711)) ## v0.40.0 (December 6, 2022) diff --git a/tfe/data_source_outputs.go b/tfe/data_source_outputs.go index ce89dcf63..5d2a4e96c 100644 --- a/tfe/data_source_outputs.go +++ b/tfe/data_source_outputs.go @@ -57,7 +57,7 @@ func (d dataSourceOutputs) ReadDataSource(ctx context.Context, req *tfprotov5.Re return resp, nil } - tftypesValues, stateTypes, err := parseStateOutput(remoteStateOutput) + tftypesValues, stateTypes, tftypesNonsensitiveValues, nonsensitiveStateTypes, err := parseStateOutput(remoteStateOutput) if err != nil { resp.Diagnostics = append(resp.Diagnostics, &tfprotov5.Diagnostic{ Severity: tfprotov5.DiagnosticSeverityError, @@ -70,23 +70,26 @@ func (d dataSourceOutputs) ReadDataSource(ctx context.Context, req *tfprotov5.Re id := fmt.Sprintf("%s-%s", orgName, wsName) state, err := tfprotov5.NewDynamicValue(tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ - "workspace": tftypes.String, - "organization": tftypes.String, - "values": tftypes.DynamicPseudoType, - "id": tftypes.String, + "workspace": tftypes.String, + "organization": tftypes.String, + "values": tftypes.DynamicPseudoType, + "nonsensitive_values": tftypes.DynamicPseudoType, + "id": tftypes.String, }, }, tftypes.NewValue(tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ - "workspace": tftypes.String, - "organization": tftypes.String, - "values": tftypes.Object{AttributeTypes: stateTypes}, - "id": tftypes.String, + "workspace": tftypes.String, + "organization": tftypes.String, + "values": tftypes.Object{AttributeTypes: stateTypes}, + "nonsensitive_values": tftypes.Object{AttributeTypes: nonsensitiveStateTypes}, + "id": tftypes.String, }, }, map[string]tftypes.Value{ - "workspace": tftypes.NewValue(tftypes.String, wsName), - "organization": tftypes.NewValue(tftypes.String, orgName), - "values": tftypes.NewValue(tftypes.Object{AttributeTypes: stateTypes}, tftypesValues), - "id": tftypes.NewValue(tftypes.String, id), + "workspace": tftypes.NewValue(tftypes.String, wsName), + "organization": tftypes.NewValue(tftypes.String, orgName), + "values": tftypes.NewValue(tftypes.Object{AttributeTypes: stateTypes}, tftypesValues), + "nonsensitive_values": tftypes.NewValue(tftypes.Object{AttributeTypes: nonsensitiveStateTypes}, tftypesNonsensitiveValues), + "id": tftypes.NewValue(tftypes.String, id), })) if err != nil { @@ -117,10 +120,11 @@ func (d dataSourceOutputs) readConfigValues(req *tfprotov5.ReadDataSourceRequest config := req.Config val, err := config.Unmarshal(tftypes.Object{ AttributeTypes: map[string]tftypes.Type{ - "workspace": tftypes.String, - "organization": tftypes.String, - "values": tftypes.DynamicPseudoType, - "id": tftypes.String, + "workspace": tftypes.String, + "organization": tftypes.String, + "values": tftypes.DynamicPseudoType, + "nonsensitive_values": tftypes.DynamicPseudoType, + "id": tftypes.String, }}) if err != nil { return orgName, wsName, fmt.Errorf("Error unmarshalling config: %w", err) @@ -154,7 +158,8 @@ type stateData struct { } type outputData struct { - Value cty.Value + Value cty.Value + Sensitive cty.Value } func (d dataSourceOutputs) readStateOutput(ctx context.Context, tfeClient *tfe.Client, orgName, wsName string) (*stateData, error) { @@ -191,40 +196,49 @@ func (d dataSourceOutputs) readStateOutput(ctx context.Context, tfeClient *tfe.C return nil, fmt.Errorf("Could not unmarshal output value: %w", err) } sd.outputs[op.Name] = &outputData{ - Value: v.Value, + Value: v.Value, + Sensitive: cty.BoolVal(op.Sensitive), } } return sd, nil } -func parseStateOutput(stateOutput *stateData) (map[string]tftypes.Value, map[string]tftypes.Type, error) { +func parseStateOutput(stateOutput *stateData) (map[string]tftypes.Value, map[string]tftypes.Type, map[string]tftypes.Value, map[string]tftypes.Type, error) { tftypesValues := map[string]tftypes.Value{} stateTypes := map[string]tftypes.Type{} + tftypesNonsensitiveValues := map[string]tftypes.Value{} + nonsensitiveStateTypes := map[string]tftypes.Type{} + for name, output := range stateOutput.outputs { marshData, err := output.Value.Type().MarshalJSON() if err != nil { - return nil, nil, fmt.Errorf("Could not marshal output type: %w", err) + return nil, nil, nil, nil, fmt.Errorf("Could not marshal output type: %w", err) } tfType, err := tftypes.ParseJSONType(marshData) if err != nil { - return nil, nil, fmt.Errorf("Could not parse json type data: %w", err) + return nil, nil, nil, nil, fmt.Errorf("Could not parse json type data: %w", err) } mByte, err := ctyjson.Marshal(output.Value, output.Value.Type()) if err != nil { - return nil, nil, fmt.Errorf("Could not marshal output value and output type: %w", err) + return nil, nil, nil, nil, fmt.Errorf("Could not marshal output value and output type: %w", err) } tfRawState := tfprotov5.RawState{ JSON: mByte, } newVal, err := tfRawState.Unmarshal(tfType) if err != nil { - return nil, nil, fmt.Errorf("Could not unmarshal tftype into value: %w", err) + return nil, nil, nil, nil, fmt.Errorf("Could not unmarshal tftype into value: %w", err) + } + if output.Sensitive.False() { + tftypesNonsensitiveValues[name] = newVal + nonsensitiveStateTypes[name] = tfType } + tftypesValues[name] = newVal stateTypes[name] = tfType } - return tftypesValues, stateTypes, nil + return tftypesValues, stateTypes, tftypesNonsensitiveValues, nonsensitiveStateTypes, nil } diff --git a/tfe/data_source_outputs_test.go b/tfe/data_source_outputs_test.go index b0e716b8b..559d44304 100644 --- a/tfe/data_source_outputs_test.go +++ b/tfe/data_source_outputs_test.go @@ -59,6 +59,51 @@ func TestAccTFEOutputs(t *testing.T) { }) } +func TestAccTFEOutputs_ReadAllNonSensitiveValues(t *testing.T) { + skipIfUnitTest(t) + + client, err := getClientUsingEnv() + if err != nil { + t.Fatalf("error getting client %v", err) + } + + rInt := rand.New(rand.NewSource(time.Now().UnixNano())).Int() + fileName := "test-fixtures/state-versions/terraform.tfstate" + orgName, wsName, orgCleanup := createStateVersion(t, client, rInt, fileName) + t.Cleanup(orgCleanup) + + waitForOutputs(t, client, orgName, wsName) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV5ProviderFactories: testAccMuxedProviders, + Steps: []resource.TestStep{ + { + Config: testAccTFEOutputs_dataSourceReadNonsensitiveValues(rInt, orgName, wsName), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr( + "tfe_organization.foobar", "name", fmt.Sprintf("tst-%d", rInt)), + resource.TestCheckResourceAttr( + "tfe_workspace.foobar", "name", fmt.Sprintf("workspace-test-%d", rInt)), + resource.TestCheckResourceAttr( + "data.tfe_outputs.foobar", "organization", orgName), + resource.TestCheckResourceAttr( + "data.tfe_outputs.foobar", "workspace", wsName), + // nonsensitive_values does not set sensitive values + resource.TestCheckNoResourceAttr("data.tfe_outputs.foobar", "nonsensitive_values.test_output_string"), + // These outputs rely on the values in test-fixtures/state-versions/terraform.tfstate + testCheckOutputState("test_output_list_string", &terraform.OutputState{Value: []interface{}{"us-west-1a"}}), + testCheckOutputState("test_output_tuple_number", &terraform.OutputState{Value: []interface{}{"1", "2"}}), + testCheckOutputState("test_output_tuple_string", &terraform.OutputState{Value: []interface{}{"one", "two"}}), + testCheckOutputState("test_output_object", &terraform.OutputState{Value: map[string]interface{}{"foo": "bar"}}), + testCheckOutputState("test_output_number", &terraform.OutputState{Value: "5"}), + testCheckOutputState("test_output_bool", &terraform.OutputState{Value: "true"}), + ), + }, + }, + }) +} + func TestAccTFEOutputs_emptyOutputs(t *testing.T) { skipIfUnitTest(t) @@ -249,6 +294,46 @@ output "test_output_bool" { `, rInt, rInt, org, workspace) } +func testAccTFEOutputs_dataSourceReadNonsensitiveValues(rInt int, org, workspace string) string { + return fmt.Sprintf(` +resource "tfe_organization" "foobar" { + name = "tst-%d" + email = "admin@company.com" +} + +resource "tfe_workspace" "foobar" { + name = "workspace-test-%d" + organization = tfe_organization.foobar.name +} + +data "tfe_outputs" "foobar" { + organization = "%s" + workspace = "%s" +} + +// All of these values reference the outputs in the file +// 'test-fixtures/state-versions/terraform.tfstate except the sensitive attr test_output_string +output "test_output_list_string" { + value = data.tfe_outputs.foobar.nonsensitive_values.test_output_list_string +} +output "test_output_tuple_number" { + value = data.tfe_outputs.foobar.nonsensitive_values.test_output_tuple_number +} +output "test_output_tuple_string" { + value = data.tfe_outputs.foobar.nonsensitive_values.test_output_tuple_string +} +output "test_output_object" { + value = data.tfe_outputs.foobar.nonsensitive_values.test_output_object +} +output "test_output_number" { + value = data.tfe_outputs.foobar.nonsensitive_values.test_output_number +} +output "test_output_bool" { + value = data.tfe_outputs.foobar.nonsensitive_values.test_output_bool +} +`, rInt, rInt, org, workspace) +} + func testAccTFEOutputs_dataSource_emptyOutputs(rInt int, org, workspace string) string { return fmt.Sprintf(` resource "tfe_organization" "foobar" { diff --git a/tfe/plugin_provider.go b/tfe/plugin_provider.go index 71b1c9cfa..0dba5f4a2 100644 --- a/tfe/plugin_provider.go +++ b/tfe/plugin_provider.go @@ -211,6 +211,12 @@ func PluginProviderServer() tfprotov5.ProviderServer { Computed: true, Sensitive: true, }, + { + Name: "nonsensitive_values", + Type: tftypes.DynamicPseudoType, + Computed: true, + Sensitive: false, + }, }, }, }, diff --git a/website/docs/d/outputs.html.markdown b/website/docs/d/outputs.html.markdown index 654b79cc1..ec86ef223 100644 --- a/website/docs/d/outputs.html.markdown +++ b/website/docs/d/outputs.html.markdown @@ -49,3 +49,4 @@ The following arguments are supported: The following attributes are exported: * `values` - The current output values for the specified workspace. +* `nonsensitive_values` - The current non-sensitive output values for the specified workspace.