| // 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" |
| "log" |
| "os" |
| "os/exec" |
| "path/filepath" |
| "strings" |
| ) |
| |
| var ( |
| skipFiles = map[string]bool{ |
| ".git": true, |
| ".gitconfig": true, |
| ".hg": true, |
| ".travis.yml": true, |
| } |
| ) |
| |
| func usage() { |
| fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0]) |
| fmt.Fprintf(os.Stderr, "\t%s gcloud --verbosity debug app deploy --version myversion ./app.yaml\tDeploy app to production\n", os.Args[0]) |
| } |
| |
| var verbose bool |
| |
| // vlogf logs to stderr if the "-v" flag is provided. |
| func vlogf(f string, v ...interface{}) { |
| if !verbose { |
| return |
| } |
| log.Printf("[aedeploy] "+f, v...) |
| } |
| |
| func main() { |
| flag.BoolVar(&verbose, "v", false, "Verbose logging.") |
| 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 { |
| vlogf("Running command %v", flag.Args()) |
| 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) |
| vlogf("Using build context %#v", ctxt) |
| appFiles, err := appFiles(ctxt) |
| if err != nil { |
| return nil, err |
| } |
| im, err := imports(ctxt, ".") |
| 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 used by the app. |
| // The return value maps full directory names to original import names. |
| func imports(ctxt *build.Context, srcDir string) (map[string]string, error) { |
| result := make(map[string]string) |
| |
| type importFrom struct { |
| path, fromDir string |
| } |
| var imports []importFrom |
| |
| pkg, err := ctxt.ImportDir(srcDir, 0) |
| if err != nil { |
| return nil, err |
| } |
| for _, v := range pkg.Imports { |
| imports = append(imports, importFrom{ |
| path: v, |
| fromDir: srcDir, |
| }) |
| } |
| |
| // Resolve all non-standard-library imports |
| for len(imports) != 0 { |
| i := imports[0] |
| imports = imports[1:] // shift |
| |
| if i.path == "C" { |
| // ignore cgo |
| continue |
| } |
| abs, err := filepath.Abs(i.fromDir) |
| if err != nil { |
| return nil, fmt.Errorf("unable to get absolute directory of %q: %v", i.fromDir, err) |
| } |
| pkg, err := ctxt.Import(i.path, abs, 0) |
| if err != nil { |
| return nil, fmt.Errorf("unable to find import %s, imported from %q: %v", i.path, i.fromDir, err) |
| } |
| |
| // TODO(cbro): handle packages that are vendored by multiple imports correctly. |
| |
| if pkg.Goroot { |
| // ignore standard library imports |
| continue |
| } |
| |
| vlogf("Located %q (imported from %q) -> %q", i.path, i.fromDir, pkg.Dir) |
| result[pkg.Dir] = i.path |
| |
| for _, v := range pkg.Imports { |
| imports = append(imports, importFrom{ |
| path: v, |
| fromDir: pkg.Dir, |
| }) |
| } |
| } |
| |
| return result, nil |
| } |
| |
| // copyTree copies srcDir to dstDir relative to dstRoot, ignoring skipFiles. |
| func copyTree(dstRoot, dstDir, srcDir string) error { |
| vlogf("Copying %q to %q", srcDir, dstDir) |
| 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) |
| } |
| vlogf("Found application files %v", appFiles) |
| return appFiles, nil |
| } |