diff --git a/builder/build.go b/builder/build.go index ecad52664..44c8cf06b 100644 --- a/builder/build.go +++ b/builder/build.go @@ -9,49 +9,51 @@ import ( "log" "os" "strings" + + "github.com/openfaas/faas-cli/stack" ) // BuildImage construct Docker image from function parameters func BuildImage(image string, handler string, functionName string, language string, nocache bool, squash bool, shrinkwrap 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")) + if shrinkwrap { + fmt.Printf("Nothing to do for: %s.\n", functionName) - if shrinkwrap { - fmt.Printf("%s shrink-wrapped to %s\n", functionName, tempPath) - } else { - builder := strings.Split(fmt.Sprintf("docker build %s-t %s .", flagStr, image), " ") + return + } - ExecCommand(tempPath, builder) - fmt.Printf("Image: %s built.\n", image) - } - break - case "Dockerfile", "dockerfile": - if shrinkwrap { - fmt.Printf("Nothing to do for: %s.\n", functionName) - } else { - tempPath := handler + 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) - break + return } fmt.Printf("Building: %s with Dockerfile. Please wait..\n", image) - flagStr := buildFlagString(nocache, squash, os.Getenv("http_proxy"), os.Getenv("https_proxy")) + } else { - 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) + tempPath = createBuildTemplate(functionName, handler, language) + fmt.Printf("Building: %s with %s template. Please wait..\n", image, language) + + if shrinkwrap { + fmt.Printf("%s shrink-wrapped to %s\n", functionName, tempPath) + + return + } } - default: + + flagStr := buildFlagString(nocache, squash, os.Getenv("http_proxy"), os.Getenv("https_proxy")) + builder := strings.Split(fmt.Sprintf("docker build %s-t %s .", flagStr, image), " ") + ExecCommand(tempPath, builder) + fmt.Printf("Image: %s built.\n", image) + + } else { log.Fatalf("Language template: %s not supported. Build a custom Dockerfile instead.", language) } } diff --git a/commands/build.go b/commands/build.go index 37f8057b2..567f7458c 100644 --- a/commands/build.go +++ b/commands/build.go @@ -149,7 +149,7 @@ func PullTemplates(templateUrl string) error { if err != nil || exists == nil { log.Println("No templates found in current directory.") - err = fetchTemplates(templateUrl) + err = fetchTemplates(templateUrl, 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 5efcda6f7..4ccc29800 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 languageExistsNotDockerfile(function.Language) { + 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 { @@ -277,3 +299,7 @@ func compileEnvironment(envvarOpts []string, yamlEnvironment map[string]string, functionAndStack := mergeMap(yamlEnvironment, fileEnvironment) return mergeMap(functionAndStack, envvarArguments), nil } + +func languageExistsNotDockerfile(language string) bool { + return len(language) > 0 && strings.ToLower(language) != "dockerfile" +} diff --git a/commands/fetch_templates.go b/commands/fetch_templates.go index f3187aaf0..f351f2189 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,182 @@ import ( "github.com/openfaas/faas-cli/proxy" ) -const ZipFileName string = "master.zip" +const ( + defaultTemplateRepository = "https://github.com/openfaas/faas-cli" + templateDirectory = "./template/" + rootLanguageDirSplitCount = 3 +) + +type ExtractAction int + +const ( + ShouldExtractData ExtractAction = iota + NewTemplateFound + DirectoryAlreadyExists + SkipWritingData +) // fetchTemplates fetch code templates from GitHub master zip file. -func fetchTemplates(templateUrl string) error { +func fetchTemplates(templateURL string, overwrite bool) error { - err := fetchMasterZip(templateUrl) + if len(templateURL) == 0 { + templateURL = defaultTemplateRepository + } - zipFile, err := zip.OpenReader(ZipFileName) + archive, err := fetchMasterZip(templateURL) if err != nil { + removeArchive(archive) return err } - log.Printf("Attempting to expand templates from %s\n", ZipFileName) + log.Printf("Attempting to expand templates from %s\n", archive) + + preExistingLanguages, fetchedLanguages, err := expandTemplatesFromZip(archive, overwrite) + if err != nil { + return err + } + + if len(preExistingLanguages) > 0 { + log.Printf("Cannot overwrite the following %d directories: %v\n", len(preExistingLanguages), preExistingLanguages) + } + + log.Printf("Fetched %d template(s) : %v from %s\n", len(fetchedLanguages), fetchedLanguages, templateURL) + + err = removeArchive(archive) + + return err +} + +// expandTemplatesFromZip() takes a path to an archive, and whether or not +// we are allowed to overwrite pre-existing language templates. It returns +// a list of languages that already exist (could not be overwritten), and +// a list of languages that are newly downloaded. +func expandTemplatesFromZip(archive string, overwrite bool) ([]string, []string, error) { + var existingLanguages []string + var fetchedLanguages []string + availableLanguages := make(map[string]bool) + + zipFile, err := zip.OpenReader(archive) + if err != nil { + return nil, nil, err + } 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 + + relativePath := z.Name[strings.Index(z.Name, "/")+1:] + if strings.Index(relativePath, "template/") != 0 { + // Process only directories inside "template" at root + continue + } + + action, language, isDirectory := canExpandTemplateData(availableLanguages, relativePath) + + var expandFromZip bool + + switch action { + + case ShouldExtractData: + expandFromZip = true + case NewTemplateFound: + expandFromZip = true + fetchedLanguages = append(fetchedLanguages, language) + case DirectoryAlreadyExists: + expandFromZip = false + existingLanguages = append(existingLanguages, language) + case SkipWritingData: + expandFromZip = false + default: + return nil, nil, errors.New(fmt.Sprintf("Don't know what to do when extracting zip: %s", archive)) + + } + + if expandFromZip { + if rc, err = z.Open(); err != nil { + break } - err = createPath(relativePath, z.Mode()) - if err != nil { - return err + 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:] != string(os.PathSeparator) { - err = writeFile(rc, z.UncompressedSize64, relativePath, z.Mode()) - if err != nil { - return err + if len(relativePath) > 1 && !isDirectory { + if err = writeFile(rc, z.UncompressedSize64, relativePath, z.Mode()); err != nil { + return nil, nil, err } } } } + zipFile.Close() + return existingLanguages, fetchedLanguages, nil +} + +// canExpandTemplateData() takes the map of available languages, and the +// path to a file in the zip archive. Returns what we should do with the file +// in form of ExtractAction enum, the language name, and whether it is a directory +func canExpandTemplateData(availableLanguages map[string]bool, relativePath string) (ExtractAction, string, bool) { + if pathSplit := strings.Split(relativePath, "/"); len(pathSplit) > 2 { + language := pathSplit[1] + + // We know that this path is a directory if the last character is a "/" + isDirectory := relativePath[len(relativePath)-1:] == "/" + + // Check if this is the root directory for a language (at ./template/lang) + if len(pathSplit) == rootLanguageDirSplitCount && isDirectory { + if !canWriteLanguage(availableLanguages, language, overwrite) { + return DirectoryAlreadyExists, language, isDirectory + } + return NewTemplateFound, language, isDirectory + } else { + if !canWriteLanguage(availableLanguages, language, overwrite) { + return SkipWritingData, language, isDirectory + } + return ShouldExtractData, language, isDirectory + } + } + // template/ + return SkipWritingData, "", true +} + +// 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 +202,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 +235,29 @@ func createPath(relativePath string, perms os.FileMode) error { err := os.MkdirAll(dir, perms) return err } + +// canWriteLanguage() tells whether the language can be expanded from the zip or not. +// availableLanguages map keeps track of which languages we know to be okay to copy. +// overwrite flag will allow to force copy the language template +func canWriteLanguage(availableLanguages map[string]bool, language string, overwrite bool) bool { + canWrite := false + if availableLanguages != nil && len(language) > 0 { + if _, found := availableLanguages[language]; found { + return availableLanguages[language] + } + canWrite = templateFolderExists(language, overwrite) + availableLanguages[language] = canWrite + } + + return canWrite +} + +// Takes a language input (e.g. "node"), tells whether or not it is OK to download +func templateFolderExists(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..44a93f257 100644 --- a/commands/fetch_templates_test.go +++ b/commands/fetch_templates_test.go @@ -1,4 +1,4 @@ -// Copyright (c) Alex Ellis, Eric Stoekl 2017. All rights reserved. +// 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 commands @@ -9,16 +9,50 @@ 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_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...") + defer tearDown_fetch_templates(t) + + // 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 := PullTemplates(ts.URL) + if err != nil { + t.Error(err) + } + + tearDown_fetch_templates(t) +} + +func Test_fetchTemplates(t *testing.T) { + defer tearDown_fetch_templates(t) + + // 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 := os.Remove("master.zip") + err := fetchTemplates(ts.URL+"/owner/repo", false) + if err != nil { + t.Error(err) + } + + tearDown_fetch_templates(t) +} + +// tearDown_fetch_templates_test cleans all files and directories created by the test +func tearDown_fetch_templates(t *testing.T) { + + // 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 +62,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 e5e58ae6e..a9f9a4e41 100644 --- a/commands/new_function.go +++ b/commands/new_function.go @@ -7,11 +7,12 @@ import ( "fmt" "io/ioutil" "os" - "path" + "sort" "strings" "github.com/morikuni/aec" "github.com/openfaas/faas-cli/builder" + "github.com/openfaas/faas-cli/stack" "github.com/spf13/cobra" ) @@ -20,6 +21,13 @@ var ( list bool ) +// Implement interface for sorting array of strings +type StrSort []string + +func (a StrSort) Len() int { return len(a) } +func (a StrSort) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a StrSort) Less(i, j int) bool { return a[i] < a[j] } + func init() { newFunctionCmd.Flags().StringVar(&lang, "lang", "", "Language or template to use") newFunctionCmd.Flags().StringVarP(&gateway, "gateway", "g", defaultGateway, "Gateway URL to store in YAML stack file") @@ -42,15 +50,23 @@ language or type in --list for a list of languages available.`, } func runNewFunction(cmd *cobra.Command, args []string) { + if list == true { + var availableTemplates []string + + 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,8 +87,9 @@ the "Dockerfile" lang type in your YAML file. 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 { @@ -134,14 +151,11 @@ functions: return } -func validTemplate(lang string) bool { - var found bool - if strings.ToLower(lang) != "dockerfile" { - found = true +func printAvailableTemplates(availableTemplates []string) string { + var result string + sort.Sort(StrSort(availableTemplates)) + for _, template := range availableTemplates { + result += fmt.Sprintf("- %s\n", template) } - if _, err := os.Stat(path.Join("./template/", lang)); err == nil { - found = true - } - - return found + return result } diff --git a/commands/new_function_test.go b/commands/new_function_test.go index 642175f9f..eb69e381c 100644 --- a/commands/new_function_test.go +++ b/commands/new_function_test.go @@ -5,37 +5,195 @@ 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: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, + }, + { + title: "invalid_1", + funcName: "new-test-invalid-1", + funcLang: "dockerfilee", + expectedMsg: LangNotExistsOutput, + }, +} + +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/template_pull.go b/commands/template_pull.go new file mode 100644 index 000000000..0cf5ce8b0 --- /dev/null +++ b/commands/template_pull.go @@ -0,0 +1,80 @@ +// 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 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() { + templatePullCmd.Flags().BoolVar(&overwrite, "overwrite", false, "Overwrite existing templates?") + + faasCmd.AddCommand(templatePullCmd) +} + +// templatePullCmd allows the user to fetch a template from a repository +var templatePullCmd = &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: runTemplatePull, +} + +func runTemplatePull(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/template_pull_test.go b/commands/template_pull_test.go new file mode 100644 index 000000000..86c0a178f --- /dev/null +++ b/commands/template_pull_test.go @@ -0,0 +1,144 @@ +// 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 commands + +import ( + "bytes" + "io/ioutil" + "log" + "net/http" + "net/http/httptest" + "os" + "regexp" + "strings" + "testing" +) + +func Test_templatePull(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_templatePull_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_templatePull_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_templatePull_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/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/csharp/template.yml b/commands/testdata/new_function/template/csharp/template.yml new file mode 100644 index 000000000..45caa67ed --- /dev/null +++ b/commands/testdata/new_function/template/csharp/template.yml @@ -0,0 +1,2 @@ +language: csharp +fprocess: dotnet ./root.dll diff --git a/commands/testdata/new_function/template/go-armhf/template.yml b/commands/testdata/new_function/template/go-armhf/template.yml new file mode 100644 index 000000000..234cf4400 --- /dev/null +++ b/commands/testdata/new_function/template/go-armhf/template.yml @@ -0,0 +1,2 @@ +language: go-armhf +fprocess: ./handler 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/go/template.yml b/commands/testdata/new_function/template/go/template.yml new file mode 100644 index 000000000..5d341d5d3 --- /dev/null +++ b/commands/testdata/new_function/template/go/template.yml @@ -0,0 +1,2 @@ +language: go +fprocess: ./handler 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-arm64/template.yml b/commands/testdata/new_function/template/node-arm64/template.yml new file mode 100644 index 000000000..97e25c5f9 --- /dev/null +++ b/commands/testdata/new_function/template/node-arm64/template.yml @@ -0,0 +1,2 @@ +language: node-arm64 +fprocess: node index.js diff --git a/commands/testdata/new_function/template/node-armhf/template.yml b/commands/testdata/new_function/template/node-armhf/template.yml new file mode 100644 index 000000000..c2313ae67 --- /dev/null +++ b/commands/testdata/new_function/template/node-armhf/template.yml @@ -0,0 +1,2 @@ +language: node-armhf +fprocess: node index.js 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/node/template.yml b/commands/testdata/new_function/template/node/template.yml new file mode 100644 index 000000000..1117efa75 --- /dev/null +++ b/commands/testdata/new_function/template/node/template.yml @@ -0,0 +1,2 @@ +language: node +fprocess: node index.js diff --git a/commands/testdata/new_function/template/python-armhf/template.yml b/commands/testdata/new_function/template/python-armhf/template.yml new file mode 100644 index 000000000..ff8986785 --- /dev/null +++ b/commands/testdata/new_function/template/python-armhf/template.yml @@ -0,0 +1,2 @@ +language: python-armhf +fprocess: python index.py 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/python/template.yml b/commands/testdata/new_function/template/python/template.yml new file mode 100644 index 000000000..5487218c4 --- /dev/null +++ b/commands/testdata/new_function/template/python/template.yml @@ -0,0 +1,2 @@ +language: python +fprocess: python index.py 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/python3/template.yml b/commands/testdata/new_function/template/python3/template.yml new file mode 100644 index 000000000..ea405e8b6 --- /dev/null +++ b/commands/testdata/new_function/template/python3/template.yml @@ -0,0 +1,2 @@ +language: python3 +fprocess: python3 index.py 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/commands/testdata/new_function/template/ruby/template.yml b/commands/testdata/new_function/template/ruby/template.yml new file mode 100644 index 000000000..417bd6133 --- /dev/null +++ b/commands/testdata/new_function/template/ruby/template.yml @@ -0,0 +1,2 @@ +language: ruby +fprocess: ruby index.rb diff --git a/guide/TEMPLATE.md b/guide/TEMPLATE.md new file mode 100644 index 000000000..f4041dd46 --- /dev/null +++ b/guide/TEMPLATE.md @@ -0,0 +1,56 @@ +# 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 +├── php5 +│   ├── Dockerfile +│   └── template.yml +├── php7 +│   ├── 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: + +```bash +./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: + +```bash +./faas-cli template pull https://github.com/itscaro/openfaas-template-php --override +``` + +## List locally available languages + +```bash +./faas-cli new --list +``` diff --git a/proxy/deploy.go b/proxy/deploy.go index 743c2248e..f331d4d4d 100644 --- a/proxy/deploy.go +++ b/proxy/deploy.go @@ -24,16 +24,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..e8c035d2c --- /dev/null +++ b/stack/language_template.go @@ -0,0 +1,65 @@ +// 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) +} + +// ParseYAMLDataForLanguageTemplate parses YAML data into language template +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 { + templateYAMLPath := "./template/" + lang + "/template.yml" + + _, err := ParseYAMLForLanguageTemplate(templateYAMLPath) + if 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..13f21b341 --- /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) { + langTemplateTest := []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 langTemplateTest { + 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 is not valid because it does not contain template.yml") + } +} diff --git a/stack/schema.go b/stack/schema.go index ebab9182d..8828413f5 100644 --- a/stack/schema.go +++ b/stack/schema.go @@ -47,3 +47,9 @@ type Services struct { Functions map[string]Function `yaml:"functions,omitempty"` Provider Provider `yaml:"provider,omitempty"` } + +// LanguageTemplate read from template.yml within root of a language template folder +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/function/handler.go b/template/go-armhf/function/handler.go new file mode 100644 index 000000000..0c3f15fb1 --- /dev/null +++ b/template/go-armhf/function/handler.go @@ -0,0 +1,10 @@ +package function + +import ( + "fmt" +) + +// Handle a serverless request +func Handle(req []byte) string { + return fmt.Sprintf("Hello, Go. You said: %s", string(req)) +} diff --git a/template/go-armhf/main.go b/template/go-armhf/main.go new file mode 100644 index 000000000..2816215c9 --- /dev/null +++ b/template/go-armhf/main.go @@ -0,0 +1,19 @@ +package main + +import ( + "fmt" + "io/ioutil" + "log" + "os" + + "handler/function" +) + +func main() { + input, err := ioutil.ReadAll(os.Stdin) + if err != nil { + log.Fatalf("Unable to read standard input: %s", err.Error()) + } + + fmt.Println(function.Handle(input)) +} 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/build.sh b/template/node-armhf/build.sh new file mode 100755 index 000000000..3e0953991 --- /dev/null +++ b/template/node-armhf/build.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +echo "Building functions/base:node-6.9.1-alpine" +docker build -t functions/base:node-6.9.1-alpine . + diff --git a/template/node-armhf/function/handler.js b/template/node-armhf/function/handler.js new file mode 100644 index 000000000..73344f435 --- /dev/null +++ b/template/node-armhf/function/handler.js @@ -0,0 +1,5 @@ +"use strict" + +module.exports = (context, callback) => { + callback(undefined, {status: "done"}); +} diff --git a/template/node-armhf/function/package.json b/template/node-armhf/function/package.json new file mode 100644 index 000000000..3d3a3e456 --- /dev/null +++ b/template/node-armhf/function/package.json @@ -0,0 +1,12 @@ +{ + "name": "function", + "version": "1.0.0", + "description": "", + "main": "handler.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC" +} diff --git a/template/node-armhf/index.js b/template/node-armhf/index.js new file mode 100644 index 000000000..1ad234276 --- /dev/null +++ b/template/node-armhf/index.js @@ -0,0 +1,31 @@ +// Copyright (c) Alex Ellis 2017. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +"use strict" + +let getStdin = require('get-stdin'); + +let handler = require('./function/handler'); + +getStdin().then(val => { + handler(val, (err, res) => { + if (err) { + return console.error(err); + } + if(isArray(res) || isObject(res)) { + console.log(JSON.stringify(res)); + } else { + process.stdout.write(res); + } + }); +}).catch(e => { + console.error(e.stack); +}); + +let isArray = (a) => { + return (!!a) && (a.constructor === Array); +}; + +let isObject = (a) => { + return (!!a) && (a.constructor === Object); +}; diff --git a/template/node-armhf/package.json b/template/node-armhf/package.json new file mode 100644 index 000000000..d35e9625d --- /dev/null +++ b/template/node-armhf/package.json @@ -0,0 +1,15 @@ +{ + "name": "NodejsBase", + "version": "1.0.0", + "description": "", + "main": "faas_index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "get-stdin": "^5.0.1" + } +} 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/function/handler.py b/template/python-armhf/function/handler.py new file mode 100644 index 000000000..1f69d4f47 --- /dev/null +++ b/template/python-armhf/function/handler.py @@ -0,0 +1,2 @@ +def handle(st): + print(st) diff --git a/template/python-armhf/function/requirements.txt b/template/python-armhf/function/requirements.txt new file mode 100644 index 000000000..e69de29bb diff --git a/template/python-armhf/index.py b/template/python-armhf/index.py new file mode 100644 index 000000000..415dd8e2d --- /dev/null +++ b/template/python-armhf/index.py @@ -0,0 +1,15 @@ +# Copyright (c) Alex Ellis 2017. All rights reserved. +# Licensed under the MIT license. See LICENSE file in the project root for full license information. + +import sys +from function import handler + +def get_stdin(): + buf = "" + for line in sys.stdin: + buf = buf + line + return buf + +if(__name__ == "__main__"): + st = get_stdin() + handler.handle(st) diff --git a/template/python-armhf/requirements.txt b/template/python-armhf/requirements.txt new file mode 100644 index 000000000..e69de29bb 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