diff --git a/libs/template/materialize_test.go b/libs/template/materialize_test.go index 07fac518..e00d535b 100644 --- a/libs/template/materialize_test.go +++ b/libs/template/materialize_test.go @@ -4,6 +4,7 @@ import ( "io/fs" "os" "path/filepath" + "runtime" "testing" "github.com/stretchr/testify/assert" @@ -24,49 +25,52 @@ func setupConfig(t *testing.T, config string) string { func assertFilePerm(t *testing.T, path string, perm fs.FileMode) { stat, err := os.Stat(path) require.NoError(t, err) - assert.Equal(t, stat.Mode().Perm(), perm) + assert.Equal(t, perm, stat.Mode().Perm()) } func TestMaterializeEmptyDirsAreNotGenerated(t *testing.T) { tmp := setupConfig(t, ` { - "a": "dir-with-file", - "b": "foo", - "c": "dir-with-skipped-file", - "d": "skipping" + "a": "this directory is created because it contains a file", + "b": "this variable is not used anywhere", + "c": "this directory will be skipped if d=foo", + "d": "foo" }`) err := Materialize("./testdata/skip_dir", tmp, filepath.Join(tmp, "config.json")) require.NoError(t, err) - assert.DirExists(t, filepath.Join(tmp, "dir-with-file")) - assert.FileExists(t, filepath.Join(tmp, "dir-with-file/.gitkeep")) - assert.NoDirExists(t, filepath.Join(tmp, "empty-dir")) - assert.NoDirExists(t, filepath.Join(tmp, "dir-with-skipped-file")) + assert.DirExists(t, filepath.Join(tmp, "this directory is created because it contains a file")) + assert.FileExists(t, filepath.Join(tmp, "this directory is created because it contains a file/.gitkeep")) + assert.NoDirExists(t, filepath.Join(tmp, "this directory will be skipped if d=foo")) tmp2 := setupConfig(t, ` { - "a": "dir-with-file", - "b": "foo", - "c": "dir-not-skipped-this-time", - "d": "not-skipping" + "a": "this directory is created because it contains a file", + "b": "this variable is not used anywhere", + "c": "this directory will be skipped if d=foo", + "d": "bar" }`) err = Materialize("./testdata/skip_dir", tmp2, filepath.Join(tmp2, "config.json")) require.NoError(t, err) - assert.DirExists(t, filepath.Join(tmp2, "dir-with-file")) - assert.FileExists(t, filepath.Join(tmp2, "dir-with-file/.gitkeep")) - assert.DirExists(t, filepath.Join(tmp2, "dir-not-skipped-this-time")) - assert.FileExists(t, filepath.Join(tmp2, "dir-not-skipped-this-time/foo")) + assert.DirExists(t, filepath.Join(tmp2, "this directory is created because it contains a file")) + assert.FileExists(t, filepath.Join(tmp2, "this directory is created because it contains a file/.gitkeep")) + assert.DirExists(t, filepath.Join(tmp2, "this directory will be skipped if d=foo")) + assert.FileExists(t, filepath.Join(tmp2, "this directory will be skipped if d=foo/abc")) } -func TestMaterializedTemplatesHaveIdenticalFilePermissionsAsTemplate(t *testing.T) { - // create template +func TestMaterializeFilePermissionsAreCopiedForUnix(t *testing.T) { + if runtime.GOOS == "windows" { + t.SkipNow() + } + tmp := t.TempDir() + + // create template schema in temp directory err := os.Mkdir(filepath.Join(tmp, "my_tmpl"), 0777) require.NoError(t, err) err = os.WriteFile(filepath.Join(tmp, "my_tmpl", "schema.json"), []byte(` { - "version": 0, "properties": { "a": { "type": "string" @@ -122,3 +126,61 @@ func TestMaterializedTemplatesHaveIdenticalFilePermissionsAsTemplate(t *testing. assertFilePerm(t, filepath.Join(instanceRoot, "foo"), 0500) assertFilePerm(t, filepath.Join(instanceRoot, "bar"), 0755) } + +func TestMaterializeFilePermissionsAreCopiedForWindows(t *testing.T) { + if runtime.GOOS != "windows" { + t.SkipNow() + } + + tmp := t.TempDir() + + // create template in temp directory + err := os.Mkdir(filepath.Join(tmp, "my_tmpl"), 0777) + require.NoError(t, err) + err = os.WriteFile(filepath.Join(tmp, "my_tmpl", "schema.json"), []byte(` + { + "properties": { + "a": { + "type": "string" + }, + "b": { + "type": "string" + } + } + }`), 0644) + require.NoError(t, err) + + // A normal file with the executable bit not flipped + err = os.Mkdir(filepath.Join(tmp, "my_tmpl", "template"), 0777) + require.NoError(t, err) + err = os.WriteFile(filepath.Join(tmp, "my_tmpl", "template", "{{.a}}"), []byte("abc"), 0666) + require.NoError(t, err) + + // A read only file + err = os.WriteFile(filepath.Join(tmp, "my_tmpl", "template", "{{.b}}"), []byte("def"), 0444) + require.NoError(t, err) + + // create config.json file + err = os.Mkdir(filepath.Join(tmp, "config"), 0777) + require.NoError(t, err) + configPath := filepath.Join(tmp, "config", "config.json") + err = os.WriteFile(configPath, []byte(` + { + "a": "Amsterdam", + "b": "Hague" + }`), 0644) + require.NoError(t, err) + + // create directory to initialize the template in + instanceRoot := filepath.Join(tmp, "instance") + err = os.Mkdir(instanceRoot, 0777) + require.NoError(t, err) + + // materialize the template + err = Materialize(filepath.Join(tmp, "my_tmpl"), instanceRoot, configPath) + require.NoError(t, err) + + // assert template files have the correct permission bits set + assertFilePerm(t, filepath.Join(instanceRoot, "Amsterdam"), 0600) + assertFilePerm(t, filepath.Join(instanceRoot, "Hague"), 0400) +} diff --git a/libs/template/schema.go b/libs/template/schema.go index 14bacf0e..4edcc0d4 100644 --- a/libs/template/schema.go +++ b/libs/template/schema.go @@ -9,27 +9,25 @@ import ( const LatestSchemaVersion = 0 +// This is a JSON Schema compliant struct that we use to do validation checks on +// the provided configuration type Schema struct { - // A version for the template schema - Version int `json:"version"` - // A list of properties that can be used in the config - Properties map[string]FieldInfo `json:"properties"` + Properties map[string]Property `json:"properties"` } -type FieldType string +type PropertyType string const ( - FieldTypeString = FieldType("string") - FieldTypeInt = FieldType("integer") - FieldTypeFloat = FieldType("float") - FieldTypeBoolean = FieldType("boolean") + PropertyTypeString = PropertyType("string") + PropertyTypeInt = PropertyType("integer") + PropertyTypeNumber = PropertyType("number") + PropertyTypeBoolean = PropertyType("boolean") ) -type FieldInfo struct { - Type FieldType `json:"type"` - Description string `json:"description"` - Validation string `json:"validation"` +type Property struct { + Type PropertyType `json:"type"` + Description string `json:"description"` } // function to check whether a float value represents an integer @@ -38,7 +36,7 @@ func isIntegerValue(v float64) bool { } // cast value to integer for config values that are floats but are supposed to be -// integeres according to the schema +// integers according to the schema // // Needed because the default json unmarshaller for maps converts all numbers to floats func castFloatToInt(config map[string]any, schema *Schema) error { @@ -50,7 +48,7 @@ func castFloatToInt(config map[string]any, schema *Schema) error { // skip non integer fields fieldInfo := schema.Properties[k] - if fieldInfo.Type != FieldTypeInt { + if fieldInfo.Type != PropertyTypeInt { continue } @@ -74,7 +72,7 @@ func castFloatToInt(config map[string]any, schema *Schema) error { return nil } -func validateType(v any, fieldType FieldType) error { +func validateType(v any, fieldType PropertyType) error { validateFunc, ok := validators[fieldType] if !ok { return nil diff --git a/libs/template/schema_test.go b/libs/template/schema_test.go index 41afc0bb..fd9395bf 100644 --- a/libs/template/schema_test.go +++ b/libs/template/schema_test.go @@ -21,13 +21,12 @@ func TestTemplateSchematIsInterger(t *testing.T) { func TestTemplateSchemaCastFloatToInt(t *testing.T) { // define schema for config schemaJson := `{ - "version": 0, "properties": { "int_val": { "type": "integer" }, "float_val": { - "type": "float" + "type": "number" }, "bool_val": { "type": "boolean" @@ -73,7 +72,6 @@ func TestTemplateSchemaCastFloatToInt(t *testing.T) { func TestTemplateSchemaCastFloatToIntFailsForUnknownTypes(t *testing.T) { // define schema for config schemaJson := `{ - "version": 0, "properties": { "foo": { "type": "integer" @@ -99,7 +97,6 @@ func TestTemplateSchemaCastFloatToIntFailsForUnknownTypes(t *testing.T) { func TestTemplateSchemaCastFloatToIntFailsWhenWithNonIntValues(t *testing.T) { // define schema for config schemaJson := `{ - "version": 0, "properties": { "foo": { "type": "integer" @@ -124,70 +121,69 @@ func TestTemplateSchemaCastFloatToIntFailsWhenWithNonIntValues(t *testing.T) { func TestTemplateSchemaValidateType(t *testing.T) { // assert validation passing - err := validateType(int(0), FieldTypeInt) + err := validateType(int(0), PropertyTypeInt) assert.NoError(t, err) - err = validateType(int32(1), FieldTypeInt) + err = validateType(int32(1), PropertyTypeInt) assert.NoError(t, err) - err = validateType(int64(1), FieldTypeInt) + err = validateType(int64(1), PropertyTypeInt) assert.NoError(t, err) - err = validateType(float32(1.1), FieldTypeFloat) + err = validateType(float32(1.1), PropertyTypeNumber) assert.NoError(t, err) - err = validateType(float64(1.2), FieldTypeFloat) + err = validateType(float64(1.2), PropertyTypeNumber) assert.NoError(t, err) - err = validateType(false, FieldTypeBoolean) + err = validateType(false, PropertyTypeBoolean) assert.NoError(t, err) - err = validateType("abc", FieldTypeString) + err = validateType("abc", PropertyTypeString) assert.NoError(t, err) // assert validation failing for integers - err = validateType(float64(1.2), FieldTypeInt) + err = validateType(float64(1.2), PropertyTypeInt) assert.ErrorContains(t, err, "expected type integer, but value is 1.2") - err = validateType(true, FieldTypeInt) + err = validateType(true, PropertyTypeInt) assert.ErrorContains(t, err, "expected type integer, but value is true") - err = validateType("abc", FieldTypeInt) + err = validateType("abc", PropertyTypeInt) assert.ErrorContains(t, err, "expected type integer, but value is \"abc\"") // assert validation failing for floats - err = validateType(int(1), FieldTypeFloat) + err = validateType(int(1), PropertyTypeNumber) assert.ErrorContains(t, err, "expected type float, but value is 1") - err = validateType(true, FieldTypeFloat) + err = validateType(true, PropertyTypeNumber) assert.ErrorContains(t, err, "expected type float, but value is true") - err = validateType("abc", FieldTypeFloat) + err = validateType("abc", PropertyTypeNumber) assert.ErrorContains(t, err, "expected type float, but value is \"abc\"") // assert validation failing for boolean - err = validateType(int(1), FieldTypeBoolean) + err = validateType(int(1), PropertyTypeBoolean) assert.ErrorContains(t, err, "expected type boolean, but value is 1") - err = validateType(float64(1), FieldTypeBoolean) + err = validateType(float64(1), PropertyTypeBoolean) assert.ErrorContains(t, err, "expected type boolean, but value is 1") - err = validateType("abc", FieldTypeBoolean) + err = validateType("abc", PropertyTypeBoolean) assert.ErrorContains(t, err, "expected type boolean, but value is \"abc\"") // assert validation failing for string - err = validateType(int(1), FieldTypeString) + err = validateType(int(1), PropertyTypeString) assert.ErrorContains(t, err, "expected type string, but value is 1") - err = validateType(float64(1), FieldTypeString) + err = validateType(float64(1), PropertyTypeString) assert.ErrorContains(t, err, "expected type string, but value is 1") - err = validateType(false, FieldTypeString) + err = validateType(false, PropertyTypeString) assert.ErrorContains(t, err, "expected type string, but value is false") } func TestTemplateSchemaValidateConfig(t *testing.T) { // define schema for config schemaJson := `{ - "version": 0, "properties": { "int_val": { "type": "integer" }, "float_val": { - "type": "float" + "type": "number" }, "bool_val": { "type": "boolean" @@ -216,13 +212,12 @@ func TestTemplateSchemaValidateConfig(t *testing.T) { func TestTemplateSchemaValidateConfigFailsForUnknownField(t *testing.T) { // define schema for config schemaJson := `{ - "version": 0, "properties": { "int_val": { "type": "integer" }, "float_val": { - "type": "float" + "type": "number" }, "bool_val": { "type": "boolean" @@ -251,13 +246,12 @@ func TestTemplateSchemaValidateConfigFailsForUnknownField(t *testing.T) { func TestTemplateSchemaValidateConfigFailsForWhenIncorrectTypes(t *testing.T) { // define schema for config schemaJson := `{ - "version": 0, "properties": { "int_val": { "type": "integer" }, "float_val": { - "type": "float" + "type": "number" }, "bool_val": { "type": "boolean" @@ -286,7 +280,6 @@ func TestTemplateSchemaValidateConfigFailsForWhenIncorrectTypes(t *testing.T) { func TestTemplateSchemaValidateConfigFailsForWhenMissingInputParams(t *testing.T) { // define schema for config schemaJson := `{ - "version": 0, "properties": { "int_val": { "type": "integer" diff --git a/libs/template/testdata/skip_dir/template/{{.c}}/abc b/libs/template/testdata/skip_dir/template/{{.c}}/abc new file mode 100644 index 00000000..6af8c585 --- /dev/null +++ b/libs/template/testdata/skip_dir/template/{{.c}}/abc @@ -0,0 +1,4 @@ +{{if eq .d "foo"}} +{{skipThisFile}} +{{end}} +Hello, World diff --git a/libs/template/testdata/skip_dir/template/{{.c}}/foo b/libs/template/testdata/skip_dir/template/{{.c}}/foo deleted file mode 100644 index 9925ff1d..00000000 --- a/libs/template/testdata/skip_dir/template/{{.c}}/foo +++ /dev/null @@ -1,4 +0,0 @@ -{{if eq .d "skipping"}} -{{skipThisFile}} -{{end}} -Hello! diff --git a/libs/template/validators.go b/libs/template/validators.go index 4442173f..c67c6edd 100644 --- a/libs/template/validators.go +++ b/libs/template/validators.go @@ -39,9 +39,9 @@ func validateInteger(v any) error { return nil } -var validators map[FieldType]Validator = map[FieldType]Validator{ - FieldTypeString: validateString, - FieldTypeBoolean: validateBoolean, - FieldTypeInt: validateInteger, - FieldTypeFloat: validateFloat, +var validators map[PropertyType]Validator = map[PropertyType]Validator{ + PropertyTypeString: validateString, + PropertyTypeBoolean: validateBoolean, + PropertyTypeInt: validateInteger, + PropertyTypeNumber: validateFloat, }