github.com/fnproject/cli@v0.0.0-20240508150455-e5d88bd86117/commands/init.go (about) 1 /* 2 * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package commands 18 19 /* 20 usage: fn init --help 21 22 o If there's a Dockerfile found, this will generate a basic 23 function file with the image and 'docker' as 'runtime' 24 like following, for example: 25 26 name: hello 27 version: 0.0.1 28 runtime: docker 29 path: /hello 30 31 then exit; if 'runtime' is 'docker' in the function file 32 and no Dockerfile exists, print an error message then exit 33 o It will then try to decipher the runtime based on 34 the files in the current directory, if it can't figure it out, 35 it will print an error message then exit. 36 */ 37 38 import ( 39 "errors" 40 "fmt" 41 "os" 42 "path/filepath" 43 "sort" 44 "strings" 45 46 "github.com/fnproject/cli/common" 47 "github.com/fnproject/cli/langs" 48 function "github.com/fnproject/cli/objects/fn" 49 modelsV2 "github.com/fnproject/fn_go/modelsv2" 50 "github.com/urfave/cli" 51 ) 52 53 type initFnCmd struct { 54 force bool 55 triggerType string 56 wd string 57 ff *common.FuncFileV20180708 58 } 59 60 func initFlags(a *initFnCmd) []cli.Flag { 61 fgs := []cli.Flag{ 62 cli.StringFlag{ 63 Name: "name", 64 Usage: "Name of the function. Defaults to directory name in lowercase.", 65 }, 66 cli.BoolFlag{ 67 Name: "force", 68 Usage: "Overwrite existing func.yaml", 69 Destination: &a.force, 70 }, 71 cli.StringFlag{ 72 Name: "runtime", 73 Usage: "Choose an existing runtime - " + langsList(), 74 }, 75 cli.StringFlag{ 76 Name: "init-image", 77 Usage: "A Docker image which will create a function template", 78 }, 79 cli.StringFlag{ 80 Name: "entrypoint", 81 Usage: "Entrypoint is the command to run to start this function - equivalent to Dockerfile ENTRYPOINT.", 82 }, 83 cli.StringFlag{ 84 Name: "cmd", 85 Usage: "Command to run to start this function - equivalent to Dockerfile CMD.", 86 }, 87 cli.StringFlag{ 88 Name: "version", 89 Usage: "Set initial function version", 90 Value: common.InitialVersion, 91 }, 92 cli.StringFlag{ 93 Name: "working-dir,w", 94 Usage: "Specify the working directory to initialise a function, must be the full path.", 95 Destination: &a.wd, 96 }, 97 cli.StringFlag{ 98 Name: "trigger", 99 Usage: "Specify the trigger type - permitted values are 'http'.", 100 Destination: &a.triggerType, 101 }, 102 cli.Uint64Flag{ 103 Name: "memory,m", 104 Usage: "Memory in MiB", 105 }, 106 cli.StringSliceFlag{ 107 Name: "config,c", 108 Usage: "Function configuration", 109 }, 110 cli.IntFlag{ 111 Name: "timeout", 112 Usage: "Function timeout (eg. 30)", 113 }, 114 cli.IntFlag{ 115 Name: "idle-timeout", 116 Usage: "Function idle timeout (eg. 30)", 117 }, 118 cli.StringSliceFlag{ 119 Name: "annotation", 120 Usage: "Function annotation (can be specified multiple times)", 121 }, 122 } 123 124 return fgs 125 } 126 127 func langsList() string { 128 allLangs := []string{} 129 for _, h := range langs.Helpers() { 130 allLangs = append(allLangs, h.LangStrings()...) 131 } 132 sort.Strings(allLangs) 133 // remove duplicates 134 var allUnique []string 135 for i, s := range allLangs { 136 if i > 0 && s == allLangs[i-1] { 137 continue 138 } 139 if deprecatedPythonRuntime(s) { 140 continue 141 } 142 allUnique = append(allUnique, s) 143 } 144 return strings.Join(allUnique, ", ") 145 } 146 147 func deprecatedPythonRuntime(runtime string) bool { 148 return runtime == "python3.8.5" || runtime == "python3.7.1" 149 } 150 151 // InitCommand returns init cli.command 152 func InitCommand() cli.Command { 153 a := &initFnCmd{ff: &common.FuncFileV20180708{}} 154 155 return cli.Command{ 156 Name: "init", 157 Usage: "\tCreate a local func.yaml file", 158 Category: "DEVELOPMENT COMMANDS", 159 Aliases: []string{"in"}, 160 Description: "This command creates a func.yaml file in the current directory.", 161 ArgsUsage: "[function-subdirectory]", 162 Action: a.init, 163 Flags: initFlags(a), 164 } 165 } 166 167 func (a *initFnCmd) init(c *cli.Context) error { 168 var err error 169 var dir string 170 var fn modelsV2.Fn 171 172 dir = common.GetWd() 173 if a.wd != "" { 174 dir = a.wd 175 } 176 177 function.WithFlags(c, &fn) 178 a.bindFn(&fn) 179 180 runtime := c.String("runtime") 181 initImage := c.String("init-image") 182 183 if runtime != "" && initImage != "" { 184 return fmt.Errorf("You can't supply --runtime with --init-image") 185 } 186 187 runtimeSpecified := runtime != "" 188 189 a.ff.Schema_version = common.LatestYamlVersion 190 if runtimeSpecified { 191 // go no further if the specified runtime is not supported 192 if runtime != common.FuncfileDockerRuntime && langs.GetLangHelper(runtime) == nil { 193 return fmt.Errorf("Init does not support the '%s' runtime", runtime) 194 } 195 if deprecatedPythonRuntime(runtime) { 196 return fmt.Errorf("Runtime %s is no more supported for new apps. Please use python or %s runtime for new apps.", runtime, runtime[:strings.LastIndex(runtime, ".")]) 197 } 198 } 199 200 path := c.Args().First() 201 if path != "" { 202 fmt.Printf("Creating function at: ./%s\n", path) 203 dir = filepath.Join(dir, path) 204 205 // check if dir exists, if it does, then we can't create function 206 if common.Exists(dir) { 207 if !a.force { 208 return fmt.Errorf("directory %s already exists, cannot init function", dir) 209 } 210 } else { 211 err = os.MkdirAll(dir, 0755) 212 if err != nil { 213 return err 214 } 215 } 216 } 217 218 if c.String("name") != "" { 219 a.ff.Name = strings.ToLower(c.String("name")) 220 } 221 222 if a.ff.Name == "" { 223 // then defaults to current directory for name, the name must be lowercase 224 a.ff.Name = strings.ToLower(filepath.Base(dir)) 225 } 226 227 if a.triggerType != "" { 228 a.triggerType = strings.ToLower(a.triggerType) 229 ok := validateTriggerType(a.triggerType) 230 if !ok { 231 return fmt.Errorf("init does not support the trigger type '%s'.\n Permitted values are 'http'.", a.triggerType) 232 } 233 234 // TODO when we allow multiple trigger definitions in a func file, we need 235 // to allow naming triggers in a func file as well as use the type of 236 // trigger to deduplicate the trigger names 237 238 trig := make([]common.Trigger, 1) 239 trig[0] = common.Trigger{ 240 Name: a.ff.Name, 241 Type: a.triggerType, 242 Source: "/" + a.ff.Name, 243 } 244 245 a.ff.Triggers = trig 246 247 } 248 249 err = os.Chdir(dir) 250 if err != nil { 251 return err 252 } 253 254 defer os.Chdir(dir) // todo: wrap this so we can log the error if changing back fails 255 256 if !a.force { 257 _, ff, err := common.LoadFuncfile(dir) 258 if _, ok := err.(*common.NotFoundError); !ok && err != nil { 259 return err 260 } 261 if ff != nil { 262 return errors.New("Function file already exists, aborting") 263 } 264 } 265 err = a.BuildFuncFileV20180708(c, dir) // TODO: Return LangHelper here, then don't need to refind the helper in generateBoilerplate() below 266 if err != nil { 267 return err 268 } 269 270 a.ff.Schema_version = common.LatestYamlVersion 271 272 if initImage != "" { 273 err := a.doInitImage(initImage, c) 274 if err != nil { 275 return err 276 } 277 } else { 278 // TODO: why don't we treat "docker" runtime as just another language helper? 279 // Then can get rid of several Docker specific if/else's like this one. 280 if runtimeSpecified && runtime != common.FuncfileDockerRuntime { 281 err := a.generateBoilerplate(dir, runtime) 282 if err != nil { 283 return err 284 } 285 } 286 } 287 288 if err := common.EncodeFuncFileV20180708YAML("func.yaml", a.ff); err != nil { 289 return err 290 } 291 292 fmt.Println("func.yaml created.") 293 return nil 294 } 295 296 func (a *initFnCmd) doInitImage(initImage string, c *cli.Context) error { 297 err := common.RunInitImage(initImage, a.ff.Name) 298 if err != nil { 299 return err 300 } 301 err = common.MergeFuncFileInitYAML("func.init.yaml", a.ff) 302 if err != nil { 303 return err 304 } 305 // Then CLI args can override some init-image options (TODO: remove this with #383) 306 if c.String("cmd") != "" { 307 a.ff.Cmd = c.String("cmd") 308 } 309 if c.String("entrypoint") != "" { 310 a.ff.Entrypoint = c.String("entrypoint") 311 } 312 _ = os.Remove("func.init.yaml") 313 return nil 314 } 315 316 func (a *initFnCmd) generateBoilerplate(path, runtime string) error { 317 helper := langs.GetLangHelper(runtime) 318 if helper != nil && helper.HasBoilerplate() { 319 if err := helper.GenerateBoilerplate(path); err != nil { 320 if err == langs.ErrBoilerplateExists { 321 return nil 322 } 323 return err 324 } 325 fmt.Println("Function boilerplate generated.") 326 } 327 return nil 328 } 329 330 func (a *initFnCmd) bindFn(fn *modelsV2.Fn) { 331 ff := a.ff 332 if fn.Memory > 0 { 333 ff.Memory = fn.Memory 334 } 335 if fn.Timeout != nil { 336 ff.Timeout = fn.Timeout 337 } 338 if fn.IdleTimeout != nil { 339 ff.IDLE_timeout = fn.IdleTimeout 340 } 341 } 342 343 // ValidateFuncName checks if the func name is valid, the name can't contain a colon and 344 // must be all lowercase 345 func ValidateFuncName(name string) error { 346 if strings.Contains(name, ":") { 347 return errors.New("Function name cannot contain a colon") 348 } 349 if strings.ToLower(name) != name { 350 return errors.New("Function name must be lowercase") 351 } 352 return nil 353 } 354 355 func (a *initFnCmd) BuildFuncFileV20180708(c *cli.Context, path string) error { 356 var err error 357 358 a.ff.Version = c.String("version") 359 if err = ValidateFuncName(a.ff.Name); err != nil { 360 return err 361 } 362 363 //if Dockerfile present, use 'docker' as 'runtime' 364 if common.Exists("Dockerfile") { 365 fmt.Println("Dockerfile found. Using runtime 'docker'.") 366 a.ff.Runtime = common.FuncfileDockerRuntime 367 return nil 368 } 369 runtime := c.String("runtime") 370 if runtime == common.FuncfileDockerRuntime { 371 return errors.New("Function file runtime is 'docker', but no Dockerfile exists") 372 } 373 374 if c.String("init-image") != "" { 375 return nil 376 } 377 378 var helper langs.LangHelper 379 if runtime == "" { 380 helper, err = detectRuntime(path) 381 if err != nil { 382 return err 383 } 384 fmt.Printf("Found %v function, assuming %v runtime.\n", helper.Runtime(), helper.Runtime()) 385 } else { 386 helper = langs.GetLangHelper(runtime) 387 } 388 if helper == nil { 389 fmt.Printf("Init does not support the %s runtime, you'll have to create your own Dockerfile for this function.\n", runtime) 390 } else { 391 if c.String("entrypoint") == "" { 392 a.ff.Entrypoint, err = helper.Entrypoint() 393 if err != nil { 394 return err 395 } 396 397 } else { 398 a.ff.Entrypoint = c.String("entrypoint") 399 } 400 401 if runtime == "" { 402 runtime = helper.Runtime() 403 } 404 405 a.ff.Runtime = runtime 406 407 if c.Uint64("memory") == 0 { 408 a.ff.Memory = helper.CustomMemory() 409 } 410 411 if c.String("cmd") == "" { 412 cmd, err := helper.Cmd() 413 if err != nil { 414 return err 415 } 416 a.ff.Cmd = cmd 417 } else { 418 a.ff.Cmd = c.String("cmd") 419 } 420 if helper.FixImagesOnInit() { 421 if a.ff.Build_image == "" { 422 buildImage, err := helper.BuildFromImage() 423 if err != nil { 424 return err 425 } 426 a.ff.Build_image = buildImage 427 } 428 if helper.IsMultiStage() { 429 if a.ff.Run_image == "" { 430 runImage, err := helper.RunFromImage() 431 if err != nil { 432 return err 433 } 434 a.ff.Run_image = runImage 435 } 436 } 437 } 438 } 439 if a.ff.Entrypoint == "" && a.ff.Cmd == "" { 440 return fmt.Errorf("Could not detect entrypoint or cmd for %v, use --entrypoint and/or --cmd to set them explicitly", a.ff.Runtime) 441 } 442 443 return nil 444 } 445 446 func detectRuntime(path string) (langs.LangHelper, error) { 447 for _, h := range langs.Helpers() { 448 filenames := []string{} 449 for _, ext := range h.Extensions() { 450 filenames = append(filenames, 451 filepath.Join(path, fmt.Sprintf("func%s", ext)), 452 filepath.Join(path, fmt.Sprintf("Func%s", ext)), 453 ) 454 } 455 for _, filename := range filenames { 456 if common.Exists(filename) { 457 return h, nil 458 } 459 } 460 } 461 return nil, fmt.Errorf("No supported files found to guess runtime, please set runtime explicitly with --runtime flag") 462 } 463 464 func validateTriggerType(triggerType string) bool { 465 switch triggerType { 466 case "http": 467 return true 468 default: 469 return false 470 } 471 }