mirror of
https://github.com/opentofu/opentofu.git
synced 2025-02-25 18:45:20 -06:00
Fix workspaces configuration using environment varariable TF_WORKSPACE (#867)
Signed-off-by: Dmitry Kisler <admin@dkisler.com>
This commit is contained in:
parent
7e1be68264
commit
c5170df91a
@ -52,6 +52,7 @@ BUG FIXES:
|
||||
* Fixed regression where `tofu` was not ignoring GPG signing key expiration by default ([822](https://github.com/opentofu/opentofu/issues/822))
|
||||
- Added experimental env variable to enforce checking of GPG signing key expiration `OPENTOFU_ENFORCE_GPG_EXPIRATION=true`
|
||||
- This is planned to default to `true` in a future release.
|
||||
* cloud: fixed a bug related to `workspaces` configuration not correctly recognizing TF_WORKSPACE environment variable ([#814](https://github.com/opentofu/opentofu/issues/814))
|
||||
|
||||
S3 BACKEND:
|
||||
|
||||
|
@ -182,20 +182,11 @@ func (b *Cloud) PrepareConfig(obj cty.Value) (cty.Value, tfdiags.Diagnostics) {
|
||||
}
|
||||
}
|
||||
|
||||
WorkspaceMapping := WorkspaceMapping{}
|
||||
// Initially set the workspace name via env var
|
||||
WorkspaceMapping.Name = os.Getenv("TF_WORKSPACE")
|
||||
// Consider preserving the state in the receiver because it's instantiated twice, see b.setConfigurationFields
|
||||
WorkspaceMapping := newWorkspacesMappingFromFields(obj)
|
||||
|
||||
if workspaces := obj.GetAttr("workspaces"); !workspaces.IsNull() {
|
||||
if val := workspaces.GetAttr("name"); !val.IsNull() {
|
||||
WorkspaceMapping.Name = val.AsString()
|
||||
}
|
||||
if val := workspaces.GetAttr("tags"); !val.IsNull() {
|
||||
err := gocty.FromCtyValue(val, &WorkspaceMapping.Tags)
|
||||
if err != nil {
|
||||
log.Panicf("An unexpected error occurred: %s", err)
|
||||
}
|
||||
}
|
||||
if diag := reconcileWorkspaceMappingEnvVars(&WorkspaceMapping); diag != nil {
|
||||
diags = diags.Append(diag)
|
||||
}
|
||||
|
||||
switch WorkspaceMapping.Strategy() {
|
||||
@ -210,6 +201,35 @@ func (b *Cloud) PrepareConfig(obj cty.Value) (cty.Value, tfdiags.Diagnostics) {
|
||||
return obj, diags
|
||||
}
|
||||
|
||||
func newWorkspacesMappingFromFields(obj cty.Value) WorkspaceMapping {
|
||||
mapping := WorkspaceMapping{}
|
||||
|
||||
config := obj.GetAttr("workspaces")
|
||||
if config.IsNull() {
|
||||
return mapping
|
||||
}
|
||||
|
||||
workspaceName := config.GetAttr("name")
|
||||
if !workspaceName.IsNull() {
|
||||
mapping.Name = workspaceName.AsString()
|
||||
}
|
||||
|
||||
workspaceTags := config.GetAttr("tags")
|
||||
if !workspaceTags.IsNull() {
|
||||
err := gocty.FromCtyValue(workspaceTags, &mapping.Tags)
|
||||
if err != nil {
|
||||
log.Panicf("An unexpected error occurred: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
projectName := config.GetAttr("project")
|
||||
if !projectName.IsNull() && projectName.AsString() != "" {
|
||||
mapping.Project = projectName.AsString()
|
||||
}
|
||||
|
||||
return mapping
|
||||
}
|
||||
|
||||
func (b *Cloud) ServiceDiscoveryAliases() ([]backend.HostAlias, error) {
|
||||
aliasHostname, err := svchost.ForComparison(genericHostname)
|
||||
if err != nil {
|
||||
@ -424,34 +444,12 @@ func (b *Cloud) setConfigurationFields(obj cty.Value) tfdiags.Diagnostics {
|
||||
b.organization = val.AsString()
|
||||
}
|
||||
|
||||
// Initially set the project via env var
|
||||
b.WorkspaceMapping.Project = os.Getenv("TF_CLOUD_PROJECT")
|
||||
// Initially, set workspaces from the configuration
|
||||
b.WorkspaceMapping = newWorkspacesMappingFromFields(obj)
|
||||
|
||||
// Initially set the workspace name via env var
|
||||
b.WorkspaceMapping.Name = os.Getenv("TF_WORKSPACE")
|
||||
|
||||
// Get the workspaces configuration block and retrieve the
|
||||
// default workspace name.
|
||||
if workspaces := obj.GetAttr("workspaces"); !workspaces.IsNull() {
|
||||
|
||||
// Check if the project is present and valid in the config.
|
||||
if val := workspaces.GetAttr("project"); !val.IsNull() && val.AsString() != "" {
|
||||
b.WorkspaceMapping.Project = val.AsString()
|
||||
}
|
||||
|
||||
// PrepareConfig checks that you cannot set both of these.
|
||||
if val := workspaces.GetAttr("name"); !val.IsNull() {
|
||||
b.WorkspaceMapping.Name = val.AsString()
|
||||
}
|
||||
if val := workspaces.GetAttr("tags"); !val.IsNull() {
|
||||
var tags []string
|
||||
err := gocty.FromCtyValue(val, &tags)
|
||||
if err != nil {
|
||||
log.Panicf("An unexpected error occurred: %s", err)
|
||||
}
|
||||
|
||||
b.WorkspaceMapping.Tags = tags
|
||||
}
|
||||
// Overwrite workspaces config from env variable
|
||||
if diag := reconcileWorkspaceMappingEnvVars(&b.WorkspaceMapping); diag != nil {
|
||||
return diags.Append(diag)
|
||||
}
|
||||
|
||||
// Determine if we are forced to use the local backend.
|
||||
@ -460,6 +458,32 @@ func (b *Cloud) setConfigurationFields(obj cty.Value) tfdiags.Diagnostics {
|
||||
return diags
|
||||
}
|
||||
|
||||
func reconcileWorkspaceMappingEnvVars(w *WorkspaceMapping) tfdiags.Diagnostic {
|
||||
// See: https://github.com/opentofu/opentofu/issues/814
|
||||
if v := os.Getenv("TF_WORKSPACE"); v != "" && w.Name == "" {
|
||||
if len(w.Tags) > 0 && !workspaceInTags(w.Tags, v) {
|
||||
return invalidWorkspaceConfigMisconfigurationEnvVar
|
||||
}
|
||||
w.Name = v
|
||||
w.Tags = nil
|
||||
}
|
||||
|
||||
if v := os.Getenv("TF_CLOUD_PROJECT"); v != "" && w.Project == "" {
|
||||
w.Project = v
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func workspaceInTags(tags []string, workspace string) bool {
|
||||
for _, tag := range tags {
|
||||
if tag == workspace {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// discover the TFC/E API service URL and version constraints.
|
||||
func (b *Cloud) discover() (*url.URL, error) {
|
||||
hostname, err := svchost.ForComparison(b.hostname)
|
||||
|
@ -172,17 +172,19 @@ func TestCloud_PrepareConfig(t *testing.T) {
|
||||
}
|
||||
|
||||
for name, tc := range cases {
|
||||
s := testServer(t)
|
||||
b := New(testDisco(s))
|
||||
t.Run(name, func(t *testing.T) {
|
||||
s := testServer(t)
|
||||
b := New(testDisco(s))
|
||||
|
||||
// Validate
|
||||
_, 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())
|
||||
// Validate
|
||||
_, 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())
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -260,7 +262,7 @@ func TestCloud_PrepareConfigWithEnvVars(t *testing.T) {
|
||||
"TF_CLOUD_PROJECT": "example-project",
|
||||
},
|
||||
},
|
||||
"with project env var ovewrite config value": {
|
||||
"with project env var overwrite config value": {
|
||||
config: cty.ObjectVal(map[string]cty.Value{
|
||||
"organization": cty.StringVal("organization"),
|
||||
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
||||
@ -273,6 +275,51 @@ func TestCloud_PrepareConfigWithEnvVars(t *testing.T) {
|
||||
"TF_CLOUD_PROJECT": "example-project",
|
||||
},
|
||||
},
|
||||
"with workspace defined by tags overwritten by TF_WORKSPACE": {
|
||||
// see https://github.com/opentofu/opentofu/issues/814 for context
|
||||
config: cty.ObjectVal(map[string]cty.Value{
|
||||
"hostname": cty.StringVal("foo"),
|
||||
"organization": cty.StringVal("bar"),
|
||||
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
||||
"name": cty.NullVal(cty.String),
|
||||
"project": cty.NullVal(cty.String),
|
||||
"tags": cty.SetVal([]cty.Value{cty.StringVal("baz"), cty.StringVal("qux")}),
|
||||
}),
|
||||
}),
|
||||
vars: map[string]string{
|
||||
"TF_WORKSPACE": "qux",
|
||||
},
|
||||
},
|
||||
"with TF_WORKSPACE value outside of the tags set": {
|
||||
// see https://github.com/opentofu/opentofu/issues/814 for context
|
||||
config: cty.ObjectVal(map[string]cty.Value{
|
||||
"hostname": cty.StringVal("foo"),
|
||||
"organization": cty.StringVal("bar"),
|
||||
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
||||
"name": cty.NullVal(cty.String),
|
||||
"project": cty.NullVal(cty.String),
|
||||
"tags": cty.SetVal([]cty.Value{cty.StringVal("baz"), cty.StringVal("qux")}),
|
||||
}),
|
||||
}),
|
||||
vars: map[string]string{
|
||||
"TF_WORKSPACE": "quxx",
|
||||
},
|
||||
expectedErr: `Invalid workspaces configuration: The workspace defined using the environment variable "TF_WORKSPACE" does not belong to "tags".`,
|
||||
},
|
||||
"with workspace block w/o attributes, TF_WORKSPACE defined": {
|
||||
config: cty.ObjectVal(map[string]cty.Value{
|
||||
"hostname": cty.StringVal("foo"),
|
||||
"organization": cty.StringVal("bar"),
|
||||
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
||||
"name": cty.NullVal(cty.String),
|
||||
"tags": cty.NullVal(cty.Set(cty.String)),
|
||||
"project": cty.NullVal(cty.String),
|
||||
}),
|
||||
}),
|
||||
vars: map[string]string{
|
||||
"TF_WORKSPACE": "qux",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range cases {
|
||||
@ -290,323 +337,13 @@ func TestCloud_PrepareConfigWithEnvVars(t *testing.T) {
|
||||
})
|
||||
|
||||
_, 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 WithEnvVars(t *testing.T) {
|
||||
cases := map[string]struct {
|
||||
setup func(b *Cloud)
|
||||
config cty.Value
|
||||
vars map[string]string
|
||||
expectedOrganization string
|
||||
expectedHostname string
|
||||
expectedWorkspaceName string
|
||||
expectedProjectName string
|
||||
expectedErr 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)),
|
||||
"project": cty.NullVal(cty.String),
|
||||
}),
|
||||
}),
|
||||
vars: map[string]string{
|
||||
"TF_CLOUD_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)),
|
||||
"project": cty.NullVal(cty.String),
|
||||
}),
|
||||
}),
|
||||
vars: map[string]string{
|
||||
"TF_CLOUD_ORGANIZATION": "we-should-not-see-this",
|
||||
},
|
||||
expectedOrganization: "hashicorp",
|
||||
},
|
||||
"with no hostname 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)),
|
||||
"project": cty.NullVal(cty.String),
|
||||
}),
|
||||
}),
|
||||
vars: map[string]string{
|
||||
"TF_CLOUD_HOSTNAME": "private.hashicorp.engineering",
|
||||
},
|
||||
expectedHostname: "private.hashicorp.engineering",
|
||||
},
|
||||
"with hostname and env var specified": {
|
||||
config: cty.ObjectVal(map[string]cty.Value{
|
||||
"hostname": cty.StringVal("private.hashicorp.engineering"),
|
||||
"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)),
|
||||
"project": cty.NullVal(cty.String),
|
||||
}),
|
||||
}),
|
||||
vars: map[string]string{
|
||||
"TF_CLOUD_HOSTNAME": "mycool.tfe-host.io",
|
||||
},
|
||||
expectedHostname: "private.hashicorp.engineering",
|
||||
},
|
||||
"an invalid workspace env var": {
|
||||
config: cty.ObjectVal(map[string]cty.Value{
|
||||
"hostname": cty.NullVal(cty.String),
|
||||
"token": cty.NullVal(cty.String),
|
||||
"organization": cty.StringVal("hashicorp"),
|
||||
"workspaces": cty.NullVal(cty.Object(map[string]cty.Type{
|
||||
"name": cty.String,
|
||||
"tags": cty.Set(cty.String),
|
||||
"project": cty.String,
|
||||
})),
|
||||
}),
|
||||
vars: map[string]string{
|
||||
"TF_WORKSPACE": "i-dont-exist-in-org",
|
||||
},
|
||||
expectedErr: `Invalid workspace selection: OpenTofu failed to find workspace "i-dont-exist-in-org" in organization hashicorp`,
|
||||
},
|
||||
"workspaces and env var specified": {
|
||||
config: cty.ObjectVal(map[string]cty.Value{
|
||||
"hostname": cty.NullVal(cty.String),
|
||||
"token": cty.NullVal(cty.String),
|
||||
"organization": cty.StringVal("mordor"),
|
||||
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
||||
"name": cty.StringVal("mt-doom"),
|
||||
"tags": cty.NullVal(cty.Set(cty.String)),
|
||||
"project": cty.NullVal(cty.String),
|
||||
}),
|
||||
}),
|
||||
vars: map[string]string{
|
||||
"TF_WORKSPACE": "shire",
|
||||
},
|
||||
expectedWorkspaceName: "mt-doom",
|
||||
},
|
||||
"env var workspace does not have specified tag": {
|
||||
setup: func(b *Cloud) {
|
||||
b.client.Organizations.Create(context.Background(), tfe.OrganizationCreateOptions{
|
||||
Name: tfe.String("mordor"),
|
||||
})
|
||||
|
||||
b.client.Workspaces.Create(context.Background(), "mordor", tfe.WorkspaceCreateOptions{
|
||||
Name: tfe.String("shire"),
|
||||
})
|
||||
},
|
||||
config: cty.ObjectVal(map[string]cty.Value{
|
||||
"hostname": cty.NullVal(cty.String),
|
||||
"token": cty.NullVal(cty.String),
|
||||
"organization": cty.StringVal("mordor"),
|
||||
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
||||
"name": cty.NullVal(cty.String),
|
||||
"tags": cty.SetVal([]cty.Value{
|
||||
cty.StringVal("cloud"),
|
||||
}),
|
||||
"project": cty.NullVal(cty.String),
|
||||
}),
|
||||
}),
|
||||
vars: map[string]string{
|
||||
"TF_WORKSPACE": "shire",
|
||||
},
|
||||
expectedErr: "OpenTofu failed to find workspace \"shire\" with the tags specified in your configuration:\n[cloud]",
|
||||
},
|
||||
"env var workspace has specified tag": {
|
||||
setup: func(b *Cloud) {
|
||||
b.client.Organizations.Create(context.Background(), tfe.OrganizationCreateOptions{
|
||||
Name: tfe.String("mordor"),
|
||||
})
|
||||
|
||||
b.client.Workspaces.Create(context.Background(), "mordor", tfe.WorkspaceCreateOptions{
|
||||
Name: tfe.String("shire"),
|
||||
Tags: []*tfe.Tag{
|
||||
{
|
||||
Name: "hobbity",
|
||||
},
|
||||
},
|
||||
})
|
||||
},
|
||||
config: cty.ObjectVal(map[string]cty.Value{
|
||||
"hostname": cty.NullVal(cty.String),
|
||||
"token": cty.NullVal(cty.String),
|
||||
"organization": cty.StringVal("mordor"),
|
||||
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
||||
"name": cty.NullVal(cty.String),
|
||||
"tags": cty.SetVal([]cty.Value{
|
||||
cty.StringVal("hobbity"),
|
||||
}),
|
||||
"project": cty.NullVal(cty.String),
|
||||
}),
|
||||
}),
|
||||
vars: map[string]string{
|
||||
"TF_WORKSPACE": "shire",
|
||||
},
|
||||
expectedWorkspaceName: "", // No error is raised, but workspace is not set
|
||||
},
|
||||
"project specified": {
|
||||
config: cty.ObjectVal(map[string]cty.Value{
|
||||
"hostname": cty.NullVal(cty.String),
|
||||
"token": cty.NullVal(cty.String),
|
||||
"organization": cty.StringVal("mordor"),
|
||||
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
||||
"name": cty.StringVal("mt-doom"),
|
||||
"tags": cty.NullVal(cty.Set(cty.String)),
|
||||
"project": cty.StringVal("my-project"),
|
||||
}),
|
||||
}),
|
||||
expectedWorkspaceName: "mt-doom",
|
||||
expectedProjectName: "my-project",
|
||||
},
|
||||
"project env var specified": {
|
||||
config: cty.ObjectVal(map[string]cty.Value{
|
||||
"hostname": cty.NullVal(cty.String),
|
||||
"token": cty.NullVal(cty.String),
|
||||
"organization": cty.StringVal("mordor"),
|
||||
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
||||
"name": cty.StringVal("mt-doom"),
|
||||
"tags": cty.NullVal(cty.Set(cty.String)),
|
||||
"project": cty.NullVal(cty.String),
|
||||
}),
|
||||
}),
|
||||
vars: map[string]string{
|
||||
"TF_CLOUD_PROJECT": "other-project",
|
||||
},
|
||||
expectedWorkspaceName: "mt-doom",
|
||||
expectedProjectName: "other-project",
|
||||
},
|
||||
"project and env var specified": {
|
||||
config: cty.ObjectVal(map[string]cty.Value{
|
||||
"hostname": cty.NullVal(cty.String),
|
||||
"token": cty.NullVal(cty.String),
|
||||
"organization": cty.StringVal("mordor"),
|
||||
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
||||
"name": cty.StringVal("mt-doom"),
|
||||
"tags": cty.NullVal(cty.Set(cty.String)),
|
||||
"project": cty.StringVal("my-project"),
|
||||
}),
|
||||
}),
|
||||
vars: map[string]string{
|
||||
"TF_CLOUD_PROJECT": "other-project",
|
||||
},
|
||||
expectedWorkspaceName: "mt-doom",
|
||||
expectedProjectName: "my-project",
|
||||
},
|
||||
"workspace exists but in different project": {
|
||||
setup: func(b *Cloud) {
|
||||
b.client.Organizations.Create(context.Background(), tfe.OrganizationCreateOptions{
|
||||
Name: tfe.String("mordor"),
|
||||
})
|
||||
|
||||
project, _ := b.client.Projects.Create(context.Background(), "mordor", tfe.ProjectCreateOptions{
|
||||
Name: "another-project",
|
||||
})
|
||||
|
||||
b.client.Workspaces.Create(context.Background(), "mordor", tfe.WorkspaceCreateOptions{
|
||||
Name: tfe.String("shire"),
|
||||
Project: project,
|
||||
})
|
||||
},
|
||||
config: cty.ObjectVal(map[string]cty.Value{
|
||||
"hostname": cty.NullVal(cty.String),
|
||||
"token": cty.NullVal(cty.String),
|
||||
"organization": cty.StringVal("mordor"),
|
||||
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
||||
"name": cty.NullVal(cty.String),
|
||||
"tags": cty.SetVal([]cty.Value{
|
||||
cty.StringVal("hobbity"),
|
||||
}),
|
||||
"project": cty.StringVal("my-project"),
|
||||
}),
|
||||
}),
|
||||
expectedProjectName: "another-project", // No error is raised, workspace is still in the original project
|
||||
},
|
||||
"with everything set as env vars": {
|
||||
config: cty.ObjectVal(map[string]cty.Value{
|
||||
"hostname": cty.NullVal(cty.String),
|
||||
"token": cty.NullVal(cty.String),
|
||||
"organization": cty.NullVal(cty.String),
|
||||
"workspaces": cty.NullVal(cty.String),
|
||||
}),
|
||||
vars: map[string]string{
|
||||
"TF_CLOUD_ORGANIZATION": "mordor",
|
||||
"TF_WORKSPACE": "mt-doom",
|
||||
"TF_CLOUD_HOSTNAME": "mycool.tfe-host.io",
|
||||
"TF_CLOUD_PROJECT": "my-project",
|
||||
},
|
||||
expectedOrganization: "mordor",
|
||||
expectedWorkspaceName: "mt-doom",
|
||||
expectedHostname: "mycool.tfe-host.io",
|
||||
expectedProjectName: "my-project",
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range cases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
b, cleanup := testUnconfiguredBackend(t)
|
||||
t.Cleanup(cleanup)
|
||||
|
||||
for k, v := range tc.vars {
|
||||
os.Setenv(k, v)
|
||||
}
|
||||
|
||||
t.Cleanup(func() {
|
||||
for k := range tc.vars {
|
||||
os.Unsetenv(k)
|
||||
}
|
||||
})
|
||||
|
||||
_, valDiags := b.PrepareConfig(tc.config)
|
||||
if valDiags.Err() != nil {
|
||||
if (valDiags.Err() == nil) != (tc.expectedErr == "") {
|
||||
t.Fatalf("%s: unexpected validation result: %v", name, valDiags.Err())
|
||||
}
|
||||
|
||||
if tc.setup != nil {
|
||||
tc.setup(b)
|
||||
}
|
||||
|
||||
diags := b.Configure(tc.config)
|
||||
if (diags.Err() != nil || tc.expectedErr != "") &&
|
||||
(diags.Err() == nil || !strings.Contains(diags.Err().Error(), tc.expectedErr)) {
|
||||
t.Fatalf("%s: unexpected configure result: %v", name, diags.Err())
|
||||
}
|
||||
|
||||
if tc.expectedOrganization != "" && tc.expectedOrganization != b.organization {
|
||||
t.Fatalf("%s: organization not valid: %s, expected: %s", name, b.organization, tc.expectedOrganization)
|
||||
}
|
||||
|
||||
if tc.expectedHostname != "" && tc.expectedHostname != b.hostname {
|
||||
t.Fatalf("%s: hostname not valid: %s, expected: %s", name, b.hostname, tc.expectedHostname)
|
||||
}
|
||||
|
||||
if tc.expectedWorkspaceName != "" && tc.expectedWorkspaceName != b.WorkspaceMapping.Name {
|
||||
t.Fatalf("%s: workspace name not valid: %s, expected: %s", name, b.WorkspaceMapping.Name, tc.expectedWorkspaceName)
|
||||
}
|
||||
|
||||
if tc.expectedProjectName != "" && tc.expectedProjectName != b.WorkspaceMapping.Project {
|
||||
t.Fatalf("%s: project name not valid: %s, expected: %s", name, b.WorkspaceMapping.Project, tc.expectedProjectName)
|
||||
if valDiags.Err() != nil {
|
||||
if !strings.Contains(valDiags.Err().Error(), tc.expectedErr) {
|
||||
t.Fatalf("%s: unexpected validation result: %v", name, valDiags.Err())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -859,170 +596,312 @@ func TestCloud_setUnavailableTerraformVersion(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCloud_setConfigurationFields(t *testing.T) {
|
||||
originalForceBackendEnv := os.Getenv("TF_FORCE_LOCAL_BACKEND")
|
||||
|
||||
func TestCloud_setConfigurationFieldsHappyPath(t *testing.T) {
|
||||
cases := map[string]struct {
|
||||
obj cty.Value
|
||||
envVars map[string]string
|
||||
expectedHostname string
|
||||
expectedOrganziation string
|
||||
expectedOrganization string
|
||||
expectedWorkspaceName string
|
||||
expectedProjectName string
|
||||
expectedWorkspaceTags []string
|
||||
expectedWorkspaceTags map[string]struct{}
|
||||
expectedForceLocal bool
|
||||
setEnv func()
|
||||
resetEnv func()
|
||||
expectedErr string
|
||||
}{
|
||||
"with hostname set": {
|
||||
"with hostname, organization and tags set": {
|
||||
obj: cty.ObjectVal(map[string]cty.Value{
|
||||
"organization": cty.StringVal("hashicorp"),
|
||||
"hostname": cty.StringVal("hashicorp.com"),
|
||||
"organization": cty.StringVal("opentofu"),
|
||||
"hostname": cty.StringVal("opentofu.org"),
|
||||
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
||||
"name": cty.StringVal("prod"),
|
||||
"tags": cty.NullVal(cty.Set(cty.String)),
|
||||
"name": cty.NullVal(cty.String),
|
||||
"tags": cty.SetVal([]cty.Value{cty.StringVal("foo"), cty.StringVal("bar")}),
|
||||
"project": cty.NullVal(cty.String),
|
||||
}),
|
||||
}),
|
||||
expectedHostname: "hashicorp.com",
|
||||
expectedOrganziation: "hashicorp",
|
||||
expectedHostname: "opentofu.org",
|
||||
expectedOrganization: "opentofu",
|
||||
expectedWorkspaceTags: map[string]struct{}{"foo": {}, "bar": {}},
|
||||
},
|
||||
"with workspace name set": {
|
||||
"with hostname and workspace name set": {
|
||||
obj: cty.ObjectVal(map[string]cty.Value{
|
||||
"organization": cty.StringVal("hashicorp"),
|
||||
"hostname": cty.StringVal("hashicorp.com"),
|
||||
"organization": cty.NullVal(cty.String),
|
||||
"hostname": cty.StringVal("opentofu.org"),
|
||||
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
||||
"name": cty.StringVal("prod"),
|
||||
"tags": cty.NullVal(cty.Set(cty.String)),
|
||||
"project": cty.NullVal(cty.String),
|
||||
}),
|
||||
}),
|
||||
expectedHostname: "hashicorp.com",
|
||||
expectedOrganziation: "hashicorp",
|
||||
expectedHostname: "opentofu.org",
|
||||
expectedWorkspaceName: "prod",
|
||||
},
|
||||
"with workspace tags set": {
|
||||
"with hostname and project name set": {
|
||||
obj: cty.ObjectVal(map[string]cty.Value{
|
||||
"organization": cty.StringVal("hashicorp"),
|
||||
"hostname": cty.StringVal("hashicorp.com"),
|
||||
"organization": cty.NullVal(cty.String),
|
||||
"hostname": cty.StringVal("opentofu.org"),
|
||||
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
||||
"name": cty.NullVal(cty.String),
|
||||
"tags": cty.SetVal(
|
||||
[]cty.Value{
|
||||
cty.StringVal("billing"),
|
||||
},
|
||||
),
|
||||
"project": cty.NullVal(cty.String),
|
||||
}),
|
||||
}),
|
||||
expectedHostname: "hashicorp.com",
|
||||
expectedOrganziation: "hashicorp",
|
||||
expectedWorkspaceTags: []string{"billing"},
|
||||
},
|
||||
"with project name set": {
|
||||
obj: cty.ObjectVal(map[string]cty.Value{
|
||||
"organization": cty.StringVal("hashicorp"),
|
||||
"hostname": cty.StringVal("hashicorp.com"),
|
||||
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
||||
"name": cty.StringVal("prod"),
|
||||
"name": cty.NullVal(cty.String),
|
||||
"tags": cty.NullVal(cty.Set(cty.String)),
|
||||
"project": cty.StringVal("my-project"),
|
||||
}),
|
||||
}),
|
||||
expectedHostname: "hashicorp.com",
|
||||
expectedOrganziation: "hashicorp",
|
||||
expectedWorkspaceName: "prod",
|
||||
expectedProjectName: "my-project",
|
||||
expectedHostname: "opentofu.org",
|
||||
expectedProjectName: "my-project",
|
||||
},
|
||||
"with force local set": {
|
||||
"with hostname and force local set (env var)": {
|
||||
obj: cty.ObjectVal(map[string]cty.Value{
|
||||
"organization": cty.StringVal("hashicorp"),
|
||||
"hostname": cty.StringVal("hashicorp.com"),
|
||||
"organization": cty.NullVal(cty.String),
|
||||
"hostname": cty.StringVal("opentofu.org"),
|
||||
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
||||
"name": cty.NullVal(cty.String),
|
||||
"tags": cty.NullVal(cty.Set(cty.String)),
|
||||
"project": cty.NullVal(cty.String),
|
||||
}),
|
||||
}),
|
||||
expectedHostname: "hashicorp.com",
|
||||
expectedOrganziation: "hashicorp",
|
||||
setEnv: func() {
|
||||
os.Setenv("TF_FORCE_LOCAL_BACKEND", "1")
|
||||
},
|
||||
resetEnv: func() {
|
||||
os.Setenv("TF_FORCE_LOCAL_BACKEND", originalForceBackendEnv)
|
||||
expectedHostname: "opentofu.org",
|
||||
envVars: map[string]string{
|
||||
"TF_FORCE_LOCAL_BACKEND": "1",
|
||||
},
|
||||
expectedForceLocal: true,
|
||||
},
|
||||
"with hostname and workspace tags set, and tags overwritten by TF_WORKSPACE": {
|
||||
// see: https://github.com/opentofu/opentofu/issues/814
|
||||
obj: cty.ObjectVal(map[string]cty.Value{
|
||||
"organization": cty.NullVal(cty.String),
|
||||
"hostname": cty.StringVal("opentofu.org"),
|
||||
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
||||
"name": cty.NullVal(cty.String),
|
||||
"tags": cty.SetVal([]cty.Value{cty.StringVal("foo"), cty.StringVal("bar")}),
|
||||
"project": cty.NullVal(cty.String),
|
||||
}),
|
||||
}),
|
||||
envVars: map[string]string{
|
||||
"TF_WORKSPACE": "foo",
|
||||
},
|
||||
expectedHostname: "opentofu.org",
|
||||
expectedWorkspaceName: "foo",
|
||||
expectedWorkspaceTags: nil,
|
||||
},
|
||||
"with hostname and workspace name set, and TF_WORKSPACE specified": {
|
||||
obj: cty.ObjectVal(map[string]cty.Value{
|
||||
"organization": cty.NullVal(cty.String),
|
||||
"hostname": cty.StringVal("opentofu.org"),
|
||||
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
||||
"name": cty.StringVal("old"),
|
||||
"tags": cty.NullVal(cty.Set(cty.String)),
|
||||
"project": cty.NullVal(cty.String),
|
||||
}),
|
||||
}),
|
||||
envVars: map[string]string{
|
||||
"TF_WORKSPACE": "new",
|
||||
},
|
||||
expectedHostname: "opentofu.org",
|
||||
expectedWorkspaceName: "old",
|
||||
expectedWorkspaceTags: nil,
|
||||
},
|
||||
"with hostname and project set, and project overwritten by TF_CLOUD_PROJECT": {
|
||||
obj: cty.ObjectVal(map[string]cty.Value{
|
||||
"organization": cty.NullVal(cty.String),
|
||||
"hostname": cty.StringVal("opentofu.org"),
|
||||
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
||||
"name": cty.NullVal(cty.String),
|
||||
"tags": cty.NullVal(cty.Set(cty.String)),
|
||||
"project": cty.StringVal("old"),
|
||||
}),
|
||||
}),
|
||||
envVars: map[string]string{
|
||||
"TF_CLOUD_PROJECT": "new",
|
||||
},
|
||||
expectedHostname: "opentofu.org",
|
||||
expectedProjectName: "old",
|
||||
},
|
||||
"with hostname set, and project specified by TF_CLOUD_PROJECT": {
|
||||
obj: cty.ObjectVal(map[string]cty.Value{
|
||||
"organization": cty.NullVal(cty.String),
|
||||
"hostname": cty.StringVal("opentofu.org"),
|
||||
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
||||
"name": cty.NullVal(cty.String),
|
||||
"tags": cty.NullVal(cty.Set(cty.String)),
|
||||
"project": cty.NullVal(cty.String),
|
||||
}),
|
||||
}),
|
||||
envVars: map[string]string{
|
||||
"TF_CLOUD_PROJECT": "new",
|
||||
},
|
||||
expectedHostname: "opentofu.org",
|
||||
expectedProjectName: "new",
|
||||
},
|
||||
"with hostname set, and organization specified by TF_CLOUD_ORGANIZATION": {
|
||||
obj: cty.ObjectVal(map[string]cty.Value{
|
||||
"hostname": cty.StringVal("opentofu.org"),
|
||||
"token": cty.NullVal(cty.String),
|
||||
"organization": cty.NullVal(cty.String),
|
||||
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
||||
"name": cty.NullVal(cty.String),
|
||||
"tags": cty.NullVal(cty.Set(cty.String)),
|
||||
"project": cty.NullVal(cty.String),
|
||||
}),
|
||||
}),
|
||||
envVars: map[string]string{
|
||||
"TF_CLOUD_ORGANIZATION": "my-org",
|
||||
},
|
||||
expectedHostname: "opentofu.org",
|
||||
expectedOrganization: "my-org",
|
||||
},
|
||||
"with hostname set, and TF_CLOUD_HOSTNAME defined": {
|
||||
obj: cty.ObjectVal(map[string]cty.Value{
|
||||
"hostname": cty.StringVal("opentofu.org"),
|
||||
"token": cty.NullVal(cty.String),
|
||||
"organization": cty.NullVal(cty.String),
|
||||
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
||||
"name": cty.NullVal(cty.String),
|
||||
"tags": cty.NullVal(cty.Set(cty.String)),
|
||||
"project": cty.NullVal(cty.String),
|
||||
}),
|
||||
}),
|
||||
envVars: map[string]string{
|
||||
"TF_CLOUD_HOSTNAME": "new",
|
||||
},
|
||||
expectedHostname: "opentofu.org",
|
||||
},
|
||||
"with hostname specified by TF_CLOUD_HOSTNAME": {
|
||||
obj: 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.NullVal(cty.String),
|
||||
"tags": cty.NullVal(cty.Set(cty.String)),
|
||||
"project": cty.NullVal(cty.String),
|
||||
}),
|
||||
}),
|
||||
envVars: map[string]string{
|
||||
"TF_CLOUD_HOSTNAME": "new",
|
||||
},
|
||||
expectedHostname: "new",
|
||||
},
|
||||
"with nothing set, all configured using env vars": {
|
||||
obj: cty.ObjectVal(map[string]cty.Value{
|
||||
"hostname": cty.NullVal(cty.String),
|
||||
"organization": cty.NullVal(cty.String),
|
||||
"workspaces": cty.NullVal(cty.String),
|
||||
}),
|
||||
envVars: map[string]string{
|
||||
"TF_CLOUD_HOSTNAME": "opentofu.org",
|
||||
"TF_CLOUD_ORGANIZATION": "opentofu",
|
||||
"TF_WORKSPACE": "foo",
|
||||
"TF_CLOUD_PROJECT": "bar",
|
||||
},
|
||||
expectedHostname: "opentofu.org",
|
||||
expectedOrganization: "opentofu",
|
||||
expectedWorkspaceName: "foo",
|
||||
expectedProjectName: "bar",
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range cases {
|
||||
b := &Cloud{}
|
||||
t.Run(name, func(t *testing.T) {
|
||||
for k, v := range tc.envVars {
|
||||
t.Setenv(k, v)
|
||||
}
|
||||
|
||||
// if `setEnv` is set, then we expect `resetEnv` to also be set
|
||||
if tc.setEnv != nil {
|
||||
tc.setEnv()
|
||||
defer tc.resetEnv()
|
||||
}
|
||||
b := &Cloud{}
|
||||
errDiags := b.setConfigurationFields(tc.obj)
|
||||
|
||||
errDiags := b.setConfigurationFields(tc.obj)
|
||||
if errDiags.HasErrors() || tc.expectedErr != "" {
|
||||
actualErr := errDiags.Err().Error()
|
||||
if !strings.Contains(actualErr, tc.expectedErr) {
|
||||
if errDiags.HasErrors() {
|
||||
t.Fatalf("%s: unexpected validation result: %v", name, errDiags.Err())
|
||||
}
|
||||
}
|
||||
|
||||
if tc.expectedHostname != "" && b.hostname != tc.expectedHostname {
|
||||
t.Fatalf("%s: expected hostname %s to match configured hostname %s", name, b.hostname, tc.expectedHostname)
|
||||
}
|
||||
if tc.expectedOrganziation != "" && b.organization != tc.expectedOrganziation {
|
||||
t.Fatalf("%s: expected organization (%s) to match configured organization (%s)", name, b.organization, tc.expectedOrganziation)
|
||||
}
|
||||
if tc.expectedWorkspaceName != "" && b.WorkspaceMapping.Name != tc.expectedWorkspaceName {
|
||||
t.Fatalf("%s: expected workspace name mapping (%s) to match configured workspace name (%s)", name, b.WorkspaceMapping.Name, tc.expectedWorkspaceName)
|
||||
}
|
||||
if len(tc.expectedWorkspaceTags) > 0 {
|
||||
presentSet := make(map[string]struct{})
|
||||
for _, tag := range b.WorkspaceMapping.Tags {
|
||||
presentSet[tag] = struct{}{}
|
||||
if b.hostname != tc.expectedHostname {
|
||||
t.Fatalf("%s: expected hostname %s to match configured hostname %s", name, b.hostname, tc.expectedHostname)
|
||||
}
|
||||
if b.organization != tc.expectedOrganization {
|
||||
t.Fatalf("%s: expected organization (%s) to match configured organization (%s)", name, b.organization, tc.expectedOrganization)
|
||||
}
|
||||
if b.WorkspaceMapping.Name != tc.expectedWorkspaceName {
|
||||
t.Fatalf("%s: expected workspace name mapping (%s) to match configured workspace name (%s)", name, b.WorkspaceMapping.Name, tc.expectedWorkspaceName)
|
||||
}
|
||||
if b.forceLocal != tc.expectedForceLocal {
|
||||
t.Fatalf("%s: expected force local backend to be set to %v", name, tc.expectedForceLocal)
|
||||
}
|
||||
if b.WorkspaceMapping.Project != tc.expectedProjectName {
|
||||
t.Fatalf("%s: expected project name mapping (%s) to match configured project name (%s)", name, b.WorkspaceMapping.Project, tc.expectedProjectName)
|
||||
}
|
||||
|
||||
expectedSet := make(map[string]struct{})
|
||||
for _, tag := range tc.expectedWorkspaceTags {
|
||||
expectedSet[tag] = struct{}{}
|
||||
// read map of configured tags
|
||||
gotTags := map[string]struct{}{}
|
||||
for _, v := range b.WorkspaceMapping.Tags {
|
||||
gotTags[v] = struct{}{}
|
||||
}
|
||||
|
||||
var missing []string
|
||||
var unexpected []string
|
||||
if len(gotTags) != len(tc.expectedWorkspaceTags) {
|
||||
t.Fatalf("%s: unordered workspace tags (%v) don't match configuration (%v)", name, gotTags, tc.expectedWorkspaceTags)
|
||||
}
|
||||
|
||||
for _, expected := range tc.expectedWorkspaceTags {
|
||||
if _, ok := presentSet[expected]; !ok {
|
||||
missing = append(missing, expected)
|
||||
for k := range tc.expectedWorkspaceTags {
|
||||
if _, ok := gotTags[k]; !ok {
|
||||
t.Fatalf("%s: unordered workspace tags (%v) don't match configuration (%v)", name, gotTags, tc.expectedWorkspaceTags)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for _, actual := range b.WorkspaceMapping.Tags {
|
||||
if _, ok := expectedSet[actual]; !ok {
|
||||
unexpected = append(unexpected, actual)
|
||||
}
|
||||
func TestCloud_setConfigurationFieldsUnhappyPath(t *testing.T) {
|
||||
cases := map[string]struct {
|
||||
obj cty.Value
|
||||
envVars map[string]string
|
||||
wantSummary string
|
||||
wantDetail string
|
||||
}{
|
||||
"cloud block is not configured": {
|
||||
obj: cty.ObjectVal(map[string]cty.Value{
|
||||
"organization": cty.NullVal(cty.String),
|
||||
"hostname": cty.NullVal(cty.String),
|
||||
"workspaces": cty.NullVal(cty.String),
|
||||
}),
|
||||
wantSummary: "Hostname is required for the cloud backend",
|
||||
wantDetail: `OpenTofu does not provide a default "hostname" attribute, so it must be set to the hostname of the cloud backend.`,
|
||||
},
|
||||
"with hostname and workspace tags set, and tags overwritten by TF_WORKSPACE": {
|
||||
// see: https://github.com/opentofu/opentofu/issues/814
|
||||
obj: cty.ObjectVal(map[string]cty.Value{
|
||||
"organization": cty.NullVal(cty.String),
|
||||
"hostname": cty.StringVal("opentofu.org"),
|
||||
"workspaces": cty.ObjectVal(map[string]cty.Value{
|
||||
"name": cty.NullVal(cty.String),
|
||||
"tags": cty.SetVal([]cty.Value{cty.StringVal("foo"), cty.StringVal("bar")}),
|
||||
"project": cty.NullVal(cty.String),
|
||||
}),
|
||||
}),
|
||||
envVars: map[string]string{
|
||||
"TF_WORKSPACE": "qux",
|
||||
},
|
||||
wantSummary: invalidWorkspaceConfigMisconfigurationEnvVar.Description().Summary,
|
||||
wantDetail: invalidWorkspaceConfigMisconfigurationEnvVar.Description().Detail,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range cases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
for k, v := range tc.envVars {
|
||||
t.Setenv(k, v)
|
||||
}
|
||||
|
||||
if len(missing) > 0 {
|
||||
t.Fatalf("%s: expected workspace tag mapping (%s) to contain the following tags: %s", name, b.WorkspaceMapping.Tags, missing)
|
||||
b := &Cloud{}
|
||||
errDiags := b.setConfigurationFields(tc.obj)
|
||||
if (tc.wantDetail != "" || tc.wantSummary != "") != errDiags.HasErrors() {
|
||||
t.Fatalf("%s error expected", name)
|
||||
}
|
||||
|
||||
if len(unexpected) > 0 {
|
||||
t.Fatalf("%s: expected workspace tag mapping (%s) to NOT contain the following tags: %s", name, b.WorkspaceMapping.Tags, unexpected)
|
||||
gotSummary := errDiags[0].Description().Summary
|
||||
if gotSummary != tc.wantSummary {
|
||||
t.Fatalf("%s diagnostic summary mismatch, want: %s, got: %s", name, tc.wantSummary, gotSummary)
|
||||
}
|
||||
|
||||
}
|
||||
if tc.expectedForceLocal != false && b.forceLocal != tc.expectedForceLocal {
|
||||
t.Fatalf("%s: expected force local backend to be set ", name)
|
||||
}
|
||||
if tc.expectedProjectName != "" && b.WorkspaceMapping.Project != tc.expectedProjectName {
|
||||
t.Fatalf("%s: expected project name mapping (%s) to match configured project name (%s)", name, b.WorkspaceMapping.Project, tc.expectedProjectName)
|
||||
}
|
||||
gotDetail := errDiags[0].Description().Detail
|
||||
if gotDetail != tc.wantDetail {
|
||||
t.Fatalf("%s diagnostic details mismatch, want: %s, got: %s", name, tc.wantDetail, gotDetail)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -38,6 +38,13 @@ var (
|
||||
fmt.Sprintf("Only one of workspace \"tags\" or \"name\" is allowed.\n\n%s", workspaceConfigurationHelp),
|
||||
cty.Path{cty.GetAttrStep{Name: "workspaces"}},
|
||||
)
|
||||
|
||||
invalidWorkspaceConfigMisconfigurationEnvVar = tfdiags.AttributeValue(
|
||||
tfdiags.Error,
|
||||
"Invalid workspaces configuration",
|
||||
fmt.Sprintf("The workspace defined using the environment variable \"TF_WORKSPACE\" does not belong to \"tags\".\n\n%s", workspaceConfigurationHelp),
|
||||
cty.Path{cty.GetAttrStep{Name: "workspaces"}},
|
||||
)
|
||||
)
|
||||
|
||||
const ignoreRemoteVersionHelp = "If you're sure you want to upgrade the state, you can force OpenTofu to continue using the -ignore-remote-version flag. This may result in an unusable workspace."
|
||||
|
@ -94,7 +94,7 @@ Remote execution with non-interactive workflows requires auto-approved deploymen
|
||||
|
||||
Use the following environment variables to configure the `cloud` block:
|
||||
|
||||
- `TF_CLOUD_ORGANIZATION` - The name of the organization. OpenTofu reads this variable when `organization` omitted from the `cloud` block`. If both are specified, the configuration takes precedence.
|
||||
- `TF_CLOUD_ORGANIZATION` - The name of the organization. OpenTofu reads this variable when `organization` omitted from the `cloud` block. If both are specified, the configuration takes precedence.
|
||||
|
||||
- `TF_CLOUD_HOSTNAME` - The hostname of the cloud backend. OpenTofu reads this when `hostname` is omitted from the `cloud` block. If both are specified, the configuration takes precedence.
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user