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  }