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 }