github.com/devcamcar/cli@v0.0.0-20181107134215-706a05759d18/commands/init.go (about) 1 package commands 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 "archive/tar" 24 "bytes" 25 "errors" 26 "fmt" 27 "io" 28 "os" 29 "os/exec" 30 "path/filepath" 31 "strings" 32 33 "github.com/fnproject/cli/common" 34 "github.com/fnproject/cli/langs" 35 function "github.com/fnproject/cli/objects/fn" 36 modelsV2 "github.com/fnproject/fn_go/modelsv2" 37 "github.com/urfave/cli" 38 ) 39 40 type initFnCmd struct { 41 force bool 42 triggerType string 43 wd string 44 ff *common.FuncFileV20180708 45 } 46 47 func initFlags(a *initFnCmd) []cli.Flag { 48 fgs := []cli.Flag{ 49 cli.StringFlag{ 50 Name: "name", 51 Usage: "Name of the function. Defaults to directory name in lowercase.", 52 }, 53 cli.BoolFlag{ 54 Name: "force", 55 Usage: "Overwrite existing func.yaml", 56 Destination: &a.force, 57 }, 58 cli.StringFlag{ 59 Name: "runtime", 60 Usage: "Choose an existing runtime - " + langsList(), 61 }, 62 cli.StringFlag{ 63 Name: "init-image", 64 Usage: "A Docker image which will create a function template", 65 }, 66 cli.StringFlag{ 67 Name: "entrypoint", 68 Usage: "Entrypoint is the command to run to start this function - equivalent to Dockerfile ENTRYPOINT.", 69 }, 70 cli.StringFlag{ 71 Name: "cmd", 72 Usage: "Command to run to start this function - equivalent to Dockerfile CMD.", 73 }, 74 cli.StringFlag{ 75 Name: "version", 76 Usage: "Set initial function version", 77 Value: common.InitialVersion, 78 }, 79 cli.StringFlag{ 80 Name: "working-dir,w", 81 Usage: "Specify the working directory to initialise a function, must be the full path.", 82 Destination: &a.wd, 83 }, 84 cli.StringFlag{ 85 Name: "trigger", 86 Usage: "Specify the trigger type - permitted values are 'http'.", 87 Destination: &a.triggerType, 88 }, 89 cli.Uint64Flag{ 90 Name: "memory,m", 91 Usage: "Memory in MiB", 92 }, 93 cli.StringFlag{ 94 Name: "type,t", 95 Usage: "Function type - sync or async", 96 }, 97 cli.StringSliceFlag{ 98 Name: "config,c", 99 Usage: "Function configuration", 100 }, 101 cli.StringSliceFlag{ 102 Name: "headers", 103 Usage: "Function response headers", 104 }, 105 cli.StringFlag{ 106 Name: "format,f", 107 Usage: "Hot container IO format - default or http", 108 }, 109 cli.IntFlag{ 110 Name: "timeout", 111 Usage: "Function timeout (eg. 30)", 112 }, 113 cli.IntFlag{ 114 Name: "idle-timeout", 115 Usage: "Function idle timeout (eg. 30)", 116 }, 117 cli.StringSliceFlag{ 118 Name: "annotation", 119 Usage: "Function annotation (can be specified multiple times)", 120 }, 121 } 122 123 return fgs 124 } 125 126 func langsList() string { 127 allLangs := []string{} 128 for _, h := range langs.Helpers() { 129 allLangs = append(allLangs, h.LangStrings()...) 130 } 131 return strings.Join(allLangs, ", ") 132 } 133 134 // InitCommand returns init cli.command 135 func InitCommand() cli.Command { 136 a := &initFnCmd{ff: &common.FuncFileV20180708{}} 137 138 return cli.Command{ 139 Name: "init", 140 Usage: "\tCreate a local func.yaml file", 141 Category: "DEVELOPMENT COMMANDS", 142 Aliases: []string{"in"}, 143 Description: "This command creates a func.yaml file in the current directory.", 144 ArgsUsage: "[function-subdirectory]", 145 Action: a.init, 146 Flags: initFlags(a), 147 } 148 } 149 150 func (a *initFnCmd) init(c *cli.Context) error { 151 var err error 152 var dir string 153 var fn modelsV2.Fn 154 155 dir = common.GetWd() 156 if a.wd != "" { 157 dir = a.wd 158 } 159 160 function.WithFlags(c, &fn) 161 a.bindFn(&fn) 162 163 runtime := c.String("runtime") 164 initImage := c.String("init-image") 165 166 if runtime != "" && initImage != "" { 167 return fmt.Errorf("You can't supply --runtime with --init-image") 168 } 169 170 runtimeSpecified := runtime != "" 171 172 a.ff.Schema_version = common.LatestYamlVersion 173 if runtimeSpecified { 174 // go no further if the specified runtime is not supported 175 if runtime != common.FuncfileDockerRuntime && langs.GetLangHelper(runtime) == nil { 176 return fmt.Errorf("Init does not support the '%s' runtime", runtime) 177 } 178 } 179 180 path := c.Args().First() 181 if path != "" { 182 fmt.Printf("Creating function at: /%s\n", path) 183 dir = filepath.Join(dir, path) 184 185 // check if dir exists, if it does, then we can't create function 186 if common.Exists(dir) { 187 if !a.force { 188 return fmt.Errorf("directory %s already exists, cannot init function", dir) 189 } 190 } else { 191 err = os.MkdirAll(dir, 0755) 192 if err != nil { 193 return err 194 } 195 } 196 } 197 198 if c.String("name") != "" { 199 a.ff.Name = strings.ToLower(c.String("name")) 200 } 201 202 if a.ff.Name == "" { 203 // then defaults to current directory for name, the name must be lowercase 204 a.ff.Name = strings.ToLower(filepath.Base(dir)) 205 } 206 207 if a.triggerType != "" { 208 a.triggerType = strings.ToLower(a.triggerType) 209 ok := validateTriggerType(a.triggerType) 210 if !ok { 211 return fmt.Errorf("Init does not support the trigger type '%s'.\n Permitted values are 'http'.", a.triggerType) 212 } 213 214 trig := make([]common.Trigger, 1) 215 trig[0] = common.Trigger{ 216 Name: a.ff.Name + "-trigger", 217 Type: a.triggerType, 218 Source: "/" + a.ff.Name + "-trigger", 219 } 220 221 a.ff.Triggers = trig 222 223 } 224 225 err = os.Chdir(dir) 226 if err != nil { 227 return err 228 } 229 230 defer os.Chdir(dir) // todo: wrap this so we can log the error if changing back fails 231 232 if !a.force { 233 _, ff, err := common.LoadFuncfile(dir) 234 if _, ok := err.(*common.NotFoundError); !ok && err != nil { 235 return err 236 } 237 if ff != nil { 238 return errors.New("Function file already exists, aborting") 239 } 240 } 241 err = a.BuildFuncFileV20180708(c, dir) // TODO: Return LangHelper here, then don't need to refind the helper in generateBoilerplate() below 242 if err != nil { 243 return err 244 } 245 246 a.ff.Schema_version = common.LatestYamlVersion 247 248 if initImage != "" { 249 250 err = runInitImage(initImage, a) 251 if err != nil { 252 return err 253 } 254 255 // Merge the func.init.yaml from the initImage with a.ff 256 // write out the new func file 257 var initFf, err = common.ParseFuncfile("func.init.yaml") 258 if err != nil { 259 return errors.New("init-image did not produce a valid func.init.yaml") 260 } 261 262 // Build up a combined func.yaml (in a.ff) from the init-image and defaults and cli-args 263 // The following fields are already in a.ff: 264 // config, cpus, idle_timeout, memory, name, path, timeout, type, triggers, version 265 // Add the following from the init-image: 266 // build, build_image, cmd, content_type, entrypoint, expects, format, headers, run_image, runtime 267 a.ff.Build = initFf.Build 268 a.ff.Build_image = initFf.BuildImage 269 a.ff.Cmd = initFf.Cmd 270 a.ff.Content_type = initFf.ContentType 271 a.ff.Entrypoint = initFf.Entrypoint 272 a.ff.Expects = initFf.Expects 273 a.ff.Format = initFf.Format 274 a.ff.Run_image = initFf.RunImage 275 a.ff.Runtime = initFf.Runtime 276 277 // Then CLI args can override some init-image options (TODO: remove this with #383) 278 if c.String("cmd") != "" { 279 a.ff.Cmd = c.String("cmd") 280 } 281 282 if c.String("entrypoint") != "" { 283 a.ff.Entrypoint = c.String("entrypoint") 284 } 285 286 if c.String("format") != "" { 287 a.ff.Format = c.String("format") 288 } 289 290 if err := common.EncodeFuncFileV20180708YAML("func.yaml", a.ff); err != nil { 291 return err 292 } 293 294 os.Remove("func.init.yaml") 295 296 } else { 297 // TODO: why don't we treat "docker" runtime as just another language helper? 298 // Then can get rid of several Docker specific if/else's like this one. 299 if runtimeSpecified && runtime != common.FuncfileDockerRuntime { 300 err := a.generateBoilerplate(dir, runtime) 301 if err != nil { 302 return err 303 } 304 } 305 306 if err := common.EncodeFuncFileV20180708YAML("func.yaml", a.ff); err != nil { 307 return err 308 } 309 } 310 311 fmt.Println("func.yaml created.") 312 return nil 313 } 314 315 func runInitImage(initImage string, a *initFnCmd) error { 316 fmt.Println("Building from init-image: " + initImage) 317 318 // Run the initImage 319 var c1ErrB bytes.Buffer 320 tarR, tarW := io.Pipe() 321 322 c1 := exec.Command("docker", "run", "-e", "FN_FUNCTION_NAME="+a.ff.Name, initImage) 323 c1.Stderr = &c1ErrB 324 c1.Stdout = tarW 325 326 c1Err := c1.Start() 327 if c1Err != nil { 328 fmt.Println(c1ErrB.String()) 329 return errors.New("Error running init-image") 330 } 331 332 err := untarStream(tarR) 333 if err != nil { 334 return errors.New("Error un-tarring the output of the init-image") 335 } 336 337 return nil 338 } 339 340 // Untars an io.Reader into the cwd 341 func untarStream(r io.Reader) error { 342 343 tr := tar.NewReader(r) 344 for { 345 header, err := tr.Next() 346 347 if err == io.EOF { 348 // if no more files are found we are finished 349 return nil 350 } 351 352 if err != nil { 353 return err 354 } 355 356 switch header.Typeflag { 357 // if its a dir and it doesn't exist create it 358 case tar.TypeDir: 359 if _, err := os.Stat(header.Name); err != nil { 360 if err := os.MkdirAll(header.Name, 0755); err != nil { 361 return err 362 } 363 } 364 365 // if it's a file create it 366 case tar.TypeReg: 367 f, err := os.OpenFile(header.Name, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode)) 368 if err != nil { 369 return err 370 } 371 372 // copy over contents 373 if _, err := io.Copy(f, tr); err != nil { 374 return err 375 } 376 377 f.Close() 378 } 379 } 380 } 381 382 func (a *initFnCmd) generateBoilerplate(path, runtime string) error { 383 helper := langs.GetLangHelper(runtime) 384 if helper != nil && helper.HasBoilerplate() { 385 if err := helper.GenerateBoilerplate(path); err != nil { 386 if err == langs.ErrBoilerplateExists { 387 return nil 388 } 389 return err 390 } 391 fmt.Println("Function boilerplate generated.") 392 } 393 return nil 394 } 395 396 func (a *initFnCmd) bindFn(fn *modelsV2.Fn) { 397 ff := a.ff 398 if fn.Format != "" { 399 ff.Format = fn.Format 400 } 401 if fn.Memory > 0 { 402 ff.Memory = fn.Memory 403 } 404 if fn.Timeout != nil { 405 ff.Timeout = fn.Timeout 406 } 407 if fn.IDLETimeout != nil { 408 ff.IDLE_timeout = fn.IDLETimeout 409 } 410 } 411 412 // ValidateFuncName checks if the func name is valid, the name can't contain a colon and 413 // must be all lowercase 414 func ValidateFuncName(name string) error { 415 if strings.Contains(name, ":") { 416 return errors.New("Function name cannot contain a colon") 417 } 418 if strings.ToLower(name) != name { 419 return errors.New("Function name must be lowercase") 420 } 421 return nil 422 } 423 424 func (a *initFnCmd) BuildFuncFileV20180708(c *cli.Context, path string) error { 425 var err error 426 427 a.ff.Version = c.String("version") 428 if err = ValidateFuncName(a.ff.Name); err != nil { 429 return err 430 } 431 432 //if Dockerfile present, use 'docker' as 'runtime' 433 if common.Exists("Dockerfile") { 434 fmt.Println("Dockerfile found. Using runtime 'docker'.") 435 a.ff.Runtime = common.FuncfileDockerRuntime 436 return nil 437 } 438 runtime := c.String("runtime") 439 if runtime == common.FuncfileDockerRuntime { 440 return errors.New("Function file runtime is 'docker', but no Dockerfile exists") 441 } 442 443 if c.String("init-image") != "" { 444 return nil 445 } 446 447 var helper langs.LangHelper 448 if runtime == "" { 449 helper, err = detectRuntime(path) 450 if err != nil { 451 return err 452 } 453 fmt.Printf("Found %v function, assuming %v runtime.\n", helper.Runtime(), helper.Runtime()) 454 //need to default this to default format to be backwards compatible. Might want to just not allow this anymore, fail here. 455 if c.String("format") == "" { 456 a.ff.Format = "default" 457 } 458 } else { 459 helper = langs.GetLangHelper(runtime) 460 } 461 if helper == nil { 462 fmt.Printf("Init does not support the %s runtime, you'll have to create your own Dockerfile for this function.\n", runtime) 463 } else { 464 if c.String("entrypoint") == "" { 465 a.ff.Entrypoint, err = helper.Entrypoint() 466 if err != nil { 467 return err 468 } 469 470 } else { 471 a.ff.Entrypoint = c.String("entrypoint") 472 } 473 474 if runtime == "" { 475 runtime = helper.Runtime() 476 } 477 478 a.ff.Runtime = runtime 479 480 if c.String("format") == "" { 481 a.ff.Format = helper.DefaultFormat() 482 } 483 484 if c.String("cmd") == "" { 485 cmd, err := helper.Cmd() 486 if err != nil { 487 return err 488 } 489 a.ff.Cmd = cmd 490 } else { 491 a.ff.Cmd = c.String("cmd") 492 } 493 if helper.FixImagesOnInit() { 494 if a.ff.Build_image == "" { 495 buildImage, err := helper.BuildFromImage() 496 if err != nil { 497 return err 498 } 499 a.ff.Build_image = buildImage 500 } 501 if helper.IsMultiStage() { 502 if a.ff.Run_image == "" { 503 runImage, err := helper.RunFromImage() 504 if err != nil { 505 return err 506 } 507 a.ff.Run_image = runImage 508 } 509 } 510 } 511 } 512 if a.ff.Entrypoint == "" && a.ff.Cmd == "" { 513 return fmt.Errorf("Could not detect entrypoint or cmd for %v, use --entrypoint and/or --cmd to set them explicitly", a.ff.Runtime) 514 } 515 516 return nil 517 } 518 519 func detectRuntime(path string) (langs.LangHelper, error) { 520 for _, h := range langs.Helpers() { 521 filenames := []string{} 522 for _, ext := range h.Extensions() { 523 filenames = append(filenames, 524 filepath.Join(path, fmt.Sprintf("func%s", ext)), 525 filepath.Join(path, fmt.Sprintf("Func%s", ext)), 526 ) 527 } 528 for _, filename := range filenames { 529 if common.Exists(filename) { 530 return h, nil 531 } 532 } 533 } 534 return nil, fmt.Errorf("No supported files found to guess runtime, please set runtime explicitly with --runtime flag") 535 } 536 537 func validateTriggerType(triggerType string) bool { 538 switch triggerType { 539 case "http": 540 return true 541 default: 542 return false 543 } 544 }