Repository: ariga/atlas
Pull request #1878 (2023-07-19 11:57:54)

schemahcl: add tf function for simple reading files


Patch:
diff --git a/cmd/atlas/internal/cmdapi/project.go b/cmd/atlas/internal/cmdapi/project.go
index e9f2421f..d1e0475b 100644
--- a/cmd/atlas/internal/cmdapi/project.go
+++ b/cmd/atlas/internal/cmdapi/project.go
@@ -404,6 +404,10 @@ func parseConfig(path, env string, opts ...LoadOption) (*Project, error) {
 	if err != nil {
 		return nil, err
 	}
+	base, err := filepath.Abs(filepath.Dir(path))
+	if err != nil {
+		return nil, err
+	}
 	cfg := &cmdext.AtlasConfig{}
 	state := schemahcl.New(
 		append(
@@ -416,6 +420,7 @@ func parseConfig(path, env string, opts ...LoadOption) (*Project, error) {
 				}),
 			}),
 			schemahcl.WithFunctions(map[string]function.Function{
+				"file": schemahcl.MakeFileFunc(base),
 				"getenv": function.New(&function.Spec{
 					Params: []function.Parameter{
 						{
diff --git a/schemahcl/stdlib.go b/schemahcl/stdlib.go
index 7a05a3a3..98a12c64 100644
--- a/schemahcl/stdlib.go
+++ b/schemahcl/stdlib.go
@@ -7,6 +7,8 @@ package schemahcl
 import (
 	"fmt"
 	"net/url"
+	"os"
+	"path/filepath"
 	"strconv"
 
 	"github.com/hashicorp/hcl/v2"
@@ -246,86 +248,115 @@ func makeToFunc(wantTy cty.Type) function.Function {
 	})
 }
 
-var urlQuerySetFunc = function.New(&function.Spec{
-	Params: []function.Parameter{
-		{
-			Name: "url",
-			Type: cty.String,
+var (
+	urlQuerySetFunc = function.New(&function.Spec{
+		Params: []function.Parameter{
+			{
+				Name: "url",
+				Type: cty.String,
+			},
+			{
+				Name: "key",
+				Type: cty.String,
+			},
+			{
+				Name: "value",
+				Type: cty.String,
+			},
+		},
+		Type: function.StaticReturnType(cty.String),
+		Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
+			u, err := url.Parse(args[0].AsString())
+			if err != nil {
+				return cty.NilVal, err
+			}
+			q := u.Query()
+			q.Set(args[1].AsString(), args[2].AsString())
+			u.RawQuery = q.Encode()
+			return cty.StringVal(u.String()), nil
 		},
-		{
-			Name: "key",
-			Type: cty.String,
+	})
+	urlSetPathFunc = function.New(&function.Spec{
+		Params: []function.Parameter{
+			{
+				Name: "url",
+				Type: cty.String,
+			},
+			{
+				Name: "path",
+				Type: cty.String,
+			},
 		},
-		{
-			Name: "value",
-			Type: cty.String,
+		Type: function.StaticReturnType(cty.String),
+		Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
+			u, err := url.Parse(args[0].AsString())
+			if err != nil {
+				return cty.NilVal, err
+			}
+			u.Path = args[1].AsString()
+			return cty.StringVal(u.String()), nil
 		},
-	},
-	Type: function.StaticReturnType(cty.String),
-	Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
-		u, err := url.Parse(args[0].AsString())
-		if err != nil {
-			return cty.NilVal, err
-		}
-		q := u.Query()
-		q.Set(args[1].AsString(), args[2].AsString())
-		u.RawQuery = q.Encode()
-		return cty.StringVal(u.String()), nil
-	},
-})
+	})
 
-var urlSetPathFunc = function.New(&function.Spec{
-	Params: []function.Parameter{
-		{
-			Name: "url",
-			Type: cty.String,
+	urlEscape = function.New(&function.Spec{
+		Params: []function.Parameter{
+			{
+				Name: "string",
+				Type: cty.String,
+			},
 		},
-		{
-			Name: "path",
-			Type: cty.String,
+		Type: function.StaticReturnType(cty.String),
+		Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
+			u := url.QueryEscape(args[0].AsString())
+			return cty.StringVal(u), nil
 		},
-	},
-	Type: function.StaticReturnType(cty.String),
-	Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
-		u, err := url.Parse(args[0].AsString())
-		if err != nil {
-			return cty.NilVal, err
-		}
-		u.Path = args[1].AsString()
-		return cty.StringVal(u.String()), nil
-	},
-})
+	})
 
-var urlEscape = function.New(&function.Spec{
-	Params: []function.Parameter{
-		{
-			Name: "string",
-			Type: cty.String,
+	printFunc = function.New(&function.Spec{
+		Params: []function.Parameter{
+			{
+				Name: "print",
+				Type: cty.DynamicPseudoType,
+			},
+		},
+		Type: func(args []cty.Value) (cty.Type, error) {
+			return args[0].Type(), nil
+		},
+		Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
+			if b, err := json.Marshal(args[0], args[0].Type()); err != nil {
+				fmt.Println(args[0].GoString())
+			} else {
+				fmt.Println(string(b))
+			}
+			return args[0], nil
 		},
-	},
-	Type: function.StaticReturnType(cty.String),
-	Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
-		u := url.QueryEscape(args[0].AsString())
-		return cty.StringVal(u), nil
-	},
-})
+	})
+)
 
-var printFunc = function.New(&function.Spec{
-	Params: []function.Parameter{
-		{
-			Name: "print",
-			Type: cty.DynamicPseudoType,
+// MakeFileFunc returns a function that reads a file
+// from the given base directory.
+func MakeFileFunc(base string) function.Function {
+	return function.New(&function.Spec{
+		Params: []function.Parameter{
+			{
+				Name: "path",
+				Type: cty.String,
+			},
+		},
+		Type: function.StaticReturnType(cty.String),
+		Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
+			if !filepath.IsAbs(base) {
+				return cty.NilVal, fmt.Errorf("base directory must be an absolute path. got: %s", base)
+			}
+			path := args[0].AsString()
+			if !filepath.IsAbs(path) {
+				path = filepath.Clean(filepath.Join(base, path))
+			}
+			src, err := os.ReadFile(path)
+			if err != nil {
+				return cty.NilVal, err
+			}
+			return cty.StringVal(string(src)), nil
 		},
-	},
-	Type: func(args []cty.Value) (cty.Type, error) {
-		return args[0].Type(), nil
-	},
-	Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
-		if b, err := json.Marshal(args[0], args[0].Type()); err != nil {
-			fmt.Println(args[0].GoString())
-		} else {
-			fmt.Println(string(b))
-		}
-		return args[0], nil
-	},
-})
+	})
+}


Test patch:
diff --git a/cmd/atlas/internal/cmdapi/project_test.go b/cmd/atlas/internal/cmdapi/project_test.go
index 275f446c..b9ed38fa 100644
--- a/cmd/atlas/internal/cmdapi/project_test.go
+++ b/cmd/atlas/internal/cmdapi/project_test.go
@@ -21,6 +21,7 @@ import (
 
 func TestEnvByName(t *testing.T) {
 	d := t.TempDir()
+	require.NoError(t, os.WriteFile(filepath.Join(d, "local.txt"), []byte("text"), 0600))
 	h := `
 variable "name" {
   type = string
@@ -30,6 +31,7 @@ variable "name" {
 locals {
   envName = atlas.env
   emptyEnv = getenv("NOT_SET")
+  opened = file("local.txt")
 }
 
 lint {
diff --git a/schemahcl/stdlib_test.go b/schemahcl/stdlib_test.go
index 9e1c4a79..9fc520b9 100644
--- a/schemahcl/stdlib_test.go
+++ b/schemahcl/stdlib_test.go
@@ -7,8 +7,10 @@ package schemahcl
 import (
 	"fmt"
 	"net/url"
+	"path/filepath"
 	"testing"
 
+	"github.com/stretchr/testify/require"
 	"github.com/zclconf/go-cty/cty"
 )
 
@@ -109,6 +111,18 @@ func TestURLEscapeFunc(t *testing.T) {
 	}
 }
 
+func TestMakeFileFunc(t *testing.T) {
+	fn := MakeFileFunc("testdata")
+	_, err := fn.Call([]cty.Value{cty.StringVal("foo")})
+	require.EqualError(t, err, "base directory must be an absolute path. got: testdata")
+	base, err := filepath.Abs("testdata")
+	require.NoError(t, err)
+	fn = MakeFileFunc(base)
+	v, err := fn.Call([]cty.Value{cty.StringVal("a.hcl")})
+	require.NoError(t, err)
+	require.Equal(t, "person \"rotemtam\" {\n  hobby = var.hobby\n}", v.AsString())
+}
+
 func Example_PrintFunc() {
 	for _, f := range []string{
 		`v  = print(1)`,