mirror of https://github.com/databricks/cli.git
First draft
This commit is contained in:
parent
1a431dd26c
commit
9ef3547a3a
|
@ -1,21 +0,0 @@
|
|||
{
|
||||
"foo": {
|
||||
"type": "string",
|
||||
"default": "abc",
|
||||
"validation": ["regex ^[abcd]*$"]
|
||||
},
|
||||
"bar": {
|
||||
"type": "integer",
|
||||
"default": 123,
|
||||
"validation": ["greaterThan 5", "lessThan 10"]
|
||||
},
|
||||
"isAws": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"project_name": {
|
||||
"type": "string",
|
||||
"default": "my_project",
|
||||
"validation": ["startsWith my_"]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
package init
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
// Executes the template by appling config on it. Returns the materialized config
|
||||
// as a string
|
||||
func executeTemplate(config map[string]any, templateDefination string) (string, error) {
|
||||
// configure template with helper functions
|
||||
tmpl, err := template.New("foo").Funcs(HelperFuncs).Parse(templateDefination)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// execute template
|
||||
result := strings.Builder{}
|
||||
err = tmpl.Execute(&result, config)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return result.String(), nil
|
||||
}
|
||||
|
||||
// TODO: allow skipping directories
|
||||
func generateDirectory(config map[string]any, parentDir, nameTempate string) (string, error) {
|
||||
dirName, err := executeTemplate(config, nameTempate)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
err = os.Mkdir(filepath.Join(parentDir, dirName), 0755)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return dirName, nil
|
||||
}
|
||||
|
||||
func generateFile(config map[string]any, parentDir, nameTempate, contentTemplate string) error {
|
||||
// compute file content
|
||||
fileContent, err := executeTemplate(config, contentTemplate)
|
||||
// TODO: maybe we need string matching here to make this work
|
||||
if err != nil && err == ErrSkipThisFile {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// create the file by executing the templatized file name
|
||||
fileName, err := executeTemplate(config, nameTempate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f, err := os.Create(filepath.Join(parentDir, fileName))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// write to file the computed content
|
||||
_, err = f.Write([]byte(fileContent))
|
||||
return err
|
||||
}
|
||||
|
||||
func walkFileTree(config map[string]any, templatePath, instancePath string) error {
|
||||
enteries, err := os.ReadDir(templatePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, entry := range enteries {
|
||||
if entry.IsDir() {
|
||||
// case: materialize a template directory
|
||||
dirName, err := generateDirectory(config, instancePath, entry.Name())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// recusive generate files and directories inside inside our newly generated
|
||||
// directory from the template defination
|
||||
err = walkFileTree(config, filepath.Join(templatePath, entry.Name()), filepath.Join(instancePath, dirName))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// case: materialize a template file with it's contents
|
||||
b, err := os.ReadFile(filepath.Join(templatePath, entry.Name()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
contentTemplate := string(b)
|
||||
fileNameTemplate := entry.Name()
|
||||
err = generateFile(config, instancePath, fileNameTemplate, contentTemplate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -1,74 +0,0 @@
|
|||
package init
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
// TODO: cleanup if template initialization fails
|
||||
// TODO: need robust way to clean up half generated files
|
||||
// TODO: define default files
|
||||
// TODO: self reference for
|
||||
|
||||
func walkFileTree(config map[string]interface{}, templatePath string, instancePath string) error {
|
||||
enteries, err := os.ReadDir(templatePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, entry := range enteries {
|
||||
if entry.Name() == SchemaFileName {
|
||||
continue
|
||||
}
|
||||
fileName := entry.Name()
|
||||
tmpl, err := template.New("filename").Parse(fileName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
result := strings.Builder{}
|
||||
err = tmpl.Execute(&result, config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resolvedFileName := result.String()
|
||||
fmt.Println(resolvedFileName)
|
||||
if entry.IsDir() {
|
||||
err := os.Mkdir(resolvedFileName, os.ModePerm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = walkFileTree(config, filepath.Join(templatePath, fileName), filepath.Join(instancePath, resolvedFileName))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
f, err := os.Create(filepath.Join(instancePath, resolvedFileName))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b, err := os.ReadFile(filepath.Join(templatePath, fileName))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// TODO: Might be able to use ParseFiles or ParseFS. Might be more suited
|
||||
contentTmpl, err := template.New("content").Funcs(HelperFuncs).Parse(string(b))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = contentTmpl.Execute(f, config)
|
||||
|
||||
// Make this assertion more robust
|
||||
if err != nil && strings.Contains(err.Error(), ErrSkipThisFile.Error()) {
|
||||
err := os.Remove(filepath.Join(instancePath, resolvedFileName))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -2,8 +2,6 @@ package init
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
|
@ -13,19 +11,4 @@ var HelperFuncs = template.FuncMap{
|
|||
"skipThisFile": func() error {
|
||||
panic(ErrSkipThisFile)
|
||||
},
|
||||
"eqString": func(a string, b string) bool {
|
||||
return a == b
|
||||
},
|
||||
"eqNumber": func(a float64, b int) bool {
|
||||
return int(a) == b
|
||||
},
|
||||
"validationError": func(message string) error {
|
||||
panic(fmt.Errorf(message))
|
||||
},
|
||||
"assertStartsWith": func(s string, substr string) error {
|
||||
if !strings.HasPrefix(s, substr) {
|
||||
panic(fmt.Errorf("%s does not start with %s.", s, substr))
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
|
|
@ -3,24 +3,38 @@ package init
|
|||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/databricks/bricks/cmd/root"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const SchemaFileName = "config.json"
|
||||
const ConfigFileName = "config.json"
|
||||
const SchemaFileName = "schema.json"
|
||||
const TemplateDirname = "template"
|
||||
|
||||
// root template defination at schema.json
|
||||
// decide on semantics of defination later
|
||||
|
||||
// initCmd represents the fs command
|
||||
var initCmd = &cobra.Command{
|
||||
Use: "init",
|
||||
Short: "Initialize Template",
|
||||
Long: `Initialize bundle template`,
|
||||
Long: `Initialize template`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
templateLocation := args[0]
|
||||
|
||||
// read the file containing schema for template input parameters
|
||||
schemaBytes, err := os.ReadFile(filepath.Join(templateLocation, SchemaFileName))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
schema := Schema{}
|
||||
err = json.Unmarshal(schemaBytes, &schema)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// read user config to initalize the template with
|
||||
var config map[string]interface{}
|
||||
b, err := os.ReadFile(SchemaFileName)
|
||||
b, err := os.ReadFile(ConfigFileName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -28,15 +42,22 @@ var initCmd = &cobra.Command{
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = walkFileTree(config, ".", ".")
|
||||
|
||||
// cast any fields that are supported to be integers. The json unmarshalling
|
||||
// for a generic map converts all numbers to floating point
|
||||
err = schema.CastFloatToInt(config)
|
||||
if err != nil {
|
||||
err2 := os.RemoveAll("favela")
|
||||
if err2 != nil {
|
||||
return err2
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
|
||||
// validate config according to schema
|
||||
err = schema.ValidateConfig(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// materialize the template
|
||||
return walkFileTree(config, filepath.Join(args[0], TemplateDirname), ".")
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,102 @@
|
|||
package init
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
type Schema map[string]FieldInfo
|
||||
|
||||
type FieldType string
|
||||
|
||||
const (
|
||||
FieldTypeString = FieldType("string")
|
||||
FieldTypeInt = FieldType("integer")
|
||||
FieldTypeFloat = FieldType("float")
|
||||
FieldTypeBoolean = FieldType("boolean")
|
||||
)
|
||||
|
||||
type FieldInfo struct {
|
||||
Type FieldType `json:"type"`
|
||||
Description string `json:"description"`
|
||||
Validation string `json:"validate"`
|
||||
}
|
||||
|
||||
// function to check whether a float value represents an integer
|
||||
func isIntegerValue(v float64) bool {
|
||||
return v == float64(int(v))
|
||||
}
|
||||
|
||||
// cast value to integer for config values that are floats but are supposed to be
|
||||
// integeres according to the schema
|
||||
//
|
||||
// Needed because the default json unmarshaller for maps converts all numbers to floats
|
||||
func (schema Schema) CastFloatToInt(config map[string]any) error {
|
||||
for k, v := range config {
|
||||
// error because all config keys should be defined in schema too
|
||||
if _, ok := schema[k]; !ok {
|
||||
return fmt.Errorf("%s is not defined as an input parameter for the template", k)
|
||||
}
|
||||
|
||||
// skip non integer fields
|
||||
fieldInfo := schema[k]
|
||||
if fieldInfo.Type != FieldTypeInt {
|
||||
continue
|
||||
}
|
||||
|
||||
// convert floating point type values to integer
|
||||
valueType := reflect.TypeOf(v)
|
||||
switch valueType.Kind() {
|
||||
case reflect.Float32:
|
||||
floatVal := v.(float32)
|
||||
if !isIntegerValue(float64(floatVal)) {
|
||||
return fmt.Errorf("expected %s to have integer value but it is %v", k, v)
|
||||
}
|
||||
config[k] = int(floatVal)
|
||||
case reflect.Float64:
|
||||
floatVal := v.(float64)
|
||||
if !isIntegerValue(floatVal) {
|
||||
return fmt.Errorf("expected %s to have integer value but it is %v", k, v)
|
||||
}
|
||||
config[k] = int(floatVal)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateType(v any, fieldType FieldType) error {
|
||||
switch fieldType {
|
||||
case FieldTypeString:
|
||||
if _, ok := v.(string); !ok {
|
||||
return fmt.Errorf("expected type string, but value is %#v", v)
|
||||
}
|
||||
case FieldTypeInt:
|
||||
if _, ok := v.(int); !ok {
|
||||
return fmt.Errorf("expected type integer, but value is %#v", v)
|
||||
}
|
||||
case FieldTypeFloat:
|
||||
if _, ok := v.(float64); !ok {
|
||||
return fmt.Errorf("expected type float, but value is %#v", v)
|
||||
}
|
||||
case FieldTypeBoolean:
|
||||
if _, ok := v.(bool); !ok {
|
||||
return fmt.Errorf("expected type boolean, but value is %#v", v)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO: add validation check for regex for string types
|
||||
func (schema Schema) ValidateConfig(config map[string]any) error {
|
||||
for k, v := range config {
|
||||
fieldMetadata, ok := schema[k]
|
||||
if !ok {
|
||||
return fmt.Errorf("%s is not defined as an input parameter for the template", k)
|
||||
}
|
||||
err := validateType(v, fieldMetadata.Type)
|
||||
if err != nil {
|
||||
return fmt.Errorf("incorrect type for %s. %w", k, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
Loading…
Reference in New Issue