aedeploy.go 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  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 Go Managed VM 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(build.Default.BuildTags, tags...),
  105. }
  106. }
  107. // bundle bundles the app into a temporary directory.
  108. func (s *app) bundle() (tmpdir string, err error) {
  109. workDir, err := ioutil.TempDir("", "aedeploy")
  110. if err != nil {
  111. return "", fmt.Errorf("unable to create tmpdir: %v", err)
  112. }
  113. for srcDir, importName := range s.imports {
  114. dstDir := "_gopath/src/" + importName
  115. if err := copyTree(workDir, dstDir, srcDir); err != nil {
  116. return workDir, fmt.Errorf("unable to copy directory %v to %v: %v", srcDir, dstDir, err)
  117. }
  118. }
  119. if err := copyTree(workDir, ".", "."); err != nil {
  120. return workDir, fmt.Errorf("unable to copy root directory to /app: %v", err)
  121. }
  122. return workDir, nil
  123. }
  124. // imports returns a map of all import directories (recursively) used by the app.
  125. // The return value maps full directory names to original import names.
  126. func imports(ctxt *build.Context, srcDir string, gopath []string) (map[string]string, error) {
  127. pkg, err := ctxt.ImportDir(srcDir, 0)
  128. if err != nil {
  129. return nil, err
  130. }
  131. // Resolve all non-standard-library imports
  132. result := make(map[string]string)
  133. for _, v := range pkg.Imports {
  134. if !strings.Contains(v, ".") {
  135. continue
  136. }
  137. src, err := findInGopath(v, gopath)
  138. if err != nil {
  139. return nil, fmt.Errorf("unable to find import %v in gopath %v: %v", v, gopath, err)
  140. }
  141. if _, ok := result[src]; ok { // Already processed
  142. continue
  143. }
  144. result[src] = v
  145. im, err := imports(ctxt, src, gopath)
  146. if err != nil {
  147. return nil, fmt.Errorf("unable to parse package %v: %v", src, err)
  148. }
  149. for k, v := range im {
  150. result[k] = v
  151. }
  152. }
  153. return result, nil
  154. }
  155. // findInGopath searches the gopath for the named import directory.
  156. func findInGopath(dir string, gopath []string) (string, error) {
  157. if v, ok := gopathCache[dir]; ok {
  158. return v, nil
  159. }
  160. for _, v := range gopath {
  161. dst := filepath.Join(v, "src", dir)
  162. if _, err := os.Stat(dst); err == nil {
  163. gopathCache[dir] = dst
  164. return dst, nil
  165. }
  166. }
  167. return "", fmt.Errorf("unable to find package %v in gopath %v", dir, gopath)
  168. }
  169. // copyTree copies srcDir to dstDir relative to dstRoot, ignoring skipFiles.
  170. func copyTree(dstRoot, dstDir, srcDir string) error {
  171. d := filepath.Join(dstRoot, dstDir)
  172. if err := os.MkdirAll(d, 0755); err != nil {
  173. return fmt.Errorf("unable to create directory %q: %v", d, err)
  174. }
  175. entries, err := ioutil.ReadDir(srcDir)
  176. if err != nil {
  177. return fmt.Errorf("unable to read dir %q: %v", srcDir, err)
  178. }
  179. for _, entry := range entries {
  180. n := entry.Name()
  181. if skipFiles[n] {
  182. continue
  183. }
  184. s := filepath.Join(srcDir, n)
  185. if entry.Mode()&os.ModeSymlink == os.ModeSymlink {
  186. if entry, err = os.Stat(s); err != nil {
  187. return fmt.Errorf("unable to stat %v: %v", s, err)
  188. }
  189. }
  190. d := filepath.Join(dstDir, n)
  191. if entry.IsDir() {
  192. if err := copyTree(dstRoot, d, s); err != nil {
  193. return fmt.Errorf("unable to copy dir %q to %q: %v", s, d, err)
  194. }
  195. continue
  196. }
  197. if err := copyFile(dstRoot, d, s); err != nil {
  198. return fmt.Errorf("unable to copy dir %q to %q: %v", s, d, err)
  199. }
  200. }
  201. return nil
  202. }
  203. // copyFile copies src to dst relative to dstRoot.
  204. func copyFile(dstRoot, dst, src string) error {
  205. s, err := os.Open(src)
  206. if err != nil {
  207. return fmt.Errorf("unable to open %q: %v", src, err)
  208. }
  209. defer s.Close()
  210. dst = filepath.Join(dstRoot, dst)
  211. d, err := os.Create(dst)
  212. if err != nil {
  213. return fmt.Errorf("unable to create %q: %v", dst, err)
  214. }
  215. _, err = io.Copy(d, s)
  216. if err != nil {
  217. d.Close() // ignore error, copy already failed.
  218. return fmt.Errorf("unable to copy %q to %q: %v", src, dst, err)
  219. }
  220. if err := d.Close(); err != nil {
  221. return fmt.Errorf("unable to close %q: %v", dst, err)
  222. }
  223. return nil
  224. }
  225. // appFiles returns a list of all Go source files in the app.
  226. func appFiles(ctxt *build.Context) ([]string, error) {
  227. pkg, err := ctxt.ImportDir(".", 0)
  228. if err != nil {
  229. return nil, err
  230. }
  231. if !pkg.IsCommand() {
  232. return nil, fmt.Errorf(`the root of your app needs to be package "main" (currently %q). Please see https://cloud.google.com/appengine/docs/go/managed-vms for more details on structuring your app.`, pkg.Name)
  233. }
  234. var appFiles []string
  235. for _, f := range pkg.GoFiles {
  236. n := filepath.Join(".", f)
  237. appFiles = append(appFiles, n)
  238. }
  239. return appFiles, nil
  240. }