github.com/fnproject/cli@v0.0.0-20240508150455-e5d88bd86117/commands/init.go (about)

     1  /*
     2   * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved.
     3   *
     4   * Licensed under the Apache License, Version 2.0 (the "License");
     5   * you may not use this file except in compliance with the License.
     6   * You may obtain a copy of the License at
     7   *
     8   *     http://www.apache.org/licenses/LICENSE-2.0
     9   *
    10   * Unless required by applicable law or agreed to in writing, software
    11   * distributed under the License is distributed on an "AS IS" BASIS,
    12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13   * See the License for the specific language governing permissions and
    14   * limitations under the License.
    15   */
    16  
    17  package commands
    18  
    19  /*
    20  usage: fn init --help
    21  
    22  o If there's a Dockerfile found, this will generate a basic
    23  function file with the image and 'docker' as 'runtime'
    24  like following, for example:
    25  
    26  name: hello
    27  version: 0.0.1
    28  runtime: docker
    29  path: /hello
    30  
    31  then exit; if 'runtime' is 'docker' in the function file
    32  and no Dockerfile exists, print an error message then exit
    33  o It will then try to decipher the runtime based on
    34  the files in the current directory, if it can't figure it out,
    35  it will print an error message then exit.
    36  */
    37  
    38  import (
    39  	"errors"
    40  	"fmt"
    41  	"os"
    42  	"path/filepath"
    43  	"sort"
    44  	"strings"
    45  
    46  	"github.com/fnproject/cli/common"
    47  	"github.com/fnproject/cli/langs"
    48  	function "github.com/fnproject/cli/objects/fn"
    49  	modelsV2 "github.com/fnproject/fn_go/modelsv2"
    50  	"github.com/urfave/cli"
    51  )
    52  
    53  type initFnCmd struct {
    54  	force       bool
    55  	triggerType string
    56  	wd          string
    57  	ff          *common.FuncFileV20180708
    58  }
    59  
    60  func initFlags(a *initFnCmd) []cli.Flag {
    61  	fgs := []cli.Flag{
    62  		cli.StringFlag{
    63  			Name:  "name",
    64  			Usage: "Name of the function. Defaults to directory name in lowercase.",
    65  		},
    66  		cli.BoolFlag{
    67  			Name:        "force",
    68  			Usage:       "Overwrite existing func.yaml",
    69  			Destination: &a.force,
    70  		},
    71  		cli.StringFlag{
    72  			Name:  "runtime",
    73  			Usage: "Choose an existing runtime - " + langsList(),
    74  		},
    75  		cli.StringFlag{
    76  			Name:  "init-image",
    77  			Usage: "A Docker image which will create a function template",
    78  		},
    79  		cli.StringFlag{
    80  			Name:  "entrypoint",
    81  			Usage: "Entrypoint is the command to run to start this function - equivalent to Dockerfile ENTRYPOINT.",
    82  		},
    83  		cli.StringFlag{
    84  			Name:  "cmd",
    85  			Usage: "Command to run to start this function - equivalent to Dockerfile CMD.",
    86  		},
    87  		cli.StringFlag{
    88  			Name:  "version",
    89  			Usage: "Set initial function version",
    90  			Value: common.InitialVersion,
    91  		},
    92  		cli.StringFlag{
    93  			Name:        "working-dir,w",
    94  			Usage:       "Specify the working directory to initialise a function, must be the full path.",
    95  			Destination: &a.wd,
    96  		},
    97  		cli.StringFlag{
    98  			Name:        "trigger",
    99  			Usage:       "Specify the trigger type - permitted values are 'http'.",
   100  			Destination: &a.triggerType,
   101  		},
   102  		cli.Uint64Flag{
   103  			Name:  "memory,m",
   104  			Usage: "Memory in MiB",
   105  		},
   106  		cli.StringSliceFlag{
   107  			Name:  "config,c",
   108  			Usage: "Function configuration",
   109  		},
   110  		cli.IntFlag{
   111  			Name:  "timeout",
   112  			Usage: "Function timeout (eg. 30)",
   113  		},
   114  		cli.IntFlag{
   115  			Name:  "idle-timeout",
   116  			Usage: "Function idle timeout (eg. 30)",
   117  		},
   118  		cli.StringSliceFlag{
   119  			Name:  "annotation",
   120  			Usage: "Function annotation (can be specified multiple times)",
   121  		},
   122  	}
   123  
   124  	return fgs
   125  }
   126  
   127  func langsList() string {
   128  	allLangs := []string{}
   129  	for _, h := range langs.Helpers() {
   130  		allLangs = append(allLangs, h.LangStrings()...)
   131  	}
   132  	sort.Strings(allLangs)
   133  	// remove duplicates
   134  	var allUnique []string
   135  	for i, s := range allLangs {
   136  		if i > 0 && s == allLangs[i-1] {
   137  			continue
   138  		}
   139  		if deprecatedPythonRuntime(s) {
   140  			continue
   141  		}
   142  		allUnique = append(allUnique, s)
   143  	}
   144  	return strings.Join(allUnique, ", ")
   145  }
   146  
   147  func deprecatedPythonRuntime(runtime string) bool {
   148  	return runtime == "python3.8.5" || runtime == "python3.7.1"
   149  }
   150  
   151  // InitCommand returns init cli.command
   152  func InitCommand() cli.Command {
   153  	a := &initFnCmd{ff: &common.FuncFileV20180708{}}
   154  
   155  	return cli.Command{
   156  		Name:        "init",
   157  		Usage:       "\tCreate a local func.yaml file",
   158  		Category:    "DEVELOPMENT COMMANDS",
   159  		Aliases:     []string{"in"},
   160  		Description: "This command creates a func.yaml file in the current directory.",
   161  		ArgsUsage:   "[function-subdirectory]",
   162  		Action:      a.init,
   163  		Flags:       initFlags(a),
   164  	}
   165  }
   166  
   167  func (a *initFnCmd) init(c *cli.Context) error {
   168  	var err error
   169  	var dir string
   170  	var fn modelsV2.Fn
   171  
   172  	dir = common.GetWd()
   173  	if a.wd != "" {
   174  		dir = a.wd
   175  	}
   176  
   177  	function.WithFlags(c, &fn)
   178  	a.bindFn(&fn)
   179  
   180  	runtime := c.String("runtime")
   181  	initImage := c.String("init-image")
   182  
   183  	if runtime != "" && initImage != "" {
   184  		return fmt.Errorf("You can't supply --runtime with --init-image")
   185  	}
   186  
   187  	runtimeSpecified := runtime != ""
   188  
   189  	a.ff.Schema_version = common.LatestYamlVersion
   190  	if runtimeSpecified {
   191  		// go no further if the specified runtime is not supported
   192  		if runtime != common.FuncfileDockerRuntime && langs.GetLangHelper(runtime) == nil {
   193  			return fmt.Errorf("Init does not support the '%s' runtime", runtime)
   194  		}
   195  		if deprecatedPythonRuntime(runtime) {
   196  			return fmt.Errorf("Runtime %s is no more supported for new apps. Please use python or %s runtime for new apps.", runtime, runtime[:strings.LastIndex(runtime, ".")])
   197  		}
   198  	}
   199  
   200  	path := c.Args().First()
   201  	if path != "" {
   202  		fmt.Printf("Creating function at: ./%s\n", path)
   203  		dir = filepath.Join(dir, path)
   204  
   205  		// check if dir exists, if it does, then we can't create function
   206  		if common.Exists(dir) {
   207  			if !a.force {
   208  				return fmt.Errorf("directory %s already exists, cannot init function", dir)
   209  			}
   210  		} else {
   211  			err = os.MkdirAll(dir, 0755)
   212  			if err != nil {
   213  				return err
   214  			}
   215  		}
   216  	}
   217  
   218  	if c.String("name") != "" {
   219  		a.ff.Name = strings.ToLower(c.String("name"))
   220  	}
   221  
   222  	if a.ff.Name == "" {
   223  		// then defaults to current directory for name, the name must be lowercase
   224  		a.ff.Name = strings.ToLower(filepath.Base(dir))
   225  	}
   226  
   227  	if a.triggerType != "" {
   228  		a.triggerType = strings.ToLower(a.triggerType)
   229  		ok := validateTriggerType(a.triggerType)
   230  		if !ok {
   231  			return fmt.Errorf("init does not support the trigger type '%s'.\n Permitted values are 'http'.", a.triggerType)
   232  		}
   233  
   234  		// TODO when we allow multiple trigger definitions in a func file, we need
   235  		// to allow naming triggers in a func file as well as use the type of
   236  		// trigger to deduplicate the trigger names
   237  
   238  		trig := make([]common.Trigger, 1)
   239  		trig[0] = common.Trigger{
   240  			Name:   a.ff.Name,
   241  			Type:   a.triggerType,
   242  			Source: "/" + a.ff.Name,
   243  		}
   244  
   245  		a.ff.Triggers = trig
   246  
   247  	}
   248  
   249  	err = os.Chdir(dir)
   250  	if err != nil {
   251  		return err
   252  	}
   253  
   254  	defer os.Chdir(dir) // todo: wrap this so we can log the error if changing back fails
   255  
   256  	if !a.force {
   257  		_, ff, err := common.LoadFuncfile(dir)
   258  		if _, ok := err.(*common.NotFoundError); !ok && err != nil {
   259  			return err
   260  		}
   261  		if ff != nil {
   262  			return errors.New("Function file already exists, aborting")
   263  		}
   264  	}
   265  	err = a.BuildFuncFileV20180708(c, dir) // TODO: Return LangHelper here, then don't need to refind the helper in generateBoilerplate() below
   266  	if err != nil {
   267  		return err
   268  	}
   269  
   270  	a.ff.Schema_version = common.LatestYamlVersion
   271  
   272  	if initImage != "" {
   273  		err := a.doInitImage(initImage, c)
   274  		if err != nil {
   275  			return err
   276  		}
   277  	} else {
   278  		// TODO: why don't we treat "docker" runtime as just another language helper?
   279  		// Then can get rid of several Docker specific if/else's like this one.
   280  		if runtimeSpecified && runtime != common.FuncfileDockerRuntime {
   281  			err := a.generateBoilerplate(dir, runtime)
   282  			if err != nil {
   283  				return err
   284  			}
   285  		}
   286  	}
   287  
   288  	if err := common.EncodeFuncFileV20180708YAML("func.yaml", a.ff); err != nil {
   289  		return err
   290  	}
   291  
   292  	fmt.Println("func.yaml created.")
   293  	return nil
   294  }
   295  
   296  func (a *initFnCmd) doInitImage(initImage string, c *cli.Context) error {
   297  	err := common.RunInitImage(initImage, a.ff.Name)
   298  	if err != nil {
   299  		return err
   300  	}
   301  	err = common.MergeFuncFileInitYAML("func.init.yaml", a.ff)
   302  	if err != nil {
   303  		return err
   304  	}
   305  	// Then CLI args can override some init-image options (TODO: remove this with #383)
   306  	if c.String("cmd") != "" {
   307  		a.ff.Cmd = c.String("cmd")
   308  	}
   309  	if c.String("entrypoint") != "" {
   310  		a.ff.Entrypoint = c.String("entrypoint")
   311  	}
   312  	_ = os.Remove("func.init.yaml")
   313  	return nil
   314  }
   315  
   316  func (a *initFnCmd) generateBoilerplate(path, runtime string) error {
   317  	helper := langs.GetLangHelper(runtime)
   318  	if helper != nil && helper.HasBoilerplate() {
   319  		if err := helper.GenerateBoilerplate(path); err != nil {
   320  			if err == langs.ErrBoilerplateExists {
   321  				return nil
   322  			}
   323  			return err
   324  		}
   325  		fmt.Println("Function boilerplate generated.")
   326  	}
   327  	return nil
   328  }
   329  
   330  func (a *initFnCmd) bindFn(fn *modelsV2.Fn) {
   331  	ff := a.ff
   332  	if fn.Memory > 0 {
   333  		ff.Memory = fn.Memory
   334  	}
   335  	if fn.Timeout != nil {
   336  		ff.Timeout = fn.Timeout
   337  	}
   338  	if fn.IdleTimeout != nil {
   339  		ff.IDLE_timeout = fn.IdleTimeout
   340  	}
   341  }
   342  
   343  // ValidateFuncName checks if the func name is valid, the name can't contain a colon and
   344  // must be all lowercase
   345  func ValidateFuncName(name string) error {
   346  	if strings.Contains(name, ":") {
   347  		return errors.New("Function name cannot contain a colon")
   348  	}
   349  	if strings.ToLower(name) != name {
   350  		return errors.New("Function name must be lowercase")
   351  	}
   352  	return nil
   353  }
   354  
   355  func (a *initFnCmd) BuildFuncFileV20180708(c *cli.Context, path string) error {
   356  	var err error
   357  
   358  	a.ff.Version = c.String("version")
   359  	if err = ValidateFuncName(a.ff.Name); err != nil {
   360  		return err
   361  	}
   362  
   363  	//if Dockerfile present, use 'docker' as 'runtime'
   364  	if common.Exists("Dockerfile") {
   365  		fmt.Println("Dockerfile found. Using runtime 'docker'.")
   366  		a.ff.Runtime = common.FuncfileDockerRuntime
   367  		return nil
   368  	}
   369  	runtime := c.String("runtime")
   370  	if runtime == common.FuncfileDockerRuntime {
   371  		return errors.New("Function file runtime is 'docker', but no Dockerfile exists")
   372  	}
   373  
   374  	if c.String("init-image") != "" {
   375  		return nil
   376  	}
   377  
   378  	var helper langs.LangHelper
   379  	if runtime == "" {
   380  		helper, err = detectRuntime(path)
   381  		if err != nil {
   382  			return err
   383  		}
   384  		fmt.Printf("Found %v function, assuming %v runtime.\n", helper.Runtime(), helper.Runtime())
   385  	} else {
   386  		helper = langs.GetLangHelper(runtime)
   387  	}
   388  	if helper == nil {
   389  		fmt.Printf("Init does not support the %s runtime, you'll have to create your own Dockerfile for this function.\n", runtime)
   390  	} else {
   391  		if c.String("entrypoint") == "" {
   392  			a.ff.Entrypoint, err = helper.Entrypoint()
   393  			if err != nil {
   394  				return err
   395  			}
   396  
   397  		} else {
   398  			a.ff.Entrypoint = c.String("entrypoint")
   399  		}
   400  
   401  		if runtime == "" {
   402  			runtime = helper.Runtime()
   403  		}
   404  
   405  		a.ff.Runtime = runtime
   406  
   407  		if c.Uint64("memory") == 0 {
   408  			a.ff.Memory = helper.CustomMemory()
   409  		}
   410  
   411  		if c.String("cmd") == "" {
   412  			cmd, err := helper.Cmd()
   413  			if err != nil {
   414  				return err
   415  			}
   416  			a.ff.Cmd = cmd
   417  		} else {
   418  			a.ff.Cmd = c.String("cmd")
   419  		}
   420  		if helper.FixImagesOnInit() {
   421  			if a.ff.Build_image == "" {
   422  				buildImage, err := helper.BuildFromImage()
   423  				if err != nil {
   424  					return err
   425  				}
   426  				a.ff.Build_image = buildImage
   427  			}
   428  			if helper.IsMultiStage() {
   429  				if a.ff.Run_image == "" {
   430  					runImage, err := helper.RunFromImage()
   431  					if err != nil {
   432  						return err
   433  					}
   434  					a.ff.Run_image = runImage
   435  				}
   436  			}
   437  		}
   438  	}
   439  	if a.ff.Entrypoint == "" && a.ff.Cmd == "" {
   440  		return fmt.Errorf("Could not detect entrypoint or cmd for %v, use --entrypoint and/or --cmd to set them explicitly", a.ff.Runtime)
   441  	}
   442  
   443  	return nil
   444  }
   445  
   446  func detectRuntime(path string) (langs.LangHelper, error) {
   447  	for _, h := range langs.Helpers() {
   448  		filenames := []string{}
   449  		for _, ext := range h.Extensions() {
   450  			filenames = append(filenames,
   451  				filepath.Join(path, fmt.Sprintf("func%s", ext)),
   452  				filepath.Join(path, fmt.Sprintf("Func%s", ext)),
   453  			)
   454  		}
   455  		for _, filename := range filenames {
   456  			if common.Exists(filename) {
   457  				return h, nil
   458  			}
   459  		}
   460  	}
   461  	return nil, fmt.Errorf("No supported files found to guess runtime, please set runtime explicitly with --runtime flag")
   462  }
   463  
   464  func validateTriggerType(triggerType string) bool {
   465  	switch triggerType {
   466  	case "http":
   467  		return true
   468  	default:
   469  		return false
   470  	}
   471  }