diff --git a/go.mod b/go.mod index d394d5a..1ce363e 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/onsi/ginkgo/v2 v2.9.2 github.com/onsi/gomega v1.27.4 github.com/patrickhuber/go-di v0.5.2 - github.com/patrickhuber/go-xplat v0.2.15 + github.com/patrickhuber/go-xplat v0.3.0 github.com/urfave/cli/v2 v2.23.4 gopkg.in/yaml.v3 v3.0.1 ) diff --git a/go.sum b/go.sum index 5b3cfd2..19a6220 100644 --- a/go.sum +++ b/go.sum @@ -56,6 +56,10 @@ github.com/patrickhuber/go-xplat v0.2.14 h1:RR3C9YbJ02XeS6CXrqRiSUor+W2UxOENnHr+ github.com/patrickhuber/go-xplat v0.2.14/go.mod h1:ZveaAmQiWcZO0nWACUrbylAF+BbGwEXdVqM3ZoEOxA4= github.com/patrickhuber/go-xplat v0.2.15 h1:qEqPqjmLp0yqHFCWqE+s41ni55JKs0DhHbK3Mt6xZFo= github.com/patrickhuber/go-xplat v0.2.15/go.mod h1:ZveaAmQiWcZO0nWACUrbylAF+BbGwEXdVqM3ZoEOxA4= +github.com/patrickhuber/go-xplat v0.2.16 h1:Km8ErsoTq3lbC5Gm65JxYW/sl290jul1+4fLuPp8QeE= +github.com/patrickhuber/go-xplat v0.2.16/go.mod h1:ZveaAmQiWcZO0nWACUrbylAF+BbGwEXdVqM3ZoEOxA4= +github.com/patrickhuber/go-xplat v0.3.0 h1:Q7lFw5HqkljVX4+V4QdyI9T4MfoDcx0ZVF+qhKP4fpM= +github.com/patrickhuber/go-xplat v0.3.0/go.mod h1:ZveaAmQiWcZO0nWACUrbylAF+BbGwEXdVqM3ZoEOxA4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= diff --git a/internal/cast/models.go b/internal/cast/models.go deleted file mode 100644 index f973130..0000000 --- a/internal/cast/models.go +++ /dev/null @@ -1,10 +0,0 @@ -package cast - -import "github.com/patrickhuber/caster/internal/models" - -// Request is the request object for casting a template -type Request struct { - Template string - Target string - Variables []models.Variable -} diff --git a/internal/cast/service.go b/internal/cast/service.go index 8a022df..80b208f 100644 --- a/internal/cast/service.go +++ b/internal/cast/service.go @@ -1,15 +1,19 @@ package cast import ( - "fmt" - "strings" - "github.com/patrickhuber/caster/internal/interpolate" "github.com/patrickhuber/caster/internal/models" "github.com/patrickhuber/go-xplat/filepath" afs "github.com/patrickhuber/go-xplat/fs" ) +// Request is the request object for casting a template +type Request struct { + Template string + Target string + Variables []models.Variable +} + // Service handles casting of a template type Service interface { Cast(req *Request) error @@ -50,10 +54,15 @@ func (s *service) Cast(req *Request) error { return err } - targetIsSpecified := len(strings.TrimSpace(req.Target)) != 0 + // if no target directory specified, use the local directory + if len(req.Target) == 0 { + req.Target = "." + } - if !targetIsSpecified { - return fmt.Errorf("target must be specified") + // resolve relative paths + req.Target, err = s.path.Abs(req.Target) + if err != nil { + return err } source := s.path.Dir(resp.SourceFile) diff --git a/internal/cast/service_test.go b/internal/cast/service_test.go index 4c50f56..a58e639 100644 --- a/internal/cast/service_test.go +++ b/internal/cast/service_test.go @@ -2,62 +2,19 @@ package cast_test import ( "strings" + "testing" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" "github.com/patrickhuber/caster/internal/cast" "github.com/patrickhuber/caster/internal/interpolate" "github.com/patrickhuber/caster/internal/models" - "gopkg.in/yaml.v3" + "github.com/stretchr/testify/require" - "github.com/patrickhuber/go-xplat/env" - "github.com/patrickhuber/go-xplat/filepath" - afs "github.com/patrickhuber/go-xplat/fs" - "github.com/patrickhuber/go-xplat/os" + "github.com/patrickhuber/go-xplat/arch" + "github.com/patrickhuber/go-xplat/host" + "github.com/patrickhuber/go-xplat/platform" ) -type ServiceTest interface { - Setup(template *models.Caster, request *cast.Request) - SetupString(content string, request *cast.Request) - SetupBytes(content []byte, request *cast.Request) - AssertExists(path string) - AssertContents(path, content string) - FileSystem() afs.FS - Environment() env.Environment -} - -type serviceTest struct { - fs afs.FS - path *filepath.Processor - env env.Environment - inter interpolate.Service -} - -func NewServiceTest() ServiceTest { - o := os.NewLinuxMock() - path := filepath.NewProcessorWithOS(o) - fs := afs.NewMemory(afs.WithProcessor(path)) - e := env.NewMemory() - - return &serviceTest{ - fs: fs, - env: e, - inter: interpolate.NewService(fs, e, path), - path: path, - } -} - -func (t *serviceTest) Setup(template *models.Caster, request *cast.Request) { - content, err := yaml.Marshal(template) - Expect(err).To(BeNil()) - t.SetupBytes(content, request) -} - -func (t *serviceTest) SetupString(content string, request *cast.Request) { - t.SetupBytes([]byte(content), request) -} - -func (t *serviceTest) SetupBytes(content []byte, request *cast.Request) { +func Setup(t *testing.T, h *host.Host, content []byte, inter interpolate.Service, request *cast.Request) { // a template is either a path to a file or directory // we need to take the path and determine its type and generate the appropriate // test file system @@ -69,258 +26,208 @@ func (t *serviceTest) SetupBytes(content []byte, request *cast.Request) { } // a file will have an extension - isFile := len(strings.TrimSpace(t.path.Ext(template))) > 0 + isFile := len(strings.TrimSpace(h.Path.Ext(template))) > 0 if !isFile { // this is a directory so we need to append the default file to the directory - template = t.path.Join(template, ".caster.yml") + template = h.Path.Join(template, ".caster.yml") } - err := t.fs.Mkdir("/", 0600) - Expect(err).To(BeNil()) + err := h.FS.MkdirAll("/output", 0600) + require.NoError(t, err) - err = t.fs.Mkdir("/template", 0600) - Expect(err).To(BeNil()) + err = h.FS.MkdirAll("/template", 0600) + require.NoError(t, err) - err = t.fs.WriteFile(template, content, 0600) - Expect(err).To(BeNil()) + err = h.FS.WriteFile(template, content, 0600) + require.NoError(t, err) - source := t.path.Dir(template) - sourceInfo, err := t.fs.Stat(source) - Expect(err).To(BeNil()) - Expect(sourceInfo.IsDir()).To(BeTrue()) + source := h.Path.Dir(template) + sourceInfo, err := h.FS.Stat(source) + require.NoError(t, err) + require.True(t, sourceInfo.IsDir()) - svc := cast.NewService(t.fs, t.inter, t.path) + svc := cast.NewService(h.FS, inter, h.Path) err = svc.Cast(request) - Expect(err).To(BeNil()) - - t.AssertExists(request.Target) -} - -func (t *serviceTest) AssertExists(path string) { - ok, err := t.fs.Exists(path) - Expect(err).To(BeNil()) - Expect(ok).To(BeTrue(), "expected '%s' to exist", path) -} + require.NoError(t, err) -func (t *serviceTest) AssertContents(path, content string) { - data, err := t.fs.ReadFile(path) - Expect(err).To(BeNil()) - Expect(string(data)).To(Equal(content)) + AssertExists(t, h, request.Target) } -func (t *serviceTest) FileSystem() afs.FS { - return t.fs +func AssertExists(t *testing.T, h *host.Host, path string) { + ok, err := h.FS.Exists(path) + require.NoError(t, err) + require.True(t, ok, "expected '%s' to exist", path) } -func (t *serviceTest) Environment() env.Environment { - return t.env +func AssertContents(t *testing.T, h *host.Host, path, content string) { + data, err := h.FS.ReadFile(path) + require.NoError(t, err) + require.Equal(t, content, string(data)) } -var _ = Describe("Service", func() { - Describe("Cast", func() { - When("caster file specified", func() { - It("applies from specified file", func() { - template := &models.Caster{ - Files: []models.File{ - { - Name: "test.yml", - Content: "test: test", - }, - }, - Folders: []models.Folder{ - { - Name: "sub", - Files: []models.File{ - { - Name: "test.yml", - }, - }, - }, - }, - } - t := NewServiceTest() - t.Setup(template, &cast.Request{ - Template: "/template/custom.yml", - Target: "/output", - }) - - t.AssertExists("/output/test.yml") - t.AssertContents("/output/test.yml", "test: test") - t.AssertExists("/output/sub") - t.AssertExists("/output/sub/test.yml") - }) - }) - It("writes plain files to target", func() { - template := &models.Caster{ - Files: []models.File{ - { - Name: "test.yml", - Content: "test: test", - }, - }, - Folders: []models.Folder{ - { - Name: "sub", - Files: []models.File{ - { - Name: "test.yml", - }, - }, - }, - }, - } - t := NewServiceTest() - t.Setup(template, &cast.Request{ - Template: "/template", - Target: "/output"}) - t.AssertExists("/output/test.yml") - t.AssertContents("/output/test.yml", "test: test") - t.AssertExists("/output/sub") - t.AssertExists("/output/sub/test.yml") - }) - It("evaluates file names", func() { - template := `--- +func TestService(t *testing.T) { + type file struct { + path string + content string + dir bool + } + type test struct { + name string + template string + files []file + request *cast.Request + stage []file + } + tests := []test{ + { + "apply_file", `files: +- name: test.yml + content: "test: test" +folders: +- name: sub + files: + - name: test.yml`, + []file{ + {"/output", "", true}, + {"/output/test.yml", "test: test", false}, + {"/output", "", true}, + {"/output/sub/test.yml", "", false}, + }, &cast.Request{ + Template: "/template/custom.yml", + Target: "/output", + }, []file{}, + }, + { + "replaces_file_names", + `--- files: - name: {{"hello"}}{{"world"}}.yml - content: "hello: world"` - - t := NewServiceTest() - t.SetupString(template, &cast.Request{Template: "/template", Target: "/output"}) - t.AssertExists("/output") - t.AssertExists("/output/helloworld.yml") - t.AssertContents("/output/helloworld.yml", "hello: world") - }) - It("evaluates folder names", func() { - template := `--- + content: "hello: world"`, + []file{ + {"/output", "", true}, + {"/output/helloworld.yml", "hello: world", false}, + }, + &cast.Request{Template: "/template", Target: "/output"}, + []file{}, + }, + { + "replaces_folder_names", + `--- folders: - name: {{"hello"}} files: - name: 1.yml - content: "one: 1"` - - t := NewServiceTest() - t.SetupString(template, &cast.Request{Template: "/template", Target: "/output"}) - t.AssertExists("/output/hello") - t.AssertExists("/output/hello/1.yml") - t.AssertContents("/output/hello/1.yml", "one: 1") - }) - It("writes ref", func() { - template := `--- + content: "one: 1"`, + []file{ + {"/output/hello", "", true}, + {"/output/hello/1.yml", "one: 1", false}, + }, + &cast.Request{Template: "/template", Target: "/output"}, + []file{}, + }, + { + "ref", + `--- files: - name: test - ref: test.txt` - t := NewServiceTest() - - err := t.FileSystem().WriteFile("/template/test.txt", []byte("test"), 0644) - Expect(err).To(BeNil()) - - t.SetupString(template, &cast.Request{Template: "/template", Target: "/output"}) - t.AssertExists("/output/test") - t.AssertContents("/output/test", "test") - }) - It("interpolates content", func() { - template := `--- + ref: test.txt`, + []file{ + {"/output/hello", "", true}, + {"/output/hello/1.yml", "one: 1", false}, + }, + &cast.Request{Template: "/template", Target: "/output"}, + []file{{"/template/test.txt", "test", false}}, + }, + { + "content", + `--- files: - name: test - content: {{ templatefile "./test.txt" . }}` - - t := NewServiceTest() - err := t.FileSystem().WriteFile("/template/test.txt", []byte("{{ .key }}"), 0644) - Expect(err).To(BeNil()) - - t.SetupString(template, &cast.Request{ + content: {{ templatefile "./test.txt" . }}`, + []file{ + {"/output/test", "value", true}, + }, + &cast.Request{ Template: "/template", Target: "/output", Variables: []models.Variable{ {Key: "key", Value: "value"}, }, - }) - t.AssertExists("/output/test") - t.AssertContents("/output/test", "value") - }) - It("can indent with multi line string", func() { - template := ` -files: + }, + []file{{"/template/test.txt", "{{ .key }}", false}}, + }, + { + "multi", + `files: - name: test content: | - {{- templatefile "./test.txt" . | nindent 4 }}` - t := NewServiceTest() - err := t.FileSystem().WriteFile("/template/test.txt", []byte("{{ .key }}\n{{ .key }}"), 0644) - Expect(err).To(BeNil()) - - t.SetupString(template, &cast.Request{ + {{- templatefile "./test.txt" . | nindent 4 }}`, + []file{ + {"/output/test", "value\nvalue", true}, + }, + &cast.Request{ Template: "/template", Target: "/output", Variables: []models.Variable{ {Key: "key", Value: "value"}, }, - }) - t.AssertExists("/output/test") - t.AssertContents("/output/test", "value\nvalue") - - }) - It("can accept variable from file", func() { - template := `--- -files: -- name: test.yml - content: {{ .variable }}` - data := "variable: test" - - t := NewServiceTest() - fs := t.FileSystem() - err := fs.WriteFile("/data.yml", []byte(data), 0644) - Expect(err).To(BeNil()) - - t.SetupString(template, &cast.Request{ - Template: "/template", - Target: "/output", - Variables: []models.Variable{ - { - File: "/data.yml", - }, - }}) - - t.AssertExists("/output/test.yml") - t.AssertContents("/output/test.yml", "test") - }) - It("can accept variable from arg", func() { - template := `--- + }, + []file{{"/template/test.txt", "{{ .key }}", false}}, + }, + { + "varfile", + `--- files: - name: test.yml - content: {{ .variable }}` - t := NewServiceTest() - t.SetupString(template, &cast.Request{ + content: {{ .variable }}`, + []file{ + {"/output/test.yml", "test", true}, + }, + &cast.Request{ Template: "/template", Target: "/output", Variables: []models.Variable{ - { - Key: "variable", - Value: "test", - }, - }}) - - t.AssertExists("/output/test.yml") - t.AssertContents("/output/test.yml", "test") - }) - It("can accept variable from env", func() { - template := `--- + {Key: "key", Value: "value"}, + }, + }, + []file{{"/data.yml", "variable: test", false}}, + }, + { + "varfile", + `--- files: - name: test.yml - content: {{ .variable }}` - t := NewServiceTest() - t.Environment().Set("CASTER_VAR_variable", "test") - t.SetupString(template, &cast.Request{ + content: {{ .variable }}`, + []file{ + {"/output/test.yml", "test", true}, + }, + &cast.Request{ Template: "/template", Target: "/output", Variables: []models.Variable{ - { - Env: "CASTER_VAR_variable", - }, - }}) - - t.AssertExists("/output/test.yml") - t.AssertContents("/output/test.yml", "test") + {Key: "variable", Value: "test"}, + }, + }, + []file{}, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + h := host.NewTest(platform.Linux, arch.AMD64) + h.OS.ChangeDirectory("/") + svc := interpolate.NewService(h.FS, h.Env, h.Path) + for _, file := range test.stage { + err := h.FS.WriteFile(file.path, []byte(file.content), 0666) + require.NoError(t, err) + } + Setup(t, h, []byte(test.template), svc, test.request) + for _, file := range test.files { + AssertExists(t, h, file.path) + if !file.dir { + AssertContents(t, h, file.path, file.content) + } + } }) - }) -}) + } +} diff --git a/internal/commands/apply_test.go b/internal/commands/apply_test.go index 0753608..dc92235 100644 --- a/internal/commands/apply_test.go +++ b/internal/commands/apply_test.go @@ -1 +1,151 @@ package commands_test + +import ( + "testing" + + "github.com/patrickhuber/caster/internal/global" + "github.com/stretchr/testify/require" +) + +func TestApply(t *testing.T) { + + t.Run("basic", func(t *testing.T) { + cx := SetupTestContext(t) + err := cx.fs.WriteFile("/template/.caster.yml", []byte("files:\n- name: test.txt\n"), 0600) + require.NoError(t, err) + + args := []string{"caster", "apply", "-t", "/template"} + cx.app.Metadata[global.OSArgs] = args + + err = cx.app.Run(args) + require.NoError(t, err) + + ok, err := cx.fs.Exists("/working/test.txt") + require.NoError(t, err) + require.True(t, ok) + }) + + t.Run("env", func(t *testing.T) { + template := `files: + - name: test.txt + content: {{ .key }} +` + cx := SetupTestContext(t) + err := cx.fs.WriteFile("/template/.caster.yml", []byte(template), 0600) + require.NoError(t, err) + + cx.env.Set("CASTER_VAR_key", "value") + + args := []string{"caster", "apply", "-t", "/template"} + cx.app.Metadata[global.OSArgs] = args + + err = cx.app.Run(args) + require.NoError(t, err) + + ok, err := cx.fs.Exists("/working/test.txt") + require.NoError(t, err) + require.True(t, ok) + + content, err := cx.fs.ReadFile("/working/test.txt") + require.NoError(t, err) + require.Equal(t, []byte("value"), content) + }) + t.Run("multi_data", func(t *testing.T) { + cx := SetupTestContext(t) + cx.fs.WriteFile("/template/.caster.yml", []byte("files:\n- name: test.txt\n content: {{.first}}{{.second}}"), 0600) + cx.fs.WriteFile("/data/1.yml", []byte("first: first"), 0600) + cx.fs.WriteFile("/data/2.yml", []byte("second: second"), 0600) + + args := []string{"caster", "apply", "--var-file", "/data/1.yml", "--var-file", "/data/2.yml", "-t", "/template"} + cx.app.Metadata[global.OSArgs] = args + err := cx.app.Run(args) + require.NoError(t, err) + + ok, err := cx.fs.Exists("/working/test.txt") + require.NoError(t, err) + require.True(t, ok) + + want := `firstsecond` + content, err := cx.fs.ReadFile("/working/test.txt") + require.NoError(t, err) + require.Equal(t, []byte(want), content) + }) + + t.Run("multi_arg", func(t *testing.T) { + cx := SetupTestContext(t) + cx.fs.WriteFile("/template/.caster.yml", []byte("files:\n- name: test.txt\n content: {{.first}}{{.second}}"), 0600) + + args := []string{"caster", "apply", "--var", "first=first", "--var", "second=second", "-t", "/template"} + cx.app.Metadata[global.OSArgs] = args + err := cx.app.Run(args) + require.NoError(t, err) + + ok, err := cx.fs.Exists("/working/test.txt") + require.NoError(t, err) + require.True(t, ok) + + want := `firstsecond` + content, err := cx.fs.ReadFile("/working/test.txt") + require.NoError(t, err) + require.Equal(t, []byte(want), content) + }) + t.Run("mixed_arg", func(t *testing.T) { + cx := SetupTestContext(t) + cx.fs.WriteFile("/template/.caster.yml", []byte("files:\n- name: test.txt\n content: {{.key}}"), 0600) + cx.fs.WriteFile("/data/1.yml", []byte("key: first"), 0600) + + args := []string{"caster", "apply", "--var-file", "/data/1.yml", "--var", "key=second", "-t", "/template"} + cx.app.Metadata[global.OSArgs] = args + err := cx.app.Run(args) + require.NoError(t, err) + + ok, err := cx.fs.Exists("/working/test.txt") + require.NoError(t, err) + require.True(t, ok) + + want := `second` + content, err := cx.fs.ReadFile("/working/test.txt") + require.NoError(t, err) + require.Equal(t, []byte(want), content) + }) + t.Run("override", func(t *testing.T) { + + cx := SetupTestContext(t) + cx.fs.WriteFile("/template/.caster.yml", []byte("files:\n- name: test.txt\n content: {{.key}}"), 0600) + cx.fs.WriteFile("/data/1.yml", []byte("key: second"), 0600) + + args := []string{"caster", "apply", "--var", "key=first", "--var-file", "/data/1.yml", "-t", "/template"} + cx.app.Metadata[global.OSArgs] = args + err := cx.app.Run(args) + require.NoError(t, err) + + ok, err := cx.fs.Exists("/working/test.txt") + require.NoError(t, err) + require.True(t, ok) + + want := `second` + content, err := cx.fs.ReadFile("/working/test.txt") + require.NoError(t, err) + require.Equal(t, []byte(want), content) + }) + + t.Run("default", func(t *testing.T) { + cx := SetupTestContext(t) + cx.fs.WriteFile("/working/.caster.yml", []byte("files:\n- name: test.txt\n content: {{.key}}"), 0600) + cx.fs.WriteFile("/data/1.yml", []byte("key: second"), 0600) + + args := []string{"caster", "apply", "--var", "key=first", "--var-file", "/data/1.yml"} + cx.app.Metadata[global.OSArgs] = args + err := cx.app.Run(args) + require.NoError(t, err) + + ok, err := cx.fs.Exists("/working/test.txt") + require.NoError(t, err) + require.True(t, ok) + + want := `second` + content, err := cx.fs.ReadFile("/working/test.txt") + require.NoError(t, err) + require.Equal(t, []byte(want), content) + }) +} diff --git a/internal/initialize/service_test.go b/internal/initialize/service_test.go index 049d743..22456ae 100644 --- a/internal/initialize/service_test.go +++ b/internal/initialize/service_test.go @@ -7,11 +7,12 @@ import ( "github.com/patrickhuber/go-xplat/filepath" "github.com/patrickhuber/go-xplat/fs" "github.com/patrickhuber/go-xplat/os" + "github.com/patrickhuber/go-xplat/platform" "github.com/stretchr/testify/require" ) func TestService(t *testing.T) { - o := os.NewLinuxMock() + o := os.NewMock(os.WithPlatform(platform.Linux)) path := filepath.NewProcessorWithOS(o) fs := fs.NewMemory(fs.WithProcessor(path)) diff --git a/internal/interpolate/service_test.go b/internal/interpolate/service_test.go index 3169e4c..e551c89 100644 --- a/internal/interpolate/service_test.go +++ b/internal/interpolate/service_test.go @@ -11,6 +11,7 @@ import ( "github.com/patrickhuber/go-xplat/filepath" afs "github.com/patrickhuber/go-xplat/fs" "github.com/patrickhuber/go-xplat/os" + "github.com/patrickhuber/go-xplat/platform" ) type ServiceTestContext struct { @@ -74,7 +75,7 @@ files: } func CreateServiceTestContext(t *testing.T) *ServiceTestContext { - o := os.NewLinuxMock() + o := os.NewMock(os.WithPlatform(platform.Linux)) path := filepath.NewProcessorWithOS(o) fs := afs.NewMemory(afs.WithProcessor(path)) require.NoError(t, fs.Mkdir("/", 0600)) diff --git a/internal/setup/test.go b/internal/setup/test.go index 555daff..b51c1b7 100644 --- a/internal/setup/test.go +++ b/internal/setup/test.go @@ -10,13 +10,14 @@ import ( "github.com/patrickhuber/go-xplat/filepath" "github.com/patrickhuber/go-xplat/fs" "github.com/patrickhuber/go-xplat/os" + "github.com/patrickhuber/go-xplat/platform" ) func NewTest() Setup { container := di.NewContainer() container.RegisterConstructor(env.NewMemory) container.RegisterConstructor(func() os.OS { - return os.NewLinuxMock() + return os.NewMock(os.WithPlatform(platform.Linux)) }) container.RegisterConstructor(func(processor *filepath.Processor) fs.FS { // options cause issues with constructor registration