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  }