1
0
Fork 0

Merge pull request #1 from atomaka/feature/serverless

Go Serverless
This commit is contained in:
Andrew Tomaka 2017-03-23 18:44:36 -04:00 committed by GitHub
commit 31c6e7966e
11 changed files with 2732 additions and 169 deletions

3
.env.sample Normal file
View File

@ -0,0 +1,3 @@
export AWS_CERTIFICATE=arn:aws:acm:REGION:ACCOUNT:certificate/HASH
export DNS_ZONE=example.com.
export DOMAIN=puns.example.com

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
node_modules/
.serverless/
.env
.env.production

View File

@ -1,25 +0,0 @@
stages:
- build
- deploy
build:
stage: build
script:
- docker build -t atomaka/punaday-api .
except:
- tags
tags:
- docker
deploy:
stage: deploy
script:
- VERSION=$(git describe --tags)
- docker build -t atomaka/punaday-api .
- docker tag atomaka/punaday-api:latest docker.atomaka.com/atomaka/punaday-api:$VERSION
- docker tag atomaka/punaday-api:latest docker.atomaka.com/atomaka/punaday-api:latest
- docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD -e me@atomaka.com docker.atomaka.com
- docker push docker.atomaka.com/atomaka/punaday-api
only:
- tags
tags:
- docker

View File

@ -1,12 +0,0 @@
FROM alpine:latest
COPY . /go/src/github.com/atomaka/punaday-api
RUN apk update \
&& apk add --no-cache go git \
&& cd /go/src/github.com/atomaka/punaday-api \
&& export GOPATH=/go \
&& go get \
&& go build -o /bin/punaday-api \
&& rm -rf /go \
&& apk del --purge git go \
&& rm -rf /var/cache/apk*
ENTRYPOINT ["/bin/punaday-api"]

4
Makefile Normal file
View File

@ -0,0 +1,4 @@
yarnbin = $(shell yarn bin)
deploy:
sh -c "source .env.production; $(yarnbin)/serverless deploy --stage production"

20
README.md Normal file
View File

@ -0,0 +1,20 @@
# punaday-api
JSON version of http://www.punoftheday.com/
## Description
Server that takes Pun of the Day puns and converts it into JSON. Currently
living as a serverless application in AWS Lamda. It is frontended by AWS
Cloudfront because AWS Cloudformation does not yet have resources for AWS API
Gateway domains. This script will also create a DNS record to point at the
AWS Cloudfront Distribution.
## Usage
* `cp .env.sample .env.production`
* `AWS_CERTIFICATE`: arn to aws certificate that you generated for a domain
* `DNS_ZONE`: Route 53 zone name (ex. `example.com.`)
* `DOMAIN`: Route 53 domain (ex. `puns.example.com`)
* Set AWS credentials via environment or credential file
* `make`

132
main.go
View File

@ -1,132 +0,0 @@
package main
import (
"encoding/json"
"log"
"net/http"
"strconv"
"strings"
"golang.org/x/net/html"
)
const PUN_BASE = "http://www.punoftheday.com"
const RANDOM_PUN = PUN_BASE + "/cgi-bin/randompun.pl"
const SELECT_PUN = PUN_BASE + "/pun"
type Pun struct {
Id int `json:"-"`
Url string `json:"url"`
Text string `json:"text"`
}
type Error struct {
Status int `json:"-"`
Detail string `json:"error"`
}
func main() {
http.HandleFunc("/puns/today", TodayPun)
http.HandleFunc("/puns/random", RandomPun)
http.HandleFunc("/puns/", ShowPun)
http.HandleFunc("/", NotFound)
log.Fatal(http.ListenAndServe(":8080", nil))
}
func NotFound(w http.ResponseWriter, r *http.Request) {
log.Printf("404 Not Found: %v\n", r.URL.Path)
w.Header().Set("Content-Type", "application/json; charset=utf-8")
err := &Error{404, "not_found"}
w.WriteHeader(err.Status)
json.NewEncoder(w).Encode(err)
}
func WritePun(w http.ResponseWriter, r *http.Request, pun Pun) {
log.Printf("200 OK: %v\n", r.URL.Path)
w.Header().Set("Content-Type", "application/json; charset=utf-8")
json.NewEncoder(w).Encode(pun)
}
func ShowPun(w http.ResponseWriter, r *http.Request) {
id := r.URL.Path[len("/puns/"):]
pun := getPun(SELECT_PUN + "/" + id)
if id != strconv.Itoa(pun.Id) {
NotFound(w, r)
} else {
WritePun(w, r, pun)
}
}
func RandomPun(w http.ResponseWriter, r *http.Request) {
pun := getPun(RANDOM_PUN)
WritePun(w, r, pun)
}
func TodayPun(w http.ResponseWriter, r *http.Request) {
pun := getPun(PUN_BASE)
pun.Text = stripQuotes(pun.Text)
WritePun(w, r, pun)
}
func stripQuotes(punText string) string {
punText = strings.Replace(punText, "“", "", 1)
punText = strings.Replace(punText, "”", "", 1)
return punText
}
func getPun(url string) Pun {
resp, err := http.Get(url)
if err != nil {
log.Println("ERROR: Unable to access " + url)
}
b := resp.Body
defer b.Close()
pun := Pun{}
z := html.NewTokenizer(b)
for {
tt := z.Next()
switch {
case tt == html.ErrorToken:
return pun
case tt == html.StartTagToken:
t := z.Token()
isParagraph := t.Data == "p"
isInput := t.Data == "input"
if isParagraph && pun.Text == "" {
z.Next()
t := z.Token()
pun.Text = t.Data
}
if isInput {
if getAttr("name", t) == "PunID" {
pun.Id, _ = strconv.Atoi(getAttr("value", t))
pun.Url = SELECT_PUN + "/" + strconv.Itoa(pun.Id)
}
}
}
}
}
func getAttr(at string, t html.Token) string {
var val string
for _, a := range t.Attr {
if a.Key == at {
val = a.Val
}
}
return val
}

11
package.json Normal file
View File

@ -0,0 +1,11 @@
{
"devDependencies": {
"babel-preset-es2015": "^6.24.0",
"serverless": "^1.9.0",
"serverless-offline": "^3.10.3",
"serverless-plugin-optimize": "^1.0.0-rc.15"
},
"dependencies": {
"axios": "^0.15.3"
}
}

82
serverless.yml Normal file
View File

@ -0,0 +1,82 @@
service: punaday-api
provider:
name: aws
runtime: nodejs4.3
memorySize: 128
plugins:
- serverless-plugin-optimize
- serverless-offline
package:
individually: true
resources:
Resources:
CloudfrontDistribution:
Type: AWS::CloudFront::Distribution
Properties:
DistributionConfig:
Aliases:
- ${env:DOMAIN}
Origins:
- DomainName: { "Fn::Join": [".", [ { "Ref": "ApiGatewayRestApi" }, "execute-api", { "Ref": "AWS::Region" }, "amazonaws.com"] ] }
OriginPath: "/production"
Id: APIGW
CustomOriginConfig:
HTTPSPort: 443
OriginProtocolPolicy: https-only
DefaultCacheBehavior:
AllowedMethods:
- HEAD
- DELETE
- POST
- GET
- OPTIONS
- PUT
- PATCH
TargetOriginId: APIGW
ForwardedValues:
QueryString: true
Cookies:
Forward: none
ViewerProtocolPolicy: redirect-to-https
DefaultTTL: 0
Enabled: true
ViewerCertificate:
AcmCertificateArn: ${env:AWS_CERTIFICATE}
SslSupportMethod: sni-only
Route53RecordSet:
Type: AWS::Route53::RecordSetGroup
Properties:
HostedZoneName: ${env:DNS_ZONE}
RecordSets:
- Name: ${env:DOMAIN}
Type: A
AliasTarget:
DNSName: { "Fn::GetAtt" : [ "CloudfrontDistribution", "DomainName"] }
HostedZoneId: Z2FDTNDATAQYW2 # always use for cloudfront
functions:
punsShow:
handler: src/puns/show.show
events:
- http:
path: puns/{id}
method: get
cors: true
punsToday:
handler: src/puns/show.today
events:
- http:
path: puns/today
method: get
cors: true
punsRandom:
handler: src/puns/show.random
events:
- http:
path: puns/random
method: get
cors: true

53
src/puns/show.js Normal file
View File

@ -0,0 +1,53 @@
const axios = require('axios')
const PUN_BASE = 'http://www.punoftheday.com'
module.exports.today = ((event, context, callback) => {
respondWithPunFrom(PUN_BASE, callback)
})
module.exports.random = ((event, context, callback) => {
respondWithPunFrom(`${PUN_BASE}/cgi-bin/randompun.pl`, callback)
})
module.exports.show = ((event, context, callback) => {
const id = event.pathParameters.id
respondWithPunFrom(`${PUN_BASE}/pun/${id}`, callback)
})
function respondWithPunFrom(url, callback) {
return axios.get(url)
.then(response => {
const pun = parsePun(response.data)
callback(null, punResponse(pun))
})
.catch(error => {
callback(null, errorResponse())
})
}
function parsePun(html) {
const punMatches = html.match(/<p>(.*)<\/p>/)
const text = punMatches[1].replace('&#8220;', '').replace('&#8221;', '')
const idMatches = html.match(/name="PunID" value="(\d+)"/)
const id = idMatches[1]
const urlMatches = html.match(/class="fb-share-button" data-href="(.*)" d/)
const url = urlMatches[1]
return { id, text, url }
}
function punResponse (pun) {
delete pun['id']
return {
statusCode: 200,
body: JSON.stringify(pun)
}
}
function errorResponse () {
return { statusCode: 500 }
}

2555
yarn.lock Normal file

File diff suppressed because it is too large Load Diff