Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions cmd/bundle/init.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package bundle

import (
"encoding/json"
"os"
"path/filepath"

"github.com/databricks/cli/libs/git"
"github.com/databricks/cli/libs/template"
"github.com/spf13/cobra"
)

var initCmd = &cobra.Command{
Use: "init TEMPLATE_PATH",
Short: "Initialize Template",
Long: `Initialize template`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
templateURL := args[0]
tmpDir := os.TempDir()
templateDir := filepath.Join(tmpDir, templateURL)
ctx := cmd.Context()

err := os.MkdirAll(templateDir, 0755)
if err != nil {
return err
}

// TODO: should we delete this directory once we are done with it?
// It's a destructive action that can be risky
err = git.Clone(ctx, templateURL, "", templateDir)
if err != nil {
return err
}

// TODO: substitute to a read config method that respects the schema
// and prompts for input variables
b, err := os.ReadFile(configFile)
if err != nil {
return err
}
config := make(map[string]any)
err = json.Unmarshal(b, &config)
if err != nil {
return err
}
return template.Materialize(ctx, config, templateDir, projectDir)
},
}

var configFile string
var projectDir string

func init() {
initCmd.Flags().StringVar(&configFile, "config-file", "", "Input parameters for template initialization.")
initCmd.Flags().StringVar(&projectDir, "project-dir", "", "The project will be initialized in this directory.")
initCmd.MarkFlagRequired("config-file")
initCmd.MarkFlagRequired("output-dir")
AddCommand(initCmd)
}
96 changes: 96 additions & 0 deletions libs/template/file.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package template

import (
"context"
"io"
"io/fs"
"os"
"path/filepath"

"github.com/databricks/cli/libs/filer"
)

// Interface for an in memory representation of a file
type file interface {
// Full path of the file, in the os native format. For example /foo/bar on
// Unix and C:\foo\bar on windows
Path() string

// Unix like file path relative to the "root" of the instantiated project. Is used to
// evaluate whether the file should be skipped by comparing it to a list of
// skip glob patterns
RelPath() string

// This function writes this file onto the disk
PersistToDisk() error
}

type fileCommon struct {
// Root path for the project instance. This path uses the system's default
// file separator. For example /foo/bar on Unix and C:\foo\bar on windows
root string

// Unix like relPath for the file (using '/' as the separator). This path
// is relative to the root. Using unix like relative paths enables skip patterns
// to work across both windows and unix based operating systems.
relPath string

// Permissions bits for the file
perm fs.FileMode
}

func (f *fileCommon) Path() string {
return filepath.Join(f.root, filepath.FromSlash(f.relPath))
}

func (f *fileCommon) RelPath() string {
return f.relPath
}

type copyFile struct {
*fileCommon

ctx context.Context

// Path of the source file that should be copied over.
srcPath string

// Filer to use to read source path
srcFiler filer.Filer
}

func (f *copyFile) PersistToDisk() error {
path := f.Path()
err := os.MkdirAll(filepath.Dir(path), 0755)
if err != nil {
return err
}
srcFile, err := f.srcFiler.Read(f.ctx, f.srcPath)
if err != nil {
return err
}
defer srcFile.Close()
dstFile, err := os.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_WRONLY, f.perm)
if err != nil {
return err
}
defer dstFile.Close()
_, err = io.Copy(dstFile, srcFile)
return err
}

type inMemoryFile struct {
*fileCommon

content []byte
}

func (f *inMemoryFile) PersistToDisk() error {
path := f.Path()

err := os.MkdirAll(filepath.Dir(path), 0755)
if err != nil {
return err
}
return os.WriteFile(path, f.content, f.perm)
}
137 changes: 137 additions & 0 deletions libs/template/file_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package template

import (
"context"
"os"
"path/filepath"
"runtime"
"testing"

"github.com/databricks/cli/libs/filer"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestTemplateFileCommonPathForWindows(t *testing.T) {
if runtime.GOOS != "windows" {
t.SkipNow()
}
f := &fileCommon{
root: `c:\a\b\c`,
relPath: "d/e",
}
assert.Equal(t, `c:\a\b\c\d\e`, f.Path())
assert.Equal(t, `d/e`, f.RelPath())
}

func TestTemplateFileCommonPathForUnix(t *testing.T) {
if runtime.GOOS != "linux" && runtime.GOOS != "darwin" {
t.SkipNow()
}
f := &fileCommon{
root: `a/b/c`,
relPath: "d/e",
}
assert.Equal(t, `a/b/c/d/e`, f.Path())
assert.Equal(t, `d/e`, f.RelPath())
}

func TestTemplateFileInMemoryFilePersistToDisk(t *testing.T) {
if runtime.GOOS != "linux" && runtime.GOOS != "darwin" {
t.SkipNow()
}
tmpDir := t.TempDir()

f := &inMemoryFile{
fileCommon: &fileCommon{
root: tmpDir,
relPath: "a/b/c",
perm: 0755,
},
content: []byte("123"),
}
err := f.PersistToDisk()
assert.NoError(t, err)

assertFileContent(t, filepath.Join(tmpDir, "a/b/c"), "123")
assertFilePermissions(t, filepath.Join(tmpDir, "a/b/c"), 0755)
}

func TestTemplateFileCopyFilePersistToDisk(t *testing.T) {
if runtime.GOOS != "linux" && runtime.GOOS != "darwin" {
t.SkipNow()
}
tmpDir := t.TempDir()

templateFiler, err := filer.NewLocalClient(tmpDir)
require.NoError(t, err)
os.WriteFile(filepath.Join(tmpDir, "source"), []byte("qwerty"), 0644)

f := &copyFile{
ctx: context.Background(),
fileCommon: &fileCommon{
root: tmpDir,
relPath: "a/b/c",
perm: 0644,
},
srcPath: "source",
srcFiler: templateFiler,
}
err = f.PersistToDisk()
assert.NoError(t, err)

assertFileContent(t, filepath.Join(tmpDir, "a/b/c"), "qwerty")
assertFilePermissions(t, filepath.Join(tmpDir, "a/b/c"), 0644)
}

// we have separate tests for windows because of differences in valid
// fs.FileMode values we can use for different operating systems.
func TestTemplateFileInMemoryFilePersistToDiskForWindows(t *testing.T) {
if runtime.GOOS != "windows" {
t.SkipNow()
}
tmpDir := t.TempDir()

f := &inMemoryFile{
fileCommon: &fileCommon{
root: tmpDir,
relPath: "a/b/c",
perm: 0666,
},
content: []byte("123"),
}
err := f.PersistToDisk()
assert.NoError(t, err)

assertFileContent(t, filepath.Join(tmpDir, "a/b/c"), "123")
assertFilePermissions(t, filepath.Join(tmpDir, "a/b/c"), 0666)
}

// we have separate tests for windows because of differences in valid
// fs.FileMode values we can use for different operating systems.
func TestTemplateFileCopyFilePersistToDiskForWindows(t *testing.T) {
if runtime.GOOS != "windows" {
t.SkipNow()
}
tmpDir := t.TempDir()

templateFiler, err := filer.NewLocalClient(tmpDir)
require.NoError(t, err)
os.WriteFile(filepath.Join(tmpDir, "source"), []byte("qwerty"), 0666)

f := &copyFile{
ctx: context.Background(),
fileCommon: &fileCommon{
root: tmpDir,
relPath: "a/b/c",
perm: 0666,
},
srcPath: "source",
srcFiler: templateFiler,
}
err = f.PersistToDisk()
assert.NoError(t, err)

assertFileContent(t, filepath.Join(tmpDir, "a/b/c"), "qwerty")
assertFilePermissions(t, filepath.Join(tmpDir, "a/b/c"), 0666)
}
4 changes: 2 additions & 2 deletions libs/template/helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ func TestTemplatePrintStringWithoutProcessing(t *testing.T) {
assert.NoError(t, err)

assert.Len(t, r.files, 1)
cleanContent := strings.Trim(string(r.files[0].content), "\n\r")
cleanContent := strings.Trim(string(r.files[0].(*inMemoryFile).content), "\n\r")
assert.Equal(t, `{{ fail "abc" }}`, cleanContent)
}

Expand All @@ -35,7 +35,7 @@ func TestTemplateRegexpCompileFunction(t *testing.T) {
assert.NoError(t, err)

assert.Len(t, r.files, 1)
content := string(r.files[0].content)
content := string(r.files[0].(*inMemoryFile).content)
assert.Contains(t, content, "0:food")
assert.Contains(t, content, "1:fool")
}
24 changes: 24 additions & 0 deletions libs/template/materialize.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package template

import (
"context"
"path/filepath"
)

const libraryDirName = "library"
const templateDirName = "template"

func Materialize(ctx context.Context, config map[string]any, templateRoot, instanceRoot string) error {
templatePath := filepath.Join(templateRoot, templateDirName)
libraryPath := filepath.Join(templateRoot, libraryDirName)

r, err := newRenderer(ctx, config, templatePath, libraryPath, instanceRoot)
if err != nil {
return err
}
err = r.walk()
if err != nil {
return err
}
return r.persistToDisk()
}
Loading