// Copyright 2015 Google Inc. All rights reserved. // Use of this source code is governed by the Apache 2.0 // license that can be found in the LICENSE file. // Program aedeploy assists with deploying App Engine "flexible environment" Go apps to production. // A temporary directory is created; the app, its subdirectories, and all its // dependencies from $GOPATH are copied into the directory; then the app // is deployed to production with the provided command. // // The app must be in "package main". // // This command must be issued from within the root directory of the app // (where the app.yaml file is located). package main import ( "flag" "fmt" "go/build" "io" "io/ioutil" "os" "os/exec" "path/filepath" "strings" ) var ( skipFiles = map[string]bool{ ".git": true, ".gitconfig": true, ".hg": true, ".travis.yml": true, } gopathCache = map[string]string{} ) func usage() { fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0]) fmt.Fprintf(os.Stderr, "\t%s gcloud --verbosity debug preview app deploy --version myversion ./app.yaml\tDeploy app to production\n", os.Args[0]) } func main() { flag.Usage = usage flag.Parse() if flag.NArg() < 1 { usage() os.Exit(1) } if err := aedeploy(); err != nil { fmt.Fprintf(os.Stderr, os.Args[0]+": Error: %v\n", err) os.Exit(1) } } func aedeploy() error { tags := []string{"appenginevm"} app, err := analyze(tags) if err != nil { return err } tmpDir, err := app.bundle() if tmpDir != "" { defer os.RemoveAll(tmpDir) } if err != nil { return err } if err := os.Chdir(tmpDir); err != nil { return fmt.Errorf("unable to chdir to %v: %v", tmpDir, err) } return deploy() } // deploy calls the provided command to deploy the app from the temporary directory. func deploy() error { cmd := exec.Command(flag.Arg(0), flag.Args()[1:]...) cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr if err := cmd.Run(); err != nil { return fmt.Errorf("unable to run %q: %v", strings.Join(flag.Args(), " "), err) } return nil } type app struct { appFiles []string imports map[string]string } // analyze checks the app for building with the given build tags and returns // app files, and a map of full directory import names to original import names. func analyze(tags []string) (*app, error) { ctxt := buildContext(tags) appFiles, err := appFiles(ctxt) if err != nil { return nil, err } gopath := filepath.SplitList(ctxt.GOPATH) im, err := imports(ctxt, ".", gopath) return &app{ appFiles: appFiles, imports: im, }, err } // buildContext returns the context for building the source. func buildContext(tags []string) *build.Context { return &build.Context{ GOARCH: "amd64", GOOS: "linux", GOROOT: build.Default.GOROOT, GOPATH: build.Default.GOPATH, Compiler: build.Default.Compiler, BuildTags: append(defaultBuildTags, tags...), } } // All build tags except go1.7, since Go 1.6 is the runtime version. var defaultBuildTags = []string{ "go1.1", "go1.2", "go1.3", "go1.4", "go1.5", "go1.6"} // bundle bundles the app into a temporary directory. func (s *app) bundle() (tmpdir string, err error) { workDir, err := ioutil.TempDir("", "aedeploy") if err != nil { return "", fmt.Errorf("unable to create tmpdir: %v", err) } for srcDir, importName := range s.imports { dstDir := "_gopath/src/" + importName if err := copyTree(workDir, dstDir, srcDir); err != nil { return workDir, fmt.Errorf("unable to copy directory %v to %v: %v", srcDir, dstDir, err) } } if err := copyTree(workDir, ".", "."); err != nil { return workDir, fmt.Errorf("unable to copy root directory to /app: %v", err) } return workDir, nil } // imports returns a map of all import directories (recursively) used by the app. // The return value maps full directory names to original import names. func imports(ctxt *build.Context, srcDir string, gopath []string) (map[string]string, error) { pkg, err := ctxt.ImportDir(srcDir, 0) if err != nil { return nil, err } // Resolve all non-standard-library imports result := make(map[string]string) for _, v := range pkg.Imports { if !strings.Contains(v, ".") { continue } src, err := findInGopath(v, gopath) if err != nil { return nil, fmt.Errorf("unable to find import %v in gopath %v: %v", v, gopath, err) } if _, ok := result[src]; ok { // Already processed continue } result[src] = v im, err := imports(ctxt, src, gopath) if err != nil { return nil, fmt.Errorf("unable to parse package %v: %v", src, err) } for k, v := range im { result[k] = v } } return result, nil } // findInGopath searches the gopath for the named import directory. func findInGopath(dir string, gopath []string) (string, error) { if v, ok := gopathCache[dir]; ok { return v, nil } for _, v := range gopath { dst := filepath.Join(v, "src", dir) if _, err := os.Stat(dst); err == nil { gopathCache[dir] = dst return dst, nil } } return "", fmt.Errorf("unable to find package %v in gopath %v", dir, gopath) } // copyTree copies srcDir to dstDir relative to dstRoot, ignoring skipFiles. func copyTree(dstRoot, dstDir, srcDir string) error { d := filepath.Join(dstRoot, dstDir) if err := os.MkdirAll(d, 0755); err != nil { return fmt.Errorf("unable to create directory %q: %v", d, err) } entries, err := ioutil.ReadDir(srcDir) if err != nil { return fmt.Errorf("unable to read dir %q: %v", srcDir, err) } for _, entry := range entries { n := entry.Name() if skipFiles[n] { continue } s := filepath.Join(srcDir, n) if entry.Mode()&os.ModeSymlink == os.ModeSymlink { if entry, err = os.Stat(s); err != nil { return fmt.Errorf("unable to stat %v: %v", s, err) } } d := filepath.Join(dstDir, n) if entry.IsDir() { if err := copyTree(dstRoot, d, s); err != nil { return fmt.Errorf("unable to copy dir %q to %q: %v", s, d, err) } continue } if err := copyFile(dstRoot, d, s); err != nil { return fmt.Errorf("unable to copy dir %q to %q: %v", s, d, err) } } return nil } // copyFile copies src to dst relative to dstRoot. func copyFile(dstRoot, dst, src string) error { s, err := os.Open(src) if err != nil { return fmt.Errorf("unable to open %q: %v", src, err) } defer s.Close() dst = filepath.Join(dstRoot, dst) d, err := os.Create(dst) if err != nil { return fmt.Errorf("unable to create %q: %v", dst, err) } _, err = io.Copy(d, s) if err != nil { d.Close() // ignore error, copy already failed. return fmt.Errorf("unable to copy %q to %q: %v", src, dst, err) } if err := d.Close(); err != nil { return fmt.Errorf("unable to close %q: %v", dst, err) } return nil } // appFiles returns a list of all Go source files in the app. func appFiles(ctxt *build.Context) ([]string, error) { pkg, err := ctxt.ImportDir(".", 0) if err != nil { return nil, err } if !pkg.IsCommand() { return nil, fmt.Errorf(`the root of your app needs to be package "main" (currently %q). Please see https://cloud.google.com/appengine/docs/flexible/go/ for more details on structuring your app.`, pkg.Name) } var appFiles []string for _, f := range pkg.GoFiles { n := filepath.Join(".", f) appFiles = append(appFiles, n) } return appFiles, nil }