From 45357f50042c1324b6fd10cf49c89f623c8b4356 Mon Sep 17 00:00:00 2001 From: Sebastian Rivera Date: Tue, 22 Mar 2022 17:56:45 -0400 Subject: [PATCH] Add TF_ORGANIZATION env var support TF_ORGANIZATION will serve as a fallback for configuring the organization in the `cloud` block. This is the first step to make it easier for users wanting to configure Terraform programmatically. --- internal/cloud/backend.go | 19 ++++-- internal/cloud/backend_test.go | 113 +++++++++++++++++++++++++++++++++ 2 files changed, 128 insertions(+), 4 deletions(-) diff --git a/internal/cloud/backend.go b/internal/cloud/backend.go index 797e11451a..65e1bcd15f 100644 --- a/internal/cloud/backend.go +++ b/internal/cloud/backend.go @@ -113,7 +113,7 @@ func (b *Cloud) ConfigSchema() *configschema.Block { }, "organization": { Type: cty.String, - Required: true, + Optional: true, Description: schemaDescriptionOrganization, }, "token": { @@ -152,8 +152,13 @@ func (b *Cloud) PrepareConfig(obj cty.Value) (cty.Value, tfdiags.Diagnostics) { return obj, diags } + // check if organization is specified in the config. if val := obj.GetAttr("organization"); val.IsNull() || val.AsString() == "" { - diags = diags.Append(invalidOrganizationConfigMissingValue) + // organization is specified in the config but is invalid, so + // we'll fallback on TF_ORGANIZATION + if val := os.Getenv("TF_ORGANIZATION"); val == "" { + diags = diags.Append(invalidOrganizationConfigMissingValue) + } } WorkspaceMapping := WorkspaceMapping{} @@ -342,8 +347,14 @@ func (b *Cloud) setConfigurationFields(obj cty.Value) tfdiags.Diagnostics { b.hostname = defaultHostname } - // Get the organization. - if val := obj.GetAttr("organization"); !val.IsNull() { + // We can have two options, setting the organization via the config + // or using TF_ORGANIZATION. Since PrepareConfig() validates that one of these + // values must exist, we'll initially set it to the env var and override it if + // specified in the configuration. + b.organization = os.Getenv("TF_ORGANIZATION") + + // Check if the organization is present and valid in the config. + if val := obj.GetAttr("organization"); !val.IsNull() && val.AsString() != "" { b.organization = val.AsString() } diff --git a/internal/cloud/backend_test.go b/internal/cloud/backend_test.go index 68e3c41677..8bc1518ee3 100644 --- a/internal/cloud/backend_test.go +++ b/internal/cloud/backend_test.go @@ -146,6 +146,119 @@ func TestCloud_PrepareConfig(t *testing.T) { } } +func TestCloud_PrepareConfigWithEnvVars(t *testing.T) { + cases := map[string]struct { + config cty.Value + vars map[string]string + expectedErr string + }{ + "with no organization": { + config: cty.ObjectVal(map[string]cty.Value{ + "organization": cty.NullVal(cty.String), + "workspaces": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("prod"), + "tags": cty.NullVal(cty.Set(cty.String)), + }), + }), + vars: map[string]string{ + "TF_ORGANIZATION": "example-org", + }, + }, + "with no organization attribute or env var": { + config: cty.ObjectVal(map[string]cty.Value{ + "organization": cty.NullVal(cty.String), + "workspaces": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("prod"), + "tags": cty.NullVal(cty.Set(cty.String)), + }), + }), + vars: map[string]string{}, + expectedErr: `Invalid organization value: The "organization" attribute value must not be empty.`, + }, + } + + for name, tc := range cases { + s := testServer(t) + b := New(testDisco(s)) + + for k, v := range tc.vars { + os.Setenv(k, v) + defer os.Unsetenv(k) + } + + _, valDiags := b.PrepareConfig(tc.config) + if valDiags.Err() != nil && tc.expectedErr != "" { + actualErr := valDiags.Err().Error() + if !strings.Contains(actualErr, tc.expectedErr) { + t.Fatalf("%s: unexpected validation result: %v", name, valDiags.Err()) + } + } + } +} + +func TestCloud_configWithEnvVars(t *testing.T) { + cases := map[string]struct { + config cty.Value + vars map[string]string + expectedOrganization string + }{ + "with no organization specified": { + config: cty.ObjectVal(map[string]cty.Value{ + "hostname": cty.NullVal(cty.String), + "token": cty.NullVal(cty.String), + "organization": cty.NullVal(cty.String), + "workspaces": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("prod"), + "tags": cty.NullVal(cty.Set(cty.String)), + }), + }), + vars: map[string]string{ + "TF_ORGANIZATION": "hashicorp", + }, + expectedOrganization: "hashicorp", + }, + "with both organization and env var specified": { + config: cty.ObjectVal(map[string]cty.Value{ + "hostname": cty.NullVal(cty.String), + "token": cty.NullVal(cty.String), + "organization": cty.StringVal("hashicorp"), + "workspaces": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("prod"), + "tags": cty.NullVal(cty.Set(cty.String)), + }), + }), + vars: map[string]string{ + "TF_ORGANIZATION": "we-should-not-see-this", + }, + expectedOrganization: "hashicorp", + }, + } + + for name, tc := range cases { + s := testServer(t) + b := New(testDisco(s)) + + for k, v := range tc.vars { + os.Setenv(k, v) + defer os.Unsetenv(k) + } + + _, valDiags := b.PrepareConfig(tc.config) + if valDiags.Err() != nil { + t.Fatalf("%s: unexpected validation result: %v", name, valDiags.Err()) + } + + diags := b.Configure(tc.config) + if diags.Err() != nil { + t.Fatalf("%s: unexpected configuration result: %v", name, valDiags.Err()) + } + + if tc.expectedOrganization != "" && tc.expectedOrganization != b.organization { + t.Fatalf("%s: organization not valid: %s, expected: %s", name, b.organization, tc.expectedOrganization) + } + } +} + func TestCloud_config(t *testing.T) { cases := map[string]struct { config cty.Value