aws provider

This commit is contained in:
David Dollar 2019-08-21 10:51:28 -04:00
parent 51b57668ed
commit c1774a5f27
No known key found for this signature in database
GPG Key ID: AFAF263FB45B2124
210 changed files with 91095 additions and 35 deletions

7
go.mod
View File

@ -4,6 +4,7 @@ go 1.12
require (
github.com/Microsoft/hcsshim v0.8.7-0.20190801035247-8694eade7dd3 // indirect
github.com/PuerkitoBio/goquery v1.5.0
github.com/aws/aws-sdk-go v1.21.10
github.com/convox/logger v0.0.0-20180522214415-e39179955b52
github.com/convox/stdapi v0.0.0-20190708203955-b81b71b6a680
@ -13,6 +14,7 @@ require (
github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96 // indirect
github.com/dustin/go-humanize v1.0.0
github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e // indirect
github.com/fatih/color v1.7.0 // indirect
github.com/fsouza/go-dockerclient v1.4.2
github.com/gobuffalo/packr v1.30.1
github.com/gobwas/glob v0.2.3
@ -22,9 +24,13 @@ require (
github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d // indirect
github.com/gorilla/websocket v1.4.0
github.com/gregjones/httpcache v0.0.0-20170728041850-787624de3eb7 // indirect
github.com/headzoo/surf v1.0.0
github.com/headzoo/ut v0.0.0-20181013193318-a13b5a7a02ca // indirect
github.com/imdario/mergo v0.3.7 // indirect
github.com/json-iterator/go v1.1.7 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/mattn/go-colorable v0.1.2 // indirect
github.com/mattn/go-runewidth v0.0.4 // indirect
github.com/miekg/dns v1.1.15
github.com/modern-go/reflect2 v1.0.1 // indirect
github.com/onsi/ginkgo v1.8.0 // indirect
@ -37,6 +43,7 @@ require (
golang.org/x/net v0.0.0-20190812203447-cdfb69ac37fc // indirect
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 // indirect
google.golang.org/appengine v1.4.0 // indirect
gopkg.in/cheggaaa/pb.v1 v1.0.28
gopkg.in/inf.v0 v0.9.0 // indirect
gopkg.in/yaml.v2 v2.2.2
k8s.io/api v0.0.0-20190118113203-912cbe2bfef3

19
go.sum
View File

@ -11,9 +11,13 @@ github.com/Microsoft/hcsshim v0.8.6/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXn
github.com/Microsoft/hcsshim v0.8.7-0.20190801035247-8694eade7dd3 h1:agmzOP0aQ42FpfjsjcI4cMDbT1GFCHFVYlv7zprTBh4=
github.com/Microsoft/hcsshim v0.8.7-0.20190801035247-8694eade7dd3/go.mod h1:8OYooOlLyAIH/z8IDfdQH0L58ksBjNzsisBpmW2vg0Y=
github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
github.com/PuerkitoBio/goquery v1.5.0 h1:uGvmFXOA73IKluu/F84Xd1tt/z07GYm8X49XKHP7EJk=
github.com/PuerkitoBio/goquery v1.5.0/go.mod h1:qD2PgZ9lccMbQlc7eEOjaeRlFQON7xY8kdmcsrnKqMg=
github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/ajg/form v0.0.0-20160822230020-523a5da1a92f/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
github.com/andybalholm/cascadia v1.0.0 h1:hOCXnnZ5A+3eVDX8pvgl4kofXv2ELss0bKcqRySc45o=
github.com/andybalholm/cascadia v1.0.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/aws/aws-sdk-go v1.21.10 h1:lTRdgyxraKbnNhx7kWeoW/Uow1TKnSNDpQGTtEXJQgk=
github.com/aws/aws-sdk-go v1.21.10/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
@ -64,6 +68,7 @@ github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25Kn
github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e h1:p1yVGRW3nmb85p1Sh1ZJSDm4A4iKLS5QNbvUHMgGu/M=
github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/structs v1.0.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
@ -303,6 +308,10 @@ github.com/hashicorp/go-multierror v0.0.0-20161216184304-ed905158d874/go.mod h1:
github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/headzoo/surf v1.0.0 h1:d2h9ftKeQYj7tKqAjQtAA0lJVkO8cTxvzdXLynmNnHM=
github.com/headzoo/surf v1.0.0/go.mod h1:/bct0m/iMNEqpn520y01yoaWxsAEigGFPnvyR1ewR5M=
github.com/headzoo/ut v0.0.0-20181013193318-a13b5a7a02ca h1:utFgFwgxaqx5OthzE3DSGrtOq7rox5r2sxZ2wbfTuK0=
github.com/headzoo/ut v0.0.0-20181013193318-a13b5a7a02ca/go.mod h1:8926sG02TCOX4RFRzIMFIzRw4xuc/TwO2gtN7teMJZ4=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/ijc/Gotty v0.0.0-20170406111628-a8b993ba6abd h1:anPrsicrIi2ColgWTVPk+TrN42hJIWlfPHSBP9S0ZkM=
@ -363,7 +372,13 @@ github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kN
github.com/markbates/sigtx v1.0.0/go.mod h1:QF1Hv6Ic6Ca6W+T+DL0Y/ypborFKyvUY9HmuCD4VeTc=
github.com/markbates/willie v1.0.9/go.mod h1:fsrFVWl91+gXpx/6dv715j7i11fYPfZ9ZGfH0DQzY7w=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc=
@ -499,6 +514,7 @@ golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTk
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180816102801-aaf60122140d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -549,6 +565,7 @@ golang.org/x/sys v0.0.0-20181206074257-70b957f3b65e/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20190102155601-82a175fd1598/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190116161447-11f53e031339/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -610,6 +627,8 @@ gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/cheggaaa/pb.v1 v1.0.28 h1:n1tBJnnK2r7g9OW2btFH91V92STTUevLXYFb8gy9EMk=
gopkg.in/cheggaaa/pb.v1 v1.0.28/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=

View File

@ -2,7 +2,7 @@ all: generate
generate:
# needs k8s.io/code-generator@release-1.11
env $(GOPATH)/src/k8s.io/code-generator/generate-groups.sh all \
$(GOPATH)/src/k8s.io/code-generator/generate-groups.sh all \
github.com/convox/convox/pkg/atom/pkg/client \
github.com/convox/convox/pkg/atom/pkg/apis \
atom:v1

547
pkg/common/aws.go Normal file
View File

@ -0,0 +1,547 @@
package common
import (
"context"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"os/exec"
"sort"
"strings"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/cloudformation"
"github.com/aws/aws-sdk-go/service/cloudformation/cloudformationiface"
"github.com/aws/aws-sdk-go/service/cloudwatchlogs"
"github.com/aws/aws-sdk-go/service/cloudwatchlogs/cloudwatchlogsiface"
"github.com/convox/convox/pkg/structs"
yaml "gopkg.in/yaml.v2"
)
func AwsCredentialsLoad() error {
if os.Getenv("AWS_ACCESS_KEY_ID") == "" {
if err := exec.Command("which", "aws").Run(); err != nil {
return fmt.Errorf("unable to find aws executable in path")
}
data, err := awscli("iam", "get-account-summary")
if err != nil {
lines := strings.Split(strings.TrimSpace(string(data)), "\n")
return fmt.Errorf("aws cli error: %s", lines[len(lines)-1])
}
env, err := setupCredentialsStatic()
if err != nil {
return err
}
if env["AWS_ACCESS_KEY_ID"] == "" {
env, err = setupCredentialsRole()
if err != nil {
return err
}
}
if env["AWS_ACCESS_KEY_ID"] == "" {
return fmt.Errorf("unable to load credentials from aws cli")
}
for k, v := range env {
os.Setenv(k, v)
}
}
if os.Getenv("AWS_REGION") == "" {
os.Setenv("AWS_REGION", "us-east-1")
}
return nil
}
func AwsErrorCode(err error) string {
if ae, ok := err.(awserr.Error); ok {
return ae.Code()
}
return ""
}
func CloudformationDescribe(cf cloudformationiface.CloudFormationAPI, stack string) (*cloudformation.Stack, error) {
req := &cloudformation.DescribeStacksInput{
StackName: aws.String(stack),
}
res, err := cf.DescribeStacks(req)
if err != nil {
return nil, err
}
if len(res.Stacks) != 1 {
return nil, fmt.Errorf("stack not found: %s", stack)
}
return res.Stacks[0], nil
}
func CloudformationInstall(cf cloudformationiface.CloudFormationAPI, name, template string, params, tags map[string]string, cb func(int, int)) error {
req := &cloudformation.CreateChangeSetInput{
Capabilities: []*string{aws.String("CAPABILITY_IAM")},
ChangeSetName: aws.String("init"),
ChangeSetType: aws.String("CREATE"),
// ClientRequestToken: aws.String(token),
Parameters: []*cloudformation.Parameter{},
StackName: aws.String(name),
Tags: []*cloudformation.Tag{},
TemplateURL: aws.String(template),
}
for k, v := range params {
req.Parameters = append(req.Parameters, &cloudformation.Parameter{
ParameterKey: aws.String(k),
ParameterValue: aws.String(v),
})
}
for k, v := range tags {
req.Tags = append(req.Tags, &cloudformation.Tag{
Key: aws.String(k),
Value: aws.String(v),
})
}
cres, err := cf.CreateChangeSet(req)
if err != nil {
return err
}
dreq := &cloudformation.DescribeChangeSetInput{
ChangeSetName: aws.String("init"),
StackName: cres.StackId,
}
if err := cf.WaitUntilChangeSetCreateComplete(dreq); err != nil {
return err
}
dres, err := cf.DescribeChangeSet(dreq)
if err != nil {
return err
}
total := len(dres.Changes)
cb(0, total)
token, err := RandomString(20)
if err != nil {
return err
}
ereq := &cloudformation.ExecuteChangeSetInput{
ChangeSetName: aws.String("init"),
ClientRequestToken: aws.String(token),
StackName: cres.StackId,
}
if _, err := cf.ExecuteChangeSet(ereq); err != nil {
return err
}
for {
time.Sleep(10 * time.Second)
res, err := cf.DescribeStacks(&cloudformation.DescribeStacksInput{
StackName: cres.StackId,
})
if err != nil {
return err
}
if len(res.Stacks) != 1 {
return fmt.Errorf("could not find stack: %s\n", *cres.StackId)
}
s := res.Stacks[0]
switch *s.StackStatus {
case "CREATE_FAILED", "DELETE_COMPLETE", "DELETE_FAILED", "DELETE_IN_PROGRESS", "ROLLBACK_COMPLETE", "ROLLBACK_FAILED", "ROLLBACK_IN_PROGRESS":
return fmt.Errorf("installation failed")
case "CREATE_COMPLETE":
return nil
}
rres, err := cf.DescribeStackResources(&cloudformation.DescribeStackResourcesInput{
StackName: aws.String(name),
})
if err != nil {
return err
}
current := 0
for _, r := range rres.StackResources {
if *r.ResourceStatus == "CREATE_COMPLETE" {
current += 1
}
}
cb(current, total)
}
return nil
}
func CloudformationParameters(template []byte) (map[string]bool, error) {
var f struct {
Parameters map[string]interface{} `yaml:"Parameters"`
}
if err := yaml.Unmarshal(template, &f); err != nil {
return nil, err
}
ps := map[string]bool{}
for p := range f.Parameters {
ps[p] = true
}
return ps, nil
}
func CloudformationUninstall(cf cloudformationiface.CloudFormationAPI, stack string) error {
_, err := cf.DeleteStack(&cloudformation.DeleteStackInput{
StackName: aws.String(stack),
})
if err != nil {
return err
}
req := &cloudformation.DescribeStacksInput{
StackName: aws.String(stack),
}
if err := cf.WaitUntilStackDeleteComplete(req); err != nil {
return err
}
return nil
}
func CloudformationUpdate(cf cloudformationiface.CloudFormationAPI, stack string, template string, changes map[string]string, tags map[string]string, topic string) error {
req := &cloudformation.UpdateStackInput{
Capabilities: []*string{aws.String("CAPABILITY_IAM")},
NotificationARNs: []*string{aws.String(topic)},
StackName: aws.String(stack),
}
params := map[string]bool{}
pexisting := map[string]bool{}
s, err := CloudformationDescribe(cf, stack)
if err != nil {
return err
}
for _, p := range s.Parameters {
pexisting[*p.ParameterKey] = true
}
if template == "" {
req.UsePreviousTemplate = aws.Bool(true)
for param := range pexisting {
params[param] = true
}
} else {
req.TemplateURL = aws.String(template)
res, err := http.Get(template)
if err != nil {
return err
}
defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body)
if err != nil {
return err
}
fp, err := CloudformationParameters(body)
if err != nil {
return err
}
for p := range fp {
params[p] = true
}
}
sorted := []string{}
for param := range params {
sorted = append(sorted, param)
}
// sort params for easier testing
sort.Strings(sorted)
for _, param := range sorted {
if value, ok := changes[param]; ok {
req.Parameters = append(req.Parameters, &cloudformation.Parameter{
ParameterKey: aws.String(param),
ParameterValue: aws.String(value),
})
} else if pexisting[param] {
req.Parameters = append(req.Parameters, &cloudformation.Parameter{
ParameterKey: aws.String(param),
UsePreviousValue: aws.Bool(true),
})
}
}
req.Tags = s.Tags
tks := []string{}
for key := range tags {
tks = append(tks, key)
}
sort.Strings(tks)
for _, key := range tks {
req.Tags = append(req.Tags, &cloudformation.Tag{
Key: aws.String(key),
Value: aws.String(tags[key]),
})
}
if _, err := cf.UpdateStack(req); err != nil {
return err
}
// dreq := &cloudformation.DescribeStacksInput{
// StackName: aws.String(stack),
// }
// if err := cf.WaitUntilStackUpdateComplete(dreq); err != nil {
// return err
// }
return nil
}
func CloudWatchLogsSubscribe(ctx context.Context, cw cloudwatchlogsiface.CloudWatchLogsAPI, group, stream string, opts structs.LogsOptions) (io.ReadCloser, error) {
r, w := io.Pipe()
go CloudWatchLogsStream(ctx, cw, w, group, stream, opts)
return r, nil
}
func CloudWatchLogsStream(ctx context.Context, cw cloudwatchlogsiface.CloudWatchLogsAPI, w io.WriteCloser, group, stream string, opts structs.LogsOptions) error {
defer w.Close()
req := &cloudwatchlogs.FilterLogEventsInput{
LogGroupName: aws.String(group),
}
if stream != "" {
req.LogStreamNames = []*string{aws.String(stream)}
} else {
req.Interleaved = aws.Bool(true)
}
if opts.Filter != nil {
req.FilterPattern = aws.String(*opts.Filter)
}
var start int64
if opts.Since != nil {
start = time.Now().UTC().Add((*opts.Since)*-1).UnixNano() / int64(time.Millisecond)
req.StartTime = aws.Int64(start)
}
var seen = map[string]bool{}
sleep := time.Duration(100 * time.Millisecond)
for {
select {
case <-ctx.Done():
return nil
default:
time.Sleep(sleep)
// check for closed writer
if _, err := w.Write([]byte{}); err != nil {
return err
}
res, err := cw.FilterLogEvents(req)
if err != nil {
switch AwsErrorCode(err) {
case "ThrottlingException", "ResourceNotFoundException":
time.Sleep(1 * time.Second)
continue
default:
return err
}
}
es := []*cloudwatchlogs.FilteredLogEvent{}
for _, e := range res.Events {
if *e.Timestamp > start {
start = *e.Timestamp + 1
}
if !seen[*e.EventId] {
es = append(es, e)
}
}
seen = map[string]bool{}
for _, e := range res.Events {
seen[*e.EventId] = true
}
if len(es) > 0 {
sleep = time.Duration(100 * time.Millisecond)
} else if sleep < 5*time.Second {
sleep *= 2
}
sort.Slice(es, func(i, j int) bool { return *es[i].Timestamp < *es[j].Timestamp })
if _, err := writeLogEvents(w, es, opts); err != nil {
return err
}
if res.NextToken != nil {
req.NextToken = res.NextToken
continue
}
req.NextToken = nil
if !DefaultBool(opts.Follow, true) {
return nil
}
req.StartTime = aws.Int64(start)
}
}
}
func awscli(args ...string) ([]byte, error) {
return exec.Command("aws", args...).CombinedOutput()
}
func setupCredentialsStatic() (map[string]string, error) {
rb, err := awscli("configure", "get", "region")
if err != nil {
return map[string]string{}, nil
}
ab, err := awscli("configure", "get", "aws_access_key_id")
if err != nil {
return map[string]string{}, nil
}
sb, err := awscli("configure", "get", "aws_secret_access_key")
if err != nil {
return map[string]string{}, nil
}
env := map[string]string{
"AWS_REGION": strings.TrimSpace(string(rb)),
"AWS_ACCESS_KEY_ID": strings.TrimSpace(string(ab)),
"AWS_SECRET_ACCESS_KEY": strings.TrimSpace(string(sb)),
}
return env, nil
}
func setupCredentialsRole() (map[string]string, error) {
rb, err := awscli("configure", "get", "role_arn")
if err != nil {
return nil, err
}
role := strings.TrimSpace(string(rb))
if role == "" {
return map[string]string{}, nil
}
data, err := awscli("sts", "assume-role", "--role-arn", role, "--role-session-name", "convox-cli")
if err != nil {
return nil, err
}
var creds struct {
Credentials struct {
AccessKeyID string `json:"AccessKeyId"`
SecretAccessKey string
SessionToken string
}
}
if err := json.Unmarshal(data, &creds); err != nil {
return nil, err
}
rgb, err := awscli("configure", "get", "region")
if err != nil {
return map[string]string{}, nil
}
env := map[string]string{
"AWS_REGION": strings.TrimSpace(string(rgb)),
"AWS_ACCESS_KEY_ID": creds.Credentials.AccessKeyID,
"AWS_SECRET_ACCESS_KEY": creds.Credentials.SecretAccessKey,
"AWS_SESSION_TOKEN": creds.Credentials.SessionToken,
}
return env, nil
}
func writeLogEvents(w io.Writer, events []*cloudwatchlogs.FilteredLogEvent, opts structs.LogsOptions) (int64, error) {
if len(events) == 0 {
return 0, nil
}
// sort.Slice(events, func(i, j int) bool { return *events[i].Timestamp < *events[j].Timestamp })
latest := int64(0)
for _, e := range events {
if *e.Timestamp > latest {
latest = *e.Timestamp
}
prefix := ""
if DefaultBool(opts.Prefix, false) {
sec := *e.Timestamp / 1000
nsec := (*e.Timestamp % 1000) * 1000
t := time.Unix(sec, nsec).UTC()
prefix = fmt.Sprintf("%s %s ", t.Format(time.RFC3339), *e.LogStreamName)
}
line := fmt.Sprintf("%s%s\n", prefix, *e.Message)
if _, err := w.Write([]byte(line)); err != nil {
return 0, err
}
}
return latest, nil
}

13
provider/aws/Makefile Normal file
View File

@ -0,0 +1,13 @@
all: all build generate release
all: build
build:
go install .
generate:
# needs k8s.io/code-generator@release-1.11
$(GOPATH)/src/k8s.io/code-generator/generate-groups.sh all \
github.com/convox/convox/provider/aws/pkg/client \
github.com/convox/convox/provider/aws/pkg/apis \
convox:v1

67
provider/aws/app.go Normal file
View File

@ -0,0 +1,67 @@
package aws
import (
"fmt"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ecr"
"github.com/convox/convox/pkg/common"
"github.com/convox/convox/pkg/structs"
am "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func (p *Provider) AppCreate(name string, opts structs.AppCreateOptions) (*structs.App, error) {
a, err := p.Provider.AppCreate(name, opts)
if err != nil {
return nil, err
}
res, err := p.ECR.CreateRepository(&ecr.CreateRepositoryInput{
RepositoryName: aws.String(fmt.Sprintf("%s/%s", p.Name, name)),
})
if err != nil {
return nil, err
}
ns, err := p.Provider.Cluster.CoreV1().Namespaces().Get(p.AppNamespace(name), am.GetOptions{})
if err != nil {
return nil, err
}
if ns.ObjectMeta.Annotations == nil {
ns.ObjectMeta.Annotations = map[string]string{}
}
ns.ObjectMeta.Annotations["convox.registry"] = *res.Repository.RepositoryUri
if _, err := p.Provider.Cluster.CoreV1().Namespaces().Update(ns); err != nil {
return nil, err
}
return a, nil
}
func (p *Provider) AppDelete(name string) error {
_, err := p.ECR.DeleteRepository(&ecr.DeleteRepositoryInput{
Force: aws.Bool(true),
RepositoryName: aws.String(fmt.Sprintf("%s/%s", p.Name, name)),
})
if err != nil {
switch common.AwsErrorCode(err) {
case "RepositoryNotFoundException":
default:
return err
}
}
return p.Provider.AppDelete(name)
}
func (p *Provider) AppIdles(name string) (bool, error) {
return false, nil
}
func (p *Provider) AppStatus(name string) (string, error) {
return "running", nil
}

197
provider/aws/aws.go Normal file
View File

@ -0,0 +1,197 @@
package aws
import (
"context"
"os"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/cloudformation"
"github.com/aws/aws-sdk-go/service/cloudformation/cloudformationiface"
"github.com/aws/aws-sdk-go/service/cloudwatchlogs"
"github.com/aws/aws-sdk-go/service/cloudwatchlogs/cloudwatchlogsiface"
"github.com/aws/aws-sdk-go/service/ecr"
"github.com/aws/aws-sdk-go/service/ecr/ecriface"
"github.com/aws/aws-sdk-go/service/s3"
"github.com/aws/aws-sdk-go/service/s3/s3iface"
"github.com/aws/aws-sdk-go/service/sqs"
"github.com/aws/aws-sdk-go/service/sqs/sqsiface"
"github.com/convox/convox/pkg/structs"
"github.com/convox/convox/pkg/templater"
"github.com/convox/convox/provider/k8s"
"github.com/gobuffalo/packr"
"k8s.io/apimachinery/pkg/util/runtime"
)
type Provider struct {
*k8s.Provider
// AccountId string
// AdminUser string
// AutoscalerRole string
// BalancerSecurity string
Bucket string
// Cluster string
// Domain string
// EventQueue string
// EventTopic string
// NodesRole string
Region string
// RackRole string
// RouterCache string
// RouterHosts string
// RouterRole string
// RouterTargets string
// RouterTargetGroup80 string
// RouterTargetGroup443 string
// StackId string
// SubnetsPublic []string
// SubnetsPrivate []string
// Vpc string
CloudFormation cloudformationiface.CloudFormationAPI
CloudWatchLogs cloudwatchlogsiface.CloudWatchLogsAPI
ECR ecriface.ECRAPI
S3 s3iface.S3API
SQS sqsiface.SQSAPI
templater *templater.Templater
}
func FromEnv() (*Provider, error) {
k, err := k8s.FromEnv()
if err != nil {
return nil, err
}
p := &Provider{
Provider: k,
Bucket: os.Getenv("BUCKET"),
// Domain: os.Getenv("DOMAIN"),
Region: os.Getenv("AWS_REGION"),
}
p.templater = templater.New(packr.NewBox("../aws/template"), p.templateHelpers())
k.Engine = p
return p, nil
}
// func FromEnv() (*Provider, error) {
// kp, err := k8s.FromEnv()
// if err != nil {
// return nil, err
// }
// p := &Provider{
// Provider: kp,
// Region: os.Getenv("AWS_REGION"),
// BaseDomain: "example.org",
// }
// p.templater = templater.New(packr.NewBox("../kaws/template"), p.templateHelpers())
// kp.Engine = p
// return p, nil
// }
func (p *Provider) Initialize(opts structs.ProviderOptions) error {
if err := p.initializeAwsServices(); err != nil {
return err
}
// os, err := p.stackOutputs(p.Name)
// if err != nil {
// return err
// }
// p.applyOutputs(os)
// if err := p.initializeIamRoles(); err != nil {
// return err
// }
if err := p.Provider.Initialize(opts); err != nil {
return err
}
runtime.ErrorHandlers = []func(error){}
// nc, err := NewNodeController(p)
// if err != nil {
// return err
// }
// stc, err := NewStackController(p)
// if err != nil {
// return err
// }
// go nc.Run()
// go stc.Run()
// go p.workerEvents()
return nil
}
func (p *Provider) WithContext(ctx context.Context) structs.Provider {
pp := *p
pp.Provider = pp.Provider.WithContext(ctx).(*k8s.Provider)
return &pp
}
// func (p *Provider) applyOutputs(outputs map[string]string) {
// p.Provider.Socket = "/var/run/docker.sock"
// p.Provider.Version = outputs["Version"]
// // p.AccountId = outputs["AccountId"]
// // p.AdminUser = outputs["AdminUser"]
// // p.AutoscalerRole = outputs["AutoscalerRole"]
// // p.BalancerSecurity = outputs["BalancerSecurity"]
// // p.BaseDomain = outputs["BaseDomain"]
// p.Bucket = outputs["RackBucket"]
// // p.Cluster = outputs["Cluster"]
// // p.EventQueue = outputs["EventQueue"]
// // p.EventTopic = outputs["EventTopic"]
// // p.NodesRole = outputs["NodesRole"]
// // p.RackRole = outputs["RackRole"]
// // p.RouterCache = outputs["RouterCache"]
// // p.RouterHosts = outputs["RouterHosts"]
// // p.RouterRole = outputs["RouterRole"]
// // p.RouterTargets = outputs["RouterTargets"]
// // p.RouterTargetGroup80 = outputs["RouterTargetGroup80"]
// // p.RouterTargetGroup443 = outputs["RouterTargetGroup443"]
// // p.StackId = outputs["StackId"]
// // p.SubnetsPublic = strings.Split(outputs["VpcPublicSubnets"], ",")
// // p.SubnetsPrivate = strings.Split(outputs["VpcPrivateSubnets"], ",")
// // p.Vpc = outputs["Vpc"]
// }
func (p *Provider) initializeAwsServices() error {
s, err := session.NewSession()
if err != nil {
return err
}
p.CloudFormation = cloudformation.New(s)
p.CloudWatchLogs = cloudwatchlogs.New(s)
p.ECR = ecr.New(s)
p.S3 = s3.New(s)
p.SQS = sqs.New(s)
return nil
}
// func (p *Provider) initializeIamRoles() error {
// if err := kubectl("patch", "deployment/api", "-n", p.Namespace, "-p", fmt.Sprintf(`{"spec":{"template":{"metadata":{"annotations":{"iam.amazonaws.com/role":%q}}}}}`, p.RackRole)); err != nil {
// return err
// }
// if err := kubectl("patch", "deployment/router", "-n", "convox-system", "-p", fmt.Sprintf(`{"spec":{"template":{"metadata":{"annotations":{"iam.amazonaws.com/role":%q}}}}}`, p.RouterRole)); err != nil {
// return err
// }
// return nil
// }

44
provider/aws/bin/cluster-config Executable file
View File

@ -0,0 +1,44 @@
#!/bin/sh
set -e
rack=$1
[ "$rack" == "" ] && echo "rack required" && exit 1
stack_output() {
aws cloudformation describe-stacks --stack-name $1 --query "Stacks[0].Outputs[?OutputKey==\`$2\`].OutputValue" --output text
}
cluster=$(stack_output $rack Cluster)
ca=$(stack_output $rack ClusterCertificateAuthority)
endpoint=$(stack_output $rack ClusterEndpoint)
go get -u github.com/kubernetes-sigs/aws-iam-authenticator/cmd/aws-iam-authenticator
cat > ~/.kube/config.$1.yml <<EOF
apiVersion: v1
clusters:
- cluster:
server: $endpoint
certificate-authority-data: $ca
name: kubernetes
contexts:
- context:
cluster: kubernetes
user: aws
name: aws
current-context: aws
kind: Config
preferences: {}
users:
- name: aws
user:
exec:
apiVersion: client.authentication.k8s.io/v1alpha1
command: aws-iam-authenticator
args:
- "token"
- "-i"
- $cluster
EOF

View File

@ -0,0 +1,6 @@
#!/bin/sh
aws cloudformation create-stack --stack-name $1 --template-body file://provider/kaws/formation/rack.yml --capabilities CAPABILITY_IAM \
--tags \
Key=system,Value=convox \
Key=rack,Value=$1 \

View File

@ -0,0 +1,493 @@
#!/bin/sh
set -e
rack=$1
[ "$rack" == "" ] && echo "rack required" && exit 1
stack_output() {
aws cloudformation describe-stacks --stack-name $1 --query "Stacks[0].Outputs[?OutputKey==\`$2\`].OutputValue" --output text
}
account=$(stack_output $rack AccountId)
autoscaler_role=$(stack_output $rack AutoscalerRole)
cluster=$(stack_output $rack Cluster)
region=$(stack_output $rack Region)
nodes_role=$(stack_output $rack NodesRole)
kubectl apply -f - <<EOF
EOF
# kube2iam
kubectl apply -f - <<EOF
EOF
# cluster-autoscaler
kubectl apply -f - <<EOF
---
EOF
# alb-ingress-controller
# kubectl apply -f - <<EOF
# ---
# apiVersion: rbac.authorization.k8s.io/v1
# kind: ClusterRole
# metadata:
# labels:
# app: alb-ingress-controller
# name: alb-ingress-controller
# rules:
# - apiGroups:
# - ""
# - extensions
# resources:
# - configmaps
# - endpoints
# - events
# - ingresses
# - ingresses/status
# - services
# verbs:
# - create
# - get
# - list
# - update
# - watch
# - patch
# - apiGroups:
# - ""
# - extensions
# resources:
# - nodes
# - pods
# - secrets
# - services
# - namespaces
# verbs:
# - get
# - list
# - watch
# ---
# apiVersion: rbac.authorization.k8s.io/v1
# kind: ClusterRoleBinding
# metadata:
# labels:
# app: alb-ingress-controller
# name: alb-ingress-controller
# roleRef:
# apiGroup: rbac.authorization.k8s.io
# kind: ClusterRole
# name: alb-ingress-controller
# subjects:
# - kind: ServiceAccount
# name: alb-ingress
# namespace: kube-system
# ---
# apiVersion: v1
# kind: ServiceAccount
# metadata:
# labels:
# app: alb-ingress-controller
# name: alb-ingress
# namespace: kube-system
# ---
# # Application Load Balancer (ALB) Ingress Controller Deployment Manifest.
# # This manifest details sensible defaults for deploying an ALB Ingress Controller.
# # GitHub: https://github.com/kubernetes-sigs/aws-alb-ingress-controller
# apiVersion: apps/v1
# kind: Deployment
# metadata:
# labels:
# app: alb-ingress-controller
# name: alb-ingress-controller
# # Namespace the ALB Ingress Controller should run in. Does not impact which
# # namespaces it's able to resolve ingress resource for. For limiting ingress
# # namespace scope, see --watch-namespace.
# namespace: kube-system
# spec:
# replicas: 1
# selector:
# matchLabels:
# app: alb-ingress-controller
# strategy:
# rollingUpdate:
# maxSurge: 1
# maxUnavailable: 1
# type: RollingUpdate
# template:
# metadata:
# creationTimestamp: null
# labels:
# app: alb-ingress-controller
# spec:
# # serviceAccountName: alb-ingress
# # Please provide serviceAccountName if you setup you k8s cluster with RBAC
# containers:
# - args:
# - /server
# # Limit the namespace where this ALB Ingress Controller deployment will
# # resolve ingress resources. If left commented, all namespaces are used.
# #- --watch-namespace=your-k8s-namespace
# # Setting the ingress-class flag below ensures that only ingress resources with the
# # annotation kubernetes.io/ingress.class: "alb" are respected by the controller. You may
# # choose any class you'd like for this controller to respect.
# - --ingress-class=alb
# env:
# # AWS region this ingress controller will operate in.
# # List of regions:
# # http://docs.aws.amazon.com/general/latest/gr/rande.html#vpc_region
# - name: AWS_REGION
# value: $region
# # Name of your cluster. Used when naming resources created
# # by the ALB Ingress Controller, providing distinction between
# # clusters.
# - name: CLUSTER_NAME
# value: $cluster
# # AWS key id for authenticating with the AWS API.
# # This is only here for examples. It's recommended you instead use
# # a project like kube2iam for granting access.
# - name: AWS_DEBUG
# value: "true"
# # Maximum number of times to retry the aws calls.
# # defaults to 20.
# - name: AWS_MAX_RETRIES
# value: "20"
# - name: POD_NAME
# valueFrom:
# fieldRef:
# apiVersion: v1
# fieldPath: metadata.name
# - name: POD_NAMESPACE
# valueFrom:
# fieldRef:
# apiVersion: v1
# fieldPath: metadata.namespace
# # Repository location of the ALB Ingress Controller.
# image: quay.io/coreos/alb-ingress-controller:1.0-beta.5
# imagePullPolicy: Always
# name: server
# resources: {}
# terminationMessagePath: /dev/termination-log
# dnsPolicy: ClusterFirst
# restartPolicy: Always
# securityContext: {}
# terminationGracePeriodSeconds: 30
# serviceAccountName: alb-ingress
# serviceAccount: alb-ingress
# EOF
# ingress-nginx
# kubectl apply -f - <<EOF
# apiVersion: v1
# kind: Namespace
# metadata:
# name: ingress-nginx
# ---
# apiVersion: extensions/v1beta1
# kind: Deployment
# metadata:
# name: default-http-backend
# labels:
# app: default-http-backend
# namespace: ingress-nginx
# spec:
# replicas: 1
# selector:
# matchLabels:
# app: default-http-backend
# template:
# metadata:
# labels:
# app: default-http-backend
# spec:
# terminationGracePeriodSeconds: 60
# containers:
# - name: default-http-backend
# # Any image is permissible as long as:
# # 1. It serves a 404 page at /
# # 2. It serves 200 on a /healthz endpoint
# image: gcr.io/google_containers/defaultbackend:1.4
# livenessProbe:
# httpGet:
# path: /healthz
# port: 8080
# scheme: HTTP
# initialDelaySeconds: 30
# timeoutSeconds: 5
# ports:
# - containerPort: 8080
# resources:
# limits:
# cpu: 10m
# memory: 20Mi
# requests:
# cpu: 10m
# memory: 20Mi
# ---
# apiVersion: v1
# kind: Service
# metadata:
# name: default-http-backend
# namespace: ingress-nginx
# labels:
# app: default-http-backend
# spec:
# ports:
# - port: 80
# targetPort: 8080
# selector:
# app: default-http-backend
# ---
# kind: ConfigMap
# apiVersion: v1
# metadata:
# name: nginx-configuration
# namespace: ingress-nginx
# labels:
# app: ingress-nginx
# ---
# kind: ConfigMap
# apiVersion: v1
# metadata:
# name: tcp-services
# namespace: ingress-nginx
# ---
# kind: ConfigMap
# apiVersion: v1
# metadata:
# name: udp-services
# namespace: ingress-nginx
# ---
# apiVersion: v1
# kind: ServiceAccount
# metadata:
# name: nginx-ingress-serviceaccount
# namespace: ingress-nginx
# ---
# apiVersion: rbac.authorization.k8s.io/v1beta1
# kind: ClusterRole
# metadata:
# name: nginx-ingress-clusterrole
# rules:
# - apiGroups:
# - ""
# resources:
# - configmaps
# - endpoints
# - nodes
# - pods
# - secrets
# verbs:
# - list
# - watch
# - apiGroups:
# - ""
# resources:
# - nodes
# verbs:
# - get
# - apiGroups:
# - ""
# resources:
# - services
# verbs:
# - get
# - list
# - watch
# - apiGroups:
# - "extensions"
# resources:
# - ingresses
# verbs:
# - get
# - list
# - watch
# - apiGroups:
# - ""
# resources:
# - events
# verbs:
# - create
# - patch
# - apiGroups:
# - "extensions"
# resources:
# - ingresses/status
# verbs:
# - update
# ---
# apiVersion: rbac.authorization.k8s.io/v1beta1
# kind: Role
# metadata:
# name: nginx-ingress-role
# namespace: ingress-nginx
# rules:
# - apiGroups:
# - ""
# resources:
# - configmaps
# - pods
# - secrets
# - namespaces
# verbs:
# - get
# - apiGroups:
# - ""
# resources:
# - configmaps
# resourceNames:
# # Defaults to "<election-id>-<ingress-class>"
# # Here: "<ingress-controller-leader>-<nginx>"
# # This has to be adapted if you change either parameter
# # when launching the nginx-ingress-controller.
# - "ingress-controller-leader-nginx"
# verbs:
# - get
# - update
# - apiGroups:
# - ""
# resources:
# - configmaps
# verbs:
# - create
# - apiGroups:
# - ""
# resources:
# - endpoints
# verbs:
# - get
# ---
# apiVersion: rbac.authorization.k8s.io/v1beta1
# kind: RoleBinding
# metadata:
# name: nginx-ingress-role-nisa-binding
# namespace: ingress-nginx
# roleRef:
# apiGroup: rbac.authorization.k8s.io
# kind: Role
# name: nginx-ingress-role
# subjects:
# - kind: ServiceAccount
# name: nginx-ingress-serviceaccount
# namespace: ingress-nginx
# ---
# apiVersion: rbac.authorization.k8s.io/v1beta1
# kind: ClusterRoleBinding
# metadata:
# name: nginx-ingress-clusterrole-nisa-binding
# roleRef:
# apiGroup: rbac.authorization.k8s.io
# kind: ClusterRole
# name: nginx-ingress-clusterrole
# subjects:
# - kind: ServiceAccount
# name: nginx-ingress-serviceaccount
# namespace: ingress-nginx
# ---
# apiVersion: extensions/v1beta1
# kind: Deployment
# metadata:
# name: nginx-ingress-controller
# namespace: ingress-nginx
# spec:
# replicas: 1
# selector:
# matchLabels:
# app: ingress-nginx
# template:
# metadata:
# labels:
# app: ingress-nginx
# annotations:
# prometheus.io/port: '10254'
# prometheus.io/scrape: 'true'
# spec:
# serviceAccountName: nginx-ingress-serviceaccount
# containers:
# - name: nginx-ingress-controller
# image: quay.io/kubernetes-ingress-controller/nginx-ingress-controller:0.17.1
# args:
# - /nginx-ingress-controller
# - --default-backend-service=\$(POD_NAMESPACE)/default-http-backend
# - --configmap=\$(POD_NAMESPACE)/nginx-configuration
# - --tcp-services-configmap=\$(POD_NAMESPACE)/tcp-services
# - --udp-services-configmap=\$(POD_NAMESPACE)/udp-services
# - --publish-service=\$(POD_NAMESPACE)/ingress-nginx
# - --annotations-prefix=nginx.ingress.kubernetes.io
# securityContext:
# capabilities:
# drop:
# - ALL
# add:
# - NET_BIND_SERVICE
# # www-data -> 33
# runAsUser: 33
# env:
# - name: POD_NAME
# valueFrom:
# fieldRef:
# fieldPath: metadata.name
# - name: POD_NAMESPACE
# valueFrom:
# fieldRef:
# fieldPath: metadata.namespace
# ports:
# - name: http
# containerPort: 80
# - name: https
# containerPort: 443
# livenessProbe:
# failureThreshold: 3
# httpGet:
# path: /healthz
# port: 10254
# scheme: HTTP
# initialDelaySeconds: 10
# periodSeconds: 10
# successThreshold: 1
# timeoutSeconds: 1
# readinessProbe:
# failureThreshold: 3
# httpGet:
# path: /healthz
# port: 10254
# scheme: HTTP
# periodSeconds: 10
# successThreshold: 1
# timeoutSeconds: 1
# ---
# kind: Service
# apiVersion: v1
# metadata:
# name: ingress-nginx
# namespace: ingress-nginx
# labels:
# app: ingress-nginx
# annotations:
# # Enable PROXY protocol
# service.beta.kubernetes.io/aws-load-balancer-proxy-protocol: '*'
# # Increase the ELB idle timeout to avoid issues with WebSockets or Server-Sent Events.
# service.beta.kubernetes.io/aws-load-balancer-connection-idle-timeout: '3600'
# spec:
# type: LoadBalancer
# selector:
# app: ingress-nginx
# ports:
# - name: http
# port: 80
# targetPort: http
# - name: https
# port: 443
# targetPort: https
# ---
# kind: ConfigMap
# apiVersion: v1
# metadata:
# name: nginx-configuration
# namespace: ingress-nginx
# labels:
# app: ingress-nginx
# data:
# use-proxy-protocol: "true"
# EOF

11
provider/aws/bin/cluster-update Executable file
View File

@ -0,0 +1,11 @@
#!/bin/sh
aws cloudformation update-stack --stack-name $1 --template-body file://provider/kaws/formation/rack.yml --capabilities CAPABILITY_IAM \
--parameters \
ParameterKey=BaseDomain,UsePreviousValue=true \
ParameterKey=CidrBase,UsePreviousValue=true \
ParameterKey=CidrMask,UsePreviousValue=true \
ParameterKey=NodeType,UsePreviousValue=true \
--tags \
Key=system,Value=convox \
Key=rack,Value=$1 \

47
provider/aws/build.go Normal file
View File

@ -0,0 +1,47 @@
package aws
import (
"io"
"os/exec"
"strings"
"github.com/convox/convox/pkg/structs"
)
func (p *Provider) BuildExport(app, id string, w io.Writer) error {
if err := p.authAppRepository(app); err != nil {
return err
}
return p.Provider.BuildExport(app, id, w)
}
func (p *Provider) BuildImport(app string, r io.Reader) (*structs.Build, error) {
if err := p.authAppRepository(app); err != nil {
return nil, err
}
return p.Provider.BuildImport(app, r)
}
func (p *Provider) authAppRepository(app string) error {
repo, _, err := p.RepositoryHost(app)
if err != nil {
return err
}
user, pass, err := p.RepositoryAuth(app)
if err != nil {
return err
}
cmd := exec.Command("docker", "login", "-u", user, "--password-stdin", repo)
cmd.Stdin = strings.NewReader(pass)
if err := cmd.Run(); err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,173 @@
package main
import (
"fmt"
"os"
"sort"
"strings"
"github.com/PuerkitoBio/goquery"
"github.com/headzoo/surf"
)
type Region struct {
Ami string
EFS bool
}
type Regions map[string]Region
func main() {
if err := run(); err != nil {
fmt.Fprintf(os.Stderr, "ERROR: %s\n", err)
}
}
func run() error {
regions := Regions{}
if err := fetchAmis(regions); err != nil {
return err
}
// fmt.Printf("regions = %+v\n", regions)
// return nil
if err := fetchEFS(regions); err != nil {
return err
}
names := []string{}
for name := range regions {
names = append(names, name)
}
sort.Strings(names)
fmt.Println(" Regions:")
for _, name := range names {
region := regions[name]
if region.Ami == "" {
continue
}
fmt.Printf(" %s:\n", name)
fmt.Printf(" AMI: %s\n", region.Ami)
fmt.Printf(" EFS: %s\n", yn(region.EFS))
}
// rns := make([]string, len(names))
// amis := make([]string, len(names))
// efss := make([]string, len(names))
// tazs := make([]string, len(names))
// elbs := make([]string, len(names))
// fargates := make([]string, len(names))
// for i, name := range names {
// region := regions[name]
// rns[i] = fmt.Sprintf("%q:", name)
// amis[i] = fmt.Sprintf(`"Ami": %q,`, region.Ami)
// efss[i] = fmt.Sprintf(`"EFS": %q,`, yn(region.EFS))
// }
// rnMax := max(rns)
// amiMax := max(amis)
// efsMax := max(efss)
// for i := range names {
// if regions[names[i]].Ami == "" {
// continue
// }
// f := fmt.Sprintf(` %%-%ds { %%-%ds %%-%ds %%-%ds %%-%ds %%-%ds },`, rnMax, amiMax, efsMax, tazMax, elbMax, fargateMax)
// fmt.Printf(f, rns[i], amis[i], efss[i], tazs[i], elbs[i], fargates[i])
// fmt.Println()
// }
return nil
}
func fetchAmis(regions Regions) error {
b := surf.NewBrowser()
if err := b.Open("https://docs.aws.amazon.com/eks/latest/userguide/eks-optimized-ami.html"); err != nil {
return err
}
// rows := b.Find("#main-content dd[data-tab*=\"kubernetes-version-1.12\"] .table-contents table:nth-child(1) tr")
rows := b.Find("#main-content dd[data-tab*=\"kubernetes-version-1.12\"] .table-contents tr")
if rows.Length() < 1 {
return fmt.Errorf("no amis found")
}
rows.Each(func(i int, s *goquery.Selection) {
if i == 0 {
return
}
name := s.Find("td:nth-child(1)")
ami := s.Find("td:nth-child(2)").Text()
if name.Text() == "" {
return
}
region := name.Find("code").Text()
regions[region] = Region{Ami: ami}
})
return nil
}
func fetchEFS(regions Regions) error {
b := surf.NewBrowser()
if err := b.Open("https://docs.aws.amazon.com/general/latest/gr/rande.html"); err != nil {
return err
}
rows := b.Find("h2#elasticfilesystem-region+.table .table-contents table tr")
if rows.Length() < 1 {
return fmt.Errorf("no efs entries found")
}
rows.Each(func(i int, s *goquery.Selection) {
if i == 0 {
return
}
name := strings.TrimSpace(s.Find("td:nth-child(2)").Text())
region := regions[name]
region.EFS = true
regions[name] = region
})
return nil
}
func max(ss []string) int {
m := 0
for _, s := range ss {
if len(s) > m {
m = len(s)
}
}
return m
}
func yn(v bool) string {
if v {
return "Yes"
}
return "No"
}

View File

@ -0,0 +1,258 @@
package aws
// import (
// "fmt"
// "reflect"
// "time"
// "github.com/aws/aws-sdk-go/aws"
// "github.com/aws/aws-sdk-go/service/cloudformation"
// "github.com/convox/convox/pkg/common"
// "github.com/convox/convox/pkg/kctl"
// ct "github.com/convox/convox/provider/aws/pkg/apis/convox/v1"
// ic "github.com/convox/convox/provider/aws/pkg/client/informers/externalversions/convox/v1"
// ac "k8s.io/api/core/v1"
// am "k8s.io/apimachinery/pkg/apis/meta/v1"
// "k8s.io/client-go/kubernetes"
// "k8s.io/client-go/tools/cache"
// )
// type StackController struct {
// Controller *kctl.Controller
// Provider *Provider
// }
// func NewStackController(p *Provider) (*StackController, error) {
// pc := &StackController{
// Provider: p,
// }
// c, err := kctl.NewController(p.Name, "convox-kaws-stack", pc)
// if err != nil {
// return nil, err
// }
// pc.Controller = c
// return pc, nil
// }
// func (c *StackController) Client() kubernetes.Interface {
// return c.Provider.Provider.Cluster
// }
// func (c *StackController) ListOptions(opts *am.ListOptions) {
// opts.LabelSelector = fmt.Sprintf("system=convox,rack=%s", c.Provider.Name)
// }
// func (c *StackController) Run() {
// cc, err := c.Provider.convoxClient()
// if err != nil {
// fmt.Printf("err: %+v\n", err)
// return
// }
// i := ic.NewFilteredStackInformer(cc, ac.NamespaceAll, 1*time.Minute, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, c.ListOptions)
// ch := make(chan error)
// go c.Controller.Run(i, ch)
// for err := range ch {
// fmt.Printf("err = %+v\n", err)
// }
// }
// func (c *StackController) Start() error {
// return nil
// }
// func (c *StackController) Stop() error {
// return nil
// }
// func (c *StackController) Add(obj interface{}) error {
// s, err := assertStack(obj)
// if err != nil {
// return err
// }
// fmt.Printf("stack add: %s/%s\n", s.ObjectMeta.Namespace, s.ObjectMeta.Name)
// if err := c.stackSync(s); err != nil {
// return err
// }
// return nil
// }
// func (c *StackController) Delete(obj interface{}) error {
// return nil
// }
// func (c *StackController) Update(prev, cur interface{}) error {
// ps, err := assertStack(prev)
// if err != nil {
// return err
// }
// cs, err := assertStack(cur)
// if err != nil {
// return err
// }
// if cs.DeletionTimestamp != nil /*&& cs.DeletionTimestamp.Time.Before(time.Now().UTC().Add(-1*time.Minute))*/ && cs.Status != "Deleting" {
// // as, err := c.Provider.Provider.AppStatus(cs.ObjectMeta.Labels["app"])
// // if err != nil {
// // return err
// // }
// // if as == "running" {
// return c.stackDelete(cs)
// // }
// }
// if reflect.DeepEqual(ps.ObjectMeta.Labels, cs.ObjectMeta.Labels) && reflect.DeepEqual(ps.Spec, cs.Spec) {
// return nil
// }
// fmt.Printf("stack update: %s/%s\n", cs.ObjectMeta.Name, cs.Status)
// if err := c.stackSync(cs); err != nil {
// return err
// }
// return nil
// }
// func (c *StackController) stackName(s *ct.Stack) string {
// return fmt.Sprintf("%s-%s", s.Namespace, s.Name)
// }
// func (c *StackController) stackSync(s *ct.Stack) error {
// if !s.DeletionTimestamp.IsZero() {
// return nil
// }
// _, err := c.Provider.CloudFormation.DescribeStacks(&cloudformation.DescribeStacksInput{
// StackName: aws.String(c.stackName(s)),
// })
// if common.AwsErrorCode(err) == "ValidationError" {
// return c.stackCreate(s)
// }
// return c.stackUpdate(s)
// }
// func (c *StackController) stackCreate(s *ct.Stack) error {
// req := &cloudformation.CreateStackInput{
// StackName: aws.String(c.stackName(s)),
// TemplateBody: aws.String(s.Spec.Template),
// NotificationARNs: []*string{aws.String(c.Provider.EventTopic)},
// Parameters: []*cloudformation.Parameter{},
// Tags: []*cloudformation.Tag{{Key: aws.String("stack"), Value: aws.String(s.ObjectMeta.Name)}},
// }
// for k, v := range s.Spec.Parameters {
// req.Parameters = append(req.Parameters, &cloudformation.Parameter{
// ParameterKey: aws.String(k),
// ParameterValue: aws.String(v),
// })
// }
// for k, v := range s.ObjectMeta.Labels {
// req.Tags = append(req.Tags, &cloudformation.Tag{
// Key: aws.String(k),
// Value: aws.String(v),
// })
// }
// if _, err := c.Provider.CloudFormation.CreateStack(req); err != nil {
// fmt.Printf("err: %+v\n", err)
// fmt.Printf("common.AwsErrorCode(err): %+v\n", common.AwsErrorCode(err))
// return err
// }
// return nil
// }
// func (c *StackController) stackDelete(s *ct.Stack) error {
// _, err := c.Provider.CloudFormation.DeleteStack(&cloudformation.DeleteStackInput{
// StackName: aws.String(c.stackName(s)),
// })
// if err != nil {
// return err
// }
// return nil
// }
// func (c *StackController) stackUpdate(s *ct.Stack) error {
// req := &cloudformation.UpdateStackInput{
// StackName: aws.String(c.stackName(s)),
// TemplateBody: aws.String(s.Spec.Template),
// Parameters: []*cloudformation.Parameter{},
// Tags: []*cloudformation.Tag{{Key: aws.String("stack"), Value: aws.String(s.ObjectMeta.Name)}},
// }
// for k, v := range s.Spec.Parameters {
// req.Parameters = append(req.Parameters, &cloudformation.Parameter{
// ParameterKey: aws.String(k),
// ParameterValue: aws.String(v),
// })
// }
// for k, v := range s.ObjectMeta.Labels {
// req.Tags = append(req.Tags, &cloudformation.Tag{
// Key: aws.String(k),
// Value: aws.String(v),
// })
// }
// if _, err := c.Provider.CloudFormation.UpdateStack(req); err != nil {
// if !cloudformationErrorNoUpdates(err) {
// return err
// }
// if err := c.statusUpdate(s, "Running"); err != nil {
// return err
// }
// return nil
// }
// if err := c.statusUpdate(s, "Updating"); err != nil {
// return err
// }
// return nil
// }
// func (c *StackController) statusUpdate(s *ct.Stack, status string) error {
// cc, err := c.Provider.convoxClient()
// if err != nil {
// return err
// }
// ss, err := cc.ConvoxV1().Stacks(s.ObjectMeta.Namespace).Get(s.ObjectMeta.Name, am.GetOptions{})
// if err != nil {
// return err
// }
// ss.Status = ct.StackStatus(status)
// if _, err := cc.ConvoxV1().Stacks(ss.ObjectMeta.Namespace).Update(ss); err != nil {
// return err
// }
// return nil
// }
// func assertStack(v interface{}) (*ct.Stack, error) {
// s, ok := v.(*ct.Stack)
// if !ok {
// return nil, fmt.Errorf("could not assert stack for type: %T", v)
// }
// return s, nil
// }

View File

@ -0,0 +1,5 @@
package aws
func (p *Provider) DeploymentTimeout() int {
return 1800
}

View File

@ -0,0 +1,920 @@
AWSTemplateFormatVersion: "2010-09-09"
Conditions:
BaseDomain: !Not [ !Condition BaseDomainBlank ]
BaseDomainBlank: !Equals [ !Ref BaseDomain, "" ]
SshKey: !Not [ !Equals [ !Ref SshKey, "" ] ]
Mappings:
Regions:
ap-northeast-1:
AMI: ami-0dfbca8d183884f02
EFS: Yes
ap-northeast-2:
AMI: ami-0a9d12fe9c2a31876
EFS: Yes
ap-south-1:
AMI: ami-0644de45344ce867e
EFS: Yes
ap-southeast-1:
AMI: ami-040bdde117f3828ab
EFS: Yes
ap-southeast-2:
AMI: ami-01bfe815f644becc0
EFS: Yes
eu-central-1:
AMI: ami-09ed3f40a2b3c11f1
EFS: Yes
eu-north-1:
AMI: ami-022cd6a50742d611a
EFS: No
eu-west-1:
AMI: ami-091fc251b67b776c3
EFS: Yes
eu-west-2:
AMI: ami-0bc8d0262346bd65e
EFS: Yes
eu-west-3:
AMI: ami-0084dea61e480763e
EFS: Yes
us-east-1:
AMI: ami-0200e65a38edfb7e1
EFS: Yes
us-east-2:
AMI: ami-0e8d353285e26a68c
EFS: Yes
us-west-2:
AMI: ami-0f11fd98b02f12a4c
EFS: Yes
Static:
Global:
Version: dev
SubnetMasks:
"16": { Public: 12, Private: 14 }
"17": { Public: 11, Private: 13 }
"18": { Public: 10, Private: 12 }
"19": { Public: 9, Private: 11 }
"20": { Public: 8, Private: 10 }
"21": { Public: 7, Private: 9 }
"22": { Public: 6, Private: 8 }
"23": { Public: 5, Private: 7 }
"24": { Public: 4, Private: 6 }
Outputs:
AccountId:
Value: !Ref AWS::AccountId
AdminUser:
Value: !Ref AdminUser
AutoscalerRole:
Value: !GetAtt AutoscalerRole.Arn
BaseDomain:
Value: !If [ BaseDomain, !Ref BaseDomain, !GetAtt DomainMapping.Domain ]
Cluster:
Value: !Ref Cluster
ClusterCertificateAuthority:
Value: !GetAtt Cluster.CertificateAuthorityData
ClusterEndpoint:
Value: !GetAtt Cluster.Endpoint
NodesRole:
Value: !Sub arn:aws:iam::${AWS::AccountId}:role/${NodesRole}
EventQueue:
Value: !Ref EventQueue
EventTopic:
Value: !Ref EventTopic
RackBucket:
Value: !Ref RackBucket
RackRole:
Value: !GetAtt RackRole.Arn
Region:
Value: !Ref AWS::Region
RouterCache:
Value: !Ref RouterCache
RouterHosts:
Value: !Ref RouterHosts
RouterRole:
Value: !GetAtt RouterRole.Arn
RouterTargets:
Value: !Ref RouterTargets
RouterTargetGroup80:
Value: !Ref RouterTargetGroup80
RouterTargetGroup443:
Value: !Ref RouterTargetGroup443
StackId:
Value: !Ref AWS::StackId
Version:
Value: !FindInMap [ Static, Global, Version ]
Vpc:
Export: { Name: !Sub "${AWS::StackName}:Vpc" }
Value: !Ref Vpc
VpcCidr:
Export: { Name: !Sub "${AWS::StackName}:VpcCidr" }
Value: !GetAtt Vpc.CidrBlock
VpcPrivateSubnets:
Value: !Sub "${VpcPrivateSubnet0},${VpcPrivateSubnet1},${VpcPrivateSubnet2}"
VpcPrivateSubnet0:
Export: { Name: !Sub "${AWS::StackName}:VpcPrivateSubnet0" }
Value: !Ref VpcPrivateSubnet0
VpcPrivateSubnet1:
Export: { Name: !Sub "${AWS::StackName}:VpcPrivateSubnet1" }
Value: !Ref VpcPrivateSubnet1
VpcPrivateSubnet2:
Export: { Name: !Sub "${AWS::StackName}:VpcPrivateSubnet2" }
Value: !Ref VpcPrivateSubnet2
VpcPublicSubnets:
Value: !Sub "${VpcPublicSubnet0},${VpcPublicSubnet1},${VpcPublicSubnet2}"
Parameters:
AdminUser:
Type: String
Default: ""
BaseDomain:
Type: String
Default: ""
CidrBase:
Type: String
Default: 10.1.0.0
AllowedPattern: (\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})
CidrMask:
Type: Number
Default: 16
MinValue: 16
MaxValue: 24
NodeType:
Type: String
Default: t3.small
SshKey:
Type: String
Default: ""
Resources:
AutoscalerRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: "Allow"
Principal: { AWS: !GetAtt NodesRole.Arn }
Action: sts:AssumeRole
Path: /convox/
Policies:
- PolicyName: alb-ingress
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- autoscaling:DescribeAutoScalingGroups
- autoscaling:DescribeAutoScalingInstances
- autoscaling:DescribeTags
Resource: "*"
- Effect: Allow
Action:
- autoscaling:SetDesiredCapacity
- autoscaling:TerminateInstanceInAutoScalingGroup
Resource: !Sub "arn:aws:autoscaling:${AWS::Region}:${AWS::AccountId}:autoScalingGroup:*:autoScalingGroupName/${Nodes}"
Cluster:
Type: AWS::EKS::Cluster
Properties:
Name: !Ref AWS::StackName
ResourcesVpcConfig:
SecurityGroupIds:
- !Ref ClusterSecurityGroup
SubnetIds:
- !Ref VpcPublicSubnet0
- !Ref VpcPublicSubnet1
- !Ref VpcPublicSubnet2
RoleArn: !GetAtt ClusterRole.Arn
ClusterRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: "Allow"
Principal: { Service: [ "eks.amazonaws.com" ] }
Action: [ "sts:AssumeRole" ]
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AmazonEKSClusterPolicy
- arn:aws:iam::aws:policy/AmazonEKSServicePolicy
Path: /convox/
ClusterSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: !Sub "${AWS::StackName} cluster"
VpcId: !Ref Vpc
Tags:
- Key: Name
Value: !Sub "${AWS::StackName} cluster"
ClusterSecurityGroupIngressNodes:
Type: AWS::EC2::SecurityGroupIngress
Properties:
GroupId: !Ref ClusterSecurityGroup
SourceSecurityGroupId: !Ref NodesSecurityGroup
IpProtocol: tcp
ToPort: 443
FromPort: 443
ClusterSecurityGroupEgressNodes:
Type: AWS::EC2::SecurityGroupEgress
Properties:
GroupId: !Ref ClusterSecurityGroup
DestinationSecurityGroupId: !Ref NodesSecurityGroup
IpProtocol: tcp
FromPort: 1025
ToPort: 65535
ClusterSecurityGroupEgressNodesControl:
Type: AWS::EC2::SecurityGroupEgress
Properties:
GroupId: !Ref ClusterSecurityGroup
DestinationSecurityGroupId: !Ref NodesSecurityGroup
IpProtocol: tcp
FromPort: 443
ToPort: 443
DomainMapper:
Type: AWS::Lambda::Function
Condition: BaseDomainBlank
Properties:
Code:
ZipFile: |
var https = require('https');
var qs = require('querystring');
var url = require('url');
exports.main = function(event, context) {
if (event.RequestType == 'Delete') {
respond(event, context, 'SUCCESS', '', {});
return;
}
var host = 'dnsbot.convox.com';
var options = { method: 'POST', hostname: host, port: 443, path: '/register/aws', headers: { 'Content-Type': 'application/x-www-form-urlencoded' } };
var data = '';
var req = https.request(options, function(res) {
res.on('data', function(chunk) { data += chunk });
res.on('end', function() {
if (res.statusCode < 300) {
respond(event, context, 'SUCCESS', '', { 'Domain': data });
} else {
respond(event, context, 'FAILED', data, {});
}
});
});
req.on('error', function(err) {
console.log('err', err);
respond(event, context, 'FAILED', err, {});
});
req.write(qs.stringify({ subdomain: event.ResourceProperties.Subdomain }));
req.end();
};
function respond(event, context, status, reason, data) {
var body = JSON.stringify({
Status: status,
Reason: reason,
PhysicalResourceId: event.ResourceProperties.Subdomain,
StackId: event.StackId,
RequestId: event.RequestId,
LogicalResourceId: event.LogicalResourceId,
Data: data
});
var u = url.parse(event.ResponseURL);
var options = { method: 'PUT', hostname: u.hostname, port: 443, path: u.path, headers: { 'Content-Length': body.length } };
var req = https.request(options, function(res) {
context.done();
});
req.on('error', function(err) {
console.log('err', err);
context.done();
});
req.write(body);
req.end();
}
Handler: index.main
Role: !GetAtt DomainMapperRole.Arn
Runtime: nodejs8.10
DomainMapperRole:
Type: AWS::IAM::Role
Condition: BaseDomainBlank
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: "Allow"
Principal: { Service: [ "lambda.amazonaws.com" ] }
Action: [ "sts:AssumeRole" ]
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
Path: /convox/
DomainMapping:
Type: Custom::DomainMapping
Condition: BaseDomainBlank
Properties:
ServiceToken: !GetAtt DomainMapper.Arn
Subdomain: !Join
- "."
- - !Select [ 0, !Split [ ".", !GetAtt Router.DNSName ] ]
- !Select [ 1, !Split [ ".", !GetAtt Router.DNSName ] ]
- !Select [ 2, !Split [ ".", !GetAtt Router.DNSName ] ]
EventQueue:
Type: AWS::SQS::Queue
EventQueuePolicy:
Type: AWS::SQS::QueuePolicy
Properties:
Queues:
- !Ref EventQueue
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal: { "AWS": "*" }
Action: sqs:SendMessage
Resource: !GetAtt EventQueue.Arn
Condition: { "ArnEquals": { "aws:SourceArn": !Ref EventTopic } }
EventTopic:
Type: AWS::SNS::Topic
Properties:
DisplayName: !Ref AWS::StackName
Subscription:
- Protocol: sqs
Endpoint: !GetAtt EventQueue.Arn
NodesInstanceProfile:
Type: AWS::IAM::InstanceProfile
Properties:
Path: /convox/
Roles:
- !Ref NodesRole
NodesLaunchConfig:
Type: AWS::AutoScaling::LaunchConfiguration
Properties:
AssociatePublicIpAddress: true
IamInstanceProfile: !Ref NodesInstanceProfile
ImageId: !FindInMap [ Regions, !Ref "AWS::Region", AMI ]
InstanceType: !Ref NodeType
KeyName: !If [ SshKey, !Ref SshKey, !Ref "AWS::NoValue" ]
SecurityGroups:
- !Ref NodesSecurityGroup
UserData:
Fn::Base64: !Sub |
#!/bin/bash -xe
/etc/eks/bootstrap.sh ${Cluster}
iptables --append PREROUTING --protocol tcp --destination 169.254.169.254 --dport 80 --in-interface eni+ --jump DNAT --table nat --to-destination `curl 169.254.169.254/latest/meta-data/local-ipv4`:8181
/opt/aws/bin/cfn-signal -e $? --stack ${AWS::StackName} --resource Nodes --region ${AWS::Region}
NodesRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: "Allow"
Principal: { Service: [ "ec2.amazonaws.com" ] }
Action: [ "sts:AssumeRole" ]
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy
- arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy
- arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly
Path: /convox/
Policies:
- PolicyName: alb-ingress
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- acm:DescribeCertificate
- acm:ListCertificates
- acm:GetCertificate
Resource: "*"
# - Effect: Allow
# Action:
# - ec2:AuthorizeSecurityGroupIngress
# - ec2:CreateSecurityGroup
# - ec2:CreateTags
# - ec2:DeleteSecurityGroup
# - ec2:DescribeInstances
# - ec2:DescribeInstanceStatus
# - ec2:DescribeSecurityGroups
# - ec2:DescribeSubnets
# - ec2:DescribeTags
# - ec2:DescribeVpcs
# - ec2:ModifyInstanceAttribute
# - ec2:RevokeSecurityGroupIngress
# Resource: "*"
- Effect: Allow
Action:
- elasticloadbalancing:AddTags
- elasticloadbalancing:CreateListener
- elasticloadbalancing:CreateLoadBalancer
- elasticloadbalancing:CreateRule
- elasticloadbalancing:CreateTargetGroup
- elasticloadbalancing:DeleteListener
- elasticloadbalancing:DeleteLoadBalancer
- elasticloadbalancing:DeleteRule
- elasticloadbalancing:DeleteTargetGroup
- elasticloadbalancing:DeregisterTargets
- elasticloadbalancing:DescribeListeners
- elasticloadbalancing:DescribeLoadBalancers
- elasticloadbalancing:DescribeLoadBalancerAttributes
- elasticloadbalancing:DescribeRules
- elasticloadbalancing:DescribeSSLPolicies
- elasticloadbalancing:DescribeTags
- elasticloadbalancing:DescribeTargetGroups
- elasticloadbalancing:DescribeTargetGroupAttributes
- elasticloadbalancing:DescribeTargetHealth
- elasticloadbalancing:ModifyListener
- elasticloadbalancing:ModifyLoadBalancerAttributes
- elasticloadbalancing:ModifyRule
- elasticloadbalancing:ModifyTargetGroup
- elasticloadbalancing:ModifyTargetGroupAttributes
- elasticloadbalancing:RegisterTargets
- elasticloadbalancing:RemoveTags
- elasticloadbalancing:SetIpAddressType
- elasticloadbalancing:SetSecurityGroups
- elasticloadbalancing:SetSubnets
- elasticloadbalancing:SetWebACL
Resource: "*"
# - Effect: Allow
# Action:
# - iam:GetServerCertificate
# - iam:ListServerCertificates
# Resource: "*"
# - Effect: Allow
# Action:
# - waf-regional:GetWebACLForResource
# Resource: "*"
# - Effect: Allow
# Action:
# - tag:GetResources
# Resource: "*"
# - Effect: Allow
# Action:
# - waf:GetWebACL
# - waf:AssociateWebACL
# - waf:DisassociateWebACL
# Resource: "*"
NodesSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: !Sub "${AWS::StackName} nodes"
SecurityGroupIngress:
- Description: mtu discovery
CidrIp: 0.0.0.0/0
IpProtocol: icmp
FromPort: 3
ToPort: 4
- Description: router traffic
CidrIp: 0.0.0.0/0
IpProtocol: tcp
FromPort: 32000
ToPort: 32001
VpcId:
!Ref Vpc
Tags:
- Key: Name
Value: !Sub "${AWS::StackName} nodes"
- Key: !Sub "kubernetes.io/cluster/${Cluster}"
Value: owned
NodesSecurityGroupIngressClusterControl:
Type: AWS::EC2::SecurityGroupIngress
Properties:
Description: eks control plane
GroupId: !Ref NodesSecurityGroup
SourceSecurityGroupId: !Ref ClusterSecurityGroup
IpProtocol: tcp
FromPort: 443
ToPort: 443
NodesSecurityGroupIngressClusterTraffic:
Type: AWS::EC2::SecurityGroupIngress
Properties:
Description: eks traffic
GroupId: !Ref NodesSecurityGroup
SourceSecurityGroupId: !Ref ClusterSecurityGroup
IpProtocol: tcp
FromPort: 1025
ToPort: 65535
NodesSecurityGroupIngressInternal:
Type: AWS::EC2::SecurityGroupIngress
Properties:
Description: internal traffic
GroupId: !Ref NodesSecurityGroup
SourceSecurityGroupId: !Ref NodesSecurityGroup
IpProtocol: "-1"
FromPort: 0
ToPort: 65535
Nodes:
Type: AWS::AutoScaling::AutoScalingGroup
Properties:
LaunchConfigurationName: !Ref NodesLaunchConfig
MinSize: 3
MaxSize: 20
TargetGroupARNs:
- !Ref RouterTargetGroup80
- !Ref RouterTargetGroup443
VPCZoneIdentifier:
- !Ref VpcPublicSubnet0
- !Ref VpcPublicSubnet1
- !Ref VpcPublicSubnet2
Tags:
- Key: Name
Value: !Ref AWS::StackName
PropagateAtLaunch: true
- Key: !Sub "kubernetes.io/cluster/${Cluster}"
Value: owned
PropagateAtLaunch: true
- Key: k8s.io/cluster-autoscaler/enabled
Value: ""
PropagateAtLaunch: false
- Key: !Sub k8s.io/cluster-autoscaler/${Cluster}
Value: ""
PropagateAtLaunch: false
UpdatePolicy:
AutoScalingRollingUpdate:
MinInstancesInService: 2
MaxBatchSize: 1
PauseTime: PT5M
SuspendProcesses: [ ScheduledActions ]
WaitOnResourceSignals: true
RackBucket:
Type: AWS::S3::Bucket
DeletionPolicy: Retain
Properties:
AccessControl: Private
BucketEncryption:
ServerSideEncryptionConfiguration:
- ServerSideEncryptionByDefault:
SSEAlgorithm: aws:kms
RackRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: "Allow"
Principal: { AWS: !GetAtt NodesRole.Arn }
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/PowerUserAccess
- arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryFullAccess
Path: /convox/
Policies:
- PolicyName: iam
PolicyDocument:
Version: 2012-10-17
Statement:
Policies:
- PolicyName: rack
PolicyDocument:
Version: "2012-10-17"
Statement:
- Action:
- acm:DescribeCertificate
- acm:RequestCertificate
Effect: Allow
Resource: "*"
- Action:
- cloudformation:DescribeStacks
Effect: Allow
Resource: !Sub "arn:aws:cloudformation:${AWS::Region}:${AWS::AccountId}:stack/${AWS::StackName}/*"
- Action:
- elasticloadbalancing:CreateListener
- elasticloadbalancing:CreateLoadBalancer
- elasticloadbalancing:CreateRule
- elasticloadbalancing:CreateTargetGroup
- elasticloadbalancing:DeleteLoadBalancer
- elasticloadbalancing:DeleteRule
- elasticloadbalancing:DeleteTargetGroup
- elasticloadbalancing:DeregisterTargets
- elasticloadbalancing:DescribeRules
- elasticloadbalancing:DescribeTargetHealth
- elasticloadbalancing:ModifyRule
- elasticloadbalancing:ModifyTargetGroup
- elasticloadbalancing:ModifyTargetGroupAttributes
- elasticloadbalancing:RegisterTargets
Effect: Allow
Resource: "*"
- Effect: Allow
Action: iam:*
Resource:
- arn:aws:iam::*:instance-profile/convox/*
- arn:aws:iam::*:policy/convox/*
- arn:aws:iam::*:role/convox/*
- arn:aws:iam::*:user/convox/*
- Effect: Allow
Action:
- iam:CreateServiceLinkedRole
Resource: "*"
Router:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
Properties:
Name: !Sub ${AWS::StackName}
Scheme: internet-facing
Subnets:
- !Ref VpcPublicSubnet0
- !Ref VpcPublicSubnet1
- !Ref VpcPublicSubnet2
Type: network
RouterListener80:
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
DefaultActions:
- Type: forward
TargetGroupArn: !Ref RouterTargetGroup80
LoadBalancerArn: !Ref Router
Port: 80
Protocol: TCP
RouterListener443:
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
DefaultActions:
- Type: forward
TargetGroupArn: !Ref RouterTargetGroup443
LoadBalancerArn: !Ref Router
Port: 443
Protocol: TCP
RouterRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: "Allow"
Principal: { AWS: !GetAtt NodesRole.Arn }
Action: sts:AssumeRole
Path: /convox/
Policies:
- PolicyName: alb-ingress
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- dynamodb:DeleteItem
- dynamodb:GetItem
- dynamodb:PutItem
Resource: !GetAtt RouterCache.Arn
- Effect: Allow
Action:
- dynamodb:GetItem
- dynamodb:UpdateItem
Resource: !GetAtt RouterHosts.Arn
- Effect: Allow
Action:
- dynamodb:GetItem
- dynamodb:UpdateItem
Resource: !GetAtt RouterTargets.Arn
RouterCache:
Type: AWS::DynamoDB::Table
Properties:
AttributeDefinitions:
- AttributeName: key
AttributeType: S
BillingMode: PAY_PER_REQUEST
KeySchema:
- AttributeName: key
KeyType: HASH
RouterHosts:
Type: AWS::DynamoDB::Table
Properties:
AttributeDefinitions:
- AttributeName: host
AttributeType: S
BillingMode: PAY_PER_REQUEST
KeySchema:
- AttributeName: host
KeyType: HASH
RouterTargets:
Type: AWS::DynamoDB::Table
Properties:
AttributeDefinitions:
- AttributeName: target
AttributeType: S
BillingMode: PAY_PER_REQUEST
KeySchema:
- AttributeName: target
KeyType: HASH
RouterTargetGroup80:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
HealthCheckIntervalSeconds: 10
HealthCheckPath: /convox/health
HealthCheckProtocol: HTTP
HealthyThresholdCount: 2
Port: 32000
Protocol: TCP
TargetType: instance
UnhealthyThresholdCount: 2
VpcId: !Ref Vpc
RouterTargetGroup443:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
HealthCheckIntervalSeconds: 10
HealthCheckPath: /convox/health
HealthCheckProtocol: HTTPS
HealthyThresholdCount: 2
Port: 32001
Protocol: TCP
TargetType: instance
UnhealthyThresholdCount: 2
VpcId: !Ref Vpc
Vpc:
Type: AWS::EC2::VPC
Properties:
CidrBlock: !Sub "${CidrBase}/${CidrMask}"
EnableDnsHostnames: true
EnableDnsSupport: true
Tags:
- Key: Name
Value: !Ref AWS::StackName
VpcGateway:
Type: AWS::EC2::InternetGateway
Properties:
Tags:
- Key: Name
Value: !Ref AWS::StackName
VpcGatewayAttachment:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
InternetGatewayId: !Ref VpcGateway
VpcId: !Ref Vpc
VpcNatAddress0:
Type: AWS::EC2::EIP
Properties:
Domain: vpc
VpcNatAddress1:
Type: AWS::EC2::EIP
Properties:
Domain: vpc
VpcNatAddress2:
Type: AWS::EC2::EIP
Properties:
Domain: vpc
VpcNat0:
Type: AWS::EC2::NatGateway
Properties:
AllocationId: !GetAtt VpcNatAddress0.AllocationId
SubnetId: !Ref VpcPublicSubnet0
VpcNat1:
Type: AWS::EC2::NatGateway
Properties:
AllocationId: !GetAtt VpcNatAddress1.AllocationId
SubnetId: !Ref VpcPublicSubnet1
VpcNat2:
Type: AWS::EC2::NatGateway
Properties:
AllocationId: !GetAtt VpcNatAddress2.AllocationId
SubnetId: !Ref VpcPublicSubnet2
VpcPrivateRoutes0:
Type: AWS::EC2::RouteTable
DependsOn: VpcNat0
Properties:
VpcId: !Ref Vpc
Tags:
- Key: Name
Value: !Sub "${AWS::StackName} private 0"
VpcPrivateRoutes0Default:
Type: AWS::EC2::Route
Properties:
DestinationCidrBlock: 0.0.0.0/0
NatGatewayId: !Ref VpcNat0
RouteTableId: !Ref VpcPrivateRoutes0
VpcPrivateRoutes1:
Type: AWS::EC2::RouteTable
DependsOn: VpcNat1
Properties:
VpcId: !Ref Vpc
Tags:
- Key: Name
Value: !Sub "${AWS::StackName} private 1"
VpcPrivateRoutes1Default:
Type: AWS::EC2::Route
Properties:
DestinationCidrBlock: 0.0.0.0/0
NatGatewayId: !Ref VpcNat1
RouteTableId: !Ref VpcPrivateRoutes1
VpcPrivateRoutes2:
Type: AWS::EC2::RouteTable
DependsOn: VpcNat2
Properties:
VpcId: !Ref Vpc
Tags:
- Key: Name
Value: !Sub "${AWS::StackName} private 2"
VpcPrivateRoutes2Default:
Type: AWS::EC2::Route
Properties:
DestinationCidrBlock: 0.0.0.0/0
NatGatewayId: !Ref VpcNat2
RouteTableId: !Ref VpcPrivateRoutes2
VpcPrivateSubnet0:
Type: AWS::EC2::Subnet
Properties:
AvailabilityZone: !Select [ 0, { "Fn::GetAZs": "" } ]
CidrBlock: !Select [ 1, !Cidr [ !GetAtt Vpc.CidrBlock, "4", !FindInMap [ "SubnetMasks", !Ref CidrMask, "Private" ] ] ]
VpcId: !Ref Vpc
Tags:
- Key: Name
Value: !Sub "${AWS::StackName} public 0"
- Key: kubernetes.io/role/elb
Value: ""
- Key: kubernetes.io/role/internal-elb
Value: ""
VpcPrivateSubnet0Routes:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref VpcPrivateSubnet0
RouteTableId: !Ref VpcPrivateRoutes0
VpcPrivateSubnet1:
Type: AWS::EC2::Subnet
Properties:
AvailabilityZone: !Select [ 1, { "Fn::GetAZs": "" } ]
CidrBlock: !Select [ 2, !Cidr [ !GetAtt Vpc.CidrBlock, "4", !FindInMap [ "SubnetMasks", !Ref CidrMask, "Private" ] ] ]
VpcId: !Ref Vpc
Tags:
- Key: Name
Value: !Sub "${AWS::StackName} public 1"
- Key: kubernetes.io/role/elb
Value: ""
- Key: kubernetes.io/role/internal-elb
Value: ""
VpcPrivateSubnet1Routes:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref VpcPrivateSubnet1
RouteTableId: !Ref VpcPrivateRoutes1
VpcPrivateSubnet2:
Type: AWS::EC2::Subnet
Properties:
AvailabilityZone: !Select [ 2, { "Fn::GetAZs": "" } ]
CidrBlock: !Select [ 3, !Cidr [ !GetAtt Vpc.CidrBlock, "4", !FindInMap [ "SubnetMasks", !Ref CidrMask, "Private" ] ] ]
VpcId: !Ref Vpc
Tags:
- Key: Name
Value: !Sub "${AWS::StackName} public 2"
- Key: kubernetes.io/role/elb
Value: ""
- Key: kubernetes.io/role/internal-elb
Value: ""
VpcPrivateSubnet2Routes:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref VpcPrivateSubnet2
RouteTableId: !Ref VpcPrivateRoutes2
VpcPublicRoutes:
Type: AWS::EC2::RouteTable
DependsOn: VpcGatewayAttachment
Properties:
VpcId: !Ref Vpc
Tags:
- Key: Name
Value: !Sub "${AWS::StackName} public"
VpcPublicRoutesDefault:
Type: AWS::EC2::Route
Properties:
DestinationCidrBlock: 0.0.0.0/0
GatewayId: !Ref VpcGateway
RouteTableId: !Ref VpcPublicRoutes
VpcPublicSubnet0:
Type: AWS::EC2::Subnet
Properties:
AvailabilityZone: !Select [ 0, { "Fn::GetAZs": "" } ]
CidrBlock: !Select [ 0, !Cidr [ !GetAtt Vpc.CidrBlock, "8", !FindInMap [ "SubnetMasks", !Ref CidrMask, "Public" ] ] ]
VpcId: !Ref Vpc
Tags:
- Key: Name
Value: !Sub "${AWS::StackName} private 0"
- Key: kubernetes.io/role/elb
Value: ""
- Key: kubernetes.io/role/internal-elb
Value: ""
VpcPublicSubnet0Routes:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref VpcPublicSubnet0
RouteTableId: !Ref VpcPublicRoutes
VpcPublicSubnet1:
Type: AWS::EC2::Subnet
Properties:
AvailabilityZone: !Select [ 1, { "Fn::GetAZs": "" } ]
CidrBlock: !Select [ 1, !Cidr [ !GetAtt Vpc.CidrBlock, "8", !FindInMap [ "SubnetMasks", !Ref CidrMask, "Public" ] ] ]
VpcId: !Ref Vpc
Tags:
- Key: Name
Value: !Sub "${AWS::StackName} private 1"
- Key: kubernetes.io/role/elb
Value: ""
- Key: kubernetes.io/role/internal-elb
Value: ""
VpcPublicSubnet1Routes:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref VpcPublicSubnet1
RouteTableId: !Ref VpcPublicRoutes
VpcPublicSubnet2:
Type: AWS::EC2::Subnet
Properties:
AvailabilityZone: !Select [ 2, { "Fn::GetAZs": "" } ]
CidrBlock: !Select [ 2, !Cidr [ !GetAtt Vpc.CidrBlock, "8", !FindInMap [ "SubnetMasks", !Ref CidrMask, "Public" ] ] ]
VpcId: !Ref Vpc
Tags:
- Key: Name
Value: !Sub "${AWS::StackName} private 2"
- Key: kubernetes.io/role/elb
Value: ""
- Key: kubernetes.io/role/internal-elb
Value: ""
VpcPublicSubnet2Routes:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref VpcPublicSubnet2
RouteTableId: !Ref VpcPublicRoutes

158
provider/aws/helpers.go Normal file
View File

@ -0,0 +1,158 @@
package aws
import (
"context"
"errors"
"fmt"
"os"
"os/exec"
"regexp"
"strings"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/cloudformation"
cv "github.com/convox/convox/provider/aws/pkg/client/clientset/versioned"
am "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func (p *Provider) appRegistry(app string) (string, error) {
ns, err := p.Provider.Cluster.CoreV1().Namespaces().Get(p.AppNamespace(app), am.GetOptions{})
if err != nil {
return "", err
}
registry, ok := ns.ObjectMeta.Annotations["convox.registry"]
if !ok {
return "", fmt.Errorf("no registry for app: %s", app)
}
return registry, nil
}
func (p *Provider) convoxClient() (cv.Interface, error) {
return cv.NewForConfig(p.Config)
}
func (p *Provider) stackOutputs(stack string) (map[string]string, error) {
res, err := p.CloudFormation.DescribeStacks(&cloudformation.DescribeStacksInput{
StackName: aws.String(stack),
})
if err != nil {
return nil, err
}
if len(res.Stacks) != 1 {
return nil, fmt.Errorf("no such stack: %s", stack)
}
outputs := map[string]string{}
for _, o := range res.Stacks[0].Outputs {
outputs[*o.OutputKey] = *o.OutputValue
}
return outputs, nil
}
func (p *Provider) stackTags(stack string) (map[string]string, error) {
res, err := p.CloudFormation.DescribeStacks(&cloudformation.DescribeStacksInput{
StackName: aws.String(stack),
})
if err != nil {
return nil, err
}
if len(res.Stacks) != 1 {
return nil, fmt.Errorf("no such stack: %s", stack)
}
tags := map[string]string{}
for _, t := range res.Stacks[0].Tags {
tags[*t.Key] = *t.Value
}
return tags, nil
}
func (p *Provider) watchForProcessTermination(ctx context.Context, app, pid string, cancel func()) {
defer cancel()
tick := time.NewTicker(2 * time.Second)
defer tick.Stop()
for {
select {
case <-ctx.Done():
return
case <-tick.C:
if _, err := p.ProcessGet(app, pid); err != nil {
time.Sleep(2 * time.Second)
cancel()
return
}
}
}
}
func cloudformationErrorNoUpdates(err error) bool {
if ae, ok := err.(awserr.Error); ok {
if ae.Code() == "ValidationError" && strings.Contains(ae.Message(), "No updates are to be performed") {
return true
}
}
return false
}
func kubectl(args ...string) error {
cmd := exec.Command("kubectl", args...)
cmd.Env = os.Environ()
out, err := cmd.CombinedOutput()
if err != nil {
return errors.New(strings.TrimSpace(string(out)))
}
return nil
}
var outputConverter = regexp.MustCompile("([a-z])([A-Z])") // lower case letter followed by upper case
func outputToEnvironment(name string) string {
return strings.ToUpper(outputConverter.ReplaceAllString(name, "${1}_${2}"))
}
func upperName(name string) string {
if name == "" {
return ""
}
// replace underscores with dashes
name = strings.Replace(name, "_", "-", -1)
// myapp -> Myapp; my-app -> MyApp
us := strings.ToUpper(name[0:1]) + name[1:]
for {
i := strings.Index(us, "-")
if i == -1 {
break
}
s := us[0:i]
if len(us) > i+1 {
s += strings.ToUpper(us[i+1 : i+2])
}
if len(us) > i+2 {
s += us[i+2:]
}
us = s
}
return us
}

18
provider/aws/instance.go Normal file
View File

@ -0,0 +1,18 @@
package aws
import (
"fmt"
"github.com/convox/convox/pkg/structs"
)
func (p *Provider) InstanceList() (structs.Instances, error) {
is, err := p.Provider.InstanceList()
if err != nil {
return nil, err
}
fmt.Println("kaws additions")
return is, nil
}

167
provider/aws/log.go Normal file
View File

@ -0,0 +1,167 @@
package aws
import (
"context"
"fmt"
"io"
"net/url"
"strings"
"sync"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/cloudwatchlogs"
"github.com/convox/convox/pkg/common"
"github.com/convox/convox/pkg/structs"
)
var sequenceTokens sync.Map
func (p *Provider) Log(app, stream string, ts time.Time, message string) error {
group := p.appLogGroup(app)
req := &cloudwatchlogs.PutLogEventsInput{
LogGroupName: aws.String(group),
LogStreamName: aws.String(stream),
LogEvents: []*cloudwatchlogs.InputLogEvent{
{
Timestamp: aws.Int64(ts.UnixNano() / int64(time.Millisecond)),
Message: aws.String(message),
},
},
}
key := fmt.Sprintf("%s/%s", *req.LogGroupName, *req.LogStreamName)
if tv, ok := sequenceTokens.Load(key); ok {
if token, ok := tv.(string); ok {
req.SequenceToken = aws.String(token)
}
}
for {
res, err := p.CloudWatchLogs.PutLogEvents(req)
switch common.AwsErrorCode(err) {
case "ResourceNotFoundException":
if strings.Contains(err.Error(), "log group") {
if err := p.createLogGroup(app); err != nil {
return err
}
}
if err := p.createLogStream(group, stream); err != nil {
return err
}
case "InvalidSequenceTokenException":
token, err := p.nextSequenceToken(group, stream)
if err != nil {
return err
}
req.SequenceToken = aws.String(token)
case "":
sequenceTokens.Store(key, *res.NextSequenceToken)
return nil
default:
return err
}
continue
}
return nil
}
func (p *Provider) AppLogs(name string, opts structs.LogsOptions) (io.ReadCloser, error) {
return common.CloudWatchLogsSubscribe(p.Context(), p.CloudWatchLogs, p.appLogGroup(name), "", opts)
}
func (p *Provider) BuildLogs(app, id string, opts structs.LogsOptions) (io.ReadCloser, error) {
b, err := p.BuildGet(app, id)
if err != nil {
return nil, err
}
switch b.Status {
case "running":
return p.ProcessLogs(app, b.Process, opts)
default:
u, err := url.Parse(b.Logs)
if err != nil {
return nil, err
}
switch u.Scheme {
case "object":
return p.ObjectFetch(u.Hostname(), u.Path)
default:
return nil, fmt.Errorf("unable to read logs for build: %s", id)
}
}
}
func (p *Provider) ProcessLogs(app, pid string, opts structs.LogsOptions) (io.ReadCloser, error) {
ps, err := p.ProcessGet(app, pid)
if err != nil {
return nil, err
}
key := fmt.Sprintf("service/%s/%s", ps.Name, pid)
ctx, cancel := context.WithCancel(p.Context())
go p.watchForProcessTermination(ctx, app, pid, cancel)
return common.CloudWatchLogsSubscribe(ctx, p.CloudWatchLogs, p.appLogGroup(app), key, opts)
}
func (p *Provider) SystemLogs(opts structs.LogsOptions) (io.ReadCloser, error) {
return common.CloudWatchLogsSubscribe(p.Context(), p.CloudWatchLogs, p.appLogGroup("rack"), "", opts)
}
func (p *Provider) appLogGroup(app string) string {
return fmt.Sprintf("%s-%s", p.Name, app)
}
func (p *Provider) createLogGroup(app string) error {
_, err := p.CloudWatchLogs.CreateLogGroup(&cloudwatchlogs.CreateLogGroupInput{
LogGroupName: aws.String(p.appLogGroup(app)),
Tags: map[string]*string{
"system": aws.String("convox"),
"rack": aws.String(p.Name),
"app": aws.String(app),
},
})
if err != nil {
return err
}
return nil
}
func (p *Provider) createLogStream(group, stream string) error {
_, err := p.CloudWatchLogs.CreateLogStream(&cloudwatchlogs.CreateLogStreamInput{
LogGroupName: aws.String(group),
LogStreamName: aws.String(stream),
})
if err != nil {
return err
}
return nil
}
func (p *Provider) nextSequenceToken(group, stream string) (string, error) {
res, err := p.CloudWatchLogs.DescribeLogStreams(&cloudwatchlogs.DescribeLogStreamsInput{
LogGroupName: aws.String(group),
LogStreamNamePrefix: aws.String(stream),
})
if err != nil {
return "", err
}
if len(res.LogStreams) != 1 {
return "", fmt.Errorf("could not describe log stream: %s/%s", group, stream)
}
if res.LogStreams[0].UploadSequenceToken == nil {
return "", fmt.Errorf("could not fetch sequence token for log stream: %s/%s", group, stream)
}
return *res.LogStreams[0].UploadSequenceToken, nil
}

163
provider/aws/object.go Normal file
View File

@ -0,0 +1,163 @@
package aws
import (
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/s3"
"github.com/aws/aws-sdk-go/service/s3/s3manager"
"github.com/convox/convox/pkg/structs"
)
func (p *Provider) ObjectDelete(app, key string) error {
exists, err := p.ObjectExists(app, key)
if err != nil {
return err
}
if !exists {
return fmt.Errorf("object not found: %s", key)
}
_, err = p.S3.DeleteObject(&s3.DeleteObjectInput{
Bucket: aws.String(p.Bucket),
Key: aws.String(p.objectKey(app, key)),
})
if err != nil {
return err
}
return nil
}
func (p *Provider) ObjectExists(app, key string) (bool, error) {
_, err := p.S3.HeadObject(&s3.HeadObjectInput{
Bucket: aws.String(p.Bucket),
Key: aws.String(p.objectKey(app, key)),
})
if err, ok := err.(awserr.Error); ok && err.Code() == "NotFound" {
return false, nil
}
if err != nil {
return false, err
}
return true, nil
}
// ObjectFetch fetches an Object
func (p *Provider) ObjectFetch(app, key string) (io.ReadCloser, error) {
res, err := p.S3.GetObject(&s3.GetObjectInput{
Bucket: aws.String(p.Bucket),
Key: aws.String(p.objectKey(app, key)),
})
if ae, ok := err.(awserr.Error); ok && ae.Code() == "NoSuchKey" {
return nil, fmt.Errorf("object not found: %s", key)
}
if err != nil {
return nil, err
}
return res.Body, nil
}
func (p *Provider) ObjectList(app, prefix string) ([]string, error) {
res, err := p.S3.ListObjectsV2(&s3.ListObjectsV2Input{
Bucket: aws.String(p.Bucket),
Delimiter: aws.String("/"),
Prefix: aws.String(p.objectKey(app, prefix)),
})
if err != nil {
return nil, err
}
objects := []string{}
for _, item := range res.Contents {
objects = append(objects, *item.Key)
}
return objects, nil
}
// ObjectStore stores an Object
func (p *Provider) ObjectStore(app, key string, r io.Reader, opts structs.ObjectStoreOptions) (*structs.Object, error) {
if key == "" {
k, err := generateTempKey()
if err != nil {
return nil, err
}
key = k
}
up := s3manager.NewUploaderWithClient(p.S3)
req := &s3manager.UploadInput{
Bucket: aws.String(p.Bucket),
Key: aws.String(p.objectKey(app, key)),
Body: r,
}
if opts.Public != nil && *opts.Public {
req.ACL = aws.String("public-read")
}
res, err := up.Upload(req)
if err != nil {
return nil, err
}
url := fmt.Sprintf("object://%s/%s", app, key)
if opts.Public != nil && *opts.Public {
url = res.Location
}
o := &structs.Object{Url: url}
return o, nil
}
func (p *Provider) objectKey(app, key string) string {
return fmt.Sprintf("%s/%s", app, key)
}
// func (p *Provider) objectPresignedURL(o *structs.Object, duration time.Duration) (string, error) {
// ou, err := url.Parse(o.Url)
// if err != nil {
// return "", err
// }
// if ou.Scheme != "object" {
// return "", fmt.Errorf("url is not an object: %s", o.Url)
// }
// req, _ := p.S3.GetObjectRequest(&s3.GetObjectInput{
// Bucket: aws.String(p.Bucket),
// Key: aws.String(p.objectKey(ou.Hostname(), ou.Path)),
// })
// su, err := req.Presign(duration)
// if err != nil {
// return "", err
// }
// return su, nil
// }
func generateTempKey() (string, error) {
data := make([]byte, 1024)
if _, err := rand.Read(data); err != nil {
return "", err
}
hash := sha256.Sum256(data)
return fmt.Sprintf("tmp/%s", hex.EncodeToString(hash[:])[0:30]), nil
}

View File

@ -0,0 +1,5 @@
package convox
const (
GroupName = "convox.com"
)

View File

@ -0,0 +1,4 @@
// +k8s:deepcopy-gen=package
// +groupName=convox.com
package v1

View File

@ -0,0 +1,37 @@
package v1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
convox "github.com/convox/convox/provider/k8s/pkg/apis/convox"
)
// SchemeGroupVersion is group version used to register these objects
var SchemeGroupVersion = schema.GroupVersion{Group: convox.GroupName, Version: "v1"}
// Kind takes an unqualified kind and returns back a Group qualified GroupKind
func Kind(kind string) schema.GroupKind {
return SchemeGroupVersion.WithKind(kind).GroupKind()
}
// Resource takes an unqualified resource and returns a Group qualified GroupResource
func Resource(resource string) schema.GroupResource {
return SchemeGroupVersion.WithResource(resource).GroupResource()
}
var (
SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)
AddToScheme = SchemeBuilder.AddToScheme
)
// Adds the list of known types to Scheme.
func addKnownTypes(scheme *runtime.Scheme) error {
scheme.AddKnownTypes(SchemeGroupVersion,
&Stack{},
&StackList{},
)
metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
return nil
}

View File

@ -0,0 +1,35 @@
package v1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// +genclient
// +genclient:noStatus
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type Stack struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec StackSpec `json:"spec,omitempty"`
Outputs map[string]string `json:"outputs,omitempty"`
Status StackStatus `json:"status,omitempty"`
}
type StackSpec struct {
Parameters map[string]string `json:"parameters"`
Template string `json:"template"`
}
type StackStatus string
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type StackList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata"`
Items []Stack `json:"items"`
}

View File

@ -0,0 +1,115 @@
// +build !ignore_autogenerated
/*
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Code generated by deepcopy-gen. DO NOT EDIT.
package v1
import (
runtime "k8s.io/apimachinery/pkg/runtime"
)
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Stack) DeepCopyInto(out *Stack) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec)
if in.Outputs != nil {
in, out := &in.Outputs, &out.Outputs
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Stack.
func (in *Stack) DeepCopy() *Stack {
if in == nil {
return nil
}
out := new(Stack)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *Stack) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *StackList) DeepCopyInto(out *StackList) {
*out = *in
out.TypeMeta = in.TypeMeta
out.ListMeta = in.ListMeta
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]Stack, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StackList.
func (in *StackList) DeepCopy() *StackList {
if in == nil {
return nil
}
out := new(StackList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *StackList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *StackSpec) DeepCopyInto(out *StackSpec) {
*out = *in
if in.Parameters != nil {
in, out := &in.Parameters, &out.Parameters
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StackSpec.
func (in *StackSpec) DeepCopy() *StackSpec {
if in == nil {
return nil
}
out := new(StackSpec)
in.DeepCopyInto(out)
return out
}

View File

@ -0,0 +1,98 @@
/*
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Code generated by client-gen. DO NOT EDIT.
package versioned
import (
convoxv1 "github.com/convox/convox/provider/aws/pkg/client/clientset/versioned/typed/convox/v1"
discovery "k8s.io/client-go/discovery"
rest "k8s.io/client-go/rest"
flowcontrol "k8s.io/client-go/util/flowcontrol"
)
type Interface interface {
Discovery() discovery.DiscoveryInterface
ConvoxV1() convoxv1.ConvoxV1Interface
// Deprecated: please explicitly pick a version if possible.
Convox() convoxv1.ConvoxV1Interface
}
// Clientset contains the clients for groups. Each group has exactly one
// version included in a Clientset.
type Clientset struct {
*discovery.DiscoveryClient
convoxV1 *convoxv1.ConvoxV1Client
}
// ConvoxV1 retrieves the ConvoxV1Client
func (c *Clientset) ConvoxV1() convoxv1.ConvoxV1Interface {
return c.convoxV1
}
// Deprecated: Convox retrieves the default version of ConvoxClient.
// Please explicitly pick a version.
func (c *Clientset) Convox() convoxv1.ConvoxV1Interface {
return c.convoxV1
}
// Discovery retrieves the DiscoveryClient
func (c *Clientset) Discovery() discovery.DiscoveryInterface {
if c == nil {
return nil
}
return c.DiscoveryClient
}
// NewForConfig creates a new Clientset for the given config.
func NewForConfig(c *rest.Config) (*Clientset, error) {
configShallowCopy := *c
if configShallowCopy.RateLimiter == nil && configShallowCopy.QPS > 0 {
configShallowCopy.RateLimiter = flowcontrol.NewTokenBucketRateLimiter(configShallowCopy.QPS, configShallowCopy.Burst)
}
var cs Clientset
var err error
cs.convoxV1, err = convoxv1.NewForConfig(&configShallowCopy)
if err != nil {
return nil, err
}
cs.DiscoveryClient, err = discovery.NewDiscoveryClientForConfig(&configShallowCopy)
if err != nil {
return nil, err
}
return &cs, nil
}
// NewForConfigOrDie creates a new Clientset for the given config and
// panics if there is an error in the config.
func NewForConfigOrDie(c *rest.Config) *Clientset {
var cs Clientset
cs.convoxV1 = convoxv1.NewForConfigOrDie(c)
cs.DiscoveryClient = discovery.NewDiscoveryClientForConfigOrDie(c)
return &cs
}
// New creates a new Clientset for the given RESTClient.
func New(c rest.Interface) *Clientset {
var cs Clientset
cs.convoxV1 = convoxv1.New(c)
cs.DiscoveryClient = discovery.NewDiscoveryClient(c)
return &cs
}

View File

@ -0,0 +1,20 @@
/*
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Code generated by client-gen. DO NOT EDIT.
// This package has the automatically generated clientset.
package versioned

View File

@ -0,0 +1,82 @@
/*
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Code generated by client-gen. DO NOT EDIT.
package fake
import (
clientset "github.com/convox/convox/provider/aws/pkg/client/clientset/versioned"
convoxv1 "github.com/convox/convox/provider/aws/pkg/client/clientset/versioned/typed/convox/v1"
fakeconvoxv1 "github.com/convox/convox/provider/aws/pkg/client/clientset/versioned/typed/convox/v1/fake"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/watch"
"k8s.io/client-go/discovery"
fakediscovery "k8s.io/client-go/discovery/fake"
"k8s.io/client-go/testing"
)
// NewSimpleClientset returns a clientset that will respond with the provided objects.
// It's backed by a very simple object tracker that processes creates, updates and deletions as-is,
// without applying any validations and/or defaults. It shouldn't be considered a replacement
// for a real clientset and is mostly useful in simple unit tests.
func NewSimpleClientset(objects ...runtime.Object) *Clientset {
o := testing.NewObjectTracker(scheme, codecs.UniversalDecoder())
for _, obj := range objects {
if err := o.Add(obj); err != nil {
panic(err)
}
}
cs := &Clientset{}
cs.discovery = &fakediscovery.FakeDiscovery{Fake: &cs.Fake}
cs.AddReactor("*", "*", testing.ObjectReaction(o))
cs.AddWatchReactor("*", func(action testing.Action) (handled bool, ret watch.Interface, err error) {
gvr := action.GetResource()
ns := action.GetNamespace()
watch, err := o.Watch(gvr, ns)
if err != nil {
return false, nil, err
}
return true, watch, nil
})
return cs
}
// Clientset implements clientset.Interface. Meant to be embedded into a
// struct to get a default implementation. This makes faking out just the method
// you want to test easier.
type Clientset struct {
testing.Fake
discovery *fakediscovery.FakeDiscovery
}
func (c *Clientset) Discovery() discovery.DiscoveryInterface {
return c.discovery
}
var _ clientset.Interface = &Clientset{}
// ConvoxV1 retrieves the ConvoxV1Client
func (c *Clientset) ConvoxV1() convoxv1.ConvoxV1Interface {
return &fakeconvoxv1.FakeConvoxV1{Fake: &c.Fake}
}
// Convox retrieves the ConvoxV1Client
func (c *Clientset) Convox() convoxv1.ConvoxV1Interface {
return &fakeconvoxv1.FakeConvoxV1{Fake: &c.Fake}
}

View File

@ -0,0 +1,20 @@
/*
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Code generated by client-gen. DO NOT EDIT.
// This package has the automatically generated fake clientset.
package fake

View File

@ -0,0 +1,54 @@
/*
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Code generated by client-gen. DO NOT EDIT.
package fake
import (
convoxv1 "github.com/convox/convox/provider/aws/pkg/apis/convox/v1"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
runtime "k8s.io/apimachinery/pkg/runtime"
schema "k8s.io/apimachinery/pkg/runtime/schema"
serializer "k8s.io/apimachinery/pkg/runtime/serializer"
)
var scheme = runtime.NewScheme()
var codecs = serializer.NewCodecFactory(scheme)
var parameterCodec = runtime.NewParameterCodec(scheme)
func init() {
v1.AddToGroupVersion(scheme, schema.GroupVersion{Version: "v1"})
AddToScheme(scheme)
}
// AddToScheme adds all types of this clientset into the given scheme. This allows composition
// of clientsets, like in:
//
// import (
// "k8s.io/client-go/kubernetes"
// clientsetscheme "k8s.io/client-go/kubernetes/scheme"
// aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme"
// )
//
// kclientset, _ := kubernetes.NewForConfig(c)
// aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme)
//
// After this, RawExtensions in Kubernetes types will serialize kube-aggregator types
// correctly.
func AddToScheme(scheme *runtime.Scheme) {
convoxv1.AddToScheme(scheme)
}

View File

@ -0,0 +1,20 @@
/*
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Code generated by client-gen. DO NOT EDIT.
// This package contains the scheme of the automatically generated clientset.
package scheme

View File

@ -0,0 +1,54 @@
/*
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Code generated by client-gen. DO NOT EDIT.
package scheme
import (
convoxv1 "github.com/convox/convox/provider/aws/pkg/apis/convox/v1"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
runtime "k8s.io/apimachinery/pkg/runtime"
schema "k8s.io/apimachinery/pkg/runtime/schema"
serializer "k8s.io/apimachinery/pkg/runtime/serializer"
)
var Scheme = runtime.NewScheme()
var Codecs = serializer.NewCodecFactory(Scheme)
var ParameterCodec = runtime.NewParameterCodec(Scheme)
func init() {
v1.AddToGroupVersion(Scheme, schema.GroupVersion{Version: "v1"})
AddToScheme(Scheme)
}
// AddToScheme adds all types of this clientset into the given scheme. This allows composition
// of clientsets, like in:
//
// import (
// "k8s.io/client-go/kubernetes"
// clientsetscheme "k8s.io/client-go/kubernetes/scheme"
// aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme"
// )
//
// kclientset, _ := kubernetes.NewForConfig(c)
// aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme)
//
// After this, RawExtensions in Kubernetes types will serialize kube-aggregator types
// correctly.
func AddToScheme(scheme *runtime.Scheme) {
convoxv1.AddToScheme(scheme)
}

View File

@ -0,0 +1,90 @@
/*
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Code generated by client-gen. DO NOT EDIT.
package v1
import (
v1 "github.com/convox/convox/provider/aws/pkg/apis/convox/v1"
"github.com/convox/convox/provider/aws/pkg/client/clientset/versioned/scheme"
serializer "k8s.io/apimachinery/pkg/runtime/serializer"
rest "k8s.io/client-go/rest"
)
type ConvoxV1Interface interface {
RESTClient() rest.Interface
StacksGetter
}
// ConvoxV1Client is used to interact with features provided by the convox.com group.
type ConvoxV1Client struct {
restClient rest.Interface
}
func (c *ConvoxV1Client) Stacks(namespace string) StackInterface {
return newStacks(c, namespace)
}
// NewForConfig creates a new ConvoxV1Client for the given config.
func NewForConfig(c *rest.Config) (*ConvoxV1Client, error) {
config := *c
if err := setConfigDefaults(&config); err != nil {
return nil, err
}
client, err := rest.RESTClientFor(&config)
if err != nil {
return nil, err
}
return &ConvoxV1Client{client}, nil
}
// NewForConfigOrDie creates a new ConvoxV1Client for the given config and
// panics if there is an error in the config.
func NewForConfigOrDie(c *rest.Config) *ConvoxV1Client {
client, err := NewForConfig(c)
if err != nil {
panic(err)
}
return client
}
// New creates a new ConvoxV1Client for the given RESTClient.
func New(c rest.Interface) *ConvoxV1Client {
return &ConvoxV1Client{c}
}
func setConfigDefaults(config *rest.Config) error {
gv := v1.SchemeGroupVersion
config.GroupVersion = &gv
config.APIPath = "/apis"
config.NegotiatedSerializer = serializer.DirectCodecFactory{CodecFactory: scheme.Codecs}
if config.UserAgent == "" {
config.UserAgent = rest.DefaultKubernetesUserAgent()
}
return nil
}
// RESTClient returns a RESTClient that is used to communicate
// with API server by this client implementation.
func (c *ConvoxV1Client) RESTClient() rest.Interface {
if c == nil {
return nil
}
return c.restClient
}

View File

@ -0,0 +1,20 @@
/*
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Code generated by client-gen. DO NOT EDIT.
// This package has the automatically generated typed clients.
package v1

View File

@ -0,0 +1,20 @@
/*
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Code generated by client-gen. DO NOT EDIT.
// Package fake has the automatically generated clients.
package fake

View File

@ -0,0 +1,40 @@
/*
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Code generated by client-gen. DO NOT EDIT.
package fake
import (
v1 "github.com/convox/convox/provider/aws/pkg/client/clientset/versioned/typed/convox/v1"
rest "k8s.io/client-go/rest"
testing "k8s.io/client-go/testing"
)
type FakeConvoxV1 struct {
*testing.Fake
}
func (c *FakeConvoxV1) Stacks(namespace string) v1.StackInterface {
return &FakeStacks{c, namespace}
}
// RESTClient returns a RESTClient that is used to communicate
// with API server by this client implementation.
func (c *FakeConvoxV1) RESTClient() rest.Interface {
var ret *rest.RESTClient
return ret
}

View File

@ -0,0 +1,128 @@
/*
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Code generated by client-gen. DO NOT EDIT.
package fake
import (
convoxv1 "github.com/convox/convox/provider/aws/pkg/apis/convox/v1"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
labels "k8s.io/apimachinery/pkg/labels"
schema "k8s.io/apimachinery/pkg/runtime/schema"
types "k8s.io/apimachinery/pkg/types"
watch "k8s.io/apimachinery/pkg/watch"
testing "k8s.io/client-go/testing"
)
// FakeStacks implements StackInterface
type FakeStacks struct {
Fake *FakeConvoxV1
ns string
}
var stacksResource = schema.GroupVersionResource{Group: "convox.com", Version: "v1", Resource: "stacks"}
var stacksKind = schema.GroupVersionKind{Group: "convox.com", Version: "v1", Kind: "Stack"}
// Get takes name of the stack, and returns the corresponding stack object, and an error if there is any.
func (c *FakeStacks) Get(name string, options v1.GetOptions) (result *convoxv1.Stack, err error) {
obj, err := c.Fake.
Invokes(testing.NewGetAction(stacksResource, c.ns, name), &convoxv1.Stack{})
if obj == nil {
return nil, err
}
return obj.(*convoxv1.Stack), err
}
// List takes label and field selectors, and returns the list of Stacks that match those selectors.
func (c *FakeStacks) List(opts v1.ListOptions) (result *convoxv1.StackList, err error) {
obj, err := c.Fake.
Invokes(testing.NewListAction(stacksResource, stacksKind, c.ns, opts), &convoxv1.StackList{})
if obj == nil {
return nil, err
}
label, _, _ := testing.ExtractFromListOptions(opts)
if label == nil {
label = labels.Everything()
}
list := &convoxv1.StackList{ListMeta: obj.(*convoxv1.StackList).ListMeta}
for _, item := range obj.(*convoxv1.StackList).Items {
if label.Matches(labels.Set(item.Labels)) {
list.Items = append(list.Items, item)
}
}
return list, err
}
// Watch returns a watch.Interface that watches the requested stacks.
func (c *FakeStacks) Watch(opts v1.ListOptions) (watch.Interface, error) {
return c.Fake.
InvokesWatch(testing.NewWatchAction(stacksResource, c.ns, opts))
}
// Create takes the representation of a stack and creates it. Returns the server's representation of the stack, and an error, if there is any.
func (c *FakeStacks) Create(stack *convoxv1.Stack) (result *convoxv1.Stack, err error) {
obj, err := c.Fake.
Invokes(testing.NewCreateAction(stacksResource, c.ns, stack), &convoxv1.Stack{})
if obj == nil {
return nil, err
}
return obj.(*convoxv1.Stack), err
}
// Update takes the representation of a stack and updates it. Returns the server's representation of the stack, and an error, if there is any.
func (c *FakeStacks) Update(stack *convoxv1.Stack) (result *convoxv1.Stack, err error) {
obj, err := c.Fake.
Invokes(testing.NewUpdateAction(stacksResource, c.ns, stack), &convoxv1.Stack{})
if obj == nil {
return nil, err
}
return obj.(*convoxv1.Stack), err
}
// Delete takes name of the stack and deletes it. Returns an error if one occurs.
func (c *FakeStacks) Delete(name string, options *v1.DeleteOptions) error {
_, err := c.Fake.
Invokes(testing.NewDeleteAction(stacksResource, c.ns, name), &convoxv1.Stack{})
return err
}
// DeleteCollection deletes a collection of objects.
func (c *FakeStacks) DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error {
action := testing.NewDeleteCollectionAction(stacksResource, c.ns, listOptions)
_, err := c.Fake.Invokes(action, &convoxv1.StackList{})
return err
}
// Patch applies the patch and returns the patched stack.
func (c *FakeStacks) Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *convoxv1.Stack, err error) {
obj, err := c.Fake.
Invokes(testing.NewPatchSubresourceAction(stacksResource, c.ns, name, data, subresources...), &convoxv1.Stack{})
if obj == nil {
return nil, err
}
return obj.(*convoxv1.Stack), err
}

View File

@ -0,0 +1,21 @@
/*
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Code generated by client-gen. DO NOT EDIT.
package v1
type StackExpansion interface{}

View File

@ -0,0 +1,157 @@
/*
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Code generated by client-gen. DO NOT EDIT.
package v1
import (
v1 "github.com/convox/convox/provider/aws/pkg/apis/convox/v1"
scheme "github.com/convox/convox/provider/aws/pkg/client/clientset/versioned/scheme"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
types "k8s.io/apimachinery/pkg/types"
watch "k8s.io/apimachinery/pkg/watch"
rest "k8s.io/client-go/rest"
)
// StacksGetter has a method to return a StackInterface.
// A group's client should implement this interface.
type StacksGetter interface {
Stacks(namespace string) StackInterface
}
// StackInterface has methods to work with Stack resources.
type StackInterface interface {
Create(*v1.Stack) (*v1.Stack, error)
Update(*v1.Stack) (*v1.Stack, error)
Delete(name string, options *metav1.DeleteOptions) error
DeleteCollection(options *metav1.DeleteOptions, listOptions metav1.ListOptions) error
Get(name string, options metav1.GetOptions) (*v1.Stack, error)
List(opts metav1.ListOptions) (*v1.StackList, error)
Watch(opts metav1.ListOptions) (watch.Interface, error)
Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1.Stack, err error)
StackExpansion
}
// stacks implements StackInterface
type stacks struct {
client rest.Interface
ns string
}
// newStacks returns a Stacks
func newStacks(c *ConvoxV1Client, namespace string) *stacks {
return &stacks{
client: c.RESTClient(),
ns: namespace,
}
}
// Get takes name of the stack, and returns the corresponding stack object, and an error if there is any.
func (c *stacks) Get(name string, options metav1.GetOptions) (result *v1.Stack, err error) {
result = &v1.Stack{}
err = c.client.Get().
Namespace(c.ns).
Resource("stacks").
Name(name).
VersionedParams(&options, scheme.ParameterCodec).
Do().
Into(result)
return
}
// List takes label and field selectors, and returns the list of Stacks that match those selectors.
func (c *stacks) List(opts metav1.ListOptions) (result *v1.StackList, err error) {
result = &v1.StackList{}
err = c.client.Get().
Namespace(c.ns).
Resource("stacks").
VersionedParams(&opts, scheme.ParameterCodec).
Do().
Into(result)
return
}
// Watch returns a watch.Interface that watches the requested stacks.
func (c *stacks) Watch(opts metav1.ListOptions) (watch.Interface, error) {
opts.Watch = true
return c.client.Get().
Namespace(c.ns).
Resource("stacks").
VersionedParams(&opts, scheme.ParameterCodec).
Watch()
}
// Create takes the representation of a stack and creates it. Returns the server's representation of the stack, and an error, if there is any.
func (c *stacks) Create(stack *v1.Stack) (result *v1.Stack, err error) {
result = &v1.Stack{}
err = c.client.Post().
Namespace(c.ns).
Resource("stacks").
Body(stack).
Do().
Into(result)
return
}
// Update takes the representation of a stack and updates it. Returns the server's representation of the stack, and an error, if there is any.
func (c *stacks) Update(stack *v1.Stack) (result *v1.Stack, err error) {
result = &v1.Stack{}
err = c.client.Put().
Namespace(c.ns).
Resource("stacks").
Name(stack.Name).
Body(stack).
Do().
Into(result)
return
}
// Delete takes name of the stack and deletes it. Returns an error if one occurs.
func (c *stacks) Delete(name string, options *metav1.DeleteOptions) error {
return c.client.Delete().
Namespace(c.ns).
Resource("stacks").
Name(name).
Body(options).
Do().
Error()
}
// DeleteCollection deletes a collection of objects.
func (c *stacks) DeleteCollection(options *metav1.DeleteOptions, listOptions metav1.ListOptions) error {
return c.client.Delete().
Namespace(c.ns).
Resource("stacks").
VersionedParams(&listOptions, scheme.ParameterCodec).
Body(options).
Do().
Error()
}
// Patch applies the patch and returns the patched stack.
func (c *stacks) Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1.Stack, err error) {
result = &v1.Stack{}
err = c.client.Patch(pt).
Namespace(c.ns).
Resource("stacks").
SubResource(subresources...).
Name(name).
Body(data).
Do().
Into(result)
return
}

View File

@ -0,0 +1,46 @@
/*
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Code generated by informer-gen. DO NOT EDIT.
package convox
import (
v1 "github.com/convox/convox/provider/aws/pkg/client/informers/externalversions/convox/v1"
internalinterfaces "github.com/convox/convox/provider/aws/pkg/client/informers/externalversions/internalinterfaces"
)
// Interface provides access to each of this group's versions.
type Interface interface {
// V1 provides access to shared informers for resources in V1.
V1() v1.Interface
}
type group struct {
factory internalinterfaces.SharedInformerFactory
namespace string
tweakListOptions internalinterfaces.TweakListOptionsFunc
}
// New returns a new Interface.
func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface {
return &group{factory: f, namespace: namespace, tweakListOptions: tweakListOptions}
}
// V1 returns a new v1.Interface.
func (g *group) V1() v1.Interface {
return v1.New(g.factory, g.namespace, g.tweakListOptions)
}

View File

@ -0,0 +1,45 @@
/*
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Code generated by informer-gen. DO NOT EDIT.
package v1
import (
internalinterfaces "github.com/convox/convox/provider/aws/pkg/client/informers/externalversions/internalinterfaces"
)
// Interface provides access to all the informers in this group version.
type Interface interface {
// Stacks returns a StackInformer.
Stacks() StackInformer
}
type version struct {
factory internalinterfaces.SharedInformerFactory
namespace string
tweakListOptions internalinterfaces.TweakListOptionsFunc
}
// New returns a new Interface.
func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface {
return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions}
}
// Stacks returns a StackInformer.
func (v *version) Stacks() StackInformer {
return &stackInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions}
}

View File

@ -0,0 +1,89 @@
/*
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Code generated by informer-gen. DO NOT EDIT.
package v1
import (
time "time"
convoxv1 "github.com/convox/convox/provider/aws/pkg/apis/convox/v1"
versioned "github.com/convox/convox/provider/aws/pkg/client/clientset/versioned"
internalinterfaces "github.com/convox/convox/provider/aws/pkg/client/informers/externalversions/internalinterfaces"
v1 "github.com/convox/convox/provider/aws/pkg/client/listers/convox/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
runtime "k8s.io/apimachinery/pkg/runtime"
watch "k8s.io/apimachinery/pkg/watch"
cache "k8s.io/client-go/tools/cache"
)
// StackInformer provides access to a shared informer and lister for
// Stacks.
type StackInformer interface {
Informer() cache.SharedIndexInformer
Lister() v1.StackLister
}
type stackInformer struct {
factory internalinterfaces.SharedInformerFactory
tweakListOptions internalinterfaces.TweakListOptionsFunc
namespace string
}
// NewStackInformer constructs a new informer for Stack type.
// Always prefer using an informer factory to get a shared informer instead of getting an independent
// one. This reduces memory footprint and number of connections to the server.
func NewStackInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer {
return NewFilteredStackInformer(client, namespace, resyncPeriod, indexers, nil)
}
// NewFilteredStackInformer constructs a new informer for Stack type.
// Always prefer using an informer factory to get a shared informer instead of getting an independent
// one. This reduces memory footprint and number of connections to the server.
func NewFilteredStackInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer {
return cache.NewSharedIndexInformer(
&cache.ListWatch{
ListFunc: func(options metav1.ListOptions) (runtime.Object, error) {
if tweakListOptions != nil {
tweakListOptions(&options)
}
return client.ConvoxV1().Stacks(namespace).List(options)
},
WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) {
if tweakListOptions != nil {
tweakListOptions(&options)
}
return client.ConvoxV1().Stacks(namespace).Watch(options)
},
},
&convoxv1.Stack{},
resyncPeriod,
indexers,
)
}
func (f *stackInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer {
return NewFilteredStackInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions)
}
func (f *stackInformer) Informer() cache.SharedIndexInformer {
return f.factory.InformerFor(&convoxv1.Stack{}, f.defaultInformer)
}
func (f *stackInformer) Lister() v1.StackLister {
return v1.NewStackLister(f.Informer().GetIndexer())
}

View File

@ -0,0 +1,180 @@
/*
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Code generated by informer-gen. DO NOT EDIT.
package externalversions
import (
reflect "reflect"
sync "sync"
time "time"
versioned "github.com/convox/convox/provider/aws/pkg/client/clientset/versioned"
convox "github.com/convox/convox/provider/aws/pkg/client/informers/externalversions/convox"
internalinterfaces "github.com/convox/convox/provider/aws/pkg/client/informers/externalversions/internalinterfaces"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
runtime "k8s.io/apimachinery/pkg/runtime"
schema "k8s.io/apimachinery/pkg/runtime/schema"
cache "k8s.io/client-go/tools/cache"
)
// SharedInformerOption defines the functional option type for SharedInformerFactory.
type SharedInformerOption func(*sharedInformerFactory) *sharedInformerFactory
type sharedInformerFactory struct {
client versioned.Interface
namespace string
tweakListOptions internalinterfaces.TweakListOptionsFunc
lock sync.Mutex
defaultResync time.Duration
customResync map[reflect.Type]time.Duration
informers map[reflect.Type]cache.SharedIndexInformer
// startedInformers is used for tracking which informers have been started.
// This allows Start() to be called multiple times safely.
startedInformers map[reflect.Type]bool
}
// WithCustomResyncConfig sets a custom resync period for the specified informer types.
func WithCustomResyncConfig(resyncConfig map[v1.Object]time.Duration) SharedInformerOption {
return func(factory *sharedInformerFactory) *sharedInformerFactory {
for k, v := range resyncConfig {
factory.customResync[reflect.TypeOf(k)] = v
}
return factory
}
}
// WithTweakListOptions sets a custom filter on all listers of the configured SharedInformerFactory.
func WithTweakListOptions(tweakListOptions internalinterfaces.TweakListOptionsFunc) SharedInformerOption {
return func(factory *sharedInformerFactory) *sharedInformerFactory {
factory.tweakListOptions = tweakListOptions
return factory
}
}
// WithNamespace limits the SharedInformerFactory to the specified namespace.
func WithNamespace(namespace string) SharedInformerOption {
return func(factory *sharedInformerFactory) *sharedInformerFactory {
factory.namespace = namespace
return factory
}
}
// NewSharedInformerFactory constructs a new instance of sharedInformerFactory for all namespaces.
func NewSharedInformerFactory(client versioned.Interface, defaultResync time.Duration) SharedInformerFactory {
return NewSharedInformerFactoryWithOptions(client, defaultResync)
}
// NewFilteredSharedInformerFactory constructs a new instance of sharedInformerFactory.
// Listers obtained via this SharedInformerFactory will be subject to the same filters
// as specified here.
// Deprecated: Please use NewSharedInformerFactoryWithOptions instead
func NewFilteredSharedInformerFactory(client versioned.Interface, defaultResync time.Duration, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) SharedInformerFactory {
return NewSharedInformerFactoryWithOptions(client, defaultResync, WithNamespace(namespace), WithTweakListOptions(tweakListOptions))
}
// NewSharedInformerFactoryWithOptions constructs a new instance of a SharedInformerFactory with additional options.
func NewSharedInformerFactoryWithOptions(client versioned.Interface, defaultResync time.Duration, options ...SharedInformerOption) SharedInformerFactory {
factory := &sharedInformerFactory{
client: client,
namespace: v1.NamespaceAll,
defaultResync: defaultResync,
informers: make(map[reflect.Type]cache.SharedIndexInformer),
startedInformers: make(map[reflect.Type]bool),
customResync: make(map[reflect.Type]time.Duration),
}
// Apply all options
for _, opt := range options {
factory = opt(factory)
}
return factory
}
// Start initializes all requested informers.
func (f *sharedInformerFactory) Start(stopCh <-chan struct{}) {
f.lock.Lock()
defer f.lock.Unlock()
for informerType, informer := range f.informers {
if !f.startedInformers[informerType] {
go informer.Run(stopCh)
f.startedInformers[informerType] = true
}
}
}
// WaitForCacheSync waits for all started informers' cache were synced.
func (f *sharedInformerFactory) WaitForCacheSync(stopCh <-chan struct{}) map[reflect.Type]bool {
informers := func() map[reflect.Type]cache.SharedIndexInformer {
f.lock.Lock()
defer f.lock.Unlock()
informers := map[reflect.Type]cache.SharedIndexInformer{}
for informerType, informer := range f.informers {
if f.startedInformers[informerType] {
informers[informerType] = informer
}
}
return informers
}()
res := map[reflect.Type]bool{}
for informType, informer := range informers {
res[informType] = cache.WaitForCacheSync(stopCh, informer.HasSynced)
}
return res
}
// InternalInformerFor returns the SharedIndexInformer for obj using an internal
// client.
func (f *sharedInformerFactory) InformerFor(obj runtime.Object, newFunc internalinterfaces.NewInformerFunc) cache.SharedIndexInformer {
f.lock.Lock()
defer f.lock.Unlock()
informerType := reflect.TypeOf(obj)
informer, exists := f.informers[informerType]
if exists {
return informer
}
resyncPeriod, exists := f.customResync[informerType]
if !exists {
resyncPeriod = f.defaultResync
}
informer = newFunc(f.client, resyncPeriod)
f.informers[informerType] = informer
return informer
}
// SharedInformerFactory provides shared informers for resources in all known
// API group versions.
type SharedInformerFactory interface {
internalinterfaces.SharedInformerFactory
ForResource(resource schema.GroupVersionResource) (GenericInformer, error)
WaitForCacheSync(stopCh <-chan struct{}) map[reflect.Type]bool
Convox() convox.Interface
}
func (f *sharedInformerFactory) Convox() convox.Interface {
return convox.New(f, f.namespace, f.tweakListOptions)
}

View File

@ -0,0 +1,62 @@
/*
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Code generated by informer-gen. DO NOT EDIT.
package externalversions
import (
"fmt"
v1 "github.com/convox/convox/provider/aws/pkg/apis/convox/v1"
schema "k8s.io/apimachinery/pkg/runtime/schema"
cache "k8s.io/client-go/tools/cache"
)
// GenericInformer is type of SharedIndexInformer which will locate and delegate to other
// sharedInformers based on type
type GenericInformer interface {
Informer() cache.SharedIndexInformer
Lister() cache.GenericLister
}
type genericInformer struct {
informer cache.SharedIndexInformer
resource schema.GroupResource
}
// Informer returns the SharedIndexInformer.
func (f *genericInformer) Informer() cache.SharedIndexInformer {
return f.informer
}
// Lister returns the GenericLister.
func (f *genericInformer) Lister() cache.GenericLister {
return cache.NewGenericLister(f.Informer().GetIndexer(), f.resource)
}
// ForResource gives generic access to a shared informer of the matching type
// TODO extend this to unknown resources with a client pool
func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource) (GenericInformer, error) {
switch resource {
// Group=convox.com, Version=v1
case v1.SchemeGroupVersion.WithResource("stacks"):
return &genericInformer{resource: resource.GroupResource(), informer: f.Convox().V1().Stacks().Informer()}, nil
}
return nil, fmt.Errorf("no informer found for %v", resource)
}

View File

@ -0,0 +1,38 @@
/*
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Code generated by informer-gen. DO NOT EDIT.
package internalinterfaces
import (
time "time"
versioned "github.com/convox/convox/provider/aws/pkg/client/clientset/versioned"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
runtime "k8s.io/apimachinery/pkg/runtime"
cache "k8s.io/client-go/tools/cache"
)
type NewInformerFunc func(versioned.Interface, time.Duration) cache.SharedIndexInformer
// SharedInformerFactory a small interface to allow for adding an informer without an import cycle
type SharedInformerFactory interface {
Start(stopCh <-chan struct{})
InformerFor(obj runtime.Object, newFunc NewInformerFunc) cache.SharedIndexInformer
}
type TweakListOptionsFunc func(*v1.ListOptions)

View File

@ -0,0 +1,27 @@
/*
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Code generated by lister-gen. DO NOT EDIT.
package v1
// StackListerExpansion allows custom methods to be added to
// StackLister.
type StackListerExpansion interface{}
// StackNamespaceListerExpansion allows custom methods to be added to
// StackNamespaceLister.
type StackNamespaceListerExpansion interface{}

View File

@ -0,0 +1,94 @@
/*
Copyright The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Code generated by lister-gen. DO NOT EDIT.
package v1
import (
v1 "github.com/convox/convox/provider/aws/pkg/apis/convox/v1"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/client-go/tools/cache"
)
// StackLister helps list Stacks.
type StackLister interface {
// List lists all Stacks in the indexer.
List(selector labels.Selector) (ret []*v1.Stack, err error)
// Stacks returns an object that can list and get Stacks.
Stacks(namespace string) StackNamespaceLister
StackListerExpansion
}
// stackLister implements the StackLister interface.
type stackLister struct {
indexer cache.Indexer
}
// NewStackLister returns a new StackLister.
func NewStackLister(indexer cache.Indexer) StackLister {
return &stackLister{indexer: indexer}
}
// List lists all Stacks in the indexer.
func (s *stackLister) List(selector labels.Selector) (ret []*v1.Stack, err error) {
err = cache.ListAll(s.indexer, selector, func(m interface{}) {
ret = append(ret, m.(*v1.Stack))
})
return ret, err
}
// Stacks returns an object that can list and get Stacks.
func (s *stackLister) Stacks(namespace string) StackNamespaceLister {
return stackNamespaceLister{indexer: s.indexer, namespace: namespace}
}
// StackNamespaceLister helps list and get Stacks.
type StackNamespaceLister interface {
// List lists all Stacks in the indexer for a given namespace.
List(selector labels.Selector) (ret []*v1.Stack, err error)
// Get retrieves the Stack from the indexer for a given namespace and name.
Get(name string) (*v1.Stack, error)
StackNamespaceListerExpansion
}
// stackNamespaceLister implements the StackNamespaceLister
// interface.
type stackNamespaceLister struct {
indexer cache.Indexer
namespace string
}
// List lists all Stacks in the indexer for a given namespace.
func (s stackNamespaceLister) List(selector labels.Selector) (ret []*v1.Stack, err error) {
err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) {
ret = append(ret, m.(*v1.Stack))
})
return ret, err
}
// Get retrieves the Stack from the indexer for a given namespace and name.
func (s stackNamespaceLister) Get(name string) (*v1.Stack, error) {
obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name)
if err != nil {
return nil, err
}
if !exists {
return nil, errors.NewNotFound(v1.Resource("stack"), name)
}
return obj.(*v1.Stack), nil
}

View File

@ -0,0 +1,59 @@
package aws
import (
"encoding/base64"
"fmt"
"regexp"
"strings"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ecr"
)
var (
reECRHost = regexp.MustCompile(`(\d+)\.dkr\.ecr\.([^.]+)\.amazonaws\.com`)
)
func (p *Provider) RepositoryAuth(app string) (string, string, error) {
repo, _, err := p.RepositoryHost(app)
if err != nil {
return "", "", err
}
if !reECRHost.MatchString(repo) {
return "", "", fmt.Errorf("invalid ecr repository: %s", repo)
}
registry := reECRHost.FindStringSubmatch(repo)
res, err := p.ECR.GetAuthorizationToken(&ecr.GetAuthorizationTokenInput{
RegistryIds: []*string{aws.String(registry[1])},
})
if err != nil {
return "", "", err
}
if len(res.AuthorizationData) != 1 {
return "", "", fmt.Errorf("no authorization data")
}
token, err := base64.StdEncoding.DecodeString(*res.AuthorizationData[0].AuthorizationToken)
if err != nil {
return "", "", err
}
parts := strings.SplitN(string(token), ":", 2)
if len(parts) != 2 {
return "", "", fmt.Errorf("invalid auth data")
}
return parts[0], parts[1], nil
}
func (p *Provider) RepositoryHost(app string) (string, bool, error) {
registry, err := p.appRegistry(app)
if err != nil {
return "", false, err
}
return registry, true, nil
}

7
provider/aws/resolver.go Normal file
View File

@ -0,0 +1,7 @@
package aws
import "fmt"
func (p *Provider) Resolver() (string, error) {
return "", fmt.Errorf("no resolver")
}

21
provider/aws/resource.go Normal file
View File

@ -0,0 +1,21 @@
package aws
import (
"fmt"
"github.com/convox/convox/pkg/manifest"
)
func (p *Provider) ResourceRender(app string, r manifest.Resource) ([]byte, error) {
params := map[string]interface{}{
"App": app,
"Namespace": p.AppNamespace(app),
"Name": r.Name,
"Parameters": r.Options,
// "Password": fmt.Sprintf("%x", sha256.Sum256([]byte(p.StackId)))[0:30],
"Password": "temp",
"Rack": p.Name,
}
return p.RenderTemplate(fmt.Sprintf("resource/%s", r.Type), params)
}

11
provider/aws/service.go Normal file
View File

@ -0,0 +1,11 @@
package aws
import (
"fmt"
"github.com/convox/convox/pkg/manifest"
)
func (p *Provider) ServiceHost(app string, s manifest.Service) string {
return fmt.Sprintf("%s.%s.%s", s.Name, app, p.Domain)
}

430
provider/aws/system.go Normal file
View File

@ -0,0 +1,430 @@
package aws
import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/cloudformation"
"github.com/convox/convox/pkg/common"
"github.com/convox/convox/pkg/structs"
"github.com/convox/convox/provider/k8s"
)
const (
cfnTemplate = "https://convox.s3.amazonaws.com/release/%s/provider/kaws/cfn/rack.yml"
)
var (
systemTemplates = []string{"api", "autoscale", "calico", "custom", "iam", "router"}
)
func (p *Provider) SystemHost() string {
return p.Domain
}
// func (p *Provider) SystemInstall(w io.Writer, opts structs.SystemInstallOptions) (string, error) {
// if err := checkKubectl(); err != nil {
// return "", err
// }
// if err := common.AwsCredentialsLoad(); err != nil {
// return "", err
// }
// name := common.DefaultString(opts.Name, "convox")
// version := common.DefaultString(opts.Version, "dev")
// params := map[string]string{}
// // if opts.Id != nil {
// // params["ClientId"] = *opts.Id
// // }
// if opts.Parameters != nil {
// for k, v := range opts.Parameters {
// params[k] = v
// }
// }
// raw := common.DefaultBool(opts.Raw, false)
// password := params["Password"]
// delete(params, "Password")
// if password == "" {
// pw, err := common.RandomString(40)
// if err != nil {
// return "", err
// }
// password = pw
// }
// var stackTotal int
// if !raw {
// fmt.Fprintf(w, "Preparing... ")
// }
// var bar *pb.ProgressBar
// s, err := session.NewSession()
// if err != nil {
// return "", err
// }
// cf := cloudformation.New(s)
// template := fmt.Sprintf(cfnTemplate, version)
// tags := map[string]string{
// "rack": name,
// "system": "convox",
// }
// err = common.CloudformationInstall(cf, name, template, params, tags, func(current, total int) {
// stackTotal = total
// if raw {
// fmt.Fprintf(w, "{ \"stack\": %q, \"current\": %d, \"total\": %d }\n", name, current, total+2)
// return
// }
// if bar == nil {
// fmt.Fprintf(w, "OK\n")
// bar = pb.New(total + 1)
// bar.Format(" ██ ")
// bar.Output = w
// bar.Prefix("Installing...")
// bar.ShowBar = false
// bar.ShowCounters = false
// bar.ShowTimeLeft = false
// bar.Start()
// }
// bar.Set(current)
// })
// if err != nil {
// return "", err
// }
// outputs, err := awsStackOutputs(name)
// if err != nil {
// return "", err
// }
// p.applyOutputs(outputs)
// config, err := writeKubeConfig(outputs)
// if err != nil {
// return "", err
// }
// os.Setenv("KUBECONFIG", config)
// if _, err := p.Provider.SystemInstall(w, opts); err != nil {
// return "", err
// }
// p.Name = name
// p.Password = password
// p.Region = outputs["Region"]
// if err := p.systemUpdate(version); err != nil {
// return "", err
// }
// time.Sleep(10 * time.Second)
// if err := p.initializeIamRoles(); err != nil {
// return "", err
// }
// url := fmt.Sprintf("https://convox:%s@%s", password, p.SystemHost())
// if raw {
// fmt.Fprintf(w, "{ \"stack\": %q, \"current\": %d, \"total\": %d }\n", name, stackTotal+1, stackTotal+2)
// } else {
// bar.Set(stackTotal + 1)
// bar.Finish()
// fmt.Fprintf(w, "Starting... ")
// }
// if err := common.EndpointWait(url); err != nil {
// return "", err
// }
// if raw {
// fmt.Fprintf(w, "{ \"stack\": %q, \"current\": %d, \"total\": %d }\n", name, stackTotal+2, stackTotal+2)
// } else {
// fmt.Fprintf(w, "OK, %s\n", p.SystemHost())
// }
// return url, nil
// }
func (p *Provider) SystemStatus() (string, error) {
return "running", nil
}
func (p *Provider) SystemTemplate(version string) ([]byte, error) {
params := map[string]interface{}{
"Version": version,
}
ts := [][]byte{}
data, err := p.Provider.SystemTemplate(version)
if err != nil {
return nil, err
}
ts = append(ts, data)
for _, st := range systemTemplates {
data, err := p.RenderTemplate(fmt.Sprintf("system/%s", st), params)
if err != nil {
return nil, err
}
ldata, err := k8s.ApplyLabels(data, "system=convox,provider=kaws")
if err != nil {
return nil, err
}
ts = append(ts, ldata)
}
return bytes.Join(ts, []byte("---\n")), nil
}
func (p *Provider) SystemUninstall(name string, w io.Writer, opts structs.SystemUninstallOptions) error {
s, err := session.NewSession()
if err != nil {
return err
}
cf := cloudformation.New(s)
res, err := cf.DescribeStacks(&cloudformation.DescribeStacksInput{
StackName: aws.String(name),
})
if err != nil {
return err
}
if len(res.Stacks) != 1 {
return fmt.Errorf("no such stack: %s", name)
}
tags := map[string]string{}
for _, t := range res.Stacks[0].Tags {
tags[*t.Key] = *t.Value
}
if tags["system"] != "convox" {
return fmt.Errorf("stack is not a convox rack: %s", name)
}
if opts.Input != nil && !common.DefaultBool(opts.Force, false) {
fmt.Fprintf(w, "Delete %s? [y/N]: ", name)
answer, err := bufio.NewReader(opts.Input).ReadString('\n')
if err != nil {
return err
}
if strings.ToLower(strings.TrimSpace(answer)) != "y" {
return fmt.Errorf("aborting")
}
}
fmt.Fprintf(w, "Deleting %s... ", name)
if err := common.CloudformationUninstall(cf, name); err != nil {
return err
}
fmt.Fprintf(w, "OK")
return nil
}
// func (p *Provider) SystemUpdate(opts structs.SystemUpdateOptions) error {
// template := fmt.Sprintf(cfnTemplate, common.DefaultString(opts.Version, p.Provider.Version))
// if err := common.CloudformationUpdate(p.CloudFormation, p.Name, template, nil, nil, p.EventTopic); err != nil {
// return err
// }
// if err := p.Provider.SystemUpdate(opts); err != nil {
// return err
// }
// return nil
// }
func (p *Provider) systemTemplate(version string) ([]byte, error) {
switch version {
case "dev":
return p.Provider.SystemTemplateLocal("kaws", version)
default:
return p.Provider.SystemTemplateRemote("kaws", version)
}
}
// func (p *Provider) systemUpdate(version string) error {
// params := map[string]interface{}{
// "AccountId": p.AccountId,
// "AdminUser": p.AdminUser,
// "AutoscalerRole": p.AutoscalerRole,
// "Cluster": p.Cluster,
// "NodesRole": p.NodesRole,
// "Password": p.Password,
// "Rack": p.Name,
// "Region": p.Region,
// "RouterCache": p.RouterCache,
// "RouterHosts": p.RouterHosts,
// "RouterRole": p.RouterRole,
// "RouterTargets": p.RouterTargets,
// "Version": version,
// }
// data, err := p.RenderTemplate("cluster", params)
// if err != nil {
// return err
// }
// if err := k8s.Apply(data); err != nil {
// return err
// }
// data, err = p.RenderTemplate("config", params)
// if err != nil {
// return err
// }
// if err := p.ApplyWait(p.Namespace, "config", version, data, fmt.Sprintf("system=convox,provider=kaws,rack=%s", p.Name), 30); err != nil {
// return err
// }
// data, err = p.systemTemplate(version)
// if err != nil {
// return err
// }
// tags := map[string]string{
// "ACCOUNT": p.AccountId,
// "CLUSTER": p.Cluster,
// "HOST": p.Domain,
// "RACK": p.Name,
// "REGION": p.Region,
// "SOCKET": "/var/run/docker.sock",
// }
// for k, v := range tags {
// data = bytes.Replace(data, []byte(fmt.Sprintf("==%s==", k)), []byte(v), -1)
// }
// if err := p.Apply(p.Namespace, "system", version, data, fmt.Sprintf("system=convox,provider=kaws,rack=%s", p.Name), 300); err != nil {
// return err
// }
// return nil
// }
func awsCommand(args ...string) ([]byte, error) {
cmd := exec.Command("aws", args...)
cmd.Env = os.Environ()
return cmd.CombinedOutput()
}
func awsStackOutputs(name string) (map[string]string, error) {
data, err := awsCommand("cloudformation", "describe-stacks", "--stack-name", name, "--query", "Stacks[0].Outputs")
if err != nil {
return nil, fmt.Errorf(strings.TrimSpace(string(data)))
}
var okvs []struct {
OutputKey string
OutputValue string
}
if err := json.Unmarshal(data, &okvs); err != nil {
return nil, err
}
os := map[string]string{}
for _, okv := range okvs {
os[okv.OutputKey] = okv.OutputValue
}
return os, nil
}
func checkKubectl() error {
ch := make(chan error, 1)
go func() { ch <- exec.Command("kubectl").Run() }()
go time.AfterFunc(3*time.Second, func() { ch <- fmt.Errorf("timeout") })
if err := <-ch; err != nil {
return fmt.Errorf("kubernetes not running or kubectl not configured, try `kubectl version`")
}
return nil
}
func writeKubeConfig(outputs map[string]string) (string, error) {
dir, err := ioutil.TempDir("", "")
if err != nil {
return "", err
}
config := filepath.Join(dir, "kubeconfig.yml")
data := []byte(fmt.Sprintf(`
apiVersion: v1
clusters:
- cluster:
server: %s
certificate-authority-data: %s
name: kubernetes
contexts:
- context:
cluster: kubernetes
user: aws
name: aws
current-context: aws
kind: Config
preferences: {}
users:
- name: aws
user:
exec:
apiVersion: client.authentication.k8s.io/v1alpha1
command: aws-iam-authenticator
args:
- "token"
- "-i"
- %s`, outputs["ClusterEndpoint"], outputs["ClusterCertificateAuthority"], outputs["Cluster"]))
if err := ioutil.WriteFile(config, data, 0644); err != nil {
return "", err
}
return config, nil
}

31
provider/aws/template.go Normal file
View File

@ -0,0 +1,31 @@
package aws
import (
"fmt"
"html/template"
"github.com/convox/convox/pkg/common"
)
func (p *Provider) RenderTemplate(name string, params map[string]interface{}) ([]byte, error) {
data, err := p.templater.Render(fmt.Sprintf("%s.yml.tmpl", name), params)
if err != nil {
return nil, err
}
return common.FormatYAML(data)
}
func (p *Provider) templateHelpers() template.FuncMap {
return template.FuncMap{
"coalesce": func(ss ...string) string {
return common.CoalesceString(ss...)
},
"safe": func(s string) template.HTML {
return template.HTML(fmt.Sprintf("%q", s))
},
"upper": func(s string) string {
return upperName(s)
},
}
}

View File

@ -0,0 +1,19 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: aws-auth
namespace: kube-system
data:
mapRoles: |
- rolearn: {{.NodesRole}}
username: system:node:{{"{{"}}EC2PrivateDNSName{{"}}"}}
groups:
- system:bootstrappers
- system:nodes
{{ with .AdminUser }}
mapUsers: |
- userarn: {{.}}
username: admin
groups:
- system:masters
{{ end }}

View File

@ -0,0 +1,22 @@
apiVersion: v1
kind: ConfigMap
metadata:
namespace: {{.Rack}}
name: env-api
data:
AWS_REGION: {{.Region}}
PASSWORD: {{.Password}}
PROVIDER: kaws
---
apiVersion: v1
kind: ConfigMap
metadata:
namespace: convox-system
name: env-router
data:
AUTOCERT: "true"
AWS_REGION: {{.Region}}
ROUTER_CACHE: {{.RouterCache}}
ROUTER_HOSTS: {{.RouterHosts}}
ROUTER_TARGETS: {{.RouterTargets}}
STORAGE: dynamodb

View File

@ -0,0 +1,78 @@
apiVersion: v1
kind: ConfigMap
metadata:
namespace: {{.Namespace}}
name: resource-{{.Name}}
labels:
system: convox
rack: {{.Rack}}
app: {{.App}}
type: resource
resource: {{.Name}}
---
apiVersion: "convox.com/v1"
kind: Stack
metadata:
namespace: {{.Namespace}}
name: resource-{{.Name}}
finalizers:
- stack.kaws.convox
labels:
system: convox
rack: {{.Rack}}
app: {{.App}}
type: resource
resource: memcached
name: {{.Name}}
spec:
parameters:
{{ range $k, $v := .Parameters }}
{{ upper $k }}: {{ safe $v }}
{{ end }}
template: |
AWSTemplateFormatVersion: "2010-09-09"
Parameters:
Class:
Type: String
Default: cache.t2.micro
Nodes:
Type: Number
Default: "1"
Version:
Type: String
Default: "1.5"
Outputs:
Url:
Value: !Sub "memcached://${CacheCluster.ConfigurationEndpoint.Address}:${CacheCluster.ConfigurationEndpoint.Port}"
Resources:
SecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: !Ref AWS::StackName
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: "11211"
ToPort: "11211"
CidrIp: !ImportValue "{{.Rack}}:VpcCidr"
VpcId: !ImportValue "{{.Rack}}:Vpc"
Tags:
- Key: Name
Value: !Ref AWS::StackName
SubnetGroup:
Type: AWS::ElastiCache::SubnetGroup
Properties:
Description: !Ref AWS::StackName
SubnetIds:
- !ImportValue "{{.Rack}}:VpcPrivateSubnet0"
- !ImportValue "{{.Rack}}:VpcPrivateSubnet1"
CacheCluster:
Type: AWS::ElastiCache::CacheCluster
Properties:
AutoMinorVersionUpgrade: true
CacheNodeType: !Ref Class
CacheSubnetGroupName: !Ref SubnetGroup
Engine: memcached
EngineVersion: !Ref Version
NumCacheNodes: !Ref Nodes
Port: "11211"
VpcSecurityGroupIds: [ !Ref SecurityGroup ]

View File

@ -0,0 +1,109 @@
apiVersion: v1
kind: ConfigMap
metadata:
namespace: {{.Namespace}}
name: resource-{{.Name}}
labels:
system: convox
rack: {{.Rack}}
app: {{.App}}
type: resource
resource: {{.Name}}
---
apiVersion: "convox.com/v1"
kind: Stack
metadata:
namespace: {{.Namespace}}
name: resource-{{.Name}}
finalizers:
- stack.kaws.convox
labels:
system: convox
rack: {{.Rack}}
app: {{.App}}
type: resource
resource: mysql
name: {{.Name}}
spec:
parameters:
{{ range $k, $v := .Parameters }}
{{ upper $k }}: {{ safe $v }}
{{ end }}
template: |
AWSTemplateFormatVersion: "2010-09-09"
Conditions:
BlankEncrypted: !Equals [ !Ref Encrypted, "" ]
BlankIops: !Equals [ !Ref Iops, "0" ]
Parameters:
Class:
Type: String
Default: db.t2.micro
Durable:
Type: String
Default: "false"
AllowedValues: [ "true", "false" ]
Encrypted:
Type: String
Default: "false"
AllowedValues: [ "true", "false" ]
Iops:
Type: Number
Default: "0"
Storage:
Type: Number
Default: "20"
Version:
Type: String
Default: "5.6"
Outputs:
Url:
Value: !Sub "mysql://app:{{.Password}}@${Instance.Endpoint.Address}:${Instance.Endpoint.Port}/app"
Resources:
SecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: !Ref AWS::StackName
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: "5432"
ToPort: "5432"
CidrIp: !ImportValue "{{.Rack}}:VpcCidr"
VpcId: !ImportValue "{{.Rack}}:Vpc"
Tags:
- Key: Name
Value: !Ref AWS::StackName
SubnetGroup:
Type: AWS::RDS::DBSubnetGroup
Properties:
DBSubnetGroupDescription: !Ref AWS::StackName
SubnetIds:
- !ImportValue "{{.Rack}}:VpcPrivateSubnet0"
- !ImportValue "{{.Rack}}:VpcPrivateSubnet1"
Instance:
Type: AWS::RDS::DBInstance
DeletionPolicy: Snapshot
Properties:
AllocatedStorage: !Ref Storage
DBInstanceClass: !Ref Class
DBInstanceIdentifier: !Ref AWS::StackName
DBName: app
DBParameterGroupName: !Ref ParameterGroup
DBSubnetGroupName: !Ref SubnetGroup
Engine: mysql
EngineVersion: !Ref Version
Iops: !If [ BlankIops, !Ref "AWS::NoValue", !Ref Iops ]
MasterUsername: app
MasterUserPassword: {{ safe .Password }}
MultiAZ: !Ref Durable
Port: "3306"
PubliclyAccessible: "false"
StorageEncrypted: !Ref Encrypted
StorageType: !If [ BlankIops, gp2, io1 ]
VPCSecurityGroups: [ !Ref SecurityGroup ]
ParameterGroup:
Type: AWS::RDS::DBParameterGroup
Properties:
Description: !Ref AWS::StackName
Family: !Sub
- mysql${Base}
- Base: !Join [ ".", [ !Select [ 0, !Split [ ".", !Ref Version ] ], !Select [ 1, !Split [ ".", !Ref Version ] ] ] ]

View File

@ -0,0 +1,113 @@
apiVersion: v1
kind: ConfigMap
metadata:
namespace: {{.Namespace}}
name: resource-{{.Name}}
labels:
system: convox
rack: {{.Rack}}
app: {{.App}}
type: resource
resource: {{.Name}}
---
apiVersion: "convox.com/v1"
kind: Stack
metadata:
namespace: {{.Namespace}}
name: resource-{{.Name}}
finalizers:
- stack.kaws.convox
labels:
system: convox
rack: {{.Rack}}
app: {{.App}}
type: resource
resource: postgres
name: {{.Name}}
spec:
parameters:
{{ range $k, $v := .Parameters }}
{{ upper $k }}: {{ safe $v }}
{{ end }}
template: |
AWSTemplateFormatVersion: "2010-09-09"
Conditions:
BlankEncrypted: !Equals [ !Ref Encrypted, "" ]
BlankIops: !Equals [ !Ref Iops, "0" ]
Version9: !Equals [ !Select [ "0", !Split [ ".", !Ref Version ] ], "9" ]
Parameters:
Class:
Type: String
Default: db.t2.micro
Durable:
Type: String
Default: "false"
AllowedValues: [ "true", "false" ]
Encrypted:
Type: String
Default: "false"
AllowedValues: [ "true", "false" ]
Iops:
Type: Number
Default: "0"
Storage:
Type: Number
Default: "20"
Version:
Type: String
Default: "10.6"
Outputs:
Url:
Value: !Sub "postgres://app:{{.Password}}@${Instance.Endpoint.Address}:${Instance.Endpoint.Port}/app"
Resources:
SecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: !Ref AWS::StackName
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: "5432"
ToPort: "5432"
CidrIp: !ImportValue "{{.Rack}}:VpcCidr"
VpcId: !ImportValue "{{.Rack}}:Vpc"
Tags:
- Key: Name
Value: !Ref AWS::StackName
SubnetGroup:
Type: AWS::RDS::DBSubnetGroup
Properties:
DBSubnetGroupDescription: !Ref AWS::StackName
SubnetIds:
- !ImportValue "{{.Rack}}:VpcPrivateSubnet0"
- !ImportValue "{{.Rack}}:VpcPrivateSubnet1"
Instance:
Type: AWS::RDS::DBInstance
DeletionPolicy: Snapshot
Properties:
AllocatedStorage: !Ref Storage
DBInstanceClass: !Ref Class
DBInstanceIdentifier: !Ref AWS::StackName
DBName: app
DBParameterGroupName: !Ref ParameterGroup
DBSubnetGroupName: !Ref SubnetGroup
Engine: postgres
EngineVersion: !Ref Version
Iops: !If [ BlankIops, !Ref "AWS::NoValue", !Ref Iops ]
MasterUsername: app
MasterUserPassword: {{ safe .Password }}
MultiAZ: !Ref Durable
Port: "5432"
PubliclyAccessible: "false"
StorageEncrypted: !Ref Encrypted
StorageType: !If [ BlankIops, gp2, io1 ]
VPCSecurityGroups: [ !Ref SecurityGroup ]
ParameterGroup:
Type: AWS::RDS::DBParameterGroup
Properties:
Description: !Ref AWS::StackName
Family: !Sub
- postgres${Base}
- Base: !If
- Version9
- !Join [ ".", [ !Select [ 0, !Split [ ".", !Ref Version ] ], !Select [ 1, !Split [ ".", !Ref Version ] ] ] ]
- !Select [ 0, !Split [ ".", !Ref Version ]]

View File

@ -0,0 +1,96 @@
apiVersion: v1
kind: ConfigMap
metadata:
namespace: {{.Namespace}}
name: resource-{{.Name}}
labels:
system: convox
rack: {{.Rack}}
app: {{.App}}
type: resource
resource: {{.Name}}
---
apiVersion: "convox.com/v1"
kind: Stack
metadata:
namespace: {{.Namespace}}
name: resource-{{.Name}}
finalizers:
- stack.kaws.convox
labels:
system: convox
rack: {{.Rack}}
app: {{.App}}
type: resource
resource: redis
name: {{.Name}}
spec:
parameters:
{{ range $k, $v := .Parameters }}
{{ upper $k }}: {{ safe $v }}
{{ end }}
template: |
AWSTemplateFormatVersion: "2010-09-09"
Conditions:
Encrypted: !Equals [ !Ref "Encrypted", "true" ]
Parameters:
Class:
Type: String
Default: cache.t2.micro
Durable:
Type: String
Default: "false"
AllowedValues: [ "true", "false" ]
Encrypted:
Type: String
Default: "false"
AllowedValues: [ "true", "false" ]
Nodes:
Type: Number
Default: "1"
Version:
Type: String
Default: "5.0"
Outputs:
Url:
Value: !Sub
- ${Protocol}${Auth}${ReplicationGroup.PrimaryEndPoint.Address}:${ReplicationGroup.PrimaryEndPoint.Port}/0
- Protocol: !If [ Encrypted, "rediss://", "redis://" ]
Auth: !If [ Encrypted, ":{{.Password}}@", "" ]
Resources:
SecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: !Ref AWS::StackName
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: "6379"
ToPort: "6379"
CidrIp: !ImportValue "{{.Rack}}:VpcCidr"
VpcId: !ImportValue "{{.Rack}}:Vpc"
Tags:
- Key: Name
Value: !Ref AWS::StackName
SubnetGroup:
Type: AWS::ElastiCache::SubnetGroup
Properties:
Description: !Ref AWS::StackName
SubnetIds:
- !ImportValue "{{.Rack}}:VpcPrivateSubnet0"
- !ImportValue "{{.Rack}}:VpcPrivateSubnet1"
ReplicationGroup:
Type: AWS::ElastiCache::ReplicationGroup
Properties:
AtRestEncryptionEnabled: !Ref Encrypted
AuthToken: !If [ Encrypted, {{ safe .Password }}, !Ref "AWS::NoValue" ]
AutomaticFailoverEnabled: !Ref Durable
AutoMinorVersionUpgrade: true
CacheNodeType: !Ref Class
CacheSubnetGroupName: !Ref SubnetGroup
Engine: redis
EngineVersion: !Ref Version
NumCacheClusters: !Ref Nodes
Port: "6379"
ReplicationGroupDescription: !Ref "AWS::StackName"
SecurityGroupIds: [ !Ref SecurityGroup ]
TransitEncryptionEnabled: !Ref Encrypted

View File

@ -0,0 +1,52 @@
apiVersion: v1
kind: ConfigMap
metadata:
namespace: {{.Namespace}}
name: resource-{{.Name}}
labels:
system: convox
rack: {{.Rack}}
app: {{.App}}
type: resource
resource: {{.Name}}
---
apiVersion: "convox.com/v1"
kind: Stack
metadata:
namespace: {{.Namespace}}
name: resource-{{.Name}}
finalizers:
- stack.kaws.convox
labels:
system: convox
rack: {{.Rack}}
app: {{.App}}
type: resource
resource: postgres
name: {{.Name}}
spec:
parameters:
{{ range $k, $v := .Parameters }}
{{ upper $k }}: {{ safe $v }}
{{ end }}
template: |
AWSTemplateFormatVersion: "2010-09-09"
Parameters:
Rack:
Type: String
Default: convox
Outputs:
Foo:
Value: !Ref Rack
Resources:
AssessmentTarget:
Type: AWS::Inspector::AssessmentTarget
Properties:
AssessmentTargetName: !Ref Rack
ResourceGroupArn: !GetAtt ResourceGroup.Arn
ResourceGroup:
Type: AWS::Inspector::ResourceGroup
Properties:
ResourceGroupTags:
- Key: rack
Value: !Ref Rack

View File

@ -0,0 +1,20 @@
apiVersion: autoscaling/v2beta1
kind: HorizontalPodAutoscaler
metadata:
namespace: convox-system
name: router
labels:
service: router
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: router
minReplicas: {{.RouterMin}}
maxReplicas: {{.RouterMax}}
metrics:
- type: Resource
resource:
name: cpu
targetAverageUtilization: 100

View File

@ -0,0 +1,14 @@
apiVersion: autoscaling/v2beta1
kind: HorizontalPodAutoscaler
metadata:
namespace: ==RACK==
name: api
labels:
service: api
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: api
minReplicas: 2
maxReplicas: 2

View File

@ -0,0 +1,154 @@
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
name: cluster-autoscaler
labels:
k8s-addon: cluster-autoscaler.addons.k8s.io
k8s-app: cluster-autoscaler
rules:
- apiGroups: [""]
resources: ["events","endpoints"]
verbs: ["create", "patch"]
- apiGroups: [""]
resources: ["pods/eviction"]
verbs: ["create"]
- apiGroups: [""]
resources: ["pods/status"]
verbs: ["update"]
- apiGroups: [""]
resources: ["endpoints"]
resourceNames: ["cluster-autoscaler"]
verbs: ["get","update"]
- apiGroups: [""]
resources: ["nodes"]
verbs: ["watch","list","get","update"]
- apiGroups: [""]
resources: ["pods","services","replicationcontrollers","persistentvolumeclaims","persistentvolumes"]
verbs: ["watch","list","get"]
- apiGroups: ["extensions"]
resources: ["replicasets","daemonsets"]
verbs: ["watch","list","get"]
- apiGroups: ["policy"]
resources: ["poddisruptionbudgets"]
verbs: ["watch","list"]
- apiGroups: ["apps"]
resources: ["statefulsets"]
verbs: ["watch","list","get"]
- apiGroups: ["storage.k8s.io"]
resources: ["storageclasses"]
verbs: ["watch","list","get"]
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: Role
metadata:
name: cluster-autoscaler
namespace: kube-system
labels:
k8s-addon: cluster-autoscaler.addons.k8s.io
k8s-app: cluster-autoscaler
rules:
- apiGroups: [""]
resources: ["configmaps"]
verbs: ["create"]
- apiGroups: [""]
resources: ["configmaps"]
resourceNames: ["cluster-autoscaler-status"]
verbs: ["delete","get","update"]
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
name: cluster-autoscaler
labels:
k8s-addon: cluster-autoscaler.addons.k8s.io
k8s-app: cluster-autoscaler
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-autoscaler
subjects:
- kind: ServiceAccount
name: cluster-autoscaler
namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: RoleBinding
metadata:
name: cluster-autoscaler
namespace: kube-system
labels:
k8s-addon: cluster-autoscaler.addons.k8s.io
k8s-app: cluster-autoscaler
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: cluster-autoscaler
subjects:
- kind: ServiceAccount
name: cluster-autoscaler
namespace: kube-system
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: cluster-autoscaler
namespace: kube-system
labels:
app: cluster-autoscaler
spec:
replicas: 1
selector:
matchLabels:
app: cluster-autoscaler
template:
metadata:
annotations:
scheduler.alpha.kubernetes.io/critical-pod: ""
labels:
app: cluster-autoscaler
spec:
serviceAccountName: cluster-autoscaler
containers:
- image: k8s.gcr.io/cluster-autoscaler:v1.2.2
name: cluster-autoscaler
resources:
limits:
cpu: 100m
memory: 300Mi
requests:
cpu: 100m
memory: 300Mi
command:
- ./cluster-autoscaler
- --cloud-provider=aws
- --expander=least-waste
- --node-group-auto-discovery=asg:tag=k8s.io/cluster-autoscaler/enabled,k8s.io/cluster-autoscaler/==CLUSTER==
- --scale-down-delay-after-add=5m
- --scale-down-delay-after-delete=2m
- --scale-down-unneeded-time=1m
- --skip-nodes-with-local-storage=false
- --stderrthreshold=info
- --v=4
env:
- name: AWS_REGION
value: ==REGION==
volumeMounts:
- name: ssl-certs
mountPath: /etc/ssl/certs/ca-certificates.crt
readOnly: true
imagePullPolicy: "Always"
volumes:
- name: ssl-certs
hostPath:
path: "/etc/ssl/certs/ca-bundle.crt"
---
apiVersion: policy/v1beta1
kind: PodDisruptionBudget
metadata:
namespace: kube-system
name: cluster-autoscaler
spec:
maxUnavailable: 1
selector:
matchLabels:
app: cluster-autoscaler

View File

@ -0,0 +1,565 @@
---
kind: DaemonSet
apiVersion: extensions/v1beta1
metadata:
name: calico-node
namespace: kube-system
labels:
k8s-app: calico-node
spec:
selector:
matchLabels:
k8s-app: calico-node
updateStrategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1
template:
metadata:
labels:
k8s-app: calico-node
annotations:
# This, along with the CriticalAddonsOnly toleration below,
# marks the pod as a critical add-on, ensuring it gets
# priority scheduling and that its resources are reserved
# if it ever gets evicted.
scheduler.alpha.kubernetes.io/critical-pod: ''
spec:
hostNetwork: true
serviceAccountName: calico-node
# Minimize downtime during a rolling upgrade or deletion; tell Kubernetes to do a "force
# deletion": https://kubernetes.io/docs/concepts/workloads/pods/pod/#termination-of-pods.
terminationGracePeriodSeconds: 0
containers:
# Runs calico/node container on each Kubernetes node. This
# container programs network policy and routes on each
# host.
- name: calico-node
image: quay.io/calico/node:v3.1.3
env:
# Use Kubernetes API as the backing datastore.
- name: DATASTORE_TYPE
value: "kubernetes"
# Use eni not cali for interface prefix
- name: FELIX_INTERFACEPREFIX
value: "eni"
# Enable felix info logging.
- name: FELIX_LOGSEVERITYSCREEN
value: "info"
# Don't enable BGP.
- name: CALICO_NETWORKING_BACKEND
value: "none"
# Cluster type to identify the deployment type
- name: CLUSTER_TYPE
value: "k8s,ecs"
# Disable file logging so `kubectl logs` works.
- name: CALICO_DISABLE_FILE_LOGGING
value: "true"
- name: FELIX_TYPHAK8SSERVICENAME
value: "calico-typha"
# Set Felix endpoint to host default action to ACCEPT.
- name: FELIX_DEFAULTENDPOINTTOHOSTACTION
value: "ACCEPT"
# This will make Felix honor AWS VPC CNI's mangle table
# rules.
- name: FELIX_IPTABLESMANGLEALLOWACTION
value: Return
# Disable IPV6 on Kubernetes.
- name: FELIX_IPV6SUPPORT
value: "false"
# Wait for the datastore.
- name: WAIT_FOR_DATASTORE
value: "true"
- name: FELIX_LOGSEVERITYSYS
value: "none"
- name: FELIX_PROMETHEUSMETRICSENABLED
value: "true"
- name: NO_DEFAULT_POOLS
value: "true"
# Set based on the k8s node name.
- name: NODENAME
valueFrom:
fieldRef:
fieldPath: spec.nodeName
# No IP address needed.
- name: IP
value: ""
- name: FELIX_HEALTHENABLED
value: "true"
securityContext:
privileged: true
livenessProbe:
httpGet:
path: /liveness
port: 9099
host: localhost
periodSeconds: 10
initialDelaySeconds: 10
failureThreshold: 6
readinessProbe:
httpGet:
path: /readiness
port: 9099
periodSeconds: 10
volumeMounts:
- mountPath: /lib/modules
name: lib-modules
readOnly: true
- mountPath: /var/run/calico
name: var-run-calico
readOnly: false
volumes:
# Used to ensure proper kmods are installed.
- name: lib-modules
hostPath:
path: /lib/modules
- name: var-run-calico
hostPath:
path: /var/run/calico
tolerations:
# Make sure calico/node gets scheduled on all nodes.
- operator: Exists
---
# Create all the CustomResourceDefinitions needed for
# Calico policy-only mode.
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
name: felixconfigurations.crd.projectcalico.org
spec:
scope: Cluster
group: crd.projectcalico.org
version: v1
names:
kind: FelixConfiguration
plural: felixconfigurations
singular: felixconfiguration
---
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
name: bgpconfigurations.crd.projectcalico.org
spec:
scope: Cluster
group: crd.projectcalico.org
version: v1
names:
kind: BGPConfiguration
plural: bgpconfigurations
singular: bgpconfiguration
---
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
name: ippools.crd.projectcalico.org
spec:
scope: Cluster
group: crd.projectcalico.org
version: v1
names:
kind: IPPool
plural: ippools
singular: ippool
---
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
name: hostendpoints.crd.projectcalico.org
spec:
scope: Cluster
group: crd.projectcalico.org
version: v1
names:
kind: HostEndpoint
plural: hostendpoints
singular: hostendpoint
---
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
name: clusterinformations.crd.projectcalico.org
spec:
scope: Cluster
group: crd.projectcalico.org
version: v1
names:
kind: ClusterInformation
plural: clusterinformations
singular: clusterinformation
---
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
name: globalnetworkpolicies.crd.projectcalico.org
spec:
scope: Cluster
group: crd.projectcalico.org
version: v1
names:
kind: GlobalNetworkPolicy
plural: globalnetworkpolicies
singular: globalnetworkpolicy
---
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
name: globalnetworksets.crd.projectcalico.org
spec:
scope: Cluster
group: crd.projectcalico.org
version: v1
names:
kind: GlobalNetworkSet
plural: globalnetworksets
singular: globalnetworkset
---
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
name: networkpolicies.crd.projectcalico.org
spec:
scope: Namespaced
group: crd.projectcalico.org
version: v1
names:
kind: NetworkPolicy
plural: networkpolicies
singular: networkpolicy
---
# Create the ServiceAccount and roles necessary for Calico.
apiVersion: v1
kind: ServiceAccount
metadata:
name: calico-node
namespace: kube-system
---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
name: calico-node
rules:
- apiGroups: [""]
resources:
- namespaces
verbs:
- get
- list
- watch
- apiGroups: [""]
resources:
- pods/status
verbs:
- update
- apiGroups: [""]
resources:
- pods
verbs:
- get
- list
- watch
- patch
- apiGroups: [""]
resources:
- services
verbs:
- get
- apiGroups: [""]
resources:
- endpoints
verbs:
- get
- apiGroups: [""]
resources:
- nodes
verbs:
- get
- list
- update
- watch
- apiGroups: ["extensions"]
resources:
- networkpolicies
verbs:
- get
- list
- watch
- apiGroups: ["networking.k8s.io"]
resources:
- networkpolicies
verbs:
- watch
- list
- apiGroups: ["crd.projectcalico.org"]
resources:
- globalfelixconfigs
- felixconfigurations
- bgppeers
- globalbgpconfigs
- bgpconfigurations
- ippools
- globalnetworkpolicies
- globalnetworksets
- networkpolicies
- clusterinformations
- hostendpoints
verbs:
- create
- get
- list
- update
- watch
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
name: calico-node
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: calico-node
subjects:
- kind: ServiceAccount
name: calico-node
namespace: kube-system
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: calico-typha
namespace: kube-system
labels:
k8s-app: calico-typha
spec:
revisionHistoryLimit: 2
template:
metadata:
labels:
k8s-app: calico-typha
annotations:
scheduler.alpha.kubernetes.io/critical-pod: ''
spec:
tolerations:
- operator: Exists
hostNetwork: true
serviceAccountName: calico-node
containers:
- image: quay.io/calico/typha:v0.7.4
name: calico-typha
ports:
- containerPort: 5473
name: calico-typha
protocol: TCP
env:
# Use eni not cali for interface prefix
- name: FELIX_INTERFACEPREFIX
value: "eni"
- name: TYPHA_LOGFILEPATH
value: "none"
- name: TYPHA_LOGSEVERITYSYS
value: "none"
- name: TYPHA_LOGSEVERITYSCREEN
value: "info"
- name: TYPHA_PROMETHEUSMETRICSENABLED
value: "true"
- name: TYPHA_CONNECTIONREBALANCINGMODE
value: "kubernetes"
- name: TYPHA_PROMETHEUSMETRICSPORT
value: "9093"
- name: TYPHA_DATASTORETYPE
value: "kubernetes"
- name: TYPHA_MAXCONNECTIONSLOWERLIMIT
value: "1"
- name: TYPHA_HEALTHENABLED
value: "true"
# This will make Felix honor AWS VPC CNI's mangle table
# rules.
- name: FELIX_IPTABLESMANGLEALLOWACTION
value: Return
volumeMounts:
- mountPath: /etc/calico
name: etc-calico
readOnly: true
livenessProbe:
httpGet:
path: /liveness
port: 9098
periodSeconds: 30
initialDelaySeconds: 30
readinessProbe:
httpGet:
path: /readiness
port: 9098
periodSeconds: 10
volumes:
- name: etc-calico
hostPath:
path: /etc/calico
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
name: typha-cpha
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: typha-cpha
subjects:
- kind: ServiceAccount
name: typha-cpha
namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
name: typha-cpha
rules:
- apiGroups: [""]
resources: ["nodes"]
verbs: ["list"]
---
kind: ConfigMap
apiVersion: v1
metadata:
name: calico-typha-horizontal-autoscaler
namespace: kube-system
data:
ladder: |-
{
"coresToReplicas": [],
"nodesToReplicas":
[
[1, 1],
[10, 2],
[100, 3],
[250, 4],
[500, 5],
[1000, 6],
[1500, 7],
[2000, 8]
]
}
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: calico-typha-horizontal-autoscaler
namespace: kube-system
labels:
k8s-app: calico-typha-autoscaler
spec:
replicas: 1
template:
metadata:
labels:
k8s-app: calico-typha-autoscaler
annotations:
scheduler.alpha.kubernetes.io/critical-pod: ''
spec:
containers:
- image: k8s.gcr.io/cluster-proportional-autoscaler-amd64:1.1.2
name: autoscaler
command:
- /cluster-proportional-autoscaler
- --namespace=kube-system
- --configmap=calico-typha-horizontal-autoscaler
- --target=deployment/calico-typha
- --logtostderr=true
- --v=2
resources:
requests:
cpu: 10m
limits:
cpu: 10m
serviceAccountName: typha-cpha
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: Role
metadata:
name: typha-cpha
namespace: kube-system
rules:
- apiGroups: [""]
resources: ["configmaps"]
verbs: ["get"]
- apiGroups: ["extensions"]
resources: ["deployments/scale"]
verbs: ["get", "update"]
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: typha-cpha
namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: RoleBinding
metadata:
name: typha-cpha
namespace: kube-system
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: typha-cpha
subjects:
- kind: ServiceAccount
name: typha-cpha
namespace: kube-system
---
apiVersion: v1
kind: Service
metadata:
name: calico-typha
namespace: kube-system
labels:
k8s-app: calico-typha
spec:
ports:
- port: 5473
protocol: TCP
targetPort: calico-typha
name: calico-typha
selector:
k8s-app: calico-typha

View File

@ -0,0 +1,22 @@
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
name: stacks.convox.com
spec:
group: convox.com
versions:
- name: v1
served: true
storage: true
version: v1
scope: Namespaced
names:
plural: stacks
singular: stack
kind: Stack
categories:
- convox
additionalPrinterColumns:
- name: Status
type: string
JSONPath: .status

View File

@ -0,0 +1,78 @@
apiVersion: v1
kind: ServiceAccount
metadata:
namespace: kube-system
name: kube2iam
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
namespace: kube-system
name: kube2iam
rules:
- apiGroups: [""]
resources: ["namespaces","pods"]
verbs: ["get","watch","list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
namespace: kube-system
name: kube2iam
subjects:
- kind: ServiceAccount
name: kube2iam
namespace: kube-system
roleRef:
kind: ClusterRole
name: kube2iam
apiGroup: rbac.authorization.k8s.io
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
namespace: kube-system
name: kube2iam
spec:
selector:
matchLabels:
name: kube2iam
template:
metadata:
labels:
name: kube2iam
spec:
hostNetwork: true
serviceAccountName: kube2iam
containers:
- image: jtblin/kube2iam:latest
name: kube2iam
args:
- "--base-role-arn=arn:aws:iam::==ACCOUNT==:role/"
- "--host-interface=eni+"
- "--host-ip=$(HOST_IP)"
- "--node=$(NODE_NAME)"
env:
- name: HOST_IP
valueFrom:
fieldRef:
fieldPath: status.podIP
- name: NODE_NAME
valueFrom:
fieldRef:
fieldPath: spec.nodeName
ports:
- containerPort: 8181
hostPort: 8181
name: http
securityContext:
privileged: true
---
apiVersion: v1
kind: ServiceAccount
metadata:
labels:
k8s-addon: cluster-autoscaler.addons.k8s.io
k8s-app: cluster-autoscaler
name: cluster-autoscaler
namespace: kube-system

View File

@ -0,0 +1,22 @@
apiVersion: v1
kind: Service
metadata:
namespace: convox-system
name: router
spec:
type: NodePort
externalTrafficPolicy: Local
ports:
- name: http
nodePort: 32000
port: 80
targetPort: 80
protocol: TCP
- name: https
nodePort: 32001
port: 443
targetPort: 443
protocol: TCP
selector:
system: convox
service: router

View File

@ -0,0 +1,244 @@
package aws
// import (
// "encoding/json"
// "fmt"
// "regexp"
// "strconv"
// "time"
// "github.com/aws/aws-sdk-go/aws"
// "github.com/aws/aws-sdk-go/service/sqs"
// am "k8s.io/apimachinery/pkg/apis/meta/v1"
// )
// var (
// eventMessageSplitter = regexp.MustCompile(`([^=]+)='([^']*)'\n`)
// stackTagCache = map[string]map[string]string{}
// )
// func (p *Provider) workerEvents() error {
// if p.EventQueue == "" {
// return fmt.Errorf("no queue url")
// }
// for {
// err := p.processQueue(p.EventQueue, 5, func(m *sqs.Message) error {
// attrs, err := topicAttributes(*m.Body)
// if err != nil {
// return err
// }
// stack := attrs["StackName"]
// tags, err := p.stackTagsCached(stack)
// if err != nil {
// return err
// }
// switch stack {
// // case p.Name:
// // fmt.Printf(fmt.Sprintf("system/cfn %s %s %s\n", attrs["ResourceStatus"], attrs["LogicalResourceId"], attrs["ResourceStatusReason"]))
// // switch attrs["ResourceType"] {
// // case "AWS::CloudFormation::Stack":
// // switch attrs["ResourceStatus"] {
// // case "ROLLBACK_COMPLETE", "UPDATE_COMPLETE":
// // fmt.Println("stack finished")
// // os, err := p.stackOutputs(p.Name)
// // if err != nil {
// // return err
// // }
// // opts := structs.SystemUpdateOptions{
// // Version: options.String(os["Version"]),
// // }
// // if err := p.Provider.SystemUpdate(opts); err != nil {
// // return err
// // }
// // }
// // }
// default:
// app := tags["app"]
// p.Log(app, fmt.Sprintf("system/cfn/%s", attrs["StackName"]), time.Now().UTC(), fmt.Sprintf("%s %s %s", attrs["ResourceStatus"], attrs["LogicalResourceId"], attrs["ResourceStatusReason"]))
// switch attrs["ResourceType"] {
// case "AWS::CloudFormation::Stack":
// if err := p.stackStatusUpdate(attrs["StackName"], attrs["ResourceStatus"]); err != nil {
// return err
// }
// }
// }
// return nil
// })
// if err != nil {
// fmt.Printf("err = %+v\n", err)
// time.Sleep(10 * time.Second)
// }
// }
// }
// func (p *Provider) processQueue(queue string, max int, handler func(m *sqs.Message) error) error {
// for {
// res, err := p.SQS.ReceiveMessage(&sqs.ReceiveMessageInput{
// QueueUrl: aws.String(queue),
// AttributeNames: []*string{aws.String("All")},
// MessageAttributeNames: []*string{aws.String("All")},
// MaxNumberOfMessages: aws.Int64(10),
// VisibilityTimeout: aws.Int64(20),
// WaitTimeSeconds: aws.Int64(10),
// })
// if err != nil {
// return err
// }
// for _, m := range res.Messages {
// filter, err := filterMessage(m, max)
// if err != nil {
// fmt.Printf("err = %+v\n", err)
// continue
// }
// if !filter {
// if err := handler(m); err != nil {
// fmt.Printf("err = %+v\n", err)
// continue
// }
// }
// _, err = p.SQS.DeleteMessage(&sqs.DeleteMessageInput{
// QueueUrl: aws.String(p.EventQueue),
// ReceiptHandle: m.ReceiptHandle,
// })
// if err != nil {
// fmt.Printf("err = %+v\n", err)
// continue
// }
// }
// }
// }
// func (p *Provider) stackStatusUpdate(stack, status string) error {
// c, err := p.convoxClient()
// if err != nil {
// return err
// }
// tags, err := p.stackTagsCached(stack)
// if err != nil {
// return err
// }
// app := tags["app"]
// kind := tags["type"]
// name := tags["name"]
// s, err := c.ConvoxV1().Stacks(p.AppNamespace(app)).Get(tags["stack"], am.GetOptions{})
// if err != nil {
// return err
// }
// switch status {
// case "CREATE_COMPLETE", "UPDATE_COMPLETE", "ROLLBACK_COMPLETE", "UPDATE_ROLLBACK_COMPLETE":
// s.Status = "Running"
// case "CREATE_FAILED", "DELETE_FAILED", "ROLLBACK_FAILED", "UPDATE_ROLLBACK_FAILED":
// s.Status = "Failed"
// case "CREATE_IN_PROGRESS":
// s.Status = "Creating"
// case "DELETE_IN_PROGRESS":
// s.Status = "Deleting"
// case "DELETE_COMPLETE":
// s.ObjectMeta.Finalizers = []string{}
// case "ROLLBACK_IN_PROGRESS", "UPDATE_ROLLBACK_IN_PROGRESS":
// s.Status = "Rollback"
// case "UPDATE_IN_PROGRESS":
// s.Status = "Updating"
// }
// switch status {
// case "CREATE_COMPLETE", "UPDATE_COMPLETE", "ROLLBACK_COMPLETE", "UPDATE_IN_PROGRESS", "UPDATE_ROLLBACK_COMPLETE":
// os, err := p.stackOutputs(stack)
// if err != nil {
// return err
// }
// s.Outputs = os
// cm, err := p.Provider.Cluster.CoreV1().ConfigMaps(p.AppNamespace(app)).Get(fmt.Sprintf("%s-%s", kind, name), am.GetOptions{})
// if err != nil {
// return err
// }
// if cm.Data == nil {
// cm.Data = map[string]string{}
// }
// for k, v := range os {
// cm.Data[outputToEnvironment(k)] = v
// }
// if _, err := p.Provider.Cluster.CoreV1().ConfigMaps(p.AppNamespace(app)).Update(cm); err != nil {
// return err
// }
// }
// if _, err := c.ConvoxV1().Stacks(p.AppNamespace(app)).Update(s); err != nil {
// return err
// }
// return nil
// }
// func filterMessage(m *sqs.Message, max int) (bool, error) {
// if cs := m.Attributes["ApproximateReceiveCount"]; cs != nil {
// c, err := strconv.Atoi(*cs)
// if err != nil {
// return false, err
// }
// if c >= max {
// return true, nil
// }
// }
// if m.Body == nil {
// return true, nil
// }
// return false, nil
// }
// func (p *Provider) stackTagsCached(stack string) (map[string]string, error) {
// tags, ok := stackTagCache[stack]
// if ok {
// return tags, nil
// }
// tags, err := p.stackTags(stack)
// if err != nil {
// return nil, err
// }
// stackTagCache[stack] = tags
// return tags, nil
// }
// func topicAttributes(body string) (map[string]string, error) {
// var parts map[string]string
// if err := json.Unmarshal([]byte(body), &parts); err != nil {
// return nil, err
// }
// attrs := map[string]string{}
// mas := eventMessageSplitter.FindAllStringSubmatch(parts["Message"], -1)
// for _, ma := range mas {
// attrs[ma[1]] = ma[2]
// }
// return attrs, nil
// }

View File

@ -2,11 +2,12 @@ package k8s
import (
"context"
"fmt"
"os"
"os/exec"
"time"
"github.com/convox/convox/pkg/atom"
"github.com/convox/convox/pkg/common"
"github.com/convox/convox/pkg/manifest"
"github.com/convox/convox/pkg/structs"
"github.com/convox/convox/pkg/templater"
@ -16,7 +17,6 @@ import (
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
metrics "k8s.io/metrics/pkg/client/clientset_generated/clientset"
)
type Engine interface {
@ -35,12 +35,13 @@ type Engine interface {
}
type Provider struct {
Config *rest.Config
Cluster kubernetes.Interface
ID string
Image string
Engine Engine
Metrics metrics.Interface
Config *rest.Config
Cluster kubernetes.Interface
Domain string
// ID string
Engine Engine
Image string
// Metrics metrics.Interface
Name string
Namespace string
Password string
@ -55,7 +56,9 @@ type Provider struct {
templater *templater.Templater
}
func New(namespace string) (*Provider, error) {
func FromEnv() (*Provider, error) {
namespace := os.Getenv("NAMESPACE")
c, err := restConfig()
if err != nil {
return nil, err
@ -71,18 +74,25 @@ func New(namespace string) (*Provider, error) {
return nil, err
}
fmt.Printf("namespace: %+v\n", namespace)
ns, err := kc.CoreV1().Namespaces().Get(namespace, am.GetOptions{})
if err != nil {
return nil, err
}
p := &Provider{
Config: c,
Cluster: kc,
Domain: os.Getenv("DOMAIN"),
Image: os.Getenv("IMAGE"),
Name: ns.Labels["rack"],
Namespace: namespace,
Namespace: ns.Name,
Password: os.Getenv("PASSWORD"),
Provider: common.CoalesceString(os.Getenv("PROVIDER"), "k8s"),
Socket: common.CoalesceString(os.Getenv("SOCKET"), "/var/run/docker.sock"),
Storage: common.CoalesceString(os.Getenv("STORAGE"), "/var/storage"),
Version: common.CoalesceString(os.Getenv("VERSION"), "dev"),
atom: ac,
ctx: context.Background(),
logger: logger.New("ns=k8s"),
}
@ -205,9 +215,9 @@ func (p *Provider) Initialize(opts structs.ProviderOptions) error {
return log.Success()
}
// func (p *Provider) Context() context.Context {
// return p.ctx
// }
func (p *Provider) Context() context.Context {
return p.ctx
}
func (p *Provider) WithContext(ctx context.Context) structs.Provider {
pp := *p

View File

@ -5,22 +5,20 @@ import (
"os"
"github.com/convox/convox/pkg/structs"
"github.com/convox/convox/provider/aws"
"github.com/convox/convox/provider/k8s"
)
var Mock = &structs.MockProvider{}
// FromEnv returns a new Provider from env vars
func FromEnv() (structs.Provider, error) {
return FromName(os.Getenv("PROVIDER"))
}
name := os.Getenv("PROVIDER")
func FromName(name string) (structs.Provider, error) {
switch name {
// case "aws":
// return aws.FromEnv()
case "aws":
return aws.FromEnv()
case "k8s":
return k8s.New(os.Getenv("NAMESPACE"))
return k8s.FromEnv()
// case "local":
// return local.FromEnv()
case "test":

1
vendor/github.com/PuerkitoBio/goquery/.gitattributes generated vendored Normal file
View File

@ -0,0 +1 @@
testdata/* linguist-vendored

16
vendor/github.com/PuerkitoBio/goquery/.gitignore generated vendored Normal file
View File

@ -0,0 +1,16 @@
# editor temporary files
*.sublime-*
.DS_Store
*.swp
#*.*#
tags
# direnv config
.env*
# test binaries
*.test
# coverage and profilte outputs
*.out

16
vendor/github.com/PuerkitoBio/goquery/.travis.yml generated vendored Normal file
View File

@ -0,0 +1,16 @@
language: go
go:
- 1.1
- 1.2.x
- 1.3.x
- 1.4.x
- 1.5.x
- 1.6.x
- 1.7.x
- 1.8.x
- 1.9.x
- "1.10.x"
- 1.11.x
- tip

12
vendor/github.com/PuerkitoBio/goquery/LICENSE generated vendored Normal file
View File

@ -0,0 +1,12 @@
Copyright (c) 2012-2016, Martin Angers & Contributors
All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
* Neither the name of the author nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

179
vendor/github.com/PuerkitoBio/goquery/README.md generated vendored Normal file
View File

@ -0,0 +1,179 @@
# goquery - a little like that j-thing, only in Go
[![build status](https://secure.travis-ci.org/PuerkitoBio/goquery.svg?branch=master)](http://travis-ci.org/PuerkitoBio/goquery) [![GoDoc](https://godoc.org/github.com/PuerkitoBio/goquery?status.png)](http://godoc.org/github.com/PuerkitoBio/goquery) [![Sourcegraph Badge](https://sourcegraph.com/github.com/PuerkitoBio/goquery/-/badge.svg)](https://sourcegraph.com/github.com/PuerkitoBio/goquery?badge)
goquery brings a syntax and a set of features similar to [jQuery][] to the [Go language][go]. It is based on Go's [net/html package][html] and the CSS Selector library [cascadia][]. Since the net/html parser returns nodes, and not a full-featured DOM tree, jQuery's stateful manipulation functions (like height(), css(), detach()) have been left off.
Also, because the net/html parser requires UTF-8 encoding, so does goquery: it is the caller's responsibility to ensure that the source document provides UTF-8 encoded HTML. See the [wiki][] for various options to do this.
Syntax-wise, it is as close as possible to jQuery, with the same function names when possible, and that warm and fuzzy chainable interface. jQuery being the ultra-popular library that it is, I felt that writing a similar HTML-manipulating library was better to follow its API than to start anew (in the same spirit as Go's `fmt` package), even though some of its methods are less than intuitive (looking at you, [index()][index]...).
## Table of Contents
* [Installation](#installation)
* [Changelog](#changelog)
* [API](#api)
* [Examples](#examples)
* [Related Projects](#related-projects)
* [Support](#support)
* [License](#license)
## Installation
Please note that because of the net/html dependency, goquery requires Go1.1+.
$ go get github.com/PuerkitoBio/goquery
(optional) To run unit tests:
$ cd $GOPATH/src/github.com/PuerkitoBio/goquery
$ go test
(optional) To run benchmarks (warning: it runs for a few minutes):
$ cd $GOPATH/src/github.com/PuerkitoBio/goquery
$ go test -bench=".*"
## Changelog
**Note that goquery's API is now stable, and will not break.**
* **2018-11-15 (v1.5.0)** : Go module support (thanks @Zaba505).
* **2018-06-07 (v1.4.1)** : Add `NewDocumentFromReader` examples.
* **2018-03-24 (v1.4.0)** : Deprecate `NewDocument(url)` and `NewDocumentFromResponse(response)`.
* **2018-01-28 (v1.3.0)** : Add `ToEnd` constant to `Slice` until the end of the selection (thanks to @davidjwilkins for raising the issue).
* **2018-01-11 (v1.2.0)** : Add `AddBack*` and deprecate `AndSelf` (thanks to @davidjwilkins).
* **2017-02-12 (v1.1.0)** : Add `SetHtml` and `SetText` (thanks to @glebtv).
* **2016-12-29 (v1.0.2)** : Optimize allocations for `Selection.Text` (thanks to @radovskyb).
* **2016-08-28 (v1.0.1)** : Optimize performance for large documents.
* **2016-07-27 (v1.0.0)** : Tag version 1.0.0.
* **2016-06-15** : Invalid selector strings internally compile to a `Matcher` implementation that never matches any node (instead of a panic). So for example, `doc.Find("~")` returns an empty `*Selection` object.
* **2016-02-02** : Add `NodeName` utility function similar to the DOM's `nodeName` property. It returns the tag name of the first element in a selection, and other relevant values of non-element nodes (see godoc for details). Add `OuterHtml` utility function similar to the DOM's `outerHTML` property (named `OuterHtml` in small caps for consistency with the existing `Html` method on the `Selection`).
* **2015-04-20** : Add `AttrOr` helper method to return the attribute's value or a default value if absent. Thanks to [piotrkowalczuk][piotr].
* **2015-02-04** : Add more manipulation functions - Prepend* - thanks again to [Andrew Stone][thatguystone].
* **2014-11-28** : Add more manipulation functions - ReplaceWith*, Wrap* and Unwrap - thanks again to [Andrew Stone][thatguystone].
* **2014-11-07** : Add manipulation functions (thanks to [Andrew Stone][thatguystone]) and `*Matcher` functions, that receive compiled cascadia selectors instead of selector strings, thus avoiding potential panics thrown by goquery via `cascadia.MustCompile` calls. This results in better performance (selectors can be compiled once and reused) and more idiomatic error handling (you can handle cascadia's compilation errors, instead of recovering from panics, which had been bugging me for a long time). Note that the actual type expected is a `Matcher` interface, that `cascadia.Selector` implements. Other matcher implementations could be used.
* **2014-11-06** : Change import paths of net/html to golang.org/x/net/html (see https://groups.google.com/forum/#!topic/golang-nuts/eD8dh3T9yyA). Make sure to update your code to use the new import path too when you call goquery with `html.Node`s.
* **v0.3.2** : Add `NewDocumentFromReader()` (thanks jweir) which allows creating a goquery document from an io.Reader.
* **v0.3.1** : Add `NewDocumentFromResponse()` (thanks assassingj) which allows creating a goquery document from an http response.
* **v0.3.0** : Add `EachWithBreak()` which allows to break out of an `Each()` loop by returning false. This function was added instead of changing the existing `Each()` to avoid breaking compatibility.
* **v0.2.1** : Make go-getable, now that [go.net/html is Go1.0-compatible][gonet] (thanks to @matrixik for pointing this out).
* **v0.2.0** : Add support for negative indices in Slice(). **BREAKING CHANGE** `Document.Root` is removed, `Document` is now a `Selection` itself (a selection of one, the root element, just like `Document.Root` was before). Add jQuery's Closest() method.
* **v0.1.1** : Add benchmarks to use as baseline for refactorings, refactor Next...() and Prev...() methods to use the new html package's linked list features (Next/PrevSibling, FirstChild). Good performance boost (40+% in some cases).
* **v0.1.0** : Initial release.
## API
goquery exposes two structs, `Document` and `Selection`, and the `Matcher` interface. Unlike jQuery, which is loaded as part of a DOM document, and thus acts on its containing document, goquery doesn't know which HTML document to act upon. So it needs to be told, and that's what the `Document` type is for. It holds the root document node as the initial Selection value to manipulate.
jQuery often has many variants for the same function (no argument, a selector string argument, a jQuery object argument, a DOM element argument, ...). Instead of exposing the same features in goquery as a single method with variadic empty interface arguments, statically-typed signatures are used following this naming convention:
* When the jQuery equivalent can be called with no argument, it has the same name as jQuery for the no argument signature (e.g.: `Prev()`), and the version with a selector string argument is called `XxxFiltered()` (e.g.: `PrevFiltered()`)
* When the jQuery equivalent **requires** one argument, the same name as jQuery is used for the selector string version (e.g.: `Is()`)
* The signatures accepting a jQuery object as argument are defined in goquery as `XxxSelection()` and take a `*Selection` object as argument (e.g.: `FilterSelection()`)
* The signatures accepting a DOM element as argument in jQuery are defined in goquery as `XxxNodes()` and take a variadic argument of type `*html.Node` (e.g.: `FilterNodes()`)
* The signatures accepting a function as argument in jQuery are defined in goquery as `XxxFunction()` and take a function as argument (e.g.: `FilterFunction()`)
* The goquery methods that can be called with a selector string have a corresponding version that take a `Matcher` interface and are defined as `XxxMatcher()` (e.g.: `IsMatcher()`)
Utility functions that are not in jQuery but are useful in Go are implemented as functions (that take a `*Selection` as parameter), to avoid a potential naming clash on the `*Selection`'s methods (reserved for jQuery-equivalent behaviour).
The complete [godoc reference documentation can be found here][doc].
Please note that Cascadia's selectors do not necessarily match all supported selectors of jQuery (Sizzle). See the [cascadia project][cascadia] for details. Invalid selector strings compile to a `Matcher` that fails to match any node. Behaviour of the various functions that take a selector string as argument follows from that fact, e.g. (where `~` is an invalid selector string):
* `Find("~")` returns an empty selection because the selector string doesn't match anything.
* `Add("~")` returns a new selection that holds the same nodes as the original selection, because it didn't add any node (selector string didn't match anything).
* `ParentsFiltered("~")` returns an empty selection because the selector string doesn't match anything.
* `ParentsUntil("~")` returns all parents of the selection because the selector string didn't match any element to stop before the top element.
## Examples
See some tips and tricks in the [wiki][].
Adapted from example_test.go:
```Go
package main
import (
"fmt"
"log"
"net/http"
"github.com/PuerkitoBio/goquery"
)
func ExampleScrape() {
// Request the HTML page.
res, err := http.Get("http://metalsucks.net")
if err != nil {
log.Fatal(err)
}
defer res.Body.Close()
if res.StatusCode != 200 {
log.Fatalf("status code error: %d %s", res.StatusCode, res.Status)
}
// Load the HTML document
doc, err := goquery.NewDocumentFromReader(res.Body)
if err != nil {
log.Fatal(err)
}
// Find the review items
doc.Find(".sidebar-reviews article .content-block").Each(func(i int, s *goquery.Selection) {
// For each item found, get the band and title
band := s.Find("a").Text()
title := s.Find("i").Text()
fmt.Printf("Review %d: %s - %s\n", i, band, title)
})
}
func main() {
ExampleScrape()
}
```
## Related Projects
- [Goq][goq], an HTML deserialization and scraping library based on goquery and struct tags.
- [andybalholm/cascadia][cascadia], the CSS selector library used by goquery.
- [suntong/cascadia][cascadiacli], a command-line interface to the cascadia CSS selector library, useful to test selectors.
- [asciimoo/colly](https://github.com/asciimoo/colly), a lightning fast and elegant Scraping Framework
- [gnulnx/goperf](https://github.com/gnulnx/goperf), a website performance test tool that also fetches static assets.
- [MontFerret/ferret](https://github.com/MontFerret/ferret), declarative web scraping.
## Support
There are a number of ways you can support the project:
* Use it, star it, build something with it, spread the word!
- If you do build something open-source or otherwise publicly-visible, let me know so I can add it to the [Related Projects](#related-projects) section!
* Raise issues to improve the project (note: doc typos and clarifications are issues too!)
- Please search existing issues before opening a new one - it may have already been adressed.
* Pull requests: please discuss new code in an issue first, unless the fix is really trivial.
- Make sure new code is tested.
- Be mindful of existing code - PRs that break existing code have a high probability of being declined, unless it fixes a serious issue.
If you desperately want to send money my way, I have a BuyMeACoffee.com page:
<a href="https://www.buymeacoffee.com/mna" target="_blank"><img src="https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png" alt="Buy Me A Coffee" style="height: 41px !important;width: 174px !important;box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;-webkit-box-shadow: 0px 3px 2px 0px rgba(190, 190, 190, 0.5) !important;" ></a>
## License
The [BSD 3-Clause license][bsd], the same as the [Go language][golic]. Cascadia's license is [here][caslic].
[jquery]: http://jquery.com/
[go]: http://golang.org/
[cascadia]: https://github.com/andybalholm/cascadia
[cascadiacli]: https://github.com/suntong/cascadia
[bsd]: http://opensource.org/licenses/BSD-3-Clause
[golic]: http://golang.org/LICENSE
[caslic]: https://github.com/andybalholm/cascadia/blob/master/LICENSE
[doc]: http://godoc.org/github.com/PuerkitoBio/goquery
[index]: http://api.jquery.com/index/
[gonet]: https://github.com/golang/net/
[html]: http://godoc.org/golang.org/x/net/html
[wiki]: https://github.com/PuerkitoBio/goquery/wiki/Tips-and-tricks
[thatguystone]: https://github.com/thatguystone
[piotr]: https://github.com/piotrkowalczuk
[goq]: https://github.com/andrewstuart/goq

124
vendor/github.com/PuerkitoBio/goquery/array.go generated vendored Normal file
View File

@ -0,0 +1,124 @@
package goquery
import (
"golang.org/x/net/html"
)
const (
maxUint = ^uint(0)
maxInt = int(maxUint >> 1)
// ToEnd is a special index value that can be used as end index in a call
// to Slice so that all elements are selected until the end of the Selection.
// It is equivalent to passing (*Selection).Length().
ToEnd = maxInt
)
// First reduces the set of matched elements to the first in the set.
// It returns a new Selection object, and an empty Selection object if the
// the selection is empty.
func (s *Selection) First() *Selection {
return s.Eq(0)
}
// Last reduces the set of matched elements to the last in the set.
// It returns a new Selection object, and an empty Selection object if
// the selection is empty.
func (s *Selection) Last() *Selection {
return s.Eq(-1)
}
// Eq reduces the set of matched elements to the one at the specified index.
// If a negative index is given, it counts backwards starting at the end of the
// set. It returns a new Selection object, and an empty Selection object if the
// index is invalid.
func (s *Selection) Eq(index int) *Selection {
if index < 0 {
index += len(s.Nodes)
}
if index >= len(s.Nodes) || index < 0 {
return newEmptySelection(s.document)
}
return s.Slice(index, index+1)
}
// Slice reduces the set of matched elements to a subset specified by a range
// of indices. The start index is 0-based and indicates the index of the first
// element to select. The end index is 0-based and indicates the index at which
// the elements stop being selected (the end index is not selected).
//
// The indices may be negative, in which case they represent an offset from the
// end of the selection.
//
// The special value ToEnd may be specified as end index, in which case all elements
// until the end are selected. This works both for a positive and negative start
// index.
func (s *Selection) Slice(start, end int) *Selection {
if start < 0 {
start += len(s.Nodes)
}
if end == ToEnd {
end = len(s.Nodes)
} else if end < 0 {
end += len(s.Nodes)
}
return pushStack(s, s.Nodes[start:end])
}
// Get retrieves the underlying node at the specified index.
// Get without parameter is not implemented, since the node array is available
// on the Selection object.
func (s *Selection) Get(index int) *html.Node {
if index < 0 {
index += len(s.Nodes) // Negative index gets from the end
}
return s.Nodes[index]
}
// Index returns the position of the first element within the Selection object
// relative to its sibling elements.
func (s *Selection) Index() int {
if len(s.Nodes) > 0 {
return newSingleSelection(s.Nodes[0], s.document).PrevAll().Length()
}
return -1
}
// IndexSelector returns the position of the first element within the
// Selection object relative to the elements matched by the selector, or -1 if
// not found.
func (s *Selection) IndexSelector(selector string) int {
if len(s.Nodes) > 0 {
sel := s.document.Find(selector)
return indexInSlice(sel.Nodes, s.Nodes[0])
}
return -1
}
// IndexMatcher returns the position of the first element within the
// Selection object relative to the elements matched by the matcher, or -1 if
// not found.
func (s *Selection) IndexMatcher(m Matcher) int {
if len(s.Nodes) > 0 {
sel := s.document.FindMatcher(m)
return indexInSlice(sel.Nodes, s.Nodes[0])
}
return -1
}
// IndexOfNode returns the position of the specified node within the Selection
// object, or -1 if not found.
func (s *Selection) IndexOfNode(node *html.Node) int {
return indexInSlice(s.Nodes, node)
}
// IndexOfSelection returns the position of the first node in the specified
// Selection object within this Selection object, or -1 if not found.
func (s *Selection) IndexOfSelection(sel *Selection) int {
if sel != nil && len(sel.Nodes) > 0 {
return indexInSlice(s.Nodes, sel.Nodes[0])
}
return -1
}

123
vendor/github.com/PuerkitoBio/goquery/doc.go generated vendored Normal file
View File

@ -0,0 +1,123 @@
// Copyright (c) 2012-2016, Martin Angers & Contributors
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without modification,
// are permitted provided that the following conditions are met:
//
// * Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above copyright notice,
// this list of conditions and the following disclaimer in the documentation and/or
// other materials provided with the distribution.
// * Neither the name of the author nor the names of its contributors may be used to
// endorse or promote products derived from this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS
// OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY
// WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
/*
Package goquery implements features similar to jQuery, including the chainable
syntax, to manipulate and query an HTML document.
It brings a syntax and a set of features similar to jQuery to the Go language.
It is based on Go's net/html package and the CSS Selector library cascadia.
Since the net/html parser returns nodes, and not a full-featured DOM
tree, jQuery's stateful manipulation functions (like height(), css(), detach())
have been left off.
Also, because the net/html parser requires UTF-8 encoding, so does goquery: it is
the caller's responsibility to ensure that the source document provides UTF-8 encoded HTML.
See the repository's wiki for various options on how to do this.
Syntax-wise, it is as close as possible to jQuery, with the same method names when
possible, and that warm and fuzzy chainable interface. jQuery being the
ultra-popular library that it is, writing a similar HTML-manipulating
library was better to follow its API than to start anew (in the same spirit as
Go's fmt package), even though some of its methods are less than intuitive (looking
at you, index()...).
It is hosted on GitHub, along with additional documentation in the README.md
file: https://github.com/puerkitobio/goquery
Please note that because of the net/html dependency, goquery requires Go1.1+.
The various methods are split into files based on the category of behavior.
The three dots (...) indicate that various "overloads" are available.
* array.go : array-like positional manipulation of the selection.
- Eq()
- First()
- Get()
- Index...()
- Last()
- Slice()
* expand.go : methods that expand or augment the selection's set.
- Add...()
- AndSelf()
- Union(), which is an alias for AddSelection()
* filter.go : filtering methods, that reduce the selection's set.
- End()
- Filter...()
- Has...()
- Intersection(), which is an alias of FilterSelection()
- Not...()
* iteration.go : methods to loop over the selection's nodes.
- Each()
- EachWithBreak()
- Map()
* manipulation.go : methods for modifying the document
- After...()
- Append...()
- Before...()
- Clone()
- Empty()
- Prepend...()
- Remove...()
- ReplaceWith...()
- Unwrap()
- Wrap...()
- WrapAll...()
- WrapInner...()
* property.go : methods that inspect and get the node's properties values.
- Attr*(), RemoveAttr(), SetAttr()
- AddClass(), HasClass(), RemoveClass(), ToggleClass()
- Html()
- Length()
- Size(), which is an alias for Length()
- Text()
* query.go : methods that query, or reflect, a node's identity.
- Contains()
- Is...()
* traversal.go : methods to traverse the HTML document tree.
- Children...()
- Contents()
- Find...()
- Next...()
- Parent[s]...()
- Prev...()
- Siblings...()
* type.go : definition of the types exposed by goquery.
- Document
- Selection
- Matcher
* utilities.go : definition of helper functions (and not methods on a *Selection)
that are not part of jQuery, but are useful to goquery.
- NodeName
- OuterHtml
*/
package goquery

70
vendor/github.com/PuerkitoBio/goquery/expand.go generated vendored Normal file
View File

@ -0,0 +1,70 @@
package goquery
import "golang.org/x/net/html"
// Add adds the selector string's matching nodes to those in the current
// selection and returns a new Selection object.
// The selector string is run in the context of the document of the current
// Selection object.
func (s *Selection) Add(selector string) *Selection {
return s.AddNodes(findWithMatcher([]*html.Node{s.document.rootNode}, compileMatcher(selector))...)
}
// AddMatcher adds the matcher's matching nodes to those in the current
// selection and returns a new Selection object.
// The matcher is run in the context of the document of the current
// Selection object.
func (s *Selection) AddMatcher(m Matcher) *Selection {
return s.AddNodes(findWithMatcher([]*html.Node{s.document.rootNode}, m)...)
}
// AddSelection adds the specified Selection object's nodes to those in the
// current selection and returns a new Selection object.
func (s *Selection) AddSelection(sel *Selection) *Selection {
if sel == nil {
return s.AddNodes()
}
return s.AddNodes(sel.Nodes...)
}
// Union is an alias for AddSelection.
func (s *Selection) Union(sel *Selection) *Selection {
return s.AddSelection(sel)
}
// AddNodes adds the specified nodes to those in the
// current selection and returns a new Selection object.
func (s *Selection) AddNodes(nodes ...*html.Node) *Selection {
return pushStack(s, appendWithoutDuplicates(s.Nodes, nodes, nil))
}
// AndSelf adds the previous set of elements on the stack to the current set.
// It returns a new Selection object containing the current Selection combined
// with the previous one.
// Deprecated: This function has been deprecated and is now an alias for AddBack().
func (s *Selection) AndSelf() *Selection {
return s.AddBack()
}
// AddBack adds the previous set of elements on the stack to the current set.
// It returns a new Selection object containing the current Selection combined
// with the previous one.
func (s *Selection) AddBack() *Selection {
return s.AddSelection(s.prevSel)
}
// AddBackFiltered reduces the previous set of elements on the stack to those that
// match the selector string, and adds them to the current set.
// It returns a new Selection object containing the current Selection combined
// with the filtered previous one
func (s *Selection) AddBackFiltered(selector string) *Selection {
return s.AddSelection(s.prevSel.Filter(selector))
}
// AddBackMatcher reduces the previous set of elements on the stack to those that match
// the mateher, and adds them to the curernt set.
// It returns a new Selection object containing the current Selection combined
// with the filtered previous one
func (s *Selection) AddBackMatcher(m Matcher) *Selection {
return s.AddSelection(s.prevSel.FilterMatcher(m))
}

163
vendor/github.com/PuerkitoBio/goquery/filter.go generated vendored Normal file
View File

@ -0,0 +1,163 @@
package goquery
import "golang.org/x/net/html"
// Filter reduces the set of matched elements to those that match the selector string.
// It returns a new Selection object for this subset of matching elements.
func (s *Selection) Filter(selector string) *Selection {
return s.FilterMatcher(compileMatcher(selector))
}
// FilterMatcher reduces the set of matched elements to those that match
// the given matcher. It returns a new Selection object for this subset
// of matching elements.
func (s *Selection) FilterMatcher(m Matcher) *Selection {
return pushStack(s, winnow(s, m, true))
}
// Not removes elements from the Selection that match the selector string.
// It returns a new Selection object with the matching elements removed.
func (s *Selection) Not(selector string) *Selection {
return s.NotMatcher(compileMatcher(selector))
}
// NotMatcher removes elements from the Selection that match the given matcher.
// It returns a new Selection object with the matching elements removed.
func (s *Selection) NotMatcher(m Matcher) *Selection {
return pushStack(s, winnow(s, m, false))
}
// FilterFunction reduces the set of matched elements to those that pass the function's test.
// It returns a new Selection object for this subset of elements.
func (s *Selection) FilterFunction(f func(int, *Selection) bool) *Selection {
return pushStack(s, winnowFunction(s, f, true))
}
// NotFunction removes elements from the Selection that pass the function's test.
// It returns a new Selection object with the matching elements removed.
func (s *Selection) NotFunction(f func(int, *Selection) bool) *Selection {
return pushStack(s, winnowFunction(s, f, false))
}
// FilterNodes reduces the set of matched elements to those that match the specified nodes.
// It returns a new Selection object for this subset of elements.
func (s *Selection) FilterNodes(nodes ...*html.Node) *Selection {
return pushStack(s, winnowNodes(s, nodes, true))
}
// NotNodes removes elements from the Selection that match the specified nodes.
// It returns a new Selection object with the matching elements removed.
func (s *Selection) NotNodes(nodes ...*html.Node) *Selection {
return pushStack(s, winnowNodes(s, nodes, false))
}
// FilterSelection reduces the set of matched elements to those that match a
// node in the specified Selection object.
// It returns a new Selection object for this subset of elements.
func (s *Selection) FilterSelection(sel *Selection) *Selection {
if sel == nil {
return pushStack(s, winnowNodes(s, nil, true))
}
return pushStack(s, winnowNodes(s, sel.Nodes, true))
}
// NotSelection removes elements from the Selection that match a node in the specified
// Selection object. It returns a new Selection object with the matching elements removed.
func (s *Selection) NotSelection(sel *Selection) *Selection {
if sel == nil {
return pushStack(s, winnowNodes(s, nil, false))
}
return pushStack(s, winnowNodes(s, sel.Nodes, false))
}
// Intersection is an alias for FilterSelection.
func (s *Selection) Intersection(sel *Selection) *Selection {
return s.FilterSelection(sel)
}
// Has reduces the set of matched elements to those that have a descendant
// that matches the selector.
// It returns a new Selection object with the matching elements.
func (s *Selection) Has(selector string) *Selection {
return s.HasSelection(s.document.Find(selector))
}
// HasMatcher reduces the set of matched elements to those that have a descendant
// that matches the matcher.
// It returns a new Selection object with the matching elements.
func (s *Selection) HasMatcher(m Matcher) *Selection {
return s.HasSelection(s.document.FindMatcher(m))
}
// HasNodes reduces the set of matched elements to those that have a
// descendant that matches one of the nodes.
// It returns a new Selection object with the matching elements.
func (s *Selection) HasNodes(nodes ...*html.Node) *Selection {
return s.FilterFunction(func(_ int, sel *Selection) bool {
// Add all nodes that contain one of the specified nodes
for _, n := range nodes {
if sel.Contains(n) {
return true
}
}
return false
})
}
// HasSelection reduces the set of matched elements to those that have a
// descendant that matches one of the nodes of the specified Selection object.
// It returns a new Selection object with the matching elements.
func (s *Selection) HasSelection(sel *Selection) *Selection {
if sel == nil {
return s.HasNodes()
}
return s.HasNodes(sel.Nodes...)
}
// End ends the most recent filtering operation in the current chain and
// returns the set of matched elements to its previous state.
func (s *Selection) End() *Selection {
if s.prevSel != nil {
return s.prevSel
}
return newEmptySelection(s.document)
}
// Filter based on the matcher, and the indicator to keep (Filter) or
// to get rid of (Not) the matching elements.
func winnow(sel *Selection, m Matcher, keep bool) []*html.Node {
// Optimize if keep is requested
if keep {
return m.Filter(sel.Nodes)
}
// Use grep
return grep(sel, func(i int, s *Selection) bool {
return !m.Match(s.Get(0))
})
}
// Filter based on an array of nodes, and the indicator to keep (Filter) or
// to get rid of (Not) the matching elements.
func winnowNodes(sel *Selection, nodes []*html.Node, keep bool) []*html.Node {
if len(nodes)+len(sel.Nodes) < minNodesForSet {
return grep(sel, func(i int, s *Selection) bool {
return isInSlice(nodes, s.Get(0)) == keep
})
}
set := make(map[*html.Node]bool)
for _, n := range nodes {
set[n] = true
}
return grep(sel, func(i int, s *Selection) bool {
return set[s.Get(0)] == keep
})
}
// Filter based on a function test, and the indicator to keep (Filter) or
// to get rid of (Not) the matching elements.
func winnowFunction(sel *Selection, f func(int, *Selection) bool, keep bool) []*html.Node {
return grep(sel, func(i int, s *Selection) bool {
return f(i, s) == keep
})
}

6
vendor/github.com/PuerkitoBio/goquery/go.mod generated vendored Normal file
View File

@ -0,0 +1,6 @@
module github.com/PuerkitoBio/goquery
require (
github.com/andybalholm/cascadia v1.0.0
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a
)

5
vendor/github.com/PuerkitoBio/goquery/go.sum generated vendored Normal file
View File

@ -0,0 +1,5 @@
github.com/andybalholm/cascadia v1.0.0 h1:hOCXnnZ5A+3eVDX8pvgl4kofXv2ELss0bKcqRySc45o=
github.com/andybalholm/cascadia v1.0.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a h1:gOpx8G595UYyvj8UK4+OFyY4rx037g3fmfhe5SasG3U=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=

39
vendor/github.com/PuerkitoBio/goquery/iteration.go generated vendored Normal file
View File

@ -0,0 +1,39 @@
package goquery
// Each iterates over a Selection object, executing a function for each
// matched element. It returns the current Selection object. The function
// f is called for each element in the selection with the index of the
// element in that selection starting at 0, and a *Selection that contains
// only that element.
func (s *Selection) Each(f func(int, *Selection)) *Selection {
for i, n := range s.Nodes {
f(i, newSingleSelection(n, s.document))
}
return s
}
// EachWithBreak iterates over a Selection object, executing a function for each
// matched element. It is identical to Each except that it is possible to break
// out of the loop by returning false in the callback function. It returns the
// current Selection object.
func (s *Selection) EachWithBreak(f func(int, *Selection) bool) *Selection {
for i, n := range s.Nodes {
if !f(i, newSingleSelection(n, s.document)) {
return s
}
}
return s
}
// Map passes each element in the current matched set through a function,
// producing a slice of string holding the returned values. The function
// f is called for each element in the selection with the index of the
// element in that selection starting at 0, and a *Selection that contains
// only that element.
func (s *Selection) Map(f func(int, *Selection) string) (result []string) {
for i, n := range s.Nodes {
result = append(result, f(i, newSingleSelection(n, s.document)))
}
return result
}

574
vendor/github.com/PuerkitoBio/goquery/manipulation.go generated vendored Normal file
View File

@ -0,0 +1,574 @@
package goquery
import (
"strings"
"golang.org/x/net/html"
)
// After applies the selector from the root document and inserts the matched elements
// after the elements in the set of matched elements.
//
// If one of the matched elements in the selection is not currently in the
// document, it's impossible to insert nodes after it, so it will be ignored.
//
// This follows the same rules as Selection.Append.
func (s *Selection) After(selector string) *Selection {
return s.AfterMatcher(compileMatcher(selector))
}
// AfterMatcher applies the matcher from the root document and inserts the matched elements
// after the elements in the set of matched elements.
//
// If one of the matched elements in the selection is not currently in the
// document, it's impossible to insert nodes after it, so it will be ignored.
//
// This follows the same rules as Selection.Append.
func (s *Selection) AfterMatcher(m Matcher) *Selection {
return s.AfterNodes(m.MatchAll(s.document.rootNode)...)
}
// AfterSelection inserts the elements in the selection after each element in the set of matched
// elements.
//
// This follows the same rules as Selection.Append.
func (s *Selection) AfterSelection(sel *Selection) *Selection {
return s.AfterNodes(sel.Nodes...)
}
// AfterHtml parses the html and inserts it after the set of matched elements.
//
// This follows the same rules as Selection.Append.
func (s *Selection) AfterHtml(html string) *Selection {
return s.AfterNodes(parseHtml(html)...)
}
// AfterNodes inserts the nodes after each element in the set of matched elements.
//
// This follows the same rules as Selection.Append.
func (s *Selection) AfterNodes(ns ...*html.Node) *Selection {
return s.manipulateNodes(ns, true, func(sn *html.Node, n *html.Node) {
if sn.Parent != nil {
sn.Parent.InsertBefore(n, sn.NextSibling)
}
})
}
// Append appends the elements specified by the selector to the end of each element
// in the set of matched elements, following those rules:
//
// 1) The selector is applied to the root document.
//
// 2) Elements that are part of the document will be moved to the new location.
//
// 3) If there are multiple locations to append to, cloned nodes will be
// appended to all target locations except the last one, which will be moved
// as noted in (2).
func (s *Selection) Append(selector string) *Selection {
return s.AppendMatcher(compileMatcher(selector))
}
// AppendMatcher appends the elements specified by the matcher to the end of each element
// in the set of matched elements.
//
// This follows the same rules as Selection.Append.
func (s *Selection) AppendMatcher(m Matcher) *Selection {
return s.AppendNodes(m.MatchAll(s.document.rootNode)...)
}
// AppendSelection appends the elements in the selection to the end of each element
// in the set of matched elements.
//
// This follows the same rules as Selection.Append.
func (s *Selection) AppendSelection(sel *Selection) *Selection {
return s.AppendNodes(sel.Nodes...)
}
// AppendHtml parses the html and appends it to the set of matched elements.
func (s *Selection) AppendHtml(html string) *Selection {
return s.AppendNodes(parseHtml(html)...)
}
// AppendNodes appends the specified nodes to each node in the set of matched elements.
//
// This follows the same rules as Selection.Append.
func (s *Selection) AppendNodes(ns ...*html.Node) *Selection {
return s.manipulateNodes(ns, false, func(sn *html.Node, n *html.Node) {
sn.AppendChild(n)
})
}
// Before inserts the matched elements before each element in the set of matched elements.
//
// This follows the same rules as Selection.Append.
func (s *Selection) Before(selector string) *Selection {
return s.BeforeMatcher(compileMatcher(selector))
}
// BeforeMatcher inserts the matched elements before each element in the set of matched elements.
//
// This follows the same rules as Selection.Append.
func (s *Selection) BeforeMatcher(m Matcher) *Selection {
return s.BeforeNodes(m.MatchAll(s.document.rootNode)...)
}
// BeforeSelection inserts the elements in the selection before each element in the set of matched
// elements.
//
// This follows the same rules as Selection.Append.
func (s *Selection) BeforeSelection(sel *Selection) *Selection {
return s.BeforeNodes(sel.Nodes...)
}
// BeforeHtml parses the html and inserts it before the set of matched elements.
//
// This follows the same rules as Selection.Append.
func (s *Selection) BeforeHtml(html string) *Selection {
return s.BeforeNodes(parseHtml(html)...)
}
// BeforeNodes inserts the nodes before each element in the set of matched elements.
//
// This follows the same rules as Selection.Append.
func (s *Selection) BeforeNodes(ns ...*html.Node) *Selection {
return s.manipulateNodes(ns, false, func(sn *html.Node, n *html.Node) {
if sn.Parent != nil {
sn.Parent.InsertBefore(n, sn)
}
})
}
// Clone creates a deep copy of the set of matched nodes. The new nodes will not be
// attached to the document.
func (s *Selection) Clone() *Selection {
ns := newEmptySelection(s.document)
ns.Nodes = cloneNodes(s.Nodes)
return ns
}
// Empty removes all children nodes from the set of matched elements.
// It returns the children nodes in a new Selection.
func (s *Selection) Empty() *Selection {
var nodes []*html.Node
for _, n := range s.Nodes {
for c := n.FirstChild; c != nil; c = n.FirstChild {
n.RemoveChild(c)
nodes = append(nodes, c)
}
}
return pushStack(s, nodes)
}
// Prepend prepends the elements specified by the selector to each element in
// the set of matched elements, following the same rules as Append.
func (s *Selection) Prepend(selector string) *Selection {
return s.PrependMatcher(compileMatcher(selector))
}
// PrependMatcher prepends the elements specified by the matcher to each
// element in the set of matched elements.
//
// This follows the same rules as Selection.Append.
func (s *Selection) PrependMatcher(m Matcher) *Selection {
return s.PrependNodes(m.MatchAll(s.document.rootNode)...)
}
// PrependSelection prepends the elements in the selection to each element in
// the set of matched elements.
//
// This follows the same rules as Selection.Append.
func (s *Selection) PrependSelection(sel *Selection) *Selection {
return s.PrependNodes(sel.Nodes...)
}
// PrependHtml parses the html and prepends it to the set of matched elements.
func (s *Selection) PrependHtml(html string) *Selection {
return s.PrependNodes(parseHtml(html)...)
}
// PrependNodes prepends the specified nodes to each node in the set of
// matched elements.
//
// This follows the same rules as Selection.Append.
func (s *Selection) PrependNodes(ns ...*html.Node) *Selection {
return s.manipulateNodes(ns, true, func(sn *html.Node, n *html.Node) {
// sn.FirstChild may be nil, in which case this functions like
// sn.AppendChild()
sn.InsertBefore(n, sn.FirstChild)
})
}
// Remove removes the set of matched elements from the document.
// It returns the same selection, now consisting of nodes not in the document.
func (s *Selection) Remove() *Selection {
for _, n := range s.Nodes {
if n.Parent != nil {
n.Parent.RemoveChild(n)
}
}
return s
}
// RemoveFiltered removes the set of matched elements by selector.
// It returns the Selection of removed nodes.
func (s *Selection) RemoveFiltered(selector string) *Selection {
return s.RemoveMatcher(compileMatcher(selector))
}
// RemoveMatcher removes the set of matched elements.
// It returns the Selection of removed nodes.
func (s *Selection) RemoveMatcher(m Matcher) *Selection {
return s.FilterMatcher(m).Remove()
}
// ReplaceWith replaces each element in the set of matched elements with the
// nodes matched by the given selector.
// It returns the removed elements.
//
// This follows the same rules as Selection.Append.
func (s *Selection) ReplaceWith(selector string) *Selection {
return s.ReplaceWithMatcher(compileMatcher(selector))
}
// ReplaceWithMatcher replaces each element in the set of matched elements with
// the nodes matched by the given Matcher.
// It returns the removed elements.
//
// This follows the same rules as Selection.Append.
func (s *Selection) ReplaceWithMatcher(m Matcher) *Selection {
return s.ReplaceWithNodes(m.MatchAll(s.document.rootNode)...)
}
// ReplaceWithSelection replaces each element in the set of matched elements with
// the nodes from the given Selection.
// It returns the removed elements.
//
// This follows the same rules as Selection.Append.
func (s *Selection) ReplaceWithSelection(sel *Selection) *Selection {
return s.ReplaceWithNodes(sel.Nodes...)
}
// ReplaceWithHtml replaces each element in the set of matched elements with
// the parsed HTML.
// It returns the removed elements.
//
// This follows the same rules as Selection.Append.
func (s *Selection) ReplaceWithHtml(html string) *Selection {
return s.ReplaceWithNodes(parseHtml(html)...)
}
// ReplaceWithNodes replaces each element in the set of matched elements with
// the given nodes.
// It returns the removed elements.
//
// This follows the same rules as Selection.Append.
func (s *Selection) ReplaceWithNodes(ns ...*html.Node) *Selection {
s.AfterNodes(ns...)
return s.Remove()
}
// SetHtml sets the html content of each element in the selection to
// specified html string.
func (s *Selection) SetHtml(html string) *Selection {
return setHtmlNodes(s, parseHtml(html)...)
}
// SetText sets the content of each element in the selection to specified content.
// The provided text string is escaped.
func (s *Selection) SetText(text string) *Selection {
return s.SetHtml(html.EscapeString(text))
}
// Unwrap removes the parents of the set of matched elements, leaving the matched
// elements (and their siblings, if any) in their place.
// It returns the original selection.
func (s *Selection) Unwrap() *Selection {
s.Parent().Each(func(i int, ss *Selection) {
// For some reason, jquery allows unwrap to remove the <head> element, so
// allowing it here too. Same for <html>. Why it allows those elements to
// be unwrapped while not allowing body is a mystery to me.
if ss.Nodes[0].Data != "body" {
ss.ReplaceWithSelection(ss.Contents())
}
})
return s
}
// Wrap wraps each element in the set of matched elements inside the first
// element matched by the given selector. The matched child is cloned before
// being inserted into the document.
//
// It returns the original set of elements.
func (s *Selection) Wrap(selector string) *Selection {
return s.WrapMatcher(compileMatcher(selector))
}
// WrapMatcher wraps each element in the set of matched elements inside the
// first element matched by the given matcher. The matched child is cloned
// before being inserted into the document.
//
// It returns the original set of elements.
func (s *Selection) WrapMatcher(m Matcher) *Selection {
return s.wrapNodes(m.MatchAll(s.document.rootNode)...)
}
// WrapSelection wraps each element in the set of matched elements inside the
// first element in the given Selection. The element is cloned before being
// inserted into the document.
//
// It returns the original set of elements.
func (s *Selection) WrapSelection(sel *Selection) *Selection {
return s.wrapNodes(sel.Nodes...)
}
// WrapHtml wraps each element in the set of matched elements inside the inner-
// most child of the given HTML.
//
// It returns the original set of elements.
func (s *Selection) WrapHtml(html string) *Selection {
return s.wrapNodes(parseHtml(html)...)
}
// WrapNode wraps each element in the set of matched elements inside the inner-
// most child of the given node. The given node is copied before being inserted
// into the document.
//
// It returns the original set of elements.
func (s *Selection) WrapNode(n *html.Node) *Selection {
return s.wrapNodes(n)
}
func (s *Selection) wrapNodes(ns ...*html.Node) *Selection {
s.Each(func(i int, ss *Selection) {
ss.wrapAllNodes(ns...)
})
return s
}
// WrapAll wraps a single HTML structure, matched by the given selector, around
// all elements in the set of matched elements. The matched child is cloned
// before being inserted into the document.
//
// It returns the original set of elements.
func (s *Selection) WrapAll(selector string) *Selection {
return s.WrapAllMatcher(compileMatcher(selector))
}
// WrapAllMatcher wraps a single HTML structure, matched by the given Matcher,
// around all elements in the set of matched elements. The matched child is
// cloned before being inserted into the document.
//
// It returns the original set of elements.
func (s *Selection) WrapAllMatcher(m Matcher) *Selection {
return s.wrapAllNodes(m.MatchAll(s.document.rootNode)...)
}
// WrapAllSelection wraps a single HTML structure, the first node of the given
// Selection, around all elements in the set of matched elements. The matched
// child is cloned before being inserted into the document.
//
// It returns the original set of elements.
func (s *Selection) WrapAllSelection(sel *Selection) *Selection {
return s.wrapAllNodes(sel.Nodes...)
}
// WrapAllHtml wraps the given HTML structure around all elements in the set of
// matched elements. The matched child is cloned before being inserted into the
// document.
//
// It returns the original set of elements.
func (s *Selection) WrapAllHtml(html string) *Selection {
return s.wrapAllNodes(parseHtml(html)...)
}
func (s *Selection) wrapAllNodes(ns ...*html.Node) *Selection {
if len(ns) > 0 {
return s.WrapAllNode(ns[0])
}
return s
}
// WrapAllNode wraps the given node around the first element in the Selection,
// making all other nodes in the Selection children of the given node. The node
// is cloned before being inserted into the document.
//
// It returns the original set of elements.
func (s *Selection) WrapAllNode(n *html.Node) *Selection {
if s.Size() == 0 {
return s
}
wrap := cloneNode(n)
first := s.Nodes[0]
if first.Parent != nil {
first.Parent.InsertBefore(wrap, first)
first.Parent.RemoveChild(first)
}
for c := getFirstChildEl(wrap); c != nil; c = getFirstChildEl(wrap) {
wrap = c
}
newSingleSelection(wrap, s.document).AppendSelection(s)
return s
}
// WrapInner wraps an HTML structure, matched by the given selector, around the
// content of element in the set of matched elements. The matched child is
// cloned before being inserted into the document.
//
// It returns the original set of elements.
func (s *Selection) WrapInner(selector string) *Selection {
return s.WrapInnerMatcher(compileMatcher(selector))
}
// WrapInnerMatcher wraps an HTML structure, matched by the given selector,
// around the content of element in the set of matched elements. The matched
// child is cloned before being inserted into the document.
//
// It returns the original set of elements.
func (s *Selection) WrapInnerMatcher(m Matcher) *Selection {
return s.wrapInnerNodes(m.MatchAll(s.document.rootNode)...)
}
// WrapInnerSelection wraps an HTML structure, matched by the given selector,
// around the content of element in the set of matched elements. The matched
// child is cloned before being inserted into the document.
//
// It returns the original set of elements.
func (s *Selection) WrapInnerSelection(sel *Selection) *Selection {
return s.wrapInnerNodes(sel.Nodes...)
}
// WrapInnerHtml wraps an HTML structure, matched by the given selector, around
// the content of element in the set of matched elements. The matched child is
// cloned before being inserted into the document.
//
// It returns the original set of elements.
func (s *Selection) WrapInnerHtml(html string) *Selection {
return s.wrapInnerNodes(parseHtml(html)...)
}
// WrapInnerNode wraps an HTML structure, matched by the given selector, around
// the content of element in the set of matched elements. The matched child is
// cloned before being inserted into the document.
//
// It returns the original set of elements.
func (s *Selection) WrapInnerNode(n *html.Node) *Selection {
return s.wrapInnerNodes(n)
}
func (s *Selection) wrapInnerNodes(ns ...*html.Node) *Selection {
if len(ns) == 0 {
return s
}
s.Each(func(i int, s *Selection) {
contents := s.Contents()
if contents.Size() > 0 {
contents.wrapAllNodes(ns...)
} else {
s.AppendNodes(cloneNode(ns[0]))
}
})
return s
}
func parseHtml(h string) []*html.Node {
// Errors are only returned when the io.Reader returns any error besides
// EOF, but strings.Reader never will
nodes, err := html.ParseFragment(strings.NewReader(h), &html.Node{Type: html.ElementNode})
if err != nil {
panic("goquery: failed to parse HTML: " + err.Error())
}
return nodes
}
func setHtmlNodes(s *Selection, ns ...*html.Node) *Selection {
for _, n := range s.Nodes {
for c := n.FirstChild; c != nil; c = n.FirstChild {
n.RemoveChild(c)
}
for _, c := range ns {
n.AppendChild(cloneNode(c))
}
}
return s
}
// Get the first child that is an ElementNode
func getFirstChildEl(n *html.Node) *html.Node {
c := n.FirstChild
for c != nil && c.Type != html.ElementNode {
c = c.NextSibling
}
return c
}
// Deep copy a slice of nodes.
func cloneNodes(ns []*html.Node) []*html.Node {
cns := make([]*html.Node, 0, len(ns))
for _, n := range ns {
cns = append(cns, cloneNode(n))
}
return cns
}
// Deep copy a node. The new node has clones of all the original node's
// children but none of its parents or siblings.
func cloneNode(n *html.Node) *html.Node {
nn := &html.Node{
Type: n.Type,
DataAtom: n.DataAtom,
Data: n.Data,
Attr: make([]html.Attribute, len(n.Attr)),
}
copy(nn.Attr, n.Attr)
for c := n.FirstChild; c != nil; c = c.NextSibling {
nn.AppendChild(cloneNode(c))
}
return nn
}
func (s *Selection) manipulateNodes(ns []*html.Node, reverse bool,
f func(sn *html.Node, n *html.Node)) *Selection {
lasti := s.Size() - 1
// net.Html doesn't provide document fragments for insertion, so to get
// things in the correct order with After() and Prepend(), the callback
// needs to be called on the reverse of the nodes.
if reverse {
for i, j := 0, len(ns)-1; i < j; i, j = i+1, j-1 {
ns[i], ns[j] = ns[j], ns[i]
}
}
for i, sn := range s.Nodes {
for _, n := range ns {
if i != lasti {
f(sn, cloneNode(n))
} else {
if n.Parent != nil {
n.Parent.RemoveChild(n)
}
f(sn, n)
}
}
}
return s
}

275
vendor/github.com/PuerkitoBio/goquery/property.go generated vendored Normal file
View File

@ -0,0 +1,275 @@
package goquery
import (
"bytes"
"regexp"
"strings"
"golang.org/x/net/html"
)
var rxClassTrim = regexp.MustCompile("[\t\r\n]")
// Attr gets the specified attribute's value for the first element in the
// Selection. To get the value for each element individually, use a looping
// construct such as Each or Map method.
func (s *Selection) Attr(attrName string) (val string, exists bool) {
if len(s.Nodes) == 0 {
return
}
return getAttributeValue(attrName, s.Nodes[0])
}
// AttrOr works like Attr but returns default value if attribute is not present.
func (s *Selection) AttrOr(attrName, defaultValue string) string {
if len(s.Nodes) == 0 {
return defaultValue
}
val, exists := getAttributeValue(attrName, s.Nodes[0])
if !exists {
return defaultValue
}
return val
}
// RemoveAttr removes the named attribute from each element in the set of matched elements.
func (s *Selection) RemoveAttr(attrName string) *Selection {
for _, n := range s.Nodes {
removeAttr(n, attrName)
}
return s
}
// SetAttr sets the given attribute on each element in the set of matched elements.
func (s *Selection) SetAttr(attrName, val string) *Selection {
for _, n := range s.Nodes {
attr := getAttributePtr(attrName, n)
if attr == nil {
n.Attr = append(n.Attr, html.Attribute{Key: attrName, Val: val})
} else {
attr.Val = val
}
}
return s
}
// Text gets the combined text contents of each element in the set of matched
// elements, including their descendants.
func (s *Selection) Text() string {
var buf bytes.Buffer
// Slightly optimized vs calling Each: no single selection object created
var f func(*html.Node)
f = func(n *html.Node) {
if n.Type == html.TextNode {
// Keep newlines and spaces, like jQuery
buf.WriteString(n.Data)
}
if n.FirstChild != nil {
for c := n.FirstChild; c != nil; c = c.NextSibling {
f(c)
}
}
}
for _, n := range s.Nodes {
f(n)
}
return buf.String()
}
// Size is an alias for Length.
func (s *Selection) Size() int {
return s.Length()
}
// Length returns the number of elements in the Selection object.
func (s *Selection) Length() int {
return len(s.Nodes)
}
// Html gets the HTML contents of the first element in the set of matched
// elements. It includes text and comment nodes.
func (s *Selection) Html() (ret string, e error) {
// Since there is no .innerHtml, the HTML content must be re-created from
// the nodes using html.Render.
var buf bytes.Buffer
if len(s.Nodes) > 0 {
for c := s.Nodes[0].FirstChild; c != nil; c = c.NextSibling {
e = html.Render(&buf, c)
if e != nil {
return
}
}
ret = buf.String()
}
return
}
// AddClass adds the given class(es) to each element in the set of matched elements.
// Multiple class names can be specified, separated by a space or via multiple arguments.
func (s *Selection) AddClass(class ...string) *Selection {
classStr := strings.TrimSpace(strings.Join(class, " "))
if classStr == "" {
return s
}
tcls := getClassesSlice(classStr)
for _, n := range s.Nodes {
curClasses, attr := getClassesAndAttr(n, true)
for _, newClass := range tcls {
if !strings.Contains(curClasses, " "+newClass+" ") {
curClasses += newClass + " "
}
}
setClasses(n, attr, curClasses)
}
return s
}
// HasClass determines whether any of the matched elements are assigned the
// given class.
func (s *Selection) HasClass(class string) bool {
class = " " + class + " "
for _, n := range s.Nodes {
classes, _ := getClassesAndAttr(n, false)
if strings.Contains(classes, class) {
return true
}
}
return false
}
// RemoveClass removes the given class(es) from each element in the set of matched elements.
// Multiple class names can be specified, separated by a space or via multiple arguments.
// If no class name is provided, all classes are removed.
func (s *Selection) RemoveClass(class ...string) *Selection {
var rclasses []string
classStr := strings.TrimSpace(strings.Join(class, " "))
remove := classStr == ""
if !remove {
rclasses = getClassesSlice(classStr)
}
for _, n := range s.Nodes {
if remove {
removeAttr(n, "class")
} else {
classes, attr := getClassesAndAttr(n, true)
for _, rcl := range rclasses {
classes = strings.Replace(classes, " "+rcl+" ", " ", -1)
}
setClasses(n, attr, classes)
}
}
return s
}
// ToggleClass adds or removes the given class(es) for each element in the set of matched elements.
// Multiple class names can be specified, separated by a space or via multiple arguments.
func (s *Selection) ToggleClass(class ...string) *Selection {
classStr := strings.TrimSpace(strings.Join(class, " "))
if classStr == "" {
return s
}
tcls := getClassesSlice(classStr)
for _, n := range s.Nodes {
classes, attr := getClassesAndAttr(n, true)
for _, tcl := range tcls {
if strings.Contains(classes, " "+tcl+" ") {
classes = strings.Replace(classes, " "+tcl+" ", " ", -1)
} else {
classes += tcl + " "
}
}
setClasses(n, attr, classes)
}
return s
}
func getAttributePtr(attrName string, n *html.Node) *html.Attribute {
if n == nil {
return nil
}
for i, a := range n.Attr {
if a.Key == attrName {
return &n.Attr[i]
}
}
return nil
}
// Private function to get the specified attribute's value from a node.
func getAttributeValue(attrName string, n *html.Node) (val string, exists bool) {
if a := getAttributePtr(attrName, n); a != nil {
val = a.Val
exists = true
}
return
}
// Get and normalize the "class" attribute from the node.
func getClassesAndAttr(n *html.Node, create bool) (classes string, attr *html.Attribute) {
// Applies only to element nodes
if n.Type == html.ElementNode {
attr = getAttributePtr("class", n)
if attr == nil && create {
n.Attr = append(n.Attr, html.Attribute{
Key: "class",
Val: "",
})
attr = &n.Attr[len(n.Attr)-1]
}
}
if attr == nil {
classes = " "
} else {
classes = rxClassTrim.ReplaceAllString(" "+attr.Val+" ", " ")
}
return
}
func getClassesSlice(classes string) []string {
return strings.Split(rxClassTrim.ReplaceAllString(" "+classes+" ", " "), " ")
}
func removeAttr(n *html.Node, attrName string) {
for i, a := range n.Attr {
if a.Key == attrName {
n.Attr[i], n.Attr[len(n.Attr)-1], n.Attr =
n.Attr[len(n.Attr)-1], html.Attribute{}, n.Attr[:len(n.Attr)-1]
return
}
}
}
func setClasses(n *html.Node, attr *html.Attribute, classes string) {
classes = strings.TrimSpace(classes)
if classes == "" {
removeAttr(n, "class")
return
}
attr.Val = classes
}

49
vendor/github.com/PuerkitoBio/goquery/query.go generated vendored Normal file
View File

@ -0,0 +1,49 @@
package goquery
import "golang.org/x/net/html"
// Is checks the current matched set of elements against a selector and
// returns true if at least one of these elements matches.
func (s *Selection) Is(selector string) bool {
return s.IsMatcher(compileMatcher(selector))
}
// IsMatcher checks the current matched set of elements against a matcher and
// returns true if at least one of these elements matches.
func (s *Selection) IsMatcher(m Matcher) bool {
if len(s.Nodes) > 0 {
if len(s.Nodes) == 1 {
return m.Match(s.Nodes[0])
}
return len(m.Filter(s.Nodes)) > 0
}
return false
}
// IsFunction checks the current matched set of elements against a predicate and
// returns true if at least one of these elements matches.
func (s *Selection) IsFunction(f func(int, *Selection) bool) bool {
return s.FilterFunction(f).Length() > 0
}
// IsSelection checks the current matched set of elements against a Selection object
// and returns true if at least one of these elements matches.
func (s *Selection) IsSelection(sel *Selection) bool {
return s.FilterSelection(sel).Length() > 0
}
// IsNodes checks the current matched set of elements against the specified nodes
// and returns true if at least one of these elements matches.
func (s *Selection) IsNodes(nodes ...*html.Node) bool {
return s.FilterNodes(nodes...).Length() > 0
}
// Contains returns true if the specified Node is within,
// at any depth, one of the nodes in the Selection object.
// It is NOT inclusive, to behave like jQuery's implementation, and
// unlike Javascript's .contains, so if the contained
// node is itself in the selection, it returns false.
func (s *Selection) Contains(n *html.Node) bool {
return sliceContains(s.Nodes, n)
}

698
vendor/github.com/PuerkitoBio/goquery/traversal.go generated vendored Normal file
View File

@ -0,0 +1,698 @@
package goquery
import "golang.org/x/net/html"
type siblingType int
// Sibling type, used internally when iterating over children at the same
// level (siblings) to specify which nodes are requested.
const (
siblingPrevUntil siblingType = iota - 3
siblingPrevAll
siblingPrev
siblingAll
siblingNext
siblingNextAll
siblingNextUntil
siblingAllIncludingNonElements
)
// Find gets the descendants of each element in the current set of matched
// elements, filtered by a selector. It returns a new Selection object
// containing these matched elements.
func (s *Selection) Find(selector string) *Selection {
return pushStack(s, findWithMatcher(s.Nodes, compileMatcher(selector)))
}
// FindMatcher gets the descendants of each element in the current set of matched
// elements, filtered by the matcher. It returns a new Selection object
// containing these matched elements.
func (s *Selection) FindMatcher(m Matcher) *Selection {
return pushStack(s, findWithMatcher(s.Nodes, m))
}
// FindSelection gets the descendants of each element in the current
// Selection, filtered by a Selection. It returns a new Selection object
// containing these matched elements.
func (s *Selection) FindSelection(sel *Selection) *Selection {
if sel == nil {
return pushStack(s, nil)
}
return s.FindNodes(sel.Nodes...)
}
// FindNodes gets the descendants of each element in the current
// Selection, filtered by some nodes. It returns a new Selection object
// containing these matched elements.
func (s *Selection) FindNodes(nodes ...*html.Node) *Selection {
return pushStack(s, mapNodes(nodes, func(i int, n *html.Node) []*html.Node {
if sliceContains(s.Nodes, n) {
return []*html.Node{n}
}
return nil
}))
}
// Contents gets the children of each element in the Selection,
// including text and comment nodes. It returns a new Selection object
// containing these elements.
func (s *Selection) Contents() *Selection {
return pushStack(s, getChildrenNodes(s.Nodes, siblingAllIncludingNonElements))
}
// ContentsFiltered gets the children of each element in the Selection,
// filtered by the specified selector. It returns a new Selection
// object containing these elements. Since selectors only act on Element nodes,
// this function is an alias to ChildrenFiltered unless the selector is empty,
// in which case it is an alias to Contents.
func (s *Selection) ContentsFiltered(selector string) *Selection {
if selector != "" {
return s.ChildrenFiltered(selector)
}
return s.Contents()
}
// ContentsMatcher gets the children of each element in the Selection,
// filtered by the specified matcher. It returns a new Selection
// object containing these elements. Since matchers only act on Element nodes,
// this function is an alias to ChildrenMatcher.
func (s *Selection) ContentsMatcher(m Matcher) *Selection {
return s.ChildrenMatcher(m)
}
// Children gets the child elements of each element in the Selection.
// It returns a new Selection object containing these elements.
func (s *Selection) Children() *Selection {
return pushStack(s, getChildrenNodes(s.Nodes, siblingAll))
}
// ChildrenFiltered gets the child elements of each element in the Selection,
// filtered by the specified selector. It returns a new
// Selection object containing these elements.
func (s *Selection) ChildrenFiltered(selector string) *Selection {
return filterAndPush(s, getChildrenNodes(s.Nodes, siblingAll), compileMatcher(selector))
}
// ChildrenMatcher gets the child elements of each element in the Selection,
// filtered by the specified matcher. It returns a new
// Selection object containing these elements.
func (s *Selection) ChildrenMatcher(m Matcher) *Selection {
return filterAndPush(s, getChildrenNodes(s.Nodes, siblingAll), m)
}
// Parent gets the parent of each element in the Selection. It returns a
// new Selection object containing the matched elements.
func (s *Selection) Parent() *Selection {
return pushStack(s, getParentNodes(s.Nodes))
}
// ParentFiltered gets the parent of each element in the Selection filtered by a
// selector. It returns a new Selection object containing the matched elements.
func (s *Selection) ParentFiltered(selector string) *Selection {
return filterAndPush(s, getParentNodes(s.Nodes), compileMatcher(selector))
}
// ParentMatcher gets the parent of each element in the Selection filtered by a
// matcher. It returns a new Selection object containing the matched elements.
func (s *Selection) ParentMatcher(m Matcher) *Selection {
return filterAndPush(s, getParentNodes(s.Nodes), m)
}
// Closest gets the first element that matches the selector by testing the
// element itself and traversing up through its ancestors in the DOM tree.
func (s *Selection) Closest(selector string) *Selection {
cs := compileMatcher(selector)
return s.ClosestMatcher(cs)
}
// ClosestMatcher gets the first element that matches the matcher by testing the
// element itself and traversing up through its ancestors in the DOM tree.
func (s *Selection) ClosestMatcher(m Matcher) *Selection {
return pushStack(s, mapNodes(s.Nodes, func(i int, n *html.Node) []*html.Node {
// For each node in the selection, test the node itself, then each parent
// until a match is found.
for ; n != nil; n = n.Parent {
if m.Match(n) {
return []*html.Node{n}
}
}
return nil
}))
}
// ClosestNodes gets the first element that matches one of the nodes by testing the
// element itself and traversing up through its ancestors in the DOM tree.
func (s *Selection) ClosestNodes(nodes ...*html.Node) *Selection {
set := make(map[*html.Node]bool)
for _, n := range nodes {
set[n] = true
}
return pushStack(s, mapNodes(s.Nodes, func(i int, n *html.Node) []*html.Node {
// For each node in the selection, test the node itself, then each parent
// until a match is found.
for ; n != nil; n = n.Parent {
if set[n] {
return []*html.Node{n}
}
}
return nil
}))
}
// ClosestSelection gets the first element that matches one of the nodes in the
// Selection by testing the element itself and traversing up through its ancestors
// in the DOM tree.
func (s *Selection) ClosestSelection(sel *Selection) *Selection {
if sel == nil {
return pushStack(s, nil)
}
return s.ClosestNodes(sel.Nodes...)
}
// Parents gets the ancestors of each element in the current Selection. It
// returns a new Selection object with the matched elements.
func (s *Selection) Parents() *Selection {
return pushStack(s, getParentsNodes(s.Nodes, nil, nil))
}
// ParentsFiltered gets the ancestors of each element in the current
// Selection. It returns a new Selection object with the matched elements.
func (s *Selection) ParentsFiltered(selector string) *Selection {
return filterAndPush(s, getParentsNodes(s.Nodes, nil, nil), compileMatcher(selector))
}
// ParentsMatcher gets the ancestors of each element in the current
// Selection. It returns a new Selection object with the matched elements.
func (s *Selection) ParentsMatcher(m Matcher) *Selection {
return filterAndPush(s, getParentsNodes(s.Nodes, nil, nil), m)
}
// ParentsUntil gets the ancestors of each element in the Selection, up to but
// not including the element matched by the selector. It returns a new Selection
// object containing the matched elements.
func (s *Selection) ParentsUntil(selector string) *Selection {
return pushStack(s, getParentsNodes(s.Nodes, compileMatcher(selector), nil))
}
// ParentsUntilMatcher gets the ancestors of each element in the Selection, up to but
// not including the element matched by the matcher. It returns a new Selection
// object containing the matched elements.
func (s *Selection) ParentsUntilMatcher(m Matcher) *Selection {
return pushStack(s, getParentsNodes(s.Nodes, m, nil))
}
// ParentsUntilSelection gets the ancestors of each element in the Selection,
// up to but not including the elements in the specified Selection. It returns a
// new Selection object containing the matched elements.
func (s *Selection) ParentsUntilSelection(sel *Selection) *Selection {
if sel == nil {
return s.Parents()
}
return s.ParentsUntilNodes(sel.Nodes...)
}
// ParentsUntilNodes gets the ancestors of each element in the Selection,
// up to but not including the specified nodes. It returns a
// new Selection object containing the matched elements.
func (s *Selection) ParentsUntilNodes(nodes ...*html.Node) *Selection {
return pushStack(s, getParentsNodes(s.Nodes, nil, nodes))
}
// ParentsFilteredUntil is like ParentsUntil, with the option to filter the
// results based on a selector string. It returns a new Selection
// object containing the matched elements.
func (s *Selection) ParentsFilteredUntil(filterSelector, untilSelector string) *Selection {
return filterAndPush(s, getParentsNodes(s.Nodes, compileMatcher(untilSelector), nil), compileMatcher(filterSelector))
}
// ParentsFilteredUntilMatcher is like ParentsUntilMatcher, with the option to filter the
// results based on a matcher. It returns a new Selection object containing the matched elements.
func (s *Selection) ParentsFilteredUntilMatcher(filter, until Matcher) *Selection {
return filterAndPush(s, getParentsNodes(s.Nodes, until, nil), filter)
}
// ParentsFilteredUntilSelection is like ParentsUntilSelection, with the
// option to filter the results based on a selector string. It returns a new
// Selection object containing the matched elements.
func (s *Selection) ParentsFilteredUntilSelection(filterSelector string, sel *Selection) *Selection {
return s.ParentsMatcherUntilSelection(compileMatcher(filterSelector), sel)
}
// ParentsMatcherUntilSelection is like ParentsUntilSelection, with the
// option to filter the results based on a matcher. It returns a new
// Selection object containing the matched elements.
func (s *Selection) ParentsMatcherUntilSelection(filter Matcher, sel *Selection) *Selection {
if sel == nil {
return s.ParentsMatcher(filter)
}
return s.ParentsMatcherUntilNodes(filter, sel.Nodes...)
}
// ParentsFilteredUntilNodes is like ParentsUntilNodes, with the
// option to filter the results based on a selector string. It returns a new
// Selection object containing the matched elements.
func (s *Selection) ParentsFilteredUntilNodes(filterSelector string, nodes ...*html.Node) *Selection {
return filterAndPush(s, getParentsNodes(s.Nodes, nil, nodes), compileMatcher(filterSelector))
}
// ParentsMatcherUntilNodes is like ParentsUntilNodes, with the
// option to filter the results based on a matcher. It returns a new
// Selection object containing the matched elements.
func (s *Selection) ParentsMatcherUntilNodes(filter Matcher, nodes ...*html.Node) *Selection {
return filterAndPush(s, getParentsNodes(s.Nodes, nil, nodes), filter)
}
// Siblings gets the siblings of each element in the Selection. It returns
// a new Selection object containing the matched elements.
func (s *Selection) Siblings() *Selection {
return pushStack(s, getSiblingNodes(s.Nodes, siblingAll, nil, nil))
}
// SiblingsFiltered gets the siblings of each element in the Selection
// filtered by a selector. It returns a new Selection object containing the
// matched elements.
func (s *Selection) SiblingsFiltered(selector string) *Selection {
return filterAndPush(s, getSiblingNodes(s.Nodes, siblingAll, nil, nil), compileMatcher(selector))
}
// SiblingsMatcher gets the siblings of each element in the Selection
// filtered by a matcher. It returns a new Selection object containing the
// matched elements.
func (s *Selection) SiblingsMatcher(m Matcher) *Selection {
return filterAndPush(s, getSiblingNodes(s.Nodes, siblingAll, nil, nil), m)
}
// Next gets the immediately following sibling of each element in the
// Selection. It returns a new Selection object containing the matched elements.
func (s *Selection) Next() *Selection {
return pushStack(s, getSiblingNodes(s.Nodes, siblingNext, nil, nil))
}
// NextFiltered gets the immediately following sibling of each element in the
// Selection filtered by a selector. It returns a new Selection object
// containing the matched elements.
func (s *Selection) NextFiltered(selector string) *Selection {
return filterAndPush(s, getSiblingNodes(s.Nodes, siblingNext, nil, nil), compileMatcher(selector))
}
// NextMatcher gets the immediately following sibling of each element in the
// Selection filtered by a matcher. It returns a new Selection object
// containing the matched elements.
func (s *Selection) NextMatcher(m Matcher) *Selection {
return filterAndPush(s, getSiblingNodes(s.Nodes, siblingNext, nil, nil), m)
}
// NextAll gets all the following siblings of each element in the
// Selection. It returns a new Selection object containing the matched elements.
func (s *Selection) NextAll() *Selection {
return pushStack(s, getSiblingNodes(s.Nodes, siblingNextAll, nil, nil))
}
// NextAllFiltered gets all the following siblings of each element in the
// Selection filtered by a selector. It returns a new Selection object
// containing the matched elements.
func (s *Selection) NextAllFiltered(selector string) *Selection {
return filterAndPush(s, getSiblingNodes(s.Nodes, siblingNextAll, nil, nil), compileMatcher(selector))
}
// NextAllMatcher gets all the following siblings of each element in the
// Selection filtered by a matcher. It returns a new Selection object
// containing the matched elements.
func (s *Selection) NextAllMatcher(m Matcher) *Selection {
return filterAndPush(s, getSiblingNodes(s.Nodes, siblingNextAll, nil, nil), m)
}
// Prev gets the immediately preceding sibling of each element in the
// Selection. It returns a new Selection object containing the matched elements.
func (s *Selection) Prev() *Selection {
return pushStack(s, getSiblingNodes(s.Nodes, siblingPrev, nil, nil))
}
// PrevFiltered gets the immediately preceding sibling of each element in the
// Selection filtered by a selector. It returns a new Selection object
// containing the matched elements.
func (s *Selection) PrevFiltered(selector string) *Selection {
return filterAndPush(s, getSiblingNodes(s.Nodes, siblingPrev, nil, nil), compileMatcher(selector))
}
// PrevMatcher gets the immediately preceding sibling of each element in the
// Selection filtered by a matcher. It returns a new Selection object
// containing the matched elements.
func (s *Selection) PrevMatcher(m Matcher) *Selection {
return filterAndPush(s, getSiblingNodes(s.Nodes, siblingPrev, nil, nil), m)
}
// PrevAll gets all the preceding siblings of each element in the
// Selection. It returns a new Selection object containing the matched elements.
func (s *Selection) PrevAll() *Selection {
return pushStack(s, getSiblingNodes(s.Nodes, siblingPrevAll, nil, nil))
}
// PrevAllFiltered gets all the preceding siblings of each element in the
// Selection filtered by a selector. It returns a new Selection object
// containing the matched elements.
func (s *Selection) PrevAllFiltered(selector string) *Selection {
return filterAndPush(s, getSiblingNodes(s.Nodes, siblingPrevAll, nil, nil), compileMatcher(selector))
}
// PrevAllMatcher gets all the preceding siblings of each element in the
// Selection filtered by a matcher. It returns a new Selection object
// containing the matched elements.
func (s *Selection) PrevAllMatcher(m Matcher) *Selection {
return filterAndPush(s, getSiblingNodes(s.Nodes, siblingPrevAll, nil, nil), m)
}
// NextUntil gets all following siblings of each element up to but not
// including the element matched by the selector. It returns a new Selection
// object containing the matched elements.
func (s *Selection) NextUntil(selector string) *Selection {
return pushStack(s, getSiblingNodes(s.Nodes, siblingNextUntil,
compileMatcher(selector), nil))
}
// NextUntilMatcher gets all following siblings of each element up to but not
// including the element matched by the matcher. It returns a new Selection
// object containing the matched elements.
func (s *Selection) NextUntilMatcher(m Matcher) *Selection {
return pushStack(s, getSiblingNodes(s.Nodes, siblingNextUntil,
m, nil))
}
// NextUntilSelection gets all following siblings of each element up to but not
// including the element matched by the Selection. It returns a new Selection
// object containing the matched elements.
func (s *Selection) NextUntilSelection(sel *Selection) *Selection {
if sel == nil {
return s.NextAll()
}
return s.NextUntilNodes(sel.Nodes...)
}
// NextUntilNodes gets all following siblings of each element up to but not
// including the element matched by the nodes. It returns a new Selection
// object containing the matched elements.
func (s *Selection) NextUntilNodes(nodes ...*html.Node) *Selection {
return pushStack(s, getSiblingNodes(s.Nodes, siblingNextUntil,
nil, nodes))
}
// PrevUntil gets all preceding siblings of each element up to but not
// including the element matched by the selector. It returns a new Selection
// object containing the matched elements.
func (s *Selection) PrevUntil(selector string) *Selection {
return pushStack(s, getSiblingNodes(s.Nodes, siblingPrevUntil,
compileMatcher(selector), nil))
}
// PrevUntilMatcher gets all preceding siblings of each element up to but not
// including the element matched by the matcher. It returns a new Selection
// object containing the matched elements.
func (s *Selection) PrevUntilMatcher(m Matcher) *Selection {
return pushStack(s, getSiblingNodes(s.Nodes, siblingPrevUntil,
m, nil))
}
// PrevUntilSelection gets all preceding siblings of each element up to but not
// including the element matched by the Selection. It returns a new Selection
// object containing the matched elements.
func (s *Selection) PrevUntilSelection(sel *Selection) *Selection {
if sel == nil {
return s.PrevAll()
}
return s.PrevUntilNodes(sel.Nodes...)
}
// PrevUntilNodes gets all preceding siblings of each element up to but not
// including the element matched by the nodes. It returns a new Selection
// object containing the matched elements.
func (s *Selection) PrevUntilNodes(nodes ...*html.Node) *Selection {
return pushStack(s, getSiblingNodes(s.Nodes, siblingPrevUntil,
nil, nodes))
}
// NextFilteredUntil is like NextUntil, with the option to filter
// the results based on a selector string.
// It returns a new Selection object containing the matched elements.
func (s *Selection) NextFilteredUntil(filterSelector, untilSelector string) *Selection {
return filterAndPush(s, getSiblingNodes(s.Nodes, siblingNextUntil,
compileMatcher(untilSelector), nil), compileMatcher(filterSelector))
}
// NextFilteredUntilMatcher is like NextUntilMatcher, with the option to filter
// the results based on a matcher.
// It returns a new Selection object containing the matched elements.
func (s *Selection) NextFilteredUntilMatcher(filter, until Matcher) *Selection {
return filterAndPush(s, getSiblingNodes(s.Nodes, siblingNextUntil,
until, nil), filter)
}
// NextFilteredUntilSelection is like NextUntilSelection, with the
// option to filter the results based on a selector string. It returns a new
// Selection object containing the matched elements.
func (s *Selection) NextFilteredUntilSelection(filterSelector string, sel *Selection) *Selection {
return s.NextMatcherUntilSelection(compileMatcher(filterSelector), sel)
}
// NextMatcherUntilSelection is like NextUntilSelection, with the
// option to filter the results based on a matcher. It returns a new
// Selection object containing the matched elements.
func (s *Selection) NextMatcherUntilSelection(filter Matcher, sel *Selection) *Selection {
if sel == nil {
return s.NextMatcher(filter)
}
return s.NextMatcherUntilNodes(filter, sel.Nodes...)
}
// NextFilteredUntilNodes is like NextUntilNodes, with the
// option to filter the results based on a selector string. It returns a new
// Selection object containing the matched elements.
func (s *Selection) NextFilteredUntilNodes(filterSelector string, nodes ...*html.Node) *Selection {
return filterAndPush(s, getSiblingNodes(s.Nodes, siblingNextUntil,
nil, nodes), compileMatcher(filterSelector))
}
// NextMatcherUntilNodes is like NextUntilNodes, with the
// option to filter the results based on a matcher. It returns a new
// Selection object containing the matched elements.
func (s *Selection) NextMatcherUntilNodes(filter Matcher, nodes ...*html.Node) *Selection {
return filterAndPush(s, getSiblingNodes(s.Nodes, siblingNextUntil,
nil, nodes), filter)
}
// PrevFilteredUntil is like PrevUntil, with the option to filter
// the results based on a selector string.
// It returns a new Selection object containing the matched elements.
func (s *Selection) PrevFilteredUntil(filterSelector, untilSelector string) *Selection {
return filterAndPush(s, getSiblingNodes(s.Nodes, siblingPrevUntil,
compileMatcher(untilSelector), nil), compileMatcher(filterSelector))
}
// PrevFilteredUntilMatcher is like PrevUntilMatcher, with the option to filter
// the results based on a matcher.
// It returns a new Selection object containing the matched elements.
func (s *Selection) PrevFilteredUntilMatcher(filter, until Matcher) *Selection {
return filterAndPush(s, getSiblingNodes(s.Nodes, siblingPrevUntil,
until, nil), filter)
}
// PrevFilteredUntilSelection is like PrevUntilSelection, with the
// option to filter the results based on a selector string. It returns a new
// Selection object containing the matched elements.
func (s *Selection) PrevFilteredUntilSelection(filterSelector string, sel *Selection) *Selection {
return s.PrevMatcherUntilSelection(compileMatcher(filterSelector), sel)
}
// PrevMatcherUntilSelection is like PrevUntilSelection, with the
// option to filter the results based on a matcher. It returns a new
// Selection object containing the matched elements.
func (s *Selection) PrevMatcherUntilSelection(filter Matcher, sel *Selection) *Selection {
if sel == nil {
return s.PrevMatcher(filter)
}
return s.PrevMatcherUntilNodes(filter, sel.Nodes...)
}
// PrevFilteredUntilNodes is like PrevUntilNodes, with the
// option to filter the results based on a selector string. It returns a new
// Selection object containing the matched elements.
func (s *Selection) PrevFilteredUntilNodes(filterSelector string, nodes ...*html.Node) *Selection {
return filterAndPush(s, getSiblingNodes(s.Nodes, siblingPrevUntil,
nil, nodes), compileMatcher(filterSelector))
}
// PrevMatcherUntilNodes is like PrevUntilNodes, with the
// option to filter the results based on a matcher. It returns a new
// Selection object containing the matched elements.
func (s *Selection) PrevMatcherUntilNodes(filter Matcher, nodes ...*html.Node) *Selection {
return filterAndPush(s, getSiblingNodes(s.Nodes, siblingPrevUntil,
nil, nodes), filter)
}
// Filter and push filters the nodes based on a matcher, and pushes the results
// on the stack, with the srcSel as previous selection.
func filterAndPush(srcSel *Selection, nodes []*html.Node, m Matcher) *Selection {
// Create a temporary Selection with the specified nodes to filter using winnow
sel := &Selection{nodes, srcSel.document, nil}
// Filter based on matcher and push on stack
return pushStack(srcSel, winnow(sel, m, true))
}
// Internal implementation of Find that return raw nodes.
func findWithMatcher(nodes []*html.Node, m Matcher) []*html.Node {
// Map nodes to find the matches within the children of each node
return mapNodes(nodes, func(i int, n *html.Node) (result []*html.Node) {
// Go down one level, becausejQuery's Find selects only within descendants
for c := n.FirstChild; c != nil; c = c.NextSibling {
if c.Type == html.ElementNode {
result = append(result, m.MatchAll(c)...)
}
}
return
})
}
// Internal implementation to get all parent nodes, stopping at the specified
// node (or nil if no stop).
func getParentsNodes(nodes []*html.Node, stopm Matcher, stopNodes []*html.Node) []*html.Node {
return mapNodes(nodes, func(i int, n *html.Node) (result []*html.Node) {
for p := n.Parent; p != nil; p = p.Parent {
sel := newSingleSelection(p, nil)
if stopm != nil {
if sel.IsMatcher(stopm) {
break
}
} else if len(stopNodes) > 0 {
if sel.IsNodes(stopNodes...) {
break
}
}
if p.Type == html.ElementNode {
result = append(result, p)
}
}
return
})
}
// Internal implementation of sibling nodes that return a raw slice of matches.
func getSiblingNodes(nodes []*html.Node, st siblingType, untilm Matcher, untilNodes []*html.Node) []*html.Node {
var f func(*html.Node) bool
// If the requested siblings are ...Until, create the test function to
// determine if the until condition is reached (returns true if it is)
if st == siblingNextUntil || st == siblingPrevUntil {
f = func(n *html.Node) bool {
if untilm != nil {
// Matcher-based condition
sel := newSingleSelection(n, nil)
return sel.IsMatcher(untilm)
} else if len(untilNodes) > 0 {
// Nodes-based condition
sel := newSingleSelection(n, nil)
return sel.IsNodes(untilNodes...)
}
return false
}
}
return mapNodes(nodes, func(i int, n *html.Node) []*html.Node {
return getChildrenWithSiblingType(n.Parent, st, n, f)
})
}
// Gets the children nodes of each node in the specified slice of nodes,
// based on the sibling type request.
func getChildrenNodes(nodes []*html.Node, st siblingType) []*html.Node {
return mapNodes(nodes, func(i int, n *html.Node) []*html.Node {
return getChildrenWithSiblingType(n, st, nil, nil)
})
}
// Gets the children of the specified parent, based on the requested sibling
// type, skipping a specified node if required.
func getChildrenWithSiblingType(parent *html.Node, st siblingType, skipNode *html.Node,
untilFunc func(*html.Node) bool) (result []*html.Node) {
// Create the iterator function
var iter = func(cur *html.Node) (ret *html.Node) {
// Based on the sibling type requested, iterate the right way
for {
switch st {
case siblingAll, siblingAllIncludingNonElements:
if cur == nil {
// First iteration, start with first child of parent
// Skip node if required
if ret = parent.FirstChild; ret == skipNode && skipNode != nil {
ret = skipNode.NextSibling
}
} else {
// Skip node if required
if ret = cur.NextSibling; ret == skipNode && skipNode != nil {
ret = skipNode.NextSibling
}
}
case siblingPrev, siblingPrevAll, siblingPrevUntil:
if cur == nil {
// Start with previous sibling of the skip node
ret = skipNode.PrevSibling
} else {
ret = cur.PrevSibling
}
case siblingNext, siblingNextAll, siblingNextUntil:
if cur == nil {
// Start with next sibling of the skip node
ret = skipNode.NextSibling
} else {
ret = cur.NextSibling
}
default:
panic("Invalid sibling type.")
}
if ret == nil || ret.Type == html.ElementNode || st == siblingAllIncludingNonElements {
return
}
// Not a valid node, try again from this one
cur = ret
}
}
for c := iter(nil); c != nil; c = iter(c) {
// If this is an ...Until case, test before append (returns true
// if the until condition is reached)
if st == siblingNextUntil || st == siblingPrevUntil {
if untilFunc(c) {
return
}
}
result = append(result, c)
if st == siblingNext || st == siblingPrev {
// Only one node was requested (immediate next or previous), so exit
return
}
}
return
}
// Internal implementation of parent nodes that return a raw slice of Nodes.
func getParentNodes(nodes []*html.Node) []*html.Node {
return mapNodes(nodes, func(i int, n *html.Node) []*html.Node {
if n.Parent != nil && n.Parent.Type == html.ElementNode {
return []*html.Node{n.Parent}
}
return nil
})
}
// Internal map function used by many traversing methods. Takes the source nodes
// to iterate on and the mapping function that returns an array of nodes.
// Returns an array of nodes mapped by calling the callback function once for
// each node in the source nodes.
func mapNodes(nodes []*html.Node, f func(int, *html.Node) []*html.Node) (result []*html.Node) {
set := make(map[*html.Node]bool)
for i, n := range nodes {
if vals := f(i, n); len(vals) > 0 {
result = appendWithoutDuplicates(result, vals, set)
}
}
return result
}

141
vendor/github.com/PuerkitoBio/goquery/type.go generated vendored Normal file
View File

@ -0,0 +1,141 @@
package goquery
import (
"errors"
"io"
"net/http"
"net/url"
"github.com/andybalholm/cascadia"
"golang.org/x/net/html"
)
// Document represents an HTML document to be manipulated. Unlike jQuery, which
// is loaded as part of a DOM document, and thus acts upon its containing
// document, GoQuery doesn't know which HTML document to act upon. So it needs
// to be told, and that's what the Document class is for. It holds the root
// document node to manipulate, and can make selections on this document.
type Document struct {
*Selection
Url *url.URL
rootNode *html.Node
}
// NewDocumentFromNode is a Document constructor that takes a root html Node
// as argument.
func NewDocumentFromNode(root *html.Node) *Document {
return newDocument(root, nil)
}
// NewDocument is a Document constructor that takes a string URL as argument.
// It loads the specified document, parses it, and stores the root Document
// node, ready to be manipulated.
//
// Deprecated: Use the net/http standard library package to make the request
// and validate the response before calling goquery.NewDocumentFromReader
// with the response's body.
func NewDocument(url string) (*Document, error) {
// Load the URL
res, e := http.Get(url)
if e != nil {
return nil, e
}
return NewDocumentFromResponse(res)
}
// NewDocumentFromReader returns a Document from an io.Reader.
// It returns an error as second value if the reader's data cannot be parsed
// as html. It does not check if the reader is also an io.Closer, the
// provided reader is never closed by this call. It is the responsibility
// of the caller to close it if required.
func NewDocumentFromReader(r io.Reader) (*Document, error) {
root, e := html.Parse(r)
if e != nil {
return nil, e
}
return newDocument(root, nil), nil
}
// NewDocumentFromResponse is another Document constructor that takes an http response as argument.
// It loads the specified response's document, parses it, and stores the root Document
// node, ready to be manipulated. The response's body is closed on return.
//
// Deprecated: Use goquery.NewDocumentFromReader with the response's body.
func NewDocumentFromResponse(res *http.Response) (*Document, error) {
if res == nil {
return nil, errors.New("Response is nil")
}
defer res.Body.Close()
if res.Request == nil {
return nil, errors.New("Response.Request is nil")
}
// Parse the HTML into nodes
root, e := html.Parse(res.Body)
if e != nil {
return nil, e
}
// Create and fill the document
return newDocument(root, res.Request.URL), nil
}
// CloneDocument creates a deep-clone of a document.
func CloneDocument(doc *Document) *Document {
return newDocument(cloneNode(doc.rootNode), doc.Url)
}
// Private constructor, make sure all fields are correctly filled.
func newDocument(root *html.Node, url *url.URL) *Document {
// Create and fill the document
d := &Document{nil, url, root}
d.Selection = newSingleSelection(root, d)
return d
}
// Selection represents a collection of nodes matching some criteria. The
// initial Selection can be created by using Document.Find, and then
// manipulated using the jQuery-like chainable syntax and methods.
type Selection struct {
Nodes []*html.Node
document *Document
prevSel *Selection
}
// Helper constructor to create an empty selection
func newEmptySelection(doc *Document) *Selection {
return &Selection{nil, doc, nil}
}
// Helper constructor to create a selection of only one node
func newSingleSelection(node *html.Node, doc *Document) *Selection {
return &Selection{[]*html.Node{node}, doc, nil}
}
// Matcher is an interface that defines the methods to match
// HTML nodes against a compiled selector string. Cascadia's
// Selector implements this interface.
type Matcher interface {
Match(*html.Node) bool
MatchAll(*html.Node) []*html.Node
Filter([]*html.Node) []*html.Node
}
// compileMatcher compiles the selector string s and returns
// the corresponding Matcher. If s is an invalid selector string,
// it returns a Matcher that fails all matches.
func compileMatcher(s string) Matcher {
cs, err := cascadia.Compile(s)
if err != nil {
return invalidMatcher{}
}
return cs
}
// invalidMatcher is a Matcher that always fails to match.
type invalidMatcher struct{}
func (invalidMatcher) Match(n *html.Node) bool { return false }
func (invalidMatcher) MatchAll(n *html.Node) []*html.Node { return nil }
func (invalidMatcher) Filter(ns []*html.Node) []*html.Node { return nil }

161
vendor/github.com/PuerkitoBio/goquery/utilities.go generated vendored Normal file
View File

@ -0,0 +1,161 @@
package goquery
import (
"bytes"
"golang.org/x/net/html"
)
// used to determine if a set (map[*html.Node]bool) should be used
// instead of iterating over a slice. The set uses more memory and
// is slower than slice iteration for small N.
const minNodesForSet = 1000
var nodeNames = []string{
html.ErrorNode: "#error",
html.TextNode: "#text",
html.DocumentNode: "#document",
html.CommentNode: "#comment",
}
// NodeName returns the node name of the first element in the selection.
// It tries to behave in a similar way as the DOM's nodeName property
// (https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeName).
//
// Go's net/html package defines the following node types, listed with
// the corresponding returned value from this function:
//
// ErrorNode : #error
// TextNode : #text
// DocumentNode : #document
// ElementNode : the element's tag name
// CommentNode : #comment
// DoctypeNode : the name of the document type
//
func NodeName(s *Selection) string {
if s.Length() == 0 {
return ""
}
switch n := s.Get(0); n.Type {
case html.ElementNode, html.DoctypeNode:
return n.Data
default:
if n.Type >= 0 && int(n.Type) < len(nodeNames) {
return nodeNames[n.Type]
}
return ""
}
}
// OuterHtml returns the outer HTML rendering of the first item in
// the selection - that is, the HTML including the first element's
// tag and attributes.
//
// Unlike InnerHtml, this is a function and not a method on the Selection,
// because this is not a jQuery method (in javascript-land, this is
// a property provided by the DOM).
func OuterHtml(s *Selection) (string, error) {
var buf bytes.Buffer
if s.Length() == 0 {
return "", nil
}
n := s.Get(0)
if err := html.Render(&buf, n); err != nil {
return "", err
}
return buf.String(), nil
}
// Loop through all container nodes to search for the target node.
func sliceContains(container []*html.Node, contained *html.Node) bool {
for _, n := range container {
if nodeContains(n, contained) {
return true
}
}
return false
}
// Checks if the contained node is within the container node.
func nodeContains(container *html.Node, contained *html.Node) bool {
// Check if the parent of the contained node is the container node, traversing
// upward until the top is reached, or the container is found.
for contained = contained.Parent; contained != nil; contained = contained.Parent {
if container == contained {
return true
}
}
return false
}
// Checks if the target node is in the slice of nodes.
func isInSlice(slice []*html.Node, node *html.Node) bool {
return indexInSlice(slice, node) > -1
}
// Returns the index of the target node in the slice, or -1.
func indexInSlice(slice []*html.Node, node *html.Node) int {
if node != nil {
for i, n := range slice {
if n == node {
return i
}
}
}
return -1
}
// Appends the new nodes to the target slice, making sure no duplicate is added.
// There is no check to the original state of the target slice, so it may still
// contain duplicates. The target slice is returned because append() may create
// a new underlying array. If targetSet is nil, a local set is created with the
// target if len(target) + len(nodes) is greater than minNodesForSet.
func appendWithoutDuplicates(target []*html.Node, nodes []*html.Node, targetSet map[*html.Node]bool) []*html.Node {
// if there are not that many nodes, don't use the map, faster to just use nested loops
// (unless a non-nil targetSet is passed, in which case the caller knows better).
if targetSet == nil && len(target)+len(nodes) < minNodesForSet {
for _, n := range nodes {
if !isInSlice(target, n) {
target = append(target, n)
}
}
return target
}
// if a targetSet is passed, then assume it is reliable, otherwise create one
// and initialize it with the current target contents.
if targetSet == nil {
targetSet = make(map[*html.Node]bool, len(target))
for _, n := range target {
targetSet[n] = true
}
}
for _, n := range nodes {
if !targetSet[n] {
target = append(target, n)
targetSet[n] = true
}
}
return target
}
// Loop through a selection, returning only those nodes that pass the predicate
// function.
func grep(sel *Selection, predicate func(i int, s *Selection) bool) (result []*html.Node) {
for i, n := range sel.Nodes {
if predicate(i, newSingleSelection(n, sel.document)) {
result = append(result, n)
}
}
return result
}
// Creates a new Selection object based on the specified nodes, and keeps the
// source Selection object on the stack (linked list).
func pushStack(fromSel *Selection, nodes []*html.Node) *Selection {
result := &Selection{nodes, fromSel.document, fromSel}
return result
}

14
vendor/github.com/andybalholm/cascadia/.travis.yml generated vendored Normal file
View File

@ -0,0 +1,14 @@
language: go
go:
- 1.3
- 1.4
install:
- go get github.com/andybalholm/cascadia
script:
- go test -v
notifications:
email: false

24
vendor/github.com/andybalholm/cascadia/LICENSE generated vendored Normal file
View File

@ -0,0 +1,24 @@
Copyright (c) 2011 Andy Balholm. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

7
vendor/github.com/andybalholm/cascadia/README.md generated vendored Normal file
View File

@ -0,0 +1,7 @@
# cascadia
[![](https://travis-ci.org/andybalholm/cascadia.svg)](https://travis-ci.org/andybalholm/cascadia)
The Cascadia package implements CSS selectors for use with the parse trees produced by the html package.
To test CSS selectors without writing Go code, check out [cascadia](https://github.com/suntong/cascadia) the command line tool, a thin wrapper around this package.

3
vendor/github.com/andybalholm/cascadia/go.mod generated vendored Normal file
View File

@ -0,0 +1,3 @@
module "github.com/andybalholm/cascadia"
require "golang.org/x/net" v0.0.0-20180218175443-cbe0f9307d01

835
vendor/github.com/andybalholm/cascadia/parser.go generated vendored Normal file
View File

@ -0,0 +1,835 @@
// Package cascadia is an implementation of CSS selectors.
package cascadia
import (
"errors"
"fmt"
"regexp"
"strconv"
"strings"
"golang.org/x/net/html"
)
// a parser for CSS selectors
type parser struct {
s string // the source text
i int // the current position
}
// parseEscape parses a backslash escape.
func (p *parser) parseEscape() (result string, err error) {
if len(p.s) < p.i+2 || p.s[p.i] != '\\' {
return "", errors.New("invalid escape sequence")
}
start := p.i + 1
c := p.s[start]
switch {
case c == '\r' || c == '\n' || c == '\f':
return "", errors.New("escaped line ending outside string")
case hexDigit(c):
// unicode escape (hex)
var i int
for i = start; i < p.i+6 && i < len(p.s) && hexDigit(p.s[i]); i++ {
// empty
}
v, _ := strconv.ParseUint(p.s[start:i], 16, 21)
if len(p.s) > i {
switch p.s[i] {
case '\r':
i++
if len(p.s) > i && p.s[i] == '\n' {
i++
}
case ' ', '\t', '\n', '\f':
i++
}
}
p.i = i
return string(rune(v)), nil
}
// Return the literal character after the backslash.
result = p.s[start : start+1]
p.i += 2
return result, nil
}
func hexDigit(c byte) bool {
return '0' <= c && c <= '9' || 'a' <= c && c <= 'f' || 'A' <= c && c <= 'F'
}
// nameStart returns whether c can be the first character of an identifier
// (not counting an initial hyphen, or an escape sequence).
func nameStart(c byte) bool {
return 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || c == '_' || c > 127
}
// nameChar returns whether c can be a character within an identifier
// (not counting an escape sequence).
func nameChar(c byte) bool {
return 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || c == '_' || c > 127 ||
c == '-' || '0' <= c && c <= '9'
}
// parseIdentifier parses an identifier.
func (p *parser) parseIdentifier() (result string, err error) {
startingDash := false
if len(p.s) > p.i && p.s[p.i] == '-' {
startingDash = true
p.i++
}
if len(p.s) <= p.i {
return "", errors.New("expected identifier, found EOF instead")
}
if c := p.s[p.i]; !(nameStart(c) || c == '\\') {
return "", fmt.Errorf("expected identifier, found %c instead", c)
}
result, err = p.parseName()
if startingDash && err == nil {
result = "-" + result
}
return
}
// parseName parses a name (which is like an identifier, but doesn't have
// extra restrictions on the first character).
func (p *parser) parseName() (result string, err error) {
i := p.i
loop:
for i < len(p.s) {
c := p.s[i]
switch {
case nameChar(c):
start := i
for i < len(p.s) && nameChar(p.s[i]) {
i++
}
result += p.s[start:i]
case c == '\\':
p.i = i
val, err := p.parseEscape()
if err != nil {
return "", err
}
i = p.i
result += val
default:
break loop
}
}
if result == "" {
return "", errors.New("expected name, found EOF instead")
}
p.i = i
return result, nil
}
// parseString parses a single- or double-quoted string.
func (p *parser) parseString() (result string, err error) {
i := p.i
if len(p.s) < i+2 {
return "", errors.New("expected string, found EOF instead")
}
quote := p.s[i]
i++
loop:
for i < len(p.s) {
switch p.s[i] {
case '\\':
if len(p.s) > i+1 {
switch c := p.s[i+1]; c {
case '\r':
if len(p.s) > i+2 && p.s[i+2] == '\n' {
i += 3
continue loop
}
fallthrough
case '\n', '\f':
i += 2
continue loop
}
}
p.i = i
val, err := p.parseEscape()
if err != nil {
return "", err
}
i = p.i
result += val
case quote:
break loop
case '\r', '\n', '\f':
return "", errors.New("unexpected end of line in string")
default:
start := i
for i < len(p.s) {
if c := p.s[i]; c == quote || c == '\\' || c == '\r' || c == '\n' || c == '\f' {
break
}
i++
}
result += p.s[start:i]
}
}
if i >= len(p.s) {
return "", errors.New("EOF in string")
}
// Consume the final quote.
i++
p.i = i
return result, nil
}
// parseRegex parses a regular expression; the end is defined by encountering an
// unmatched closing ')' or ']' which is not consumed
func (p *parser) parseRegex() (rx *regexp.Regexp, err error) {
i := p.i
if len(p.s) < i+2 {
return nil, errors.New("expected regular expression, found EOF instead")
}
// number of open parens or brackets;
// when it becomes negative, finished parsing regex
open := 0
loop:
for i < len(p.s) {
switch p.s[i] {
case '(', '[':
open++
case ')', ']':
open--
if open < 0 {
break loop
}
}
i++
}
if i >= len(p.s) {
return nil, errors.New("EOF in regular expression")
}
rx, err = regexp.Compile(p.s[p.i:i])
p.i = i
return rx, err
}
// skipWhitespace consumes whitespace characters and comments.
// It returns true if there was actually anything to skip.
func (p *parser) skipWhitespace() bool {
i := p.i
for i < len(p.s) {
switch p.s[i] {
case ' ', '\t', '\r', '\n', '\f':
i++
continue
case '/':
if strings.HasPrefix(p.s[i:], "/*") {
end := strings.Index(p.s[i+len("/*"):], "*/")
if end != -1 {
i += end + len("/**/")
continue
}
}
}
break
}
if i > p.i {
p.i = i
return true
}
return false
}
// consumeParenthesis consumes an opening parenthesis and any following
// whitespace. It returns true if there was actually a parenthesis to skip.
func (p *parser) consumeParenthesis() bool {
if p.i < len(p.s) && p.s[p.i] == '(' {
p.i++
p.skipWhitespace()
return true
}
return false
}
// consumeClosingParenthesis consumes a closing parenthesis and any preceding
// whitespace. It returns true if there was actually a parenthesis to skip.
func (p *parser) consumeClosingParenthesis() bool {
i := p.i
p.skipWhitespace()
if p.i < len(p.s) && p.s[p.i] == ')' {
p.i++
return true
}
p.i = i
return false
}
// parseTypeSelector parses a type selector (one that matches by tag name).
func (p *parser) parseTypeSelector() (result Selector, err error) {
tag, err := p.parseIdentifier()
if err != nil {
return nil, err
}
return typeSelector(tag), nil
}
// parseIDSelector parses a selector that matches by id attribute.
func (p *parser) parseIDSelector() (Selector, error) {
if p.i >= len(p.s) {
return nil, fmt.Errorf("expected id selector (#id), found EOF instead")
}
if p.s[p.i] != '#' {
return nil, fmt.Errorf("expected id selector (#id), found '%c' instead", p.s[p.i])
}
p.i++
id, err := p.parseName()
if err != nil {
return nil, err
}
return attributeEqualsSelector("id", id), nil
}
// parseClassSelector parses a selector that matches by class attribute.
func (p *parser) parseClassSelector() (Selector, error) {
if p.i >= len(p.s) {
return nil, fmt.Errorf("expected class selector (.class), found EOF instead")
}
if p.s[p.i] != '.' {
return nil, fmt.Errorf("expected class selector (.class), found '%c' instead", p.s[p.i])
}
p.i++
class, err := p.parseIdentifier()
if err != nil {
return nil, err
}
return attributeIncludesSelector("class", class), nil
}
// parseAttributeSelector parses a selector that matches by attribute value.
func (p *parser) parseAttributeSelector() (Selector, error) {
if p.i >= len(p.s) {
return nil, fmt.Errorf("expected attribute selector ([attribute]), found EOF instead")
}
if p.s[p.i] != '[' {
return nil, fmt.Errorf("expected attribute selector ([attribute]), found '%c' instead", p.s[p.i])
}
p.i++
p.skipWhitespace()
key, err := p.parseIdentifier()
if err != nil {
return nil, err
}
p.skipWhitespace()
if p.i >= len(p.s) {
return nil, errors.New("unexpected EOF in attribute selector")
}
if p.s[p.i] == ']' {
p.i++
return attributeExistsSelector(key), nil
}
if p.i+2 >= len(p.s) {
return nil, errors.New("unexpected EOF in attribute selector")
}
op := p.s[p.i : p.i+2]
if op[0] == '=' {
op = "="
} else if op[1] != '=' {
return nil, fmt.Errorf(`expected equality operator, found "%s" instead`, op)
}
p.i += len(op)
p.skipWhitespace()
if p.i >= len(p.s) {
return nil, errors.New("unexpected EOF in attribute selector")
}
var val string
var rx *regexp.Regexp
if op == "#=" {
rx, err = p.parseRegex()
} else {
switch p.s[p.i] {
case '\'', '"':
val, err = p.parseString()
default:
val, err = p.parseIdentifier()
}
}
if err != nil {
return nil, err
}
p.skipWhitespace()
if p.i >= len(p.s) {
return nil, errors.New("unexpected EOF in attribute selector")
}
if p.s[p.i] != ']' {
return nil, fmt.Errorf("expected ']', found '%c' instead", p.s[p.i])
}
p.i++
switch op {
case "=":
return attributeEqualsSelector(key, val), nil
case "!=":
return attributeNotEqualSelector(key, val), nil
case "~=":
return attributeIncludesSelector(key, val), nil
case "|=":
return attributeDashmatchSelector(key, val), nil
case "^=":
return attributePrefixSelector(key, val), nil
case "$=":
return attributeSuffixSelector(key, val), nil
case "*=":
return attributeSubstringSelector(key, val), nil
case "#=":
return attributeRegexSelector(key, rx), nil
}
return nil, fmt.Errorf("attribute operator %q is not supported", op)
}
var errExpectedParenthesis = errors.New("expected '(' but didn't find it")
var errExpectedClosingParenthesis = errors.New("expected ')' but didn't find it")
var errUnmatchedParenthesis = errors.New("unmatched '('")
// parsePseudoclassSelector parses a pseudoclass selector like :not(p).
func (p *parser) parsePseudoclassSelector() (Selector, error) {
if p.i >= len(p.s) {
return nil, fmt.Errorf("expected pseudoclass selector (:pseudoclass), found EOF instead")
}
if p.s[p.i] != ':' {
return nil, fmt.Errorf("expected attribute selector (:pseudoclass), found '%c' instead", p.s[p.i])
}
p.i++
name, err := p.parseIdentifier()
if err != nil {
return nil, err
}
name = toLowerASCII(name)
switch name {
case "not", "has", "haschild":
if !p.consumeParenthesis() {
return nil, errExpectedParenthesis
}
sel, parseErr := p.parseSelectorGroup()
if parseErr != nil {
return nil, parseErr
}
if !p.consumeClosingParenthesis() {
return nil, errExpectedClosingParenthesis
}
switch name {
case "not":
return negatedSelector(sel), nil
case "has":
return hasDescendantSelector(sel), nil
case "haschild":
return hasChildSelector(sel), nil
}
case "contains", "containsown":
if !p.consumeParenthesis() {
return nil, errExpectedParenthesis
}
if p.i == len(p.s) {
return nil, errUnmatchedParenthesis
}
var val string
switch p.s[p.i] {
case '\'', '"':
val, err = p.parseString()
default:
val, err = p.parseIdentifier()
}
if err != nil {
return nil, err
}
val = strings.ToLower(val)
p.skipWhitespace()
if p.i >= len(p.s) {
return nil, errors.New("unexpected EOF in pseudo selector")
}
if !p.consumeClosingParenthesis() {
return nil, errExpectedClosingParenthesis
}
switch name {
case "contains":
return textSubstrSelector(val), nil
case "containsown":
return ownTextSubstrSelector(val), nil
}
case "matches", "matchesown":
if !p.consumeParenthesis() {
return nil, errExpectedParenthesis
}
rx, err := p.parseRegex()
if err != nil {
return nil, err
}
if p.i >= len(p.s) {
return nil, errors.New("unexpected EOF in pseudo selector")
}
if !p.consumeClosingParenthesis() {
return nil, errExpectedClosingParenthesis
}
switch name {
case "matches":
return textRegexSelector(rx), nil
case "matchesown":
return ownTextRegexSelector(rx), nil
}
case "nth-child", "nth-last-child", "nth-of-type", "nth-last-of-type":
if !p.consumeParenthesis() {
return nil, errExpectedParenthesis
}
a, b, err := p.parseNth()
if err != nil {
return nil, err
}
if !p.consumeClosingParenthesis() {
return nil, errExpectedClosingParenthesis
}
if a == 0 {
switch name {
case "nth-child":
return simpleNthChildSelector(b, false), nil
case "nth-of-type":
return simpleNthChildSelector(b, true), nil
case "nth-last-child":
return simpleNthLastChildSelector(b, false), nil
case "nth-last-of-type":
return simpleNthLastChildSelector(b, true), nil
}
}
return nthChildSelector(a, b,
name == "nth-last-child" || name == "nth-last-of-type",
name == "nth-of-type" || name == "nth-last-of-type"),
nil
case "first-child":
return simpleNthChildSelector(1, false), nil
case "last-child":
return simpleNthLastChildSelector(1, false), nil
case "first-of-type":
return simpleNthChildSelector(1, true), nil
case "last-of-type":
return simpleNthLastChildSelector(1, true), nil
case "only-child":
return onlyChildSelector(false), nil
case "only-of-type":
return onlyChildSelector(true), nil
case "input":
return inputSelector, nil
case "empty":
return emptyElementSelector, nil
case "root":
return rootSelector, nil
}
return nil, fmt.Errorf("unknown pseudoclass :%s", name)
}
// parseInteger parses a decimal integer.
func (p *parser) parseInteger() (int, error) {
i := p.i
start := i
for i < len(p.s) && '0' <= p.s[i] && p.s[i] <= '9' {
i++
}
if i == start {
return 0, errors.New("expected integer, but didn't find it")
}
p.i = i
val, err := strconv.Atoi(p.s[start:i])
if err != nil {
return 0, err
}
return val, nil
}
// parseNth parses the argument for :nth-child (normally of the form an+b).
func (p *parser) parseNth() (a, b int, err error) {
// initial state
if p.i >= len(p.s) {
goto eof
}
switch p.s[p.i] {
case '-':
p.i++
goto negativeA
case '+':
p.i++
goto positiveA
case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
goto positiveA
case 'n', 'N':
a = 1
p.i++
goto readN
case 'o', 'O', 'e', 'E':
id, nameErr := p.parseName()
if nameErr != nil {
return 0, 0, nameErr
}
id = toLowerASCII(id)
if id == "odd" {
return 2, 1, nil
}
if id == "even" {
return 2, 0, nil
}
return 0, 0, fmt.Errorf("expected 'odd' or 'even', but found '%s' instead", id)
default:
goto invalid
}
positiveA:
if p.i >= len(p.s) {
goto eof
}
switch p.s[p.i] {
case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
a, err = p.parseInteger()
if err != nil {
return 0, 0, err
}
goto readA
case 'n', 'N':
a = 1
p.i++
goto readN
default:
goto invalid
}
negativeA:
if p.i >= len(p.s) {
goto eof
}
switch p.s[p.i] {
case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
a, err = p.parseInteger()
if err != nil {
return 0, 0, err
}
a = -a
goto readA
case 'n', 'N':
a = -1
p.i++
goto readN
default:
goto invalid
}
readA:
if p.i >= len(p.s) {
goto eof
}
switch p.s[p.i] {
case 'n', 'N':
p.i++
goto readN
default:
// The number we read as a is actually b.
return 0, a, nil
}
readN:
p.skipWhitespace()
if p.i >= len(p.s) {
goto eof
}
switch p.s[p.i] {
case '+':
p.i++
p.skipWhitespace()
b, err = p.parseInteger()
if err != nil {
return 0, 0, err
}
return a, b, nil
case '-':
p.i++
p.skipWhitespace()
b, err = p.parseInteger()
if err != nil {
return 0, 0, err
}
return a, -b, nil
default:
return a, 0, nil
}
eof:
return 0, 0, errors.New("unexpected EOF while attempting to parse expression of form an+b")
invalid:
return 0, 0, errors.New("unexpected character while attempting to parse expression of form an+b")
}
// parseSimpleSelectorSequence parses a selector sequence that applies to
// a single element.
func (p *parser) parseSimpleSelectorSequence() (Selector, error) {
var result Selector
if p.i >= len(p.s) {
return nil, errors.New("expected selector, found EOF instead")
}
switch p.s[p.i] {
case '*':
// It's the universal selector. Just skip over it, since it doesn't affect the meaning.
p.i++
case '#', '.', '[', ':':
// There's no type selector. Wait to process the other till the main loop.
default:
r, err := p.parseTypeSelector()
if err != nil {
return nil, err
}
result = r
}
loop:
for p.i < len(p.s) {
var ns Selector
var err error
switch p.s[p.i] {
case '#':
ns, err = p.parseIDSelector()
case '.':
ns, err = p.parseClassSelector()
case '[':
ns, err = p.parseAttributeSelector()
case ':':
ns, err = p.parsePseudoclassSelector()
default:
break loop
}
if err != nil {
return nil, err
}
if result == nil {
result = ns
} else {
result = intersectionSelector(result, ns)
}
}
if result == nil {
result = func(n *html.Node) bool {
return n.Type == html.ElementNode
}
}
return result, nil
}
// parseSelector parses a selector that may include combinators.
func (p *parser) parseSelector() (result Selector, err error) {
p.skipWhitespace()
result, err = p.parseSimpleSelectorSequence()
if err != nil {
return
}
for {
var combinator byte
if p.skipWhitespace() {
combinator = ' '
}
if p.i >= len(p.s) {
return
}
switch p.s[p.i] {
case '+', '>', '~':
combinator = p.s[p.i]
p.i++
p.skipWhitespace()
case ',', ')':
// These characters can't begin a selector, but they can legally occur after one.
return
}
if combinator == 0 {
return
}
c, err := p.parseSimpleSelectorSequence()
if err != nil {
return nil, err
}
switch combinator {
case ' ':
result = descendantSelector(result, c)
case '>':
result = childSelector(result, c)
case '+':
result = siblingSelector(result, c, true)
case '~':
result = siblingSelector(result, c, false)
}
}
panic("unreachable")
}
// parseSelectorGroup parses a group of selectors, separated by commas.
func (p *parser) parseSelectorGroup() (result Selector, err error) {
result, err = p.parseSelector()
if err != nil {
return
}
for p.i < len(p.s) {
if p.s[p.i] != ',' {
return result, nil
}
p.i++
c, err := p.parseSelector()
if err != nil {
return nil, err
}
result = unionSelector(result, c)
}
return
}

622
vendor/github.com/andybalholm/cascadia/selector.go generated vendored Normal file
View File

@ -0,0 +1,622 @@
package cascadia
import (
"bytes"
"fmt"
"regexp"
"strings"
"golang.org/x/net/html"
)
// the Selector type, and functions for creating them
// A Selector is a function which tells whether a node matches or not.
type Selector func(*html.Node) bool
// hasChildMatch returns whether n has any child that matches a.
func hasChildMatch(n *html.Node, a Selector) bool {
for c := n.FirstChild; c != nil; c = c.NextSibling {
if a(c) {
return true
}
}
return false
}
// hasDescendantMatch performs a depth-first search of n's descendants,
// testing whether any of them match a. It returns true as soon as a match is
// found, or false if no match is found.
func hasDescendantMatch(n *html.Node, a Selector) bool {
for c := n.FirstChild; c != nil; c = c.NextSibling {
if a(c) || (c.Type == html.ElementNode && hasDescendantMatch(c, a)) {
return true
}
}
return false
}
// Compile parses a selector and returns, if successful, a Selector object
// that can be used to match against html.Node objects.
func Compile(sel string) (Selector, error) {
p := &parser{s: sel}
compiled, err := p.parseSelectorGroup()
if err != nil {
return nil, err
}
if p.i < len(sel) {
return nil, fmt.Errorf("parsing %q: %d bytes left over", sel, len(sel)-p.i)
}
return compiled, nil
}
// MustCompile is like Compile, but panics instead of returning an error.
func MustCompile(sel string) Selector {
compiled, err := Compile(sel)
if err != nil {
panic(err)
}
return compiled
}
// MatchAll returns a slice of the nodes that match the selector,
// from n and its children.
func (s Selector) MatchAll(n *html.Node) []*html.Node {
return s.matchAllInto(n, nil)
}
func (s Selector) matchAllInto(n *html.Node, storage []*html.Node) []*html.Node {
if s(n) {
storage = append(storage, n)
}
for child := n.FirstChild; child != nil; child = child.NextSibling {
storage = s.matchAllInto(child, storage)
}
return storage
}
// Match returns true if the node matches the selector.
func (s Selector) Match(n *html.Node) bool {
return s(n)
}
// MatchFirst returns the first node that matches s, from n and its children.
func (s Selector) MatchFirst(n *html.Node) *html.Node {
if s.Match(n) {
return n
}
for c := n.FirstChild; c != nil; c = c.NextSibling {
m := s.MatchFirst(c)
if m != nil {
return m
}
}
return nil
}
// Filter returns the nodes in nodes that match the selector.
func (s Selector) Filter(nodes []*html.Node) (result []*html.Node) {
for _, n := range nodes {
if s(n) {
result = append(result, n)
}
}
return result
}
// typeSelector returns a Selector that matches elements with a given tag name.
func typeSelector(tag string) Selector {
tag = toLowerASCII(tag)
return func(n *html.Node) bool {
return n.Type == html.ElementNode && n.Data == tag
}
}
// toLowerASCII returns s with all ASCII capital letters lowercased.
func toLowerASCII(s string) string {
var b []byte
for i := 0; i < len(s); i++ {
if c := s[i]; 'A' <= c && c <= 'Z' {
if b == nil {
b = make([]byte, len(s))
copy(b, s)
}
b[i] = s[i] + ('a' - 'A')
}
}
if b == nil {
return s
}
return string(b)
}
// attributeSelector returns a Selector that matches elements
// where the attribute named key satisifes the function f.
func attributeSelector(key string, f func(string) bool) Selector {
key = toLowerASCII(key)
return func(n *html.Node) bool {
if n.Type != html.ElementNode {
return false
}
for _, a := range n.Attr {
if a.Key == key && f(a.Val) {
return true
}
}
return false
}
}
// attributeExistsSelector returns a Selector that matches elements that have
// an attribute named key.
func attributeExistsSelector(key string) Selector {
return attributeSelector(key, func(string) bool { return true })
}
// attributeEqualsSelector returns a Selector that matches elements where
// the attribute named key has the value val.
func attributeEqualsSelector(key, val string) Selector {
return attributeSelector(key,
func(s string) bool {
return s == val
})
}
// attributeNotEqualSelector returns a Selector that matches elements where
// the attribute named key does not have the value val.
func attributeNotEqualSelector(key, val string) Selector {
key = toLowerASCII(key)
return func(n *html.Node) bool {
if n.Type != html.ElementNode {
return false
}
for _, a := range n.Attr {
if a.Key == key && a.Val == val {
return false
}
}
return true
}
}
// attributeIncludesSelector returns a Selector that matches elements where
// the attribute named key is a whitespace-separated list that includes val.
func attributeIncludesSelector(key, val string) Selector {
return attributeSelector(key,
func(s string) bool {
for s != "" {
i := strings.IndexAny(s, " \t\r\n\f")
if i == -1 {
return s == val
}
if s[:i] == val {
return true
}
s = s[i+1:]
}
return false
})
}
// attributeDashmatchSelector returns a Selector that matches elements where
// the attribute named key equals val or starts with val plus a hyphen.
func attributeDashmatchSelector(key, val string) Selector {
return attributeSelector(key,
func(s string) bool {
if s == val {
return true
}
if len(s) <= len(val) {
return false
}
if s[:len(val)] == val && s[len(val)] == '-' {
return true
}
return false
})
}
// attributePrefixSelector returns a Selector that matches elements where
// the attribute named key starts with val.
func attributePrefixSelector(key, val string) Selector {
return attributeSelector(key,
func(s string) bool {
if strings.TrimSpace(s) == "" {
return false
}
return strings.HasPrefix(s, val)
})
}
// attributeSuffixSelector returns a Selector that matches elements where
// the attribute named key ends with val.
func attributeSuffixSelector(key, val string) Selector {
return attributeSelector(key,
func(s string) bool {
if strings.TrimSpace(s) == "" {
return false
}
return strings.HasSuffix(s, val)
})
}
// attributeSubstringSelector returns a Selector that matches nodes where
// the attribute named key contains val.
func attributeSubstringSelector(key, val string) Selector {
return attributeSelector(key,
func(s string) bool {
if strings.TrimSpace(s) == "" {
return false
}
return strings.Contains(s, val)
})
}
// attributeRegexSelector returns a Selector that matches nodes where
// the attribute named key matches the regular expression rx
func attributeRegexSelector(key string, rx *regexp.Regexp) Selector {
return attributeSelector(key,
func(s string) bool {
return rx.MatchString(s)
})
}
// intersectionSelector returns a selector that matches nodes that match
// both a and b.
func intersectionSelector(a, b Selector) Selector {
return func(n *html.Node) bool {
return a(n) && b(n)
}
}
// unionSelector returns a selector that matches elements that match
// either a or b.
func unionSelector(a, b Selector) Selector {
return func(n *html.Node) bool {
return a(n) || b(n)
}
}
// negatedSelector returns a selector that matches elements that do not match a.
func negatedSelector(a Selector) Selector {
return func(n *html.Node) bool {
if n.Type != html.ElementNode {
return false
}
return !a(n)
}
}
// writeNodeText writes the text contained in n and its descendants to b.
func writeNodeText(n *html.Node, b *bytes.Buffer) {
switch n.Type {
case html.TextNode:
b.WriteString(n.Data)
case html.ElementNode:
for c := n.FirstChild; c != nil; c = c.NextSibling {
writeNodeText(c, b)
}
}
}
// nodeText returns the text contained in n and its descendants.
func nodeText(n *html.Node) string {
var b bytes.Buffer
writeNodeText(n, &b)
return b.String()
}
// nodeOwnText returns the contents of the text nodes that are direct
// children of n.
func nodeOwnText(n *html.Node) string {
var b bytes.Buffer
for c := n.FirstChild; c != nil; c = c.NextSibling {
if c.Type == html.TextNode {
b.WriteString(c.Data)
}
}
return b.String()
}
// textSubstrSelector returns a selector that matches nodes that
// contain the given text.
func textSubstrSelector(val string) Selector {
return func(n *html.Node) bool {
text := strings.ToLower(nodeText(n))
return strings.Contains(text, val)
}
}
// ownTextSubstrSelector returns a selector that matches nodes that
// directly contain the given text
func ownTextSubstrSelector(val string) Selector {
return func(n *html.Node) bool {
text := strings.ToLower(nodeOwnText(n))
return strings.Contains(text, val)
}
}
// textRegexSelector returns a selector that matches nodes whose text matches
// the specified regular expression
func textRegexSelector(rx *regexp.Regexp) Selector {
return func(n *html.Node) bool {
return rx.MatchString(nodeText(n))
}
}
// ownTextRegexSelector returns a selector that matches nodes whose text
// directly matches the specified regular expression
func ownTextRegexSelector(rx *regexp.Regexp) Selector {
return func(n *html.Node) bool {
return rx.MatchString(nodeOwnText(n))
}
}
// hasChildSelector returns a selector that matches elements
// with a child that matches a.
func hasChildSelector(a Selector) Selector {
return func(n *html.Node) bool {
if n.Type != html.ElementNode {
return false
}
return hasChildMatch(n, a)
}
}
// hasDescendantSelector returns a selector that matches elements
// with any descendant that matches a.
func hasDescendantSelector(a Selector) Selector {
return func(n *html.Node) bool {
if n.Type != html.ElementNode {
return false
}
return hasDescendantMatch(n, a)
}
}
// nthChildSelector returns a selector that implements :nth-child(an+b).
// If last is true, implements :nth-last-child instead.
// If ofType is true, implements :nth-of-type instead.
func nthChildSelector(a, b int, last, ofType bool) Selector {
return func(n *html.Node) bool {
if n.Type != html.ElementNode {
return false
}
parent := n.Parent
if parent == nil {
return false
}
if parent.Type == html.DocumentNode {
return false
}
i := -1
count := 0
for c := parent.FirstChild; c != nil; c = c.NextSibling {
if (c.Type != html.ElementNode) || (ofType && c.Data != n.Data) {
continue
}
count++
if c == n {
i = count
if !last {
break
}
}
}
if i == -1 {
// This shouldn't happen, since n should always be one of its parent's children.
return false
}
if last {
i = count - i + 1
}
i -= b
if a == 0 {
return i == 0
}
return i%a == 0 && i/a >= 0
}
}
// simpleNthChildSelector returns a selector that implements :nth-child(b).
// If ofType is true, implements :nth-of-type instead.
func simpleNthChildSelector(b int, ofType bool) Selector {
return func(n *html.Node) bool {
if n.Type != html.ElementNode {
return false
}
parent := n.Parent
if parent == nil {
return false
}
if parent.Type == html.DocumentNode {
return false
}
count := 0
for c := parent.FirstChild; c != nil; c = c.NextSibling {
if c.Type != html.ElementNode || (ofType && c.Data != n.Data) {
continue
}
count++
if c == n {
return count == b
}
if count >= b {
return false
}
}
return false
}
}
// simpleNthLastChildSelector returns a selector that implements
// :nth-last-child(b). If ofType is true, implements :nth-last-of-type
// instead.
func simpleNthLastChildSelector(b int, ofType bool) Selector {
return func(n *html.Node) bool {
if n.Type != html.ElementNode {
return false
}
parent := n.Parent
if parent == nil {
return false
}
if parent.Type == html.DocumentNode {
return false
}
count := 0
for c := parent.LastChild; c != nil; c = c.PrevSibling {
if c.Type != html.ElementNode || (ofType && c.Data != n.Data) {
continue
}
count++
if c == n {
return count == b
}
if count >= b {
return false
}
}
return false
}
}
// onlyChildSelector returns a selector that implements :only-child.
// If ofType is true, it implements :only-of-type instead.
func onlyChildSelector(ofType bool) Selector {
return func(n *html.Node) bool {
if n.Type != html.ElementNode {
return false
}
parent := n.Parent
if parent == nil {
return false
}
if parent.Type == html.DocumentNode {
return false
}
count := 0
for c := parent.FirstChild; c != nil; c = c.NextSibling {
if (c.Type != html.ElementNode) || (ofType && c.Data != n.Data) {
continue
}
count++
if count > 1 {
return false
}
}
return count == 1
}
}
// inputSelector is a Selector that matches input, select, textarea and button elements.
func inputSelector(n *html.Node) bool {
return n.Type == html.ElementNode && (n.Data == "input" || n.Data == "select" || n.Data == "textarea" || n.Data == "button")
}
// emptyElementSelector is a Selector that matches empty elements.
func emptyElementSelector(n *html.Node) bool {
if n.Type != html.ElementNode {
return false
}
for c := n.FirstChild; c != nil; c = c.NextSibling {
switch c.Type {
case html.ElementNode, html.TextNode:
return false
}
}
return true
}
// descendantSelector returns a Selector that matches an element if
// it matches d and has an ancestor that matches a.
func descendantSelector(a, d Selector) Selector {
return func(n *html.Node) bool {
if !d(n) {
return false
}
for p := n.Parent; p != nil; p = p.Parent {
if a(p) {
return true
}
}
return false
}
}
// childSelector returns a Selector that matches an element if
// it matches d and its parent matches a.
func childSelector(a, d Selector) Selector {
return func(n *html.Node) bool {
return d(n) && n.Parent != nil && a(n.Parent)
}
}
// siblingSelector returns a Selector that matches an element
// if it matches s2 and in is preceded by an element that matches s1.
// If adjacent is true, the sibling must be immediately before the element.
func siblingSelector(s1, s2 Selector, adjacent bool) Selector {
return func(n *html.Node) bool {
if !s2(n) {
return false
}
if adjacent {
for n = n.PrevSibling; n != nil; n = n.PrevSibling {
if n.Type == html.TextNode || n.Type == html.CommentNode {
continue
}
return s1(n)
}
return false
}
// Walk backwards looking for element that matches s1
for c := n.PrevSibling; c != nil; c = c.PrevSibling {
if s1(c) {
return true
}
}
return false
}
}
// rootSelector implements :root
func rootSelector(n *html.Node) bool {
if n.Type != html.ElementNode {
return false
}
if n.Parent == nil {
return false
}
return n.Parent.Type == html.DocumentNode
}

View File

@ -0,0 +1,57 @@
package s3err
import (
"fmt"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/aws/request"
)
// RequestFailure provides additional S3 specific metadata for the request
// failure.
type RequestFailure struct {
awserr.RequestFailure
hostID string
}
// NewRequestFailure returns a request failure error decordated with S3
// specific metadata.
func NewRequestFailure(err awserr.RequestFailure, hostID string) *RequestFailure {
return &RequestFailure{RequestFailure: err, hostID: hostID}
}
func (r RequestFailure) Error() string {
extra := fmt.Sprintf("status code: %d, request id: %s, host id: %s",
r.StatusCode(), r.RequestID(), r.hostID)
return awserr.SprintError(r.Code(), r.Message(), extra, r.OrigErr())
}
func (r RequestFailure) String() string {
return r.Error()
}
// HostID returns the HostID request response value.
func (r RequestFailure) HostID() string {
return r.hostID
}
// RequestFailureWrapperHandler returns a handler to rap an
// awserr.RequestFailure with the S3 request ID 2 from the response.
func RequestFailureWrapperHandler() request.NamedHandler {
return request.NamedHandler{
Name: "awssdk.s3.errorHandler",
Fn: func(req *request.Request) {
reqErr, ok := req.Error.(awserr.RequestFailure)
if !ok || reqErr == nil {
return
}
hostID := req.HTTPResponse.Header.Get("X-Amz-Id-2")
if req.Error == nil {
return
}
req.Error = NewRequestFailure(reqErr, hostID)
},
}
}

View File

@ -0,0 +1,144 @@
package eventstream
import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"strconv"
)
type decodedMessage struct {
rawMessage
Headers decodedHeaders `json:"headers"`
}
type jsonMessage struct {
Length json.Number `json:"total_length"`
HeadersLen json.Number `json:"headers_length"`
PreludeCRC json.Number `json:"prelude_crc"`
Headers decodedHeaders `json:"headers"`
Payload []byte `json:"payload"`
CRC json.Number `json:"message_crc"`
}
func (d *decodedMessage) UnmarshalJSON(b []byte) (err error) {
var jsonMsg jsonMessage
if err = json.Unmarshal(b, &jsonMsg); err != nil {
return err
}
d.Length, err = numAsUint32(jsonMsg.Length)
if err != nil {
return err
}
d.HeadersLen, err = numAsUint32(jsonMsg.HeadersLen)
if err != nil {
return err
}
d.PreludeCRC, err = numAsUint32(jsonMsg.PreludeCRC)
if err != nil {
return err
}
d.Headers = jsonMsg.Headers
d.Payload = jsonMsg.Payload
d.CRC, err = numAsUint32(jsonMsg.CRC)
if err != nil {
return err
}
return nil
}
func (d *decodedMessage) MarshalJSON() ([]byte, error) {
jsonMsg := jsonMessage{
Length: json.Number(strconv.Itoa(int(d.Length))),
HeadersLen: json.Number(strconv.Itoa(int(d.HeadersLen))),
PreludeCRC: json.Number(strconv.Itoa(int(d.PreludeCRC))),
Headers: d.Headers,
Payload: d.Payload,
CRC: json.Number(strconv.Itoa(int(d.CRC))),
}
return json.Marshal(jsonMsg)
}
func numAsUint32(n json.Number) (uint32, error) {
v, err := n.Int64()
if err != nil {
return 0, fmt.Errorf("failed to get int64 json number, %v", err)
}
return uint32(v), nil
}
func (d decodedMessage) Message() Message {
return Message{
Headers: Headers(d.Headers),
Payload: d.Payload,
}
}
type decodedHeaders Headers
func (hs *decodedHeaders) UnmarshalJSON(b []byte) error {
var jsonHeaders []struct {
Name string `json:"name"`
Type valueType `json:"type"`
Value interface{} `json:"value"`
}
decoder := json.NewDecoder(bytes.NewReader(b))
decoder.UseNumber()
if err := decoder.Decode(&jsonHeaders); err != nil {
return err
}
var headers Headers
for _, h := range jsonHeaders {
value, err := valueFromType(h.Type, h.Value)
if err != nil {
return err
}
headers.Set(h.Name, value)
}
(*hs) = decodedHeaders(headers)
return nil
}
func valueFromType(typ valueType, val interface{}) (Value, error) {
switch typ {
case trueValueType:
return BoolValue(true), nil
case falseValueType:
return BoolValue(false), nil
case int8ValueType:
v, err := val.(json.Number).Int64()
return Int8Value(int8(v)), err
case int16ValueType:
v, err := val.(json.Number).Int64()
return Int16Value(int16(v)), err
case int32ValueType:
v, err := val.(json.Number).Int64()
return Int32Value(int32(v)), err
case int64ValueType:
v, err := val.(json.Number).Int64()
return Int64Value(v), err
case bytesValueType:
v, err := base64.StdEncoding.DecodeString(val.(string))
return BytesValue(v), err
case stringValueType:
v, err := base64.StdEncoding.DecodeString(val.(string))
return StringValue(string(v)), err
case timestampValueType:
v, err := val.(json.Number).Int64()
return TimestampValue(timeFromEpochMilli(v)), err
case uuidValueType:
v, err := base64.StdEncoding.DecodeString(val.(string))
var tv UUIDValue
copy(tv[:], v)
return tv, err
default:
panic(fmt.Sprintf("unknown type, %s, %T", typ.String(), val))
}
}

View File

@ -0,0 +1,199 @@
package eventstream
import (
"bytes"
"encoding/binary"
"encoding/hex"
"encoding/json"
"fmt"
"hash"
"hash/crc32"
"io"
"github.com/aws/aws-sdk-go/aws"
)
// Decoder provides decoding of an Event Stream messages.
type Decoder struct {
r io.Reader
logger aws.Logger
}
// NewDecoder initializes and returns a Decoder for decoding event
// stream messages from the reader provided.
func NewDecoder(r io.Reader) *Decoder {
return &Decoder{
r: r,
}
}
// Decode attempts to decode a single message from the event stream reader.
// Will return the event stream message, or error if Decode fails to read
// the message from the stream.
func (d *Decoder) Decode(payloadBuf []byte) (m Message, err error) {
reader := d.r
if d.logger != nil {
debugMsgBuf := bytes.NewBuffer(nil)
reader = io.TeeReader(reader, debugMsgBuf)
defer func() {
logMessageDecode(d.logger, debugMsgBuf, m, err)
}()
}
crc := crc32.New(crc32IEEETable)
hashReader := io.TeeReader(reader, crc)
prelude, err := decodePrelude(hashReader, crc)
if err != nil {
return Message{}, err
}
if prelude.HeadersLen > 0 {
lr := io.LimitReader(hashReader, int64(prelude.HeadersLen))
m.Headers, err = decodeHeaders(lr)
if err != nil {
return Message{}, err
}
}
if payloadLen := prelude.PayloadLen(); payloadLen > 0 {
buf, err := decodePayload(payloadBuf, io.LimitReader(hashReader, int64(payloadLen)))
if err != nil {
return Message{}, err
}
m.Payload = buf
}
msgCRC := crc.Sum32()
if err := validateCRC(reader, msgCRC); err != nil {
return Message{}, err
}
return m, nil
}
// UseLogger specifies the Logger that that the decoder should use to log the
// message decode to.
func (d *Decoder) UseLogger(logger aws.Logger) {
d.logger = logger
}
func logMessageDecode(logger aws.Logger, msgBuf *bytes.Buffer, msg Message, decodeErr error) {
w := bytes.NewBuffer(nil)
defer func() { logger.Log(w.String()) }()
fmt.Fprintf(w, "Raw message:\n%s\n",
hex.Dump(msgBuf.Bytes()))
if decodeErr != nil {
fmt.Fprintf(w, "Decode error: %v\n", decodeErr)
return
}
rawMsg, err := msg.rawMessage()
if err != nil {
fmt.Fprintf(w, "failed to create raw message, %v\n", err)
return
}
decodedMsg := decodedMessage{
rawMessage: rawMsg,
Headers: decodedHeaders(msg.Headers),
}
fmt.Fprintf(w, "Decoded message:\n")
encoder := json.NewEncoder(w)
if err := encoder.Encode(decodedMsg); err != nil {
fmt.Fprintf(w, "failed to generate decoded message, %v\n", err)
}
}
func decodePrelude(r io.Reader, crc hash.Hash32) (messagePrelude, error) {
var p messagePrelude
var err error
p.Length, err = decodeUint32(r)
if err != nil {
return messagePrelude{}, err
}
p.HeadersLen, err = decodeUint32(r)
if err != nil {
return messagePrelude{}, err
}
if err := p.ValidateLens(); err != nil {
return messagePrelude{}, err
}
preludeCRC := crc.Sum32()
if err := validateCRC(r, preludeCRC); err != nil {
return messagePrelude{}, err
}
p.PreludeCRC = preludeCRC
return p, nil
}
func decodePayload(buf []byte, r io.Reader) ([]byte, error) {
w := bytes.NewBuffer(buf[0:0])
_, err := io.Copy(w, r)
return w.Bytes(), err
}
func decodeUint8(r io.Reader) (uint8, error) {
type byteReader interface {
ReadByte() (byte, error)
}
if br, ok := r.(byteReader); ok {
v, err := br.ReadByte()
return uint8(v), err
}
var b [1]byte
_, err := io.ReadFull(r, b[:])
return uint8(b[0]), err
}
func decodeUint16(r io.Reader) (uint16, error) {
var b [2]byte
bs := b[:]
_, err := io.ReadFull(r, bs)
if err != nil {
return 0, err
}
return binary.BigEndian.Uint16(bs), nil
}
func decodeUint32(r io.Reader) (uint32, error) {
var b [4]byte
bs := b[:]
_, err := io.ReadFull(r, bs)
if err != nil {
return 0, err
}
return binary.BigEndian.Uint32(bs), nil
}
func decodeUint64(r io.Reader) (uint64, error) {
var b [8]byte
bs := b[:]
_, err := io.ReadFull(r, bs)
if err != nil {
return 0, err
}
return binary.BigEndian.Uint64(bs), nil
}
func validateCRC(r io.Reader, expect uint32) error {
msgCRC, err := decodeUint32(r)
if err != nil {
return err
}
if msgCRC != expect {
return ChecksumError{}
}
return nil
}

View File

@ -0,0 +1,114 @@
package eventstream
import (
"bytes"
"encoding/binary"
"hash"
"hash/crc32"
"io"
)
// Encoder provides EventStream message encoding.
type Encoder struct {
w io.Writer
headersBuf *bytes.Buffer
}
// NewEncoder initializes and returns an Encoder to encode Event Stream
// messages to an io.Writer.
func NewEncoder(w io.Writer) *Encoder {
return &Encoder{
w: w,
headersBuf: bytes.NewBuffer(nil),
}
}
// Encode encodes a single EventStream message to the io.Writer the Encoder
// was created with. An error is returned if writing the message fails.
func (e *Encoder) Encode(msg Message) error {
e.headersBuf.Reset()
err := encodeHeaders(e.headersBuf, msg.Headers)
if err != nil {
return err
}
crc := crc32.New(crc32IEEETable)
hashWriter := io.MultiWriter(e.w, crc)
headersLen := uint32(e.headersBuf.Len())
payloadLen := uint32(len(msg.Payload))
if err := encodePrelude(hashWriter, crc, headersLen, payloadLen); err != nil {
return err
}
if headersLen > 0 {
if _, err := io.Copy(hashWriter, e.headersBuf); err != nil {
return err
}
}
if payloadLen > 0 {
if _, err := hashWriter.Write(msg.Payload); err != nil {
return err
}
}
msgCRC := crc.Sum32()
return binary.Write(e.w, binary.BigEndian, msgCRC)
}
func encodePrelude(w io.Writer, crc hash.Hash32, headersLen, payloadLen uint32) error {
p := messagePrelude{
Length: minMsgLen + headersLen + payloadLen,
HeadersLen: headersLen,
}
if err := p.ValidateLens(); err != nil {
return err
}
err := binaryWriteFields(w, binary.BigEndian,
p.Length,
p.HeadersLen,
)
if err != nil {
return err
}
p.PreludeCRC = crc.Sum32()
err = binary.Write(w, binary.BigEndian, p.PreludeCRC)
if err != nil {
return err
}
return nil
}
func encodeHeaders(w io.Writer, headers Headers) error {
for _, h := range headers {
hn := headerName{
Len: uint8(len(h.Name)),
}
copy(hn.Name[:hn.Len], h.Name)
if err := hn.encode(w); err != nil {
return err
}
if err := h.Value.encode(w); err != nil {
return err
}
}
return nil
}
func binaryWriteFields(w io.Writer, order binary.ByteOrder, vs ...interface{}) error {
for _, v := range vs {
if err := binary.Write(w, order, v); err != nil {
return err
}
}
return nil
}

View File

@ -0,0 +1,23 @@
package eventstream
import "fmt"
// LengthError provides the error for items being larger than a maximum length.
type LengthError struct {
Part string
Want int
Have int
Value interface{}
}
func (e LengthError) Error() string {
return fmt.Sprintf("%s length invalid, %d/%d, %v",
e.Part, e.Want, e.Have, e.Value)
}
// ChecksumError provides the error for message checksum invalidation errors.
type ChecksumError struct{}
func (e ChecksumError) Error() string {
return "message checksum mismatch"
}

View File

@ -0,0 +1,196 @@
package eventstreamapi
import (
"fmt"
"io"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/private/protocol"
"github.com/aws/aws-sdk-go/private/protocol/eventstream"
)
// Unmarshaler provides the interface for unmarshaling a EventStream
// message into a SDK type.
type Unmarshaler interface {
UnmarshalEvent(protocol.PayloadUnmarshaler, eventstream.Message) error
}
// EventStream headers with specific meaning to async API functionality.
const (
MessageTypeHeader = `:message-type` // Identifies type of message.
EventMessageType = `event`
ErrorMessageType = `error`
ExceptionMessageType = `exception`
// Message Events
EventTypeHeader = `:event-type` // Identifies message event type e.g. "Stats".
// Message Error
ErrorCodeHeader = `:error-code`
ErrorMessageHeader = `:error-message`
// Message Exception
ExceptionTypeHeader = `:exception-type`
)
// EventReader provides reading from the EventStream of an reader.
type EventReader struct {
reader io.ReadCloser
decoder *eventstream.Decoder
unmarshalerForEventType func(string) (Unmarshaler, error)
payloadUnmarshaler protocol.PayloadUnmarshaler
payloadBuf []byte
}
// NewEventReader returns a EventReader built from the reader and unmarshaler
// provided. Use ReadStream method to start reading from the EventStream.
func NewEventReader(
reader io.ReadCloser,
payloadUnmarshaler protocol.PayloadUnmarshaler,
unmarshalerForEventType func(string) (Unmarshaler, error),
) *EventReader {
return &EventReader{
reader: reader,
decoder: eventstream.NewDecoder(reader),
payloadUnmarshaler: payloadUnmarshaler,
unmarshalerForEventType: unmarshalerForEventType,
payloadBuf: make([]byte, 10*1024),
}
}
// UseLogger instructs the EventReader to use the logger and log level
// specified.
func (r *EventReader) UseLogger(logger aws.Logger, logLevel aws.LogLevelType) {
if logger != nil && logLevel.Matches(aws.LogDebugWithEventStreamBody) {
r.decoder.UseLogger(logger)
}
}
// ReadEvent attempts to read a message from the EventStream and return the
// unmarshaled event value that the message is for.
//
// For EventStream API errors check if the returned error satisfies the
// awserr.Error interface to get the error's Code and Message components.
//
// EventUnmarshalers called with EventStream messages must take copies of the
// message's Payload. The payload will is reused between events read.
func (r *EventReader) ReadEvent() (event interface{}, err error) {
msg, err := r.decoder.Decode(r.payloadBuf)
if err != nil {
return nil, err
}
defer func() {
// Reclaim payload buffer for next message read.
r.payloadBuf = msg.Payload[0:0]
}()
typ, err := GetHeaderString(msg, MessageTypeHeader)
if err != nil {
return nil, err
}
switch typ {
case EventMessageType:
return r.unmarshalEventMessage(msg)
case ExceptionMessageType:
err = r.unmarshalEventException(msg)
return nil, err
case ErrorMessageType:
return nil, r.unmarshalErrorMessage(msg)
default:
return nil, fmt.Errorf("unknown eventstream message type, %v", typ)
}
}
func (r *EventReader) unmarshalEventMessage(
msg eventstream.Message,
) (event interface{}, err error) {
eventType, err := GetHeaderString(msg, EventTypeHeader)
if err != nil {
return nil, err
}
ev, err := r.unmarshalerForEventType(eventType)
if err != nil {
return nil, err
}
err = ev.UnmarshalEvent(r.payloadUnmarshaler, msg)
if err != nil {
return nil, err
}
return ev, nil
}
func (r *EventReader) unmarshalEventException(
msg eventstream.Message,
) (err error) {
eventType, err := GetHeaderString(msg, ExceptionTypeHeader)
if err != nil {
return err
}
ev, err := r.unmarshalerForEventType(eventType)
if err != nil {
return err
}
err = ev.UnmarshalEvent(r.payloadUnmarshaler, msg)
if err != nil {
return err
}
var ok bool
err, ok = ev.(error)
if !ok {
err = messageError{
code: "SerializationError",
msg: fmt.Sprintf(
"event stream exception %s mapped to non-error %T, %v",
eventType, ev, ev,
),
}
}
return err
}
func (r *EventReader) unmarshalErrorMessage(msg eventstream.Message) (err error) {
var msgErr messageError
msgErr.code, err = GetHeaderString(msg, ErrorCodeHeader)
if err != nil {
return err
}
msgErr.msg, err = GetHeaderString(msg, ErrorMessageHeader)
if err != nil {
return err
}
return msgErr
}
// Close closes the EventReader's EventStream reader.
func (r *EventReader) Close() error {
return r.reader.Close()
}
// GetHeaderString returns the value of the header as a string. If the header
// is not set or the value is not a string an error will be returned.
func GetHeaderString(msg eventstream.Message, headerName string) (string, error) {
headerVal := msg.Headers.Get(headerName)
if headerVal == nil {
return "", fmt.Errorf("error header %s not present", headerName)
}
v, ok := headerVal.Get().(string)
if !ok {
return "", fmt.Errorf("error header value is not a string, %T", headerVal)
}
return v, nil
}

Some files were not shown because too many files have changed in this diff Show More