Move nginx to root directory

This commit is contained in:
Manuel de Brito Fontes 2017-10-06 16:58:36 -03:00
parent cf2ab5f5f7
commit 2139ee85e7
53 changed files with 666 additions and 1733 deletions

View file

@ -0,0 +1,57 @@
/*
Copyright 2015 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 (
"os"
"os/signal"
"syscall"
"time"
"github.com/golang/glog"
)
func main() {
// start a new nginx controller
ngx := newNGINXController()
go handleSigterm(ngx)
// start the controller
ngx.Start()
// wait
glog.Infof("shutting down Ingress controller...")
for {
glog.Infof("Handled quit, awaiting pod deletion")
time.Sleep(30 * time.Second)
}
}
func handleSigterm(ngx *NGINXController) {
signalChan := make(chan os.Signal, 1)
signal.Notify(signalChan, syscall.SIGTERM)
<-signalChan
glog.Infof("Received SIGTERM, shutting down")
exitCode := 0
if err := ngx.Stop(); err != nil {
glog.Infof("Error during shutdown %v", err)
exitCode = 1
}
glog.Infof("Exiting with %v", exitCode)
os.Exit(exitCode)
}

View file

@ -0,0 +1,97 @@
/*
Copyright 2016 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/prometheus/client_golang/prometheus"
"k8s.io/ingress/controllers/nginx/pkg/metric/collector"
)
const (
ngxStatusPath = "/nginx_status"
ngxVtsPath = "/nginx_status/format/json"
)
func (n *NGINXController) setupMonitor(sm statusModule) {
csm := n.statusModule
if csm != sm {
glog.Infof("changing prometheus collector from %v to %v", csm, sm)
n.stats.stop(csm)
n.stats.start(sm)
n.statusModule = sm
}
}
type statsCollector struct {
process prometheus.Collector
basic collector.Stopable
vts collector.Stopable
namespace string
watchClass string
port int
}
func (s *statsCollector) stop(sm statusModule) {
switch sm {
case defaultStatusModule:
s.basic.Stop()
prometheus.Unregister(s.basic)
case vtsStatusModule:
s.vts.Stop()
prometheus.Unregister(s.vts)
}
}
func (s *statsCollector) start(sm statusModule) {
switch sm {
case defaultStatusModule:
s.basic = collector.NewNginxStatus(s.namespace, s.watchClass, s.port, ngxStatusPath)
prometheus.Register(s.basic)
break
case vtsStatusModule:
s.vts = collector.NewNGINXVTSCollector(s.namespace, s.watchClass, s.port, ngxVtsPath)
prometheus.Register(s.vts)
break
}
}
func newStatsCollector(ns, class, binary string, port int) *statsCollector {
glog.Infof("starting new nginx stats collector for Ingress controller running in namespace %v (class %v)", ns, class)
glog.Infof("collector extracting information from port %v", port)
pc, err := collector.NewNamedProcess(true, collector.BinaryNameMatcher{
Name: "nginx",
Binary: binary,
})
if err != nil {
glog.Fatalf("unexpected error registering nginx collector: %v", err)
}
err = prometheus.Register(pc)
if err != nil {
glog.Fatalf("unexpected error registering nginx collector: %v", err)
}
return &statsCollector{
namespace: ns,
watchClass: class,
process: pc,
port: port,
}
}

799
pkg/cmd/controller/nginx.go Normal file
View file

@ -0,0 +1,799 @@
/*
Copyright 2015 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"
"errors"
"fmt"
"io/ioutil"
"net"
"net/http"
"os"
"os/exec"
"strconv"
"strings"
"syscall"
"time"
"github.com/golang/glog"
"github.com/mitchellh/go-ps"
"github.com/spf13/pflag"
proxyproto "github.com/armon/go-proxyproto"
"github.com/ncabatoff/process-exporter/proc"
apiv1 "k8s.io/api/core/v1"
extensions "k8s.io/api/extensions/v1beta1"
"k8s.io/ingress/controllers/nginx/pkg/config"
ngx_template "k8s.io/ingress/controllers/nginx/pkg/template"
"k8s.io/ingress/controllers/nginx/pkg/version"
"k8s.io/ingress/core/pkg/ingress"
"k8s.io/ingress/core/pkg/ingress/controller"
"k8s.io/ingress/core/pkg/ingress/defaults"
"k8s.io/ingress/core/pkg/net/dns"
"k8s.io/ingress/core/pkg/net/ssl"
)
type statusModule string
const (
ngxHealthPath = "/healthz"
defaultStatusModule statusModule = "default"
vtsStatusModule statusModule = "vts"
defUpstreamName = "upstream-default-backend"
)
var (
tmplPath = "/etc/nginx/template/nginx.tmpl"
cfgPath = "/etc/nginx/nginx.conf"
nginxBinary = "/usr/sbin/nginx"
defIngressClass = "nginx"
)
// newNGINXController creates a new NGINX Ingress controller.
// If the environment variable NGINX_BINARY exists it will be used
// as source for nginx commands
func newNGINXController() *NGINXController {
ngx := os.Getenv("NGINX_BINARY")
if ngx == "" {
ngx = nginxBinary
}
h, err := dns.GetSystemNameServers()
if err != nil {
glog.Warningf("unexpected error reading system nameservers: %v", err)
}
n := &NGINXController{
binary: ngx,
configmap: &apiv1.ConfigMap{},
isIPV6Enabled: isIPv6Enabled(),
resolver: h,
ports: &config.ListenPorts{},
backendDefaults: config.NewDefault().Backend,
}
var onChange func()
onChange = func() {
template, err := ngx_template.NewTemplate(tmplPath, onChange)
if err != nil {
// this error is different from the rest because it must be clear why nginx is not working
glog.Errorf(`
-------------------------------------------------------------------------------
Error loading new template : %v
-------------------------------------------------------------------------------
`, err)
return
}
n.t.Close()
n.t = template
glog.Info("new NGINX template loaded")
}
ngxTpl, err := ngx_template.NewTemplate(tmplPath, onChange)
if err != nil {
glog.Fatalf("invalid NGINX template: %v", err)
}
n.t = ngxTpl
return n
}
// NGINXController ...
type NGINXController struct {
controller *controller.GenericController
t *ngx_template.Template
configmap *apiv1.ConfigMap
storeLister *ingress.StoreLister
binary string
resolver []net.IP
cmdArgs []string
stats *statsCollector
statusModule statusModule
// returns true if IPV6 is enabled in the pod
isIPV6Enabled bool
// returns true if proxy protocol es enabled
isProxyProtocolEnabled bool
isSSLPassthroughEnabled bool
isShuttingDown bool
proxy *proxy
ports *config.ListenPorts
backendDefaults defaults.Backend
}
// Start start a new NGINX master process running in foreground.
func (n *NGINXController) Start() {
n.isShuttingDown = false
n.controller = controller.NewIngressController(n)
go n.controller.Start()
done := make(chan error, 1)
cmd := exec.Command(n.binary, "-c", cfgPath)
// put nginx in another process group to prevent it
// to receive signals meant for the controller
cmd.SysProcAttr = &syscall.SysProcAttr{
Setpgid: true,
Pgid: 0,
}
glog.Info("starting NGINX process...")
n.start(cmd, done)
// if the nginx master process dies the workers continue to process requests,
// passing checks but in case of updates in ingress no updates will be
// reflected in the nginx configuration which can lead to confusion and report
// issues because of this behavior.
// To avoid this issue we restart nginx in case of errors.
for {
err := <-done
if n.isShuttingDown {
break
}
if exitError, ok := err.(*exec.ExitError); ok {
waitStatus := exitError.Sys().(syscall.WaitStatus)
glog.Warningf(`
-------------------------------------------------------------------------------
NGINX master process died (%v): %v
-------------------------------------------------------------------------------
`, waitStatus.ExitStatus(), err)
}
cmd.Process.Release()
cmd = exec.Command(n.binary, "-c", cfgPath)
// we wait until the workers are killed
for {
conn, err := net.DialTimeout("tcp", "127.0.0.1:80", 1*time.Second)
if err != nil {
break
}
conn.Close()
// kill nginx worker processes
fs, err := proc.NewFS("/proc")
procs, _ := fs.FS.AllProcs()
for _, p := range procs {
pn, err := p.Comm()
if err != nil {
glog.Errorf("unexpected error obtaining process information: %v", err)
continue
}
if pn == "nginx" {
osp, err := os.FindProcess(p.PID)
if err != nil {
glog.Errorf("unexpected error obtaining process information: %v", err)
continue
}
osp.Signal(syscall.SIGQUIT)
}
}
time.Sleep(100 * time.Millisecond)
}
// restart a new nginx master process if the controller
// is not being stopped
n.start(cmd, done)
}
}
// Stop gracefully stops the NGINX master process.
func (n *NGINXController) Stop() error {
n.isShuttingDown = true
n.controller.Stop()
// Send stop signal to Nginx
glog.Info("stopping NGINX process...")
cmd := exec.Command(n.binary, "-c", cfgPath, "-s", "quit")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Run()
if err != nil {
return err
}
// Wait for the Nginx process disappear
waitForNginxShutdown()
glog.Info("NGINX process has stopped")
return nil
}
func (n *NGINXController) start(cmd *exec.Cmd, done chan error) {
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Start(); err != nil {
glog.Fatalf("nginx error: %v", err)
done <- err
return
}
n.cmdArgs = cmd.Args
go func() {
done <- cmd.Wait()
}()
}
// BackendDefaults returns the nginx defaults
func (n NGINXController) BackendDefaults() defaults.Backend {
return n.backendDefaults
}
// printDiff returns the difference between the running configuration
// and the new one
func (n NGINXController) printDiff(data []byte) {
if !glog.V(2) {
return
}
in, err := os.Open(cfgPath)
if err != nil {
return
}
src, err := ioutil.ReadAll(in)
in.Close()
if err != nil {
return
}
if !bytes.Equal(src, data) {
tmpfile, err := ioutil.TempFile("", "nginx-cfg-diff")
if err != nil {
glog.Errorf("error creating temporal file: %s", err)
return
}
defer tmpfile.Close()
err = ioutil.WriteFile(tmpfile.Name(), data, 0644)
if err != nil {
return
}
diffOutput, err := diff(src, data)
if err != nil {
glog.Errorf("error computing diff: %s", err)
return
}
glog.Infof("NGINX configuration diff\n")
glog.Infof("%v", string(diffOutput))
os.Remove(tmpfile.Name())
}
}
// Info return build information
func (n NGINXController) Info() *ingress.BackendInfo {
return &ingress.BackendInfo{
Name: "NGINX",
Release: version.RELEASE,
Build: version.COMMIT,
Repository: version.REPO,
}
}
// DefaultEndpoint returns the default endpoint to be use as default server that returns 404.
func (n NGINXController) DefaultEndpoint() ingress.Endpoint {
return ingress.Endpoint{
Address: "127.0.0.1",
Port: fmt.Sprintf("%v", n.ports.Default),
Target: &apiv1.ObjectReference{},
}
}
// ConfigureFlags allow to configure more flags before the parsing of
// command line arguments
func (n *NGINXController) ConfigureFlags(flags *pflag.FlagSet) {
flags.BoolVar(&n.isSSLPassthroughEnabled, "enable-ssl-passthrough", false, `Enable SSL passthrough feature. Default is disabled`)
flags.IntVar(&n.ports.HTTP, "http-port", 80, `Indicates the port to use for HTTP traffic`)
flags.IntVar(&n.ports.HTTPS, "https-port", 443, `Indicates the port to use for HTTPS traffic`)
flags.IntVar(&n.ports.Status, "status-port", 18080, `Indicates the TCP port to use for exposing the nginx status page`)
flags.IntVar(&n.ports.SSLProxy, "ssl-passtrough-proxy-port", 442, `Default port to use internally for SSL when SSL Passthgough is enabled`)
flags.IntVar(&n.ports.Default, "default-server-port", 8181, `Default port to use for exposing the default server (catch all)`)
}
// OverrideFlags customize NGINX controller flags
func (n *NGINXController) OverrideFlags(flags *pflag.FlagSet) {
// we check port collisions
if !isPortAvailable(n.ports.HTTP) {
glog.Fatalf("Port %v is already in use. Please check the flag --http-port", n.ports.HTTP)
}
if !isPortAvailable(n.ports.HTTPS) {
glog.Fatalf("Port %v is already in use. Please check the flag --https-port", n.ports.HTTPS)
}
if !isPortAvailable(n.ports.Status) {
glog.Fatalf("Port %v is already in use. Please check the flag --status-port", n.ports.Status)
}
if !isPortAvailable(n.ports.Default) {
glog.Fatalf("Port %v is already in use. Please check the flag --default-server-port", n.ports.Default)
}
ic, _ := flags.GetString("ingress-class")
wc, _ := flags.GetString("watch-namespace")
if ic == "" {
ic = defIngressClass
}
if ic != defIngressClass {
glog.Warningf("only Ingress with class %v will be processed by this ingress controller", ic)
}
flags.Set("ingress-class", ic)
h, _ := flags.GetInt("healthz-port")
n.ports.Health = h
n.stats = newStatsCollector(wc, ic, n.binary, n.ports.Status)
if n.isSSLPassthroughEnabled {
if !isPortAvailable(n.ports.SSLProxy) {
glog.Fatalf("Port %v is already in use. Please check the flag --ssl-passtrough-proxy-port", n.ports.SSLProxy)
}
glog.Info("starting TLS proxy for SSL passthrough")
n.proxy = &proxy{
Default: &server{
Hostname: "localhost",
IP: "127.0.0.1",
Port: n.ports.SSLProxy,
ProxyProtocol: true,
},
}
listener, err := net.Listen("tcp", fmt.Sprintf(":%v", n.ports.HTTPS))
if err != nil {
glog.Fatalf("%v", err)
}
proxyList := &proxyproto.Listener{Listener: listener}
// start goroutine that accepts tcp connections in port 443
go func() {
for {
var conn net.Conn
var err error
if n.isProxyProtocolEnabled {
// we need to wrap the listener in order to decode
// proxy protocol before handling the connection
conn, err = proxyList.Accept()
} else {
conn, err = listener.Accept()
}
if err != nil {
glog.Warningf("unexpected error accepting tcp connection: %v", err)
continue
}
glog.V(3).Infof("remote address %s to local %s", conn.RemoteAddr(), conn.LocalAddr())
go n.proxy.Handle(conn)
}
}()
}
}
// DefaultIngressClass just return the default ingress class
func (n NGINXController) DefaultIngressClass() string {
return defIngressClass
}
// testTemplate checks if the NGINX configuration inside the byte array is valid
// running the command "nginx -t" using a temporal file.
func (n NGINXController) testTemplate(cfg []byte) error {
if len(cfg) == 0 {
return fmt.Errorf("invalid nginx configuration (empty)")
}
tmpfile, err := ioutil.TempFile("", "nginx-cfg")
if err != nil {
return err
}
defer tmpfile.Close()
err = ioutil.WriteFile(tmpfile.Name(), cfg, 0644)
if err != nil {
return err
}
out, err := exec.Command(n.binary, "-t", "-c", tmpfile.Name()).CombinedOutput()
if err != nil {
// this error is different from the rest because it must be clear why nginx is not working
oe := fmt.Sprintf(`
-------------------------------------------------------------------------------
Error: %v
%v
-------------------------------------------------------------------------------
`, err, string(out))
return errors.New(oe)
}
os.Remove(tmpfile.Name())
return nil
}
// SetConfig sets the configured configmap
func (n *NGINXController) SetConfig(cmap *apiv1.ConfigMap) {
n.configmap = cmap
n.isProxyProtocolEnabled = false
m := map[string]string{}
if cmap != nil {
m = cmap.Data
}
val, ok := m["use-proxy-protocol"]
if ok {
b, err := strconv.ParseBool(val)
if err == nil {
n.isProxyProtocolEnabled = b
}
}
n.backendDefaults = ngx_template.ReadConfig(m).Backend
}
// SetListers sets the configured store listers in the generic ingress controller
func (n *NGINXController) SetListers(lister *ingress.StoreLister) {
n.storeLister = lister
}
// UpdateIngressStatus custom Ingress status update
func (n *NGINXController) UpdateIngressStatus(*extensions.Ingress) []apiv1.LoadBalancerIngress {
return nil
}
// OnUpdate is called by syncQueue in https://github.com/kubernetes/ingress/blob/master/core/pkg/ingress/controller/controller.go#L426
// periodically to keep the configuration in sync.
//
// convert configmap to custom configuration object (different in each implementation)
// write the custom template (the complexity depends on the implementation)
// write the configuration file
// returning nill implies the backend will be reloaded.
// if an error is returned means requeue the update
func (n *NGINXController) OnUpdate(ingressCfg ingress.Configuration) error {
cfg := ngx_template.ReadConfig(n.configmap.Data)
cfg.Resolver = n.resolver
servers := []*server{}
for _, pb := range ingressCfg.PassthroughBackends {
svc := pb.Service
if svc == nil {
glog.Warningf("missing service for PassthroughBackends %v", pb.Backend)
continue
}
port, err := strconv.Atoi(pb.Port.String())
if err != nil {
for _, sp := range svc.Spec.Ports {
if sp.Name == pb.Port.String() {
port = int(sp.Port)
break
}
}
} else {
for _, sp := range svc.Spec.Ports {
if sp.Port == int32(port) {
port = int(sp.Port)
break
}
}
}
//TODO: Allow PassthroughBackends to specify they support proxy-protocol
servers = append(servers, &server{
Hostname: pb.Hostname,
IP: svc.Spec.ClusterIP,
Port: port,
ProxyProtocol: false,
})
}
if n.isSSLPassthroughEnabled {
n.proxy.ServerList = servers
}
// we need to check if the status module configuration changed
if cfg.EnableVtsStatus {
n.setupMonitor(vtsStatusModule)
} else {
n.setupMonitor(defaultStatusModule)
}
// NGINX cannot resize the hash tables used to store server names.
// For this reason we check if the defined size defined is correct
// for the FQDN defined in the ingress rules adjusting the value
// if is required.
// https://trac.nginx.org/nginx/ticket/352
// https://trac.nginx.org/nginx/ticket/631
var longestName int
var serverNameBytes int
redirectServers := make(map[string]string)
for _, srv := range ingressCfg.Servers {
if longestName < len(srv.Hostname) {
longestName = len(srv.Hostname)
}
serverNameBytes += len(srv.Hostname)
if srv.RedirectFromToWWW {
var n string
if strings.HasPrefix(srv.Hostname, "www.") {
n = strings.TrimLeft(srv.Hostname, "www.")
} else {
n = fmt.Sprintf("www.%v", srv.Hostname)
}
glog.V(3).Infof("creating redirect from %v to %v", srv.Hostname, n)
if _, ok := redirectServers[n]; !ok {
found := false
for _, esrv := range ingressCfg.Servers {
if esrv.Hostname == n {
found = true
break
}
}
if !found {
redirectServers[n] = srv.Hostname
}
}
}
}
if cfg.ServerNameHashBucketSize == 0 {
nameHashBucketSize := nginxHashBucketSize(longestName)
glog.V(3).Infof("adjusting ServerNameHashBucketSize variable to %v", nameHashBucketSize)
cfg.ServerNameHashBucketSize = nameHashBucketSize
}
serverNameHashMaxSize := nextPowerOf2(serverNameBytes)
if cfg.ServerNameHashMaxSize < serverNameHashMaxSize {
glog.V(3).Infof("adjusting ServerNameHashMaxSize variable to %v", serverNameHashMaxSize)
cfg.ServerNameHashMaxSize = serverNameHashMaxSize
}
// the limit of open files is per worker process
// and we leave some room to avoid consuming all the FDs available
wp, err := strconv.Atoi(cfg.WorkerProcesses)
glog.V(3).Infof("number of worker processes: %v", wp)
if err != nil {
wp = 1
}
maxOpenFiles := (sysctlFSFileMax() / wp) - 1024
glog.V(3).Infof("maximum number of open file descriptors : %v", sysctlFSFileMax())
if maxOpenFiles < 1024 {
// this means the value of RLIMIT_NOFILE is too low.
maxOpenFiles = 1024
}
setHeaders := map[string]string{}
if cfg.ProxySetHeaders != "" {
cmap, exists, err := n.storeLister.ConfigMap.GetByKey(cfg.ProxySetHeaders)
if err != nil {
glog.Warningf("unexpected error reading configmap %v: %v", cfg.ProxySetHeaders, err)
}
if exists {
setHeaders = cmap.(*apiv1.ConfigMap).Data
}
}
addHeaders := map[string]string{}
if cfg.AddHeaders != "" {
cmap, exists, err := n.storeLister.ConfigMap.GetByKey(cfg.AddHeaders)
if err != nil {
glog.Warningf("unexpected error reading configmap %v: %v", cfg.AddHeaders, err)
}
if exists {
addHeaders = cmap.(*apiv1.ConfigMap).Data
}
}
sslDHParam := ""
if cfg.SSLDHParam != "" {
secretName := cfg.SSLDHParam
s, exists, err := n.storeLister.Secret.GetByKey(secretName)
if err != nil {
glog.Warningf("unexpected error reading secret %v: %v", secretName, err)
}
if exists {
secret := s.(*apiv1.Secret)
nsSecName := strings.Replace(secretName, "/", "-", -1)
dh, ok := secret.Data["dhparam.pem"]
if ok {
pemFileName, err := ssl.AddOrUpdateDHParam(nsSecName, dh)
if err != nil {
glog.Warningf("unexpected error adding or updating dhparam %v file: %v", nsSecName, err)
} else {
sslDHParam = pemFileName
}
}
}
}
cfg.SSLDHParam = sslDHParam
tc := config.TemplateConfig{
ProxySetHeaders: setHeaders,
AddHeaders: addHeaders,
MaxOpenFiles: maxOpenFiles,
BacklogSize: sysctlSomaxconn(),
Backends: ingressCfg.Backends,
PassthroughBackends: ingressCfg.PassthroughBackends,
Servers: ingressCfg.Servers,
TCPBackends: ingressCfg.TCPEndpoints,
UDPBackends: ingressCfg.UDPEndpoints,
HealthzURI: ngxHealthPath,
CustomErrors: len(cfg.CustomHTTPErrors) > 0,
Cfg: cfg,
IsIPV6Enabled: n.isIPV6Enabled && !cfg.DisableIpv6,
RedirectServers: redirectServers,
IsSSLPassthroughEnabled: n.isSSLPassthroughEnabled,
ListenPorts: n.ports,
PublishService: n.controller.GetPublishService(),
}
content, err := n.t.Write(tc)
if err != nil {
return err
}
err = n.testTemplate(content)
if err != nil {
return err
}
n.printDiff(content)
err = ioutil.WriteFile(cfgPath, content, 0644)
if err != nil {
return err
}
o, err := exec.Command(n.binary, "-s", "reload", "-c", cfgPath).CombinedOutput()
if err != nil {
return fmt.Errorf("%v\n%v", err, string(o))
}
return nil
}
// nginxHashBucketSize computes the correct nginx hash_bucket_size for a hash with the given longest key
func nginxHashBucketSize(longestString int) int {
// See https://github.com/kubernetes/ingress/issues/623 for an explanation
wordSize := 8 // Assume 64 bit CPU
n := longestString + 2
aligned := (n + wordSize - 1) & ^(wordSize - 1)
rawSize := wordSize + wordSize + aligned
return nextPowerOf2(rawSize)
}
// Name returns the healthcheck name
func (n NGINXController) Name() string {
return "Ingress Controller"
}
// Check returns if the nginx healthz endpoint is returning ok (status code 200)
func (n NGINXController) Check(_ *http.Request) error {
res, err := http.Get(fmt.Sprintf("http://localhost:%v%v", n.ports.Status, ngxHealthPath))
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode != 200 {
return fmt.Errorf("ingress controller is not healthy")
}
// check the nginx master process is running
fs, err := proc.NewFS("/proc")
if err != nil {
glog.Errorf("%v", err)
return err
}
f, err := ioutil.ReadFile("/run/nginx.pid")
if err != nil {
glog.Errorf("%v", err)
return err
}
pid, err := strconv.Atoi(strings.TrimRight(string(f), "\r\n"))
if err != nil {
return err
}
_, err = fs.NewProc(int(pid))
if err != nil {
glog.Errorf("%v", err)
return err
}
return nil
}
// http://graphics.stanford.edu/~seander/bithacks.html#RoundUpPowerOf2
// https://play.golang.org/p/TVSyCcdxUh
func nextPowerOf2(v int) int {
v--
v |= v >> 1
v |= v >> 2
v |= v >> 4
v |= v >> 8
v |= v >> 16
v++
return v
}
func isIPv6Enabled() bool {
cmd := exec.Command("test", "-f", "/proc/net/if_inet6")
return cmd.Run() == nil
}
// isNginxRunning returns true if a process with the name 'nginx' is found
func isNginxProcessPresent() bool {
processes, _ := ps.Processes()
for _, p := range processes {
if p.Executable() == "nginx" {
return true
}
}
return false
}
func waitForNginxShutdown() {
timer := time.NewTicker(time.Second * 1)
defer timer.Stop()
for {
select {
case <-timer.C:
if !isNginxProcessPresent() {
return
}
}
}
}

View 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 "testing"
func TestNginxHashBucketSize(t *testing.T) {
tests := []struct {
n int
expected int
}{
{0, 32},
{1, 32},
{2, 32},
{3, 32},
// ...
{13, 32},
{14, 32},
{15, 64},
{16, 64},
// ...
{45, 64},
{46, 64},
{47, 128},
{48, 128},
// ...
// ...
{109, 128},
{110, 128},
{111, 256},
{112, 256},
// ...
{237, 256},
{238, 256},
{239, 512},
{240, 512},
}
for _, test := range tests {
actual := nginxHashBucketSize(test.n)
if actual != test.expected {
t.Errorf("Test nginxHashBucketSize(%d): expected %d but returned %d", test.n, test.expected, actual)
}
}
}

110
pkg/cmd/controller/tcp.go Normal file
View file

@ -0,0 +1,110 @@
package main
import (
"fmt"
"io"
"net"
"github.com/golang/glog"
"github.com/paultag/sniff/parser"
)
type server struct {
Hostname string
IP string
Port int
ProxyProtocol bool
}
type proxy struct {
ServerList []*server
Default *server
}
func (p *proxy) Get(host string) *server {
if p.ServerList == nil {
return p.Default
}
for _, s := range p.ServerList {
if s.Hostname == host {
return s
}
}
return p.Default
}
func (p *proxy) Handle(conn net.Conn) {
defer conn.Close()
data := make([]byte, 4096)
length, err := conn.Read(data)
if err != nil {
glog.V(4).Infof("error reading the first 4k of the connection: %s", err)
return
}
proxy := p.Default
hostname, err := parser.GetHostname(data[:])
if err == nil {
glog.V(4).Infof("parsed hostname from TLS Client Hello: %s", hostname)
proxy = p.Get(hostname)
}
if proxy == nil {
glog.V(4).Infof("there is no configured proxy for SSL connections")
return
}
clientConn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", proxy.IP, proxy.Port))
if err != nil {
return
}
defer clientConn.Close()
if proxy.ProxyProtocol {
//Write out the proxy-protocol header
localAddr := conn.LocalAddr().(*net.TCPAddr)
remoteAddr := conn.RemoteAddr().(*net.TCPAddr)
protocol := "UNKNOWN"
if remoteAddr.IP.To4() != nil {
protocol = "TCP4"
} else if remoteAddr.IP.To16() != nil {
protocol = "TCP6"
}
proxyProtocolHeader := fmt.Sprintf("PROXY %s %s %s %d %d\r\n", protocol, remoteAddr.IP.String(), localAddr.IP.String(), remoteAddr.Port, localAddr.Port)
glog.V(4).Infof("Writing proxy protocol header - %s", proxyProtocolHeader)
_, err = fmt.Fprintf(clientConn, proxyProtocolHeader)
}
if err != nil {
glog.Errorf("unexpected error writing proxy-protocol header: %s", err)
clientConn.Close()
} else {
_, err = clientConn.Write(data[:length])
if err != nil {
glog.Errorf("unexpected error writing first 4k of proxy data: %s", err)
clientConn.Close()
}
}
pipe(clientConn, conn)
}
func pipe(client, server net.Conn) {
doCopy := func(s, c net.Conn, cancel chan<- bool) {
io.Copy(s, c)
cancel <- true
}
cancel := make(chan bool, 2)
go doCopy(server, client, cancel)
go doCopy(client, server, cancel)
select {
case <-cancel:
return
}
}

View file

@ -0,0 +1,87 @@
/*
Copyright 2015 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 (
"fmt"
"io/ioutil"
"net"
"os"
"os/exec"
"syscall"
"github.com/golang/glog"
"k8s.io/kubernetes/pkg/util/sysctl"
)
// sysctlSomaxconn returns the value of net.core.somaxconn, i.e.
// maximum number of connections that can be queued for acceptance
// http://nginx.org/en/docs/http/ngx_http_core_module.html#listen
func sysctlSomaxconn() int {
maxConns, err := sysctl.New().GetSysctl("net/core/somaxconn")
if err != nil || maxConns < 512 {
glog.V(3).Infof("system net.core.somaxconn=%v (using system default)", maxConns)
return 511
}
return maxConns
}
// sysctlFSFileMax returns the value of fs.file-max, i.e.
// maximum number of open file descriptors
func sysctlFSFileMax() int {
var rLimit syscall.Rlimit
err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rLimit)
if err != nil {
glog.Errorf("unexpected error reading system maximum number of open file descriptors (RLIMIT_NOFILE): %v", err)
// returning 0 means don't render the value
return 0
}
return int(rLimit.Max)
}
func diff(b1, b2 []byte) ([]byte, error) {
f1, err := ioutil.TempFile("", "a")
if err != nil {
return nil, err
}
defer f1.Close()
defer os.Remove(f1.Name())
f2, err := ioutil.TempFile("", "b")
if err != nil {
return nil, err
}
defer f2.Close()
defer os.Remove(f2.Name())
f1.Write(b1)
f2.Write(b2)
out, _ := exec.Command("diff", "-u", f1.Name(), f2.Name()).CombinedOutput()
return out, nil
}
func isPortAvailable(p int) bool {
ln, err := net.Listen("tcp", fmt.Sprintf(":%v", p))
if err != nil {
return false
}
ln.Close()
return true
}

View file

@ -0,0 +1,41 @@
/*
Copyright 2015 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 "testing"
func TestDiff(t *testing.T) {
tests := []struct {
a []byte
b []byte
empty bool
}{
{[]byte(""), []byte(""), true},
{[]byte("a"), []byte("a"), true},
{[]byte("a"), []byte("b"), false},
}
for _, test := range tests {
b, err := diff(test.a, test.b)
if err != nil {
t.Fatalf("unexpected error returned: %v", err)
}
if len(b) == 0 && !test.empty {
t.Fatalf("expected empty but returned %s", b)
}
}
}

529
pkg/config/config.go Normal file
View file

@ -0,0 +1,529 @@
/*
Copyright 2016 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 config
import (
"fmt"
"runtime"
"strconv"
"github.com/golang/glog"
apiv1 "k8s.io/api/core/v1"
"k8s.io/ingress/core/pkg/ingress"
"k8s.io/ingress/core/pkg/ingress/defaults"
)
const (
// http://nginx.org/en/docs/http/ngx_http_core_module.html#client_max_body_size
// Sets the maximum allowed size of the client request body
bodySize = "1m"
// http://nginx.org/en/docs/ngx_core_module.html#error_log
// Configures logging level [debug | info | notice | warn | error | crit | alert | emerg]
// Log levels above are listed in the order of increasing severity
errorLevel = "notice"
// HTTP Strict Transport Security (often abbreviated as HSTS) is a security feature (HTTP header)
// that tell browsers that it should only be communicated with using HTTPS, instead of using HTTP.
// https://developer.mozilla.org/en-US/docs/Web/Security/HTTP_strict_transport_security
// max-age is the time, in seconds, that the browser should remember that this site is only to be accessed using HTTPS.
hstsMaxAge = "15724800"
gzipTypes = "application/atom+xml application/javascript application/x-javascript application/json application/rss+xml application/vnd.ms-fontobject application/x-font-ttf application/x-web-app-manifest+json application/xhtml+xml application/xml font/opentype image/svg+xml image/x-icon text/css text/plain text/x-component"
logFormatUpstream = `%v - [$the_real_ip] - $remote_user [$time_local] "$request" $status $body_bytes_sent "$http_referer" "$http_user_agent" $request_length $request_time [$proxy_upstream_name] $upstream_addr $upstream_response_length $upstream_response_time $upstream_status`
logFormatStream = `[$time_local] $protocol $status $bytes_sent $bytes_received $session_time`
// http://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_buffer_size
// Sets the size of the buffer used for sending data.
// 4k helps NGINX to improve TLS Time To First Byte (TTTFB)
// https://www.igvita.com/2013/12/16/optimizing-nginx-tls-time-to-first-byte/
sslBufferSize = "4k"
// Enabled ciphers list to enabled. The ciphers are specified in the format understood by the OpenSSL library
// http://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_ciphers
sslCiphers = "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:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA"
// SSL enabled protocols to use
// http://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_protocols
sslProtocols = "TLSv1 TLSv1.1 TLSv1.2"
// Time during which a client may reuse the session parameters stored in a cache.
// http://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_session_timeout
sslSessionTimeout = "10m"
// Size of the SSL shared cache between all worker processes.
// http://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_session_cache
sslSessionCacheSize = "10m"
// Default setting for load balancer algorithm
defaultLoadBalancerAlgorithm = "least_conn"
// Parameters for a shared memory zone that will keep states for various keys.
// http://nginx.org/en/docs/http/ngx_http_limit_conn_module.html#limit_conn_zone
defaultLimitConnZoneVariable = "$binary_remote_addr"
)
// Configuration represents the content of nginx.conf file
type Configuration struct {
defaults.Backend `json:",squash"`
// Sets the name of the configmap that contains the headers to pass to the client
AddHeaders string `json:"add-headers,omitempty"`
// AllowBackendServerHeader enables the return of the header Server from the backend
// instead of the generic nginx string.
// http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_hide_header
// By default this is disabled
AllowBackendServerHeader bool `json:"allow-backend-server-header"`
// AccessLogPath sets the path of the access logs if enabled
// http://nginx.org/en/docs/http/ngx_http_log_module.html#access_log
// By default access logs go to /var/log/nginx/access.log
AccessLogPath string `json:"access-log-path,omitempty"`
// ErrorLogPath sets the path of the error logs
// http://nginx.org/en/docs/ngx_core_module.html#error_log
// By default error logs go to /var/log/nginx/error.log
ErrorLogPath string `json:"error-log-path,omitempty"`
// EnableDynamicTLSRecords enables dynamic TLS record sizes
// https://blog.cloudflare.com/optimizing-tls-over-tcp-to-reduce-latency
// By default this is enabled
EnableDynamicTLSRecords bool `json:"enable-dynamic-tls-records"`
// ClientHeaderBufferSize allows to configure a custom buffer
// size for reading client request header
// http://nginx.org/en/docs/http/ngx_http_core_module.html#client_header_buffer_size
ClientHeaderBufferSize string `json:"client-header-buffer-size"`
// Defines a timeout for reading client request header, in seconds
// http://nginx.org/en/docs/http/ngx_http_core_module.html#client_header_timeout
ClientHeaderTimeout int `json:"client-header-timeout,omitempty"`
// Sets buffer size for reading client request body
// http://nginx.org/en/docs/http/ngx_http_core_module.html#client_body_buffer_size
ClientBodyBufferSize string `json:"client-body-buffer-size,omitempty"`
// Defines a timeout for reading client request body, in seconds
// http://nginx.org/en/docs/http/ngx_http_core_module.html#client_body_timeout
ClientBodyTimeout int `json:"client-body-timeout,omitempty"`
// DisableAccessLog disables the Access Log globally from NGINX ingress controller
//http://nginx.org/en/docs/http/ngx_http_log_module.html
DisableAccessLog bool `json:"disable-access-log,omitempty"`
// DisableIpv6 disable listening on ipv6 address
DisableIpv6 bool `json:"disable-ipv6,omitempty"`
// EnableUnderscoresInHeaders enables underscores in header names
// http://nginx.org/en/docs/http/ngx_http_core_module.html#underscores_in_headers
// By default this is disabled
EnableUnderscoresInHeaders bool `json:"enable-underscores-in-headers"`
// IgnoreInvalidHeaders set if header fields with invalid names should be ignored
// http://nginx.org/en/docs/http/ngx_http_core_module.html#ignore_invalid_headers
// By default this is enabled
IgnoreInvalidHeaders bool `json:"ignore-invalid-headers"`
// EnableVtsStatus allows the replacement of the default status page with a third party module named
// nginx-module-vts - https://github.com/vozlt/nginx-module-vts
// By default this is disabled
EnableVtsStatus bool `json:"enable-vts-status,omitempty"`
// Vts config on http level
// Description: Sets parameters for a shared memory zone that will keep states for various keys. The cache is shared between all worker processe
// https://github.com/vozlt/nginx-module-vts#vhost_traffic_status_zone
// Default value is 10m
VtsStatusZoneSize string `json:"vts-status-zone-size,omitempty"`
// Vts config on http level
// Description: Enables the keys by user defined variable. The key is a key string to calculate traffic.
// The name is a group string to calculate traffic. The key and name can contain variables such as $host,
// $server_name. The name's group belongs to filterZones if specified. The key's group belongs to serverZones
// if not specified second argument name. The example with geoip module is as follows:
// https://github.com/vozlt/nginx-module-vts#vhost_traffic_status_filter_by_set_key
// Default value is $geoip_country_code country::*
VtsDefaultFilterKey string `json:"vts-default-filter-key,omitempty"`
// RetryNonIdempotent since 1.9.13 NGINX will not retry non-idempotent requests (POST, LOCK, PATCH)
// in case of an error. The previous behavior can be restored using the value true
RetryNonIdempotent bool `json:"retry-non-idempotent"`
// http://nginx.org/en/docs/ngx_core_module.html#error_log
// Configures logging level [debug | info | notice | warn | error | crit | alert | emerg]
// Log levels above are listed in the order of increasing severity
ErrorLogLevel string `json:"error-log-level,omitempty"`
// https://nginx.org/en/docs/http/ngx_http_v2_module.html#http2_max_field_size
// HTTP2MaxFieldSize Limits the maximum size of an HPACK-compressed request header field
HTTP2MaxFieldSize string `json:"http2-max-field-size,omitempty"`
// https://nginx.org/en/docs/http/ngx_http_v2_module.html#http2_max_header_size
// HTTP2MaxHeaderSize Limits the maximum size of the entire request header list after HPACK decompression
HTTP2MaxHeaderSize string `json:"http2-max-header-size,omitempty"`
// Enables or disables the header HSTS in servers running SSL
HSTS bool `json:"hsts,omitempty"`
// Enables or disables the use of HSTS in all the subdomains of the servername
// Default: true
HSTSIncludeSubdomains bool `json:"hsts-include-subdomains,omitempty"`
// HTTP Strict Transport Security (often abbreviated as HSTS) is a security feature (HTTP header)
// that tell browsers that it should only be communicated with using HTTPS, instead of using HTTP.
// https://developer.mozilla.org/en-US/docs/Web/Security/HTTP_strict_transport_security
// max-age is the time, in seconds, that the browser should remember that this site is only to be
// accessed using HTTPS.
HSTSMaxAge string `json:"hsts-max-age,omitempty"`
// Enables or disables the preload attribute in HSTS feature
HSTSPreload bool `json:"hsts-preload,omitempty"`
// Time during which a keep-alive client connection will stay open on the server side.
// The zero value disables keep-alive client connections
// http://nginx.org/en/docs/http/ngx_http_core_module.html#keepalive_timeout
KeepAlive int `json:"keep-alive,omitempty"`
// Sets the maximum number of requests that can be served through one keep-alive connection.
// http://nginx.org/en/docs/http/ngx_http_core_module.html#keepalive_requests
KeepAliveRequests int `json:"keep-alive-requests,omitempty"`
// LargeClientHeaderBuffers Sets the maximum number and size of buffers used for reading
// large client request header.
// http://nginx.org/en/docs/http/ngx_http_core_module.html#large_client_header_buffers
// Default: 4 8k
LargeClientHeaderBuffers string `json:"large-client-header-buffers"`
// Enable json escaping
// http://nginx.org/en/docs/http/ngx_http_log_module.html#log_format
LogFormatEscapeJSON bool `json:"log-format-escape-json,omitempty"`
// Customize upstream log_format
// http://nginx.org/en/docs/http/ngx_http_log_module.html#log_format
LogFormatUpstream string `json:"log-format-upstream,omitempty"`
// Customize stream log_format
// http://nginx.org/en/docs/http/ngx_http_log_module.html#log_format
LogFormatStream string `json:"log-format-stream,omitempty"`
// Maximum number of simultaneous connections that can be opened by each worker process
// http://nginx.org/en/docs/ngx_core_module.html#worker_connections
MaxWorkerConnections int `json:"max-worker-connections,omitempty"`
// Sets the bucket size for the map variables hash tables.
// Default value depends on the processors cache line size.
// http://nginx.org/en/docs/http/ngx_http_map_module.html#map_hash_bucket_size
MapHashBucketSize int `json:"map-hash-bucket-size,omitempty"`
// If UseProxyProtocol is enabled ProxyRealIPCIDR defines the default the IP/network address
// of your external load balancer
ProxyRealIPCIDR []string `json:"proxy-real-ip-cidr,omitempty"`
// Sets the name of the configmap that contains the headers to pass to the backend
ProxySetHeaders string `json:"proxy-set-headers,omitempty"`
// Maximum size of the server names hash tables used in server names, map directives values,
// MIME types, names of request header strings, etcd.
// http://nginx.org/en/docs/hash.html
// http://nginx.org/en/docs/http/ngx_http_core_module.html#server_names_hash_max_size
ServerNameHashMaxSize int `json:"server-name-hash-max-size,omitempty"`
// Size of the bucket for the server names hash tables
// http://nginx.org/en/docs/hash.html
// http://nginx.org/en/docs/http/ngx_http_core_module.html#server_names_hash_bucket_size
ServerNameHashBucketSize int `json:"server-name-hash-bucket-size,omitempty"`
// Size of the bucket for the proxy headers hash tables
// http://nginx.org/en/docs/hash.html
// https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_headers_hash_max_size
ProxyHeadersHashMaxSize int `json:"proxy-headers-hash-max-size,omitempty"`
// Maximum size of the bucket for the proxy headers hash tables
// http://nginx.org/en/docs/hash.html
// https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_headers_hash_bucket_size
ProxyHeadersHashBucketSize int `json:"proxy-headers-hash-bucket-size,omitempty"`
// Enables or disables emitting nginx version in error messages and in the “Server” response header field.
// http://nginx.org/en/docs/http/ngx_http_core_module.html#server_tokens
// Default: true
ShowServerTokens bool `json:"server-tokens"`
// Enabled ciphers list to enabled. The ciphers are specified in the format understood by
// the OpenSSL library
// http://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_ciphers
SSLCiphers string `json:"ssl-ciphers,omitempty"`
// Specifies a curve for ECDHE ciphers.
// http://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_ecdh_curve
SSLECDHCurve string `json:"ssl-ecdh-curve,omitempty"`
// The secret that contains Diffie-Hellman key to help with "Perfect Forward Secrecy"
// https://www.openssl.org/docs/manmaster/apps/dhparam.html
// https://wiki.mozilla.org/Security/Server_Side_TLS#DHE_handshake_and_dhparam
// http://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_dhparam
SSLDHParam string `json:"ssl-dh-param,omitempty"`
// SSL enabled protocols to use
// http://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_protocols
SSLProtocols string `json:"ssl-protocols,omitempty"`
// Enables or disables the use of shared SSL cache among worker processes.
// http://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_session_cache
SSLSessionCache bool `json:"ssl-session-cache,omitempty"`
// Size of the SSL shared cache between all worker processes.
// http://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_session_cache
SSLSessionCacheSize string `json:"ssl-session-cache-size,omitempty"`
// Enables or disables session resumption through TLS session tickets.
// http://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_session_tickets
SSLSessionTickets bool `json:"ssl-session-tickets,omitempty"`
// Time during which a client may reuse the session parameters stored in a cache.
// http://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_session_timeout
SSLSessionTimeout string `json:"ssl-session-timeout,omitempty"`
// http://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_buffer_size
// Sets the size of the buffer used for sending data.
// 4k helps NGINX to improve TLS Time To First Byte (TTTFB)
// https://www.igvita.com/2013/12/16/optimizing-nginx-tls-time-to-first-byte/
SSLBufferSize string `json:"ssl-buffer-size,omitempty"`
// Enables or disables the use of the PROXY protocol to receive client connection
// (real IP address) information passed through proxy servers and load balancers
// such as HAproxy and Amazon Elastic Load Balancer (ELB).
// https://www.nginx.com/resources/admin-guide/proxy-protocol/
UseProxyProtocol bool `json:"use-proxy-protocol,omitempty"`
// Enables or disables the use of the nginx module that compresses responses using the "gzip" method
// http://nginx.org/en/docs/http/ngx_http_gzip_module.html
UseGzip bool `json:"use-gzip,omitempty"`
// Enables or disables the HTTP/2 support in secure connections
// http://nginx.org/en/docs/http/ngx_http_v2_module.html
// Default: true
UseHTTP2 bool `json:"use-http2,omitempty"`
// MIME types in addition to "text/html" to compress. The special value “*” matches any MIME type.
// Responses with the “text/html” type are always compressed if UseGzip is enabled
GzipTypes string `json:"gzip-types,omitempty"`
// Defines the number of worker processes. By default auto means number of available CPU cores
// http://nginx.org/en/docs/ngx_core_module.html#worker_processes
WorkerProcesses string `json:"worker-processes,omitempty"`
// Defines a timeout for a graceful shutdown of worker processes
// http://nginx.org/en/docs/ngx_core_module.html#worker_shutdown_timeout
WorkerShutdownTimeout string `json:"worker-shutdown-timeout,omitempty"`
// Defines the load balancing algorithm to use. The deault is round-robin
LoadBalanceAlgorithm string `json:"load-balance,omitempty"`
// Sets the bucket size for the variables hash table.
// http://nginx.org/en/docs/http/ngx_http_map_module.html#variables_hash_bucket_size
VariablesHashBucketSize int `json:"variables-hash-bucket-size,omitempty"`
// Sets the maximum size of the variables hash table.
// http://nginx.org/en/docs/http/ngx_http_map_module.html#variables_hash_max_size
VariablesHashMaxSize int `json:"variables-hash-max-size,omitempty"`
// Activates the cache for connections to upstream servers.
// The connections parameter sets the maximum number of idle keepalive connections to
// upstream servers that are preserved in the cache of each worker process. When this
// number is exceeded, the least recently used connections are closed.
// http://nginx.org/en/docs/http/ngx_http_upstream_module.html#keepalive
// Default: 32
UpstreamKeepaliveConnections int `json:"upstream-keepalive-connections,omitempty"`
// Sets the maximum size of the variables hash table.
// http://nginx.org/en/docs/http/ngx_http_map_module.html#variables_hash_max_size
LimitConnZoneVariable string `json:"limit-conn-zone-variable,omitempty"`
// Sets the timeout between two successive read or write operations on client or proxied server connections.
// If no data is transmitted within this time, the connection is closed.
// http://nginx.org/en/docs/stream/ngx_stream_proxy_module.html#proxy_timeout
ProxyStreamTimeout string `json:"proxy-stream-timeout,omitempty"`
// Sets the ipv4 addresses on which the server will accept requests.
BindAddressIpv4 []string `json:"bind-address-ipv4,omitempty"`
// Sets the ipv6 addresses on which the server will accept requests.
BindAddressIpv6 []string `json:"bind-address-ipv6,omitempty"`
// Sets the header field for identifying the originating IP address of a client
// Default is X-Forwarded-For
ForwardedForHeader string `json:"forwarded-for-header,omitempty"`
// EnableOpentracing enables the nginx Opentracing extension
// https://github.com/rnburn/nginx-opentracing
// By default this is disabled
EnableOpentracing bool `json:"enable-opentracing"`
// ZipkinCollectorHost specifies the host to use when uploading traces
ZipkinCollectorHost string `json:"zipkin-collector-host"`
// ZipkinCollectorPort specifies the port to use when uploading traces
ZipkinCollectorPort int `json:"zipkin-collector-port"`
// ZipkinServiceName specifies the service name to use for any traces created
// Default: nginx
ZipkinServiceName string `json:"zipkin-service-name"`
// HTTPSnippet adds custom configuration to the http section of the nginx configuration
HTTPSnippet string `json:"http-snippet"`
// ServerSnippet adds custom configuration to all the servers in the nginx configuration
ServerSnippet string `json:"server-snippet"`
// LocationSnippet adds custom configuration to all the locations in the nginx configuration
LocationSnippet string `json:"location-snippet"`
}
// NewDefault returns the default nginx configuration
func NewDefault() Configuration {
defIPCIDR := make([]string, 0)
defIPCIDR = append(defIPCIDR, "0.0.0.0/0")
defBindAddress := make([]string, 0)
cfg := Configuration{
AllowBackendServerHeader: false,
AccessLogPath: "/var/log/nginx/access.log",
ErrorLogPath: "/var/log/nginx/error.log",
ClientHeaderBufferSize: "1k",
ClientHeaderTimeout: 60,
ClientBodyBufferSize: "8k",
ClientBodyTimeout: 60,
EnableDynamicTLSRecords: true,
EnableUnderscoresInHeaders: false,
ErrorLogLevel: errorLevel,
ForwardedForHeader: "X-Forwarded-For",
HTTP2MaxFieldSize: "4k",
HTTP2MaxHeaderSize: "16k",
HSTS: true,
HSTSIncludeSubdomains: true,
HSTSMaxAge: hstsMaxAge,
HSTSPreload: false,
IgnoreInvalidHeaders: true,
GzipTypes: gzipTypes,
KeepAlive: 75,
KeepAliveRequests: 100,
LargeClientHeaderBuffers: "4 8k",
LogFormatEscapeJSON: false,
LogFormatStream: logFormatStream,
LogFormatUpstream: logFormatUpstream,
MaxWorkerConnections: 16384,
MapHashBucketSize: 64,
ProxyRealIPCIDR: defIPCIDR,
ServerNameHashMaxSize: 1024,
ProxyHeadersHashMaxSize: 512,
ProxyHeadersHashBucketSize: 64,
ShowServerTokens: true,
SSLBufferSize: sslBufferSize,
SSLCiphers: sslCiphers,
SSLECDHCurve: "auto",
SSLProtocols: sslProtocols,
SSLSessionCache: true,
SSLSessionCacheSize: sslSessionCacheSize,
SSLSessionTickets: true,
SSLSessionTimeout: sslSessionTimeout,
UseGzip: true,
WorkerProcesses: strconv.Itoa(runtime.NumCPU()),
WorkerShutdownTimeout: "10s",
LoadBalanceAlgorithm: defaultLoadBalancerAlgorithm,
VtsStatusZoneSize: "10m",
VtsDefaultFilterKey: "$geoip_country_code country::*",
VariablesHashBucketSize: 64,
VariablesHashMaxSize: 2048,
UseHTTP2: true,
ProxyStreamTimeout: "600s",
Backend: defaults.Backend{
ProxyBodySize: bodySize,
ProxyConnectTimeout: 5,
ProxyReadTimeout: 60,
ProxySendTimeout: 60,
ProxyBufferSize: "4k",
ProxyCookieDomain: "off",
ProxyCookiePath: "off",
ProxyNextUpstream: "error timeout invalid_header http_502 http_503 http_504",
ProxyRequestBuffering: "on",
SSLRedirect: true,
CustomHTTPErrors: []int{},
WhitelistSourceRange: []string{},
SkipAccessLogURLs: []string{},
LimitRate: 0,
LimitRateAfter: 0,
},
UpstreamKeepaliveConnections: 32,
LimitConnZoneVariable: defaultLimitConnZoneVariable,
BindAddressIpv4: defBindAddress,
BindAddressIpv6: defBindAddress,
ZipkinCollectorPort: 9411,
ZipkinServiceName: "nginx",
}
if glog.V(5) {
cfg.ErrorLogLevel = "debug"
}
return cfg
}
// BuildLogFormatUpstream format the log_format upstream using
// proxy_protocol_addr as remote client address if UseProxyProtocol
// is enabled.
func (cfg Configuration) BuildLogFormatUpstream() string {
if cfg.LogFormatUpstream == logFormatUpstream {
return fmt.Sprintf(cfg.LogFormatUpstream, "$the_real_ip")
}
return cfg.LogFormatUpstream
}
// TemplateConfig contains the nginx configuration to render the file nginx.conf
type TemplateConfig struct {
ProxySetHeaders map[string]string
AddHeaders map[string]string
MaxOpenFiles int
BacklogSize int
Backends []*ingress.Backend
PassthroughBackends []*ingress.SSLPassthroughBackend
Servers []*ingress.Server
TCPBackends []ingress.L4Service
UDPBackends []ingress.L4Service
HealthzURI string
CustomErrors bool
Cfg Configuration
IsIPV6Enabled bool
IsSSLPassthroughEnabled bool
RedirectServers map[string]string
ListenPorts *ListenPorts
PublishService *apiv1.Service
}
// ListenPorts describe the ports required to run the
// NGINX Ingress controller
type ListenPorts struct {
HTTP int
HTTPS int
Status int
Health int
Default int
SSLProxy int
}

46
pkg/config/config_test.go Normal file
View 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 config
import (
"fmt"
"testing"
)
func TestBuildLogFormatUpstream(t *testing.T) {
testCases := []struct {
useProxyProtocol bool // use proxy protocol
curLogFormat string
expected string
}{
{true, logFormatUpstream, fmt.Sprintf(logFormatUpstream, "$the_real_ip")},
{false, logFormatUpstream, fmt.Sprintf(logFormatUpstream, "$the_real_ip")},
{true, "my-log-format", "my-log-format"},
{false, "john-log-format", "john-log-format"},
}
for _, testCase := range testCases {
cfg := NewDefault()
cfg.UseProxyProtocol = testCase.useProxyProtocol
cfg.LogFormatUpstream = testCase.curLogFormat
result := cfg.BuildLogFormatUpstream()
if result != testCase.expected {
t.Errorf(" expected %v but return %v", testCase.expected, result)
}
}
}

View file

@ -0,0 +1,151 @@
/*
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 collector
import (
"github.com/golang/glog"
"github.com/prometheus/client_golang/prometheus"
)
type (
nginxStatusCollector struct {
scrapeChan chan scrapeRequest
ngxHealthPort int
ngxVtsPath string
data *nginxStatusData
watchNamespace string
ingressClass string
}
nginxStatusData struct {
active *prometheus.Desc
accepted *prometheus.Desc
handled *prometheus.Desc
requests *prometheus.Desc
reading *prometheus.Desc
writing *prometheus.Desc
waiting *prometheus.Desc
}
)
// NewNginxStatus returns a new prometheus collector the default nginx status module
func NewNginxStatus(watchNamespace, ingressClass string, ngxHealthPort int, ngxVtsPath string) Stopable {
p := nginxStatusCollector{
scrapeChan: make(chan scrapeRequest),
ngxHealthPort: ngxHealthPort,
ngxVtsPath: ngxVtsPath,
watchNamespace: watchNamespace,
ingressClass: ingressClass,
}
p.data = &nginxStatusData{
active: prometheus.NewDesc(
prometheus.BuildFQName(ns, "", "active_connections"),
"total number of active connections",
[]string{"ingress_class", "namespace"}, nil),
accepted: prometheus.NewDesc(
prometheus.BuildFQName(ns, "", "accepted_connections"),
"total number of accepted client connections",
[]string{"ingress_class", "namespace"}, nil),
handled: prometheus.NewDesc(
prometheus.BuildFQName(ns, "", "handled_connections"),
"total number of handled connections",
[]string{"ingress_class", "namespace"}, nil),
requests: prometheus.NewDesc(
prometheus.BuildFQName(ns, "", "total_requests"),
"total number of client requests",
[]string{"ingress_class", "namespace"}, nil),
reading: prometheus.NewDesc(
prometheus.BuildFQName(ns, "", "current_reading_connections"),
"current number of connections where nginx is reading the request header",
[]string{"ingress_class", "namespace"}, nil),
writing: prometheus.NewDesc(
prometheus.BuildFQName(ns, "", "current_writing_connections"),
"current number of connections where nginx is writing the response back to the client",
[]string{"ingress_class", "namespace"}, nil),
waiting: prometheus.NewDesc(
prometheus.BuildFQName(ns, "", "current_waiting_connections"),
"current number of idle client connections waiting for a request",
[]string{"ingress_class", "namespace"}, nil),
}
go p.start()
return p
}
// Describe implements prometheus.Collector.
func (p nginxStatusCollector) Describe(ch chan<- *prometheus.Desc) {
ch <- p.data.active
ch <- p.data.accepted
ch <- p.data.handled
ch <- p.data.requests
ch <- p.data.reading
ch <- p.data.writing
ch <- p.data.waiting
}
// Collect implements prometheus.Collector.
func (p nginxStatusCollector) Collect(ch chan<- prometheus.Metric) {
req := scrapeRequest{results: ch, done: make(chan struct{})}
p.scrapeChan <- req
<-req.done
}
func (p nginxStatusCollector) start() {
for req := range p.scrapeChan {
ch := req.results
p.scrape(ch)
req.done <- struct{}{}
}
}
func (p nginxStatusCollector) Stop() {
close(p.scrapeChan)
}
// nginxStatusCollector scrape the nginx status
func (p nginxStatusCollector) scrape(ch chan<- prometheus.Metric) {
s, err := getNginxStatus(p.ngxHealthPort, p.ngxVtsPath)
if err != nil {
glog.Warningf("unexpected error obtaining nginx status info: %v", err)
return
}
ch <- prometheus.MustNewConstMetric(p.data.active,
prometheus.GaugeValue, float64(s.Active), p.ingressClass, p.watchNamespace)
ch <- prometheus.MustNewConstMetric(p.data.accepted,
prometheus.GaugeValue, float64(s.Accepted), p.ingressClass, p.watchNamespace)
ch <- prometheus.MustNewConstMetric(p.data.handled,
prometheus.GaugeValue, float64(s.Handled), p.ingressClass, p.watchNamespace)
ch <- prometheus.MustNewConstMetric(p.data.requests,
prometheus.GaugeValue, float64(s.Requests), p.ingressClass, p.watchNamespace)
ch <- prometheus.MustNewConstMetric(p.data.reading,
prometheus.GaugeValue, float64(s.Reading), p.ingressClass, p.watchNamespace)
ch <- prometheus.MustNewConstMetric(p.data.writing,
prometheus.GaugeValue, float64(s.Writing), p.ingressClass, p.watchNamespace)
ch <- prometheus.MustNewConstMetric(p.data.waiting,
prometheus.GaugeValue, float64(s.Waiting), p.ingressClass, p.watchNamespace)
}

View file

@ -0,0 +1,174 @@
/*
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 collector
import (
"path/filepath"
"github.com/golang/glog"
common "github.com/ncabatoff/process-exporter"
"github.com/ncabatoff/process-exporter/proc"
"github.com/prometheus/client_golang/prometheus"
)
// BinaryNameMatcher ...
type BinaryNameMatcher struct {
Name string
Binary string
}
// MatchAndName returns false if the match failed, otherwise
// true and the resulting name.
func (em BinaryNameMatcher) MatchAndName(nacl common.NameAndCmdline) (bool, string) {
if len(nacl.Cmdline) == 0 {
return false, ""
}
cmd := filepath.Base(em.Binary)
return em.Name == cmd, ""
}
type namedProcessData struct {
numProcs *prometheus.Desc
cpuSecs *prometheus.Desc
readBytes *prometheus.Desc
writeBytes *prometheus.Desc
memResidentbytes *prometheus.Desc
memVirtualbytes *prometheus.Desc
startTime *prometheus.Desc
}
type namedProcess struct {
*proc.Grouper
scrapeChan chan scrapeRequest
fs *proc.FS
data namedProcessData
}
// NewNamedProcess returns a new prometheus collector for the nginx process
func NewNamedProcess(children bool, mn common.MatchNamer) (prometheus.Collector, error) {
fs, err := proc.NewFS("/proc")
if err != nil {
return nil, err
}
p := namedProcess{
scrapeChan: make(chan scrapeRequest),
Grouper: proc.NewGrouper(children, mn),
fs: fs,
}
_, err = p.Update(p.fs.AllProcs())
if err != nil {
return nil, err
}
p.data = namedProcessData{
numProcs: prometheus.NewDesc(
"num_procs",
"number of processes",
nil, nil),
cpuSecs: prometheus.NewDesc(
"cpu_seconds_total",
"Cpu usage in seconds",
nil, nil),
readBytes: prometheus.NewDesc(
"read_bytes_total",
"number of bytes read",
nil, nil),
writeBytes: prometheus.NewDesc(
"write_bytes_total",
"number of bytes written",
nil, nil),
memResidentbytes: prometheus.NewDesc(
"resident_memory_bytes",
"number of bytes of memory in use",
nil, nil),
memVirtualbytes: prometheus.NewDesc(
"virtual_memory_bytes",
"number of bytes of memory in use",
nil, nil),
startTime: prometheus.NewDesc(
"oldest_start_time_seconds",
"start time in seconds since 1970/01/01",
nil, nil),
}
go p.start()
return p, nil
}
// Describe implements prometheus.Collector.
func (p namedProcess) Describe(ch chan<- *prometheus.Desc) {
ch <- p.data.cpuSecs
ch <- p.data.numProcs
ch <- p.data.readBytes
ch <- p.data.writeBytes
ch <- p.data.memResidentbytes
ch <- p.data.memVirtualbytes
ch <- p.data.startTime
}
// Collect implements prometheus.Collector.
func (p namedProcess) Collect(ch chan<- prometheus.Metric) {
req := scrapeRequest{results: ch, done: make(chan struct{})}
p.scrapeChan <- req
<-req.done
}
func (p namedProcess) start() {
for req := range p.scrapeChan {
ch := req.results
p.scrape(ch)
req.done <- struct{}{}
}
}
func (p namedProcess) Stop() {
close(p.scrapeChan)
}
func (p namedProcess) scrape(ch chan<- prometheus.Metric) {
_, err := p.Update(p.fs.AllProcs())
if err != nil {
glog.Warningf("unexpected error obtaining nginx process info: %v", err)
return
}
for _, gcounts := range p.Groups() {
ch <- prometheus.MustNewConstMetric(p.data.numProcs,
prometheus.GaugeValue, float64(gcounts.Procs))
ch <- prometheus.MustNewConstMetric(p.data.memResidentbytes,
prometheus.GaugeValue, float64(gcounts.Memresident))
ch <- prometheus.MustNewConstMetric(p.data.memVirtualbytes,
prometheus.GaugeValue, float64(gcounts.Memvirtual))
ch <- prometheus.MustNewConstMetric(p.data.startTime,
prometheus.GaugeValue, float64(gcounts.OldestStartTime.Unix()))
ch <- prometheus.MustNewConstMetric(p.data.cpuSecs,
prometheus.CounterValue, gcounts.Cpu)
ch <- prometheus.MustNewConstMetric(p.data.readBytes,
prometheus.CounterValue, float64(gcounts.ReadBytes))
ch <- prometheus.MustNewConstMetric(p.data.writeBytes,
prometheus.CounterValue, float64(gcounts.WriteBytes))
}
}

View file

@ -0,0 +1,30 @@
/*
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 collector
import "github.com/prometheus/client_golang/prometheus"
// Stopable defines a prometheus collector that can be stopped
type Stopable interface {
prometheus.Collector
Stop()
}
type scrapeRequest struct {
results chan<- prometheus.Metric
done chan struct{}
}

View file

@ -0,0 +1,225 @@
/*
Copyright 2016 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 collector
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"regexp"
"strconv"
"github.com/golang/glog"
)
var (
ac = regexp.MustCompile(`Active connections: (\d+)`)
sahr = regexp.MustCompile(`(\d+)\s(\d+)\s(\d+)`)
reading = regexp.MustCompile(`Reading: (\d+)`)
writing = regexp.MustCompile(`Writing: (\d+)`)
waiting = regexp.MustCompile(`Waiting: (\d+)`)
)
type basicStatus struct {
// Active total number of active connections
Active int
// Accepted total number of accepted client connections
Accepted int
// Handled total number of handled connections. Generally, the parameter value is the same as accepts unless some resource limits have been reached (for example, the worker_connections limit).
Handled int
// Requests total number of client requests.
Requests int
// Reading current number of connections where nginx is reading the request header.
Reading int
// Writing current number of connections where nginx is writing the response back to the client.
Writing int
// Waiting current number of idle client connections waiting for a request.
Waiting int
}
// https://github.com/vozlt/nginx-module-vts
type vts struct {
NginxVersion string `json:"nginxVersion"`
LoadMsec int `json:"loadMsec"`
NowMsec int `json:"nowMsec"`
// Total connections and requests(same as stub_status_module in NGINX)
Connections connections `json:"connections"`
// Traffic(in/out) and request and response counts and cache hit ratio per each server zone
ServerZones map[string]serverZone `json:"serverZones"`
// Traffic(in/out) and request and response counts and cache hit ratio per each server zone filtered through
// the vhost_traffic_status_filter_by_set_key directive
FilterZones map[string]map[string]filterZone `json:"filterZones"`
// Traffic(in/out) and request and response counts per server in each upstream group
UpstreamZones map[string][]upstreamZone `json:"upstreamZones"`
}
type serverZone struct {
RequestCounter float64 `json:"requestCounter"`
InBytes float64 `json:"inBytes"`
OutBytes float64 `json:"outBytes"`
Responses response `json:"responses"`
Cache cache `json:"cache"`
}
type filterZone struct {
RequestCounter float64 `json:"requestCounter"`
InBytes float64 `json:"inBytes"`
OutBytes float64 `json:"outBytes"`
Cache cache `json:"cache"`
Responses response `json:"responses"`
}
type upstreamZone struct {
Responses response `json:"responses"`
Server string `json:"server"`
RequestCounter float64 `json:"requestCounter"`
InBytes float64 `json:"inBytes"`
OutBytes float64 `json:"outBytes"`
ResponseMsec float64 `json:"responseMsec"`
Weight float64 `json:"weight"`
MaxFails float64 `json:"maxFails"`
FailTimeout float64 `json:"failTimeout"`
Backup BoolToFloat64 `json:"backup"`
Down BoolToFloat64 `json:"down"`
}
type cache struct {
Miss float64 `json:"miss"`
Bypass float64 `json:"bypass"`
Expired float64 `json:"expired"`
Stale float64 `json:"stale"`
Updating float64 `json:"updating"`
Revalidated float64 `json:"revalidated"`
Hit float64 `json:"hit"`
Scarce float64 `json:"scarce"`
}
type response struct {
OneXx float64 `json:"1xx"`
TwoXx float64 `json:"2xx"`
TheeXx float64 `json:"3xx"`
FourXx float64 `json:"4xx"`
FiveXx float64 `json:"5xx"`
}
type connections struct {
Active float64 `json:"active"`
Reading float64 `json:"reading"`
Writing float64 `json:"writing"`
Waiting float64 `json:"waiting"`
Accepted float64 `json:"accepted"`
Handled float64 `json:"handled"`
Requests float64 `json:"requests"`
}
// BoolToFloat64 ...
type BoolToFloat64 float64
// UnmarshalJSON ...
func (bit BoolToFloat64) UnmarshalJSON(data []byte) error {
asString := string(data)
if asString == "1" || asString == "true" {
bit = 1
} else if asString == "0" || asString == "false" {
bit = 0
} else {
return fmt.Errorf(fmt.Sprintf("boolean unmarshal error: invalid input %s", asString))
}
return nil
}
func getNginxStatus(port int, path string) (*basicStatus, error) {
url := fmt.Sprintf("http://localhost:%v%v", port, path)
glog.V(3).Infof("start scraping url: %v", url)
data, err := httpBody(url)
if err != nil {
return nil, fmt.Errorf("unexpected error scraping nginx status page: %v", err)
}
return parse(string(data)), nil
}
func httpBody(url string) ([]byte, error) {
resp, err := http.DefaultClient.Get(url)
if err != nil {
return nil, fmt.Errorf("unexpected error scraping nginx : %v", err)
}
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("unexpected error scraping nginx (%v)", err)
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 400 {
return nil, fmt.Errorf("unexpected error scraping nginx (status %v)", resp.StatusCode)
}
return data, nil
}
func getNginxVtsMetrics(port int, path string) (*vts, error) {
url := fmt.Sprintf("http://localhost:%v%v", port, path)
glog.V(3).Infof("start scraping url: %v", url)
data, err := httpBody(url)
if err != nil {
return nil, fmt.Errorf("unexpected error scraping nginx vts (%v)", err)
}
var vts *vts
err = json.Unmarshal(data, &vts)
if err != nil {
return nil, fmt.Errorf("unexpected error json unmarshal (%v)", err)
}
glog.V(3).Infof("scrape returned : %v", vts)
return vts, nil
}
func parse(data string) *basicStatus {
acr := ac.FindStringSubmatch(data)
sahrr := sahr.FindStringSubmatch(data)
readingr := reading.FindStringSubmatch(data)
writingr := writing.FindStringSubmatch(data)
waitingr := waiting.FindStringSubmatch(data)
return &basicStatus{
toInt(acr, 1),
toInt(sahrr, 1),
toInt(sahrr, 2),
toInt(sahrr, 3),
toInt(readingr, 1),
toInt(writingr, 1),
toInt(waitingr, 1),
}
}
func toInt(data []string, pos int) int {
if len(data) == 0 {
return 0
}
if pos > len(data) {
return 0
}
if v, err := strconv.Atoi(data[pos]); err == nil {
return v
}
return 0
}

View file

@ -0,0 +1,72 @@
/*
Copyright 2015 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 collector
import (
"testing"
"github.com/kylelemons/godebug/pretty"
)
func TestParseStatus(t *testing.T) {
tests := []struct {
in string
out *basicStatus
}{
{`Active connections: 43
server accepts handled requests
7368 7368 10993
Reading: 0 Writing: 5 Waiting: 38`,
&basicStatus{43, 7368, 7368, 10993, 0, 5, 38},
},
{`Active connections: 0
server accepts handled requests
1 7 0
Reading: A Writing: B Waiting: 38`,
&basicStatus{0, 1, 7, 0, 0, 0, 38},
},
}
for _, test := range tests {
r := parse(test.in)
if diff := pretty.Compare(r, test.out); diff != "" {
t.Logf("%v", diff)
t.Fatalf("expected %v but returned %v", test.out, r)
}
}
}
func TestToint(t *testing.T) {
tests := []struct {
in []string
pos int
exp int
}{
{[]string{}, 0, 0},
{[]string{}, 1, 0},
{[]string{"A"}, 0, 0},
{[]string{"1"}, 0, 1},
{[]string{"a", "2"}, 1, 2},
}
for _, test := range tests {
v := toInt(test.in, test.pos)
if v != test.exp {
t.Fatalf("expected %v but returned %v", test.exp, v)
}
}
}

273
pkg/metric/collector/vts.go Normal file
View file

@ -0,0 +1,273 @@
/*
Copyright 2016 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 collector
import (
"reflect"
"github.com/golang/glog"
"github.com/prometheus/client_golang/prometheus"
)
const ns = "nginx"
type (
vtsCollector struct {
scrapeChan chan scrapeRequest
port int
path string
data *vtsData
watchNamespace string
ingressClass string
}
vtsData struct {
bytes *prometheus.Desc
cache *prometheus.Desc
connections *prometheus.Desc
responses *prometheus.Desc
requests *prometheus.Desc
filterZoneBytes *prometheus.Desc
filterZoneResponses *prometheus.Desc
filterZoneCache *prometheus.Desc
upstreamBackup *prometheus.Desc
upstreamBytes *prometheus.Desc
upstreamDown *prometheus.Desc
upstreamFailTimeout *prometheus.Desc
upstreamMaxFails *prometheus.Desc
upstreamResponses *prometheus.Desc
upstreamRequests *prometheus.Desc
upstreamResponseMsec *prometheus.Desc
upstreamWeight *prometheus.Desc
}
)
// NewNGINXVTSCollector returns a new prometheus collector for the VTS module
func NewNGINXVTSCollector(watchNamespace, ingressClass string, port int, path string) Stopable {
p := vtsCollector{
scrapeChan: make(chan scrapeRequest),
port: port,
path: path,
watchNamespace: watchNamespace,
ingressClass: ingressClass,
}
p.data = &vtsData{
bytes: prometheus.NewDesc(
prometheus.BuildFQName(ns, "", "bytes_total"),
"Nginx bytes count",
[]string{"ingress_class", "namespace", "server_zone", "direction"}, nil),
cache: prometheus.NewDesc(
prometheus.BuildFQName(ns, "", "cache_total"),
"Nginx cache count",
[]string{"ingress_class", "namespace", "server_zone", "type"}, nil),
connections: prometheus.NewDesc(
prometheus.BuildFQName(ns, "", "connections_total"),
"Nginx connections count",
[]string{"ingress_class", "namespace", "type"}, nil),
responses: prometheus.NewDesc(
prometheus.BuildFQName(ns, "", "responses_total"),
"The number of responses with status codes 1xx, 2xx, 3xx, 4xx, and 5xx.",
[]string{"ingress_class", "namespace", "server_zone", "status_code"}, nil),
requests: prometheus.NewDesc(
prometheus.BuildFQName(ns, "", "requests_total"),
"The total number of requested client connections.",
[]string{"ingress_class", "namespace", "server_zone"}, nil),
filterZoneBytes: prometheus.NewDesc(
prometheus.BuildFQName(ns, "", "filterzone_bytes_total"),
"Nginx bytes count",
[]string{"ingress_class", "namespace", "server_zone", "key", "direction"}, nil),
filterZoneResponses: prometheus.NewDesc(
prometheus.BuildFQName(ns, "", "filterzone_responses_total"),
"The number of responses with status codes 1xx, 2xx, 3xx, 4xx, and 5xx.",
[]string{"ingress_class", "namespace", "server_zone", "key", "status_code"}, nil),
filterZoneCache: prometheus.NewDesc(
prometheus.BuildFQName(ns, "", "filterzone_cache_total"),
"Nginx cache count",
[]string{"ingress_class", "namespace", "server_zone", "key", "type"}, nil),
upstreamBackup: prometheus.NewDesc(
prometheus.BuildFQName(ns, "", "upstream_backup"),
"Current backup setting of the server.",
[]string{"ingress_class", "namespace", "upstream", "server"}, nil),
upstreamBytes: prometheus.NewDesc(
prometheus.BuildFQName(ns, "", "upstream_bytes_total"),
"The total number of bytes sent to this server.",
[]string{"ingress_class", "namespace", "upstream", "server", "direction"}, nil),
upstreamDown: prometheus.NewDesc(
prometheus.BuildFQName(ns, "", "vts_upstream_down_total"),
"Current down setting of the server.",
[]string{"ingress_class", "namespace", "upstream", "server"}, nil),
upstreamFailTimeout: prometheus.NewDesc(
prometheus.BuildFQName(ns, "", "upstream_fail_timeout"),
"Current fail_timeout setting of the server.",
[]string{"ingress_class", "namespace", "upstream", "server"}, nil),
upstreamMaxFails: prometheus.NewDesc(
prometheus.BuildFQName(ns, "", "upstream_maxfails"),
"Current max_fails setting of the server.",
[]string{"ingress_class", "namespace", "upstream", "server"}, nil),
upstreamResponses: prometheus.NewDesc(
prometheus.BuildFQName(ns, "", "upstream_responses_total"),
"The number of upstream responses with status codes 1xx, 2xx, 3xx, 4xx, and 5xx.",
[]string{"ingress_class", "namespace", "upstream", "server", "status_code"}, nil),
upstreamRequests: prometheus.NewDesc(
prometheus.BuildFQName(ns, "", "upstream_requests_total"),
"The total number of client connections forwarded to this server.",
[]string{"ingress_class", "namespace", "upstream", "server"}, nil),
upstreamResponseMsec: prometheus.NewDesc(
prometheus.BuildFQName(ns, "", "upstream_response_msecs_avg"),
"The average of only upstream response processing times in milliseconds.",
[]string{"ingress_class", "namespace", "upstream", "server"}, nil),
upstreamWeight: prometheus.NewDesc(
prometheus.BuildFQName(ns, "", "upstream_weight"),
"Current upstream weight setting of the server.",
[]string{"ingress_class", "namespace", "upstream", "server"}, nil),
}
go p.start()
return p
}
// Describe implements prometheus.Collector.
func (p vtsCollector) Describe(ch chan<- *prometheus.Desc) {
ch <- p.data.bytes
ch <- p.data.cache
ch <- p.data.connections
ch <- p.data.requests
ch <- p.data.responses
ch <- p.data.upstreamBackup
ch <- p.data.upstreamBytes
ch <- p.data.upstreamDown
ch <- p.data.upstreamFailTimeout
ch <- p.data.upstreamMaxFails
ch <- p.data.upstreamRequests
ch <- p.data.upstreamResponseMsec
ch <- p.data.upstreamResponses
ch <- p.data.upstreamWeight
ch <- p.data.filterZoneBytes
ch <- p.data.filterZoneCache
ch <- p.data.filterZoneResponses
}
// Collect implements prometheus.Collector.
func (p vtsCollector) Collect(ch chan<- prometheus.Metric) {
req := scrapeRequest{results: ch, done: make(chan struct{})}
p.scrapeChan <- req
<-req.done
}
func (p vtsCollector) start() {
for req := range p.scrapeChan {
ch := req.results
p.scrapeVts(ch)
req.done <- struct{}{}
}
}
func (p vtsCollector) Stop() {
close(p.scrapeChan)
}
// scrapeVts scrape nginx vts metrics
func (p vtsCollector) scrapeVts(ch chan<- prometheus.Metric) {
nginxMetrics, err := getNginxVtsMetrics(p.port, p.path)
if err != nil {
glog.Warningf("unexpected error obtaining nginx status info: %v", err)
return
}
reflectMetrics(&nginxMetrics.Connections, p.data.connections, ch, p.ingressClass, p.watchNamespace)
for name, zones := range nginxMetrics.UpstreamZones {
for pos, value := range zones {
reflectMetrics(&zones[pos].Responses, p.data.upstreamResponses, ch, p.ingressClass, p.watchNamespace, name, value.Server)
ch <- prometheus.MustNewConstMetric(p.data.upstreamRequests,
prometheus.CounterValue, zones[pos].RequestCounter, p.ingressClass, p.watchNamespace, name, value.Server)
ch <- prometheus.MustNewConstMetric(p.data.upstreamDown,
prometheus.CounterValue, float64(zones[pos].Down), p.ingressClass, p.watchNamespace, name, value.Server)
ch <- prometheus.MustNewConstMetric(p.data.upstreamWeight,
prometheus.CounterValue, zones[pos].Weight, p.ingressClass, p.watchNamespace, name, value.Server)
ch <- prometheus.MustNewConstMetric(p.data.upstreamResponseMsec,
prometheus.CounterValue, zones[pos].ResponseMsec, p.ingressClass, p.watchNamespace, name, value.Server)
ch <- prometheus.MustNewConstMetric(p.data.upstreamBackup,
prometheus.CounterValue, float64(zones[pos].Backup), p.ingressClass, p.watchNamespace, name, value.Server)
ch <- prometheus.MustNewConstMetric(p.data.upstreamFailTimeout,
prometheus.CounterValue, zones[pos].FailTimeout, p.ingressClass, p.watchNamespace, name, value.Server)
ch <- prometheus.MustNewConstMetric(p.data.upstreamMaxFails,
prometheus.CounterValue, zones[pos].MaxFails, p.ingressClass, p.watchNamespace, name, value.Server)
ch <- prometheus.MustNewConstMetric(p.data.upstreamBytes,
prometheus.CounterValue, zones[pos].InBytes, p.ingressClass, p.watchNamespace, name, value.Server, "in")
ch <- prometheus.MustNewConstMetric(p.data.upstreamBytes,
prometheus.CounterValue, zones[pos].OutBytes, p.ingressClass, p.watchNamespace, name, value.Server, "out")
}
}
for name, zone := range nginxMetrics.ServerZones {
reflectMetrics(&zone.Responses, p.data.responses, ch, p.ingressClass, p.watchNamespace, name)
reflectMetrics(&zone.Cache, p.data.cache, ch, p.ingressClass, p.watchNamespace, name)
ch <- prometheus.MustNewConstMetric(p.data.requests,
prometheus.CounterValue, zone.RequestCounter, p.ingressClass, p.watchNamespace, name)
ch <- prometheus.MustNewConstMetric(p.data.bytes,
prometheus.CounterValue, zone.InBytes, p.ingressClass, p.watchNamespace, name, "in")
ch <- prometheus.MustNewConstMetric(p.data.bytes,
prometheus.CounterValue, zone.OutBytes, p.ingressClass, p.watchNamespace, name, "out")
}
for serverZone, keys := range nginxMetrics.FilterZones {
for name, zone := range keys {
reflectMetrics(&zone.Responses, p.data.filterZoneResponses, ch, p.ingressClass, p.watchNamespace, serverZone, name)
reflectMetrics(&zone.Cache, p.data.filterZoneCache, ch, p.ingressClass, p.watchNamespace, serverZone, name)
ch <- prometheus.MustNewConstMetric(p.data.filterZoneBytes,
prometheus.CounterValue, zone.InBytes, p.ingressClass, p.watchNamespace, serverZone, name, "in")
ch <- prometheus.MustNewConstMetric(p.data.filterZoneBytes,
prometheus.CounterValue, zone.OutBytes, p.ingressClass, p.watchNamespace, serverZone, name, "out")
}
}
}
func reflectMetrics(value interface{}, desc *prometheus.Desc, ch chan<- prometheus.Metric, labels ...string) {
val := reflect.ValueOf(value).Elem()
for i := 0; i < val.NumField(); i++ {
tag := val.Type().Field(i).Tag
l := append(labels, tag.Get("json"))
ch <- prometheus.MustNewConstMetric(desc,
prometheus.CounterValue, val.Field(i).Interface().(float64),
l...)
}
}

137
pkg/template/configmap.go Normal file
View file

@ -0,0 +1,137 @@
/*
Copyright 2015 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 template
import (
"fmt"
"net"
"strconv"
"strings"
"github.com/golang/glog"
"github.com/mitchellh/mapstructure"
"k8s.io/ingress/controllers/nginx/pkg/config"
ing_net "k8s.io/ingress/core/pkg/net"
)
const (
customHTTPErrors = "custom-http-errors"
skipAccessLogUrls = "skip-access-log-urls"
whitelistSourceRange = "whitelist-source-range"
proxyRealIPCIDR = "proxy-real-ip-cidr"
bindAddress = "bind-address"
)
// ReadConfig obtains the configuration defined by the user merged with the defaults.
func ReadConfig(src map[string]string) config.Configuration {
conf := map[string]string{}
if src != nil {
// we need to copy the configmap data because the content is altered
for k, v := range src {
conf[k] = v
}
}
errors := make([]int, 0)
skipUrls := make([]string, 0)
whitelist := make([]string, 0)
proxylist := make([]string, 0)
bindAddressIpv4List := make([]string, 0)
bindAddressIpv6List := make([]string, 0)
if val, ok := conf[customHTTPErrors]; ok {
delete(conf, customHTTPErrors)
for _, i := range strings.Split(val, ",") {
j, err := strconv.Atoi(i)
if err != nil {
glog.Warningf("%v is not a valid http code: %v", i, err)
} else {
errors = append(errors, j)
}
}
}
if val, ok := conf[skipAccessLogUrls]; ok {
delete(conf, skipAccessLogUrls)
skipUrls = strings.Split(val, ",")
}
if val, ok := conf[whitelistSourceRange]; ok {
delete(conf, whitelistSourceRange)
whitelist = append(whitelist, strings.Split(val, ",")...)
}
if val, ok := conf[proxyRealIPCIDR]; ok {
delete(conf, proxyRealIPCIDR)
proxylist = append(proxylist, strings.Split(val, ",")...)
} else {
proxylist = append(proxylist, "0.0.0.0/0")
}
if val, ok := conf[bindAddress]; ok {
delete(conf, bindAddress)
for _, i := range strings.Split(val, ",") {
ns := net.ParseIP(i)
if ns != nil {
if ing_net.IsIPV6(ns) {
bindAddressIpv6List = append(bindAddressIpv6List, fmt.Sprintf("[%v]", ns))
} else {
bindAddressIpv4List = append(bindAddressIpv4List, fmt.Sprintf("%v", ns))
}
} else {
glog.Warningf("%v is not a valid textual representation of an IP address", i)
}
}
}
to := config.NewDefault()
to.CustomHTTPErrors = filterErrors(errors)
to.SkipAccessLogURLs = skipUrls
to.WhitelistSourceRange = whitelist
to.ProxyRealIPCIDR = proxylist
to.BindAddressIpv4 = bindAddressIpv4List
to.BindAddressIpv6 = bindAddressIpv6List
config := &mapstructure.DecoderConfig{
Metadata: nil,
WeaklyTypedInput: true,
Result: &to,
TagName: "json",
}
decoder, err := mapstructure.NewDecoder(config)
if err != nil {
glog.Warningf("unexpected error merging defaults: %v", err)
}
err = decoder.Decode(conf)
if err != nil {
glog.Warningf("unexpected error merging defaults: %v", err)
}
return to
}
func filterErrors(codes []int) []int {
var fa []int
for _, code := range codes {
if code > 299 && code < 600 {
fa = append(fa, code)
} else {
glog.Warningf("error code %v is not valid for custom error pages", code)
}
}
return fa
}

View file

@ -0,0 +1,95 @@
/*
Copyright 2015 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 template
import (
"testing"
"github.com/kylelemons/godebug/pretty"
"k8s.io/ingress/controllers/nginx/pkg/config"
)
func TestFilterErrors(t *testing.T) {
e := filterErrors([]int{200, 300, 345, 500, 555, 999})
if len(e) != 4 {
t.Errorf("expected 4 elements but %v returned", len(e))
}
}
func TestMergeConfigMapToStruct(t *testing.T) {
conf := map[string]string{
"custom-http-errors": "300,400,demo",
"proxy-read-timeout": "1",
"proxy-send-timeout": "2",
"skip-access-log-urls": "/log,/demo,/test",
"use-proxy-protocol": "true",
"disable-access-log": "true",
"access-log-path": "/var/log/test/access.log",
"error-log-path": "/var/log/test/error.log",
"use-gzip": "true",
"enable-dynamic-tls-records": "false",
"gzip-types": "text/html",
"proxy-real-ip-cidr": "1.1.1.1/8,2.2.2.2/24",
"bind-address": "1.1.1.1,2.2.2.2,3.3.3,2001:db8:a0b:12f0::1,3731:54:65fe:2::a7,33:33:33::33::33",
"worker-shutdown-timeout": "99s",
}
def := config.NewDefault()
def.CustomHTTPErrors = []int{300, 400}
def.DisableAccessLog = true
def.AccessLogPath = "/var/log/test/access.log"
def.ErrorLogPath = "/var/log/test/error.log"
def.SkipAccessLogURLs = []string{"/log", "/demo", "/test"}
def.ProxyReadTimeout = 1
def.ProxySendTimeout = 2
def.EnableDynamicTLSRecords = false
def.UseProxyProtocol = true
def.GzipTypes = "text/html"
def.ProxyRealIPCIDR = []string{"1.1.1.1/8", "2.2.2.2/24"}
def.BindAddressIpv4 = []string{"1.1.1.1", "2.2.2.2"}
def.BindAddressIpv6 = []string{"[2001:db8:a0b:12f0::1]", "[3731:54:65fe:2::a7]"}
def.WorkerShutdownTimeout = "99s"
to := ReadConfig(conf)
if diff := pretty.Compare(to, def); diff != "" {
t.Errorf("unexpected diff: (-got +want)\n%s", diff)
}
def = config.NewDefault()
to = ReadConfig(map[string]string{})
if diff := pretty.Compare(to, def); diff != "" {
t.Errorf("unexpected diff: (-got +want)\n%s", diff)
}
def = config.NewDefault()
def.WhitelistSourceRange = []string{"1.1.1.1/32"}
to = ReadConfig(map[string]string{
"whitelist-source-range": "1.1.1.1/32",
})
if diff := pretty.Compare(to, def); diff != "" {
t.Errorf("unexpected diff: (-got +want)\n%s", diff)
}
}
func TestDefaultLoadBalance(t *testing.T) {
conf := map[string]string{}
to := ReadConfig(conf)
if to.LoadBalanceAlgorithm != "least_conn" {
t.Errorf("default load balance algorithm wrong")
}
}

690
pkg/template/template.go Normal file
View file

@ -0,0 +1,690 @@
/*
Copyright 2015 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 template
import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"net"
"net/url"
"os"
"os/exec"
"strconv"
"strings"
text_template "text/template"
"github.com/golang/glog"
"github.com/pborman/uuid"
extensions "k8s.io/api/extensions/v1beta1"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/ingress/controllers/nginx/pkg/config"
"k8s.io/ingress/core/pkg/ingress"
"k8s.io/ingress/core/pkg/ingress/annotations/ratelimit"
ing_net "k8s.io/ingress/core/pkg/net"
"k8s.io/ingress/core/pkg/watch"
)
const (
slash = "/"
defBufferSize = 65535
)
// Template ...
type Template struct {
tmpl *text_template.Template
fw watch.FileWatcher
s int
}
//NewTemplate returns a new Template instance or an
//error if the specified template file contains errors
func NewTemplate(file string, onChange func()) (*Template, error) {
tmpl, err := text_template.New("nginx.tmpl").Funcs(funcMap).ParseFiles(file)
if err != nil {
return nil, err
}
fw, err := watch.NewFileWatcher(file, onChange)
if err != nil {
return nil, err
}
return &Template{
tmpl: tmpl,
fw: fw,
s: defBufferSize,
}, nil
}
// Close removes the file watcher
func (t *Template) Close() {
t.fw.Close()
}
// Write populates a buffer using a template with NGINX configuration
// and the servers and upstreams created by Ingress rules
func (t *Template) Write(conf config.TemplateConfig) ([]byte, error) {
tmplBuf := bytes.NewBuffer(make([]byte, 0, t.s))
outCmdBuf := bytes.NewBuffer(make([]byte, 0, t.s))
defer func() {
if t.s < tmplBuf.Cap() {
glog.V(2).Infof("adjusting template buffer size from %v to %v", t.s, tmplBuf.Cap())
t.s = tmplBuf.Cap()
}
}()
if glog.V(3) {
b, err := json.Marshal(conf)
if err != nil {
glog.Errorf("unexpected error: %v", err)
}
glog.Infof("NGINX configuration: %v", string(b))
}
err := t.tmpl.Execute(tmplBuf, conf)
if err != nil {
return nil, err
}
// squeezes multiple adjacent empty lines to be single
// spaced this is to avoid the use of regular expressions
cmd := exec.Command("/ingress-controller/clean-nginx-conf.sh")
cmd.Stdin = tmplBuf
cmd.Stdout = outCmdBuf
if err := cmd.Run(); err != nil {
glog.Warningf("unexpected error cleaning template: %v", err)
return tmplBuf.Bytes(), nil
}
return outCmdBuf.Bytes(), nil
}
var (
funcMap = text_template.FuncMap{
"empty": func(input interface{}) bool {
check, ok := input.(string)
if ok {
return len(check) == 0
}
return true
},
"buildLocation": buildLocation,
"buildAuthLocation": buildAuthLocation,
"buildAuthResponseHeaders": buildAuthResponseHeaders,
"buildProxyPass": buildProxyPass,
"filterRateLimits": filterRateLimits,
"buildRateLimitZones": buildRateLimitZones,
"buildRateLimit": buildRateLimit,
"buildResolvers": buildResolvers,
"buildUpstreamName": buildUpstreamName,
"isLocationAllowed": isLocationAllowed,
"buildLogFormatUpstream": buildLogFormatUpstream,
"buildDenyVariable": buildDenyVariable,
"getenv": os.Getenv,
"contains": strings.Contains,
"hasPrefix": strings.HasPrefix,
"hasSuffix": strings.HasSuffix,
"toUpper": strings.ToUpper,
"toLower": strings.ToLower,
"formatIP": formatIP,
"buildNextUpstream": buildNextUpstream,
"getIngressInformation": getIngressInformation,
"serverConfig": func(all config.TemplateConfig, server *ingress.Server) interface{} {
return struct{ First, Second interface{} }{all, server}
},
"isValidClientBodyBufferSize": isValidClientBodyBufferSize,
"buildForwardedFor": buildForwardedFor,
"buildAuthSignURL": buildAuthSignURL,
}
)
// formatIP will wrap IPv6 addresses in [] and return IPv4 addresses
// without modification. If the input cannot be parsed as an IP address
// it is returned without modification.
func formatIP(input string) string {
ip := net.ParseIP(input)
if ip == nil {
return input
}
if v4 := ip.To4(); v4 != nil {
return input
}
return fmt.Sprintf("[%s]", input)
}
// buildResolvers returns the resolvers reading the /etc/resolv.conf file
func buildResolvers(input interface{}) string {
// NGINX need IPV6 addresses to be surrounded by brackets
nss, ok := input.([]net.IP)
if !ok {
glog.Errorf("expected a '[]net.IP' type but %T was returned", input)
return ""
}
if len(nss) == 0 {
return ""
}
r := []string{"resolver"}
for _, ns := range nss {
if ing_net.IsIPV6(ns) {
r = append(r, fmt.Sprintf("[%v]", ns))
} else {
r = append(r, fmt.Sprintf("%v", ns))
}
}
r = append(r, "valid=30s;")
return strings.Join(r, " ")
}
// buildLocation produces the location string, if the ingress has redirects
// (specified through the ingress.kubernetes.io/rewrite-to annotation)
func buildLocation(input interface{}) string {
location, ok := input.(*ingress.Location)
if !ok {
glog.Errorf("expected an '*ingress.Location' type but %T was returned", input)
return slash
}
path := location.Path
if len(location.Rewrite.Target) > 0 && location.Rewrite.Target != path {
if path == slash {
return fmt.Sprintf("~* %s", path)
}
// baseuri regex will parse basename from the given location
baseuri := `(?<baseuri>.*)`
if !strings.HasSuffix(path, slash) {
// Not treat the slash after "location path" as a part of baseuri
baseuri = fmt.Sprintf(`\/?%s`, baseuri)
}
return fmt.Sprintf(`~* ^%s%s`, path, baseuri)
}
return path
}
// TODO: Needs Unit Tests
func buildAuthLocation(input interface{}) string {
location, ok := input.(*ingress.Location)
if !ok {
glog.Errorf("expected an '*ingress.Location' type but %T was returned", input)
return ""
}
if location.ExternalAuth.URL == "" {
return ""
}
str := base64.URLEncoding.EncodeToString([]byte(location.Path))
// avoid locations containing the = char
str = strings.Replace(str, "=", "", -1)
return fmt.Sprintf("/_external-auth-%v", str)
}
func buildAuthResponseHeaders(input interface{}) []string {
location, ok := input.(*ingress.Location)
res := []string{}
if !ok {
glog.Errorf("expected an '*ingress.Location' type but %T was returned", input)
return res
}
if len(location.ExternalAuth.ResponseHeaders) == 0 {
return res
}
for i, h := range location.ExternalAuth.ResponseHeaders {
hvar := strings.ToLower(h)
hvar = strings.NewReplacer("-", "_").Replace(hvar)
res = append(res, fmt.Sprintf("auth_request_set $authHeader%v $upstream_http_%v;", i, hvar))
res = append(res, fmt.Sprintf("proxy_set_header '%v' $authHeader%v;", h, i))
}
return res
}
func buildLogFormatUpstream(input interface{}) string {
cfg, ok := input.(config.Configuration)
if !ok {
glog.Errorf("expected a 'config.Configuration' type but %T was returned", input)
return ""
}
return cfg.BuildLogFormatUpstream()
}
// buildProxyPass produces the proxy pass string, if the ingress has redirects
// (specified through the ingress.kubernetes.io/rewrite-to annotation)
// If the annotation ingress.kubernetes.io/add-base-url:"true" is specified it will
// add a base tag in the head of the response from the service
func buildProxyPass(host string, b interface{}, loc interface{}) string {
backends, ok := b.([]*ingress.Backend)
if !ok {
glog.Errorf("expected an '[]*ingress.Backend' type but %T was returned", b)
return ""
}
location, ok := loc.(*ingress.Location)
if !ok {
glog.Errorf("expected a '*ingress.Location' type but %T was returned", loc)
return ""
}
path := location.Path
proto := "http"
upstreamName := location.Backend
for _, backend := range backends {
if backend.Name == location.Backend {
if backend.Secure || backend.SSLPassthrough {
proto = "https"
}
if isSticky(host, location, backend.SessionAffinity.CookieSessionAffinity.Locations) {
upstreamName = fmt.Sprintf("sticky-%v", upstreamName)
}
break
}
}
// defProxyPass returns the default proxy_pass, just the name of the upstream
defProxyPass := fmt.Sprintf("proxy_pass %s://%s;", proto, upstreamName)
// if the path in the ingress rule is equals to the target: no special rewrite
if path == location.Rewrite.Target {
return defProxyPass
}
if !strings.HasSuffix(path, slash) {
path = fmt.Sprintf("%s/", path)
}
if len(location.Rewrite.Target) > 0 {
abu := ""
if location.Rewrite.AddBaseURL {
// path has a slash suffix, so that it can be connected with baseuri directly
bPath := fmt.Sprintf("%s%s", path, "$baseuri")
if len(location.Rewrite.BaseURLScheme) > 0 {
abu = fmt.Sprintf(`subs_filter '<head(.*)>' '<head$1><base href="%v://$http_host%v">' r;
subs_filter '<HEAD(.*)>' '<HEAD$1><base href="%v://$http_host%v">' r;
`, location.Rewrite.BaseURLScheme, bPath, location.Rewrite.BaseURLScheme, bPath)
} else {
abu = fmt.Sprintf(`subs_filter '<head(.*)>' '<head$1><base href="$scheme://$http_host%v">' r;
subs_filter '<HEAD(.*)>' '<HEAD$1><base href="$scheme://$http_host%v">' r;
`, bPath, bPath)
}
}
if location.Rewrite.Target == slash {
// special case redirect to /
// ie /something to /
return fmt.Sprintf(`
rewrite %s(.*) /$1 break;
rewrite %s / break;
proxy_pass %s://%s;
%v`, path, location.Path, proto, upstreamName, abu)
}
return fmt.Sprintf(`
rewrite %s(.*) %s/$1 break;
proxy_pass %s://%s;
%v`, path, location.Rewrite.Target, proto, upstreamName, abu)
}
// default proxy_pass
return defProxyPass
}
// TODO: Needs Unit Tests
func filterRateLimits(input interface{}) []ratelimit.RateLimit {
ratelimits := []ratelimit.RateLimit{}
found := sets.String{}
servers, ok := input.([]*ingress.Server)
if !ok {
glog.Errorf("expected a '[]ratelimit.RateLimit' type but %T was returned", input)
return ratelimits
}
for _, server := range servers {
for _, loc := range server.Locations {
if loc.RateLimit.ID != "" && !found.Has(loc.RateLimit.ID) {
found.Insert(loc.RateLimit.ID)
ratelimits = append(ratelimits, loc.RateLimit)
}
}
}
return ratelimits
}
// TODO: Needs Unit Tests
// buildRateLimitZones produces an array of limit_conn_zone in order to allow
// rate limiting of request. Each Ingress rule could have up to three zones, one
// for connection limit by IP address, one for limiting requests per minute, and
// one for limiting requests per second.
func buildRateLimitZones(input interface{}) []string {
zones := sets.String{}
servers, ok := input.([]*ingress.Server)
if !ok {
glog.Errorf("expected a '[]*ingress.Server' type but %T was returned", input)
return zones.List()
}
for _, server := range servers {
for _, loc := range server.Locations {
if loc.RateLimit.Connections.Limit > 0 {
zone := fmt.Sprintf("limit_conn_zone $limit_%s zone=%v:%vm;",
loc.RateLimit.ID,
loc.RateLimit.Connections.Name,
loc.RateLimit.Connections.SharedSize)
if !zones.Has(zone) {
zones.Insert(zone)
}
}
if loc.RateLimit.RPM.Limit > 0 {
zone := fmt.Sprintf("limit_req_zone $limit_%s zone=%v:%vm rate=%vr/m;",
loc.RateLimit.ID,
loc.RateLimit.RPM.Name,
loc.RateLimit.RPM.SharedSize,
loc.RateLimit.RPM.Limit)
if !zones.Has(zone) {
zones.Insert(zone)
}
}
if loc.RateLimit.RPS.Limit > 0 {
zone := fmt.Sprintf("limit_req_zone $limit_%s zone=%v:%vm rate=%vr/s;",
loc.RateLimit.ID,
loc.RateLimit.RPS.Name,
loc.RateLimit.RPS.SharedSize,
loc.RateLimit.RPS.Limit)
if !zones.Has(zone) {
zones.Insert(zone)
}
}
}
}
return zones.List()
}
// buildRateLimit produces an array of limit_req to be used inside the Path of
// Ingress rules. The order: connections by IP first, then RPS, and RPM last.
func buildRateLimit(input interface{}) []string {
limits := []string{}
loc, ok := input.(*ingress.Location)
if !ok {
glog.Errorf("expected an '*ingress.Location' type but %T was returned", input)
return limits
}
if loc.RateLimit.Connections.Limit > 0 {
limit := fmt.Sprintf("limit_conn %v %v;",
loc.RateLimit.Connections.Name, loc.RateLimit.Connections.Limit)
limits = append(limits, limit)
}
if loc.RateLimit.RPS.Limit > 0 {
limit := fmt.Sprintf("limit_req zone=%v burst=%v nodelay;",
loc.RateLimit.RPS.Name, loc.RateLimit.RPS.Burst)
limits = append(limits, limit)
}
if loc.RateLimit.RPM.Limit > 0 {
limit := fmt.Sprintf("limit_req zone=%v burst=%v nodelay;",
loc.RateLimit.RPM.Name, loc.RateLimit.RPM.Burst)
limits = append(limits, limit)
}
if loc.RateLimit.LimitRateAfter > 0 {
limit := fmt.Sprintf("limit_rate_after %vk;",
loc.RateLimit.LimitRateAfter)
limits = append(limits, limit)
}
if loc.RateLimit.LimitRate > 0 {
limit := fmt.Sprintf("limit_rate %vk;",
loc.RateLimit.LimitRate)
limits = append(limits, limit)
}
return limits
}
func isLocationAllowed(input interface{}) bool {
loc, ok := input.(*ingress.Location)
if !ok {
glog.Errorf("expected an '*ingress.Location' type but %T was returned", input)
return false
}
return loc.Denied == nil
}
var (
denyPathSlugMap = map[string]string{}
)
// buildDenyVariable returns a nginx variable for a location in a
// server to be used in the whitelist check
// This method uses a unique id generator library to reduce the
// size of the string to be used as a variable in nginx to avoid
// issue with the size of the variable bucket size directive
func buildDenyVariable(a interface{}) string {
l, ok := a.(string)
if !ok {
glog.Errorf("expected a 'string' type but %T was returned", a)
return ""
}
if _, ok := denyPathSlugMap[l]; !ok {
denyPathSlugMap[l] = buildRandomUUID()
}
return fmt.Sprintf("$deny_%v", denyPathSlugMap[l])
}
// TODO: Needs Unit Tests
func buildUpstreamName(host string, b interface{}, loc interface{}) string {
backends, ok := b.([]*ingress.Backend)
if !ok {
glog.Errorf("expected an '[]*ingress.Backend' type but %T was returned", b)
return ""
}
location, ok := loc.(*ingress.Location)
if !ok {
glog.Errorf("expected a '*ingress.Location' type but %T was returned", loc)
return ""
}
upstreamName := location.Backend
for _, backend := range backends {
if backend.Name == location.Backend {
if backend.SessionAffinity.AffinityType == "cookie" &&
isSticky(host, location, backend.SessionAffinity.CookieSessionAffinity.Locations) {
upstreamName = fmt.Sprintf("sticky-%v", upstreamName)
}
break
}
}
return upstreamName
}
// TODO: Needs Unit Tests
func isSticky(host string, loc *ingress.Location, stickyLocations map[string][]string) bool {
if _, ok := stickyLocations[host]; ok {
for _, sl := range stickyLocations[host] {
if sl == loc.Path {
return true
}
}
}
return false
}
func buildNextUpstream(input interface{}) string {
nextUpstream, ok := input.(string)
if !ok {
glog.Errorf("expected a 'string' type but %T was returned", input)
return ""
}
parts := strings.Split(nextUpstream, " ")
nextUpstreamCodes := make([]string, 0, len(parts))
for _, v := range parts {
if v != "" && v != "non_idempotent" {
nextUpstreamCodes = append(nextUpstreamCodes, v)
}
}
return strings.Join(nextUpstreamCodes, " ")
}
// buildRandomUUID return a random string to be used in the template
func buildRandomUUID() string {
s := uuid.New()
return strings.Replace(s, "-", "", -1)
}
func isValidClientBodyBufferSize(input interface{}) bool {
s, ok := input.(string)
if !ok {
glog.Errorf("expected an 'string' type but %T was returned", input)
return false
}
if s == "" {
return false
}
_, err := strconv.Atoi(s)
if err != nil {
sLowercase := strings.ToLower(s)
kCheck := strings.TrimSuffix(sLowercase, "k")
_, err := strconv.Atoi(kCheck)
if err == nil {
return true
}
mCheck := strings.TrimSuffix(sLowercase, "m")
_, err = strconv.Atoi(mCheck)
if err == nil {
return true
}
glog.Errorf("client-body-buffer-size '%v' was provided in an incorrect format, hence it will not be set.", s)
return false
}
return true
}
type ingressInformation struct {
Namespace string
Rule string
Service string
Annotations map[string]string
}
func getIngressInformation(i, p interface{}) *ingressInformation {
ing, ok := i.(*extensions.Ingress)
if !ok {
glog.Errorf("expected an '*extensions.Ingress' type but %T was returned", i)
return &ingressInformation{}
}
path, ok := p.(string)
if !ok {
glog.Errorf("expected a 'string' type but %T was returned", p)
return &ingressInformation{}
}
if ing == nil {
return &ingressInformation{}
}
info := &ingressInformation{
Namespace: ing.GetNamespace(),
Rule: ing.GetName(),
Annotations: ing.Annotations,
}
if ing.Spec.Backend != nil {
info.Service = ing.Spec.Backend.ServiceName
}
for _, rule := range ing.Spec.Rules {
if rule.HTTP == nil {
continue
}
for _, rPath := range rule.HTTP.Paths {
if path == rPath.Path {
info.Service = rPath.Backend.ServiceName
return info
}
}
}
return info
}
func buildForwardedFor(input interface{}) string {
s, ok := input.(string)
if !ok {
glog.Errorf("expected a 'string' type but %T was returned", input)
return ""
}
ffh := strings.Replace(s, "-", "_", -1)
ffh = strings.ToLower(ffh)
return fmt.Sprintf("$http_%v", ffh)
}
func buildAuthSignURL(input interface{}) string {
s, ok := input.(string)
if !ok {
glog.Errorf("expected an 'string' type but %T was returned", input)
return ""
}
u, _ := url.Parse(s)
q := u.Query()
if len(q) == 0 {
return fmt.Sprintf("%v?rd=$scheme://$http_host$request_uri", s)
}
if q.Get("rd") != "" {
return s
}
return fmt.Sprintf("%v&rd=$scheme://$http_host$request_uri", s)
}

View file

@ -0,0 +1,372 @@
/*
Copyright 2015 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 template
import (
"encoding/json"
"io/ioutil"
"net"
"os"
"path"
"reflect"
"strings"
"testing"
"k8s.io/ingress/controllers/nginx/pkg/config"
"k8s.io/ingress/core/pkg/ingress"
"k8s.io/ingress/core/pkg/ingress/annotations/authreq"
"k8s.io/ingress/core/pkg/ingress/annotations/rewrite"
)
var (
// TODO: add tests for secure endpoints
tmplFuncTestcases = map[string]struct {
Path string
Target string
Location string
ProxyPass string
AddBaseURL bool
BaseURLScheme string
}{
"invalid redirect / to /": {"/", "/", "/", "proxy_pass http://upstream-name;", false, ""},
"redirect / to /jenkins": {"/", "/jenkins", "~* /",
`
rewrite /(.*) /jenkins/$1 break;
proxy_pass http://upstream-name;
`, false, ""},
"redirect /something to /": {"/something", "/", `~* ^/something\/?(?<baseuri>.*)`, `
rewrite /something/(.*) /$1 break;
rewrite /something / break;
proxy_pass http://upstream-name;
`, false, ""},
"redirect /end-with-slash/ to /not-root": {"/end-with-slash/", "/not-root", "~* ^/end-with-slash/(?<baseuri>.*)", `
rewrite /end-with-slash/(.*) /not-root/$1 break;
proxy_pass http://upstream-name;
`, false, ""},
"redirect /something-complex to /not-root": {"/something-complex", "/not-root", `~* ^/something-complex\/?(?<baseuri>.*)`, `
rewrite /something-complex/(.*) /not-root/$1 break;
proxy_pass http://upstream-name;
`, false, ""},
"redirect / to /jenkins and rewrite": {"/", "/jenkins", "~* /", `
rewrite /(.*) /jenkins/$1 break;
proxy_pass http://upstream-name;
subs_filter '<head(.*)>' '<head$1><base href="$scheme://$http_host/$baseuri">' r;
subs_filter '<HEAD(.*)>' '<HEAD$1><base href="$scheme://$http_host/$baseuri">' r;
`, true, ""},
"redirect /something to / and rewrite": {"/something", "/", `~* ^/something\/?(?<baseuri>.*)`, `
rewrite /something/(.*) /$1 break;
rewrite /something / break;
proxy_pass http://upstream-name;
subs_filter '<head(.*)>' '<head$1><base href="$scheme://$http_host/something/$baseuri">' r;
subs_filter '<HEAD(.*)>' '<HEAD$1><base href="$scheme://$http_host/something/$baseuri">' r;
`, true, ""},
"redirect /end-with-slash/ to /not-root and rewrite": {"/end-with-slash/", "/not-root", `~* ^/end-with-slash/(?<baseuri>.*)`, `
rewrite /end-with-slash/(.*) /not-root/$1 break;
proxy_pass http://upstream-name;
subs_filter '<head(.*)>' '<head$1><base href="$scheme://$http_host/end-with-slash/$baseuri">' r;
subs_filter '<HEAD(.*)>' '<HEAD$1><base href="$scheme://$http_host/end-with-slash/$baseuri">' r;
`, true, ""},
"redirect /something-complex to /not-root and rewrite": {"/something-complex", "/not-root", `~* ^/something-complex\/?(?<baseuri>.*)`, `
rewrite /something-complex/(.*) /not-root/$1 break;
proxy_pass http://upstream-name;
subs_filter '<head(.*)>' '<head$1><base href="$scheme://$http_host/something-complex/$baseuri">' r;
subs_filter '<HEAD(.*)>' '<HEAD$1><base href="$scheme://$http_host/something-complex/$baseuri">' r;
`, true, ""},
"redirect /something to / and rewrite with specific scheme": {"/something", "/", `~* ^/something\/?(?<baseuri>.*)`, `
rewrite /something/(.*) /$1 break;
rewrite /something / break;
proxy_pass http://upstream-name;
subs_filter '<head(.*)>' '<head$1><base href="http://$http_host/something/$baseuri">' r;
subs_filter '<HEAD(.*)>' '<HEAD$1><base href="http://$http_host/something/$baseuri">' r;
`, true, "http"},
}
)
func TestFormatIP(t *testing.T) {
cases := map[string]struct {
Input, Output string
}{
"ipv4-localhost": {"127.0.0.1", "127.0.0.1"},
"ipv4-internet": {"8.8.8.8", "8.8.8.8"},
"ipv6-localhost": {"::1", "[::1]"},
"ipv6-internet": {"2001:4860:4860::8888", "[2001:4860:4860::8888]"},
"invalid-ip": {"nonsense", "nonsense"},
"empty-ip": {"", ""},
}
for k, tc := range cases {
res := formatIP(tc.Input)
if res != tc.Output {
t.Errorf("%s: called formatIp('%s'); expected '%v' but returned '%v'", k, tc.Input, tc.Output, res)
}
}
}
func TestBuildLocation(t *testing.T) {
for k, tc := range tmplFuncTestcases {
loc := &ingress.Location{
Path: tc.Path,
Rewrite: rewrite.Redirect{Target: tc.Target, AddBaseURL: tc.AddBaseURL},
}
newLoc := buildLocation(loc)
if tc.Location != newLoc {
t.Errorf("%s: expected '%v' but returned %v", k, tc.Location, newLoc)
}
}
}
func TestBuildProxyPass(t *testing.T) {
for k, tc := range tmplFuncTestcases {
loc := &ingress.Location{
Path: tc.Path,
Rewrite: rewrite.Redirect{Target: tc.Target, AddBaseURL: tc.AddBaseURL, BaseURLScheme: tc.BaseURLScheme},
Backend: "upstream-name",
}
pp := buildProxyPass("", []*ingress.Backend{}, loc)
if !strings.EqualFold(tc.ProxyPass, pp) {
t.Errorf("%s: expected \n'%v'\nbut returned \n'%v'", k, tc.ProxyPass, pp)
}
}
}
func TestBuildAuthResponseHeaders(t *testing.T) {
loc := &ingress.Location{
ExternalAuth: authreq.External{ResponseHeaders: []string{"h1", "H-With-Caps-And-Dashes"}},
}
headers := buildAuthResponseHeaders(loc)
expected := []string{
"auth_request_set $authHeader0 $upstream_http_h1;",
"proxy_set_header 'h1' $authHeader0;",
"auth_request_set $authHeader1 $upstream_http_h_with_caps_and_dashes;",
"proxy_set_header 'H-With-Caps-And-Dashes' $authHeader1;",
}
if !reflect.DeepEqual(expected, headers) {
t.Errorf("Expected \n'%v'\nbut returned \n'%v'", expected, headers)
}
}
func TestTemplateWithData(t *testing.T) {
pwd, _ := os.Getwd()
f, err := os.Open(path.Join(pwd, "../../test/data/config.json"))
if err != nil {
t.Errorf("unexpected error reading json file: %v", err)
}
defer f.Close()
data, err := ioutil.ReadFile(f.Name())
if err != nil {
t.Error("unexpected error reading json file: ", err)
}
var dat config.TemplateConfig
if err := json.Unmarshal(data, &dat); err != nil {
t.Errorf("unexpected error unmarshalling json: %v", err)
}
if dat.ListenPorts == nil {
dat.ListenPorts = &config.ListenPorts{}
}
tf, err := os.Open(path.Join(pwd, "../../rootfs/etc/nginx/template/nginx.tmpl"))
if err != nil {
t.Errorf("unexpected error reading json file: %v", err)
}
defer tf.Close()
ngxTpl, err := NewTemplate(tf.Name(), func() {})
if err != nil {
t.Errorf("invalid NGINX template: %v", err)
}
_, err = ngxTpl.Write(dat)
if err != nil {
t.Errorf("invalid NGINX template: %v", err)
}
}
func BenchmarkTemplateWithData(b *testing.B) {
pwd, _ := os.Getwd()
f, err := os.Open(path.Join(pwd, "../../test/data/config.json"))
if err != nil {
b.Errorf("unexpected error reading json file: %v", err)
}
defer f.Close()
data, err := ioutil.ReadFile(f.Name())
if err != nil {
b.Error("unexpected error reading json file: ", err)
}
var dat config.TemplateConfig
if err := json.Unmarshal(data, &dat); err != nil {
b.Errorf("unexpected error unmarshalling json: %v", err)
}
tf, err := os.Open(path.Join(pwd, "../../rootfs/etc/nginx/template/nginx.tmpl"))
if err != nil {
b.Errorf("unexpected error reading json file: %v", err)
}
defer tf.Close()
ngxTpl, err := NewTemplate(tf.Name(), func() {})
if err != nil {
b.Errorf("invalid NGINX template: %v", err)
}
for i := 0; i < b.N; i++ {
ngxTpl.Write(dat)
}
}
func TestBuildDenyVariable(t *testing.T) {
a := buildDenyVariable("host1.example.com_/.well-known/acme-challenge")
b := buildDenyVariable("host1.example.com_/.well-known/acme-challenge")
if !reflect.DeepEqual(a, b) {
t.Errorf("Expected '%v' but returned '%v'", a, b)
}
}
func TestBuildClientBodyBufferSize(t *testing.T) {
a := isValidClientBodyBufferSize("1000")
if a != true {
t.Errorf("Expected '%v' but returned '%v'", true, a)
}
b := isValidClientBodyBufferSize("1000k")
if b != true {
t.Errorf("Expected '%v' but returned '%v'", true, b)
}
c := isValidClientBodyBufferSize("1000m")
if c != true {
t.Errorf("Expected '%v' but returned '%v'", true, c)
}
d := isValidClientBodyBufferSize("1000km")
if d != false {
t.Errorf("Expected '%v' but returned '%v'", false, d)
}
e := isValidClientBodyBufferSize("1000mk")
if e != false {
t.Errorf("Expected '%v' but returned '%v'", false, e)
}
f := isValidClientBodyBufferSize("1000kk")
if f != false {
t.Errorf("Expected '%v' but returned '%v'", false, f)
}
g := isValidClientBodyBufferSize("1000mm")
if g != false {
t.Errorf("Expected '%v' but returned '%v'", false, g)
}
h := isValidClientBodyBufferSize(nil)
if h != false {
t.Errorf("Expected '%v' but returned '%v'", false, h)
}
i := isValidClientBodyBufferSize("")
if i != false {
t.Errorf("Expected '%v' but returned '%v'", false, i)
}
}
func TestIsLocationAllowed(t *testing.T) {
loc := ingress.Location{
Denied: nil,
}
isAllowed := isLocationAllowed(&loc)
if !isAllowed {
t.Errorf("Expected '%v' but returned '%v'", true, isAllowed)
}
}
func TestBuildForwardedFor(t *testing.T) {
inputStr := "X-Forwarded-For"
outputStr := buildForwardedFor(inputStr)
validStr := "$http_x_forwarded_for"
if outputStr != validStr {
t.Errorf("Expected '%v' but returned '%v'", validStr, outputStr)
}
}
func TestBuildResolvers(t *testing.T) {
ipOne := net.ParseIP("192.0.0.1")
ipTwo := net.ParseIP("2001:db8:1234:0000:0000:0000:0000:0000")
ipList := []net.IP{ipOne, ipTwo}
validResolver := "resolver 192.0.0.1 [2001:db8:1234::] valid=30s;"
resolver := buildResolvers(ipList)
if resolver != validResolver {
t.Errorf("Expected '%v' but returned '%v'", validResolver, resolver)
}
}
func TestBuildNextUpstream(t *testing.T) {
nextUpstream := "timeout http_500 http_502 non_idempotent"
validNextUpstream := "timeout http_500 http_502"
buildNextUpstream := buildNextUpstream(nextUpstream)
if buildNextUpstream != validNextUpstream {
t.Errorf("Expected '%v' but returned '%v'", validNextUpstream, buildNextUpstream)
}
}
func TestBuildRateLimit(t *testing.T) {
loc := &ingress.Location{}
loc.RateLimit.Connections.Name = "con"
loc.RateLimit.Connections.Limit = 1
loc.RateLimit.RPS.Name = "rps"
loc.RateLimit.RPS.Limit = 1
loc.RateLimit.RPS.Burst = 1
loc.RateLimit.RPM.Name = "rpm"
loc.RateLimit.RPM.Limit = 2
loc.RateLimit.RPM.Burst = 2
loc.RateLimit.LimitRateAfter = 1
loc.RateLimit.LimitRate = 1
validLimits := []string{
"limit_conn con 1;",
"limit_req zone=rps burst=1 nodelay;",
"limit_req zone=rpm burst=2 nodelay;",
"limit_rate_after 1k;",
"limit_rate 1k;",
}
limits := buildRateLimit(loc)
for i, limit := range limits {
if limit != validLimits[i] {
t.Errorf("Expected '%v' but returned '%v'", validLimits, limits)
}
}
}
func TestBuildAuthSignURL(t *testing.T) {
cases := map[string]struct {
Input, Output string
}{
"default url": {"http://google.com", "http://google.com?rd=$scheme://$http_host$request_uri"},
"with random field": {"http://google.com?cat=0", "http://google.com?cat=0&rd=$scheme://$http_host$request_uri"},
"with rd field": {"http://google.com?cat&rd=$request", "http://google.com?cat&rd=$request"},
}
for k, tc := range cases {
res := buildAuthSignURL(tc.Input)
if res != tc.Output {
t.Errorf("%s: called buildAuthSignURL('%s'); expected '%v' but returned '%v'", k, tc.Input, tc.Output, res)
}
}
}

26
pkg/version/version.go Normal file
View file

@ -0,0 +1,26 @@
/*
Copyright 2015 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 returns the release version
RELEASE = "UNKNOWN"
// REPO returns the git repository URL
REPO = "UNKNOWN"
// COMMIT returns the short sha from git
COMMIT = "UNKNOWN"
)