github.com/iron-io/functions@v0.0.0-20180820112432-d59d7d1c40b2/fn/commands/images/deploy.go (about) 1 package commands 2 3 import ( 4 "errors" 5 "fmt" 6 "io" 7 "net/http" 8 "os" 9 "path/filepath" 10 "time" 11 12 "github.com/iron-io/functions/fn/common" 13 functions "github.com/iron-io/functions_go" 14 "github.com/urfave/cli" 15 ) 16 17 func Deploy() cli.Command { 18 cmd := deploycmd{ 19 RoutesApi: functions.NewRoutesApi(), 20 } 21 var flags []cli.Flag 22 flags = append(flags, cmd.flags()...) 23 return cli.Command{ 24 Name: "deploy", 25 ArgsUsage: "<appName>", 26 Usage: "scan local directory for functions, build and push all of them to `APPNAME`.", 27 Flags: flags, 28 Action: cmd.scan, 29 } 30 } 31 32 type deploycmd struct { 33 appName string 34 *functions.RoutesApi 35 36 wd string 37 verbose bool 38 incremental bool 39 skippush bool 40 41 verbwriter io.Writer 42 } 43 44 func (p *deploycmd) flags() []cli.Flag { 45 return []cli.Flag{ 46 cli.BoolFlag{ 47 Name: "v", 48 Usage: "verbose mode", 49 Destination: &p.verbose, 50 }, 51 cli.StringFlag{ 52 Name: "d", 53 Usage: "working directory", 54 Destination: &p.wd, 55 EnvVar: "WORK_DIR", 56 Value: "./", 57 }, 58 cli.BoolFlag{ 59 Name: "i", 60 Usage: "uses incremental building", 61 Destination: &p.incremental, 62 }, 63 cli.BoolFlag{ 64 Name: "skip-push", 65 Usage: "does not push Docker built images onto Docker Hub - useful for local development.", 66 Destination: &p.skippush, 67 }, 68 } 69 } 70 71 func (p *deploycmd) scan(c *cli.Context) error { 72 p.appName = c.Args().First() 73 p.verbwriter = common.Verbwriter(p.verbose) 74 75 var walked bool 76 77 err := filepath.Walk(p.wd, func(path string, info os.FileInfo, err error) error { 78 if path != p.wd && info.IsDir() { 79 return filepath.SkipDir 80 } 81 82 if !isFuncfile(path, info) { 83 return nil 84 } 85 86 if p.incremental && !isstale(path) { 87 return nil 88 } 89 90 e := p.deploy(path) 91 if err != nil { 92 fmt.Fprintln(p.verbwriter, path, e) 93 } 94 95 now := time.Now() 96 os.Chtimes(path, now, now) 97 walked = true 98 return e 99 }) 100 if err != nil { 101 fmt.Fprintf(p.verbwriter, "file walk error: %s\n", err) 102 } 103 104 if !walked { 105 return errors.New("No function file found.") 106 } 107 108 return nil 109 } 110 111 // deploy will take the found function and check for the presence of a 112 // Dockerfile, and run a three step process: parse functions file, build and 113 // push the container, and finally it will update function's route. Optionally, 114 // the route can be overriden inside the functions file. 115 func (p *deploycmd) deploy(path string) error { 116 fmt.Fprintln(p.verbwriter, "deploying", path) 117 118 funcfile, err := common.Buildfunc(p.verbwriter, path) 119 if err != nil { 120 return err 121 } 122 123 if p.skippush { 124 return nil 125 } 126 127 if err := common.Dockerpush(funcfile); err != nil { 128 return err 129 } 130 131 return p.route(path, funcfile) 132 } 133 134 func (p *deploycmd) route(path string, ff *common.Funcfile) error { 135 if err := common.ResetBasePath(p.Configuration); err != nil { 136 return fmt.Errorf("error setting endpoint: %v", err) 137 } 138 139 if ff.Path == nil { 140 _, path := common.AppNamePath(ff.FullName()) 141 ff.Path = &path 142 } 143 144 if ff.Memory == nil { 145 ff.Memory = new(int64) 146 } 147 if ff.Type == nil { 148 ff.Type = new(string) 149 } 150 if ff.Format == nil { 151 ff.Format = new(string) 152 } 153 if ff.MaxConcurrency == nil { 154 ff.MaxConcurrency = new(int) 155 } 156 if ff.Timeout == nil { 157 dur := time.Duration(0) 158 ff.Timeout = &dur 159 } 160 161 headers := make(map[string][]string) 162 for k, v := range ff.Headers { 163 headers[k] = []string{v} 164 } 165 body := functions.RouteWrapper{ 166 Route: functions.Route{ 167 Path: *ff.Path, 168 Image: ff.FullName(), 169 Memory: *ff.Memory, 170 Type_: *ff.Type, 171 Config: expandEnvConfig(ff.Config), 172 Headers: headers, 173 Format: *ff.Format, 174 MaxConcurrency: int32(*ff.MaxConcurrency), 175 Timeout: int32(ff.Timeout.Seconds()), 176 }, 177 } 178 179 fmt.Fprintf(p.verbwriter, "updating API with app: %s route: %s name: %s \n", p.appName, *ff.Path, ff.Name) 180 181 wrapper, resp, err := p.AppsAppRoutesPost(p.appName, body) 182 if err != nil { 183 return fmt.Errorf("error getting routes: %v", err) 184 } 185 if resp.StatusCode == http.StatusBadRequest { 186 return fmt.Errorf("error storing this route: %s", wrapper.Error_.Message) 187 } 188 189 return nil 190 } 191 192 func expandEnvConfig(configs map[string]string) map[string]string { 193 for k, v := range configs { 194 configs[k] = os.ExpandEnv(v) 195 } 196 return configs 197 } 198 199 func isFuncfile(path string, info os.FileInfo) bool { 200 if info.IsDir() { 201 return false 202 } 203 204 basefn := filepath.Base(path) 205 for _, fn := range common.Validfn { 206 if basefn == fn { 207 return true 208 } 209 } 210 211 return false 212 } 213 214 // Theory of operation: this takes an optimistic approach to detect whether a 215 // package must be rebuild/bump/deployed. It loads for all files mtime's and 216 // compare with functions.json own mtime. If any file is younger than 217 // functions.json, it triggers a rebuild. 218 // The problem with this approach is that depending on the OS running it, the 219 // time granularity of these timestamps might lead to false negatives - that is 220 // a package that is stale but it is not recompiled. A more elegant solution 221 // could be applied here, like https://golang.org/src/cmd/go/pkg.go#L1111 222 func isstale(path string) bool { 223 fi, err := os.Stat(path) 224 if err != nil { 225 return true 226 } 227 228 fnmtime := fi.ModTime() 229 dir := filepath.Dir(path) 230 err = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { 231 if info.IsDir() { 232 return nil 233 } 234 if info.ModTime().After(fnmtime) { 235 return errors.New("found stale package") 236 } 237 return nil 238 }) 239 240 return err != nil 241 }