diff --git a/cmd/expandybird/expander/expander.go b/cmd/expandybird/expander/expander.go index 0507d9012..1939f4e74 100644 --- a/cmd/expandybird/expander/expander.go +++ b/cmd/expandybird/expander/expander.go @@ -58,6 +58,11 @@ func (e *expander) ExpandChart(request *common.ExpansionRequest) (*common.Expans chartInv := request.ChartInvocation chartFile := request.Chart.Chartfile chartMembers := request.Chart.Members + + if chartInv.Type != chartFile.Name { + return nil, fmt.Errorf("Request chart invocation does not match provided chart") + } + schemaName := chartInv.Type + ".schema" if chartFile.Expander == nil { @@ -89,7 +94,7 @@ func (e *expander) ExpandChart(request *common.ExpansionRequest) (*common.Expans message := fmt.Sprintf("The entrypoint in the chart.yaml cannot be found: %s", chartFile.Expander.Entrypoint) return nil, fmt.Errorf("%s: %s", chartInv.Name, message) } - if schemaIndex == -1 { + if chartFile.Schema != "" && schemaIndex == -1 { message := fmt.Sprintf("The schema in the chart.yaml cannot be found: %s", chartFile.Schema) return nil, fmt.Errorf("%s: %s", chartInv.Name, message) } diff --git a/cmd/expandybird/expander/expander_test.go b/cmd/expandybird/expander/expander_test.go index 648811e4c..358c2c090 100644 --- a/cmd/expandybird/expander/expander_test.go +++ b/cmd/expandybird/expander/expander_test.go @@ -16,168 +16,691 @@ limitations under the License. package expander -/* import ( "fmt" - "io" - "io/ioutil" - "os" - "path" "reflect" + "runtime" "strings" "testing" + "github.com/kubernetes/helm/pkg/chart" "github.com/kubernetes/helm/pkg/common" - "github.com/kubernetes/helm/pkg/util" ) -var importFileNames = []string{ - "../test/replicatedservice.py", -} - -var validFileName = "../test/ValidContent.yaml" -var outputFileName = "../test/ExpectedOutput.yaml" -var archiveFileName = "../test/TestArchive.tar" var expanderName = "../../../expansion/expansion.py" -type ExpanderTestCase struct { +type testCase struct { Description string - TemplateFileName string - ImportFileNames []string + Request *common.ExpansionRequest + ExpectedResponse *common.ExpansionResponse ExpectedError string } -func (etc *ExpanderTestCase) GetTemplate(t *testing.T) *common.Template { - template, err := util.NewTemplateFromFileNames(etc.TemplateFileName, etc.ImportFileNames) - if err != nil { - t.Fatalf("cannot create template for test case '%s': %s", etc.Description, err) - } +// content provides an easy way to provide file content verbatim in tests. +func content(lines []string) []byte { + return []byte(strings.Join(lines, "\n") + "\n") +} - return template +// funcName returns the name of the calling function. +func funcName() string { + pc, _, _, _ := runtime.Caller(1) + return runtime.FuncForPC(pc).Name() } -func GetOutputString(t *testing.T, description string) string { - output, err := ioutil.ReadFile(outputFileName) +func testExpansion(t *testing.T, req *common.ExpansionRequest, + expResponse *common.ExpansionResponse, expError string) { + backend := NewExpander(expanderName) + response, err := backend.ExpandChart(req) if err != nil { - t.Fatalf("cannot read output file for test case '%s': %s", description, err) + message := err.Error() + if expResponse != nil || !strings.Contains(message, expError) { + t.Fatalf("unexpected error: %s\n", message) + } + } else { + if expResponse == nil { + t.Fatalf("expected error did not occur: %s\n", expError) + } + if !reflect.DeepEqual(response, expResponse) { + message := fmt.Sprintf( + "want:\n%s\nhave:\n%s\n", expResponse, response) + t.Fatalf("output mismatch:\n%s\n", message) + } } +} - return string(output) +var pyExpander = &chart.Expander{ + Name: "ExpandyBird", + Entrypoint: "templates/main.py", } -func expandAndVerifyOutput(t *testing.T, actualOutput, description string) { - actualResult, err := NewExpansionResult(actualOutput) - if err != nil { - t.Fatalf("error in test case '%s': %s\n", description, err) - } +var jinjaExpander = &chart.Expander{ + Name: "ExpandyBird", + Entrypoint: "templates/main.jinja", +} - expectedOutput := GetOutputString(t, description) - expectedResult, err := NewExpansionResult(expectedOutput) - if err != nil { - t.Fatalf("error in test case '%s': %s\n", description, err) - } +func TestEmptyJinja(t *testing.T) { + testExpansion( + t, + &common.ExpansionRequest{ + ChartInvocation: &common.Resource{ + Name: "test_invocation", + Type: funcName(), + }, + Chart: &chart.Content{ + Chartfile: &chart.Chartfile{ + Name: funcName(), + Expander: jinjaExpander, + }, + Members: []*chart.Member{ + { + Path: "templates/main.jinja", + Content: content([]string{"resources:"}), + }, + }, + }, + }, + &common.ExpansionResponse{ + Resources: []interface{}{}, + }, + "", // Error + ) +} - if !reflect.DeepEqual(actualResult, expectedResult) { - message := fmt.Sprintf("want:\n%s\nhave:\n%s\n", expectedOutput, actualOutput) - t.Fatalf("error in test case '%s':\n%s\n", description, message) - } +func TestEmptyPython(t *testing.T) { + testExpansion( + t, + &common.ExpansionRequest{ + ChartInvocation: &common.Resource{ + Name: "test_invocation", + Type: funcName(), + }, + Chart: &chart.Content{ + Chartfile: &chart.Chartfile{ + Name: funcName(), + Expander: pyExpander, + }, + Members: []*chart.Member{ + { + Path: "templates/main.py", + Content: content([]string{ + "def GenerateConfig(ctx):", + " return 'resources:'", + }), + }, + }, + }, + }, + &common.ExpansionResponse{ + Resources: []interface{}{}, + }, + "", // Error + ) } -func testExpandTemplateFromFile(t *testing.T, fileName, baseName string, importFileNames []string, - constructor func(string, io.Reader, []string) (*common.Template, error)) { - file, err := os.Open(fileName) - if err != nil { - t.Fatalf("cannot open file %s: %s", fileName, err) - } +func TestSimpleJinja(t *testing.T) { + testExpansion( + t, + &common.ExpansionRequest{ + ChartInvocation: &common.Resource{ + Name: "test_invocation", + Type: funcName(), + }, + Chart: &chart.Content{ + Chartfile: &chart.Chartfile{ + Name: funcName(), + Expander: jinjaExpander, + }, + Members: []*chart.Member{ + { + Path: "templates/main.jinja", + Content: content([]string{ + "resources:", + "- name: foo", + " type: bar", + }), + }, + }, + }, + }, + &common.ExpansionResponse{ + Resources: []interface{}{ + map[string]interface{}{ + "name": "foo", + "type": "bar", + }, + }, + }, + "", // Error + ) +} - template, err := constructor(baseName, file, importFileNames) - if err != nil { - t.Fatalf("cannot create template from file %s: %s", fileName, err) - } +func TestSimplePython(t *testing.T) { + testExpansion( + t, + &common.ExpansionRequest{ + ChartInvocation: &common.Resource{ + Name: "test_invocation", + Type: funcName(), + }, + Chart: &chart.Content{ + Chartfile: &chart.Chartfile{ + Name: funcName(), + Expander: pyExpander, + }, + Members: []*chart.Member{ + { + Path: "templates/main.py", + Content: content([]string{ + "def GenerateConfig(ctx):", + " return '''resources:", + "- name: foo", + " type: bar", + "'''", + }), + }, + }, + }, + }, + &common.ExpansionResponse{ + Resources: []interface{}{ + map[string]interface{}{ + "name": "foo", + "type": "bar", + }, + }, + }, + "", // Error + ) +} - backend := NewExpander(expanderName) - actualOutput, err := backend.ExpandTemplate(template) - if err != nil { - t.Fatalf("cannot expand template from file %s: %s", fileName, err) - } +func TestPropertiesJinja(t *testing.T) { + testExpansion( + t, + &common.ExpansionRequest{ + ChartInvocation: &common.Resource{ + Name: "test_invocation", + Type: funcName(), + Properties: map[string]interface{}{ + "prop1": 3.0, + "prop2": "foo", + }, + }, + Chart: &chart.Content{ + Chartfile: &chart.Chartfile{ + Name: funcName(), + Expander: jinjaExpander, + }, + Members: []*chart.Member{ + { + Path: "templates/main.jinja", + Content: content([]string{ + "resources:", + "- name: foo", + " type: {{ properties.prop2 }}", + " properties:", + " something: {{ properties.prop1 }}", + }), + }, + }, + }, + }, + &common.ExpansionResponse{ + Resources: []interface{}{ + map[string]interface{}{ + "name": "foo", + "properties": map[string]interface{}{ + "something": 3.0, + }, + "type": "foo", + }, + }, + }, + "", // Error + ) +} - description := fmt.Sprintf("test expand template from file: %s", fileName) - expandAndVerifyOutput(t, actualOutput, description) -} - -func TestExpandTemplateFromReader(t *testing.T) { - baseName := path.Base(validFileName) - testExpandTemplateFromFile(t, validFileName, baseName, importFileNames, util.NewTemplateFromReader) -} - -func TestExpandTemplateFromArchive(t *testing.T) { - baseName := path.Base(validFileName) - testExpandTemplateFromFile(t, archiveFileName, baseName, nil, util.NewTemplateFromArchive) -} - -var ExpanderTestCases = []ExpanderTestCase{ - { - "expect error for invalid file name", - "../test/InvalidFileName.yaml", - importFileNames, - "ExpansionError: Exception", - }, - { - "expect error for invalid property", - "../test/InvalidProperty.yaml", - importFileNames, - "ExpansionError: Exception", - }, - { - "expect error for malformed content", - "../test/MalformedContent.yaml", - importFileNames, - "ExpansionError: Error parsing YAML: mapping values are not allowed here", - }, - { - "expect error for missing imports", - "../test/MissingImports.yaml", - importFileNames, - "ExpansionError: Exception", - }, - { - "expect error for missing resource name", - "../test/MissingResourceName.yaml", - importFileNames, - "ExpansionError: Resource does not have a name", - }, - { - "expect error for missing type name", - "../test/MissingTypeName.yaml", - importFileNames, - "ExpansionError: Resource does not have type defined", - }, - { - "expect success", - validFileName, - importFileNames, - "", - }, -} - -func TestExpandTemplate(t *testing.T) { - backend := NewExpander(expanderName) - for _, etc := range ExpanderTestCases { - template := etc.GetTemplate(t) - actualOutput, err := backend.ExpandTemplate(template) - if err != nil { - message := err.Error() - if !strings.Contains(message, etc.ExpectedError) { - t.Fatalf("error in test case '%s': %s\n", etc.Description, message) - } - } else { - if etc.ExpectedError != "" { - t.Fatalf("expected error did not occur in test case '%s': %s\n", - etc.Description, etc.ExpectedError) - } - - expandAndVerifyOutput(t, actualOutput, etc.Description) - } - } +func TestPropertiesPython(t *testing.T) { + testExpansion( + t, + &common.ExpansionRequest{ + ChartInvocation: &common.Resource{ + Name: "test_invocation", + Type: funcName(), + Properties: map[string]interface{}{ + "prop1": 3.0, + "prop2": "foo", + }, + }, + Chart: &chart.Content{ + Chartfile: &chart.Chartfile{ + Name: funcName(), + Expander: pyExpander, + }, + Members: []*chart.Member{ + { + Path: "templates/main.py", + Content: content([]string{ + "def GenerateConfig(ctx):", + " return '''resources:", + "- name: foo", + " type: %(prop2)s", + " properties:", + " something: %(prop1)s", + "''' % ctx.properties", + }), + }, + }, + }, + }, + &common.ExpansionResponse{ + Resources: []interface{}{ + map[string]interface{}{ + "name": "foo", + "properties": map[string]interface{}{ + "something": 3.0, + }, + "type": "foo", + }, + }, + }, + "", // Error + ) +} + +func TestMultiFileJinja(t *testing.T) { + testExpansion( + t, + &common.ExpansionRequest{ + ChartInvocation: &common.Resource{ + Name: "test_invocation", + Type: funcName(), + }, + Chart: &chart.Content{ + Chartfile: &chart.Chartfile{ + Name: funcName(), + Expander: jinjaExpander, + }, + Members: []*chart.Member{ + { + Path: "templates/main.jinja", + Content: content([]string{"{% include 'templates/secondary.jinja' %}"}), + }, + { + Path: "templates/secondary.jinja", + Content: content([]string{ + "resources:", + "- name: foo", + " type: bar", + }), + }, + }, + }, + }, + &common.ExpansionResponse{ + Resources: []interface{}{ + map[string]interface{}{ + "name": "foo", + "type": "bar", + }, + }, + }, + "", // Error + ) +} + +var schemaContent = content([]string{ + `{`, + ` "required": ["prop1", "prop2"],`, + ` "additionalProperties": false,`, + ` "properties": {`, + ` "prop1": {`, + ` "description": "Nice description.",`, + ` "type": "integer"`, + ` },`, + ` "prop2": {`, + ` "description": "Nice description.",`, + ` "type": "string"`, + ` }`, + ` }`, + `}`, +}) + +func TestSchema(t *testing.T) { + testExpansion( + t, + &common.ExpansionRequest{ + ChartInvocation: &common.Resource{ + Name: "test_invocation", + Type: funcName(), + Properties: map[string]interface{}{ + "prop1": 3.0, + "prop2": "foo", + }, + }, + Chart: &chart.Content{ + Chartfile: &chart.Chartfile{ + Name: funcName(), + Expander: jinjaExpander, + Schema: "Schema.yaml", + }, + Members: []*chart.Member{ + { + Path: "Schema.yaml", + Content: schemaContent, + }, + { + Path: "templates/main.jinja", + Content: content([]string{ + "resources:", + "- name: foo", + " type: {{ properties.prop2 }}", + " properties:", + " something: {{ properties.prop1 }}", + }), + }, + }, + }, + }, + &common.ExpansionResponse{ + Resources: []interface{}{ + map[string]interface{}{ + "name": "foo", + "properties": map[string]interface{}{ + "something": 3.0, + }, + "type": "foo", + }, + }, + }, + "", // Error + ) +} + +func TestSchemaFail(t *testing.T) { + testExpansion( + t, + &common.ExpansionRequest{ + ChartInvocation: &common.Resource{ + Name: "test_invocation", + Type: funcName(), + Properties: map[string]interface{}{ + "prop1": 3.0, + "prop3": "foo", + }, + }, + Chart: &chart.Content{ + Chartfile: &chart.Chartfile{ + Name: funcName(), + Expander: jinjaExpander, + Schema: "Schema.yaml", + }, + Members: []*chart.Member{ + { + Path: "Schema.yaml", + Content: schemaContent, + }, + { + Path: "templates/main.jinja", + Content: content([]string{ + "resources:", + "- name: foo", + " type: {{ properties.prop2 }}", + " properties:", + " something: {{ properties.prop1 }}", + }), + }, + }, + }, + }, + nil, // Response. + "Invalid properties for", + ) +} + +func TestMultiFileJinjaMissing(t *testing.T) { + testExpansion( + t, + &common.ExpansionRequest{ + ChartInvocation: &common.Resource{ + Name: "test_invocation", + Type: funcName(), + }, + Chart: &chart.Content{ + Chartfile: &chart.Chartfile{ + Name: funcName(), + Expander: jinjaExpander, + }, + Members: []*chart.Member{ + { + Path: "templates/main.jinja", + Content: content([]string{"{% include 'templates/secondary.jinja' %}"}), + }, + }, + }, + }, + nil, // Response + "TemplateNotFound: templates/secondary.jinja", + ) +} + +func TestMultiFilePython(t *testing.T) { + testExpansion( + t, + &common.ExpansionRequest{ + ChartInvocation: &common.Resource{ + Name: "test_invocation", + Type: funcName(), + }, + Chart: &chart.Content{ + Chartfile: &chart.Chartfile{ + Name: funcName(), + Expander: pyExpander, + }, + Members: []*chart.Member{ + { + Path: "templates/main.py", + Content: content([]string{ + "from templates import second", + "import templates.third", + "def GenerateConfig(ctx):", + " t2 = second.Gen()", + " t3 = templates.third.Gen()", + " return t2", + }), + }, + { + Path: "templates/second.py", + Content: content([]string{ + "def Gen():", + " return '''resources:", + "- name: foo", + " type: bar", + "'''", + }), + }, + { + Path: "templates/third.py", + Content: content([]string{ + "def Gen():", + " return '''resources:", + "- name: foo", + " type: bar", + "'''", + }), + }, + }, + }, + }, + &common.ExpansionResponse{ + Resources: []interface{}{ + map[string]interface{}{ + "name": "foo", + "type": "bar", + }, + }, + }, + "", // Error + ) +} + +func TestMultiFilePythonMissing(t *testing.T) { + testExpansion( + t, + &common.ExpansionRequest{ + ChartInvocation: &common.Resource{ + Name: "test_invocation", + Type: funcName(), + }, + Chart: &chart.Content{ + Chartfile: &chart.Chartfile{ + Name: funcName(), + Expander: pyExpander, + }, + Members: []*chart.Member{ + { + Path: "templates/main.py", + Content: content([]string{ + "from templates import second", + }), + }, + }, + }, + }, + nil, // Response + "cannot import name second", // Error + ) +} + +func TestWrongChartName(t *testing.T) { + testExpansion( + t, + &common.ExpansionRequest{ + ChartInvocation: &common.Resource{ + Name: "test_invocation", + Type: funcName(), + }, + Chart: &chart.Content{ + Chartfile: &chart.Chartfile{ + Name: "WrongName", + Expander: jinjaExpander, + }, + Members: []*chart.Member{ + { + Path: "templates/main.jinja", + Content: content([]string{"resources:"}), + }, + }, + }, + }, + nil, // Response + "Request chart invocation does not match provided chart", + ) +} + +func TestEntrypointNotFound(t *testing.T) { + testExpansion( + t, + &common.ExpansionRequest{ + ChartInvocation: &common.Resource{ + Name: "test_invocation", + Type: funcName(), + }, + Chart: &chart.Content{ + Chartfile: &chart.Chartfile{ + Name: funcName(), + Expander: jinjaExpander, + }, + Members: []*chart.Member{}, + }, + }, + nil, // Response + "The entrypoint in the chart.yaml cannot be found", + ) +} + +func TestMalformedResource(t *testing.T) { + testExpansion( + t, + &common.ExpansionRequest{ + ChartInvocation: &common.Resource{ + Name: "test_invocation", + Type: funcName(), + }, + Chart: &chart.Content{ + Chartfile: &chart.Chartfile{ + Name: funcName(), + Expander: jinjaExpander, + }, + Members: []*chart.Member{ + { + Path: "templates/main.jinja", + Content: content([]string{ + "resources:", + "fail", + }), + }, + }, + }, + }, + nil, // Response + "could not found expected ':'", // [sic] + ) +} + +func TestResourceNoName(t *testing.T) { + testExpansion( + t, + &common.ExpansionRequest{ + ChartInvocation: &common.Resource{ + Name: "test_invocation", + Type: funcName(), + }, + Chart: &chart.Content{ + Chartfile: &chart.Chartfile{ + Name: funcName(), + Expander: jinjaExpander, + }, + Members: []*chart.Member{ + { + Path: "templates/main.jinja", + Content: content([]string{ + "resources:", + "- type: bar", + }), + }, + }, + }, + }, + nil, // Response. + "Resource does not have a name", + ) +} + +func TestResourceNoType(t *testing.T) { + testExpansion( + t, + &common.ExpansionRequest{ + ChartInvocation: &common.Resource{ + Name: "test_invocation", + Type: funcName(), + }, + Chart: &chart.Content{ + Chartfile: &chart.Chartfile{ + Name: funcName(), + Expander: jinjaExpander, + }, + Members: []*chart.Member{ + { + Path: "templates/main.jinja", + Content: content([]string{ + "resources:", + "- name: foo", + }), + }, + }, + }, + }, + nil, // Response. + "Resource does not have type defined", + ) } -*/