github.com/iron-io/functions@v0.0.0-20180820112432-d59d7d1c40b2/fn/commands/lambda.go (about)

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