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 }