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

     1  package commands
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"os"
     7  	"path/filepath"
     8  	"strings"
     9  	"time"
    10  
    11  	client "github.com/fnproject/cli/client"
    12  	common "github.com/fnproject/cli/common"
    13  	apps "github.com/fnproject/cli/objects/app"
    14  	function "github.com/fnproject/cli/objects/fn"
    15  	trigger "github.com/fnproject/cli/objects/trigger"
    16  	v2Client "github.com/fnproject/fn_go/clientv2"
    17  	clientApps "github.com/fnproject/fn_go/clientv2/apps"
    18  	modelsV2 "github.com/fnproject/fn_go/modelsv2"
    19  	"github.com/urfave/cli"
    20  )
    21  
    22  // DeployCommand returns deploy cli.command
    23  func DeployCommand() cli.Command {
    24  	cmd := deploycmd{}
    25  	var flags []cli.Flag
    26  	flags = append(flags, cmd.flags()...)
    27  	return cli.Command{
    28  		Name:    "deploy",
    29  		Usage:   "\tDeploys a function to the functions server (bumps, build, pushes and updates functions and/or triggers).",
    30  		Aliases: []string{"dp"},
    31  		Before: func(cxt *cli.Context) error {
    32  			provider, err := client.CurrentProvider()
    33  			if err != nil {
    34  				return err
    35  			}
    36  			cmd.clientV2 = provider.APIClientv2()
    37  			return nil
    38  		},
    39  		Category:    "DEVELOPMENT COMMANDS",
    40  		Description: "This command deploys one or all (--all) functions to the function server.",
    41  		ArgsUsage:   "[function-subdirectory]",
    42  		Flags:       flags,
    43  		Action:      cmd.deploy,
    44  	}
    45  }
    46  
    47  type deploycmd struct {
    48  	appName  string
    49  	clientV2 *v2Client.Fn
    50  
    51  	wd       string
    52  	verbose  bool
    53  	local    bool
    54  	noCache  bool
    55  	registry string
    56  	all      bool
    57  	noBump   bool
    58  }
    59  
    60  func (p *deploycmd) flags() []cli.Flag {
    61  	return []cli.Flag{
    62  		cli.StringFlag{
    63  			Name:        "app",
    64  			Usage:       "App name to deploy to",
    65  			Destination: &p.appName,
    66  		},
    67  		cli.BoolFlag{
    68  			Name:        "verbose, v",
    69  			Usage:       "Verbose mode",
    70  			Destination: &p.verbose,
    71  		},
    72  		cli.BoolFlag{
    73  			Name:        "no-cache",
    74  			Usage:       "Don't use Docker cache for the build",
    75  			Destination: &p.noCache,
    76  		},
    77  		cli.BoolFlag{
    78  			Name:        "local, skip-push", // todo: deprecate skip-push
    79  			Usage:       "Do not push Docker built images onto Docker Hub - useful for local development.",
    80  			Destination: &p.local,
    81  		},
    82  		cli.StringFlag{
    83  			Name:        "registry",
    84  			Usage:       "Set the Docker owner for images and optionally the registry. This will be prefixed to your function name for pushing to Docker registries.\r  eg: `--registry username` will set your Docker Hub owner. `--registry registry.hub.docker.com/username` will set the registry and owner. ",
    85  			Destination: &p.registry,
    86  		},
    87  		cli.BoolFlag{
    88  			Name:        "all",
    89  			Usage:       "If in root directory containing `app.yaml`, this will deploy all functions",
    90  			Destination: &p.all,
    91  		},
    92  		cli.BoolFlag{
    93  			Name:        "no-bump",
    94  			Usage:       "Do not bump the version, assuming external version management",
    95  			Destination: &p.noBump,
    96  		},
    97  		cli.StringSliceFlag{
    98  			Name:  "build-arg",
    99  			Usage: "Set build time variables",
   100  		},
   101  		cli.StringFlag{
   102  			Name:  "working-dir,w",
   103  			Usage: "Specify the working directory to deploy a function, must be the full path.",
   104  		},
   105  	}
   106  }
   107  
   108  // deploy deploys a function or a set of functions for an app
   109  // By default this will deploy a single function, either the function in the current directory
   110  // or if an arg is passed in, a function in the path representing that arg, relative to the
   111  // current working directory.
   112  //
   113  // If user passes in --all flag, it will deploy all functions in an app. An app must have an `app.yaml`
   114  // file in it's root directory. The functions will be deployed based on the directory structure
   115  // on the file system (can be overridden using the `path` arg in each `func.yaml`. The index/root function
   116  // is the one that lives in the same directory as the app.yaml.
   117  func (p *deploycmd) deploy(c *cli.Context) error {
   118  	appName := ""
   119  	dir := common.GetDir(c)
   120  
   121  	appf, err := common.LoadAppfile(dir)
   122  
   123  	if err != nil {
   124  		if _, ok := err.(*common.NotFoundError); ok {
   125  			if p.all {
   126  				return err
   127  			}
   128  			// otherwise, it's ok
   129  		} else {
   130  			return err
   131  		}
   132  	} else {
   133  		appName = appf.Name
   134  	}
   135  	if p.appName != "" {
   136  		// flag overrides all
   137  		appName = p.appName
   138  	}
   139  
   140  	if appName == "" {
   141  		return errors.New("App name must be provided, try `--app APP_NAME`")
   142  	}
   143  
   144  	if p.all {
   145  		return p.deployAll(c, appName, appf)
   146  	}
   147  	return p.deploySingle(c, appName, appf)
   148  }
   149  
   150  // deploySingle deploys a single function, either the current directory or if in the context
   151  // of an app and user provides relative path as the first arg, it will deploy that function.
   152  func (p *deploycmd) deploySingle(c *cli.Context, appName string, appf *common.AppFile) error {
   153  	var dir string
   154  	wd := common.GetWd()
   155  
   156  	if c.String("working-dir") != "" {
   157  		dir = c.String("working-dir")
   158  	} else {
   159  		// if we're in the context of an app, first arg is path to the function
   160  		path := c.Args().First()
   161  		if path != "" {
   162  			fmt.Printf("Deploying function at: /%s\n", path)
   163  		}
   164  		dir = filepath.Join(wd, path)
   165  	}
   166  
   167  	err := os.Chdir(dir)
   168  	if err != nil {
   169  		return err
   170  	}
   171  	defer os.Chdir(wd)
   172  
   173  	ffV, err := common.ReadInFuncFile()
   174  	if err != nil {
   175  		return err
   176  	}
   177  
   178  	switch common.GetFuncYamlVersion(ffV) {
   179  	case common.LatestYamlVersion:
   180  		fpath, ff, err := common.FindAndParseFuncFileV20180708(dir)
   181  		if err != nil {
   182  			return err
   183  		}
   184  		if appf != nil {
   185  			if dir == wd {
   186  				setFuncInfoV20180708(ff, appf.Name)
   187  			}
   188  		}
   189  
   190  		if appf != nil {
   191  			err = p.updateAppConfig(appf)
   192  			if err != nil {
   193  				return fmt.Errorf("Failed to update app config: %v", err)
   194  			}
   195  		}
   196  
   197  		return p.deployFuncV20180708(c, appName, wd, fpath, ff)
   198  	default:
   199  		return fmt.Errorf("routes are no longer supported, please use the migrate command to update your metadata")
   200  	}
   201  }
   202  
   203  // deployAll deploys all functions in an app.
   204  func (p *deploycmd) deployAll(c *cli.Context, appName string, appf *common.AppFile) error {
   205  	if appf != nil {
   206  		err := p.updateAppConfig(appf)
   207  		if err != nil {
   208  			return fmt.Errorf("Failed to update app config: %v", err)
   209  		}
   210  	}
   211  
   212  	var dir string
   213  	wd := common.GetWd()
   214  
   215  	if c.String("dir") != "" {
   216  		dir = c.String("dir")
   217  	} else {
   218  		dir = wd
   219  	}
   220  
   221  	var funcFound bool
   222  	err := common.WalkFuncsV20180708(dir, func(path string, ff *common.FuncFileV20180708, err error) error {
   223  		if err != nil { // probably some issue with funcfile parsing, can decide to handle this differently if we'd like
   224  			return err
   225  		}
   226  		dir := filepath.Dir(path)
   227  		if dir == wd {
   228  			setFuncInfoV20180708(ff, appName)
   229  		} else {
   230  			// change dirs
   231  			err = os.Chdir(dir)
   232  			if err != nil {
   233  				return err
   234  			}
   235  			p2 := strings.TrimPrefix(dir, wd)
   236  			if ff.Name == "" {
   237  				ff.Name = strings.Replace(p2, "/", "-", -1)
   238  				if strings.HasPrefix(ff.Name, "-") {
   239  					ff.Name = ff.Name[1:]
   240  				}
   241  				// todo: should we prefix appname too?
   242  			}
   243  		}
   244  
   245  		err = p.deployFuncV20180708(c, appName, wd, path, ff)
   246  		if err != nil {
   247  			return fmt.Errorf("deploy error on %s: %v", path, err)
   248  		}
   249  
   250  		now := time.Now()
   251  		os.Chtimes(path, now, now)
   252  		funcFound = true
   253  		return nil
   254  	})
   255  	if err != nil {
   256  		return err
   257  	}
   258  
   259  	if !funcFound {
   260  		return errors.New("No functions found to deploy")
   261  	}
   262  
   263  	return nil
   264  }
   265  
   266  func (p *deploycmd) deployFuncV20180708(c *cli.Context, appName, baseDir, funcfilePath string, funcfile *common.FuncFileV20180708) error {
   267  	if appName == "" {
   268  		return errors.New("App name must be provided, try `--app APP_NAME`")
   269  	}
   270  
   271  	if funcfile.Name == "" {
   272  		funcfile.Name = filepath.Base(filepath.Dir(funcfilePath)) // todo: should probably make a copy of ff before changing it
   273  	}
   274  	fmt.Printf("Deploying %s to app: %s\n", funcfile.Name, appName)
   275  
   276  	var err error
   277  	if !p.noBump {
   278  		funcfile2, err := common.BumpItV20180708(funcfilePath, common.Patch)
   279  		if err != nil {
   280  			return err
   281  		}
   282  		funcfile.Version = funcfile2.Version
   283  		// TODO: this whole funcfile handling needs some love, way too confusing. Only bump makes permanent changes to it.
   284  	}
   285  
   286  	buildArgs := c.StringSlice("build-arg")
   287  	_, err = common.BuildFuncV20180708(c.GlobalBool("verbose"), funcfilePath, funcfile, buildArgs, p.noCache)
   288  	if err != nil {
   289  		return err
   290  	}
   291  
   292  	if !p.local {
   293  		if err := common.DockerPushV20180708(funcfile); err != nil {
   294  			return err
   295  		}
   296  	}
   297  
   298  	return p.updateFunction(c, appName, funcfile)
   299  }
   300  
   301  func setRootFuncInfo(ff *common.FuncFile, appName string) {
   302  	if ff.Name == "" {
   303  		fmt.Println("Setting name")
   304  		ff.Name = fmt.Sprintf("%s-root", appName)
   305  	}
   306  	if ff.Path == "" {
   307  		// then in root dir, so this will be deployed at /
   308  		ff.Path = "/"
   309  	}
   310  }
   311  
   312  func setFuncInfoV20180708(ff *common.FuncFileV20180708, appName string) {
   313  	if ff.Name == "" {
   314  		fmt.Println("Setting name")
   315  		ff.Name = fmt.Sprintf("%s-root", appName)
   316  	}
   317  }
   318  
   319  func (p *deploycmd) updateFunction(c *cli.Context, appName string, ff *common.FuncFileV20180708) error {
   320  	fmt.Printf("Updating function %s using image %s...\n", ff.Name, ff.ImageNameV20180708())
   321  
   322  	fn := &modelsV2.Fn{}
   323  	if err := function.WithFuncFileV20180708(ff, fn); err != nil {
   324  		return fmt.Errorf("Error getting function with funcfile: %s", err)
   325  	}
   326  
   327  	app, err := apps.GetAppByName(p.clientV2, appName)
   328  	if err != nil {
   329  		app = &modelsV2.App{
   330  			Name: appName,
   331  		}
   332  
   333  		err = apps.CreateApp(p.clientV2, app)
   334  		if err != nil {
   335  			return err
   336  		}
   337  		app, err = apps.GetAppByName(p.clientV2, appName)
   338  		if err != nil {
   339  			return err
   340  		}
   341  	}
   342  
   343  	fnRes, err := function.GetFnByName(p.clientV2, app.ID, ff.Name)
   344  	if err != nil {
   345  		fn.Name = ff.Name
   346  		err := function.CreateFn(p.clientV2, appName, fn)
   347  		if err != nil {
   348  			return err
   349  		}
   350  	} else {
   351  		fn.ID = fnRes.ID
   352  		err = function.PutFn(p.clientV2, fn.ID, fn)
   353  		if err != nil {
   354  			return err
   355  		}
   356  	}
   357  
   358  	if fnRes == nil {
   359  		fn, err = function.GetFnByName(p.clientV2, app.ID, ff.Name)
   360  		if err != nil {
   361  			return err
   362  		}
   363  	}
   364  
   365  	if len(ff.Triggers) != 0 {
   366  		for _, t := range ff.Triggers {
   367  			trig := &modelsV2.Trigger{
   368  				AppID:  app.ID,
   369  				FnID:   fn.ID,
   370  				Name:   t.Name,
   371  				Source: t.Source,
   372  				Type:   t.Type,
   373  			}
   374  
   375  			trigs, err := trigger.GetTriggerByName(p.clientV2, app.ID, fn.ID, t.Name)
   376  			if err != nil {
   377  				err = trigger.CreateTrigger(p.clientV2, trig)
   378  				if err != nil {
   379  					return err
   380  				}
   381  			} else {
   382  				trig.ID = trigs.ID
   383  				err = trigger.PutTrigger(p.clientV2, trig)
   384  				if err != nil {
   385  					return err
   386  				}
   387  			}
   388  		}
   389  	}
   390  
   391  	return nil
   392  }
   393  
   394  
   395  func (p *deploycmd) updateAppConfig(appf *common.AppFile) error {
   396  	app, err := apps.GetAppByName(p.clientV2, appf.Name)
   397  	if err != nil {
   398  		switch err.(type) {
   399  		case apps.NameNotFoundError:
   400  			param := clientApps.NewCreateAppParams()
   401  			param.Body = &modelsV2.App{
   402  				Name:        appf.Name,
   403  				Config:      appf.Config,
   404  				Annotations: appf.Annotations,
   405  			}
   406  			if _, err = p.clientV2.Apps.CreateApp(param); err != nil {
   407  				return err
   408  			}
   409  			return nil
   410  		default:
   411  			return err
   412  		}
   413  	}
   414  	param := clientApps.NewUpdateAppParams()
   415  	param.AppID = app.ID
   416  	param.Body = &modelsV2.App{
   417  		Config:      appf.Config,
   418  		Annotations: appf.Annotations,
   419  	}
   420  
   421  	if _, err = p.clientV2.Apps.UpdateApp(param); err != nil {
   422  		return err
   423  	}
   424  	return nil
   425  
   426  }