Initial version of HAProxy controller
This commit is contained in:
parent
1cebef2dbf
commit
5f73b611f4
10 changed files with 500 additions and 0 deletions
1
controllers/haproxy/.gitignore
vendored
Normal file
1
controllers/haproxy/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
rootfs/haproxy-ingress-controller
|
16
controllers/haproxy/Makefile
Normal file
16
controllers/haproxy/Makefile
Normal file
|
@ -0,0 +1,16 @@
|
|||
RELEASE=0.9.0-beta.1
|
||||
PREFIX=localhost/haproxy-ingress-controller
|
||||
|
||||
PKG=k8s.io/ingress/controllers/haproxy/pkg
|
||||
REPO_INFO=$(shell git config --get remote.origin.url)
|
||||
COMMIT=git-$(shell git rev-parse --short HEAD)
|
||||
|
||||
GOOS=linux
|
||||
|
||||
build:
|
||||
CGO_ENABLED=0 GOOS=$(GOOS) go build \
|
||||
-v -installsuffix cgo \
|
||||
-ldflags "-s -w -X $(PKG)/version.RELEASE=$(RELEASE) -X $(PKG)/version.COMMIT=$(COMMIT) -X $(PKG)/version.REPO=$(REPO_INFO)" \
|
||||
-o rootfs/haproxy-ingress-controller $(PKG)/controller
|
||||
container: build
|
||||
docker build -t $(PREFIX):$(RELEASE) rootfs
|
56
controllers/haproxy/pkg/controller/config.go
Normal file
56
controllers/haproxy/pkg/controller/config.go
Normal file
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
Copyright 2017 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.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/golang/glog"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"k8s.io/ingress/core/pkg/ingress"
|
||||
)
|
||||
|
||||
type (
|
||||
configuration struct {
|
||||
Backends []*ingress.Backend
|
||||
Servers []*ingress.Server
|
||||
TCPEndpoints []*ingress.Location
|
||||
UDPEndpoints []*ingress.Location
|
||||
PassthroughBackends []*ingress.SSLPassthroughBackend
|
||||
Syslog string `json:"syslog-endpoint"`
|
||||
}
|
||||
)
|
||||
|
||||
func newConfig(cfg *ingress.Configuration, data map[string]string) *configuration {
|
||||
conf := configuration{
|
||||
Backends: cfg.Backends,
|
||||
Servers: cfg.Servers,
|
||||
TCPEndpoints: cfg.TCPEndpoints,
|
||||
UDPEndpoints: cfg.UPDEndpoints,
|
||||
PassthroughBackends: cfg.PassthroughBackends,
|
||||
}
|
||||
decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
|
||||
WeaklyTypedInput: true,
|
||||
Result: &conf,
|
||||
TagName: "json",
|
||||
})
|
||||
if err != nil {
|
||||
glog.Warningf("error configuring decoder: %v", err)
|
||||
}
|
||||
if err = decoder.Decode(data); err != nil {
|
||||
glog.Warningf("error decoding config: %v", err)
|
||||
}
|
||||
return &conf
|
||||
}
|
124
controllers/haproxy/pkg/controller/haproxy.go
Normal file
124
controllers/haproxy/pkg/controller/haproxy.go
Normal file
|
@ -0,0 +1,124 @@
|
|||
/*
|
||||
Copyright 2017 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.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/golang/glog"
|
||||
"io/ioutil"
|
||||
"k8s.io/ingress/controllers/haproxy/pkg/version"
|
||||
"k8s.io/ingress/core/pkg/ingress"
|
||||
"k8s.io/ingress/core/pkg/ingress/controller"
|
||||
"k8s.io/ingress/core/pkg/ingress/defaults"
|
||||
"k8s.io/kubernetes/pkg/api"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
type haproxyController struct {
|
||||
controller *controller.GenericController
|
||||
configMap *api.ConfigMap
|
||||
command string
|
||||
configFile string
|
||||
template *template
|
||||
}
|
||||
|
||||
func newHAProxyController() *haproxyController {
|
||||
return &haproxyController{
|
||||
command: "/haproxy-wrapper",
|
||||
configFile: "/usr/local/etc/haproxy/haproxy.cfg",
|
||||
template: newTemplate("haproxy.tmpl", "/usr/local/etc/haproxy/haproxy.tmpl"),
|
||||
}
|
||||
}
|
||||
|
||||
func (haproxy *haproxyController) Info() *ingress.BackendInfo {
|
||||
return &ingress.BackendInfo{
|
||||
Name: "HAProxy",
|
||||
Release: version.RELEASE,
|
||||
Build: version.COMMIT,
|
||||
Repository: version.REPO,
|
||||
}
|
||||
}
|
||||
|
||||
func (haproxy *haproxyController) Start() {
|
||||
controller := controller.NewIngressController(haproxy)
|
||||
haproxy.controller = controller
|
||||
haproxy.controller.Start()
|
||||
}
|
||||
|
||||
func (haproxy *haproxyController) Stop() error {
|
||||
err := haproxy.controller.Stop()
|
||||
return err
|
||||
}
|
||||
|
||||
func (haproxy *haproxyController) Name() string {
|
||||
return "HAProxy Ingress Controller"
|
||||
}
|
||||
|
||||
func (haproxy *haproxyController) Check(_ *http.Request) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (haproxy *haproxyController) SetConfig(configMap *api.ConfigMap) {
|
||||
haproxy.configMap = configMap
|
||||
}
|
||||
|
||||
func (haproxy *haproxyController) BackendDefaults() defaults.Backend {
|
||||
return defaults.Backend{}
|
||||
}
|
||||
|
||||
func (haproxy *haproxyController) OnUpdate(cfg ingress.Configuration) ([]byte, error) {
|
||||
conf := newConfig(&cfg, haproxy.configMap.Data)
|
||||
data, err := haproxy.template.execute(conf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (haproxy *haproxyController) Reload(data []byte) ([]byte, bool, error) {
|
||||
if !haproxy.configChanged(data) {
|
||||
return nil, false, nil
|
||||
}
|
||||
// TODO missing HAProxy validation before overwrite and try to reload
|
||||
err := ioutil.WriteFile(haproxy.configFile, data, 0644)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
out, err := haproxy.reloadHaproxy()
|
||||
if len(out) > 0 {
|
||||
glog.Infof("HAProxy output:\n%v", string(out))
|
||||
}
|
||||
return out, true, err
|
||||
}
|
||||
|
||||
func (haproxy *haproxyController) configChanged(data []byte) bool {
|
||||
if _, err := os.Stat(haproxy.configFile); os.IsNotExist(err) {
|
||||
return true
|
||||
}
|
||||
cfg, err := ioutil.ReadFile(haproxy.configFile)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return !bytes.Equal(cfg, data)
|
||||
}
|
||||
|
||||
func (haproxy *haproxyController) reloadHaproxy() ([]byte, error) {
|
||||
out, err := exec.Command(haproxy.command, haproxy.configFile).CombinedOutput()
|
||||
return out, err
|
||||
}
|
46
controllers/haproxy/pkg/controller/main.go
Normal file
46
controllers/haproxy/pkg/controller/main.go
Normal file
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
Copyright 2017 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.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/golang/glog"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func main() {
|
||||
hc := newHAProxyController()
|
||||
errCh := make(chan error)
|
||||
go handleSignal(hc, errCh)
|
||||
hc.Start()
|
||||
code := 0
|
||||
err := <-errCh
|
||||
if err != nil {
|
||||
glog.Warningf("Error stopping Ingress: %v", err)
|
||||
code++
|
||||
}
|
||||
glog.Infof("Exiting (%v)", code)
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
func handleSignal(hc *haproxyController, err chan error) {
|
||||
sig := make(chan os.Signal, 1)
|
||||
signal.Notify(sig, syscall.SIGTERM, syscall.SIGINT)
|
||||
glog.Infof("Shutting down with signal %v", <-sig)
|
||||
err <- hc.Stop()
|
||||
}
|
59
controllers/haproxy/pkg/controller/template.go
Normal file
59
controllers/haproxy/pkg/controller/template.go
Normal file
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
Copyright 2017 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.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/golang/glog"
|
||||
"os/exec"
|
||||
gotemplate "text/template"
|
||||
)
|
||||
|
||||
type template struct {
|
||||
tmpl *gotemplate.Template
|
||||
rawConfig *bytes.Buffer
|
||||
fmtConfig *bytes.Buffer
|
||||
}
|
||||
|
||||
func newTemplate(name string, file string) *template {
|
||||
tmpl, err := gotemplate.New(name).ParseFiles(file)
|
||||
if err != nil {
|
||||
glog.Fatalf("Cannot read template file: %v", err)
|
||||
}
|
||||
return &template{
|
||||
tmpl: tmpl,
|
||||
rawConfig: bytes.NewBuffer(make([]byte, 0, 16384)),
|
||||
fmtConfig: bytes.NewBuffer(make([]byte, 0, 16384)),
|
||||
}
|
||||
}
|
||||
|
||||
func (t *template) execute(conf *configuration) ([]byte, error) {
|
||||
t.rawConfig.Reset()
|
||||
t.fmtConfig.Reset()
|
||||
if err := t.tmpl.Execute(t.rawConfig, conf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cmd := exec.Command("sed", "/^ *$/d")
|
||||
cmd.Stdin = t.rawConfig
|
||||
cmd.Stdout = t.fmtConfig
|
||||
if err := cmd.Run(); err != nil {
|
||||
glog.Errorf("Template cleaning has failed: %v", err)
|
||||
// TODO recover and return raw buffer
|
||||
return nil, err
|
||||
}
|
||||
return t.fmtConfig.Bytes(), nil
|
||||
}
|
26
controllers/haproxy/pkg/version/version.go
Normal file
26
controllers/haproxy/pkg/version/version.go
Normal file
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
Copyright 2017 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.
|
||||
*/
|
||||
|
||||
package version
|
||||
|
||||
var (
|
||||
// RELEASE Release version
|
||||
RELEASE = "UNKNOWN"
|
||||
// REPO Git repository URL
|
||||
REPO = "UNKNOWN"
|
||||
// COMMIT Short sha from git commit
|
||||
COMMIT = "UNKNOWN"
|
||||
)
|
28
controllers/haproxy/rootfs/Dockerfile
Normal file
28
controllers/haproxy/rootfs/Dockerfile
Normal file
|
@ -0,0 +1,28 @@
|
|||
# Copyright 2017 The Kubernetes Authors. All rights reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
FROM haproxy:1.7-alpine
|
||||
RUN apk --no-cache add openssl
|
||||
|
||||
# dumb-init kindly manages SIGCHLD from forked HAProxy processes
|
||||
ARG DUMB_INIT_SHA256=81231da1cd074fdc81af62789fead8641ef3f24b6b07366a1c34e5b059faf363
|
||||
RUN wget -O/dumb-init https://github.com/Yelp/dumb-init/releases/download/v1.2.0/dumb-init_1.2.0_amd64\
|
||||
&& echo "$DUMB_INIT_SHA256 /dumb-init" | sha256sum -c -\
|
||||
&& chmod +x /dumb-init
|
||||
|
||||
COPY haproxy-ingress-controller /
|
||||
COPY haproxy-wrapper /
|
||||
COPY haproxy.tmpl /usr/local/etc/haproxy/
|
||||
|
||||
ENTRYPOINT ["/dumb-init", "--", "/haproxy-ingress-controller"]
|
31
controllers/haproxy/rootfs/haproxy-wrapper
Executable file
31
controllers/haproxy/rootfs/haproxy-wrapper
Executable file
|
@ -0,0 +1,31 @@
|
|||
#!/bin/sh
|
||||
#
|
||||
# Copyright 2017 The Kubernetes Authors. All rights reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
# A script to help with haproxy reloads. Needs sudo for :80. Running it for the
|
||||
# first time starts haproxy, each subsequent invocation will perform a
|
||||
# soft-reload.
|
||||
# Receives /path/to/haproxy.cfg as the first parameter
|
||||
# HAProxy options:
|
||||
# -f config file
|
||||
# -p pid file
|
||||
# -D run as daemon
|
||||
# -sf soft reload, wait for pids to finish handling requests
|
||||
# send pids a resume signal if reload of new config fails
|
||||
|
||||
set -e
|
||||
|
||||
pidFile="/var/run/haproxy.pid"
|
||||
haproxy -f "$1" -p "$pidFile" -D -sf $(cat "$pidFile" 2>/dev/null || :)
|
113
controllers/haproxy/rootfs/haproxy.tmpl
Normal file
113
controllers/haproxy/rootfs/haproxy.tmpl
Normal file
|
@ -0,0 +1,113 @@
|
|||
{{ $cfg := . }}
|
||||
global
|
||||
daemon
|
||||
stats socket /tmp/haproxy
|
||||
#server-state-file global
|
||||
#server-state-base /var/state/haproxy/
|
||||
{{ if ne $cfg.Syslog "" }}
|
||||
log {{ $cfg.Syslog }} format rfc5424 local0
|
||||
log-tag ingress
|
||||
{{ end }}
|
||||
tune.ssl.default-dh-param 1024
|
||||
ssl-default-bind-ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!3DES:!MD5:!PSK
|
||||
ssl-default-bind-options no-tls-tickets
|
||||
|
||||
defaults
|
||||
log global
|
||||
#load-server-state-from-file global
|
||||
option redispatch
|
||||
option dontlognull
|
||||
option http-server-close
|
||||
option http-keep-alive
|
||||
timeout http-request 5s
|
||||
timeout connect 5s
|
||||
timeout client 50s
|
||||
timeout client-fin 50s
|
||||
timeout server 50s
|
||||
timeout tunnel 1h
|
||||
timeout http-keep-alive 60s
|
||||
#default_backend #default-backend
|
||||
|
||||
######
|
||||
###### Backends
|
||||
######
|
||||
{{ range $backend := $cfg.Backends }}
|
||||
backend {{ $backend.Name }}
|
||||
mode http
|
||||
balance roundrobin
|
||||
{{ range $endpoint := $backend.Endpoints }}
|
||||
{{ $target := (print $endpoint.Address ":" $endpoint.Port) }}
|
||||
server {{ $target }} {{ $target }} check port {{ $endpoint.Port }} inter 2s
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
######
|
||||
###### HTTP frontend
|
||||
######
|
||||
frontend httpfront
|
||||
bind *:80
|
||||
mode http
|
||||
{{ if ne $cfg.Syslog "" }}
|
||||
option httplog
|
||||
{{ end }}
|
||||
option forwardfor
|
||||
{{ range $server := $cfg.Servers }}
|
||||
{{ if and (ne $server.Hostname "_") (ne $server.SSLCertificate "") }}
|
||||
redirect scheme https if { hdr(host) {{ $server.Hostname }} }
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ range $server := $cfg.Servers }}
|
||||
{{ if and (ne $server.Hostname "_") (eq $server.SSLCertificate "") }}
|
||||
{{ range $location := $server.Locations }}
|
||||
use_backend {{ $location.Backend }} if { hdr(host) {{ $server.Hostname }} } { path_beg {{ $location.Path }} }
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
######
|
||||
###### HTTPS frontends (tcp mode)
|
||||
######
|
||||
frontend httpsfront
|
||||
bind :443
|
||||
mode tcp
|
||||
tcp-request inspect-delay 5s
|
||||
tcp-request content accept if { req.ssl_hello_type 1 }
|
||||
{{ range $server := $cfg.Servers }}
|
||||
{{ if and (ne $server.Hostname "_") (ne $server.SSLCertificate "") }}
|
||||
use_backend httpsback-{{ $server.Hostname }} if { req.ssl_sni -i {{ $server.Hostname }} }
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{ range $server := $cfg.Servers }}
|
||||
{{ if and (ne $server.Hostname "_") (ne $server.SSLCertificate "") }}
|
||||
##
|
||||
## {{ $server.Hostname }}
|
||||
backend httpsback-{{ $server.Hostname }}
|
||||
mode tcp
|
||||
server {{ $server.Hostname }} unix@/var/run/haproxy-{{ $server.Hostname }}.sock send-proxy-v2
|
||||
|
||||
frontend httpsfront-{{ $server.Hostname }}
|
||||
# CRT PEM checksum: {{ $server.SSLPemChecksum }}
|
||||
bind unix@/var/run/haproxy-{{ $server.Hostname }}.sock ssl crt {{ $server.SSLCertificate }} no-sslv3 accept-proxy
|
||||
mode http
|
||||
{{ if ne $cfg.Syslog "" }}
|
||||
option httplog
|
||||
{{ end }}
|
||||
option forwardfor
|
||||
rspadd Strict-Transport-Security:\ max-age=15768000
|
||||
{{ range $location := $server.Locations }}
|
||||
use_backend {{ $location.Backend }} if { path_beg {{ $location.Path }} }
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
######
|
||||
###### Status page
|
||||
######
|
||||
listen stats
|
||||
bind *:1936
|
||||
mode http
|
||||
stats enable
|
||||
stats realm Haproxy\ Statistics
|
||||
stats uri /
|
||||
no log
|
Loading…
Reference in a new issue