aedeploy.go 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  1. // Copyright 2015 Google Inc. All rights reserved.
  2. // Use of this source code is governed by the Apache 2.0
  3. // license that can be found in the LICENSE file.
  4. // Program aedeploy assists with deploying App Engine "flexible environment" Go apps to production.
  5. // A temporary directory is created; the app, its subdirectories, and all its
  6. // dependencies from $GOPATH are copied into the directory; then the app
  7. // is deployed to production with the provided command.
  8. //
  9. // The app must be in "package main".
  10. //
  11. // This command must be issued from within the root directory of the app
  12. // (where the app.yaml file is located).
  13. package main
  14. import (
  15. "flag"
  16. "fmt"
  17. "go/build"
  18. "io"
  19. "io/ioutil"
  20. "os"
  21. "os/exec"
  22. "path/filepath"
  23. "strings"
  24. )
  25. var (
  26. skipFiles = map[string]bool{
  27. ".git": true,
  28. ".gitconfig": true,
  29. ".hg": true,
  30. ".travis.yml": true,
  31. }
  32. gopathCache = map[string]string{}
  33. )
  34. func usage() {
  35. fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0])
  36. fmt.Fprintf(os.Stderr, "\t%s gcloud --verbosity debug preview app deploy --version myversion ./app.yaml\tDeploy app to production\n", os.Args[0])
  37. }
  38. func main() {
  39. flag.Usage = usage
  40. flag.Parse()
  41. if flag.NArg() < 1 {
  42. usage()
  43. os.Exit(1)
  44. }
  45. if err := aedeploy(); err != nil {
  46. fmt.Fprintf(os.Stderr, os.Args[0]+": Error: %v\n", err)
  47. os.Exit(1)
  48. }
  49. }
  50. func aedeploy() error {
  51. tags := []string{"appenginevm"}
  52. app, err := analyze(tags)
  53. if err != nil {
  54. return err
  55. }
  56. tmpDir, err := app.bundle()
  57. if tmpDir != "" {
  58. defer os.RemoveAll(tmpDir)
  59. }
  60. if err != nil {
  61. return err
  62. }
  63. if err := os.Chdir(tmpDir); err != nil {
  64. return fmt.Errorf("unable to chdir to %v: %v", tmpDir, err)
  65. }
  66. return deploy()
  67. }
  68. // deploy calls the provided command to deploy the app from the temporary directory.
  69. func deploy() error {
  70. cmd := exec.Command(flag.Arg(0), flag.Args()[1:]...)
  71. cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
  72. if err := cmd.Run(); err != nil {
  73. return fmt.Errorf("unable to run %q: %v", strings.Join(flag.Args(), " "), err)
  74. }
  75. return nil
  76. }
  77. type app struct {
  78. appFiles []string
  79. imports map[string]string
  80. }
  81. // analyze checks the app for building with the given build tags and returns
  82. // app files, and a map of full directory import names to original import names.
  83. func analyze(tags []string) (*app, error) {
  84. ctxt := buildContext(tags)
  85. appFiles, err := appFiles(ctxt)
  86. if err != nil {
  87. return nil, err
  88. }
  89. gopath := filepath.SplitList(ctxt.GOPATH)
  90. im, err := imports(ctxt, ".", gopath)
  91. return &app{
  92. appFiles: appFiles,
  93. imports: im,
  94. }, err
  95. }
  96. // buildContext returns the context for building the source.
  97. func buildContext(tags []string) *build.Context {
  98. return &build.Context{
  99. GOARCH: "amd64",
  100. GOOS: "linux",
  101. GOROOT: build.Default.GOROOT,
  102. GOPATH: build.Default.GOPATH,
  103. Compiler: build.Default.Compiler,
  104. BuildTags: append(defaultBuildTags, tags...),
  105. }
  106. }
  107. // All build tags except go1.7, since Go 1.6 is the runtime version.
  108. var defaultBuildTags = []string{
  109. "go1.1", "go1.2", "go1.3", "go1.4", "go1.5", "go1.6"}
  110. // bundle bundles the app into a temporary directory.
  111. func (s *app) bundle() (tmpdir string, err error) {
  112. workDir, err := ioutil.TempDir("", "aedeploy")
  113. if err != nil {
  114. return "", fmt.Errorf("unable to create tmpdir: %v", err)
  115. }
  116. for srcDir, importName := range s.imports {
  117. dstDir := "_gopath/src/" + importName
  118. if err := copyTree(workDir, dstDir, srcDir); err != nil {
  119. return workDir, fmt.Errorf("unable to copy directory %v to %v: %v", srcDir, dstDir, err)
  120. }
  121. }
  122. if err := copyTree(workDir, ".", "."); err != nil {
  123. return workDir, fmt.Errorf("unable to copy root directory to /app: %v", err)
  124. }
  125. return workDir, nil
  126. }
  127. // imports returns a map of all import directories (recursively) used by the app.
  128. // The return value maps full directory names to original import names.
  129. func imports(ctxt *build.Context, srcDir string, gopath []string) (map[string]string, error) {
  130. pkg, err := ctxt.ImportDir(srcDir, 0)
  131. if err != nil {
  132. return nil, err
  133. }
  134. // Resolve all non-standard-library imports
  135. result := make(map[string]string)
  136. for _, v := range pkg.Imports {
  137. if !strings.Contains(v, ".") {
  138. continue
  139. }
  140. src, err := findInGopath(v, gopath)
  141. if err != nil {
  142. return nil, fmt.Errorf("unable to find import %v in gopath %v: %v", v, gopath, err)
  143. }
  144. if _, ok := result[src]; ok { // Already processed
  145. continue
  146. }
  147. result[src] = v
  148. im, err := imports(ctxt, src, gopath)
  149. if err != nil {
  150. return nil, fmt.Errorf("unable to parse package %v: %v", src, err)
  151. }
  152. for k, v := range im {
  153. result[k] = v
  154. }
  155. }
  156. return result, nil
  157. }
  158. // findInGopath searches the gopath for the named import directory.
  159. func findInGopath(dir string, gopath []string) (string, error) {
  160. if v, ok := gopathCache[dir]; ok {
  161. return v, nil
  162. }
  163. for _, v := range gopath {
  164. dst := filepath.Join(v, "src", dir)
  165. if _, err := os.Stat(dst); err == nil {
  166. gopathCache[dir] = dst
  167. return dst, nil
  168. }
  169. }
  170. return "", fmt.Errorf("unable to find package %v in gopath %v", dir, gopath)
  171. }
  172. // copyTree copies srcDir to dstDir relative to dstRoot, ignoring skipFiles.
  173. func copyTree(dstRoot, dstDir, srcDir string) error {
  174. d := filepath.Join(dstRoot, dstDir)
  175. if err := os.MkdirAll(d, 0755); err != nil {
  176. return fmt.Errorf("unable to create directory %q: %v", d, err)
  177. }
  178. entries, err := ioutil.ReadDir(srcDir)
  179. if err != nil {
  180. return fmt.Errorf("unable to read dir %q: %v", srcDir, err)
  181. }
  182. for _, entry := range entries {
  183. n := entry.Name()
  184. if skipFiles[n] {
  185. continue
  186. }
  187. s := filepath.Join(srcDir, n)
  188. if entry.Mode()&os.ModeSymlink == os.ModeSymlink {
  189. if entry, err = os.Stat(s); err != nil {
  190. return fmt.Errorf("unable to stat %v: %v", s, err)
  191. }
  192. }
  193. d := filepath.Join(dstDir, n)
  194. if entry.IsDir() {
  195. if err := copyTree(dstRoot, d, s); err != nil {
  196. return fmt.Errorf("unable to copy dir %q to %q: %v", s, d, err)
  197. }
  198. continue
  199. }
  200. if err := copyFile(dstRoot, d, s); err != nil {
  201. return fmt.Errorf("unable to copy dir %q to %q: %v", s, d, err)
  202. }
  203. }
  204. return nil
  205. }
  206. // copyFile copies src to dst relative to dstRoot.
  207. func copyFile(dstRoot, dst, src string) error {
  208. s, err := os.Open(src)
  209. if err != nil {
  210. return fmt.Errorf("unable to open %q: %v", src, err)
  211. }
  212. defer s.Close()
  213. dst = filepath.Join(dstRoot, dst)
  214. d, err := os.Create(dst)
  215. if err != nil {
  216. return fmt.Errorf("unable to create %q: %v", dst, err)
  217. }
  218. _, err = io.Copy(d, s)
  219. if err != nil {
  220. d.Close() // ignore error, copy already failed.
  221. return fmt.Errorf("unable to copy %q to %q: %v", src, dst, err)
  222. }
  223. if err := d.Close(); err != nil {
  224. return fmt.Errorf("unable to close %q: %v", dst, err)
  225. }
  226. return nil
  227. }
  228. // appFiles returns a list of all Go source files in the app.
  229. func appFiles(ctxt *build.Context) ([]string, error) {
  230. pkg, err := ctxt.ImportDir(".", 0)
  231. if err != nil {
  232. return nil, err
  233. }
  234. if !pkg.IsCommand() {
  235. 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)
  236. }
  237. var appFiles []string
  238. for _, f := range pkg.GoFiles {
  239. n := filepath.Join(".", f)
  240. appFiles = append(appFiles, n)
  241. }
  242. return appFiles, nil
  243. }