core: path.module, path.root, path.cwd use fwd slashes on all platforms

Previously we used the native slash type for the host platform, but that
leads to issues if the same configuration is applied on both Windows and
non-Windows systems.

Since Windows supports slashes and backslashes, we can safely return
always slashes here and require that users combine the result with
subsequent path parts using slashes, like:

    "${path.module}/foo/bar"

Previously the above would lead to an error on Windows if path.module
contained any backslashes.

This is not really possible to unit test directly right now since we
always run our tests on Unix systems and filepath.ToSlash is a no-op on
Unix. However, this does include some tests for the basic behavior to
verify that it's not regressed as a result of this change.

This will need to be reported in the changelog as a potential breaking
change, since anyone who was using Terraform _exclusively_ on Windows may
have been using expressions like "${path.module}foo\\bar" which they will
now need to update.

This fixes #14986.
This commit is contained in:
Martin Atkins 2018-12-19 12:29:29 -08:00
parent 2cf63d068f
commit cf9499cb78
2 changed files with 48 additions and 3 deletions

View File

@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"log" "log"
"os" "os"
"path/filepath"
"strconv" "strconv"
"sync" "sync"
@ -423,7 +424,7 @@ func (d *evaluationStateData) GetPathAttr(addr addrs.PathAttr, rng tfdiags.Sourc
}) })
return cty.DynamicVal, diags return cty.DynamicVal, diags
} }
return cty.StringVal(wd), diags return cty.StringVal(filepath.ToSlash(wd)), diags
case "module": case "module":
moduleConfig := d.Evaluator.Config.DescendentForInstance(d.ModulePath) moduleConfig := d.Evaluator.Config.DescendentForInstance(d.ModulePath)
@ -433,11 +434,11 @@ func (d *evaluationStateData) GetPathAttr(addr addrs.PathAttr, rng tfdiags.Sourc
panic(fmt.Sprintf("module.path read from module %s, which has no configuration", d.ModulePath)) panic(fmt.Sprintf("module.path read from module %s, which has no configuration", d.ModulePath))
} }
sourceDir := moduleConfig.Module.SourceDir sourceDir := moduleConfig.Module.SourceDir
return cty.StringVal(sourceDir), diags return cty.StringVal(filepath.ToSlash(sourceDir)), diags
case "root": case "root":
sourceDir := d.Evaluator.Config.Module.SourceDir sourceDir := d.Evaluator.Config.Module.SourceDir
return cty.StringVal(sourceDir), diags return cty.StringVal(filepath.ToSlash(sourceDir)), diags
default: default:
suggestion := nameSuggestion(addr.Name, []string{"cwd", "module", "root"}) suggestion := nameSuggestion(addr.Name, []string{"cwd", "module", "root"})

View File

@ -7,6 +7,7 @@ import (
"github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/addrs" "github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/configs"
"github.com/hashicorp/terraform/tfdiags" "github.com/hashicorp/terraform/tfdiags"
) )
@ -34,3 +35,46 @@ func TestEvaluatorGetTerraformAttr(t *testing.T) {
} }
}) })
} }
func TestEvaluatorGetPathAttr(t *testing.T) {
evaluator := &Evaluator{
Meta: &ContextMeta{
Env: "foo",
},
Config: &configs.Config{
Module: &configs.Module{
SourceDir: "bar/baz",
},
},
}
data := &evaluationStateData{
Evaluator: evaluator,
}
scope := evaluator.Scope(data, nil)
t.Run("module", func(t *testing.T) {
want := cty.StringVal("bar/baz")
got, diags := scope.Data.GetPathAttr(addrs.PathAttr{
Name: "module",
}, tfdiags.SourceRange{})
if len(diags) != 0 {
t.Errorf("unexpected diagnostics %s", spew.Sdump(diags))
}
if !got.RawEquals(want) {
t.Errorf("wrong result %#v; want %#v", got, want)
}
})
t.Run("root", func(t *testing.T) {
want := cty.StringVal("bar/baz")
got, diags := scope.Data.GetPathAttr(addrs.PathAttr{
Name: "root",
}, tfdiags.SourceRange{})
if len(diags) != 0 {
t.Errorf("unexpected diagnostics %s", spew.Sdump(diags))
}
if !got.RawEquals(want) {
t.Errorf("wrong result %#v; want %#v", got, want)
}
})
}