github.com/devcamcar/cli@v0.0.0-20181107134215-706a05759d18/common/common.go (about) 1 package common 2 3 import ( 4 "bufio" 5 "bytes" 6 "encoding/json" 7 "errors" 8 "fmt" 9 "io" 10 "io/ioutil" 11 "log" 12 "os" 13 "os/exec" 14 "os/signal" 15 "path/filepath" 16 "strings" 17 "time" 18 "unicode" 19 20 "github.com/spf13/viper" 21 yaml "gopkg.in/yaml.v2" 22 23 "github.com/coreos/go-semver/semver" 24 "github.com/fatih/color" 25 "github.com/fnproject/cli/config" 26 "github.com/fnproject/cli/langs" 27 "github.com/urfave/cli" 28 ) 29 30 // Global docker variables. 31 const ( 32 FunctionsDockerImage = "fnproject/fnserver" 33 FuncfileDockerRuntime = "docker" 34 MinRequiredDockerVersion = "17.5.0" 35 ) 36 37 // GetWd returns working directory. 38 func GetWd() string { 39 wd, err := os.Getwd() 40 if err != nil { 41 log.Fatalln("Couldn't get working directory:", err) 42 } 43 return wd 44 } 45 46 // GetDir returns the dir if defined as a flag in cli.Context 47 func GetDir(c *cli.Context) string { 48 var dir string 49 if c.String("working-dir") != "" { 50 dir = c.String("working-dir") 51 } else { 52 dir = GetWd() 53 } 54 55 return dir 56 } 57 58 // BuildFunc bumps version and builds function. 59 func BuildFunc(verbose bool, fpath string, funcfile *FuncFile, buildArg []string, noCache bool) (*FuncFile, error) { 60 var err error 61 if funcfile.Version == "" { 62 funcfile, err = BumpIt(fpath, Patch) 63 if err != nil { 64 return nil, err 65 } 66 } 67 68 if err := localBuild(fpath, funcfile.Build); err != nil { 69 return nil, err 70 } 71 72 if err := dockerBuild(verbose, fpath, funcfile, buildArg, noCache); err != nil { 73 return nil, err 74 } 75 76 return funcfile, nil 77 } 78 79 // BuildFunc bumps version and builds function. 80 func BuildFuncV20180708(verbose bool, fpath string, funcfile *FuncFileV20180708, buildArg []string, noCache bool) (*FuncFileV20180708, error) { 81 var err error 82 83 if funcfile.Version == "" { 84 funcfile, err = BumpItV20180708(fpath, Patch) 85 if err != nil { 86 return nil, err 87 } 88 } 89 90 if err := localBuild(fpath, funcfile.Build); err != nil { 91 return nil, err 92 } 93 94 if err := dockerBuildV20180708(verbose, fpath, funcfile, buildArg, noCache); err != nil { 95 return nil, err 96 } 97 98 return funcfile, nil 99 } 100 101 func localBuild(path string, steps []string) error { 102 for _, cmd := range steps { 103 exe := exec.Command("/bin/sh", "-c", cmd) 104 exe.Dir = filepath.Dir(path) 105 if err := exe.Run(); err != nil { 106 return fmt.Errorf("error running command %v (%v)", cmd, err) 107 } 108 } 109 110 return nil 111 } 112 113 func PrintContextualInfo() { 114 var registry, currentContext string 115 registry = viper.GetString(config.EnvFnRegistry) 116 if registry == "" { 117 registry = "FN_REGISTRY is not set." 118 } 119 fmt.Println("FN_REGISTRY: ", registry) 120 121 currentContext = viper.GetString(config.CurrentContext) 122 if currentContext == "" { 123 currentContext = "No context currently in use." 124 } 125 fmt.Println("Current Context: ", currentContext) 126 } 127 128 func dockerBuild(verbose bool, fpath string, ff *FuncFile, buildArgs []string, noCache bool) error { 129 err := dockerVersionCheck() 130 if err != nil { 131 return err 132 } 133 134 dir := filepath.Dir(fpath) 135 136 var helper langs.LangHelper 137 dockerfile := filepath.Join(dir, "Dockerfile") 138 if !Exists(dockerfile) { 139 if ff.Runtime == FuncfileDockerRuntime { 140 return fmt.Errorf("Dockerfile does not exist for 'docker' runtime") 141 } 142 helper = langs.GetLangHelper(ff.Runtime) 143 if helper == nil { 144 return fmt.Errorf("Cannot build, no language helper found for %v", ff.Runtime) 145 } 146 dockerfile, err = writeTmpDockerfile(helper, dir, ff) 147 if err != nil { 148 return err 149 } 150 defer os.Remove(dockerfile) 151 if helper.HasPreBuild() { 152 err := helper.PreBuild() 153 if err != nil { 154 return err 155 } 156 } 157 } 158 err = RunBuild(verbose, dir, ff.ImageName(), dockerfile, buildArgs, noCache) 159 if err != nil { 160 return err 161 } 162 163 if helper != nil { 164 err := helper.AfterBuild() 165 if err != nil { 166 return err 167 } 168 } 169 return nil 170 } 171 172 func dockerBuildV20180708(verbose bool, fpath string, ff *FuncFileV20180708, buildArgs []string, noCache bool) error { 173 err := dockerVersionCheck() 174 if err != nil { 175 return err 176 } 177 178 dir := filepath.Dir(fpath) 179 180 var helper langs.LangHelper 181 dockerfile := filepath.Join(dir, "Dockerfile") 182 if !Exists(dockerfile) { 183 if ff.Runtime == FuncfileDockerRuntime { 184 return fmt.Errorf("Dockerfile does not exist for 'docker' runtime") 185 } 186 helper = langs.GetLangHelper(ff.Runtime) 187 if helper == nil { 188 return fmt.Errorf("Cannot build, no language helper found for %v", ff.Runtime) 189 } 190 dockerfile, err = writeTmpDockerfileV20180708(helper, dir, ff) 191 if err != nil { 192 return err 193 } 194 defer os.Remove(dockerfile) 195 if helper.HasPreBuild() { 196 err := helper.PreBuild() 197 if err != nil { 198 return err 199 } 200 } 201 } 202 err = RunBuild(verbose, dir, ff.ImageNameV20180708(), dockerfile, buildArgs, noCache) 203 if err != nil { 204 return err 205 } 206 207 if helper != nil { 208 err := helper.AfterBuild() 209 if err != nil { 210 return err 211 } 212 } 213 return nil 214 } 215 216 // RunBuild runs function from func.yaml/json/yml. 217 func RunBuild(verbose bool, dir, imageName, dockerfile string, buildArgs []string, noCache bool) error { 218 cancel := make(chan os.Signal, 3) 219 signal.Notify(cancel, os.Interrupt) // and others perhaps 220 defer signal.Stop(cancel) 221 222 result := make(chan error, 1) 223 224 buildOut := ioutil.Discard 225 buildErr := ioutil.Discard 226 227 quit := make(chan struct{}) 228 fmt.Fprintf(os.Stderr, "Building image %v ", imageName) 229 if verbose { 230 fmt.Println() 231 buildOut = os.Stdout 232 buildErr = os.Stderr 233 PrintContextualInfo() 234 } else { 235 // print dots. quit channel explanation: https://stackoverflow.com/a/16466581/105562 236 ticker := time.NewTicker(1 * time.Second) 237 go func() { 238 for { 239 select { 240 case <-ticker.C: 241 fmt.Fprintf(os.Stderr, ".") 242 case <-quit: 243 ticker.Stop() 244 return 245 } 246 } 247 }() 248 } 249 250 go func(done chan<- error) { 251 args := []string{ 252 "build", 253 "-t", imageName, 254 "-f", dockerfile, 255 } 256 if noCache { 257 args = append(args, "--no-cache") 258 } 259 260 if len(buildArgs) > 0 { 261 for _, buildArg := range buildArgs { 262 args = append(args, "--build-arg", buildArg) 263 } 264 } 265 args = append(args, 266 "--build-arg", "HTTP_PROXY", 267 "--build-arg", "HTTPS_PROXY", 268 ".") 269 cmd := exec.Command("docker", args...) 270 cmd.Dir = dir 271 cmd.Stderr = buildErr // Doesn't look like there's any output to stderr on docker build, whether it's successful or not. 272 cmd.Stdout = buildOut 273 done <- cmd.Run() 274 }(result) 275 276 select { 277 case err := <-result: 278 close(quit) 279 fmt.Fprintln(os.Stderr) 280 if err != nil { 281 if verbose == false { 282 fmt.Printf("%v Run with `--verbose` flag to see what went wrong. eg: `fn --verbose CMD`\n", color.RedString("Error during build.")) 283 } 284 return fmt.Errorf("error running docker build: %v", err) 285 } 286 case signal := <-cancel: 287 close(quit) 288 fmt.Fprintln(os.Stderr) 289 return fmt.Errorf("build cancelled on signal %v", signal) 290 } 291 return nil 292 } 293 294 func dockerVersionCheck() error { 295 out, err := exec.Command("docker", "version", "--format", "{{.Server.Version}}").Output() 296 if err != nil { 297 return fmt.Errorf("Cannot connect to the Docker daemon, make sure you have it installed and running: %v", err) 298 } 299 // dev / test builds append '-ce', trim this 300 trimmed := strings.TrimRightFunc(string(out), func(r rune) bool { return r != '.' && !unicode.IsDigit(r) }) 301 302 v, err := semver.NewVersion(trimmed) 303 if err != nil { 304 return fmt.Errorf("could not check Docker version: %v", err) 305 } 306 vMin, err := semver.NewVersion(MinRequiredDockerVersion) 307 if err != nil { 308 return fmt.Errorf("our bad, sorry... please make an issue, detailed error: %v", err) 309 } 310 if v.LessThan(*vMin) { 311 return fmt.Errorf("please upgrade your version of Docker to %s or greater", MinRequiredDockerVersion) 312 } 313 return nil 314 } 315 316 // Exists check file exists. 317 func Exists(name string) bool { 318 if _, err := os.Stat(name); err != nil { 319 if os.IsNotExist(err) { 320 return false 321 } 322 } 323 return true 324 } 325 326 func writeTmpDockerfile(helper langs.LangHelper, dir string, ff *FuncFile) (string, error) { 327 if ff.Entrypoint == "" && ff.Cmd == "" { 328 return "", errors.New("entrypoint and cmd are missing, you must provide one or the other") 329 } 330 331 fd, err := ioutil.TempFile(dir, "Dockerfile") 332 if err != nil { 333 return "", err 334 } 335 defer fd.Close() 336 337 // multi-stage build: https://medium.com/travis-on-docker/multi-stage-docker-builds-for-creating-tiny-go-images-e0e1867efe5a 338 dfLines := []string{} 339 bi := ff.BuildImage 340 if bi == "" { 341 bi, err = helper.BuildFromImage() 342 if err != nil { 343 return "", err 344 } 345 } 346 if helper.IsMultiStage() { 347 // build stage 348 dfLines = append(dfLines, fmt.Sprintf("FROM %s as build-stage", bi)) 349 } else { 350 dfLines = append(dfLines, fmt.Sprintf("FROM %s", bi)) 351 } 352 dfLines = append(dfLines, "WORKDIR /function") 353 dfLines = append(dfLines, helper.DockerfileBuildCmds()...) 354 if helper.IsMultiStage() { 355 // final stage 356 ri := ff.RunImage 357 if ri == "" { 358 ri, err = helper.RunFromImage() 359 if err != nil { 360 return "", err 361 } 362 } 363 dfLines = append(dfLines, fmt.Sprintf("FROM %s", ri)) 364 dfLines = append(dfLines, "WORKDIR /function") 365 dfLines = append(dfLines, helper.DockerfileCopyCmds()...) 366 } 367 if ff.Entrypoint != "" { 368 dfLines = append(dfLines, fmt.Sprintf("ENTRYPOINT [%s]", stringToSlice(ff.Entrypoint))) 369 } 370 if ff.Cmd != "" { 371 dfLines = append(dfLines, fmt.Sprintf("CMD [%s]", stringToSlice(ff.Cmd))) 372 } 373 err = writeLines(fd, dfLines) 374 if err != nil { 375 return "", err 376 } 377 return fd.Name(), err 378 } 379 380 func writeTmpDockerfileV20180708(helper langs.LangHelper, dir string, ff *FuncFileV20180708) (string, error) { 381 if ff.Entrypoint == "" && ff.Cmd == "" { 382 return "", errors.New("entrypoint and cmd are missing, you must provide one or the other") 383 } 384 385 fd, err := ioutil.TempFile(dir, "Dockerfile") 386 if err != nil { 387 return "", err 388 } 389 defer fd.Close() 390 391 // multi-stage build: https://medium.com/travis-on-docker/multi-stage-docker-builds-for-creating-tiny-go-images-e0e1867efe5a 392 dfLines := []string{} 393 bi := ff.Build_image 394 if bi == "" { 395 bi, err = helper.BuildFromImage() 396 if err != nil { 397 return "", err 398 } 399 } 400 if helper.IsMultiStage() { 401 // build stage 402 dfLines = append(dfLines, fmt.Sprintf("FROM %s as build-stage", bi)) 403 } else { 404 dfLines = append(dfLines, fmt.Sprintf("FROM %s", bi)) 405 } 406 dfLines = append(dfLines, "WORKDIR /function") 407 dfLines = append(dfLines, helper.DockerfileBuildCmds()...) 408 if helper.IsMultiStage() { 409 // final stage 410 ri := ff.Run_image 411 if ri == "" { 412 ri, err = helper.RunFromImage() 413 if err != nil { 414 return "", err 415 } 416 } 417 dfLines = append(dfLines, fmt.Sprintf("FROM %s", ri)) 418 dfLines = append(dfLines, "WORKDIR /function") 419 dfLines = append(dfLines, helper.DockerfileCopyCmds()...) 420 } 421 if ff.Entrypoint != "" { 422 dfLines = append(dfLines, fmt.Sprintf("ENTRYPOINT [%s]", stringToSlice(ff.Entrypoint))) 423 } 424 if ff.Cmd != "" { 425 dfLines = append(dfLines, fmt.Sprintf("CMD [%s]", stringToSlice(ff.Cmd))) 426 } 427 err = writeLines(fd, dfLines) 428 if err != nil { 429 return "", err 430 } 431 return fd.Name(), err 432 } 433 434 func writeLines(w io.Writer, lines []string) error { 435 writer := bufio.NewWriter(w) 436 for _, l := range lines { 437 _, err := writer.WriteString(l + "\n") 438 if err != nil { 439 return err 440 } 441 } 442 writer.Flush() 443 return nil 444 } 445 446 func stringToSlice(in string) string { 447 epvals := strings.Fields(in) 448 var buffer bytes.Buffer 449 for i, s := range epvals { 450 if i > 0 { 451 buffer.WriteString(", ") 452 } 453 buffer.WriteString("\"") 454 buffer.WriteString(s) 455 buffer.WriteString("\"") 456 } 457 return buffer.String() 458 } 459 460 // ExtractConfig parses key-value configuration into a map 461 func ExtractConfig(configs []string) map[string]string { 462 c := make(map[string]string) 463 for _, v := range configs { 464 kv := strings.SplitN(v, "=", 2) 465 if len(kv) == 2 { 466 c[kv[0]] = kv[1] 467 } 468 } 469 return c 470 } 471 472 // DockerPush pushes to docker registry. 473 func DockerPush(ff *FuncFile) error { 474 err := ValidateFullImageName(ff.ImageName()) 475 if err != nil { 476 return err 477 } 478 fmt.Printf("Pushing %v to docker registry...", ff.ImageName()) 479 cmd := exec.Command("docker", "push", ff.ImageName()) 480 cmd.Stderr = os.Stderr 481 cmd.Stdout = os.Stdout 482 if err := cmd.Run(); err != nil { 483 return fmt.Errorf("error running docker push, are you logged into docker?: %v", err) 484 } 485 return nil 486 } 487 488 // DockerPush pushes to docker registry. 489 func DockerPushV20180708(ff *FuncFileV20180708) error { 490 err := ValidateFullImageName(ff.ImageNameV20180708()) 491 if err != nil { 492 return err 493 } 494 fmt.Printf("Pushing %v to docker registry...", ff.ImageNameV20180708()) 495 cmd := exec.Command("docker", "push", ff.ImageNameV20180708()) 496 cmd.Stderr = os.Stderr 497 cmd.Stdout = os.Stdout 498 if err := cmd.Run(); err != nil { 499 return fmt.Errorf("error running docker push, are you logged into docker?: %v", err) 500 } 501 return nil 502 } 503 504 // ValidateFullImageName validates that the full image name (REGISTRY/name:tag) is allowed for push 505 // remember that private registries must be supported here 506 func ValidateFullImageName(n string) error { 507 parts := strings.Split(n, "/") 508 fmt.Println("Parts: ", parts) 509 if len(parts) < 2 { 510 return errors.New("image name must have a dockerhub owner or private registry. Be sure to set FN_REGISTRY env var, pass in --registry or configure your context file") 511 512 } 513 return ValidateTagImageName(n) 514 } 515 516 // ValidateTagImageName validates that the last part of the image name (name:tag) is allowed for create/update 517 func ValidateTagImageName(n string) error { 518 parts := strings.Split(n, "/") 519 lastParts := strings.Split(parts[len(parts)-1], ":") 520 if len(lastParts) != 2 { 521 return errors.New("image name must have a tag") 522 } 523 return nil 524 } 525 526 func appNamePath(img string) (string, string) { 527 sep := strings.Index(img, "/") 528 if sep < 0 { 529 return "", "" 530 } 531 tag := strings.Index(img[sep:], ":") 532 if tag < 0 { 533 tag = len(img[sep:]) 534 } 535 return img[:sep], img[sep : sep+tag] 536 } 537 538 // ExtractAnnotations extract annotations from command flags. 539 func ExtractAnnotations(c *cli.Context) map[string]interface{} { 540 annotations := make(map[string]interface{}) 541 for _, s := range c.StringSlice("annotation") { 542 parts := strings.Split(s, "=") 543 if len(parts) == 2 { 544 var v interface{} 545 err := json.Unmarshal([]byte(parts[1]), &v) 546 if err != nil { 547 fmt.Fprintf(os.Stderr, "Unable to parse annotation value '%v'. Annotations values must be valid JSON strings.\n", parts[1]) 548 } else { 549 annotations[parts[0]] = v 550 } 551 } else { 552 fmt.Fprintf(os.Stderr, "Annotations must be specified in the form key='value', where value is a valid JSON string") 553 } 554 } 555 return annotations 556 } 557 558 func ReadInFuncFile() (map[string]interface{}, error) { 559 wd := GetWd() 560 561 fpath, err := FindFuncfile(wd) 562 if err != nil { 563 return nil, err 564 } 565 566 b, err := ioutil.ReadFile(fpath) 567 if err != nil { 568 return nil, fmt.Errorf("could not open %s for parsing. Error: %v", fpath, err) 569 } 570 var ff map[string]interface{} 571 err = yaml.Unmarshal(b, &ff) 572 if err != nil { 573 return nil, err 574 } 575 576 return ff, nil 577 } 578 579 func GetFuncYamlVersion(oldFF map[string]interface{}) int { 580 if _, ok := oldFF["schema_version"]; ok { 581 return oldFF["schema_version"].(int) 582 } 583 return 1 584 }