ingress-nginx-helm/controllers/nginx/pkg/cmd/controller/nginx.go

592 lines
15 KiB
Go
Raw Normal View History

/*
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"
2016-11-29 01:39:17 +00:00
"errors"
"fmt"
"io/ioutil"
2016-11-29 01:39:17 +00:00
"net"
2016-11-27 00:09:59 +00:00
"net/http"
"os"
"os/exec"
"strconv"
"strings"
2016-11-29 01:39:17 +00:00
"syscall"
"time"
"github.com/golang/glog"
"github.com/spf13/pflag"
proxyproto "github.com/armon/go-proxyproto"
2017-04-01 14:39:42 +00:00
api_v1 "k8s.io/client-go/pkg/api/v1"
2016-11-16 18:24:26 +00:00
"k8s.io/ingress/controllers/nginx/pkg/config"
ngx_template "k8s.io/ingress/controllers/nginx/pkg/template"
"k8s.io/ingress/controllers/nginx/pkg/version"
2016-11-29 01:39:17 +00:00
"k8s.io/ingress/core/pkg/ingress"
"k8s.io/ingress/core/pkg/ingress/defaults"
2017-04-11 14:47:49 +00:00
"k8s.io/ingress/core/pkg/net/dns"
"k8s.io/ingress/core/pkg/net/ssl"
2016-11-29 01:39:17 +00:00
)
type statusModule string
2016-11-29 01:39:17 +00:00
const (
ngxHealthPort = 18080
ngxHealthPath = "/healthz"
defaultStatusModule statusModule = "default"
vtsStatusModule statusModule = "vts"
errNoChild = "wait: no child processes"
)
var (
2017-03-02 15:50:31 +00:00
tmplPath = "/etc/nginx/template/nginx.tmpl"
cfgPath = "/etc/nginx/nginx.conf"
binary = "/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() ingress.Controller {
ngx := os.Getenv("NGINX_BINARY")
if ngx == "" {
ngx = binary
}
2017-04-11 14:47:49 +00:00
h, err := dns.GetSystemNameServers()
if err != nil {
glog.Warningf("unexpected error reading system nameservers: %v", err)
}
2017-03-12 15:27:05 +00:00
n := &NGINXController{
binary: ngx,
configmap: &api_v1.ConfigMap{},
isIPV6Enabled: isIPv6Enabled(),
2017-04-11 14:47:49 +00:00
resolver: h,
proxy: &proxy{
Default: &server{
Hostname: "localhost",
IP: "127.0.0.1",
Port: 442,
},
},
}
listener, err := net.Listen("tcp", ":443")
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
}
2017-04-20 19:48:14 +00:00
glog.V(3).Infof("remote address %s to local %s", conn.RemoteAddr(), conn.LocalAddr())
go n.proxy.Handle(conn)
}
}()
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
2016-11-29 01:39:17 +00:00
2016-11-16 18:24:26 +00:00
go n.Start()
2017-03-12 15:27:05 +00:00
return ingress.Controller(n)
}
// NGINXController ...
type NGINXController struct {
t *ngx_template.Template
2017-04-01 14:39:42 +00:00
configmap *api_v1.ConfigMap
storeLister ingress.StoreLister
binary string
resolver []net.IP
cmdArgs []string
2017-03-12 15:27:05 +00:00
watchClass string
namespace string
stats *statsCollector
statusModule statusModule
// returns true if IPV6 is enabled in the pod
isIPV6Enabled bool
2017-04-11 14:47:49 +00:00
// returns true if proxy protocol es enabled
isProxyProtocolEnabled bool
proxy *proxy
}
2016-11-16 18:24:26 +00:00
// Start start a new NGINX master process running in foreground.
2017-03-12 15:27:05 +00:00
func (n *NGINXController) Start() {
glog.Info("starting NGINX process...")
2016-11-29 01:39:17 +00:00
done := make(chan error, 1)
cmd := exec.Command(n.binary, "-c", cfgPath)
2016-11-29 01:39:17 +00:00
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 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 {
2016-11-29 01:39:17 +00:00
break
}
conn.Close()
2016-11-29 01:39:17 +00:00
time.Sleep(1 * time.Second)
}
// start a new nginx master process
n.start(cmd, done)
}
}
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)
2016-11-29 01:39:17 +00:00
done <- err
return
}
n.cmdArgs = cmd.Args
2016-11-29 01:39:17 +00:00
go func() {
done <- cmd.Wait()
}()
}
2016-11-16 18:24:26 +00:00
// Reload checks if the running configuration file is different
// to the specified and reload nginx if required
2016-11-24 00:14:14 +00:00
func (n NGINXController) Reload(data []byte) ([]byte, bool, error) {
2016-11-16 18:24:26 +00:00
if !n.isReloadRequired(data) {
2016-11-24 00:14:14 +00:00
return []byte("Reload not required"), false, nil
2016-11-16 18:24:26 +00:00
}
err := ioutil.WriteFile(cfgPath, data, 0644)
if err != nil {
2016-11-24 00:14:14 +00:00
return nil, false, err
}
2016-11-24 00:14:14 +00:00
o, e := exec.Command(n.binary, "-s", "reload").CombinedOutput()
2016-11-24 00:14:14 +00:00
return o, true, e
}
2016-11-16 18:24:26 +00:00
// BackendDefaults returns the nginx defaults
func (n NGINXController) BackendDefaults() defaults.Backend {
if n.configmap == nil {
d := config.NewDefault()
return d.Backend
}
return ngx_template.ReadConfig(n.configmap.Data).Backend
}
// isReloadRequired check if the new configuration file is different
// from the current one.
2016-11-16 18:24:26 +00:00
func (n NGINXController) isReloadRequired(data []byte) bool {
in, err := os.Open(cfgPath)
if err != nil {
return false
}
src, err := ioutil.ReadAll(in)
in.Close()
if err != nil {
return false
}
if !bytes.Equal(src, data) {
tmpfile, err := ioutil.TempFile("", "nginx-cfg-diff")
if err != nil {
glog.Errorf("error creating temporal file: %s", err)
return false
}
defer tmpfile.Close()
err = ioutil.WriteFile(tmpfile.Name(), data, 0644)
if err != nil {
return false
}
diffOutput, err := diff(src, data)
if err != nil {
glog.Errorf("error computing diff: %s", err)
return true
}
if glog.V(2) {
glog.Infof("NGINX configuration diff\n")
glog.Infof("%v", string(diffOutput))
}
2017-03-07 14:23:19 +00:00
os.Remove(tmpfile.Name())
return len(diffOutput) > 0
}
return false
}
// Info return build information
2016-11-16 18:24:26 +00:00
func (n NGINXController) Info() *ingress.BackendInfo {
return &ingress.BackendInfo{
Name: "NGINX",
Release: version.RELEASE,
Build: version.COMMIT,
Repository: version.REPO,
}
}
// OverrideFlags customize NGINX controller flags
2017-03-12 15:27:05 +00:00
func (n *NGINXController) OverrideFlags(flags *pflag.FlagSet) {
ic, _ := flags.GetString("ingress-class")
wc, _ := flags.GetString("watch-namespace")
if ic == "" {
ic = defIngressClass
}
2017-03-12 15:27:05 +00:00
if ic != defIngressClass {
glog.Warningf("only Ingress with class %v will be processed by this ingress controller", ic)
}
flags.Set("ingress-class", ic)
n.stats = newStatsCollector(wc, ic, n.binary)
2017-03-02 15:50:31 +00:00
}
// 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()
2017-03-03 07:17:32 +00:00
err = ioutil.WriteFile(tmpfile.Name(), cfg, 0644)
if err != nil {
return err
}
2016-11-24 00:14:14 +00:00
out, err := exec.Command(n.binary, "-t", "-c", tmpfile.Name()).CombinedOutput()
if err != nil && err.Error() != errNoChild {
// this error is different from the rest because it must be clear why nginx is not working
2016-11-16 18:24:26 +00:00
oe := fmt.Sprintf(`
-------------------------------------------------------------------------------
Error: %v
%v
-------------------------------------------------------------------------------
`, err, string(out))
2016-11-16 18:24:26 +00:00
return errors.New(oe)
}
os.Remove(tmpfile.Name())
return nil
}
// SetConfig sets the configured configmap
2017-04-01 14:39:42 +00:00
func (n *NGINXController) SetConfig(cmap *api_v1.ConfigMap) {
n.configmap = cmap
n.isProxyProtocolEnabled = false
if cmap == nil {
return
}
val, ok := cmap.Data["use-proxy-protocol"]
if ok {
b, err := strconv.ParseBool(val)
if err == nil {
n.isProxyProtocolEnabled = b
return
}
}
}
// SetListers sets the configured store listers in the generic ingress controller
func (n *NGINXController) SetListers(lister ingress.StoreLister) {
n.storeLister = lister
}
// OnUpdate is called by syncQueue in https://github.com/aledbf/ingress-controller/blob/master/pkg/ingress/controller/controller.go#L82
// 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) ([]byte, error) {
var longestName int
for _, srv := range ingressCfg.Servers {
2016-11-16 18:24:26 +00:00
if longestName < len(srv.Hostname) {
longestName = len(srv.Hostname)
}
}
cfg := ngx_template.ReadConfig(n.configmap.Data)
2017-04-11 14:47:49 +00:00
cfg.Resolver = n.resolver
// we need to check if the status module configuration changed
2017-03-12 15:27:05 +00:00
if cfg.EnableVtsStatus {
n.setupMonitor(vtsStatusModule)
} else {
n.setupMonitor(defaultStatusModule)
}
// NGINX cannot resize the has 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
nameHashBucketSize := nginxHashBucketSize(longestName)
if cfg.ServerNameHashBucketSize == 0 {
glog.V(3).Infof("adjusting ServerNameHashBucketSize variable to %v", nameHashBucketSize)
cfg.ServerNameHashBucketSize = nameHashBucketSize
}
serverNameHashMaxSize := nextPowerOf2(len(ingressCfg.Servers))
if cfg.ServerNameHashMaxSize == 0 {
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)
if err != nil {
wp = 1
}
maxOpenFiles := (sysctlFSFileMax() / wp) - 1024
if maxOpenFiles < 0 {
// 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 {
2017-04-01 14:39:42 +00:00
setHeaders = cmap.(*api_v1.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 {
2017-04-01 14:39:42 +00:00
secret := s.(*api_v1.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
content, err := n.t.Write(config.TemplateConfig{
ProxySetHeaders: setHeaders,
MaxOpenFiles: maxOpenFiles,
BacklogSize: sysctlSomaxconn(),
Backends: ingressCfg.Backends,
PassthroughBackends: ingressCfg.PassthroughBackends,
Servers: ingressCfg.Servers,
TCPBackends: ingressCfg.TCPEndpoints,
2017-02-07 06:35:39 +00:00
UDPBackends: ingressCfg.UDPEndpoints,
2017-01-18 03:29:59 +00:00
HealthzURI: ngxHealthPath,
CustomErrors: len(cfg.CustomHTTPErrors) > 0,
Cfg: cfg,
IsIPV6Enabled: n.isIPV6Enabled && !cfg.DisableIpv6,
})
if err != nil {
return nil, err
}
if err := n.testTemplate(content); err != nil {
return nil, err
}
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
}
}
}
servers = append(servers, &server{
Hostname: pb.Hostname,
IP: svc.Spec.ClusterIP,
Port: port,
})
}
n.proxy.ServerList = servers
return content, 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)
}
2016-11-27 00:09:59 +00:00
// 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 {
2016-11-29 01:39:17 +00:00
res, err := http.Get(fmt.Sprintf("http://localhost:%v%v", ngxHealthPort, ngxHealthPath))
2016-11-27 00:09:59 +00:00
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode != 200 {
return fmt.Errorf("ingress controller is not healthy")
2016-11-27 00:09:59 +00:00
}
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
}