github.com/tonto/cli@v0.0.0-20180104210444-aec958fa47db/deploy.go (about) 1 package main 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 fnclient "github.com/fnproject/fn_go/client" 13 "github.com/fnproject/fn_go/models" 14 "github.com/urfave/cli" 15 ) 16 17 func deploy() cli.Command { 18 cmd := deploycmd{ 19 Fn: client.APIClient(), 20 } 21 var flags []cli.Flag 22 flags = append(flags, cmd.flags()...) 23 return cli.Command{ 24 Name: "deploy", 25 Usage: "deploys a function to the functions server. (bumps, build, pushes and updates route)", 26 Flags: flags, 27 Action: cmd.deploy, 28 } 29 } 30 31 type deploycmd struct { 32 appName string 33 *fnclient.Fn 34 35 wd string 36 verbose bool 37 local bool 38 noCache bool 39 registry string 40 all bool 41 } 42 43 func (cmd *deploycmd) Registry() string { 44 return cmd.registry 45 } 46 47 func (p *deploycmd) flags() []cli.Flag { 48 return []cli.Flag{ 49 cli.StringFlag{ 50 Name: "app", 51 Usage: "app name to deploy to", 52 Destination: &p.appName, 53 }, 54 cli.BoolFlag{ 55 Name: "verbose, v", 56 Usage: "verbose mode", 57 Destination: &p.verbose, 58 }, 59 cli.BoolFlag{ 60 Name: "no-cache", 61 Usage: "Don't use Docker cache for the build", 62 Destination: &p.noCache, 63 }, 64 cli.BoolFlag{ 65 Name: "local, skip-push", // todo: deprecate skip-push 66 Usage: "does not push Docker built images onto Docker Hub - useful for local development.", 67 Destination: &p.local, 68 }, 69 cli.StringFlag{ 70 Name: "registry", 71 Usage: "Sets the Docker owner for images and optionally the registry. This will be prefixed to your function name for pushing to Docker registries. eg: `--registry username` will set your Docker Hub owner. `--registry registry.hub.docker.com/username` will set the registry and owner.", 72 Destination: &p.registry, 73 }, 74 cli.BoolFlag{ 75 Name: "all", 76 Usage: "if in root directory containing `app.yaml`, this will deploy all functions", 77 Destination: &p.all, 78 }, 79 } 80 } 81 82 // deploy deploys a function or a set of functions for an app 83 // By default this will deploy a single function, either the function in the current directory 84 // or if an arg is passed in, a function in the path representing that arg, relative to the 85 // current working directory. 86 // 87 // If user passes in --all flag, it will deploy all functions in an app. An app must have an `app.yaml` 88 // file in it's root directory. The functions will be deployed based on the directory structure 89 // on the file system (can be overridden using the `path` arg in each `func.yaml`. The index/root function 90 // is the one that lives in the same directory as the app.yaml. 91 func (p *deploycmd) deploy(c *cli.Context) error { 92 setRegistryEnv(p) 93 94 appName := "" 95 96 appf, err := loadAppfile() 97 if err != nil { 98 if _, ok := err.(*notFoundError); ok { 99 if p.all { 100 return err 101 } 102 // otherwise, it's ok 103 } else { 104 return err 105 } 106 107 } else { 108 appName = appf.Name 109 } 110 if p.appName != "" { 111 // flag overrides all 112 appName = p.appName 113 } 114 115 if appName == "" { 116 return errors.New("app name must be provided, try `--app APP_NAME`.") 117 } 118 119 if p.all { 120 return p.deployAll(c, appName, appf) 121 } 122 return p.deploySingle(c, appName, appf) 123 } 124 125 // deploySingle deploys a single function, either the current directory or if in the context 126 // of an app and user provides relative path as the first arg, it will deploy that function. 127 func (p *deploycmd) deploySingle(c *cli.Context, appName string, appf *appfile) error { 128 wd := getWd() 129 130 dir := wd 131 // if we're in the context of an app, first arg is path to the function 132 path := c.Args().First() 133 if path != "" { 134 fmt.Printf("Deploying function at: /%s\n", path) 135 dir = filepath.Join(wd, path) 136 err := os.Chdir(dir) 137 if err != nil { 138 return err 139 } 140 defer os.Chdir(wd) // todo: wrap this so we can log the error if changing back fails 141 } 142 143 fpath, ff, err := findAndParseFuncfile(dir) 144 if err != nil { 145 return err 146 } 147 if appf != nil { 148 if dir == wd { 149 setRootFuncInfo(ff, appf.Name) 150 } 151 } 152 return p.deployFunc(c, appName, wd, fpath, ff) 153 } 154 155 // deployAll deploys all functions in an app. 156 func (p *deploycmd) deployAll(c *cli.Context, appName string, appf *appfile) error { 157 wd := getWd() 158 159 var funcFound bool 160 err := walkFuncs(wd, func(path string, ff *funcfile, err error) error { 161 if err != nil { // probably some issue with funcfile parsing, can decide to handle this differently if we'd like 162 return err 163 } 164 dir := filepath.Dir(path) 165 if dir == wd { 166 setRootFuncInfo(ff, appName) 167 } else { 168 // change dirs 169 err = os.Chdir(dir) 170 if err != nil { 171 return err 172 } 173 p2 := strings.TrimPrefix(dir, wd) 174 if ff.Name == "" { 175 ff.Name = strings.Replace(p2, "/", "-", -1) 176 if strings.HasPrefix(ff.Name, "-") { 177 ff.Name = ff.Name[1:] 178 } 179 // todo: should we prefix appname too? 180 } 181 if ff.Path == "" { 182 ff.Path = p2 183 } 184 } 185 err = p.deployFunc(c, appName, wd, path, ff) 186 if err != nil { 187 return fmt.Errorf("deploy error on %s: %v", path, err) 188 } 189 190 now := time.Now() 191 os.Chtimes(path, now, now) 192 funcFound = true 193 return nil 194 }) 195 if err != nil { 196 return err 197 } 198 199 if !funcFound { 200 return errors.New("no functions found to deploy") 201 } 202 return nil 203 } 204 205 // deployFunc performs several actions to deploy to a functions server. 206 // Parse func.yaml file, bump version, build image, push to registry, and 207 // finally it will update function's route. Optionally, 208 // the route can be overriden inside the func.yaml file. 209 func (p *deploycmd) deployFunc(c *cli.Context, appName, baseDir, funcfilePath string, funcfile *funcfile) error { 210 if appName == "" { 211 return errors.New("app name must be provided, try `--app APP_NAME`.") 212 } 213 dir := filepath.Dir(funcfilePath) 214 // get name from directory if it's not defined 215 if funcfile.Name == "" { 216 funcfile.Name = filepath.Base(filepath.Dir(funcfilePath)) // todo: should probably make a copy of ff before changing it 217 } 218 if funcfile.Path == "" { 219 if dir == "." { 220 funcfile.Path = "/" 221 } else { 222 funcfile.Path = "/" + filepath.Base(dir) 223 } 224 225 } 226 fmt.Printf("Deploying %s to app: %s at path: %s\n", funcfile.Name, appName, funcfile.Path) 227 228 funcfile2, err := bumpIt(funcfilePath, Patch) 229 if err != nil { 230 return err 231 } 232 funcfile.Version = funcfile2.Version 233 // TODO: this whole funcfile handling needs some love, way too confusing. Only bump makes permanent changes to it. 234 235 _, err = buildfunc(c, funcfilePath, funcfile, p.noCache) 236 if err != nil { 237 return err 238 } 239 240 if !p.local { 241 if err := dockerPush(funcfile); err != nil { 242 return err 243 } 244 } 245 246 return p.updateRoute(c, appName, funcfile) 247 } 248 249 func setRootFuncInfo(ff *funcfile, appName string) { 250 if ff.Name == "" { 251 fmt.Println("setting name") 252 ff.Name = fmt.Sprintf("%s-root", appName) 253 } 254 if ff.Path == "" { 255 // then in root dir, so this will be deployed at / 256 ff.Path = "/" 257 } 258 } 259 260 func (p *deploycmd) updateRoute(c *cli.Context, appName string, ff *funcfile) error { 261 fmt.Printf("Updating route %s using image %s...\n", ff.Path, ff.ImageName()) 262 263 routesCmd := routesCmd{client: client.APIClient()} 264 rt := &models.Route{} 265 if err := routeWithFuncFile(ff, rt); err != nil { 266 return fmt.Errorf("error getting route with funcfile: %s", err) 267 } 268 return routesCmd.putRoute(c, appName, ff.Path, rt) 269 } 270 271 func expandEnvConfig(configs map[string]string) map[string]string { 272 for k, v := range configs { 273 configs[k] = os.ExpandEnv(v) 274 } 275 return configs 276 } 277 278 // Theory of operation: this takes an optimistic approach to detect whether a 279 // package must be rebuild/bump/deployed. It loads for all files mtime's and 280 // compare with functions.json own mtime. If any file is younger than 281 // functions.json, it triggers a rebuild. 282 // The problem with this approach is that depending on the OS running it, the 283 // time granularity of these timestamps might lead to false negatives - that is 284 // a package that is stale but it is not recompiled. A more elegant solution 285 // could be applied here, like https://golang.org/src/cmd/go/pkg.go#L1111 286 func isstale(path string) bool { 287 fi, err := os.Stat(path) 288 if err != nil { 289 return true 290 } 291 292 fnmtime := fi.ModTime() 293 dir := filepath.Dir(path) 294 err = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { 295 if info.IsDir() { 296 return nil 297 } 298 if info.ModTime().After(fnmtime) { 299 return errors.New("found stale package") 300 } 301 return nil 302 }) 303 304 return err != nil 305 }