github.com/tonto/cli@v0.0.0-20180104210444-aec958fa47db/lambda.go (about)

     1  package main
     2  
     3  import (
     4  	"archive/zip"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     8  	"io/ioutil"
     9  	"net/http"
    10  	"os"
    11  	"path/filepath"
    12  	"strings"
    13  
    14  	"github.com/aws/aws-sdk-go/aws"
    15  	"github.com/aws/aws-sdk-go/aws/credentials"
    16  	"github.com/aws/aws-sdk-go/aws/session"
    17  	aws_lambda "github.com/aws/aws-sdk-go/service/lambda"
    18  	"github.com/moby/moby/pkg/jsonmessage"
    19  	"github.com/urfave/cli"
    20  	yaml "gopkg.in/yaml.v2"
    21  )
    22  
    23  var runtimes = map[string]string{
    24  	"nodejs4.3": "lambda-nodejs4.3",
    25  }
    26  
    27  func lambda() cli.Command {
    28  	var flags []cli.Flag
    29  
    30  	flags = append(flags, getFlags()...)
    31  
    32  	return cli.Command{
    33  		Name:  "lambda",
    34  		Usage: "create and publish lambda functions",
    35  		Subcommands: []cli.Command{
    36  			{
    37  				Name:      "aws-import",
    38  				Usage:     `converts an existing Lambda function to an image, where the function code is downloaded to a directory in the current working directory that has the same name as the Lambda function.`,
    39  				ArgsUsage: "<arn> <region> <image/name>",
    40  				Action:    awsImport,
    41  				Flags:     flags,
    42  			},
    43  		},
    44  	}
    45  }
    46  
    47  func getFlags() []cli.Flag {
    48  	return []cli.Flag{
    49  		cli.StringFlag{
    50  			Name:  "payload",
    51  			Usage: "Payload to pass to the Lambda function. This is usually a JSON object.",
    52  			Value: "{}",
    53  		},
    54  		cli.StringFlag{
    55  			Name:  "version",
    56  			Usage: "Version of the function to import.",
    57  			Value: "$LATEST",
    58  		},
    59  		cli.BoolFlag{
    60  			Name:  "download-only",
    61  			Usage: "Only download the function into a directory. Will not create a Docker image.",
    62  		},
    63  		cli.StringSliceFlag{
    64  			Name:  "config",
    65  			Usage: "function configuration",
    66  		},
    67  	}
    68  }
    69  
    70  func transcribeEnvConfig(configs []string) map[string]string {
    71  	c := make(map[string]string)
    72  	for _, v := range configs {
    73  		kv := strings.SplitN(v, "=", 2)
    74  		if len(kv) == 1 {
    75  			// TODO: Make sure it is compatible cross platform
    76  			c[kv[0]] = fmt.Sprintf("$%s", kv[0])
    77  		} else {
    78  			c[kv[0]] = kv[1]
    79  		}
    80  	}
    81  	return c
    82  }
    83  
    84  func awsImport(c *cli.Context) error {
    85  	args := c.Args()
    86  
    87  	version := c.String("version")
    88  	downloadOnly := c.Bool("download-only")
    89  	profile := c.String("profile")
    90  	arn := args[0]
    91  	region := args[1]
    92  	image := args[2]
    93  
    94  	function, err := getFunction(profile, region, version, arn)
    95  	if err != nil {
    96  		return err
    97  	}
    98  	functionName := *function.Configuration.FunctionName
    99  
   100  	err = os.Mkdir(fmt.Sprintf("./%s", functionName), os.ModePerm)
   101  	if err != nil {
   102  		return err
   103  	}
   104  
   105  	tmpFileName, err := downloadToFile(*function.Code.Location)
   106  	if err != nil {
   107  		return err
   108  	}
   109  	defer os.Remove(tmpFileName)
   110  
   111  	if downloadOnly {
   112  		// Since we are a command line program that will quit soon, it is OK to
   113  		// let the OS clean `files` up.
   114  		return err
   115  	}
   116  
   117  	opts := createImageOptions{
   118  		Name:          functionName,
   119  		Base:          runtimes[(*function.Configuration.Runtime)],
   120  		Package:       "",
   121  		Handler:       *function.Configuration.Handler,
   122  		OutputStream:  newdockerJSONWriter(os.Stdout),
   123  		RawJSONStream: true,
   124  		Config:        transcribeEnvConfig(c.StringSlice("config")),
   125  	}
   126  
   127  	runtime := *function.Configuration.Runtime
   128  	rh, ok := runtimeImportHandlers[runtime]
   129  	if !ok {
   130  		return fmt.Errorf("unsupported runtime %v", runtime)
   131  	}
   132  
   133  	_, err = rh(functionName, tmpFileName, &opts)
   134  	if err != nil {
   135  		return nil
   136  	}
   137  
   138  	if image != "" {
   139  		opts.Name = image
   140  	}
   141  
   142  	fmt.Print("Creating func.yaml ... ")
   143  	if err := createFunctionYaml(opts, functionName); err != nil {
   144  		return err
   145  	}
   146  	fmt.Println("OK")
   147  
   148  	return nil
   149  }
   150  
   151  var (
   152  	runtimeImportHandlers = map[string]func(functionName, tmpFileName string, opts *createImageOptions) ([]fileLike, error){
   153  		"nodejs4.3": basicImportHandler,
   154  		"python2.7": basicImportHandler,
   155  		"java8": func(functionName, tmpFileName string, opts *createImageOptions) ([]fileLike, error) {
   156  			fmt.Println("Found Java Lambda function. Going to assume code is a single JAR file.")
   157  			path := filepath.Join(functionName, "function.jar")
   158  			if err := os.Rename(tmpFileName, path); err != nil {
   159  				return nil, err
   160  			}
   161  			fd, err := os.Open(path)
   162  			if err != nil {
   163  				return nil, err
   164  			}
   165  
   166  			files := []fileLike{fd}
   167  			opts.Package = filepath.Base(files[0].(*os.File).Name())
   168  			return files, nil
   169  		},
   170  	}
   171  )
   172  
   173  func basicImportHandler(functionName, tmpFileName string, opts *createImageOptions) ([]fileLike, error) {
   174  	return unzipAndGetTopLevelFiles(functionName, tmpFileName)
   175  }
   176  
   177  func createFunctionYaml(opts createImageOptions, functionName string) error {
   178  	strs := strings.Split(opts.Name, "/")
   179  	path := fmt.Sprintf("/%s", strs[1])
   180  
   181  	funcDesc := &funcfile{
   182  		Name:    opts.Name,
   183  		Version: "0.0.1",
   184  		Runtime: opts.Base,
   185  		Cmd:     opts.Handler,
   186  	}
   187  	funcDesc.Config = opts.Config
   188  	funcDesc.Path = path
   189  
   190  	out, err := yaml.Marshal(funcDesc)
   191  	if err != nil {
   192  		return err
   193  	}
   194  
   195  	return ioutil.WriteFile(filepath.Join(functionName, "func.yaml"), out, 0644)
   196  }
   197  
   198  type createImageOptions struct {
   199  	Name          string
   200  	Base          string
   201  	Package       string // Used for Java, empty string for others.
   202  	Handler       string
   203  	OutputStream  io.Writer
   204  	RawJSONStream bool
   205  	Config        map[string]string
   206  }
   207  
   208  type fileLike interface {
   209  	io.Reader
   210  	Stat() (os.FileInfo, error)
   211  }
   212  
   213  var errNoFiles = errors.New("No files to add to image")
   214  
   215  type dockerJSONWriter struct {
   216  	under io.Writer
   217  	w     io.Writer
   218  }
   219  
   220  func newdockerJSONWriter(under io.Writer) *dockerJSONWriter {
   221  	r, w := io.Pipe()
   222  	go func() {
   223  		err := jsonmessage.DisplayJSONMessagesStream(r, under, 1, true, nil)
   224  		if err != nil {
   225  			fmt.Fprintln(os.Stderr, err)
   226  			os.Exit(1)
   227  		}
   228  	}()
   229  	return &dockerJSONWriter{under, w}
   230  }
   231  
   232  func (djw *dockerJSONWriter) Write(p []byte) (int, error) {
   233  	return djw.w.Write(p)
   234  }
   235  
   236  func downloadToFile(url string) (string, error) {
   237  	downloadResp, err := http.Get(url)
   238  	if err != nil {
   239  		return "", err
   240  	}
   241  	defer downloadResp.Body.Close()
   242  
   243  	// zip reader needs ReaderAt, hence the indirection.
   244  	tmpFile, err := ioutil.TempFile("", "lambda-function-")
   245  	if err != nil {
   246  		return "", err
   247  	}
   248  
   249  	if _, err := io.Copy(tmpFile, downloadResp.Body); err != nil {
   250  		return "", err
   251  	}
   252  	if err := tmpFile.Close(); err != nil {
   253  		return "", err
   254  	}
   255  	return tmpFile.Name(), nil
   256  }
   257  
   258  func unzipAndGetTopLevelFiles(dst, src string) (files []fileLike, topErr error) {
   259  	files = make([]fileLike, 0)
   260  
   261  	zipReader, err := zip.OpenReader(src)
   262  	if err != nil {
   263  		return files, err
   264  	}
   265  	defer zipReader.Close()
   266  
   267  	var fd *os.File
   268  	for _, f := range zipReader.File {
   269  		path := filepath.Join(dst, f.Name)
   270  		fmt.Printf("Extracting '%s' to '%s'\n", f.Name, path)
   271  		if f.FileInfo().IsDir() {
   272  			if err := os.Mkdir(path, 0644); err != nil {
   273  				return nil, err
   274  			}
   275  			// Only top-level dirs go into the list since that is what CreateImage expects.
   276  			if filepath.Dir(f.Name) == filepath.Base(f.Name) {
   277  				fd, topErr = os.Open(path)
   278  				if topErr != nil {
   279  					break
   280  				}
   281  				files = append(files, fd)
   282  			}
   283  		} else {
   284  			// We do not close fd here since we may want to use it to dockerize.
   285  			fd, topErr = os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0644)
   286  			if topErr != nil {
   287  				break
   288  			}
   289  
   290  			var zipFd io.ReadCloser
   291  			zipFd, topErr = f.Open()
   292  			if topErr != nil {
   293  				break
   294  			}
   295  
   296  			if _, topErr = io.Copy(fd, zipFd); topErr != nil {
   297  				// OK to skip closing fd here.
   298  				break
   299  			}
   300  
   301  			if err := zipFd.Close(); err != nil {
   302  				return nil, err
   303  			}
   304  
   305  			// Only top-level files go into the list since that is what CreateImage expects.
   306  			if filepath.Dir(f.Name) == "." {
   307  				if _, topErr = fd.Seek(0, 0); topErr != nil {
   308  					break
   309  				}
   310  
   311  				files = append(files, fd)
   312  			} else {
   313  				if err := fd.Close(); err != nil {
   314  					return nil, err
   315  				}
   316  			}
   317  		}
   318  	}
   319  	return
   320  }
   321  
   322  func getFunction(awsProfile, awsRegion, version, arn string) (*aws_lambda.GetFunctionOutput, error) {
   323  	creds := credentials.NewChainCredentials([]credentials.Provider{
   324  		&credentials.EnvProvider{},
   325  		&credentials.SharedCredentialsProvider{
   326  			Filename: "", // Look in default location.
   327  			Profile:  awsProfile,
   328  		},
   329  	})
   330  
   331  	conf := aws.NewConfig().WithCredentials(creds).WithCredentialsChainVerboseErrors(true).WithRegion(awsRegion)
   332  	sess := session.New(conf)
   333  	conn := aws_lambda.New(sess)
   334  	resp, err := conn.GetFunction(&aws_lambda.GetFunctionInput{
   335  		FunctionName: aws.String(arn),
   336  		Qualifier:    aws.String(version),
   337  	})
   338  
   339  	return resp, err
   340  }