github.com/tonto/cli@v0.0.0-20180104210444-aec958fa47db/init.go (about) 1 package main 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 "errors" 24 "fmt" 25 "os" 26 "path/filepath" 27 "strings" 28 29 "github.com/fnproject/cli/langs" 30 "github.com/fnproject/fn_go/models" 31 "github.com/urfave/cli" 32 ) 33 34 type initFnCmd struct { 35 force bool 36 ff *funcfile 37 } 38 39 func initFlags(a *initFnCmd) []cli.Flag { 40 fgs := []cli.Flag{ 41 cli.StringFlag{ 42 Name: "name", 43 Usage: "name of the function. Defaults to directory name.", 44 Destination: &a.ff.Name, 45 }, 46 cli.BoolFlag{ 47 Name: "force", 48 Usage: "overwrite existing func.yaml", 49 Destination: &a.force, 50 }, 51 cli.StringFlag{ 52 Name: "runtime", 53 Usage: "choose an existing runtime - " + langsList(), 54 Destination: &a.ff.Runtime, 55 }, 56 cli.StringFlag{ 57 Name: "entrypoint", 58 Usage: "entrypoint is the command to run to start this function - equivalent to Dockerfile ENTRYPOINT.", 59 Destination: &a.ff.Entrypoint, 60 }, 61 cli.StringFlag{ 62 Name: "cmd", 63 Usage: "command to run to start this function - equivalent to Dockerfile CMD.", 64 Destination: &a.ff.Entrypoint, 65 }, 66 cli.StringFlag{ 67 Name: "version", 68 Usage: "set initial function version", 69 Destination: &a.ff.Version, 70 Value: initialVersion, 71 }, 72 } 73 74 return append(fgs, routeFlags...) 75 } 76 77 func langsList() string { 78 allLangs := []string{} 79 for _, h := range langs.Helpers() { 80 allLangs = append(allLangs, h.LangStrings()...) 81 } 82 return strings.Join(allLangs, ", ") 83 } 84 85 func initFn() cli.Command { 86 a := &initFnCmd{ff: &funcfile{}} 87 88 return cli.Command{ 89 Name: "init", 90 Usage: "create a local func.yaml file", 91 Description: "Creates a func.yaml file in the current directory.", 92 ArgsUsage: "[FUNCTION_NAME]", 93 Action: a.init, 94 Flags: initFlags(a), 95 } 96 } 97 98 func (a *initFnCmd) init(c *cli.Context) error { 99 wd := getWd() 100 101 var rt models.Route 102 routeWithFlags(c, &rt) 103 a.bindRoute(&rt) 104 105 runtimeSpecified := a.ff.Runtime != "" 106 if runtimeSpecified { 107 // go no further if the specified runtime is not supported 108 if a.ff.Runtime != funcfileDockerRuntime && langs.GetLangHelper(a.ff.Runtime) == nil { 109 return fmt.Errorf("Init does not support the '%s' runtime.", a.ff.Runtime) 110 } 111 } 112 113 var err error 114 path := c.Args().First() 115 if path != "" { 116 fmt.Printf("Creating function at: /%s\n", path) 117 dir := filepath.Join(wd, path) 118 // check if dir exists, if it does, then we can't create function 119 if exists(dir) { 120 if !a.force { 121 return fmt.Errorf("directory %s already exists, cannot init function", dir) 122 } 123 } else { 124 err = os.MkdirAll(dir, 0755) 125 if err != nil { 126 return err 127 } 128 } 129 err = os.Chdir(dir) 130 if err != nil { 131 return err 132 } 133 defer os.Chdir(wd) // todo: wrap this so we can log the error if changing back fails 134 } 135 136 if !a.force { 137 _, ff, err := loadFuncfile() 138 if _, ok := err.(*notFoundError); !ok && err != nil { 139 return err 140 } 141 if ff != nil { 142 return errors.New("Function file already exists, aborting.") 143 } 144 } 145 146 err = a.buildFuncFile(c) // TODO: Return LangHelper here, then don't need to refind the helper in generateBoilerplate() below 147 if err != nil { 148 return err 149 } 150 151 // TODO: why don't we treat "docker" runtime as just another language helper? Then can get rid of several Docker 152 // specific if/else's like this one. 153 if runtimeSpecified && a.ff.Runtime != funcfileDockerRuntime { 154 err := a.generateBoilerplate() 155 if err != nil { 156 return err 157 } 158 } 159 160 if err := encodeFuncfileYAML("func.yaml", a.ff); err != nil { 161 return err 162 } 163 fmt.Println("func.yaml created.") 164 return nil 165 } 166 167 func (a *initFnCmd) generateBoilerplate() error { 168 helper := langs.GetLangHelper(a.ff.Runtime) 169 if helper != nil && helper.HasBoilerplate() { 170 if err := helper.GenerateBoilerplate(); err != nil { 171 if err == langs.ErrBoilerplateExists { 172 return nil 173 } 174 return err 175 } 176 fmt.Println("Function boilerplate generated.") 177 } 178 return nil 179 } 180 181 func (a *initFnCmd) bindRoute(rt *models.Route) { 182 ff := a.ff 183 if rt.Format != "" { 184 ff.Format = rt.Format 185 } 186 if rt.Type != "" { 187 ff.Type = rt.Type 188 } 189 if rt.Memory > 0 { 190 ff.Memory = rt.Memory 191 } 192 if rt.Timeout != nil { 193 ff.Timeout = rt.Timeout 194 } 195 if rt.IDLETimeout != nil { 196 ff.IDLETimeout = rt.IDLETimeout 197 } 198 } 199 200 func (a *initFnCmd) buildFuncFile(c *cli.Context) error { 201 wd := getWd() 202 var err error 203 204 if a.ff.Name == "" { 205 // then defaults to current directory for name, we'll just leave it out of func.yaml 206 // a.Name = filepath.Base(pwd) 207 } else if strings.Contains(a.ff.Name, ":") { 208 return errors.New("function name cannot contain a colon") 209 } 210 211 //if Dockerfile present, use 'docker' as 'runtime' 212 if exists("Dockerfile") { 213 fmt.Println("Dockerfile found. Using runtime 'docker'.") 214 a.ff.Runtime = funcfileDockerRuntime 215 return nil 216 } 217 if a.ff.Runtime == funcfileDockerRuntime { 218 return errors.New("function file runtime is 'docker', but no Dockerfile exists") 219 } 220 221 var helper langs.LangHelper 222 if a.ff.Runtime == "" { 223 helper, err = detectRuntime(wd) 224 if err != nil { 225 return err 226 } 227 fmt.Printf("Found %v function, assuming %v runtime.\n", helper.Runtime(), helper.Runtime()) 228 } else { 229 fmt.Println("Runtime:", a.ff.Runtime) 230 helper = langs.GetLangHelper(a.ff.Runtime) 231 } 232 if helper == nil { 233 fmt.Printf("Init does not support the %s runtime, you'll have to create your own Dockerfile for this function.\n", a.ff.Runtime) 234 } else { 235 if a.ff.Entrypoint == "" { 236 a.ff.Entrypoint, err = helper.Entrypoint() 237 if err != nil { 238 return err 239 } 240 } 241 242 if a.ff.Runtime == "" { 243 a.ff.Runtime = helper.Runtime() 244 } 245 246 if a.ff.Format == "" { 247 a.ff.Format = helper.DefaultFormat() 248 } 249 250 if a.ff.Cmd == "" { 251 cmd, err := helper.Cmd() 252 if err != nil { 253 return err 254 } 255 a.ff.Cmd = cmd 256 } 257 258 if helper.FixImagesOnInit() { 259 if a.ff.BuildImage == "" { 260 buildImage, err := helper.BuildFromImage() 261 if err != nil { 262 return err 263 } 264 a.ff.BuildImage = buildImage 265 } 266 if helper.IsMultiStage() { 267 if a.ff.RunImage == "" { 268 runImage, err := helper.RunFromImage() 269 if err != nil { 270 return err 271 } 272 a.ff.RunImage = runImage 273 } 274 } 275 } 276 } 277 278 if a.ff.Entrypoint == "" && a.ff.Cmd == "" { 279 return fmt.Errorf("could not detect entrypoint or cmd for %v, use --entrypoint and/or --cmd to set them explicitly", a.ff.Runtime) 280 } 281 282 return nil 283 } 284 285 func detectRuntime(path string) (langs.LangHelper, error) { 286 for _, h := range langs.Helpers() { 287 filenames := []string{} 288 for _, ext := range h.Extensions() { 289 filenames = append(filenames, 290 filepath.Join(path, fmt.Sprintf("func%s", ext)), 291 filepath.Join(path, fmt.Sprintf("Func%s", ext)), 292 filepath.Join(path, fmt.Sprintf("src/main%s", ext)), // rust 293 ) 294 } 295 for _, filename := range filenames { 296 if exists(filename) { 297 return h, nil 298 } 299 } 300 } 301 return nil, fmt.Errorf("no supported files found to guess runtime, please set runtime explicitly with --runtime flag") 302 }