diff --git a/builder/build.go b/builder/build.go index 187fd615c..89caa3063 100644 --- a/builder/build.go +++ b/builder/build.go @@ -5,6 +5,7 @@ package builder import ( "fmt" + "github.com/openfaas/faas-cli/stack" "io/ioutil" "log" "os" @@ -14,29 +15,26 @@ import ( // BuildImage construct Docker image from function parameters func BuildImage(image string, handler string, functionName string, language string, nocache bool, squash bool) { - switch language { - case "node", "python", "ruby", "csharp", "python3", "go": - tempPath := createBuildTemplate(functionName, handler, language) + if stack.IsValidTemplate(language) { - fmt.Printf("Building: %s with %s template. Please wait..\n", image, language) + var tempPath string + if strings.ToLower(language) == "dockerfile" { - flagStr := buildFlagString(nocache, squash, os.Getenv("http_proxy"), os.Getenv("https_proxy")) + tempPath = handler + if _, err := os.Stat(handler); err != nil { + fmt.Printf("Unable to build %s, %s is an invalid path\n", image, handler) + fmt.Printf("Image: %s not built.\n", image) - builder := strings.Split(fmt.Sprintf("docker build %s-t %s .", flagStr, image), " ") - fmt.Println(strings.Join(builder, " ")) - ExecCommand(tempPath, builder) - fmt.Printf("Image: %s built.\n", image) + return + } + fmt.Printf("Building: %s with Dockerfile. Please wait..\n", image) + + } else { - break - case "Dockerfile", "dockerfile": - tempPath := handler - if _, err := os.Stat(handler); err != nil { - fmt.Printf("Unable to build %s, %s is an invalid path\n", image, handler) - fmt.Printf("Image: %s not built.\n", image) + tempPath = createBuildTemplate(functionName, handler, language) + fmt.Printf("Building: %s with %s template. Please wait..\n", image, language) - break } - fmt.Printf("Building: %s with Dockerfile. Please wait..\n", image) flagStr := buildFlagString(nocache, squash, os.Getenv("http_proxy"), os.Getenv("https_proxy")) @@ -45,7 +43,7 @@ func BuildImage(image string, handler string, functionName string, language stri ExecCommand(tempPath, builder) fmt.Printf("Image: %s built.\n", image) - default: + } else { log.Fatalf("Language template: %s not supported. Build a custom Dockerfile instead.", language) } } diff --git a/commands/add_template.go b/commands/add_template.go new file mode 100644 index 000000000..6802cbf82 --- /dev/null +++ b/commands/add_template.go @@ -0,0 +1,80 @@ +// Copyright (c) Alex Ellis, Eric Stoekl 2017. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +package commands + +import ( + "errors" + + "fmt" + + "os" + + "regexp" + + "github.com/spf13/cobra" +) + +// Args and Flags that are to be added to commands + +const ( + repositoryRegexpMockedServer = `^http://127.0.0.1:\d+/([a-z0-9-]+)/([a-z0-9-]+)$` + repositoryRegexpGithub = `^https://github.com/([a-z0-9-]+)/([a-z0-9-]+)/?$` +) + +var ( + repository string + overwrite bool +) + +var supportedVerbs = [...]string{"pull"} + +func init() { + addTemplateCmd.Flags().BoolVar(&overwrite, "overwrite", false, "Overwrite existing templates?") + + faasCmd.AddCommand(addTemplateCmd) +} + +// addTemplateCmd allows the user to fetch a template from a repository +var addTemplateCmd = &cobra.Command{ + Use: "template pull ", + Args: func(cmd *cobra.Command, args []string) error { + msg := fmt.Sprintf(`Must use a supported verb for 'faas-cli template' +Currently supported verbs: %v`, supportedVerbs) + + if len(args) == 0 { + return errors.New(msg) + } + + if args[0] != "pull" { + return errors.New(msg) + } + + if len(args) > 1 { + var validURL = regexp.MustCompile(repositoryRegexpGithub + "|" + repositoryRegexpMockedServer) + + if !validURL.MatchString(args[1]) { + return errors.New("The repository URL must be in the format https://github.com//") + } + } + return nil + }, + Short: "Downloads templates from the specified github repo", + Long: `Downloads the compressed github repo specified by [URL], and extracts the 'template' + directory from the root of the repo, if it exists.`, + Example: "faas-cli template pull https://github.com/openfaas/faas-cli", + Run: runAddTemplate, +} + +func runAddTemplate(cmd *cobra.Command, args []string) { + repository := "" + if len(args) > 1 { + repository = args[1] + } + + fmt.Println("Fetch templates from repository: " + repository) + if err := fetchTemplates(repository, overwrite); err != nil { + fmt.Println(err) + + os.Exit(1) + } +} diff --git a/commands/add_template_test.go b/commands/add_template_test.go new file mode 100644 index 000000000..44c8583d5 --- /dev/null +++ b/commands/add_template_test.go @@ -0,0 +1,144 @@ +// Copyright (c) Alex Ellis, Eric Stoekl 2017. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +package commands + +import ( + "bytes" + "io/ioutil" + "log" + "net/http" + "net/http/httptest" + "os" + "regexp" + "strings" + "testing" +) + +func Test_addTemplate(t *testing.T) { + defer tearDown_fetch_templates(t) + + ts := httpTestServer(t) + defer ts.Close() + + repository = ts.URL + "/owner/repo" + faasCmd.SetArgs([]string{"template", "pull", repository}) + faasCmd.Execute() + + // Verify created directories + if _, err := os.Stat("template"); err != nil { + t.Fatalf("The directory %s was not created", "template") + } +} + +func Test_addTemplate_with_overwriting(t *testing.T) { + defer tearDown_fetch_templates(t) + + ts := httpTestServer(t) + defer ts.Close() + + repository = ts.URL + "/owner/repo" + faasCmd.SetArgs([]string{"template", "pull", repository}) + faasCmd.Execute() + + var buf bytes.Buffer + log.SetOutput(&buf) + + r := regexp.MustCompile(`(?m:Cannot overwrite the following \d+ directories:)`) + + faasCmd.SetArgs([]string{"template", "pull", repository}) + faasCmd.Execute() + + if !r.MatchString(buf.String()) { + t.Fatal(buf.String()) + } + + buf.Reset() + + faasCmd.SetArgs([]string{"template", "pull", repository, "--overwrite"}) + faasCmd.Execute() + + str := buf.String() + if r.MatchString(str) { + t.Fatal() + } + + // Verify created directories + if _, err := os.Stat("template"); err != nil { + t.Fatalf("The directory %s was not created", "template") + } +} + +func Test_addTemplate_no_arg(t *testing.T) { + defer tearDown_fetch_templates(t) + var buf bytes.Buffer + + faasCmd.SetArgs([]string{"template", "pull"}) + faasCmd.SetOutput(&buf) + faasCmd.Execute() + + if strings.Contains(buf.String(), "Error: A repository URL must be specified") { + t.Fatal("Output does not contain the required string") + } +} + +func Test_addTemplate_error_not_valid_url(t *testing.T) { + var buf bytes.Buffer + + faasCmd.SetArgs([]string{"template", "pull", "git@github.com:openfaas/faas-cli.git"}) + faasCmd.SetOutput(&buf) + faasCmd.Execute() + + if !strings.Contains(buf.String(), "Error: The repository URL must be in the format https://github.com//") { + t.Fatal("Output does not contain the required string", buf.String()) + } +} + +// httpTestServer returns a testing http server +func httpTestServer(t *testing.T) *httptest.Server { + const sampleMasterZipPath string = "testdata/master_test.zip" + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + + if _, err := os.Stat(sampleMasterZipPath); os.IsNotExist(err) { + t.Error(err) + } + + fileData, err := ioutil.ReadFile(sampleMasterZipPath) + if err != nil { + t.Error(err) + } + + w.Write(fileData) + })) + + return ts +} + +func Test_repositoryUrlRegExp(t *testing.T) { + var url string + r := regexp.MustCompile(repositoryRegexpGithub) + + url = "http://github.com/owner/repo" + if r.MatchString(url) { + t.Errorf("Url %s must start with https", url) + } + + url = "https://github.com/owner/repo.git" + if r.MatchString(url) { + t.Errorf("Url %s must not end with .git or must start with https", url) + } + + url = "https://github.com/owner/repo//" + if r.MatchString(url) { + t.Errorf("Url %s must end with no or one slash", url) + } + + url = "https://github.com/owner/repo" + if !r.MatchString(url) { + t.Errorf("Url %s must be valid", url) + } + + url = "https://github.com/owner/repo/" + if !r.MatchString(url) { + t.Errorf("Url %s must be valid", url) + } +} diff --git a/commands/build.go b/commands/build.go index 90e3b158d..5442970bb 100644 --- a/commands/build.go +++ b/commands/build.go @@ -78,7 +78,7 @@ func runBuild(cmd *cobra.Command, args []string) error { } } - if pullErr := PullTemplates(""); pullErr != nil { + if pullErr := PullTemplates(); pullErr != nil { return fmt.Errorf("could not pull templates for OpenFaaS: %v", pullErr) } @@ -139,13 +139,13 @@ func build(services *stack.Services, queueDepth int) { } // PullTemplates pulls templates from Github from the master zip download file. -func PullTemplates(templateUrl string) error { +func PullTemplates() error { var err error exists, err := os.Stat("./template") if err != nil || exists == nil { log.Println("No templates found in current directory.") - err = fetchTemplates(templateUrl) + err = fetchTemplates("", false) if err != nil { log.Println("Unable to download templates from Github.") return err diff --git a/commands/deploy.go b/commands/deploy.go index 1c78302ae..ffc763fcc 100644 --- a/commands/deploy.go +++ b/commands/deploy.go @@ -66,7 +66,7 @@ var deployCmd = &cobra.Command{ [--handler HANDLER_DIR] [--fprocess PROCESS] [--env ENVVAR=VALUE ...] - [--label LABEL=VALUE ...] + [--label LABEL=VALUE ...] [--replace=false] [--update=false] [--constraint PLACEMENT_CONSTRAINT ...] @@ -160,6 +160,28 @@ func runDeploy(cmd *cobra.Command, args []string) { log.Fatalln(envErr) } + // Get FProcess to use from the ./template/template.yml, if a template is being used + if function.Language != "" && function.Language != "Dockerfile" && function.Language != "dockerfile" { + pathToTemplateYAML := "./template/" + function.Language + "/template.yml" + if _, err := os.Stat(pathToTemplateYAML); os.IsNotExist(err) { + log.Fatalln(err.Error()) + return + } + + var langTemplate stack.LanguageTemplate + parsedLangTemplate, err := stack.ParseYAMLForLanguageTemplate(pathToTemplateYAML) + + if err != nil { + log.Fatalln(err.Error()) + return + } + + if parsedLangTemplate != nil { + langTemplate = *parsedLangTemplate + function.FProcess = langTemplate.FProcess + } + } + proxy.DeployFunction(function.FProcess, services.Provider.GatewayURL, function.Name, function.Image, function.Language, replace, allEnvironment, services.Provider.Network, constraints, update, secrets, allLabels) } } else { diff --git a/commands/fetch_templates.go b/commands/fetch_templates.go index f3187aaf0..c80909e7c 100644 --- a/commands/fetch_templates.go +++ b/commands/fetch_templates.go @@ -5,6 +5,7 @@ package commands import ( "archive/zip" + "errors" "fmt" "io" "io/ioutil" @@ -18,76 +19,133 @@ import ( "github.com/openfaas/faas-cli/proxy" ) -const ZipFileName string = "master.zip" +const ( + defaultTemplateRepository = "https://github.com/openfaas/faas-cli" + templateDirectory = "./template/" +) // fetchTemplates fetch code templates from GitHub master zip file. -func fetchTemplates(templateUrl string) error { +func fetchTemplates(templateURL string, overwrite bool) error { + var existingLanguages []string + availableLanguages := make(map[string]bool) + var fetchedTemplates []string + + if len(templateURL) == 0 { + templateURL = defaultTemplateRepository + } - err := fetchMasterZip(templateUrl) + archive, err := fetchMasterZip(templateURL) + if err != nil { + removeArchive(archive) + return err + } - zipFile, err := zip.OpenReader(ZipFileName) + zipFile, err := zip.OpenReader(archive) if err != nil { return err } - log.Printf("Attempting to expand templates from %s\n", ZipFileName) + log.Printf("Attempting to expand templates from %s\n", archive) for _, z := range zipFile.File { - relativePath := strings.Replace(z.Name, "faas-cli-master/", "", -1) - if strings.Index(relativePath, "template") == 0 { - fmt.Printf("Found \"%s\"\n", relativePath) - rc, err := z.Open() - if err != nil { - return err - } + var rc io.ReadCloser - err = createPath(relativePath, z.Mode()) - if err != nil { - return err - } + relativePath := z.Name[strings.Index(z.Name, "/")+1:] + if strings.Index(relativePath, "template/") != 0 { + // Process only directories inside "template" at root + continue + } + + var language string + if languageSplit := strings.Split(relativePath, "/"); len(languageSplit) > 2 { + language = languageSplit[1] + + if len(languageSplit) == 3 && relativePath[len(relativePath)-1:] == "/" { + // template/language/ + + if !canWriteLanguage(&availableLanguages, language, overwrite) { + existingLanguages = append(existingLanguages, language) + continue + } + fetchedTemplates = append(fetchedTemplates, language) + } else { + // template/language/* - // If relativePath is just a directory, then skip expanding it. - if len(relativePath) > 1 && relativePath[len(relativePath)-1:] != string(os.PathSeparator) { - err = writeFile(rc, z.UncompressedSize64, relativePath, z.Mode()) - if err != nil { - return err + if !canWriteLanguage(&availableLanguages, language, overwrite) { + continue } } + } else { + // template/ + continue + } + + if rc, err = z.Open(); err != nil { + break + } + + if err = createPath(relativePath, z.Mode()); err != nil { + break + } + + // If relativePath is just a directory, then skip expanding it. + if len(relativePath) > 1 && relativePath[len(relativePath)-1:] != "/" { + if err = writeFile(rc, z.UncompressedSize64, relativePath, z.Mode()); err != nil { + break + } } } + if len(existingLanguages) > 0 { + log.Printf("Cannot overwrite the following %d directories: %v\n", len(existingLanguages), existingLanguages) + } + + zipFile.Close() + + log.Printf("Fetched %d template(s) : %v from %s\n", len(fetchedTemplates), fetchedTemplates, templateURL) + + err = removeArchive(archive) + + return err +} + +// removeArchive removes the given file +func removeArchive(archive string) error { log.Printf("Cleaning up zip file...") - if _, err := os.Stat(ZipFileName); err == nil { - os.Remove(ZipFileName) + if _, err := os.Stat(archive); err == nil { + return os.Remove(archive) } else { return err } - fmt.Println("") - - return err } -func fetchMasterZip(templateUrl string) error { +// fetchMasterZip downloads a zip file from a repository URL +func fetchMasterZip(templateURL string) (string, error) { var err error - if _, err = os.Stat(ZipFileName); err != nil { - if len(templateUrl) == 0 { - templateUrl = "https://github.com/openfaas/faas-cli/archive/" + ZipFileName - } + templateURL = strings.TrimRight(templateURL, "/") + templateURL = templateURL + "/archive/master.zip" + archive := "master.zip" + if _, err := os.Stat(archive); err != nil { timeout := 120 * time.Second client := proxy.MakeHTTPClient(&timeout) - req, err := http.NewRequest(http.MethodGet, templateUrl, nil) + req, err := http.NewRequest(http.MethodGet, templateURL, nil) if err != nil { log.Println(err.Error()) - return err + return "", err } - log.Printf("HTTP GET %s\n", templateUrl) + log.Printf("HTTP GET %s\n", templateURL) res, err := client.Do(req) if err != nil { log.Println(err.Error()) - return err + return "", err + } + if res.StatusCode != http.StatusOK { + err := errors.New(fmt.Sprintf("%s is not valid, status code %d", templateURL, res.StatusCode)) + log.Println(err.Error()) + return "", err } if res.Body != nil { defer res.Body.Close() @@ -95,36 +153,27 @@ func fetchMasterZip(templateUrl string) error { bytesOut, err := ioutil.ReadAll(res.Body) if err != nil { log.Println(err.Error()) - return err + return "", err } - log.Printf("Writing %dKb to %s\n", len(bytesOut)/1024, ZipFileName) - err = ioutil.WriteFile(ZipFileName, bytesOut, 0700) + log.Printf("Writing %dKb to %s\n", len(bytesOut)/1024, archive) + err = ioutil.WriteFile(archive, bytesOut, 0700) if err != nil { log.Println(err.Error()) + return "", err } } fmt.Println("") - return err + return archive, err } func writeFile(rc io.ReadCloser, size uint64, relativePath string, perms os.FileMode) error { var err error defer rc.Close() - fmt.Printf("Writing %d bytes to \"%s\"\n", size, relativePath) - if strings.HasSuffix(relativePath, "/") { - mkdirErr := os.MkdirAll(relativePath, perms) - if mkdirErr != nil { - return fmt.Errorf("error making directory %s got: %s", relativePath, mkdirErr) - } - return err - } - - // Create a file instead. f, err := os.OpenFile(relativePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perms) if err != nil { - return fmt.Errorf("error writing to %s got: %s", relativePath, err) + return err } defer f.Close() _, err = io.CopyN(f, rc, int64(size)) @@ -137,3 +186,26 @@ func createPath(relativePath string, perms os.FileMode) error { err := os.MkdirAll(dir, perms) return err } + +// canWriteLanguage tells whether the language can be processed or not +func canWriteLanguage(availableLanguages *map[string]bool, language string, overwrite bool) bool { + canWrite := false + if len(language) > 0 { + if _, ok := (*availableLanguages)[language]; ok { + return (*availableLanguages)[language] + } + canWrite = checkLanguage(language, overwrite) + (*availableLanguages)[language] = canWrite + } + + return canWrite +} + +func checkLanguage(language string, overwrite bool) bool { + dir := templateDirectory + language + if _, err := os.Stat(dir); err == nil && !overwrite { + // The directory template/language/ exists + return false + } + return true +} diff --git a/commands/fetch_templates_test.go b/commands/fetch_templates_test.go index ef0b0adb5..537fb4380 100644 --- a/commands/fetch_templates_test.go +++ b/commands/fetch_templates_test.go @@ -9,16 +9,31 @@ import ( "testing" ) -var SmallestZipFile = []byte{80, 75, 05, 06, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00, 00} +func Test_fetchTemplates(t *testing.T) { + defer tearDown_fetch_templates(t) -func Test_PullTemplates(t *testing.T) { - // Remove existing master.zip file if it exists - if _, err := os.Stat("master.zip"); err == nil { - t.Log("Found a master.zip file, removing it...") + // Create fake server for testing. + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.ServeFile(w, r, "testdata/master_test.zip") + })) + defer ts.Close() + + err := fetchTemplates(ts.URL+"/owner/repo", false) + if err != nil { + t.Error(err) + } +} + +// tearDown_fetch_templates_test cleans all files and directories created by the test +func tearDown_fetch_templates(t *testing.T) { - err := os.Remove("master.zip") + // Remove existing archive file if it exists + if _, err := os.Stat("template-owner-repo.zip"); err == nil { + t.Log("The archive was not deleted") + + err := os.Remove("template-owner-repo.zip") if err != nil { - t.Fatal(err) + t.Log(err) } } @@ -28,21 +43,15 @@ func Test_PullTemplates(t *testing.T) { err := os.RemoveAll("template/") if err != nil { - t.Fatal(err) + t.Log(err) } + } else { + t.Logf("Directory template was not created: %s", err) } - // Create fake server for testing. - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - - // Write out the minimum number of bytes to make the response a valid .zip file - w.Write(SmallestZipFile) - - })) - defer ts.Close() - - err := PullTemplates(ts.URL) - if err != nil { - t.Error(err) + // Verify the downloaded archive + archive := "template-owner-repo.zip" + if _, err := os.Stat(archive); err == nil { + t.Fatalf("The archive %s was not deleted", archive) } } diff --git a/commands/new_function.go b/commands/new_function.go index 4ca08fff4..7a0f3a3ff 100644 --- a/commands/new_function.go +++ b/commands/new_function.go @@ -7,11 +7,11 @@ import ( "fmt" "io/ioutil" "os" - "path" "strings" "github.com/morikuni/aec" "github.com/openfaas/faas-cli/builder" + "github.com/openfaas/faas-cli/stack" "github.com/spf13/cobra" ) @@ -44,15 +44,22 @@ language or type in --list for a list of languages available.`, } func runNewFunction(cmd *cobra.Command, args []string) { + var availableTemplates []string + if list == true { + if templateFolders, err := ioutil.ReadDir(templateDirectory); err != nil { + fmt.Printf("No language templates were found. Please run 'faas-cli template pull'.") + return + } else { + for _, file := range templateFolders { + if file.IsDir() { + availableTemplates = append(availableTemplates, file.Name()) + } + } + } + fmt.Printf(`Languages available as templates: -- node -- python -- python3 -- ruby -- csharp -- Dockerfile -- go +` + printAvailableTemplates(availableTemplates) + ` Or alternatively create a folder containing a Dockerfile, then pick the "Dockerfile" lang type in your YAML file. @@ -71,10 +78,11 @@ the "Dockerfile" lang type in your YAML file. return } - PullTemplates("") + PullTemplates() - if validTemplate(lang) == false { + if stack.IsValidTemplate(lang) == false { fmt.Printf("%s is unavailable or not supported.\n", lang) + return } if _, err := os.Stat(functionName); err == nil { @@ -132,14 +140,10 @@ functions: return } -func validTemplate(lang string) bool { - var found bool - if strings.ToLower(lang) != "dockerfile" { - found = true - } - if _, err := os.Stat(path.Join("./template/", lang)); err == nil { - found = true +func printAvailableTemplates(availableTemplates []string) string { + var result string + for _, template := range availableTemplates { + result += fmt.Sprintf("- %s\n", template) } - - return found + return result } diff --git a/commands/new_function_test.go b/commands/new_function_test.go index 642175f9f..1f8b2d73d 100644 --- a/commands/new_function_test.go +++ b/commands/new_function_test.go @@ -5,37 +5,189 @@ package commands import ( "os" + "path/filepath" + "reflect" "regexp" + "strings" "testing" + "github.com/openfaas/faas-cli/stack" "github.com/openfaas/faas-cli/test" ) -func Test_new(t *testing.T) { - //TODO activate the teardown when PR#87 is merged defer teardown() - funcName := "test-function" +const SuccessMsg = `(?m:Function created in folder)` +const InvalidYAMLMsg = `is not valid YAML` +const InvalidYAMLMap = `map is empty` +const ListOptionOutput = `Languages available as templates: +- csharp +- go +- go-armhf +- node +- node-arm64 +- node-armhf +- python +- python-armhf +- python3 +- ruby` - // Symlink template directory at root to current directory to avoid re-downloading templates - os.Symlink("../template", "template") +const LangNotExistsOutput = `(?m:bash is unavailable or not supported.)` + +type NewFunctionTest struct { + title string + funcName string + funcLang string + expectedMsg string +} + +var NewFunctionTests = []NewFunctionTest{ + { + title: "new_1", + funcName: "new-test-1", + funcLang: "ruby", + expectedMsg: SuccessMsg, + }, + { + title: "new_2", + funcName: "new-test-2", + funcLang: "dockerfile", + expectedMsg: SuccessMsg, + }, +} + +func runNewFunctionTest(t *testing.T, nft NewFunctionTest) { + funcName := nft.funcName + funcLang := nft.funcLang + var funcYAML string + funcYAML = funcName + ".yml" // Cleanup the created directory defer func() { os.RemoveAll(funcName) - os.Remove(funcName + ".yml") - os.Remove("template") + os.Remove(funcYAML) }() + cmdParameters := []string{ + "new", + funcName, + "--lang=" + funcLang, + "--gateway=" + defaultGateway, + } + stdOut := test.CaptureStdout(func() { - faasCmd.SetArgs([]string{ - "new", - funcName, - "--lang=python", + faasCmd.SetArgs(cmdParameters) + faasCmd.Execute() + }) + + // Validate new function output + if found, err := regexp.MatchString(nft.expectedMsg, stdOut); err != nil || !found { + t.Fatalf("Output is not as expected: %s\n", stdOut) + } + + if nft.expectedMsg == SuccessMsg { + + // Make sure that the folder and file was created: + if _, err := os.Stat("./" + funcName); os.IsNotExist(err) { + t.Fatalf("%s/ directory was not created", funcName) + } + + if _, err := os.Stat(funcYAML); os.IsNotExist(err) { + t.Fatalf("\"%s\" yaml file was not created", funcYAML) + } + + // Make sure that the information in the YAML file is correct: + parsedServices, err := stack.ParseYAMLFile(funcYAML, "", "") + if err != nil { + t.Fatalf("Couldn't open modified YAML file \"%s\" due to error: %v", funcYAML, err) + } + services := *parsedServices + + var testServices stack.Services + testServices.Provider = stack.Provider{Name: "faas", GatewayURL: defaultGateway} + if !reflect.DeepEqual(services.Provider, testServices.Provider) { + t.Fatalf("YAML `provider` section was not created correctly for file %s: got %v", funcYAML, services.Provider) + } + + testServices.Functions = make(map[string]stack.Function) + testServices.Functions[funcName] = stack.Function{Language: funcLang, Image: funcName, Handler: "./" + funcName} + if !reflect.DeepEqual(services.Functions[funcName], testServices.Functions[funcName]) { + t.Fatalf("YAML `functions` section was not created correctly for file %s, got %v", funcYAML, services.Functions[funcName]) + } + } + +} + +func Test_newFunctionTests(t *testing.T) { + + homeDir, _ := filepath.Abs(".") + if err := os.Chdir("testdata/new_function"); err != nil { + t.Fatalf("Error on cd to testdata dir: %v", err) + } + + for _, test := range NewFunctionTests { + t.Run(test.title, func(t *testing.T) { + runNewFunctionTest(t, test) }) + } + + if err := os.Chdir(homeDir); err != nil { + t.Fatalf("Error on cd back to commands/ directory: %v", err) + } +} + +func Test_newFunctionListCmds(t *testing.T) { + + homeDir, _ := filepath.Abs(".") + if err := os.Chdir("testdata/new_function"); err != nil { + t.Fatalf("Error on cd to testdata dir: %v", err) + } + + cmdParameters := []string{ + "new", + "--list", + } + + stdOut := test.CaptureStdout(func() { + faasCmd.SetArgs(cmdParameters) faasCmd.Execute() + }) + + // Validate new function output + if !strings.HasPrefix(stdOut, ListOptionOutput) { + t.Fatalf("Output is not as expected: %s\n", stdOut) + } + if err := os.Chdir(homeDir); err != nil { + t.Fatalf("Error on cd back to commands/ directory: %v", err) + } +} + +func Test_languageNotExists(t *testing.T) { + + homeDir, _ := filepath.Abs(".") + if err := os.Chdir("testdata/new_function"); err != nil { + t.Fatalf("Error on cd to testdata dir: %v", err) + } + + // Attempt to create a function with a non-existing language + cmdParameters := []string{ + "new", + "sampleName", + "--lang=bash", + "--gateway=" + defaultGateway, + "--list=false", + } + + stdOut := test.CaptureStdout(func() { + faasCmd.SetArgs(cmdParameters) + faasCmd.Execute() }) - if found, err := regexp.MatchString(`(?m:Function created in folder)`, stdOut); err != nil || !found { - t.Fatalf("Output is not as expected:\n%s", stdOut) + // Validate new function output + if found, err := regexp.MatchString(LangNotExistsOutput, stdOut); err != nil || !found { + t.Fatalf("Output is not as expected: %s\n", stdOut) + } + + if err := os.Chdir(homeDir); err != nil { + t.Fatalf("Error on cd back to commands/ directory: %v", err) } } diff --git a/commands/testdata/master_test.zip b/commands/testdata/master_test.zip new file mode 100755 index 000000000..42b74f209 Binary files /dev/null and b/commands/testdata/master_test.zip differ diff --git a/commands/testdata/new_function/template/csharp/function/.gitignore b/commands/testdata/new_function/template/csharp/function/.gitignore new file mode 100644 index 000000000..e69de29bb diff --git a/commands/testdata/new_function/template/go-armhf/.gitignore b/commands/testdata/new_function/template/go-armhf/.gitignore new file mode 100644 index 000000000..e69de29bb diff --git a/commands/testdata/new_function/template/go/function/.gitignore b/commands/testdata/new_function/template/go/function/.gitignore new file mode 100644 index 000000000..e69de29bb diff --git a/commands/testdata/new_function/template/node-arm64/function/.gitignore b/commands/testdata/new_function/template/node-arm64/function/.gitignore new file mode 100644 index 000000000..e69de29bb diff --git a/commands/testdata/new_function/template/node-armhf/.gitignore b/commands/testdata/new_function/template/node-armhf/.gitignore new file mode 100644 index 000000000..e69de29bb diff --git a/commands/testdata/new_function/template/node/function/.gitignore b/commands/testdata/new_function/template/node/function/.gitignore new file mode 100644 index 000000000..e69de29bb diff --git a/commands/testdata/new_function/template/python-armhf/.gitignore b/commands/testdata/new_function/template/python-armhf/.gitignore new file mode 100644 index 000000000..e69de29bb diff --git a/commands/testdata/new_function/template/python/function/.gitignore b/commands/testdata/new_function/template/python/function/.gitignore new file mode 100644 index 000000000..e69de29bb diff --git a/commands/testdata/new_function/template/python3/function/.gitignore b/commands/testdata/new_function/template/python3/function/.gitignore new file mode 100644 index 000000000..e69de29bb diff --git a/commands/testdata/new_function/template/ruby/function/.gitignore b/commands/testdata/new_function/template/ruby/function/.gitignore new file mode 100644 index 000000000..e69de29bb diff --git a/guide/TEMPLATE.md b/guide/TEMPLATE.md new file mode 100644 index 000000000..572ac776d --- /dev/null +++ b/guide/TEMPLATE.md @@ -0,0 +1,53 @@ +# Using template from external repository + +## Repository structure + +The external repository must have a directory named ```template``` at the root directory, in which there are directories +containing templates. The directory for each template can be freely named with alphanumeric characters and hyphen. + +Example: + +``` +template +├── csharp +│   ├── Dockerfile +│   └── template.yml +├── node +│   ├── Dockerfile +│   └── template.yml +├── node-armhf +│   ├── Dockerfile +│   └── template.yml +├── python +│   ├── Dockerfile +│   └── template.yml +├── python-armhf +│   ├── Dockerfile +│   └── template.yml +├── php-56 +│   ├── Dockerfile +│   └── template.yml +├── php-71 +│   ├── Dockerfile +│   └── template.yml +├── php-latest +│   ├── Dockerfile +│   └── template.yml +└── ruby + ├── Dockerfile + └── template.yml +``` + +## Download external repository + +In order to build functions using 3rd party templates, you need to add 3rd templates before the build step, with the following command: + +``` +./faas-cli template pull https://github.com/itscaro/openfaas-template-php +``` + +If you need to update the downloaded repository, just add the flag `--overwrite` to the download command: + +``` +./faas-cli template pull https://github.com/itscaro/openfaas-template-php --override +``` diff --git a/proxy/deploy.go b/proxy/deploy.go index a7742dafb..d20e3cb9e 100644 --- a/proxy/deploy.go +++ b/proxy/deploy.go @@ -26,16 +26,6 @@ func DeployFunction(fprocess string, gateway string, functionName string, image var fprocessTemplate string if len(fprocess) > 0 { fprocessTemplate = fprocess - } else if language == "python" { - fprocessTemplate = "python index.py" - } else if language == "node" { - fprocessTemplate = "node index.js" - } else if language == "ruby" { - fprocessTemplate = "ruby index.rb" - } else if language == "csharp" { - fprocessTemplate = "dotnet ./root.dll" - } else if language == "python3" { - fprocessTemplate = "python3 index.py" } gateway = strings.TrimRight(gateway, "/") diff --git a/stack/language_template.go b/stack/language_template.go new file mode 100644 index 000000000..d612807f8 --- /dev/null +++ b/stack/language_template.go @@ -0,0 +1,61 @@ +// Copyright (c) Alex Ellis 2017. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +package stack + +import ( + "fmt" + "io/ioutil" + "net/url" + "os" + "strings" + + yaml "gopkg.in/yaml.v2" +) + +func ParseYAMLForLanguageTemplate(file string) (*LanguageTemplate, error) { + var err error + var fileData []byte + + urlParsed, err := url.Parse(file) + if err == nil && len(urlParsed.Scheme) > 0 { + fmt.Println("Parsed: " + urlParsed.String()) + fileData, err = fetchYAML(urlParsed) + if err != nil { + return nil, err + } + } else { + fileData, err = ioutil.ReadFile(file) + if err != nil { + return nil, err + } + } + + return ParseYAMLDataForLanguageTemplate(fileData) +} + +// iParseYAMLDataForLanguageTemplate parses YAML data into language template +// Use the alias ParseYAMLDataForLanguageTemplate +func ParseYAMLDataForLanguageTemplate(fileData []byte) (*LanguageTemplate, error) { + var langTemplate LanguageTemplate + var err error + + err = yaml.Unmarshal(fileData, &langTemplate) + if err != nil { + fmt.Printf("Error with YAML file\n") + return nil, err + } + + return &langTemplate, err +} + +func IsValidTemplate(lang string) bool { + var found bool + if strings.ToLower(lang) == "dockerfile" { + found = true + } else if _, err := os.Stat("./template/" + lang); err == nil { + found = true + } + + return found +} diff --git a/stack/language_template_test.go b/stack/language_template_test.go new file mode 100644 index 000000000..d784a5dfc --- /dev/null +++ b/stack/language_template_test.go @@ -0,0 +1,77 @@ +// Copyright (c) Alex Ellis 2017. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +package stack + +import ( + "fmt" + "os" + "reflect" + "testing" +) + +func Test_ParseYAMLDataForLanguageTemplate(t *testing.T) { + dataProvider := []struct { + Input string + Expected *LanguageTemplate + }{ + { + ` +language: python +fprocess: python index.py +`, + &LanguageTemplate{ + Language: "python", + FProcess: "python index.py", + }, + }, + { + ` +language: python +`, + &LanguageTemplate{ + Language: "python", + }, + }, + { + ` +fprocess: python index.py +`, + &LanguageTemplate{ + FProcess: "python index.py", + }, + }, + } + + for k, i := range dataProvider { + t.Run(fmt.Sprintf("%d", k), func(t *testing.T) { + if actual, err := ParseYAMLDataForLanguageTemplate([]byte(i.Input)); err != nil { + t.Errorf("test failed, %s", err) + } else { + if !reflect.DeepEqual(actual, i.Expected) { + t.Errorf("does not match expected result;\n parsedYAML: [%+v]\n expected: [%+v]", + actual, + i.Expected, + ) + } + } + }) + } +} + +func Test_IsValidTemplate(t *testing.T) { + if !IsValidTemplate("Dockerfile") || !IsValidTemplate("dockerfile") { + t.Fatalf("Dockerfile and dockerfile must be valid") + } + + if IsValidTemplate("unknown-language") { + t.Fatalf("unknown-language must be invalid") + } + + os.MkdirAll("template/python", 0600) + defer func() { + os.RemoveAll("template") + }() + if !IsValidTemplate("python") { + t.Fatalf("python must be valid") + } +} diff --git a/stack/schema.go b/stack/schema.go index ebab9182d..e243c48b2 100644 --- a/stack/schema.go +++ b/stack/schema.go @@ -47,3 +47,8 @@ type Services struct { Functions map[string]Function `yaml:"functions,omitempty"` Provider Provider `yaml:"provider,omitempty"` } + +type LanguageTemplate struct { + Language string `yaml:"language"` + FProcess string `yaml:"fprocess"` +} diff --git a/template/csharp/template.yml b/template/csharp/template.yml new file mode 100644 index 000000000..45caa67ed --- /dev/null +++ b/template/csharp/template.yml @@ -0,0 +1,2 @@ +language: csharp +fprocess: dotnet ./root.dll diff --git a/template/go-armhf/template.yml b/template/go-armhf/template.yml new file mode 100644 index 000000000..234cf4400 --- /dev/null +++ b/template/go-armhf/template.yml @@ -0,0 +1,2 @@ +language: go-armhf +fprocess: ./handler diff --git a/template/go/template.yml b/template/go/template.yml new file mode 100644 index 000000000..5d341d5d3 --- /dev/null +++ b/template/go/template.yml @@ -0,0 +1,2 @@ +language: go +fprocess: ./handler diff --git a/template/node-arm64/template.yml b/template/node-arm64/template.yml new file mode 100644 index 000000000..97e25c5f9 --- /dev/null +++ b/template/node-arm64/template.yml @@ -0,0 +1,2 @@ +language: node-arm64 +fprocess: node index.js diff --git a/template/node-armhf/template.yml b/template/node-armhf/template.yml new file mode 100644 index 000000000..c2313ae67 --- /dev/null +++ b/template/node-armhf/template.yml @@ -0,0 +1,2 @@ +language: node-armhf +fprocess: node index.js diff --git a/template/node/template.yml b/template/node/template.yml new file mode 100644 index 000000000..1117efa75 --- /dev/null +++ b/template/node/template.yml @@ -0,0 +1,2 @@ +language: node +fprocess: node index.js diff --git a/template/python-armhf/template.yml b/template/python-armhf/template.yml new file mode 100644 index 000000000..ff8986785 --- /dev/null +++ b/template/python-armhf/template.yml @@ -0,0 +1,2 @@ +language: python-armhf +fprocess: python index.py diff --git a/template/python/template.yml b/template/python/template.yml new file mode 100644 index 000000000..5487218c4 --- /dev/null +++ b/template/python/template.yml @@ -0,0 +1,2 @@ +language: python +fprocess: python index.py diff --git a/template/python3/template.yml b/template/python3/template.yml new file mode 100644 index 000000000..ea405e8b6 --- /dev/null +++ b/template/python3/template.yml @@ -0,0 +1,2 @@ +language: python3 +fprocess: python3 index.py diff --git a/template/ruby/template.yml b/template/ruby/template.yml new file mode 100644 index 000000000..417bd6133 --- /dev/null +++ b/template/ruby/template.yml @@ -0,0 +1,2 @@ +language: ruby +fprocess: ruby index.rb diff --git a/test/utils.go b/test/utils.go index 2311c3b3a..df65f526f 100644 --- a/test/utils.go +++ b/test/utils.go @@ -130,3 +130,25 @@ func CaptureStdout(f func()) string { return b.String() } + +// Copy the src file to dst. Any existing file will be overwritten and will not +// copy file attributes. +func Copy(src, dst string) error { + in, err := os.Open(src) + if err != nil { + return err + } + defer in.Close() + + out, err := os.Create(dst) + if err != nil { + return err + } + defer out.Close() + + _, err = io.Copy(out, in) + if err != nil { + return err + } + return out.Close() +}