├── .gitignore ├── .dockerignore ├── README.md ├── manifests ├── ingress.yaml ├── whoami.yaml └── ingress-controller.yaml ├── go.mod ├── server ├── event.go ├── config.go ├── server.go ├── server_test.go ├── route_test.go └── route.go ├── Dockerfile ├── LICENSE ├── main.go ├── watcher └── watcher.go └── go.sum /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git/ 2 | Dockerfile 3 | README.md 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `kubernetes-simple-ingress-controller` 2 | A simple [ingress controller](https://kubernetes.io/docs/concepts/services-networking/ingress-controllers/) for Kubernetes. 3 | -------------------------------------------------------------------------------- /manifests/ingress.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions/v1beta1 2 | kind: Ingress 3 | metadata: 4 | name: whoami 5 | spec: 6 | rules: 7 | - host: who.qikqiak.com 8 | http: 9 | paths: 10 | - path: / 11 | backend: 12 | serviceName: whoami 13 | servicePort: 80 14 | -------------------------------------------------------------------------------- /manifests/whoami.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: whoami 5 | labels: 6 | app: whoami 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: whoami 12 | template: 13 | metadata: 14 | labels: 15 | app: whoami 16 | spec: 17 | containers: 18 | - name: whoami 19 | image: cnych/whoami 20 | ports: 21 | - containerPort: 80 22 | 23 | --- 24 | kind: Service 25 | apiVersion: v1 26 | metadata: 27 | name: whoami 28 | spec: 29 | selector: 30 | app: whoami 31 | ports: 32 | - protocol: TCP 33 | port: 80 34 | targetPort: 80 -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/cnych/kubernetes-simple-ingress-controller 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/bep/debounce v1.2.0 7 | github.com/imdario/mergo v0.3.7 // indirect 8 | github.com/rs/zerolog v1.15.0 9 | github.com/stretchr/testify v1.3.0 10 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 // indirect 11 | golang.org/x/sync v0.0.0-20190423024810-112230192c58 12 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 // indirect 13 | k8s.io/api v0.0.0-20190826194732-9f642ccb7a30 14 | k8s.io/apimachinery v0.0.0-20190826114657-e31a5531b558 15 | k8s.io/client-go v0.0.0-20190819141724-e14f31a72a77 16 | k8s.io/utils v0.0.0-20190809000727-6c36bc71fc4a // indirect 17 | ) 18 | -------------------------------------------------------------------------------- /server/event.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | ) 7 | 8 | // An Event is used to communicate that something has happened. 9 | type Event struct { 10 | once sync.Once 11 | C chan struct{} 12 | } 13 | 14 | // NewEvent creates a new Event. 15 | func NewEvent() *Event { 16 | return &Event{ 17 | C: make(chan struct{}), 18 | } 19 | } 20 | 21 | // Set sets the event by closing the C channel. After the first time, calls to set are a no-op. 22 | func (e *Event) Set() { 23 | e.once.Do(func() { 24 | close(e.C) 25 | }) 26 | } 27 | 28 | // Wait waits for the event to get set. 29 | func (e *Event) Wait(ctx context.Context) { 30 | select { 31 | case <-ctx.Done(): 32 | case <-e.C: 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.13-alpine 2 | 3 | RUN apk add --update \ 4 | ca-certificates \ 5 | && rm -rf /var/cache/apk/* 6 | 7 | RUN echo "nobody:x:65534:65534:Nobody:/:" > /etc_passwd 8 | 9 | ENV GO111MODULE=on 10 | ENV CGO_ENABLED=0 11 | ENV GOPROXY="https://goproxy.io" 12 | 13 | WORKDIR /go/src/github.com/cnych/kubernetes-simple-ingress-controller 14 | COPY go.mod go.sum ./ 15 | RUN go mod download 16 | 17 | COPY . ./ 18 | RUN go install -ldflags='-d -s -w' -tags netgo -installsuffix netgo -v ./... 19 | 20 | FROM scratch 21 | 22 | COPY --from=0 /go/bin/kubernetes-simple-ingress-controller /bin/kubernetes-simple-ingress-controller 23 | COPY --from=0 /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt 24 | COPY --from=0 /etc_passwd /etc/passwd 25 | 26 | CMD ["/bin/kubernetes-simple-ingress-controller"] 27 | -------------------------------------------------------------------------------- /server/config.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | type config struct { 4 | host string 5 | port int 6 | tlsPort int 7 | } 8 | 9 | func defaultConfig() *config { 10 | return &config{ 11 | host: "0.0.0.0", 12 | port: 80, 13 | tlsPort: 443, 14 | } 15 | } 16 | 17 | // An Option modifies the config. 18 | type Option func(*config) 19 | 20 | // WithHost sets the host to bind in the config. 21 | func WithHost(host string) Option { 22 | return func(cfg *config) { 23 | cfg.host = host 24 | } 25 | } 26 | 27 | // WithPort sets the port in the config. 28 | func WithPort(port int) Option { 29 | return func(cfg *config) { 30 | cfg.port = port 31 | } 32 | } 33 | 34 | // WithTLSPort sets the TLS port in the config. 35 | func WithTLSPort(port int) Option { 36 | return func(cfg *config) { 37 | cfg.tlsPort = port 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Caleb Doxsey 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /manifests/ingress-controller.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: k8s-simple-ingress-controller 6 | namespace: default 7 | 8 | --- 9 | kind: ClusterRole 10 | apiVersion: rbac.authorization.k8s.io/v1beta1 11 | metadata: 12 | name: k8s-simple-ingress-controller 13 | rules: 14 | - apiGroups: 15 | - "" 16 | resources: 17 | - services 18 | - endpoints 19 | - secrets 20 | verbs: 21 | - get 22 | - list 23 | - watch 24 | - apiGroups: 25 | - extensions 26 | resources: 27 | - ingresses 28 | verbs: 29 | - get 30 | - list 31 | - watch 32 | 33 | --- 34 | kind: ClusterRoleBinding 35 | apiVersion: rbac.authorization.k8s.io/v1beta1 36 | metadata: 37 | name: k8s-simple-ingress-controller 38 | roleRef: 39 | apiGroup: rbac.authorization.k8s.io 40 | kind: ClusterRole 41 | name: k8s-simple-ingress-controller 42 | subjects: 43 | - kind: ServiceAccount 44 | name: k8s-simple-ingress-controller 45 | namespace: default 46 | 47 | --- 48 | apiVersion: extensions/v1beta1 49 | kind: DaemonSet 50 | metadata: 51 | name: k8s-simple-ingress-controller 52 | labels: 53 | app: ingress-controller 54 | spec: 55 | selector: 56 | matchLabels: 57 | app: ingress-controller 58 | template: 59 | metadata: 60 | labels: 61 | app: ingress-controller 62 | spec: 63 | hostNetwork: true 64 | dnsPolicy: ClusterFirstWithHostNet 65 | serviceAccountName: k8s-simple-ingress-controller 66 | containers: 67 | - name: k8s-simple-ingress-controller 68 | image: cnych/k8s-simple-ingress-controller:v0.1 69 | ports: 70 | - name: http 71 | containerPort: 80 72 | - name: https 73 | containerPort: 443 74 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "github.com/cnych/kubernetes-simple-ingress-controller/server" 7 | "github.com/cnych/kubernetes-simple-ingress-controller/watcher" 8 | "github.com/rs/zerolog" 9 | "github.com/rs/zerolog/log" 10 | "golang.org/x/sync/errgroup" 11 | "k8s.io/apimachinery/pkg/util/runtime" 12 | "k8s.io/client-go/kubernetes" 13 | "k8s.io/client-go/rest" 14 | "os" 15 | ) 16 | 17 | var ( 18 | host string 19 | port, tlsPort int 20 | ) 21 | 22 | func main() { 23 | flag.StringVar(&host, "host", "0.0.0.0", "the host to bind") 24 | flag.IntVar(&port, "port", 80, "the insecure http port") 25 | flag.IntVar(&tlsPort, "tls-port", 443, "the secure https port") 26 | flag.Parse() 27 | 28 | log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) 29 | 30 | // ErrorHandlers 是一个函数列表,当发生一些错误时,会调用这些函数。 31 | runtime.ErrorHandlers = []func(error) { 32 | func(err error) { 33 | log.Warn().Err(err).Msg("[k8s]") 34 | }, 35 | } 36 | 37 | // 从集群内的token和ca.crt获取 Config 38 | config, err := rest.InClusterConfig() 39 | // 由于我们要通过集群内部的 Service 进行服务的访问,所以不能在集群外部使用,所以不能使用 kubeconfig 的方式来获取 Config 40 | if err != nil { 41 | log.Fatal().Err(err).Msg("get kubernetes configuration failed") 42 | } 43 | 44 | // 从 Config 中创建一个新的 Clientset 45 | client, err := kubernetes.NewForConfig(config) 46 | if err != nil { 47 | log.Fatal().Err(err).Msg("create kubernetes client failed") 48 | } 49 | 50 | s := server.New(server.WithHost(host), server.WithPort(port), server.WithTLSPort(tlsPort)) 51 | w := watcher.New(client, func(payload *watcher.Payload) { 52 | s.Update(payload) 53 | }) 54 | 55 | var eg errgroup.Group 56 | eg.Go(func() error { 57 | return s.Run(context.TODO()) 58 | }) 59 | eg.Go(func() error { 60 | return w.Run(context.TODO()) 61 | }) 62 | if err := eg.Wait(); err != nil { 63 | log.Fatal().Err(err).Send() 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "fmt" 7 | stdlog "log" 8 | "net/http" 9 | "net/http/httputil" 10 | "sync/atomic" 11 | 12 | "github.com/cnych/kubernetes-simple-ingress-controller/watcher" 13 | "github.com/rs/zerolog/log" 14 | "golang.org/x/sync/errgroup" 15 | ) 16 | 17 | // A Server serves HTTP pages. 18 | type Server struct { 19 | cfg *config 20 | routingTable atomic.Value 21 | 22 | ready *Event 23 | } 24 | 25 | // New 创建一个新的服务器 26 | func New(options ...Option) *Server { 27 | cfg := defaultConfig() 28 | for _, o := range options { 29 | o(cfg) 30 | } 31 | s := &Server{ 32 | cfg: cfg, 33 | ready: NewEvent(), 34 | } 35 | s.routingTable.Store(NewRoutingTable(nil)) 36 | return s 37 | } 38 | 39 | // Run 启动服务器. 40 | func (s *Server) Run(ctx context.Context) error { 41 | // 直到第一个 payload 数据后才开始监听 42 | s.ready.Wait(ctx) 43 | 44 | // 启动 80 和 443 两个端口 45 | var eg errgroup.Group 46 | eg.Go(func() error { 47 | // 当前的 Server 实现了 Handler 接口(ServeHTTP函数) 48 | srv := http.Server{ 49 | Addr: fmt.Sprintf("%s:%d", s.cfg.host, s.cfg.tlsPort), 50 | Handler: s, 51 | } 52 | srv.TLSConfig = &tls.Config{ 53 | GetCertificate: func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) { 54 | return s.routingTable.Load().(*RoutingTable).GetCertificate(hello.ServerName) 55 | }, 56 | } 57 | log.Info().Str("addr", srv.Addr).Msg("starting secure HTTP server") 58 | err := srv.ListenAndServeTLS("", "") 59 | if err != nil { 60 | return fmt.Errorf("error serving tls: %w", err) 61 | } 62 | return nil 63 | }) 64 | eg.Go(func() error { 65 | srv := http.Server{ 66 | Addr: fmt.Sprintf("%s:%d", s.cfg.host, s.cfg.port), 67 | Handler: s, 68 | } 69 | log.Info().Str("addr", srv.Addr).Msg("starting insecure HTTP server") 70 | err := srv.ListenAndServe() 71 | if err != nil { 72 | return fmt.Errorf("error serving non-tls: %w", err) 73 | } 74 | return nil 75 | }) 76 | return eg.Wait() 77 | } 78 | 79 | // ServeHTTP serves an HTTP request. 80 | func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { 81 | // 获取后端的真实服务地址 82 | backendURL, err := s.routingTable.Load().(*RoutingTable).GetBackend(r.Host, r.URL.Path) 83 | if err != nil { 84 | http.Error(w, "upstream server not found", http.StatusNotFound) 85 | return 86 | } 87 | log.Info().Str("host", r.Host).Str("path", r.URL.Path).Str("backend", backendURL.String()).Msg("proxying request") 88 | // 使用 NewSingleHostReverseProxy 进行代理请求 89 | p := httputil.NewSingleHostReverseProxy(backendURL) 90 | p.ErrorLog = stdlog.New(log.Logger, "", 0) 91 | p.ServeHTTP(w, r) 92 | } 93 | 94 | // Update 更新路由表根据新的 Ingress 规则 95 | func (s *Server) Update(payload *watcher.Payload) { 96 | s.routingTable.Store(NewRoutingTable(payload)) 97 | s.ready.Set() 98 | } 99 | -------------------------------------------------------------------------------- /server/server_test.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "fmt" 7 | "github.com/bep/debounce" 8 | "io" 9 | "io/ioutil" 10 | "net" 11 | "net/http" 12 | "sync/atomic" 13 | "testing" 14 | "time" 15 | 16 | "github.com/calebdoxsey/kubernetes-simple-ingress-controller/watcher" 17 | "github.com/stretchr/testify/assert" 18 | extensionsv1beta1 "k8s.io/api/extensions/v1beta1" 19 | "k8s.io/apimachinery/pkg/util/intstr" 20 | ) 21 | 22 | func TestServer(t *testing.T) { 23 | ctx, cancel := context.WithCancel(context.Background()) 24 | defer cancel() 25 | ctx, _ = context.WithTimeout(ctx, time.Second*30) 26 | 27 | httpPort, tlsPort := getFreePort(t), getFreePort(t) 28 | svcAPort := getFreePort(t) 29 | 30 | go func() { 31 | srv := &http.Server{ 32 | Addr: fmt.Sprintf("127.0.0.1:%d", svcAPort), 33 | Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 34 | _, _ = io.WriteString(w, "svc-a") 35 | }), 36 | } 37 | go func() { 38 | <-ctx.Done() 39 | _ = srv.Close() 40 | }() 41 | err := srv.ListenAndServe() 42 | if err != nil { 43 | t.Fatal(err) 44 | } 45 | }() 46 | 47 | s := New(WithHost("127.0.0.1"), 48 | WithPort(httpPort), 49 | WithTLSPort(tlsPort)) 50 | go func() { 51 | err := s.Run(ctx) 52 | if err != nil { 53 | t.Fatal(err) 54 | } 55 | }() 56 | 57 | s.Update(&watcher.Payload{ 58 | Ingresses: []watcher.IngressPayload{ 59 | { 60 | Ingress: &extensionsv1beta1.Ingress{ 61 | Spec: extensionsv1beta1.IngressSpec{ 62 | Rules: []extensionsv1beta1.IngressRule{{ 63 | Host: "www.example.com", 64 | IngressRuleValue: extensionsv1beta1.IngressRuleValue{HTTP: &extensionsv1beta1.HTTPIngressRuleValue{ 65 | Paths: []extensionsv1beta1.HTTPIngressPath{{ 66 | Path: "/", 67 | Backend: extensionsv1beta1.IngressBackend{ 68 | ServiceName: "127.0.0.1", 69 | ServicePort: intstr.FromString("port-a"), 70 | }, 71 | }}, 72 | }}, 73 | }}, 74 | }, 75 | }, 76 | ServicePorts: map[string]map[string]int{ 77 | "127.0.0.1": { 78 | "port-a": svcAPort, 79 | }, 80 | }, 81 | }, 82 | }, 83 | TLSCertificates: map[string]*tls.Certificate{}, 84 | }) 85 | 86 | if !waitForPort(ctx, httpPort) { 87 | t.Fatalf("http server never started on %d", httpPort) 88 | } 89 | 90 | req, err := http.NewRequest("GET", fmt.Sprintf("http://127.0.0.1:%d/", httpPort), nil) 91 | assert.NoError(t, err) 92 | req.Host = "www.example.com" 93 | req = req.WithContext(ctx) 94 | res, err := http.DefaultClient.Do(req) 95 | assert.NoError(t, err) 96 | assert.Equal(t, 200, res.StatusCode) 97 | bs, err := ioutil.ReadAll(res.Body) 98 | assert.NoError(t, err) 99 | _ = res.Body.Close() 100 | assert.Equal(t, "svc-a", string(bs)) 101 | 102 | } 103 | 104 | func getFreePort(t *testing.T) (int) { 105 | li, err := net.Listen("tcp4", "127.0.0.1:0") 106 | if err != nil { 107 | t.Fatal(err) 108 | } 109 | defer li.Close() 110 | return li.Addr().(*net.TCPAddr).Port 111 | } 112 | 113 | func waitForPort(ctx context.Context, port int) bool { 114 | ctx, cleanup := context.WithTimeout(ctx, time.Second*10) 115 | defer cleanup() 116 | 117 | ticker := time.NewTicker(time.Millisecond * 50) 118 | defer ticker.Stop() 119 | 120 | for range ticker.C { 121 | select { 122 | case <-ctx.Done(): 123 | return false 124 | default: 125 | } 126 | 127 | if conn, err := net.Dial("tcp4", fmt.Sprintf("127.0.0.1:%d", port)); err == nil { 128 | _ = conn.Close() 129 | return true 130 | } 131 | } 132 | panic("impossible") 133 | } 134 | 135 | 136 | func TestDebounced(t *testing.T) { 137 | var counter uint64 138 | 139 | f := func() { 140 | fmt.Println("calling", time.Now().String()) 141 | atomic.AddUint64(&counter, 1) 142 | } 143 | 144 | debounced := debounce.New(100 * time.Millisecond) 145 | 146 | for i := 0; i < 3; i++ { 147 | for j := 0; j < 10; j++ { 148 | fmt.Println("start", i, j) 149 | debounced(f) 150 | fmt.Println("end", i, j) 151 | } 152 | 153 | time.Sleep(200 * time.Millisecond) 154 | } 155 | 156 | c := int(atomic.LoadUint64(&counter)) 157 | 158 | fmt.Println("Counter is", c) 159 | // Output: Counter is 3 160 | 161 | } -------------------------------------------------------------------------------- /server/route_test.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "crypto/tls" 5 | "net/url" 6 | "testing" 7 | 8 | "github.com/cnych/kubernetes-simple-ingress-controller/watcher" 9 | "github.com/stretchr/testify/assert" 10 | extensionsv1beta1 "k8s.io/api/extensions/v1beta1" 11 | "k8s.io/apimachinery/pkg/util/intstr" 12 | ) 13 | 14 | func TestRoutingTable(t *testing.T) { 15 | t.Run("empty payload", func(t *testing.T) { 16 | rt := NewRoutingTable(nil) 17 | u, err := rt.GetBackend("host", "/") 18 | assert.Nil(t, u) 19 | assert.Error(t, err) 20 | 21 | cert, err := rt.GetCertificate("host") 22 | assert.Nil(t, cert) 23 | assert.Error(t, err) 24 | }) 25 | t.Run("default backend with no rules", func(t *testing.T) { 26 | rt := NewRoutingTable(&watcher.Payload{ 27 | Ingresses: []watcher.IngressPayload{{ 28 | Ingress: &extensionsv1beta1.Ingress{Spec: extensionsv1beta1.IngressSpec{ 29 | Backend: &extensionsv1beta1.IngressBackend{ 30 | ServiceName: "example.default.svc.cluster.local", 31 | ServicePort: intstr.FromInt(80), 32 | }, 33 | }}, 34 | }}, 35 | }) 36 | u, err := rt.GetBackend("www.example.com", "/users/1234") 37 | assert.Error(t, err) 38 | assert.Nil(t, u) 39 | }) 40 | t.Run("default backend with host rule", func(t *testing.T) { 41 | rt := NewRoutingTable(&watcher.Payload{ 42 | Ingresses: []watcher.IngressPayload{{ 43 | Ingress: &extensionsv1beta1.Ingress{Spec: extensionsv1beta1.IngressSpec{ 44 | Backend: &extensionsv1beta1.IngressBackend{ 45 | ServiceName: "example", 46 | ServicePort: intstr.FromInt(80), 47 | }, 48 | Rules: []extensionsv1beta1.IngressRule{{ 49 | Host: "www.example.com", 50 | }}, 51 | }}, 52 | }}, 53 | }) 54 | u, err := rt.GetBackend("www.example.com:8443", "/users/1234") 55 | assert.NoError(t, err) 56 | assert.Equal(t, &url.URL{ 57 | Scheme: "http", 58 | Host: "example:80", 59 | }, u) 60 | }) 61 | t.Run("default backend with named port", func(t *testing.T) { 62 | rt := NewRoutingTable(&watcher.Payload{ 63 | Ingresses: []watcher.IngressPayload{{ 64 | Ingress: &extensionsv1beta1.Ingress{Spec: extensionsv1beta1.IngressSpec{ 65 | Backend: &extensionsv1beta1.IngressBackend{ 66 | ServiceName: "example", 67 | ServicePort: intstr.FromString("http"), 68 | }, 69 | Rules: []extensionsv1beta1.IngressRule{{ 70 | Host: "www.example.com", 71 | }}, 72 | }}, 73 | ServicePorts: map[string]map[string]int{ 74 | "example": {"http": 80}, 75 | }, 76 | }}, 77 | }) 78 | u, err := rt.GetBackend("www.example.com", "/users/1234") 79 | assert.NoError(t, err) 80 | assert.Equal(t, &url.URL{ 81 | Scheme: "http", 82 | Host: "example:80", 83 | }, u) 84 | }) 85 | t.Run("tls cert", func(t *testing.T) { 86 | cert1 := new(tls.Certificate) 87 | rt := NewRoutingTable(&watcher.Payload{ 88 | Ingresses: []watcher.IngressPayload{{ 89 | Ingress: &extensionsv1beta1.Ingress{Spec: extensionsv1beta1.IngressSpec{ 90 | Backend: &extensionsv1beta1.IngressBackend{ 91 | ServiceName: "example.default.svc.cluster.local", 92 | ServicePort: intstr.FromInt(80), 93 | }, 94 | TLS: []extensionsv1beta1.IngressTLS{{ 95 | Hosts: []string{"www.example.com"}, 96 | SecretName: "example", 97 | }}, 98 | Rules: []extensionsv1beta1.IngressRule{{ 99 | Host: "www.example.com", 100 | }}, 101 | }}, 102 | }}, 103 | TLSCertificates: map[string]*tls.Certificate{ 104 | "example": cert1, 105 | }, 106 | }) 107 | cert, err := rt.GetCertificate("www.example.com") 108 | assert.NoError(t, err) 109 | assert.Equal(t, cert, cert1) 110 | }) 111 | t.Run("wildcard tls cert", func(t *testing.T) { 112 | cert1 := new(tls.Certificate) 113 | rt := NewRoutingTable(&watcher.Payload{ 114 | Ingresses: []watcher.IngressPayload{{ 115 | Ingress: &extensionsv1beta1.Ingress{Spec: extensionsv1beta1.IngressSpec{ 116 | Backend: &extensionsv1beta1.IngressBackend{ 117 | ServiceName: "example.default.svc.cluster.local", 118 | ServicePort: intstr.FromInt(80), 119 | }, 120 | TLS: []extensionsv1beta1.IngressTLS{{ 121 | Hosts: []string{"*.example.com"}, 122 | SecretName: "example", 123 | }}, 124 | Rules: []extensionsv1beta1.IngressRule{{ 125 | Host: "www.example.com", 126 | }}, 127 | }}, 128 | }}, 129 | TLSCertificates: map[string]*tls.Certificate{ 130 | "example": cert1, 131 | }, 132 | }) 133 | cert, err := rt.GetCertificate("www.example.com") 134 | assert.NoError(t, err) 135 | assert.Equal(t, cert, cert1) 136 | }) 137 | } 138 | -------------------------------------------------------------------------------- /server/route.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "crypto/tls" 5 | "errors" 6 | "fmt" 7 | "net/url" 8 | "regexp" 9 | "strings" 10 | 11 | "github.com/cnych/kubernetes-simple-ingress-controller/watcher" 12 | "github.com/rs/zerolog/log" 13 | extensionsv1beta1 "k8s.io/api/extensions/v1beta1" 14 | "k8s.io/apimachinery/pkg/util/intstr" 15 | ) 16 | 17 | // A RoutingTable contains the information needed to route a request. 18 | type RoutingTable struct { 19 | certificatesByHost map[string]map[string]*tls.Certificate 20 | backendsByHost map[string][]routingTableBackend 21 | } 22 | 23 | type routingTableBackend struct { 24 | pathRE *regexp.Regexp 25 | url *url.URL 26 | } 27 | 28 | func newRoutingTableBackend(path string, serviceName string, servicePort int) (routingTableBackend, error) { 29 | rtb := routingTableBackend{ 30 | url: &url.URL{ 31 | Scheme: "http", 32 | Host: fmt.Sprintf("%s:%d", serviceName, servicePort), 33 | }, 34 | } 35 | var err error 36 | if path != "" { 37 | rtb.pathRE, err = regexp.Compile(path) 38 | } 39 | return rtb, err 40 | } 41 | 42 | func (rtb routingTableBackend) matches(path string) bool { 43 | if rtb.pathRE == nil { 44 | return true 45 | } 46 | return rtb.pathRE.MatchString(path) 47 | } 48 | 49 | // NewRoutingTable creates a new RoutingTable. 50 | func NewRoutingTable(payload *watcher.Payload) *RoutingTable { 51 | rt := &RoutingTable{ 52 | certificatesByHost: make(map[string]map[string]*tls.Certificate), 53 | backendsByHost: make(map[string][]routingTableBackend), 54 | } 55 | rt.init(payload) 56 | return rt 57 | } 58 | 59 | func (rt *RoutingTable) init(payload *watcher.Payload) { 60 | if payload == nil { 61 | return 62 | } 63 | // 根据 payload 数据重新初始化 路由表 64 | for _, ingressPayload := range payload.Ingresses { // 循环所有的 IngressPayload 65 | for _, rule := range ingressPayload.Ingress.Spec.Rules { // 循环 Ingress Rules 规则 66 | m, ok := rt.certificatesByHost[rule.Host] 67 | if !ok { 68 | m = make(map[string]*tls.Certificate) 69 | rt.certificatesByHost[rule.Host] = m 70 | } 71 | // 更新路由表证书信息 72 | for _, t := range ingressPayload.Ingress.Spec.TLS { 73 | for _, h := range t.Hosts { 74 | cert, ok := payload.TLSCertificates[t.SecretName] 75 | if ok { 76 | m[h] = cert 77 | } 78 | } 79 | } 80 | rt.addBackend(ingressPayload, rule) 81 | } 82 | } 83 | } 84 | 85 | func (rt *RoutingTable) addBackend(ingressPayload watcher.IngressPayload, rule extensionsv1beta1.IngressRule) { 86 | if rule.HTTP == nil { 87 | if ingressPayload.Ingress.Spec.Backend != nil { 88 | backend := ingressPayload.Ingress.Spec.Backend 89 | rtb, err := newRoutingTableBackend("", backend.ServiceName, 90 | rt.getServicePort(ingressPayload, backend.ServiceName, backend.ServicePort)) 91 | if err != nil { 92 | // this shouldn't happen 93 | log.Error().Err(err).Send() 94 | return 95 | } 96 | rt.backendsByHost[rule.Host] = append(rt.backendsByHost[rule.Host], rtb) 97 | } 98 | } else { 99 | for _, path := range rule.HTTP.Paths { 100 | backend := path.Backend 101 | rtb, err := newRoutingTableBackend(path.Path, backend.ServiceName, 102 | rt.getServicePort(ingressPayload, backend.ServiceName, backend.ServicePort)) 103 | if err != nil { 104 | log.Error().Err(err).Interface("path", path).Msg("invalid ingress rule path regex") 105 | continue 106 | } 107 | rt.backendsByHost[rule.Host] = append(rt.backendsByHost[rule.Host], rtb) 108 | } 109 | } 110 | } 111 | 112 | func (rt *RoutingTable) getServicePort(ingressPayload watcher.IngressPayload, serviceName string, servicePort intstr.IntOrString) int { 113 | if servicePort.Type == intstr.Int { 114 | return servicePort.IntValue() 115 | } 116 | if m, ok := ingressPayload.ServicePorts[serviceName]; ok { 117 | return m[servicePort.String()] 118 | } 119 | return 80 120 | } 121 | 122 | func (rt *RoutingTable) matches(sni string, certHost string) bool { 123 | for strings.HasPrefix(certHost, "*.") { 124 | if idx := strings.IndexByte(sni, '.'); idx >= 0 { 125 | sni = sni[idx+1:] 126 | } else { 127 | return false 128 | } 129 | certHost = certHost[2:] 130 | } 131 | return sni == certHost 132 | } 133 | 134 | // GetCertificate gets a certificate. 135 | func (rt *RoutingTable) GetCertificate(sni string) (*tls.Certificate, error) { 136 | hostCerts, ok := rt.certificatesByHost[sni] 137 | if ok { 138 | for h, cert := range hostCerts { 139 | if rt.matches(sni, h) { 140 | return cert, nil 141 | } 142 | } 143 | } 144 | return nil, errors.New("certificate not found") 145 | } 146 | 147 | // GetBackend gets the backend for the given host and path. 148 | func (rt *RoutingTable) GetBackend(host, path string) (*url.URL, error) { 149 | if idx := strings.IndexByte(host, ':'); idx > 0 { 150 | host = host[:idx] 151 | } 152 | backends := rt.backendsByHost[host] 153 | for _, backend := range backends { 154 | if backend.matches(path) { 155 | return backend.url, nil 156 | } 157 | } 158 | return nil, errors.New("backend not found") 159 | } 160 | -------------------------------------------------------------------------------- /watcher/watcher.go: -------------------------------------------------------------------------------- 1 | package watcher 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "sync" 7 | "time" 8 | 9 | "github.com/bep/debounce" 10 | "github.com/rs/zerolog/log" 11 | extensionsv1beta1 "k8s.io/api/extensions/v1beta1" 12 | "k8s.io/apimachinery/pkg/labels" 13 | "k8s.io/client-go/informers" 14 | "k8s.io/client-go/kubernetes" 15 | "k8s.io/client-go/tools/cache" 16 | ) 17 | 18 | // A Payload is a collection of Kubernetes data loaded by the watcher. 19 | type Payload struct { 20 | Ingresses []IngressPayload 21 | TLSCertificates map[string]*tls.Certificate 22 | } 23 | 24 | // An IngressPayload is an ingress + its service ports. 25 | type IngressPayload struct { 26 | Ingress *extensionsv1beta1.Ingress 27 | ServicePorts map[string]map[string]int 28 | } 29 | 30 | // A Watcher watches for ingresses in the kubernetes cluster 31 | type Watcher struct { 32 | client kubernetes.Interface 33 | onChange func(*Payload) 34 | } 35 | 36 | // New creates a new Watcher. 37 | func New(client kubernetes.Interface, onChange func(*Payload)) *Watcher { 38 | return &Watcher{ 39 | client: client, 40 | onChange: onChange, 41 | } 42 | } 43 | 44 | // Run runs the watcher. 45 | func (w *Watcher) Run(ctx context.Context) error { 46 | factory := informers.NewSharedInformerFactory(w.client, time.Minute) 47 | secretLister := factory.Core().V1().Secrets().Lister() 48 | serviceLister := factory.Core().V1().Services().Lister() 49 | ingressLister := factory.Extensions().V1beta1().Ingresses().Lister() 50 | 51 | addBackend := func(ingressPayload *IngressPayload, backend extensionsv1beta1.IngressBackend) { 52 | // 通过 Ingress 所在的 namespace 和 ServiceName 获取 Service 对象 53 | svc, err := serviceLister.Services(ingressPayload.Ingress.Namespace).Get(backend.ServiceName) 54 | if err != nil { 55 | log.Error().Err(err). 56 | Str("namespace", ingressPayload.Ingress.Namespace). 57 | Str("name", backend.ServiceName). 58 | Msg("unknown service") 59 | } else { 60 | // Service 端口映射 61 | m := make(map[string]int) 62 | for _, port := range svc.Spec.Ports { 63 | m[port.Name] = int(port.Port) 64 | } 65 | ingressPayload.ServicePorts[svc.Name] = m 66 | // {svcname: {httpport: 80, httpsport: 443}} 67 | } 68 | } 69 | 70 | onChange := func() { 71 | payload := &Payload{ 72 | TLSCertificates: make(map[string]*tls.Certificate), 73 | } 74 | 75 | // 获得所有的 Ingress 76 | ingresses, err := ingressLister.List(labels.Everything()) 77 | if err != nil { 78 | log.Error().Err(err).Msg("failed to list ingresses") 79 | return 80 | } 81 | 82 | for _, ingress := range ingresses { 83 | // 构造 IngressPayload 结构 84 | ingressPayload := IngressPayload{ 85 | Ingress: ingress, 86 | ServicePorts: make(map[string]map[string]int), 87 | } 88 | payload.Ingresses = append(payload.Ingresses, ingressPayload) 89 | 90 | //apiVersion: extensions/v1beta1 91 | //kind: Ingress 92 | //metadata: 93 | // name: test-ingress 94 | //spec: 95 | // backend: 96 | // serviceName: testsvc 97 | // servicePort: 80 98 | if ingress.Spec.Backend != nil { 99 | // 给 ingressPayload 组装数据 100 | addBackend(&ingressPayload, *ingress.Spec.Backend) 101 | } 102 | //apiVersion: extensions/v1beta1 103 | //kind: Ingress 104 | //metadata: 105 | // name: test 106 | //spec: 107 | // rules: 108 | // - host: foo.bar.com 109 | // http: 110 | // paths: 111 | // - backend: 112 | // serviceName: s1 113 | // servicePort: 80 114 | for _, rule := range ingress.Spec.Rules { 115 | if rule.HTTP != nil { 116 | continue 117 | } 118 | for _, path := range rule.HTTP.Paths { 119 | // 给 ingressPayload 组装数据 120 | addBackend(&ingressPayload, path.Backend) 121 | } 122 | } 123 | 124 | // 证书处理 125 | for _, rec := range ingress.Spec.TLS { 126 | if rec.SecretName != "" { 127 | // 获取证书对应的 secret 128 | secret, err := secretLister.Secrets(ingress.Namespace).Get(rec.SecretName) 129 | if err != nil { 130 | log.Error(). 131 | Err(err). 132 | Str("namespace", ingress.Namespace). 133 | Str("name", rec.SecretName). 134 | Msg("unknown secret") 135 | continue 136 | } 137 | // 加载证书 138 | cert, err := tls.X509KeyPair(secret.Data["tls.crt"], secret.Data["tls.key"]) 139 | if err != nil { 140 | log.Error(). 141 | Err(err). 142 | Str("namespace", ingress.Namespace). 143 | Str("name", rec.SecretName). 144 | Msg("invalid tls certificate") 145 | continue 146 | } 147 | 148 | payload.TLSCertificates[rec.SecretName] = &cert 149 | } 150 | } 151 | } 152 | 153 | w.onChange(payload) 154 | } 155 | 156 | debounced := debounce.New(time.Second) 157 | handler := cache.ResourceEventHandlerFuncs{ 158 | AddFunc: func(obj interface{}) { 159 | debounced(onChange) 160 | }, 161 | UpdateFunc: func(oldObj, newObj interface{}) { 162 | debounced(onChange) 163 | }, 164 | DeleteFunc: func(obj interface{}) { 165 | debounced(onChange) 166 | }, 167 | } 168 | 169 | // 启动 Secret、Ingress、Service 的 Informer,用同一个事件处理器 handler 170 | var wg sync.WaitGroup 171 | wg.Add(1) 172 | go func() { 173 | informer := factory.Core().V1().Secrets().Informer() 174 | informer.AddEventHandler(handler) 175 | informer.Run(ctx.Done()) 176 | wg.Done() 177 | }() 178 | 179 | wg.Add(1) 180 | go func() { 181 | informer := factory.Extensions().V1beta1().Ingresses().Informer() 182 | informer.AddEventHandler(handler) 183 | informer.Run(ctx.Done()) 184 | wg.Done() 185 | }() 186 | 187 | wg.Add(1) 188 | go func() { 189 | informer := factory.Core().V1().Services().Informer() 190 | informer.AddEventHandler(handler) 191 | informer.Run(ctx.Done()) 192 | wg.Done() 193 | }() 194 | 195 | wg.Wait() 196 | return nil 197 | } 198 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | github.com/Azure/go-autorest v11.1.2+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= 3 | github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= 4 | github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= 5 | github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= 6 | github.com/bep/debounce v1.2.0 h1:wXds8Kq8qRfwAOpAxHrJDbCXgC5aHSzgQb/0gKsHQqo= 7 | github.com/bep/debounce v1.2.0/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= 8 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 9 | github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 11 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 12 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 13 | github.com/dgrijalva/jwt-go v0.0.0-20160705203006-01aeca54ebda/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 14 | github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= 15 | github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= 16 | github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= 17 | github.com/evanphx/json-patch v0.0.0-20190203023257-5858425f7550/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= 18 | github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= 19 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 20 | github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 21 | github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= 22 | github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= 23 | github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg= 24 | github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc= 25 | github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I= 26 | github.com/gogo/protobuf v0.0.0-20171007142547-342cbe0a0415/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 27 | github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d h1:3PaI8p3seN09VjbTYC/QWlUZdZ1qS1zGjy7LH2Wt07I= 28 | github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= 29 | github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903 h1:LbsanbbD6LieFkXbj9YNNBupiGHJgFeLpO0j0Fza1h8= 30 | github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 31 | github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 32 | github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= 33 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 34 | github.com/google/btree v0.0.0-20160524151835-7d79101e329e/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 35 | github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= 36 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 37 | github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= 38 | github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= 39 | github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= 40 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 41 | github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 42 | github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 43 | github.com/googleapis/gnostic v0.0.0-20170426233943-68f4ded48ba9/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= 44 | github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d h1:7XGaL1e6bYS1yIonGp9761ExpPPV1ui0SAC59Yube9k= 45 | github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= 46 | github.com/gophercloud/gophercloud v0.0.0-20190126172459-c818fa66e4c8/go.mod h1:3WdhXV3rUYy9p6AUW8d94kr+HS62Y4VL9mBnFxsD8q4= 47 | github.com/gregjones/httpcache v0.0.0-20170728041850-787624de3eb7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= 48 | github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo= 49 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 50 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 51 | github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= 52 | github.com/imdario/mergo v0.3.7 h1:Y+UAYTZ7gDEuOfhxKWy+dvb5dRQ6rJjFSdX2HZY1/gI= 53 | github.com/imdario/mergo v0.3.7/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= 54 | github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 55 | github.com/json-iterator/go v0.0.0-20180701071628-ab8a2e0c74be/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 56 | github.com/json-iterator/go v1.1.7 h1:KfgG9LzI+pYjr4xvmz/5H4FXjokeP+rlHLhv3iH62Fo= 57 | github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 58 | github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= 59 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 60 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 61 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 62 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 63 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 64 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 65 | github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 66 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 67 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 68 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 69 | github.com/modern-go/reflect2 v0.0.0-20180320133207-05fbef0ca5da/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 70 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 71 | github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= 72 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 73 | github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 74 | github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= 75 | github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 76 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 77 | github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 78 | github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= 79 | github.com/onsi/gomega v0.0.0-20190113212917-5533ce8a0da3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 80 | github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 81 | github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= 82 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 83 | github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 84 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 85 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 86 | github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= 87 | github.com/rs/zerolog v1.15.0 h1:uPRuwkWF4J6fGsJ2R0Gn2jB1EQiav9k3S6CSdygQJXY= 88 | github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= 89 | github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= 90 | github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 91 | github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 92 | github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= 93 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 94 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 95 | github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 96 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 97 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 98 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 99 | github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= 100 | golang.org/x/crypto v0.0.0-20181025213731-e84da0312774/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 101 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= 102 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 103 | golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 104 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 105 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 106 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 107 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 108 | golang.org/x/net v0.0.0-20190812203447-cdfb69ac37fc h1:gkKoSkUmnU6bpS/VhkuO27bzQeSA51uaEfbOW5dNb68= 109 | golang.org/x/net v0.0.0-20190812203447-cdfb69ac37fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 110 | golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 111 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0= 112 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 113 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 114 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 115 | golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= 116 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 117 | golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 118 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 119 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 120 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 121 | golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f h1:25KHgbfyiSm6vwQLbM3zZIe1v9p/3ea4Rz+nnM5K/i4= 122 | golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 123 | golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 124 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 125 | golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 126 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= 127 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 128 | golang.org/x/time v0.0.0-20161028155119-f51c12702a4d/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 129 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ= 130 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 131 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 132 | golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 133 | golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 134 | golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 135 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 136 | google.golang.org/appengine v1.5.0 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c= 137 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 138 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 139 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 140 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 141 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 142 | gopkg.in/inf.v0 v0.9.0 h1:3zYtXIO92bvsdS3ggAdA8Gb4Azj0YU+TVY1uGYNFA8o= 143 | gopkg.in/inf.v0 v0.9.0/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 144 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 145 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 146 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 147 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 148 | k8s.io/api v0.0.0-20190819141258-3544db3b9e44/go.mod h1:AOxZTnaXR/xiarlQL0JUfwQPxjmKDvVYoRp58cA7lUo= 149 | k8s.io/api v0.0.0-20190826194732-9f642ccb7a30 h1:m4gvK7Qr77kV77ugv9jzC7HSlPElqL93RGGU6DIaTFk= 150 | k8s.io/api v0.0.0-20190826194732-9f642ccb7a30/go.mod h1:wKwR91YLONtsDxeJMXqvshPVeRU6ek8/1MfKoDBFqU0= 151 | k8s.io/apimachinery v0.0.0-20190817020851-f2f3a405f61d/go.mod h1:3jediapYqJ2w1BFw7lAZPCx7scubsTfosqHkhXCWJKw= 152 | k8s.io/apimachinery v0.0.0-20190826114657-e31a5531b558 h1:89s1htYZqlP8Xpj+rGQ4ys/Xksmqrf4Xws0fZuWoWZg= 153 | k8s.io/apimachinery v0.0.0-20190826114657-e31a5531b558/go.mod h1:EZoIMuAgG/4v58YL+bz0kqnivqupk28fKYxFCa5e6X8= 154 | k8s.io/client-go v0.0.0-20190819141724-e14f31a72a77 h1:w1BoabVnPpPqQCY3sHK4qVwa12Lk8ip1pKMR1C+qbdo= 155 | k8s.io/client-go v0.0.0-20190819141724-e14f31a72a77/go.mod h1:DmkJD5UDP87MVqUQ5VJ6Tj9Oen8WzXPhk3la4qpyG4g= 156 | k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= 157 | k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= 158 | k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= 159 | k8s.io/klog v0.3.1/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= 160 | k8s.io/klog v0.4.0 h1:lCJCxf/LIowc2IGS9TPjWDyXY4nOmdGdfcwwDQCOURQ= 161 | k8s.io/klog v0.4.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= 162 | k8s.io/kube-openapi v0.0.0-20190228160746-b3a7cee44a30/go.mod h1:BXM9ceUBTj2QnfH2MK1odQs778ajze1RxcmP6S8RVVc= 163 | k8s.io/kube-openapi v0.0.0-20190709113604-33be087ad058/go.mod h1:nfDlWeOsu3pUf4yWGL+ERqohP4YsZcBJXWMK+gkzOA4= 164 | k8s.io/utils v0.0.0-20190221042446-c2654d5206da/go.mod h1:8k8uAuAQ0rXslZKaEWd0c3oVhZz7sSzSiPnVZayjIX0= 165 | k8s.io/utils v0.0.0-20190809000727-6c36bc71fc4a h1:uy5HAgt4Ha5rEMbhZA+aM1j2cq5LmR6LQ71EYC2sVH4= 166 | k8s.io/utils v0.0.0-20190809000727-6c36bc71fc4a/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= 167 | sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI= 168 | sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs= 169 | sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= 170 | --------------------------------------------------------------------------------