github.com/devcamcar/cli@v0.0.0-20181107134215-706a05759d18/commands/init.go (about)

     1  package commands
     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  	"archive/tar"
    24  	"bytes"
    25  	"errors"
    26  	"fmt"
    27  	"io"
    28  	"os"
    29  	"os/exec"
    30  	"path/filepath"
    31  	"strings"
    32  
    33  	"github.com/fnproject/cli/common"
    34  	"github.com/fnproject/cli/langs"
    35  	function "github.com/fnproject/cli/objects/fn"
    36  	modelsV2 "github.com/fnproject/fn_go/modelsv2"
    37  	"github.com/urfave/cli"
    38  )
    39  
    40  type initFnCmd struct {
    41  	force       bool
    42  	triggerType string
    43  	wd          string
    44  	ff          *common.FuncFileV20180708
    45  }
    46  
    47  func initFlags(a *initFnCmd) []cli.Flag {
    48  	fgs := []cli.Flag{
    49  		cli.StringFlag{
    50  			Name:  "name",
    51  			Usage: "Name of the function. Defaults to directory name in lowercase.",
    52  		},
    53  		cli.BoolFlag{
    54  			Name:        "force",
    55  			Usage:       "Overwrite existing func.yaml",
    56  			Destination: &a.force,
    57  		},
    58  		cli.StringFlag{
    59  			Name:  "runtime",
    60  			Usage: "Choose an existing runtime - " + langsList(),
    61  		},
    62  		cli.StringFlag{
    63  			Name:  "init-image",
    64  			Usage: "A Docker image which will create a function template",
    65  		},
    66  		cli.StringFlag{
    67  			Name:  "entrypoint",
    68  			Usage: "Entrypoint is the command to run to start this function - equivalent to Dockerfile ENTRYPOINT.",
    69  		},
    70  		cli.StringFlag{
    71  			Name:  "cmd",
    72  			Usage: "Command to run to start this function - equivalent to Dockerfile CMD.",
    73  		},
    74  		cli.StringFlag{
    75  			Name:  "version",
    76  			Usage: "Set initial function version",
    77  			Value: common.InitialVersion,
    78  		},
    79  		cli.StringFlag{
    80  			Name:        "working-dir,w",
    81  			Usage:       "Specify the working directory to initialise a function, must be the full path.",
    82  			Destination: &a.wd,
    83  		},
    84  		cli.StringFlag{
    85  			Name:        "trigger",
    86  			Usage:       "Specify the trigger type - permitted values are 'http'.",
    87  			Destination: &a.triggerType,
    88  		},
    89  		cli.Uint64Flag{
    90  			Name:  "memory,m",
    91  			Usage: "Memory in MiB",
    92  		},
    93  		cli.StringFlag{
    94  			Name:  "type,t",
    95  			Usage: "Function type - sync or async",
    96  		},
    97  		cli.StringSliceFlag{
    98  			Name:  "config,c",
    99  			Usage: "Function configuration",
   100  		},
   101  		cli.StringSliceFlag{
   102  			Name:  "headers",
   103  			Usage: "Function response headers",
   104  		},
   105  		cli.StringFlag{
   106  			Name:  "format,f",
   107  			Usage: "Hot container IO format - default or http",
   108  		},
   109  		cli.IntFlag{
   110  			Name:  "timeout",
   111  			Usage: "Function timeout (eg. 30)",
   112  		},
   113  		cli.IntFlag{
   114  			Name:  "idle-timeout",
   115  			Usage: "Function idle timeout (eg. 30)",
   116  		},
   117  		cli.StringSliceFlag{
   118  			Name:  "annotation",
   119  			Usage: "Function annotation (can be specified multiple times)",
   120  		},
   121  	}
   122  
   123  	return fgs
   124  }
   125  
   126  func langsList() string {
   127  	allLangs := []string{}
   128  	for _, h := range langs.Helpers() {
   129  		allLangs = append(allLangs, h.LangStrings()...)
   130  	}
   131  	return strings.Join(allLangs, ", ")
   132  }
   133  
   134  // InitCommand returns init cli.command
   135  func InitCommand() cli.Command {
   136  	a := &initFnCmd{ff: &common.FuncFileV20180708{}}
   137  
   138  	return cli.Command{
   139  		Name:        "init",
   140  		Usage:       "\tCreate a local func.yaml file",
   141  		Category:    "DEVELOPMENT COMMANDS",
   142  		Aliases:     []string{"in"},
   143  		Description: "This command creates a func.yaml file in the current directory.",
   144  		ArgsUsage:   "[function-subdirectory]",
   145  		Action:      a.init,
   146  		Flags:       initFlags(a),
   147  	}
   148  }
   149  
   150  func (a *initFnCmd) init(c *cli.Context) error {
   151  	var err error
   152  	var dir string
   153  	var fn modelsV2.Fn
   154  
   155  	dir = common.GetWd()
   156  	if a.wd != "" {
   157  		dir = a.wd
   158  	}
   159  
   160  	function.WithFlags(c, &fn)
   161  	a.bindFn(&fn)
   162  
   163  	runtime := c.String("runtime")
   164  	initImage := c.String("init-image")
   165  
   166  	if runtime != "" && initImage != "" {
   167  		return fmt.Errorf("You can't supply --runtime with --init-image")
   168  	}
   169  
   170  	runtimeSpecified := runtime != ""
   171  
   172  	a.ff.Schema_version = common.LatestYamlVersion
   173  	if runtimeSpecified {
   174  		// go no further if the specified runtime is not supported
   175  		if runtime != common.FuncfileDockerRuntime && langs.GetLangHelper(runtime) == nil {
   176  			return fmt.Errorf("Init does not support the '%s' runtime", runtime)
   177  		}
   178  	}
   179  
   180  	path := c.Args().First()
   181  	if path != "" {
   182  		fmt.Printf("Creating function at: /%s\n", path)
   183  		dir = filepath.Join(dir, path)
   184  
   185  		// check if dir exists, if it does, then we can't create function
   186  		if common.Exists(dir) {
   187  			if !a.force {
   188  				return fmt.Errorf("directory %s already exists, cannot init function", dir)
   189  			}
   190  		} else {
   191  			err = os.MkdirAll(dir, 0755)
   192  			if err != nil {
   193  				return err
   194  			}
   195  		}
   196  	}
   197  
   198  	if c.String("name") != "" {
   199  		a.ff.Name = strings.ToLower(c.String("name"))
   200  	}
   201  
   202  	if a.ff.Name == "" {
   203  		// then defaults to current directory for name, the name must be lowercase
   204  		a.ff.Name = strings.ToLower(filepath.Base(dir))
   205  	}
   206  
   207  	if a.triggerType != "" {
   208  		a.triggerType = strings.ToLower(a.triggerType)
   209  		ok := validateTriggerType(a.triggerType)
   210  		if !ok {
   211  			return fmt.Errorf("Init does not support the trigger type '%s'.\n Permitted values are 'http'.", a.triggerType)
   212  		}
   213  
   214  		trig := make([]common.Trigger, 1)
   215  		trig[0] = common.Trigger{
   216  			Name:   a.ff.Name + "-trigger",
   217  			Type:   a.triggerType,
   218  			Source: "/" + a.ff.Name + "-trigger",
   219  		}
   220  
   221  		a.ff.Triggers = trig
   222  
   223  	}
   224  
   225  	err = os.Chdir(dir)
   226  	if err != nil {
   227  		return err
   228  	}
   229  
   230  	defer os.Chdir(dir) // todo: wrap this so we can log the error if changing back fails
   231  
   232  	if !a.force {
   233  		_, ff, err := common.LoadFuncfile(dir)
   234  		if _, ok := err.(*common.NotFoundError); !ok && err != nil {
   235  			return err
   236  		}
   237  		if ff != nil {
   238  			return errors.New("Function file already exists, aborting")
   239  		}
   240  	}
   241  	err = a.BuildFuncFileV20180708(c, dir) // TODO: Return LangHelper here, then don't need to refind the helper in generateBoilerplate() below
   242  	if err != nil {
   243  		return err
   244  	}
   245  
   246  	a.ff.Schema_version = common.LatestYamlVersion
   247  
   248  	if initImage != "" {
   249  
   250  		err = runInitImage(initImage, a)
   251  		if err != nil {
   252  			return err
   253  		}
   254  
   255  		// Merge the func.init.yaml from the initImage with a.ff
   256  		//     write out the new func file
   257  		var initFf, err = common.ParseFuncfile("func.init.yaml")
   258  		if err != nil {
   259  			return errors.New("init-image did not produce a valid func.init.yaml")
   260  		}
   261  
   262  		// Build up a combined func.yaml (in a.ff) from the init-image and defaults and cli-args
   263  		//     The following fields are already in a.ff:
   264  		//         config, cpus, idle_timeout, memory, name, path, timeout, type, triggers, version
   265  		//     Add the following from the init-image:
   266  		//         build, build_image, cmd, content_type, entrypoint, expects, format, headers, run_image, runtime
   267  		a.ff.Build = initFf.Build
   268  		a.ff.Build_image = initFf.BuildImage
   269  		a.ff.Cmd = initFf.Cmd
   270  		a.ff.Content_type = initFf.ContentType
   271  		a.ff.Entrypoint = initFf.Entrypoint
   272  		a.ff.Expects = initFf.Expects
   273  		a.ff.Format = initFf.Format
   274  		a.ff.Run_image = initFf.RunImage
   275  		a.ff.Runtime = initFf.Runtime
   276  
   277  		// Then CLI args can override some init-image options (TODO: remove this with #383)
   278  		if c.String("cmd") != "" {
   279  			a.ff.Cmd = c.String("cmd")
   280  		}
   281  
   282  		if c.String("entrypoint") != "" {
   283  			a.ff.Entrypoint = c.String("entrypoint")
   284  		}
   285  
   286  		if c.String("format") != "" {
   287  			a.ff.Format = c.String("format")
   288  		}
   289  
   290  		if err := common.EncodeFuncFileV20180708YAML("func.yaml", a.ff); err != nil {
   291  			return err
   292  		}
   293  
   294  		os.Remove("func.init.yaml")
   295  
   296  	} else {
   297  		// TODO: why don't we treat "docker" runtime as just another language helper?
   298  		// Then can get rid of several Docker specific if/else's like this one.
   299  		if runtimeSpecified && runtime != common.FuncfileDockerRuntime {
   300  			err := a.generateBoilerplate(dir, runtime)
   301  			if err != nil {
   302  				return err
   303  			}
   304  		}
   305  
   306  		if err := common.EncodeFuncFileV20180708YAML("func.yaml", a.ff); err != nil {
   307  			return err
   308  		}
   309  	}
   310  
   311  	fmt.Println("func.yaml created.")
   312  	return nil
   313  }
   314  
   315  func runInitImage(initImage string, a *initFnCmd) error {
   316  	fmt.Println("Building from init-image: " + initImage)
   317  
   318  	// Run the initImage
   319  	var c1ErrB bytes.Buffer
   320  	tarR, tarW := io.Pipe()
   321  
   322  	c1 := exec.Command("docker", "run", "-e", "FN_FUNCTION_NAME="+a.ff.Name, initImage)
   323  	c1.Stderr = &c1ErrB
   324  	c1.Stdout = tarW
   325  
   326  	c1Err := c1.Start()
   327  	if c1Err != nil {
   328  		fmt.Println(c1ErrB.String())
   329  		return errors.New("Error running init-image")
   330  	}
   331  
   332  	err := untarStream(tarR)
   333  	if err != nil {
   334  		return errors.New("Error un-tarring the output of the init-image")
   335  	}
   336  
   337  	return nil
   338  }
   339  
   340  // Untars an io.Reader into the cwd
   341  func untarStream(r io.Reader) error {
   342  
   343  	tr := tar.NewReader(r)
   344  	for {
   345  		header, err := tr.Next()
   346  
   347  		if err == io.EOF {
   348  			// if no more files are found we are finished
   349  			return nil
   350  		}
   351  
   352  		if err != nil {
   353  			return err
   354  		}
   355  
   356  		switch header.Typeflag {
   357  		// if its a dir and it doesn't exist create it
   358  		case tar.TypeDir:
   359  			if _, err := os.Stat(header.Name); err != nil {
   360  				if err := os.MkdirAll(header.Name, 0755); err != nil {
   361  					return err
   362  				}
   363  			}
   364  
   365  		// if it's a file create it
   366  		case tar.TypeReg:
   367  			f, err := os.OpenFile(header.Name, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode))
   368  			if err != nil {
   369  				return err
   370  			}
   371  
   372  			// copy over contents
   373  			if _, err := io.Copy(f, tr); err != nil {
   374  				return err
   375  			}
   376  
   377  			f.Close()
   378  		}
   379  	}
   380  }
   381  
   382  func (a *initFnCmd) generateBoilerplate(path, runtime string) error {
   383  	helper := langs.GetLangHelper(runtime)
   384  	if helper != nil && helper.HasBoilerplate() {
   385  		if err := helper.GenerateBoilerplate(path); err != nil {
   386  			if err == langs.ErrBoilerplateExists {
   387  				return nil
   388  			}
   389  			return err
   390  		}
   391  		fmt.Println("Function boilerplate generated.")
   392  	}
   393  	return nil
   394  }
   395  
   396  func (a *initFnCmd) bindFn(fn *modelsV2.Fn) {
   397  	ff := a.ff
   398  	if fn.Format != "" {
   399  		ff.Format = fn.Format
   400  	}
   401  	if fn.Memory > 0 {
   402  		ff.Memory = fn.Memory
   403  	}
   404  	if fn.Timeout != nil {
   405  		ff.Timeout = fn.Timeout
   406  	}
   407  	if fn.IDLETimeout != nil {
   408  		ff.IDLE_timeout = fn.IDLETimeout
   409  	}
   410  }
   411  
   412  // ValidateFuncName checks if the func name is valid, the name can't contain a colon and
   413  // must be all lowercase
   414  func ValidateFuncName(name string) error {
   415  	if strings.Contains(name, ":") {
   416  		return errors.New("Function name cannot contain a colon")
   417  	}
   418  	if strings.ToLower(name) != name {
   419  		return errors.New("Function name must be lowercase")
   420  	}
   421  	return nil
   422  }
   423  
   424  func (a *initFnCmd) BuildFuncFileV20180708(c *cli.Context, path string) error {
   425  	var err error
   426  
   427  	a.ff.Version = c.String("version")
   428  	if err = ValidateFuncName(a.ff.Name); err != nil {
   429  		return err
   430  	}
   431  
   432  	//if Dockerfile present, use 'docker' as 'runtime'
   433  	if common.Exists("Dockerfile") {
   434  		fmt.Println("Dockerfile found. Using runtime 'docker'.")
   435  		a.ff.Runtime = common.FuncfileDockerRuntime
   436  		return nil
   437  	}
   438  	runtime := c.String("runtime")
   439  	if runtime == common.FuncfileDockerRuntime {
   440  		return errors.New("Function file runtime is 'docker', but no Dockerfile exists")
   441  	}
   442  
   443  	if c.String("init-image") != "" {
   444  		return nil
   445  	}
   446  
   447  	var helper langs.LangHelper
   448  	if runtime == "" {
   449  		helper, err = detectRuntime(path)
   450  		if err != nil {
   451  			return err
   452  		}
   453  		fmt.Printf("Found %v function, assuming %v runtime.\n", helper.Runtime(), helper.Runtime())
   454  		//need to default this to default format to be backwards compatible. Might want to just not allow this anymore, fail here.
   455  		if c.String("format") == "" {
   456  			a.ff.Format = "default"
   457  		}
   458  	} else {
   459  		helper = langs.GetLangHelper(runtime)
   460  	}
   461  	if helper == nil {
   462  		fmt.Printf("Init does not support the %s runtime, you'll have to create your own Dockerfile for this function.\n", runtime)
   463  	} else {
   464  		if c.String("entrypoint") == "" {
   465  			a.ff.Entrypoint, err = helper.Entrypoint()
   466  			if err != nil {
   467  				return err
   468  			}
   469  
   470  		} else {
   471  			a.ff.Entrypoint = c.String("entrypoint")
   472  		}
   473  
   474  		if runtime == "" {
   475  			runtime = helper.Runtime()
   476  		}
   477  
   478  		a.ff.Runtime = runtime
   479  
   480  		if c.String("format") == "" {
   481  			a.ff.Format = helper.DefaultFormat()
   482  		}
   483  
   484  		if c.String("cmd") == "" {
   485  			cmd, err := helper.Cmd()
   486  			if err != nil {
   487  				return err
   488  			}
   489  			a.ff.Cmd = cmd
   490  		} else {
   491  			a.ff.Cmd = c.String("cmd")
   492  		}
   493  		if helper.FixImagesOnInit() {
   494  			if a.ff.Build_image == "" {
   495  				buildImage, err := helper.BuildFromImage()
   496  				if err != nil {
   497  					return err
   498  				}
   499  				a.ff.Build_image = buildImage
   500  			}
   501  			if helper.IsMultiStage() {
   502  				if a.ff.Run_image == "" {
   503  					runImage, err := helper.RunFromImage()
   504  					if err != nil {
   505  						return err
   506  					}
   507  					a.ff.Run_image = runImage
   508  				}
   509  			}
   510  		}
   511  	}
   512  	if a.ff.Entrypoint == "" && a.ff.Cmd == "" {
   513  		return fmt.Errorf("Could not detect entrypoint or cmd for %v, use --entrypoint and/or --cmd to set them explicitly", a.ff.Runtime)
   514  	}
   515  
   516  	return nil
   517  }
   518  
   519  func detectRuntime(path string) (langs.LangHelper, error) {
   520  	for _, h := range langs.Helpers() {
   521  		filenames := []string{}
   522  		for _, ext := range h.Extensions() {
   523  			filenames = append(filenames,
   524  				filepath.Join(path, fmt.Sprintf("func%s", ext)),
   525  				filepath.Join(path, fmt.Sprintf("Func%s", ext)),
   526  			)
   527  		}
   528  		for _, filename := range filenames {
   529  			if common.Exists(filename) {
   530  				return h, nil
   531  			}
   532  		}
   533  	}
   534  	return nil, fmt.Errorf("No supported files found to guess runtime, please set runtime explicitly with --runtime flag")
   535  }
   536  
   537  func validateTriggerType(triggerType string) bool {
   538  	switch triggerType {
   539  	case "http":
   540  		return true
   541  	default:
   542  		return false
   543  	}
   544  }