├── LICENSE ├── Makefile ├── README.asciidoc ├── app ├── contexts.go ├── controllers.go ├── hrefs.go ├── media_types.go ├── test │ └── hello_testing.go └── user_types.go ├── aws_serverless_application_model.yaml ├── client ├── client.go ├── hello.go ├── media_types.go └── user_types.go ├── design.go ├── docs └── README.html ├── hello.go ├── main.go ├── swagger ├── swagger.json └── swagger.yaml └── tool ├── cli └── commands.go └── helloserverlessgoa-cli └── main.go /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2017 Couchbase, Inc. 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | 204 | 205 | 206 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # 2 | # This is free and unencumbered software released into the public domain. 3 | # 4 | # Anyone is free to copy, modify, publish, use, compile, sell, or 5 | # distribute this software, either in source code form or as a compiled 6 | # binary, for any purpose, commercial or non-commercial, and by any 7 | # means. 8 | # 9 | # In jurisdictions that recognize copyright laws, the author or authors 10 | # of this software dedicate any and all copyright interest in the 11 | # software to the public domain. We make this dedication for the benefit 12 | # of the public at large and to the detriment of our heirs and 13 | # successors. We intend this dedication to be an overt act of 14 | # relinquishment in perpetuity of all present and future rights to this 15 | # software under copyright law. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 20 | # IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 21 | # OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 22 | # ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 23 | # OTHER DEALINGS IN THE SOFTWARE. 24 | # 25 | # For more information, please refer to 26 | # 27 | 28 | HANDLER ?= handler 29 | PACKAGE ?= $(HANDLER) 30 | GOPATH ?= $(HOME)/go 31 | 32 | WORKDIR = $(CURDIR:$(GOPATH)%=/go%) 33 | ifeq ($(WORKDIR),$(CURDIR)) 34 | WORKDIR = /build 35 | endif 36 | 37 | docker: 38 | @docker run --rm \ 39 | -e HANDLER=$(HANDLER) \ 40 | -e PACKAGE=$(PACKAGE) \ 41 | -v $(GOPATH):/go \ 42 | -v $(CURDIR):/build \ 43 | -w $(WORKDIR) \ 44 | eawsy/aws-lambda-go-shim:latest make all 45 | 46 | .PHONY: docker 47 | 48 | all: build pack perm 49 | 50 | .PHONY: all 51 | 52 | build: 53 | @go build -buildmode=plugin -ldflags='-w -s' -o $(HANDLER).so 54 | 55 | .PHONY: build 56 | 57 | pack: 58 | @pack $(HANDLER) $(HANDLER).so $(PACKAGE).zip 59 | 60 | .PHONY: pack 61 | 62 | perm: 63 | @chown $(shell stat -c '%u:%g' .) $(HANDLER).so $(PACKAGE).zip 64 | 65 | .PHONY: perm 66 | 67 | clean: 68 | @rm -rf $(HANDLER).so $(PACKAGE).zip 69 | 70 | .PHONY: clean 71 | -------------------------------------------------------------------------------- /README.asciidoc: -------------------------------------------------------------------------------- 1 | [%hardbreaks] 2 | 3 | = 🎩 Deploy Goa API backend on AWS Lambda 4 | :toc: left 5 | :toclevels: 3 6 | 7 | link:https://github.com/goadesign/goa[Goa] is a powerful way to to build REST API backends in Go using it's powerful design langugage and OpenAPI Spec generation capabilities. 8 | 9 | It's possible to deploy your Goa backend on AWS Lambda, with help from link:https://github.com/eawsy/aws-lambda-go-shim[eawsy/aws-lambda-go-shim] and link:https://github.com/eawsy/aws-lambda-go-net[aws-lambda-go-net]. 10 | 11 | This guide walks you through the entire process. 12 | 13 | == Installation 14 | 15 | === Deploy aws-lambda-go-shim 16 | 17 | NOTE: You might want to check the link:https://github.com/eawsy/aws-lambda-go-shim[latest instructions], in case these are out of date. 18 | 19 | ==== Create a project directory 20 | 21 | ``` 22 | mkdir serverless-forms; cd serverless-forms 23 | ``` 24 | 25 | Replace `serverless-forms` with your own project name. 26 | 27 | ==== Get dependencies 28 | 29 | This assumes you have Go 1.8 installed. 30 | 31 | ``` 32 | docker pull eawsy/aws-lambda-go-shim:latest 33 | go get -u -d github.com/eawsy/aws-lambda-go-core/... 34 | wget -O Makefile https://git.io/vytH8 35 | ``` 36 | 37 | ==== Add lambda function handler 38 | 39 | Create a new file `handler.go` in your project directory with the following content: 40 | 41 | ``` 42 | package main 43 | 44 | import ( 45 | "encoding/json" 46 | 47 | "github.com/eawsy/aws-lambda-go-core/service/lambda/runtime" 48 | ) 49 | 50 | func Handle(evt json.RawMessage, ctx *runtime.Context) (interface{}, error) { 51 | return "Hello, World!", nil 52 | } 53 | 54 | ``` 55 | 56 | This is the function that will be called back by AWS Lambda (through the shim) 57 | 58 | ==== Build handler.zip 59 | 60 | Run make: 61 | 62 | ``` 63 | make 64 | ``` 65 | 66 | and now you should have a new file called `handler.zip` 67 | 68 | ``` 69 | $ ls -alh handler.zip 70 | -rw-r--r--@ 1 tleyden staff 1.5M Jun 4 10:20 handler.zip 71 | ``` 72 | 73 | ==== Create AWS Lambda IAM Role 74 | 75 | NOTE: you can also do this manually via the AWS Web UI, and if you've already created an AWS Lambda function before, you already have this role and can skip this step. 76 | 77 | ``` 78 | cat > trust-policy.json < `c := controller.NewHelloController(service)` 352 | . Open `controllers/hello.go` and change the `app` package import to `goa-generated/app` 353 | 354 | ==== Run goa standalone server 355 | 356 | ``` 357 | go run main.go 358 | ``` 359 | 360 | and you should see output: 361 | 362 | ``` 363 | 2017/06/04 12:32:00 [INFO] mount ctrl=Hello action=Show route=GET /hello/:whatToSay 364 | 2017/06/04 12:32:00 [INFO] listen transport=http addr=:8080 365 | ``` 366 | 367 | and if you curl: 368 | 369 | ``` 370 | $ curl localhost:8080/hello/foo 371 | {"hello":""} 372 | ``` 373 | 374 | ==== Customize controller behavior 375 | 376 | Open `controllers/hello.go` and look for this line: 377 | 378 | ``` 379 | res := &app.Hello{} 380 | ``` 381 | 382 | and add a new line, so it's now: 383 | 384 | ``` 385 | res := &app.Hello{} 386 | res.Hello = ctx.WhatToSay 387 | ``` 388 | 389 | Now return the goa api server via `go run main.go`, and retry that curl request: 390 | 391 | ``` 392 | $ curl localhost:8080/hello/world 393 | {"hello":"world"} 394 | ``` 395 | 396 | and it now echos the parameter passed along the request path. 397 | 398 | === Deploy Goa API backend to Lambda 399 | 400 | ==== Merge the handler.go and main.go files 401 | 402 | At this point there are two files that need to have their functionality merged: 403 | 404 | . `handler.go` -- this contains the Lambda / API Gateway stub code that was previously pushed up to AWS in a previous step 405 | . `main.go` -- this contains the goa REST API server 406 | 407 | `handler.go` is deleted and it's functionality gets merged into `main.go` after some minor refactoring. 408 | 409 | ``` 410 | //go:generate goagen bootstrap -d github.com/tleyden/serverless-forms/design 411 | 412 | package main 413 | 414 | import ( 415 | "net/http" 416 | 417 | "github.com/eawsy/aws-lambda-go-net/service/lambda/runtime/net" 418 | "github.com/eawsy/aws-lambda-go-net/service/lambda/runtime/net/apigatewayproxy" 419 | "github.com/goadesign/goa" 420 | "github.com/goadesign/goa/middleware" 421 | controller "github.com/tleyden/serverless-forms/controllers" 422 | "github.com/tleyden/serverless-forms/goa-generated/app" 423 | ) 424 | 425 | func createGoaService() *goa.Service { 426 | 427 | // Create service 428 | service := goa.New("HelloServerlessGoa") 429 | 430 | // Mount middleware 431 | service.Use(middleware.RequestID()) 432 | service.Use(middleware.LogRequest(true)) 433 | service.Use(middleware.ErrorHandler(service, true)) 434 | service.Use(middleware.Recover()) 435 | 436 | // Mount "hello" controller 437 | c := controller.NewHelloController(service) 438 | app.MountHelloController(service, c) 439 | 440 | return service 441 | } 442 | 443 | func main() { 444 | 445 | service := createGoaService() 446 | 447 | // Start service 448 | if err := service.ListenAndServe(":8080"); err != nil { 449 | service.LogError("startup", "err", err) 450 | } 451 | 452 | } 453 | 454 | // Handle is the exported handler called by AWS Lambda. 455 | var Handle apigatewayproxy.Handler 456 | 457 | func init() { 458 | 459 | ln := net.Listen() 460 | 461 | // Amazon API Gateway Binary support out of the box. 462 | Handle = apigatewayproxy.New(ln, nil).Handle 463 | 464 | service := createGoaService() 465 | 466 | // Any Go framework complying with the Go http.Handler interface can be used. 467 | // This includes, but is not limited to, Vanilla Go, Gin, Echo, Gorrila, etc. 468 | go http.Serve(ln, service.Mux) 469 | 470 | } 471 | 472 | ``` 473 | 474 | === Deploy to AWS Lambda 475 | 476 | Re-run the same steps previously mentioned in <> 477 | 478 | ``` 479 | $ make 480 | $ aws cloudformation package \ 481 | --template-file aws_serverless_application_model.yaml \ 482 | --output-template-file aws_serverless_application_model.out.yaml \ 483 | --s3-bucket $S3_BUCKET 484 | $ aws cloudformation deploy \ 485 | --template-file aws_serverless_application_model.out.yaml \ 486 | --capabilities CAPABILITY_IAM \ 487 | --stack-name $CLOUDFORMATION_STACK_NAME \ 488 | --region us-east-1 489 | $ aws cloudformation describe-stacks \ 490 | --stack-name $CLOUDFORMATION_STACK_NAME \ 491 | --query Stacks[0].Outputs[0] 492 | ``` 493 | 494 | === Verify 495 | 496 | ``` 497 | $ curl https://7phv3wewuk.execute-api.us-east-1.amazonaws.com/Prod/hello/serverless-goa-world 498 | {"hello":"serverless-goa-world"} 499 | ``` 500 | -------------------------------------------------------------------------------- /app/contexts.go: -------------------------------------------------------------------------------- 1 | // Code generated by goagen v1.2.0-dirty, DO NOT EDIT. 2 | // 3 | // API "HelloServerlessGoa": Application Contexts 4 | // 5 | // Command: 6 | // $ goagen 7 | // --design=github.com/tleyden/serverless-forms/design 8 | // --out=$(GOPATH)/src/github.com/tleyden/serverless-forms/goa-generated 9 | // --version=v1.2.0-dirty 10 | 11 | package app 12 | 13 | import ( 14 | "context" 15 | "github.com/goadesign/goa" 16 | "net/http" 17 | ) 18 | 19 | // ShowHelloContext provides the hello show action context. 20 | type ShowHelloContext struct { 21 | context.Context 22 | *goa.ResponseData 23 | *goa.RequestData 24 | WhatToSay string 25 | } 26 | 27 | // NewShowHelloContext parses the incoming request URL and body, performs validations and creates the 28 | // context used by the hello controller show action. 29 | func NewShowHelloContext(ctx context.Context, r *http.Request, service *goa.Service) (*ShowHelloContext, error) { 30 | var err error 31 | resp := goa.ContextResponse(ctx) 32 | resp.Service = service 33 | req := goa.ContextRequest(ctx) 34 | req.Request = r 35 | rctx := ShowHelloContext{Context: ctx, ResponseData: resp, RequestData: req} 36 | paramWhatToSay := req.Params["whatToSay"] 37 | if len(paramWhatToSay) > 0 { 38 | rawWhatToSay := paramWhatToSay[0] 39 | rctx.WhatToSay = rawWhatToSay 40 | } 41 | return &rctx, err 42 | } 43 | 44 | // OK sends a HTTP response with status code 200. 45 | func (ctx *ShowHelloContext) OK(r *Hello) error { 46 | ctx.ResponseData.Header().Set("Content-Type", "application/vnd.hello+json") 47 | return ctx.ResponseData.Service.Send(ctx.Context, 200, r) 48 | } 49 | 50 | // NotFound sends a HTTP response with status code 404. 51 | func (ctx *ShowHelloContext) NotFound() error { 52 | ctx.ResponseData.WriteHeader(404) 53 | return nil 54 | } 55 | -------------------------------------------------------------------------------- /app/controllers.go: -------------------------------------------------------------------------------- 1 | // Code generated by goagen v1.2.0-dirty, DO NOT EDIT. 2 | // 3 | // API "HelloServerlessGoa": Application Controllers 4 | // 5 | // Command: 6 | // $ goagen 7 | // --design=github.com/tleyden/serverless-forms/design 8 | // --out=$(GOPATH)/src/github.com/tleyden/serverless-forms/goa-generated 9 | // --version=v1.2.0-dirty 10 | 11 | package app 12 | 13 | import ( 14 | "context" 15 | "github.com/goadesign/goa" 16 | "net/http" 17 | ) 18 | 19 | // initService sets up the service encoders, decoders and mux. 20 | func initService(service *goa.Service) { 21 | // Setup encoders and decoders 22 | service.Encoder.Register(goa.NewJSONEncoder, "application/json") 23 | service.Encoder.Register(goa.NewGobEncoder, "application/gob", "application/x-gob") 24 | service.Encoder.Register(goa.NewXMLEncoder, "application/xml") 25 | service.Decoder.Register(goa.NewJSONDecoder, "application/json") 26 | service.Decoder.Register(goa.NewGobDecoder, "application/gob", "application/x-gob") 27 | service.Decoder.Register(goa.NewXMLDecoder, "application/xml") 28 | 29 | // Setup default encoder and decoder 30 | service.Encoder.Register(goa.NewJSONEncoder, "*/*") 31 | service.Decoder.Register(goa.NewJSONDecoder, "*/*") 32 | } 33 | 34 | // HelloController is the controller interface for the Hello actions. 35 | type HelloController interface { 36 | goa.Muxer 37 | Show(*ShowHelloContext) error 38 | } 39 | 40 | // MountHelloController "mounts" a Hello resource controller on the given service. 41 | func MountHelloController(service *goa.Service, ctrl HelloController) { 42 | initService(service) 43 | var h goa.Handler 44 | 45 | h = func(ctx context.Context, rw http.ResponseWriter, req *http.Request) error { 46 | // Check if there was an error loading the request 47 | if err := goa.ContextError(ctx); err != nil { 48 | return err 49 | } 50 | // Build the context 51 | rctx, err := NewShowHelloContext(ctx, req, service) 52 | if err != nil { 53 | return err 54 | } 55 | return ctrl.Show(rctx) 56 | } 57 | service.Mux.Handle("GET", "/hello/:whatToSay", ctrl.MuxHandler("show", h, nil)) 58 | service.LogInfo("mount", "ctrl", "Hello", "action", "Show", "route", "GET /hello/:whatToSay") 59 | } 60 | -------------------------------------------------------------------------------- /app/hrefs.go: -------------------------------------------------------------------------------- 1 | // Code generated by goagen v1.2.0-dirty, DO NOT EDIT. 2 | // 3 | // API "HelloServerlessGoa": Application Resource Href Factories 4 | // 5 | // Command: 6 | // $ goagen 7 | // --design=github.com/tleyden/serverless-forms/design 8 | // --out=$(GOPATH)/src/github.com/tleyden/serverless-forms/goa-generated 9 | // --version=v1.2.0-dirty 10 | 11 | package app 12 | 13 | import ( 14 | "fmt" 15 | "strings" 16 | ) 17 | 18 | // HelloHref returns the resource href. 19 | func HelloHref(whatToSay interface{}) string { 20 | paramwhatToSay := strings.TrimLeftFunc(fmt.Sprintf("%v", whatToSay), func(r rune) bool { return r == '/' }) 21 | return fmt.Sprintf("/hello/%v", paramwhatToSay) 22 | } 23 | -------------------------------------------------------------------------------- /app/media_types.go: -------------------------------------------------------------------------------- 1 | // Code generated by goagen v1.2.0-dirty, DO NOT EDIT. 2 | // 3 | // API "HelloServerlessGoa": Application Media Types 4 | // 5 | // Command: 6 | // $ goagen 7 | // --design=github.com/tleyden/serverless-forms/design 8 | // --out=$(GOPATH)/src/github.com/tleyden/serverless-forms/goa-generated 9 | // --version=v1.2.0-dirty 10 | 11 | package app 12 | 13 | import ( 14 | "github.com/goadesign/goa" 15 | ) 16 | 17 | // Hello World (default view) 18 | // 19 | // Identifier: application/vnd.hello+json; view=default 20 | type Hello struct { 21 | // What was said 22 | Hello string `form:"hello" json:"hello" xml:"hello"` 23 | } 24 | 25 | // Validate validates the Hello media type instance. 26 | func (mt *Hello) Validate() (err error) { 27 | if mt.Hello == "" { 28 | err = goa.MergeErrors(err, goa.MissingAttributeError(`response`, "hello")) 29 | } 30 | return 31 | } 32 | -------------------------------------------------------------------------------- /app/test/hello_testing.go: -------------------------------------------------------------------------------- 1 | // Code generated by goagen v1.2.0-dirty, DO NOT EDIT. 2 | // 3 | // API "HelloServerlessGoa": hello TestHelpers 4 | // 5 | // Command: 6 | // $ goagen 7 | // --design=github.com/tleyden/serverless-forms/design 8 | // --out=$(GOPATH)/src/github.com/tleyden/serverless-forms/goa-generated 9 | // --version=v1.2.0-dirty 10 | 11 | package test 12 | 13 | import ( 14 | "bytes" 15 | "context" 16 | "fmt" 17 | "github.com/goadesign/goa" 18 | "github.com/goadesign/goa/goatest" 19 | "github.com/tleyden/serverless-forms/goa-generated/app" 20 | "io" 21 | "log" 22 | "net/http" 23 | "net/http/httptest" 24 | "net/url" 25 | ) 26 | 27 | // ShowHelloNotFound runs the method Show of the given controller with the given parameters. 28 | // It returns the response writer so it's possible to inspect the response headers. 29 | // If ctx is nil then context.Background() is used. 30 | // If service is nil then a default service is created. 31 | func ShowHelloNotFound(t goatest.TInterface, ctx context.Context, service *goa.Service, ctrl app.HelloController, whatToSay string) http.ResponseWriter { 32 | // Setup service 33 | var ( 34 | logBuf bytes.Buffer 35 | resp interface{} 36 | 37 | respSetter goatest.ResponseSetterFunc = func(r interface{}) { resp = r } 38 | ) 39 | if service == nil { 40 | service = goatest.Service(&logBuf, respSetter) 41 | } else { 42 | logger := log.New(&logBuf, "", log.Ltime) 43 | service.WithLogger(goa.NewLogger(logger)) 44 | newEncoder := func(io.Writer) goa.Encoder { return respSetter } 45 | service.Encoder = goa.NewHTTPEncoder() // Make sure the code ends up using this decoder 46 | service.Encoder.Register(newEncoder, "*/*") 47 | } 48 | 49 | // Setup request context 50 | rw := httptest.NewRecorder() 51 | u := &url.URL{ 52 | Path: fmt.Sprintf("/hello/%v", whatToSay), 53 | } 54 | req, err := http.NewRequest("GET", u.String(), nil) 55 | if err != nil { 56 | panic("invalid test " + err.Error()) // bug 57 | } 58 | prms := url.Values{} 59 | prms["whatToSay"] = []string{fmt.Sprintf("%v", whatToSay)} 60 | if ctx == nil { 61 | ctx = context.Background() 62 | } 63 | goaCtx := goa.NewContext(goa.WithAction(ctx, "HelloTest"), rw, req, prms) 64 | showCtx, _err := app.NewShowHelloContext(goaCtx, req, service) 65 | if _err != nil { 66 | panic("invalid test data " + _err.Error()) // bug 67 | } 68 | 69 | // Perform action 70 | _err = ctrl.Show(showCtx) 71 | 72 | // Validate response 73 | if _err != nil { 74 | t.Fatalf("controller returned %+v, logs:\n%s", _err, logBuf.String()) 75 | } 76 | if rw.Code != 404 { 77 | t.Errorf("invalid response status code: got %+v, expected 404", rw.Code) 78 | } 79 | 80 | // Return results 81 | return rw 82 | } 83 | 84 | // ShowHelloOK runs the method Show of the given controller with the given parameters. 85 | // It returns the response writer so it's possible to inspect the response headers and the media type struct written to the response. 86 | // If ctx is nil then context.Background() is used. 87 | // If service is nil then a default service is created. 88 | func ShowHelloOK(t goatest.TInterface, ctx context.Context, service *goa.Service, ctrl app.HelloController, whatToSay string) (http.ResponseWriter, *app.Hello) { 89 | // Setup service 90 | var ( 91 | logBuf bytes.Buffer 92 | resp interface{} 93 | 94 | respSetter goatest.ResponseSetterFunc = func(r interface{}) { resp = r } 95 | ) 96 | if service == nil { 97 | service = goatest.Service(&logBuf, respSetter) 98 | } else { 99 | logger := log.New(&logBuf, "", log.Ltime) 100 | service.WithLogger(goa.NewLogger(logger)) 101 | newEncoder := func(io.Writer) goa.Encoder { return respSetter } 102 | service.Encoder = goa.NewHTTPEncoder() // Make sure the code ends up using this decoder 103 | service.Encoder.Register(newEncoder, "*/*") 104 | } 105 | 106 | // Setup request context 107 | rw := httptest.NewRecorder() 108 | u := &url.URL{ 109 | Path: fmt.Sprintf("/hello/%v", whatToSay), 110 | } 111 | req, err := http.NewRequest("GET", u.String(), nil) 112 | if err != nil { 113 | panic("invalid test " + err.Error()) // bug 114 | } 115 | prms := url.Values{} 116 | prms["whatToSay"] = []string{fmt.Sprintf("%v", whatToSay)} 117 | if ctx == nil { 118 | ctx = context.Background() 119 | } 120 | goaCtx := goa.NewContext(goa.WithAction(ctx, "HelloTest"), rw, req, prms) 121 | showCtx, _err := app.NewShowHelloContext(goaCtx, req, service) 122 | if _err != nil { 123 | panic("invalid test data " + _err.Error()) // bug 124 | } 125 | 126 | // Perform action 127 | _err = ctrl.Show(showCtx) 128 | 129 | // Validate response 130 | if _err != nil { 131 | t.Fatalf("controller returned %+v, logs:\n%s", _err, logBuf.String()) 132 | } 133 | if rw.Code != 200 { 134 | t.Errorf("invalid response status code: got %+v, expected 200", rw.Code) 135 | } 136 | var mt *app.Hello 137 | if resp != nil { 138 | var ok bool 139 | mt, ok = resp.(*app.Hello) 140 | if !ok { 141 | t.Fatalf("invalid response media: got %+v, expected instance of app.Hello", resp) 142 | } 143 | _err = mt.Validate() 144 | if _err != nil { 145 | t.Errorf("invalid response media type: %s", _err) 146 | } 147 | } 148 | 149 | // Return results 150 | return rw, mt 151 | } 152 | -------------------------------------------------------------------------------- /app/user_types.go: -------------------------------------------------------------------------------- 1 | // Code generated by goagen v1.2.0-dirty, DO NOT EDIT. 2 | // 3 | // API "HelloServerlessGoa": Application User Types 4 | // 5 | // Command: 6 | // $ goagen 7 | // --design=github.com/tleyden/serverless-forms/design 8 | // --out=$(GOPATH)/src/github.com/tleyden/serverless-forms/goa-generated 9 | // --version=v1.2.0-dirty 10 | 11 | package app 12 | -------------------------------------------------------------------------------- /aws_serverless_application_model.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Transform: AWS::Serverless-2016-10-31 3 | Resources: 4 | Function: 5 | Type: AWS::Serverless::Function 6 | Properties: 7 | Handler: handler.Handle 8 | Runtime: python2.7 9 | CodeUri: ./handler.zip 10 | Events: 11 | ApiRoot: 12 | Type: Api 13 | Properties: 14 | Path: / 15 | Method: ANY 16 | ApiGreedy: 17 | Type: Api 18 | Properties: 19 | Path: /{proxy+} 20 | Method: ANY 21 | Outputs: 22 | URL: 23 | Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod" -------------------------------------------------------------------------------- /client/client.go: -------------------------------------------------------------------------------- 1 | // Code generated by goagen v1.2.0-dirty, DO NOT EDIT. 2 | // 3 | // API "HelloServerlessGoa": Client 4 | // 5 | // Command: 6 | // $ goagen 7 | // --design=github.com/tleyden/serverless-forms/design 8 | // --out=$(GOPATH)/src/github.com/tleyden/serverless-forms/goa-generated 9 | // --version=v1.2.0-dirty 10 | 11 | package client 12 | 13 | import ( 14 | "github.com/goadesign/goa" 15 | goaclient "github.com/goadesign/goa/client" 16 | ) 17 | 18 | // Client is the HelloServerlessGoa service client. 19 | type Client struct { 20 | *goaclient.Client 21 | Encoder *goa.HTTPEncoder 22 | Decoder *goa.HTTPDecoder 23 | } 24 | 25 | // New instantiates the client. 26 | func New(c goaclient.Doer) *Client { 27 | client := &Client{ 28 | Client: goaclient.New(c), 29 | Encoder: goa.NewHTTPEncoder(), 30 | Decoder: goa.NewHTTPDecoder(), 31 | } 32 | 33 | // Setup encoders and decoders 34 | client.Encoder.Register(goa.NewJSONEncoder, "application/json") 35 | client.Encoder.Register(goa.NewGobEncoder, "application/gob", "application/x-gob") 36 | client.Encoder.Register(goa.NewXMLEncoder, "application/xml") 37 | client.Decoder.Register(goa.NewJSONDecoder, "application/json") 38 | client.Decoder.Register(goa.NewGobDecoder, "application/gob", "application/x-gob") 39 | client.Decoder.Register(goa.NewXMLDecoder, "application/xml") 40 | 41 | // Setup default encoder and decoder 42 | client.Encoder.Register(goa.NewJSONEncoder, "*/*") 43 | client.Decoder.Register(goa.NewJSONDecoder, "*/*") 44 | 45 | return client 46 | } 47 | -------------------------------------------------------------------------------- /client/hello.go: -------------------------------------------------------------------------------- 1 | // Code generated by goagen v1.2.0-dirty, DO NOT EDIT. 2 | // 3 | // API "HelloServerlessGoa": hello Resource Client 4 | // 5 | // Command: 6 | // $ goagen 7 | // --design=github.com/tleyden/serverless-forms/design 8 | // --out=$(GOPATH)/src/github.com/tleyden/serverless-forms/goa-generated 9 | // --version=v1.2.0-dirty 10 | 11 | package client 12 | 13 | import ( 14 | "context" 15 | "fmt" 16 | "net/http" 17 | "net/url" 18 | ) 19 | 20 | // ShowHelloPath computes a request path to the show action of hello. 21 | func ShowHelloPath(whatToSay string) string { 22 | param0 := whatToSay 23 | 24 | return fmt.Sprintf("/hello/%s", param0) 25 | } 26 | 27 | // Say Hello 28 | func (c *Client) ShowHello(ctx context.Context, path string) (*http.Response, error) { 29 | req, err := c.NewShowHelloRequest(ctx, path) 30 | if err != nil { 31 | return nil, err 32 | } 33 | return c.Client.Do(ctx, req) 34 | } 35 | 36 | // NewShowHelloRequest create the request corresponding to the show action endpoint of the hello resource. 37 | func (c *Client) NewShowHelloRequest(ctx context.Context, path string) (*http.Request, error) { 38 | scheme := c.Scheme 39 | if scheme == "" { 40 | scheme = "http" 41 | } 42 | u := url.URL{Host: c.Host, Scheme: scheme, Path: path} 43 | req, err := http.NewRequest("GET", u.String(), nil) 44 | if err != nil { 45 | return nil, err 46 | } 47 | return req, nil 48 | } 49 | -------------------------------------------------------------------------------- /client/media_types.go: -------------------------------------------------------------------------------- 1 | // Code generated by goagen v1.2.0-dirty, DO NOT EDIT. 2 | // 3 | // API "HelloServerlessGoa": Application Media Types 4 | // 5 | // Command: 6 | // $ goagen 7 | // --design=github.com/tleyden/serverless-forms/design 8 | // --out=$(GOPATH)/src/github.com/tleyden/serverless-forms/goa-generated 9 | // --version=v1.2.0-dirty 10 | 11 | package client 12 | 13 | import ( 14 | "github.com/goadesign/goa" 15 | "net/http" 16 | ) 17 | 18 | // Hello World (default view) 19 | // 20 | // Identifier: application/vnd.hello+json; view=default 21 | type Hello struct { 22 | // What was said 23 | Hello string `form:"hello" json:"hello" xml:"hello"` 24 | } 25 | 26 | // Validate validates the Hello media type instance. 27 | func (mt *Hello) Validate() (err error) { 28 | if mt.Hello == "" { 29 | err = goa.MergeErrors(err, goa.MissingAttributeError(`response`, "hello")) 30 | } 31 | return 32 | } 33 | 34 | // DecodeHello decodes the Hello instance encoded in resp body. 35 | func (c *Client) DecodeHello(resp *http.Response) (*Hello, error) { 36 | var decoded Hello 37 | err := c.Decoder.Decode(&decoded, resp.Body, resp.Header.Get("Content-Type")) 38 | return &decoded, err 39 | } 40 | -------------------------------------------------------------------------------- /client/user_types.go: -------------------------------------------------------------------------------- 1 | // Code generated by goagen v1.2.0-dirty, DO NOT EDIT. 2 | // 3 | // API "HelloServerlessGoa": Application User Types 4 | // 5 | // Command: 6 | // $ goagen 7 | // --design=github.com/tleyden/serverless-forms/design 8 | // --out=$(GOPATH)/src/github.com/tleyden/serverless-forms/goa-generated 9 | // --version=v1.2.0-dirty 10 | 11 | package client 12 | -------------------------------------------------------------------------------- /design.go: -------------------------------------------------------------------------------- 1 | package design 2 | 3 | import ( 4 | . "github.com/goadesign/goa/design" 5 | . "github.com/goadesign/goa/design/apidsl" 6 | ) 7 | 8 | var _ = API("HelloServerlessGoa", func() { 9 | Title("Goa Server API Example") 10 | Description("Goa API powered by AWS Lambda and API Gateway") 11 | Scheme("http") 12 | Host("localhost:8080") 13 | }) 14 | 15 | var _ = Resource("hello", func() { 16 | BasePath("/hello") 17 | DefaultMedia(HelloMedia) 18 | 19 | Action("show", func() { 20 | Description("Say Hello") 21 | Routing(GET("/:whatToSay")) 22 | Params(func() { 23 | Param("whatToSay", String, "What To Say Hello To") 24 | }) 25 | Response(OK) 26 | Response(NotFound) 27 | }) 28 | }) 29 | 30 | var HelloMedia = MediaType("application/vnd.hello+json", func() { 31 | Description("Hello World") 32 | Attributes(func() { 33 | Attribute("hello", String, "What was said") 34 | Required("hello") 35 | }) 36 | View("default", func() { 37 | Attribute("hello") 38 | }) 39 | }) 40 | -------------------------------------------------------------------------------- /docs/README.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 🎩 Deploy Goa API backend on AWS Lambda 9 | 10 | 420 | 421 | 422 | 472 |
473 |
474 |
475 |
476 |

Goa is a powerful way to to build REST API backends in Go using it’s powerful design langugage and OpenAPI Spec generation capabilities.

477 |
478 |
479 |

It’s possible to deploy your Goa backend on AWS Lambda, with help from eawsy/aws-lambda-go-shim and aws-lambda-go-net.

480 |
481 |
482 |

This guide walks you through the entire process.

483 |
484 |
485 |
486 |
487 |

Installation

488 |
489 |
490 |

Deploy aws-lambda-go-shim

491 |
492 | 493 | 494 | 497 | 500 | 501 |
495 |
Note
496 |
498 | You might want to check the latest instructions, in case these are out of date. 499 |
502 |
503 |
504 |

Create a project directory

505 |
506 |
507 |
mkdir serverless-forms; cd serverless-forms
508 |
509 |
510 |
511 |

Replace serverless-forms with your own project name.

512 |
513 |
514 |
515 |

Get dependencies

516 |
517 |

This assumes you have Go 1.8 installed.

518 |
519 |
520 |
521 |
docker pull eawsy/aws-lambda-go-shim:latest
 522 | go get -u -d github.com/eawsy/aws-lambda-go-core/...
 523 | wget -O Makefile https://git.io/vytH8
524 |
525 |
526 |
527 |
528 |

Add lambda function handler

529 |
530 |

Create a new file handler.go in your project directory with the following content:

531 |
532 |
533 |
534 |
package main
 535 | 
 536 | import (
 537 | 	"encoding/json"
 538 | 
 539 | 	"github.com/eawsy/aws-lambda-go-core/service/lambda/runtime"
 540 | )
 541 | 
 542 | func Handle(evt json.RawMessage, ctx *runtime.Context) (interface{}, error) {
 543 | 	return "Hello, World!", nil
 544 | }
545 |
546 |
547 |
548 |

This is the function that will be called back by AWS Lambda (through the shim)

549 |
550 |
551 |
552 |

Build handler.zip

553 |
554 |

Run make:

555 |
556 |
557 |
558 |
make
559 |
560 |
561 |
562 |

and now you should have a new file called handler.zip

563 |
564 |
565 |
566 |
$ ls -alh handler.zip
 567 | -rw-r--r--@ 1 tleyden  staff   1.5M Jun  4 10:20 handler.zip
568 |
569 |
570 |
571 |
572 |

Create AWS Lambda IAM Role

573 |
574 | 575 | 576 | 579 | 582 | 583 |
577 |
Note
578 |
580 | you can also do this manually via the AWS Web UI, and if you’ve already created an AWS Lambda function before, you already have this role and can skip this step. 581 |
584 |
585 |
586 |
587 |
cat > trust-policy.json <<EOL
 588 | {
 589 |   "Version": "2012-10-17",
 590 |   "Statement": [{
 591 |     "Effect": "Allow",
 592 |     "Principal": {
 593 |       "Service": "lambda.amazonaws.com"
 594 |     },
 595 |     "Action": "sts:AssumeRole"
 596 |   }]
 597 | }
 598 | EOL
 599 | 
 600 | aws iam create-role --role-name lambda_basic_execution --assume-role-policy-document file://trust-policy.json
 601 | aws iam attach-role-policy --role-name lambda_basic_execution --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
602 |
603 |
604 |
605 |
606 |

Deploy to AWS Lambda

607 |
608 |

Find your AWS account number from the AWS Web Admin, and replace 19382281 below with your AWS account number.

609 |
610 |
611 |
612 |
AWS_ACCOUNT_NUMBER=19382281
613 |
614 |
615 |
616 |

Deploy the Lambda function:

617 |
618 |
619 |
620 |
aws lambda create-function \
 621 |   --role arn:aws:iam::$AWS_ACCOUNT_NUMBER:role/lambda_basic_execution \
 622 |   --function-name preview-go \
 623 |   --zip-file fileb://handler.zip \
 624 |   --runtime python2.7 \
 625 |   --handler handler.Handle
626 |
627 |
628 |
629 |
630 |

Verify

631 |
632 |
    633 |
  1. 634 |

    In the AWS Web Admin, go to the Lambda section

    635 |
  2. 636 |
  3. 637 |

    Choose the preview-go lambda function

    638 |
  4. 639 |
  5. 640 |

    Under Actions, select Test Function

    641 |
  6. 642 |
  7. 643 |

    Hit the Save and Test button

    644 |
  8. 645 |
  9. 646 |

    Under "The area below shows the result returned by your function execution.", you should see "Hello World!" — this means it worked!

    647 |
  10. 648 |
649 |
650 |
651 |
652 |
653 |

Deploy aws-lambda-go-shim behind API Gateway

654 |
655 | 656 | 657 | 660 | 663 | 664 |
658 |
Note
659 |
661 | The latest version of these docs is available on the eawsy/aws-lambda-go-net 662 |
665 |
666 |
667 |

Get dependencies

668 |
669 |
670 |
go get -u -d github.com/eawsy/aws-lambda-go-net/...
671 |
672 |
673 |
674 |
675 |

Update handler.go

676 |
677 |
678 |
package main
 679 | 
 680 | import (
 681 | 	"net/http"
 682 | 
 683 | 	"github.com/eawsy/aws-lambda-go-net/service/lambda/runtime/net"
 684 | 	"github.com/eawsy/aws-lambda-go-net/service/lambda/runtime/net/apigatewayproxy"
 685 | )
 686 | 
 687 | // Handle is the exported handler called by AWS Lambda.
 688 | var Handle apigatewayproxy.Handler
 689 | 
 690 | func init() {
 691 | 	ln := net.Listen()
 692 | 
 693 | 	// Amazon API Gateway binary media types are supported out of the box.
 694 | 	// If you don't send or receive binary data, you can safely set it to nil.
 695 | 	Handle = apigatewayproxy.New(ln, []string{"image/png"}).Handle
 696 | 
 697 | 	// Any Go framework complying with the Go http.Handler interface can be used.
 698 | 	// This includes, but is not limited to, Vanilla Go, Gin, Echo, Gorrila, Goa, etc.
 699 | 	go http.Serve(ln, http.HandlerFunc(handle))
 700 | }
 701 | 
 702 | func handle(w http.ResponseWriter, r *http.Request) {
 703 | 	w.Write([]byte("Hello, World!"))
 704 | }
705 |
706 |
707 |
708 |
709 |

Rebuild handler.zip

710 |
711 |
712 |
make
713 |
714 |
715 |
716 |
717 |

Create SAML (AWS Serverless Application Model) file

718 |
719 |

Create a new file named aws_serverless_application_model.yaml with the following content:

720 |
721 |
722 |
723 |
AWSTemplateFormatVersion: '2010-09-09'
 724 | Transform: AWS::Serverless-2016-10-31
 725 | Resources:
 726 |   Function:
 727 |     Type: AWS::Serverless::Function
 728 |     Properties:
 729 |       Handler: handler.Handle
 730 |       Runtime: python2.7
 731 |       CodeUri: ./handler.zip
 732 |       Events:
 733 |         ApiRoot:
 734 |           Type: Api
 735 |           Properties:
 736 |             Path: /
 737 |             Method: ANY
 738 |         ApiGreedy:
 739 |           Type: Api
 740 |           Properties:
 741 |             Path: /{proxy+}
 742 |             Method: ANY
 743 | Outputs:
 744 |   URL:
 745 |     Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod"
746 |
747 |
748 |
749 |
750 |

Create an S3 bucket

751 |
752 |

Create a new S3 bucket which will hold your packaged cloudformation templates.

753 |
754 |
755 |
756 |
$ aws s3api create-bucket --bucket my-bucket
 757 | $ S3_BUCKET="my-bucket"
758 |
759 |
760 |
761 | 762 | 763 | 766 | 769 | 770 |
764 |
Note
765 |
767 | see aws s3api docs, this might need more parameters. 768 |
771 |
772 |
773 |
774 |

Deploy to AWS Lambda

775 |
776 |

Upload the packaged cloudformation template to s3:

777 |
778 |
779 |
780 |
aws cloudformation package \
 781 |   --template-file aws_serverless_application_model.yaml \
 782 |   --output-template-file aws_serverless_application_model.out.yaml \
 783 |   --s3-bucket $S3_BUCKET
784 |
785 |
786 |
787 |

Choose a name for your cloudformation stack

788 |
789 |
790 |
791 |
CLOUDFORMATION_STACK_NAME="HelloServerlessGolangApi"
792 |
793 |
794 |
795 |

Deploy the cloudformation stack

796 |
797 |
798 |
799 |
aws cloudformation deploy \
 800 |   --template-file aws_serverless_application_model.out.yaml \
 801 |   --capabilities CAPABILITY_IAM \
 802 |   --stack-name $CLOUDFORMATION_STACK_NAME \
 803 |   --region us-east-1
804 |
805 |
806 |
807 |
808 |

Verify

809 |
810 |

Find out the URL of the API Gateway endpoint via Cloudformation Template outputs:

811 |
812 |
813 |
814 |
aws cloudformation describe-stacks \
 815 |   --stack-name $CLOUDFORMATION_STACK_NAME \
 816 |   --query Stacks[0].Outputs[0]
817 |
818 |
819 |
820 |

This will give you a URL like:

821 |
822 |
823 |
824 |
------------------------------------------------------------------------------
 825 | |                               DescribeStacks                               |
 826 | +-----------+----------------------------------------------------------------+
 827 | | OutputKey |                          OutputValue                           |
 828 | +-----------+----------------------------------------------------------------+
 829 | |  URL      |  https://7phv3eeluk.execute-api.us-east-1.amazonaws.com/Prod   |
 830 | +-----------+----------------------------------------------------------------+
831 |
832 |
833 |
834 |

Now try to issue a curl request against it:

835 |
836 |
837 |
838 |
$ curl https://7phv3eeluk.execute-api.us-east-1.amazonaws.com/Prod
 839 | Hello, World!
840 |
841 |
842 |
843 |
844 |
845 |

Generate Goa API backend

846 |
847 |

Create design.go

848 |
849 |
850 |
package design
 851 | 
 852 | import (
 853 | 	. "github.com/goadesign/goa/design"
 854 | 	. "github.com/goadesign/goa/design/apidsl"
 855 | )
 856 | 
 857 | var _ = API("HelloServerlessGoa", func() {
 858 | 	Title("Goa Server API Example")
 859 | 	Description("Goa API powered by AWS Lambda and API Gateway")
 860 | 	Scheme("http")
 861 | 	Host("localhost:8080")
 862 | })
 863 | 
 864 | var _ = Resource("hello", func() {
 865 | 	BasePath("/hello")
 866 | 	DefaultMedia(HelloMedia)
 867 | 
 868 | 	Action("show", func() {
 869 | 		Description("Say Hello")
 870 | 		Routing(GET("/:whatToSay"))
 871 | 		Params(func() {
 872 | 			Param("whatToSay", String, "What To Say Hello To")
 873 | 		})
 874 | 		Response(OK)
 875 | 		Response(NotFound)
 876 | 	})
 877 | })
 878 | 
 879 | var HelloMedia = MediaType("application/vnd.hello+json", func() {
 880 | 	Description("Hello World")
 881 | 	Attributes(func() {
 882 | 		Attribute("hello", String, "What was said")
 883 | 		Required("hello")
 884 | 	})
 885 | 	View("default", func() {
 886 | 		Attribute("hello")
 887 | 	})
 888 | })
889 |
890 |
891 |
892 |
893 |

Generate goa code

894 |
895 |

Generate the controller, which we will customize:

896 |
897 |
898 |
899 |
goagen controller --force --pkg controller -d github.com/tleyden/serverless-forms/design -o ./controllers
900 |
901 |
902 |
903 |

and the remaining goa generated code, which we won’t touch.

904 |
905 |
906 |
907 |
goagen app -d github.com/tleyden/serverless-forms/design -o ./goa-generated
 908 | goagen client -d github.com/tleyden/serverless-forms/design -o ./goa-generated
 909 | goagen swagger -d github.com/tleyden/serverless-forms/design -o ./goa-generated
910 |
911 |
912 |
913 |

Generate the main scaffolding:

914 |
915 |
916 |
917 |
goagen main -d github.com/tleyden/serverless-forms/design
918 |
919 |
920 |
921 |

and remove the hello.go which we don’t need, since it’s already in the controllers directory

922 |
923 |
924 |
925 |
rm hello.go
926 |
927 |
928 |
929 |
930 |

Goa fixups

931 |
932 |

Sorry, this part is really ugly, I need to get in touch with the goa folks to try to make this cleaner. Part of the issue is that I’m putting everything in the goa-generated directory, to keep the generated code separate, which breaks the package names.

933 |
934 |
935 |
    936 |
  1. 937 |

    Open main.go and

    938 |
    939 |
      940 |
    1. 941 |

      Change the app package import to goa-generated/app

      942 |
    2. 943 |
    3. 944 |

      Add this package import: controller "github.com/tleyden/serverless-forms/controllers"

      945 |
    4. 946 |
    5. 947 |

      Change c := NewHelloController(service)c := controller.NewHelloController(service)

      948 |
    6. 949 |
    950 |
    951 |
  2. 952 |
  3. 953 |

    Open controllers/hello.go and change the app package import to goa-generated/app

    954 |
  4. 955 |
956 |
957 |
958 |
959 |

Run goa standalone server

960 |
961 |
962 |
go run main.go
963 |
964 |
965 |
966 |

and you should see output:

967 |
968 |
969 |
970 |
2017/06/04 12:32:00 [INFO] mount ctrl=Hello action=Show route=GET /hello/:whatToSay
 971 | 2017/06/04 12:32:00 [INFO] listen transport=http addr=:8080
972 |
973 |
974 |
975 |

and if you curl:

976 |
977 |
978 |
979 |
$ curl localhost:8080/hello/foo
 980 | {"hello":""}
981 |
982 |
983 |
984 |
985 |

Customize controller behavior

986 |
987 |

Open controllers/hello.go and look for this line:

988 |
989 |
990 |
991 |
res := &app.Hello{}
992 |
993 |
994 |
995 |

and add a new line, so it’s now:

996 |
997 |
998 |
999 |
res := &app.Hello{}
1000 | res.Hello = ctx.WhatToSay
1001 |
1002 |
1003 |
1004 |

Now return the goa api server via go run main.go, and retry that curl request:

1005 |
1006 |
1007 |
1008 |
$ curl localhost:8080/hello/world
1009 | {"hello":"world"}
1010 |
1011 |
1012 |
1013 |

and it now echos the parameter passed along the request path.

1014 |
1015 |
1016 |
1017 |
1018 |

Deploy Goa API backend to Lambda

1019 |
1020 |

Merge the handler.go and main.go files

1021 |
1022 |

At this point there are two files that need to have their functionality merged:

1023 |
1024 |
1025 |
    1026 |
  1. 1027 |

    handler.go — this contains the Lambda / API Gateway stub code that was previously pushed up to AWS in a previous step

    1028 |
  2. 1029 |
  3. 1030 |

    main.go — this contains the goa REST API server

    1031 |
  4. 1032 |
1033 |
1034 |
1035 |

handler.go is deleted and it’s functionality gets merged into main.go after some minor refactoring.

1036 |
1037 |
1038 |
1039 |
//go:generate goagen bootstrap -d github.com/tleyden/serverless-forms/design
1040 | 
1041 | package main
1042 | 
1043 | import (
1044 | 	"net/http"
1045 | 
1046 | 	"github.com/eawsy/aws-lambda-go-net/service/lambda/runtime/net"
1047 | 	"github.com/eawsy/aws-lambda-go-net/service/lambda/runtime/net/apigatewayproxy"
1048 | 	"github.com/goadesign/goa"
1049 | 	"github.com/goadesign/goa/middleware"
1050 | 	controller "github.com/tleyden/serverless-forms/controllers"
1051 | 	"github.com/tleyden/serverless-forms/goa-generated/app"
1052 | )
1053 | 
1054 | func createGoaService() *goa.Service {
1055 | 
1056 | 	// Create service
1057 | 	service := goa.New("HelloServerlessGoa")
1058 | 
1059 | 	// Mount middleware
1060 | 	service.Use(middleware.RequestID())
1061 | 	service.Use(middleware.LogRequest(true))
1062 | 	service.Use(middleware.ErrorHandler(service, true))
1063 | 	service.Use(middleware.Recover())
1064 | 
1065 | 	// Mount "hello" controller
1066 | 	c := controller.NewHelloController(service)
1067 | 	app.MountHelloController(service, c)
1068 | 
1069 | 	return service
1070 | }
1071 | 
1072 | func main() {
1073 | 
1074 | 	service := createGoaService()
1075 | 
1076 | 	// Start service
1077 | 	if err := service.ListenAndServe(":8080"); err != nil {
1078 | 		service.LogError("startup", "err", err)
1079 | 	}
1080 | 
1081 | }
1082 | 
1083 | // Handle is the exported handler called by AWS Lambda.
1084 | var Handle apigatewayproxy.Handler
1085 | 
1086 | func init() {
1087 | 
1088 | 	ln := net.Listen()
1089 | 
1090 | 	// Amazon API Gateway Binary support out of the box.
1091 | 	Handle = apigatewayproxy.New(ln, nil).Handle
1092 | 
1093 | 	service := createGoaService()
1094 | 
1095 | 	// Any Go framework complying with the Go http.Handler interface can be used.
1096 | 	// This includes, but is not limited to, Vanilla Go, Gin, Echo, Gorrila, etc.
1097 | 	go http.Serve(ln, service.Mux)
1098 | 
1099 | }
1100 |
1101 |
1102 |
1103 |
1104 |
1105 |

Deploy to AWS Lambda

1106 |
1107 |

Re-run the same steps previously mentioned in Deploy aws-lambda-go-shim behind API Gateway

1108 |
1109 |
1110 |
1111 |
$ make
1112 | $ aws cloudformation package \
1113 |   --template-file aws_serverless_application_model.yaml \
1114 |   --output-template-file aws_serverless_application_model.out.yaml \
1115 |   --s3-bucket $S3_BUCKET
1116 | $ aws cloudformation deploy \
1117 |   --template-file aws_serverless_application_model.out.yaml \
1118 |   --capabilities CAPABILITY_IAM \
1119 |   --stack-name $CLOUDFORMATION_STACK_NAME \
1120 |   --region us-east-1
1121 | $ aws cloudformation describe-stacks \
1122 |   --stack-name $CLOUDFORMATION_STACK_NAME \
1123 |   --query Stacks[0].Outputs[0]
1124 |
1125 |
1126 |
1127 |
1128 |

Verify

1129 |
1130 |
1131 |
$ curl https://7phv3wewuk.execute-api.us-east-1.amazonaws.com/Prod/hello/serverless-goa-world
1132 | {"hello":"serverless-goa-world"}
1133 |
1134 |
1135 |
1136 |
1137 |
1138 |
1139 | 1144 | 1145 | -------------------------------------------------------------------------------- /hello.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "github.com/goadesign/goa" 5 | "github.com/tleyden/serverless-forms/goa-generated/app" 6 | ) 7 | 8 | // HelloController implements the hello resource. 9 | type HelloController struct { 10 | *goa.Controller 11 | } 12 | 13 | // NewHelloController creates a hello controller. 14 | func NewHelloController(service *goa.Service) *HelloController { 15 | return &HelloController{Controller: service.NewController("HelloController")} 16 | } 17 | 18 | // Show runs the show action. 19 | func (c *HelloController) Show(ctx *app.ShowHelloContext) error { 20 | // HelloController_Show: start_implement 21 | 22 | // Put your logic here 23 | 24 | // HelloController_Show: end_implement 25 | res := &app.Hello{} 26 | 27 | res.Hello = ctx.WhatToSay 28 | 29 | return ctx.OK(res) 30 | } 31 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | //go:generate goagen bootstrap -d github.com/tleyden/serverless-forms/design 2 | 3 | package main 4 | 5 | import ( 6 | "net/http" 7 | 8 | "github.com/eawsy/aws-lambda-go-net/service/lambda/runtime/net" 9 | "github.com/eawsy/aws-lambda-go-net/service/lambda/runtime/net/apigatewayproxy" 10 | "github.com/goadesign/goa" 11 | "github.com/goadesign/goa/middleware" 12 | controller "github.com/tleyden/serverless-forms/controllers" 13 | "github.com/tleyden/serverless-forms/goa-generated/app" 14 | ) 15 | 16 | func createGoaService() *goa.Service { 17 | 18 | // Create service 19 | service := goa.New("HelloServerlessGoa") 20 | 21 | // Mount middleware 22 | service.Use(middleware.RequestID()) 23 | service.Use(middleware.LogRequest(true)) 24 | service.Use(middleware.ErrorHandler(service, true)) 25 | service.Use(middleware.Recover()) 26 | 27 | // Mount "hello" controller 28 | c := controller.NewHelloController(service) 29 | app.MountHelloController(service, c) 30 | 31 | return service 32 | } 33 | 34 | func main() { 35 | 36 | service := createGoaService() 37 | 38 | // Start service 39 | if err := service.ListenAndServe(":8080"); err != nil { 40 | service.LogError("startup", "err", err) 41 | } 42 | 43 | } 44 | 45 | // Handle is the exported handler called by AWS Lambda. 46 | var Handle apigatewayproxy.Handler 47 | 48 | func init() { 49 | 50 | ln := net.Listen() 51 | 52 | // Amazon API Gateway Binary support out of the box. 53 | Handle = apigatewayproxy.New(ln, nil).Handle 54 | 55 | service := createGoaService() 56 | 57 | // Any Go framework complying with the Go http.Handler interface can be used. 58 | // This includes, but is not limited to, Vanilla Go, Gin, Echo, Gorrila, etc. 59 | go http.Serve(ln, service.Mux) 60 | 61 | } 62 | -------------------------------------------------------------------------------- /swagger/swagger.json: -------------------------------------------------------------------------------- 1 | {"swagger":"2.0","info":{"title":"Goa Server API Example","description":"Goa API powered by AWS Lambda and API Gateway","version":""},"host":"localhost:8080","schemes":["http"],"consumes":["application/json","application/xml","application/gob","application/x-gob"],"produces":["application/json","application/xml","application/gob","application/x-gob"],"paths":{"/hello/{whatToSay}":{"get":{"tags":["hello"],"summary":"show hello","description":"Say Hello","operationId":"hello#show","produces":["application/vnd.hello+json"],"parameters":[{"name":"whatToSay","in":"path","description":"What To Say Hello To","required":true,"type":"string"}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/Hello"}},"404":{"description":"Not Found"}},"schemes":["http"]}}},"definitions":{"Hello":{"title":"Mediatype identifier: application/vnd.hello+json; view=default","type":"object","properties":{"hello":{"type":"string","description":"What was said","example":"Voluptatem voluptates ipsum alias."}},"description":"Hello World (default view)","example":{"hello":"Voluptatem voluptates ipsum alias."},"required":["hello"]}},"responses":{"NotFound":{"description":"Not Found"},"OK":{"description":"OK","schema":{"$ref":"#/definitions/Hello"}}}} -------------------------------------------------------------------------------- /swagger/swagger.yaml: -------------------------------------------------------------------------------- 1 | consumes: 2 | - application/json 3 | - application/xml 4 | - application/gob 5 | - application/x-gob 6 | definitions: 7 | Hello: 8 | description: Hello World (default view) 9 | example: 10 | hello: Voluptatem voluptates ipsum alias. 11 | properties: 12 | hello: 13 | description: What was said 14 | example: Voluptatem voluptates ipsum alias. 15 | type: string 16 | required: 17 | - hello 18 | title: 'Mediatype identifier: application/vnd.hello+json; view=default' 19 | type: object 20 | host: localhost:8080 21 | info: 22 | description: Goa API powered by AWS Lambda and API Gateway 23 | title: Goa Server API Example 24 | version: "" 25 | paths: 26 | /hello/{whatToSay}: 27 | get: 28 | description: Say Hello 29 | operationId: hello#show 30 | parameters: 31 | - description: What To Say Hello To 32 | in: path 33 | name: whatToSay 34 | required: true 35 | type: string 36 | produces: 37 | - application/vnd.hello+json 38 | responses: 39 | "200": 40 | description: OK 41 | schema: 42 | $ref: '#/definitions/Hello' 43 | "404": 44 | description: Not Found 45 | schemes: 46 | - http 47 | summary: show hello 48 | tags: 49 | - hello 50 | produces: 51 | - application/json 52 | - application/xml 53 | - application/gob 54 | - application/x-gob 55 | responses: 56 | NotFound: 57 | description: Not Found 58 | OK: 59 | description: OK 60 | schema: 61 | $ref: '#/definitions/Hello' 62 | schemes: 63 | - http 64 | swagger: "2.0" 65 | -------------------------------------------------------------------------------- /tool/cli/commands.go: -------------------------------------------------------------------------------- 1 | // Code generated by goagen v1.2.0-dirty, DO NOT EDIT. 2 | // 3 | // API "HelloServerlessGoa": CLI Commands 4 | // 5 | // Command: 6 | // $ goagen 7 | // --design=github.com/tleyden/serverless-forms/design 8 | // --out=$(GOPATH)/src/github.com/tleyden/serverless-forms/goa-generated 9 | // --version=v1.2.0-dirty 10 | 11 | package cli 12 | 13 | import ( 14 | "context" 15 | "encoding/json" 16 | "fmt" 17 | "github.com/goadesign/goa" 18 | goaclient "github.com/goadesign/goa/client" 19 | uuid "github.com/goadesign/goa/uuid" 20 | "github.com/spf13/cobra" 21 | "github.com/tleyden/serverless-forms/goa-generated/client" 22 | "log" 23 | "net/url" 24 | "os" 25 | "strconv" 26 | "strings" 27 | "time" 28 | ) 29 | 30 | type ( 31 | // ShowHelloCommand is the command line data structure for the show action of hello 32 | ShowHelloCommand struct { 33 | // What To Say Hello To 34 | WhatToSay string 35 | PrettyPrint bool 36 | } 37 | ) 38 | 39 | // RegisterCommands registers the resource action CLI commands. 40 | func RegisterCommands(app *cobra.Command, c *client.Client) { 41 | var command, sub *cobra.Command 42 | command = &cobra.Command{ 43 | Use: "show", 44 | Short: `Say Hello`, 45 | } 46 | tmp1 := new(ShowHelloCommand) 47 | sub = &cobra.Command{ 48 | Use: `hello ["/hello/WHATTOSAY"]`, 49 | Short: ``, 50 | RunE: func(cmd *cobra.Command, args []string) error { return tmp1.Run(c, args) }, 51 | } 52 | tmp1.RegisterFlags(sub, c) 53 | sub.PersistentFlags().BoolVar(&tmp1.PrettyPrint, "pp", false, "Pretty print response body") 54 | command.AddCommand(sub) 55 | app.AddCommand(command) 56 | } 57 | 58 | func intFlagVal(name string, parsed int) *int { 59 | if hasFlag(name) { 60 | return &parsed 61 | } 62 | return nil 63 | } 64 | 65 | func float64FlagVal(name string, parsed float64) *float64 { 66 | if hasFlag(name) { 67 | return &parsed 68 | } 69 | return nil 70 | } 71 | 72 | func boolFlagVal(name string, parsed bool) *bool { 73 | if hasFlag(name) { 74 | return &parsed 75 | } 76 | return nil 77 | } 78 | 79 | func stringFlagVal(name string, parsed string) *string { 80 | if hasFlag(name) { 81 | return &parsed 82 | } 83 | return nil 84 | } 85 | 86 | func hasFlag(name string) bool { 87 | for _, arg := range os.Args[1:] { 88 | if strings.HasPrefix(arg, "--"+name) { 89 | return true 90 | } 91 | } 92 | return false 93 | } 94 | 95 | func jsonVal(val string) (*interface{}, error) { 96 | var t interface{} 97 | err := json.Unmarshal([]byte(val), &t) 98 | if err != nil { 99 | return nil, err 100 | } 101 | return &t, nil 102 | } 103 | 104 | func jsonArray(ins []string) ([]interface{}, error) { 105 | if ins == nil { 106 | return nil, nil 107 | } 108 | var vals []interface{} 109 | for _, id := range ins { 110 | val, err := jsonVal(id) 111 | if err != nil { 112 | return nil, err 113 | } 114 | vals = append(vals, val) 115 | } 116 | return vals, nil 117 | } 118 | 119 | func timeVal(val string) (*time.Time, error) { 120 | t, err := time.Parse(time.RFC3339, val) 121 | if err != nil { 122 | return nil, err 123 | } 124 | return &t, nil 125 | } 126 | 127 | func timeArray(ins []string) ([]time.Time, error) { 128 | if ins == nil { 129 | return nil, nil 130 | } 131 | var vals []time.Time 132 | for _, id := range ins { 133 | val, err := timeVal(id) 134 | if err != nil { 135 | return nil, err 136 | } 137 | vals = append(vals, *val) 138 | } 139 | return vals, nil 140 | } 141 | 142 | func uuidVal(val string) (*uuid.UUID, error) { 143 | t, err := uuid.FromString(val) 144 | if err != nil { 145 | return nil, err 146 | } 147 | return &t, nil 148 | } 149 | 150 | func uuidArray(ins []string) ([]uuid.UUID, error) { 151 | if ins == nil { 152 | return nil, nil 153 | } 154 | var vals []uuid.UUID 155 | for _, id := range ins { 156 | val, err := uuidVal(id) 157 | if err != nil { 158 | return nil, err 159 | } 160 | vals = append(vals, *val) 161 | } 162 | return vals, nil 163 | } 164 | 165 | func float64Val(val string) (*float64, error) { 166 | t, err := strconv.ParseFloat(val, 64) 167 | if err != nil { 168 | return nil, err 169 | } 170 | return &t, nil 171 | } 172 | 173 | func float64Array(ins []string) ([]float64, error) { 174 | if ins == nil { 175 | return nil, nil 176 | } 177 | var vals []float64 178 | for _, id := range ins { 179 | val, err := float64Val(id) 180 | if err != nil { 181 | return nil, err 182 | } 183 | vals = append(vals, *val) 184 | } 185 | return vals, nil 186 | } 187 | 188 | func boolVal(val string) (*bool, error) { 189 | t, err := strconv.ParseBool(val) 190 | if err != nil { 191 | return nil, err 192 | } 193 | return &t, nil 194 | } 195 | 196 | func boolArray(ins []string) ([]bool, error) { 197 | if ins == nil { 198 | return nil, nil 199 | } 200 | var vals []bool 201 | for _, id := range ins { 202 | val, err := boolVal(id) 203 | if err != nil { 204 | return nil, err 205 | } 206 | vals = append(vals, *val) 207 | } 208 | return vals, nil 209 | } 210 | 211 | // Run makes the HTTP request corresponding to the ShowHelloCommand command. 212 | func (cmd *ShowHelloCommand) Run(c *client.Client, args []string) error { 213 | var path string 214 | if len(args) > 0 { 215 | path = args[0] 216 | } else { 217 | path = fmt.Sprintf("/hello/%v", url.QueryEscape(cmd.WhatToSay)) 218 | } 219 | logger := goa.NewLogger(log.New(os.Stderr, "", log.LstdFlags)) 220 | ctx := goa.WithLogger(context.Background(), logger) 221 | resp, err := c.ShowHello(ctx, path) 222 | if err != nil { 223 | goa.LogError(ctx, "failed", "err", err) 224 | return err 225 | } 226 | 227 | goaclient.HandleResponse(c.Client, resp, cmd.PrettyPrint) 228 | return nil 229 | } 230 | 231 | // RegisterFlags registers the command flags with the command line. 232 | func (cmd *ShowHelloCommand) RegisterFlags(cc *cobra.Command, c *client.Client) { 233 | var whatToSay string 234 | cc.Flags().StringVar(&cmd.WhatToSay, "whatToSay", whatToSay, `What To Say Hello To`) 235 | } 236 | -------------------------------------------------------------------------------- /tool/helloserverlessgoa-cli/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | goaclient "github.com/goadesign/goa/client" 6 | "github.com/spf13/cobra" 7 | "github.com/tleyden/serverless-forms/goa-generated/client" 8 | "github.com/tleyden/serverless-forms/goa-generated/tool/cli" 9 | "net/http" 10 | "os" 11 | "time" 12 | ) 13 | 14 | func main() { 15 | // Create command line parser 16 | app := &cobra.Command{ 17 | Use: "HelloServerlessGoa-cli", 18 | Short: `CLI client for the HelloServerlessGoa service`, 19 | } 20 | 21 | // Create client struct 22 | httpClient := newHTTPClient() 23 | c := client.New(goaclient.HTTPClientDoer(httpClient)) 24 | 25 | // Register global flags 26 | app.PersistentFlags().StringVarP(&c.Scheme, "scheme", "s", "", "Set the requests scheme") 27 | app.PersistentFlags().StringVarP(&c.Host, "host", "H", "localhost:8080", "API hostname") 28 | app.PersistentFlags().DurationVarP(&httpClient.Timeout, "timeout", "t", time.Duration(20)*time.Second, "Set the request timeout") 29 | app.PersistentFlags().BoolVar(&c.Dump, "dump", false, "Dump HTTP request and response.") 30 | 31 | // Initialize API client 32 | c.UserAgent = "HelloServerlessGoa-cli/0" 33 | 34 | // Register API commands 35 | cli.RegisterCommands(app, c) 36 | 37 | // Execute! 38 | if err := app.Execute(); err != nil { 39 | fmt.Fprintf(os.Stderr, err.Error()) 40 | os.Exit(-1) 41 | } 42 | } 43 | 44 | // newHTTPClient returns the HTTP client used by the API client to make requests to the service. 45 | func newHTTPClient() *http.Client { 46 | // TBD: Change as needed (e.g. to use a different transport to control redirection policy or 47 | // disable cert validation or...) 48 | return http.DefaultClient 49 | } 50 | --------------------------------------------------------------------------------