300 lines
7.6 KiB
Go
300 lines
7.6 KiB
Go
|
// 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
|
||
|
visited := make(map[importFrom]bool)
|
||
|
|
||
|
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
|
||
|
}
|
||
|
if _, ok := visited[i]; ok {
|
||
|
// already scanned
|
||
|
continue
|
||
|
}
|
||
|
visited[i] = true
|
||
|
|
||
|
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
|
||
|
}
|