github.com/tonto/cli@v0.0.0-20180104210444-aec958fa47db/init.go (about)

     1  package main
     2  
     3  /*
     4   usage: fn init --help
     5  
     6   o If there's a Dockerfile found, this will generate a basic
     7     function file with the image and 'docker' as 'runtime'
     8     like following, for example:
     9  
    10     name: hello
    11     version: 0.0.1
    12     runtime: docker
    13     path: /hello
    14  
    15     then exit; if 'runtime' is 'docker' in the function file
    16     and no Dockerfile exists,  print an error message then exit
    17   o It will then try to decipher the runtime based on
    18     the files in the current directory, if it can't figure it out,
    19     it will print an error message then exit.
    20  */
    21  
    22  import (
    23  	"errors"
    24  	"fmt"
    25  	"os"
    26  	"path/filepath"
    27  	"strings"
    28  
    29  	"github.com/fnproject/cli/langs"
    30  	"github.com/fnproject/fn_go/models"
    31  	"github.com/urfave/cli"
    32  )
    33  
    34  type initFnCmd struct {
    35  	force bool
    36  	ff    *funcfile
    37  }
    38  
    39  func initFlags(a *initFnCmd) []cli.Flag {
    40  	fgs := []cli.Flag{
    41  		cli.StringFlag{
    42  			Name:        "name",
    43  			Usage:       "name of the function. Defaults to directory name.",
    44  			Destination: &a.ff.Name,
    45  		},
    46  		cli.BoolFlag{
    47  			Name:        "force",
    48  			Usage:       "overwrite existing func.yaml",
    49  			Destination: &a.force,
    50  		},
    51  		cli.StringFlag{
    52  			Name:        "runtime",
    53  			Usage:       "choose an existing runtime - " + langsList(),
    54  			Destination: &a.ff.Runtime,
    55  		},
    56  		cli.StringFlag{
    57  			Name:        "entrypoint",
    58  			Usage:       "entrypoint is the command to run to start this function - equivalent to Dockerfile ENTRYPOINT.",
    59  			Destination: &a.ff.Entrypoint,
    60  		},
    61  		cli.StringFlag{
    62  			Name:        "cmd",
    63  			Usage:       "command to run to start this function - equivalent to Dockerfile CMD.",
    64  			Destination: &a.ff.Entrypoint,
    65  		},
    66  		cli.StringFlag{
    67  			Name:        "version",
    68  			Usage:       "set initial function version",
    69  			Destination: &a.ff.Version,
    70  			Value:       initialVersion,
    71  		},
    72  	}
    73  
    74  	return append(fgs, routeFlags...)
    75  }
    76  
    77  func langsList() string {
    78  	allLangs := []string{}
    79  	for _, h := range langs.Helpers() {
    80  		allLangs = append(allLangs, h.LangStrings()...)
    81  	}
    82  	return strings.Join(allLangs, ", ")
    83  }
    84  
    85  func initFn() cli.Command {
    86  	a := &initFnCmd{ff: &funcfile{}}
    87  
    88  	return cli.Command{
    89  		Name:        "init",
    90  		Usage:       "create a local func.yaml file",
    91  		Description: "Creates a func.yaml file in the current directory.",
    92  		ArgsUsage:   "[FUNCTION_NAME]",
    93  		Action:      a.init,
    94  		Flags:       initFlags(a),
    95  	}
    96  }
    97  
    98  func (a *initFnCmd) init(c *cli.Context) error {
    99  	wd := getWd()
   100  
   101  	var rt models.Route
   102  	routeWithFlags(c, &rt)
   103  	a.bindRoute(&rt)
   104  
   105  	runtimeSpecified := a.ff.Runtime != ""
   106  	if runtimeSpecified {
   107  		// go no further if the specified runtime is not supported
   108  		if a.ff.Runtime != funcfileDockerRuntime && langs.GetLangHelper(a.ff.Runtime) == nil {
   109  			return fmt.Errorf("Init does not support the '%s' runtime.", a.ff.Runtime)
   110  		}
   111  	}
   112  
   113  	var err error
   114  	path := c.Args().First()
   115  	if path != "" {
   116  		fmt.Printf("Creating function at: /%s\n", path)
   117  		dir := filepath.Join(wd, path)
   118  		// check if dir exists, if it does, then we can't create function
   119  		if exists(dir) {
   120  			if !a.force {
   121  				return fmt.Errorf("directory %s already exists, cannot init function", dir)
   122  			}
   123  		} else {
   124  			err = os.MkdirAll(dir, 0755)
   125  			if err != nil {
   126  				return err
   127  			}
   128  		}
   129  		err = os.Chdir(dir)
   130  		if err != nil {
   131  			return err
   132  		}
   133  		defer os.Chdir(wd) // todo: wrap this so we can log the error if changing back fails
   134  	}
   135  
   136  	if !a.force {
   137  		_, ff, err := loadFuncfile()
   138  		if _, ok := err.(*notFoundError); !ok && err != nil {
   139  			return err
   140  		}
   141  		if ff != nil {
   142  			return errors.New("Function file already exists, aborting.")
   143  		}
   144  	}
   145  
   146  	err = a.buildFuncFile(c) // TODO: Return LangHelper here, then don't need to refind the helper in generateBoilerplate() below
   147  	if err != nil {
   148  		return err
   149  	}
   150  
   151  	// TODO: why don't we treat "docker" runtime as just another language helper? Then can get rid of several Docker
   152  	// specific if/else's like this one.
   153  	if runtimeSpecified && a.ff.Runtime != funcfileDockerRuntime {
   154  		err := a.generateBoilerplate()
   155  		if err != nil {
   156  			return err
   157  		}
   158  	}
   159  
   160  	if err := encodeFuncfileYAML("func.yaml", a.ff); err != nil {
   161  		return err
   162  	}
   163  	fmt.Println("func.yaml created.")
   164  	return nil
   165  }
   166  
   167  func (a *initFnCmd) generateBoilerplate() error {
   168  	helper := langs.GetLangHelper(a.ff.Runtime)
   169  	if helper != nil && helper.HasBoilerplate() {
   170  		if err := helper.GenerateBoilerplate(); err != nil {
   171  			if err == langs.ErrBoilerplateExists {
   172  				return nil
   173  			}
   174  			return err
   175  		}
   176  		fmt.Println("Function boilerplate generated.")
   177  	}
   178  	return nil
   179  }
   180  
   181  func (a *initFnCmd) bindRoute(rt *models.Route) {
   182  	ff := a.ff
   183  	if rt.Format != "" {
   184  		ff.Format = rt.Format
   185  	}
   186  	if rt.Type != "" {
   187  		ff.Type = rt.Type
   188  	}
   189  	if rt.Memory > 0 {
   190  		ff.Memory = rt.Memory
   191  	}
   192  	if rt.Timeout != nil {
   193  		ff.Timeout = rt.Timeout
   194  	}
   195  	if rt.IDLETimeout != nil {
   196  		ff.IDLETimeout = rt.IDLETimeout
   197  	}
   198  }
   199  
   200  func (a *initFnCmd) buildFuncFile(c *cli.Context) error {
   201  	wd := getWd()
   202  	var err error
   203  
   204  	if a.ff.Name == "" {
   205  		// then defaults to current directory for name, we'll just leave it out of func.yaml
   206  		// a.Name = filepath.Base(pwd)
   207  	} else if strings.Contains(a.ff.Name, ":") {
   208  		return errors.New("function name cannot contain a colon")
   209  	}
   210  
   211  	//if Dockerfile present, use 'docker' as 'runtime'
   212  	if exists("Dockerfile") {
   213  		fmt.Println("Dockerfile found. Using runtime 'docker'.")
   214  		a.ff.Runtime = funcfileDockerRuntime
   215  		return nil
   216  	}
   217  	if a.ff.Runtime == funcfileDockerRuntime {
   218  		return errors.New("function file runtime is 'docker', but no Dockerfile exists")
   219  	}
   220  
   221  	var helper langs.LangHelper
   222  	if a.ff.Runtime == "" {
   223  		helper, err = detectRuntime(wd)
   224  		if err != nil {
   225  			return err
   226  		}
   227  		fmt.Printf("Found %v function, assuming %v runtime.\n", helper.Runtime(), helper.Runtime())
   228  	} else {
   229  		fmt.Println("Runtime:", a.ff.Runtime)
   230  		helper = langs.GetLangHelper(a.ff.Runtime)
   231  	}
   232  	if helper == nil {
   233  		fmt.Printf("Init does not support the %s runtime, you'll have to create your own Dockerfile for this function.\n", a.ff.Runtime)
   234  	} else {
   235  		if a.ff.Entrypoint == "" {
   236  			a.ff.Entrypoint, err = helper.Entrypoint()
   237  			if err != nil {
   238  				return err
   239  			}
   240  		}
   241  
   242  		if a.ff.Runtime == "" {
   243  			a.ff.Runtime = helper.Runtime()
   244  		}
   245  
   246  		if a.ff.Format == "" {
   247  			a.ff.Format = helper.DefaultFormat()
   248  		}
   249  
   250  		if a.ff.Cmd == "" {
   251  			cmd, err := helper.Cmd()
   252  			if err != nil {
   253  				return err
   254  			}
   255  			a.ff.Cmd = cmd
   256  		}
   257  
   258  		if helper.FixImagesOnInit() {
   259  			if a.ff.BuildImage == "" {
   260  				buildImage, err := helper.BuildFromImage()
   261  				if err != nil {
   262  					return err
   263  				}
   264  				a.ff.BuildImage = buildImage
   265  			}
   266  			if helper.IsMultiStage() {
   267  				if a.ff.RunImage == "" {
   268  					runImage, err := helper.RunFromImage()
   269  					if err != nil {
   270  						return err
   271  					}
   272  					a.ff.RunImage = runImage
   273  				}
   274  			}
   275  		}
   276  	}
   277  
   278  	if a.ff.Entrypoint == "" && a.ff.Cmd == "" {
   279  		return fmt.Errorf("could not detect entrypoint or cmd for %v, use --entrypoint and/or --cmd to set them explicitly", a.ff.Runtime)
   280  	}
   281  
   282  	return nil
   283  }
   284  
   285  func detectRuntime(path string) (langs.LangHelper, error) {
   286  	for _, h := range langs.Helpers() {
   287  		filenames := []string{}
   288  		for _, ext := range h.Extensions() {
   289  			filenames = append(filenames,
   290  				filepath.Join(path, fmt.Sprintf("func%s", ext)),
   291  				filepath.Join(path, fmt.Sprintf("Func%s", ext)),
   292  				filepath.Join(path, fmt.Sprintf("src/main%s", ext)), // rust
   293  			)
   294  		}
   295  		for _, filename := range filenames {
   296  			if exists(filename) {
   297  				return h, nil
   298  			}
   299  		}
   300  	}
   301  	return nil, fmt.Errorf("no supported files found to guess runtime, please set runtime explicitly with --runtime flag")
   302  }