github.com/aneshas/cli@v0.0.0-20180104210444-aec958fa47db/common.go (about) 1 package main 2 3 import ( 4 "bufio" 5 "bytes" 6 "errors" 7 "fmt" 8 "io" 9 "io/ioutil" 10 "log" 11 "os" 12 "os/exec" 13 "os/signal" 14 "path/filepath" 15 "strings" 16 "time" 17 "unicode" 18 19 "github.com/coreos/go-semver/semver" 20 "github.com/fatih/color" 21 "github.com/fnproject/cli/langs" 22 "github.com/urfave/cli" 23 ) 24 25 const ( 26 functionsDockerImage = "fnproject/fnserver" 27 funcfileDockerRuntime = "docker" 28 minRequiredDockerVersion = "17.5.0" 29 envFnRegistry = "FN_REGISTRY" 30 ) 31 32 type HasRegistry interface { 33 Registry() string 34 } 35 36 func setRegistryEnv(hr HasRegistry) { 37 if hr.Registry() != "" { 38 err := os.Setenv(envFnRegistry, hr.Registry()) 39 if err != nil { 40 log.Fatalf("Couldn't set %s env var: %v\n", envFnRegistry, err) 41 } 42 } 43 } 44 45 func getWd() string { 46 wd, err := os.Getwd() 47 if err != nil { 48 log.Fatalln("Couldn't get working directory:", err) 49 } 50 return wd 51 } 52 53 func buildfunc(c *cli.Context, fpath string, funcfile *funcfile, noCache bool) (*funcfile, error) { 54 var err error 55 if funcfile.Version == "" { 56 funcfile, err = bumpIt(fpath, Patch) 57 if err != nil { 58 return nil, err 59 } 60 } 61 62 if err := localBuild(fpath, funcfile.Build); err != nil { 63 return nil, err 64 } 65 66 if err := dockerBuild(c, fpath, funcfile, noCache); err != nil { 67 return nil, err 68 } 69 70 return funcfile, nil 71 } 72 73 func localBuild(path string, steps []string) error { 74 for _, cmd := range steps { 75 exe := exec.Command("/bin/sh", "-c", cmd) 76 exe.Dir = filepath.Dir(path) 77 if err := exe.Run(); err != nil { 78 return fmt.Errorf("error running command %v (%v)", cmd, err) 79 } 80 } 81 82 return nil 83 } 84 85 func dockerBuild(c *cli.Context, fpath string, ff *funcfile, noCache bool) error { 86 err := dockerVersionCheck() 87 if err != nil { 88 return err 89 } 90 91 dir := filepath.Dir(fpath) 92 93 var helper langs.LangHelper 94 dockerfile := filepath.Join(dir, "Dockerfile") 95 if !exists(dockerfile) { 96 if ff.Runtime == funcfileDockerRuntime { 97 return fmt.Errorf("Dockerfile does not exist for 'docker' runtime") 98 } 99 helper = langs.GetLangHelper(ff.Runtime) 100 if helper == nil { 101 return fmt.Errorf("Cannot build, no language helper found for %v", ff.Runtime) 102 } 103 dockerfile, err = writeTmpDockerfile(helper, dir, ff) 104 if err != nil { 105 return err 106 } 107 defer os.Remove(dockerfile) 108 if helper.HasPreBuild() { 109 err := helper.PreBuild() 110 if err != nil { 111 return err 112 } 113 } 114 } 115 err = runBuild(c, dir, ff.ImageName(), dockerfile, noCache) 116 if err != nil { 117 return err 118 } 119 120 if helper != nil { 121 err := helper.AfterBuild() 122 if err != nil { 123 return err 124 } 125 } 126 return nil 127 } 128 129 func runBuild(c *cli.Context, dir, imageName, dockerfile string, noCache bool) error { 130 cancel := make(chan os.Signal, 3) 131 signal.Notify(cancel, os.Interrupt) // and others perhaps 132 defer signal.Stop(cancel) 133 134 result := make(chan error, 1) 135 136 buildOut := ioutil.Discard 137 buildErr := ioutil.Discard 138 139 quit := make(chan struct{}) 140 fmt.Printf("Building image %v ", imageName) 141 if c.GlobalBool("verbose") { 142 fmt.Println() 143 buildOut = os.Stdout 144 buildErr = os.Stderr 145 } else { 146 // print dots. quit channel explanation: https://stackoverflow.com/a/16466581/105562 147 ticker := time.NewTicker(1 * time.Second) 148 go func() { 149 for { 150 select { 151 case <-ticker.C: 152 fmt.Print(".") 153 case <-quit: 154 ticker.Stop() 155 return 156 } 157 } 158 }() 159 } 160 161 go func(done chan<- error) { 162 args := []string{ 163 "build", 164 "-t", imageName, 165 "-f", dockerfile, 166 } 167 if noCache { 168 args = append(args, "--no-cache") 169 } 170 args = append(args, 171 "--build-arg", "HTTP_PROXY", 172 "--build-arg", "HTTPS_PROXY", 173 ".") 174 cmd := exec.Command("docker", args...) 175 cmd.Dir = dir 176 cmd.Stderr = buildErr // Doesn't look like there's any output to stderr on docker build, whether it's successful or not. 177 cmd.Stdout = buildOut 178 done <- cmd.Run() 179 }(result) 180 181 select { 182 case err := <-result: 183 close(quit) 184 fmt.Println() 185 if err != nil { 186 fmt.Printf("%v Run with `--verbose` flag to see what went wrong. eg: `fn --verbose CMD`\n", color.RedString("Error during build.")) 187 return fmt.Errorf("error running docker build: %v", err) 188 } 189 case signal := <-cancel: 190 close(quit) 191 fmt.Println() 192 return fmt.Errorf("build cancelled on signal %v", signal) 193 } 194 return nil 195 } 196 197 func dockerVersionCheck() error { 198 out, err := exec.Command("docker", "version", "--format", "{{.Server.Version}}").Output() 199 if err != nil { 200 return fmt.Errorf("could not check Docker version: %v", err) 201 } 202 // dev / test builds append '-ce', trim this 203 trimmed := strings.TrimRightFunc(string(out), func(r rune) bool { return r != '.' && !unicode.IsDigit(r) }) 204 205 v, err := semver.NewVersion(trimmed) 206 if err != nil { 207 return fmt.Errorf("could not check Docker version: %v", err) 208 } 209 vMin, err := semver.NewVersion(minRequiredDockerVersion) 210 if err != nil { 211 return fmt.Errorf("our bad, sorry... please make an issue.", err) 212 } 213 if v.LessThan(*vMin) { 214 return fmt.Errorf("please upgrade your version of Docker to %s or greater", minRequiredDockerVersion) 215 } 216 return nil 217 } 218 219 func exists(name string) bool { 220 if _, err := os.Stat(name); err != nil { 221 if os.IsNotExist(err) { 222 return false 223 } 224 } 225 return true 226 } 227 228 func writeTmpDockerfile(helper langs.LangHelper, dir string, ff *funcfile) (string, error) { 229 if ff.Entrypoint == "" && ff.Cmd == "" { 230 return "", errors.New("entrypoint and cmd are missing, you must provide one or the other") 231 } 232 233 fd, err := ioutil.TempFile(dir, "Dockerfile") 234 if err != nil { 235 return "", err 236 } 237 defer fd.Close() 238 239 // multi-stage build: https://medium.com/travis-on-docker/multi-stage-docker-builds-for-creating-tiny-go-images-e0e1867efe5a 240 dfLines := []string{} 241 bi := ff.BuildImage 242 if bi == "" { 243 bi, err = helper.BuildFromImage() 244 if err != nil { 245 return "", err 246 } 247 } 248 if helper.IsMultiStage() { 249 // build stage 250 dfLines = append(dfLines, fmt.Sprintf("FROM %s as build-stage", bi)) 251 } else { 252 dfLines = append(dfLines, fmt.Sprintf("FROM %s", bi)) 253 } 254 dfLines = append(dfLines, "WORKDIR /function") 255 dfLines = append(dfLines, helper.DockerfileBuildCmds()...) 256 if helper.IsMultiStage() { 257 // final stage 258 ri := ff.RunImage 259 if ri == "" { 260 ri, err = helper.RunFromImage() 261 if err != nil { 262 return "", err 263 } 264 } 265 dfLines = append(dfLines, fmt.Sprintf("FROM %s", ri)) 266 dfLines = append(dfLines, "WORKDIR /function") 267 dfLines = append(dfLines, helper.DockerfileCopyCmds()...) 268 } 269 if ff.Entrypoint != "" { 270 dfLines = append(dfLines, fmt.Sprintf("ENTRYPOINT [%s]", stringToSlice(ff.Entrypoint))) 271 } 272 if ff.Cmd != "" { 273 dfLines = append(dfLines, fmt.Sprintf("CMD [%s]", stringToSlice(ff.Cmd))) 274 } 275 err = writeLines(fd, dfLines) 276 if err != nil { 277 return "", err 278 } 279 return fd.Name(), err 280 } 281 282 func writeLines(w io.Writer, lines []string) error { 283 writer := bufio.NewWriter(w) 284 for _, l := range lines { 285 _, err := writer.WriteString(l + "\n") 286 if err != nil { 287 return err 288 } 289 } 290 writer.Flush() 291 return nil 292 } 293 294 func stringToSlice(in string) string { 295 epvals := strings.Fields(in) 296 var buffer bytes.Buffer 297 for i, s := range epvals { 298 if i > 0 { 299 buffer.WriteString(", ") 300 } 301 buffer.WriteString("\"") 302 buffer.WriteString(s) 303 buffer.WriteString("\"") 304 } 305 return buffer.String() 306 } 307 308 func extractEnvConfig(configs []string) map[string]string { 309 c := make(map[string]string) 310 for _, v := range configs { 311 kv := strings.SplitN(v, "=", 2) 312 if len(kv) == 2 { 313 c[kv[0]] = os.ExpandEnv(kv[1]) 314 } 315 } 316 return c 317 } 318 319 func dockerPush(ff *funcfile) error { 320 err := validateImageName(ff.ImageName()) 321 if err != nil { 322 return err 323 } 324 fmt.Printf("Pushing %v to docker registry...", ff.ImageName()) 325 cmd := exec.Command("docker", "push", ff.ImageName()) 326 cmd.Stderr = os.Stderr 327 cmd.Stdout = os.Stdout 328 if err := cmd.Run(); err != nil { 329 return fmt.Errorf("error running docker push: %v", err) 330 } 331 return nil 332 } 333 334 // validateImageName validates that the full image name (FN_REGISTRY/name:tag) is allowed for push 335 // remember that private registries must be supported here 336 func validateImageName(n string) error { 337 parts := strings.Split(n, "/") 338 if len(parts) < 2 { 339 return errors.New("image name must have a dockerhub owner or private registry. Be sure to set FN_REGISTRY env var or pass in --registry") 340 } 341 lastParts := strings.Split(parts[len(parts)-1], ":") 342 if len(lastParts) != 2 { 343 return errors.New("image name must have a tag") 344 } 345 return nil 346 } 347 348 func appNamePath(img string) (string, string) { 349 sep := strings.Index(img, "/") 350 if sep < 0 { 351 return "", "" 352 } 353 tag := strings.Index(img[sep:], ":") 354 if tag < 0 { 355 tag = len(img[sep:]) 356 } 357 return img[:sep], img[sep : sep+tag] 358 }