349 lines
9.9 KiB
Go
349 lines
9.9 KiB
Go
// 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 (
|
|
"fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/openfaas/faas-cli/builder"
|
|
"github.com/openfaas/faas-cli/stack"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
var (
|
|
appendFile string
|
|
list bool
|
|
quiet bool
|
|
memoryLimit string
|
|
cpuLimit string
|
|
memoryRequest string
|
|
cpuRequest string
|
|
)
|
|
|
|
func init() {
|
|
newFunctionCmd.Flags().StringVar(&language, "lang", "", "Language or template to use")
|
|
newFunctionCmd.Flags().StringVarP(&gateway, "gateway", "g", defaultGateway, "Gateway URL to store in YAML stack file")
|
|
newFunctionCmd.Flags().StringVar(&handlerDir, "handler", "", "directory the handler will be written to")
|
|
newFunctionCmd.Flags().StringVarP(&imagePrefix, "prefix", "p", "", "Set prefix for the function image")
|
|
|
|
newFunctionCmd.Flags().StringVar(&memoryLimit, "memory-limit", "", "Set a limit for the memory")
|
|
newFunctionCmd.Flags().StringVar(&cpuLimit, "cpu-limit", "", "Set a limit for the CPU")
|
|
|
|
newFunctionCmd.Flags().StringVar(&memoryRequest, "memory-request", "", "Set a request or the memory")
|
|
newFunctionCmd.Flags().StringVar(&cpuRequest, "cpu-request", "", "Set a request value for the CPU")
|
|
|
|
newFunctionCmd.Flags().BoolVar(&list, "list", false, "List available languages")
|
|
newFunctionCmd.Flags().StringVarP(&appendFile, "append", "a", "", "Append to existing YAML file")
|
|
newFunctionCmd.Flags().BoolVarP(&quiet, "quiet", "q", false, "Skip template notes")
|
|
|
|
faasCmd.AddCommand(newFunctionCmd)
|
|
}
|
|
|
|
// newFunctionCmd displays newFunction information
|
|
var newFunctionCmd = &cobra.Command{
|
|
Use: "new FUNCTION_NAME --lang=FUNCTION_LANGUAGE [--gateway=http://host:port] | --list | --append=STACK_FILE)",
|
|
Short: "Create a new template in the current folder with the name given as name",
|
|
Long: `The new command creates a new function based upon hello-world in the given
|
|
language or type in --list for a list of languages available.`,
|
|
Example: ` faas-cli new chatbot --lang node
|
|
faas-cli new chatbot --lang node --append stack.yml
|
|
faas-cli new text-parser --lang python --quiet
|
|
faas-cli new text-parser --lang python --gateway http://mydomain:8080
|
|
faas-cli new --list`,
|
|
PreRunE: preRunNewFunction,
|
|
RunE: runNewFunction,
|
|
}
|
|
|
|
// validateFunctionName provides least-common-denominator validation - i.e. only allows valid Kubernetes services names
|
|
func validateFunctionName(functionName string) error {
|
|
// Regex for RFC-1123 validation:
|
|
// k8s.io/kubernetes/pkg/util/validation/validation.go
|
|
var validDNS = regexp.MustCompile(`^[a-z0-9]([-a-z0-9]*[a-z0-9])?$`)
|
|
if matched := validDNS.MatchString(functionName); !matched {
|
|
return fmt.Errorf(`function name can only contain a-z, 0-9 and dashes`)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// preRunNewFunction validates args & flags
|
|
func preRunNewFunction(cmd *cobra.Command, args []string) error {
|
|
if list == true {
|
|
return nil
|
|
}
|
|
|
|
language, _ = validateLanguageFlag(language)
|
|
|
|
if len(language) == 0 && len(args) < 1 {
|
|
cmd.Help()
|
|
os.Exit(0)
|
|
}
|
|
if len(language) == 0 {
|
|
return fmt.Errorf("you must supply a function language with the --lang flag")
|
|
}
|
|
|
|
if len(args) < 1 {
|
|
return fmt.Errorf(`please provide a name for the function`)
|
|
}
|
|
|
|
functionName = args[0]
|
|
|
|
if err := validateFunctionName(functionName); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func runNewFunction(cmd *cobra.Command, args []string) error {
|
|
if list == true {
|
|
var availableTemplates []string
|
|
|
|
templateFolders, err := ioutil.ReadDir(templateDirectory)
|
|
if err != nil {
|
|
return fmt.Errorf(`no language templates were found.
|
|
|
|
Download templates:
|
|
faas-cli template pull download the default templates
|
|
faas-cli template store list view the community template store`)
|
|
}
|
|
|
|
for _, file := range templateFolders {
|
|
if file.IsDir() {
|
|
availableTemplates = append(availableTemplates, file.Name())
|
|
}
|
|
}
|
|
|
|
fmt.Printf("Languages available as templates:\n%s\n", printAvailableTemplates(availableTemplates))
|
|
|
|
return nil
|
|
}
|
|
|
|
templateAddress := getTemplateURL("", os.Getenv(templateURLEnvironment), DefaultTemplateRepository)
|
|
PullTemplates(templateAddress)
|
|
|
|
if !stack.IsValidTemplate(language) {
|
|
return fmt.Errorf("%s is unavailable or not supported", language)
|
|
}
|
|
|
|
var fileName, outputMsg string
|
|
appendMode := len(appendFile) > 0
|
|
|
|
if appendMode {
|
|
if (strings.HasSuffix(appendFile, ".yml") || strings.HasSuffix(appendFile, ".yaml")) == false {
|
|
return fmt.Errorf("when appending to a stack the suffix should be .yml or .yaml")
|
|
}
|
|
|
|
if _, statErr := os.Stat(appendFile); statErr != nil {
|
|
return fmt.Errorf("unable to find file: %s - %s", appendFile, statErr.Error())
|
|
}
|
|
|
|
var duplicateError error
|
|
duplicateError = duplicateFunctionName(functionName, appendFile)
|
|
|
|
if duplicateError != nil {
|
|
return duplicateError
|
|
}
|
|
|
|
fileName = appendFile
|
|
outputMsg = fmt.Sprintf("Stack file updated: %s\n", fileName)
|
|
|
|
} else {
|
|
gateway = getGatewayURL(gateway, defaultGateway, gateway, os.Getenv(openFaaSURLEnvironment))
|
|
fileName = functionName + ".yml"
|
|
outputMsg = fmt.Sprintf("Stack file written: %s\n", fileName)
|
|
}
|
|
|
|
if len(handlerDir) == 0 {
|
|
handlerDir = functionName
|
|
}
|
|
|
|
if _, err := os.Stat(handlerDir); err == nil {
|
|
return fmt.Errorf("folder: %s already exists", handlerDir)
|
|
}
|
|
|
|
_, err := os.Stat(fileName)
|
|
if err == nil && appendMode == false {
|
|
return fmt.Errorf("file: %s already exists", fileName)
|
|
}
|
|
|
|
if err := os.Mkdir(handlerDir, 0700); err != nil {
|
|
return fmt.Errorf("folder: could not create %s : %s", handlerDir, err)
|
|
}
|
|
fmt.Printf("Folder: %s created.\n", handlerDir)
|
|
|
|
if err := updateGitignore(); err != nil {
|
|
return fmt.Errorf("got unexpected error while updating .gitignore file: %s", err)
|
|
}
|
|
|
|
pathToTemplateYAML := fmt.Sprintf("./template/%s/template.yml", language)
|
|
if _, err := os.Stat(pathToTemplateYAML); os.IsNotExist(err) {
|
|
return err
|
|
}
|
|
|
|
langTemplate, err := stack.ParseYAMLForLanguageTemplate(pathToTemplateYAML)
|
|
if err != nil {
|
|
return fmt.Errorf("error reading language template: %s", err.Error())
|
|
}
|
|
|
|
templateHandlerFolder := "function"
|
|
if len(langTemplate.HandlerFolder) > 0 {
|
|
templateHandlerFolder = langTemplate.HandlerFolder
|
|
}
|
|
|
|
fromTemplateHandler := filepath.Join("template", language, templateHandlerFolder)
|
|
|
|
// Create function directory from template.
|
|
builder.CopyFiles(fromTemplateHandler, handlerDir)
|
|
printLogo()
|
|
fmt.Printf("\nFunction created in folder: %s\n", handlerDir)
|
|
|
|
imageName := fmt.Sprintf("%s:latest", functionName)
|
|
|
|
imagePrefixVal := getPrefixValue()
|
|
|
|
if imagePrefixVal = strings.TrimSpace(imagePrefixVal); len(imagePrefixVal) > 0 {
|
|
imageName = fmt.Sprintf("%s/%s", imagePrefixVal, imageName)
|
|
}
|
|
|
|
function := stack.Function{
|
|
Name: functionName,
|
|
Handler: "./" + handlerDir,
|
|
Language: language,
|
|
Image: imageName,
|
|
}
|
|
|
|
if len(memoryLimit) > 0 || len(cpuLimit) > 0 {
|
|
function.Limits = &stack.FunctionResources{
|
|
CPU: cpuLimit,
|
|
Memory: memoryLimit,
|
|
}
|
|
}
|
|
|
|
if len(memoryRequest) > 0 || len(cpuRequest) > 0 {
|
|
function.Requests = &stack.FunctionResources{
|
|
CPU: cpuRequest,
|
|
Memory: memoryRequest,
|
|
}
|
|
}
|
|
|
|
yamlContent := prepareYAMLContent(appendMode, gateway, &function)
|
|
|
|
f, err := os.OpenFile("./"+fileName, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600)
|
|
if err != nil {
|
|
return fmt.Errorf("could not open file '%s' %s", fileName, err)
|
|
}
|
|
|
|
_, stackWriteErr := f.Write([]byte(yamlContent))
|
|
if stackWriteErr != nil {
|
|
return fmt.Errorf("error writing stack file %s", stackWriteErr)
|
|
}
|
|
|
|
fmt.Print(outputMsg)
|
|
|
|
if !quiet {
|
|
languageTemplate, _ := stack.LoadLanguageTemplate(language)
|
|
|
|
if languageTemplate.WelcomeMessage != "" {
|
|
fmt.Printf("\nNotes:\n")
|
|
fmt.Printf("%s\n", languageTemplate.WelcomeMessage)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func getPrefixValue() string {
|
|
prefix := ""
|
|
if len(imagePrefix) > 0 {
|
|
return imagePrefix
|
|
}
|
|
|
|
if val, ok := os.LookupEnv("OPENFAAS_PREFIX"); ok && len(val) > 0 {
|
|
prefix = val
|
|
}
|
|
return prefix
|
|
}
|
|
|
|
func prepareYAMLContent(appendMode bool, gateway string, function *stack.Function) (yamlContent string) {
|
|
|
|
yamlContent = ` ` + function.Name + `:
|
|
lang: ` + function.Language + `
|
|
handler: ` + function.Handler + `
|
|
image: ` + function.Image + `
|
|
`
|
|
|
|
if function.Requests != nil && (len(function.Requests.CPU) > 0 || len(function.Requests.Memory) > 0) {
|
|
yamlContent += " requests:\n"
|
|
if len(function.Requests.CPU) > 0 {
|
|
yamlContent += ` cpu: ` + function.Requests.CPU + "\n"
|
|
}
|
|
|
|
if len(function.Requests.Memory) > 0 {
|
|
yamlContent += ` memory: ` + function.Requests.Memory + "\n"
|
|
}
|
|
}
|
|
|
|
if function.Limits != nil && (len(function.Limits.CPU) > 0 || len(function.Limits.Memory) > 0) {
|
|
yamlContent += " limits:\n"
|
|
if len(function.Limits.CPU) > 0 {
|
|
yamlContent += ` cpu: ` + function.Limits.CPU + "\n"
|
|
}
|
|
|
|
if len(function.Limits.Memory) > 0 {
|
|
yamlContent += ` memory: ` + function.Limits.Memory + "\n"
|
|
}
|
|
}
|
|
|
|
yamlContent += "\n"
|
|
if !appendMode {
|
|
|
|
yamlContent = `version: ` + defaultSchemaVersion + `
|
|
provider:
|
|
name: openfaas
|
|
gateway: ` + gateway + `
|
|
functions:
|
|
` + yamlContent
|
|
}
|
|
|
|
return yamlContent
|
|
}
|
|
|
|
func printAvailableTemplates(availableTemplates []string) string {
|
|
var result string
|
|
sort.Slice(availableTemplates, func(i, j int) bool {
|
|
return availableTemplates[i] < availableTemplates[j]
|
|
})
|
|
for _, template := range availableTemplates {
|
|
result += fmt.Sprintf("- %s\n", template)
|
|
}
|
|
return result
|
|
}
|
|
|
|
func duplicateFunctionName(functionName string, appendFile string) error {
|
|
fileBytes, readErr := ioutil.ReadFile(appendFile)
|
|
if readErr != nil {
|
|
return fmt.Errorf("unable to read %s to append, %s", appendFile, readErr)
|
|
}
|
|
|
|
services, parseErr := stack.ParseYAMLData(fileBytes, "", "", envsubst)
|
|
|
|
if parseErr != nil {
|
|
return fmt.Errorf("Error parsing %s yml file", appendFile)
|
|
}
|
|
|
|
if _, exists := services.Functions[functionName]; exists {
|
|
return fmt.Errorf(`
|
|
Function %s already exists in %s file.
|
|
Cannot have duplicate function names in same yaml file`, functionName, appendFile)
|
|
}
|
|
|
|
return nil
|
|
}
|