Live Nginx (re)configuration without reloading (#2174)

This commit is contained in:
Elvin Efendi 2018-03-18 09:13:41 -04:00 committed by Manuel Alejandro de Brito Fontes
parent 41cefeb178
commit c90a4e811e
13 changed files with 759 additions and 114 deletions

File diff suppressed because one or more lines are too long

View file

@ -604,23 +604,24 @@ func (cfg Configuration) BuildLogFormatUpstream() string {
// 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
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
DynamicConfigurationEnabled bool
}
// ListenPorts describe the ports required to run the

View file

@ -95,6 +95,8 @@ type Configuration struct {
FakeCertificateSHA string
SyncRateLimit float32
DynamicConfigurationEnabled bool
}
// GetPublishService returns the configured service used to set ingress status
@ -167,6 +169,15 @@ func (n *NGINXController) syncIngress(item interface{}) error {
if !n.isForceReload() && n.runningConfig.Equal(&pcfg) {
glog.V(3).Infof("skipping backend reload (no changes detected)")
return nil
} else if !n.isForceReload() && n.cfg.DynamicConfigurationEnabled && n.IsDynamicallyConfigurable(&pcfg) {
err := n.ConfigureDynamically(&pcfg)
if err == nil {
glog.Infof("dynamic reconfiguration succeeded, skipping reload")
n.runningConfig = &pcfg
return nil
}
glog.Warningf("falling back to reload, could not dynamically reconfigure: %v", err)
}
glog.Infof("backend reload required")
@ -182,6 +193,19 @@ func (n *NGINXController) syncIngress(item interface{}) error {
incReloadCount()
setSSLExpireTime(servers)
if n.isForceReload() && n.cfg.DynamicConfigurationEnabled {
go func() {
// it takes time for Nginx to start listening on the port
time.Sleep(1 * time.Second)
err := n.ConfigureDynamically(&pcfg)
if err == nil {
glog.Infof("dynamic reconfiguration succeeded")
} else {
glog.Warningf("could not dynamically reconfigure: %v", err)
}
}()
}
n.runningConfig = &pcfg
n.SetForceReload(false)

View file

@ -18,10 +18,12 @@ package controller
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net"
"net/http"
"os"
"os/exec"
"strconv"
@ -606,23 +608,24 @@ func (n *NGINXController) OnUpdate(ingressCfg ingress.Configuration) error {
cfg.SSLDHParam = sslDHParam
tc := ngx_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.cfg.EnableSSLPassthrough,
ListenPorts: n.cfg.ListenPorts,
PublishService: n.GetPublishService(),
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.cfg.EnableSSLPassthrough,
ListenPorts: n.cfg.ListenPorts,
PublishService: n.GetPublishService(),
DynamicConfigurationEnabled: n.cfg.DynamicConfigurationEnabled,
}
content, err := n.t.Write(tc)
@ -745,3 +748,43 @@ func (n *NGINXController) setupSSLProxy() {
}
}()
}
// IsDynamicallyConfigurable decides if the new configuration can be dynamically configured without reloading
func (n *NGINXController) IsDynamicallyConfigurable(pcfg *ingress.Configuration) bool {
var copyOfRunningConfig ingress.Configuration = *n.runningConfig
var copyOfPcfg ingress.Configuration = *pcfg
copyOfRunningConfig.Backends = []*ingress.Backend{}
copyOfPcfg.Backends = []*ingress.Backend{}
return copyOfRunningConfig.Equal(&copyOfPcfg)
}
// ConfigureDynamically JSON encodes new Backends and POSTs it to an internal HTTP endpoint
// that is handled by Lua
func (n *NGINXController) ConfigureDynamically(pcfg *ingress.Configuration) error {
buf, err := json.Marshal(pcfg.Backends)
if err != nil {
return err
}
glog.V(2).Infof("posting backends configuration: %s", buf)
url := fmt.Sprintf("http://localhost:%d/configuration/backends", n.cfg.ListenPorts.Status)
resp, err := http.Post(url, "application/json", bytes.NewReader(buf))
if err != nil {
return err
}
defer func() {
if err := resp.Body.Close(); err != nil {
glog.Warningf("error while closing response body: \n%v", err)
}
}()
if resp.StatusCode != http.StatusCreated {
return fmt.Errorf("Unexpected error code: %d", resp.StatusCode)
}
return nil
}

View file

@ -16,7 +16,78 @@ limitations under the License.
package controller
import "testing"
import (
"testing"
"k8s.io/ingress-nginx/internal/ingress"
)
func TestIsDynamicallyConfigurable(t *testing.T) {
backends := []*ingress.Backend{{
Name: "fakenamespace-myapp-80",
Endpoints: []ingress.Endpoint{
{
Address: "10.0.0.1",
Port: "8080",
},
{
Address: "10.0.0.2",
Port: "8080",
},
},
}}
servers := []*ingress.Server{{
Hostname: "myapp.fake",
Locations: []*ingress.Location{
{
Path: "/",
Backend: "fakenamespace-myapp-80",
},
},
}}
commonConfig := &ingress.Configuration{
Backends: backends,
Servers: servers,
}
n := &NGINXController{
runningConfig: &ingress.Configuration{
Backends: backends,
Servers: servers,
},
}
newConfig := commonConfig
if !n.IsDynamicallyConfigurable(newConfig) {
t.Errorf("When new config is same as the running config it should be deemed as dynamically configurable")
}
newConfig = &ingress.Configuration{
Backends: []*ingress.Backend{{Name: "another-backend-8081"}},
Servers: []*ingress.Server{{Hostname: "myapp1.fake"}},
}
if n.IsDynamicallyConfigurable(newConfig) {
t.Errorf("Expected to not be dynamically configurable when there's more than just backends change")
}
newConfig = &ingress.Configuration{
Backends: []*ingress.Backend{{Name: "a-backend-8080"}},
Servers: servers,
}
if !n.IsDynamicallyConfigurable(newConfig) {
t.Errorf("Expected to be dynamically configurable when only backends change")
}
if !n.runningConfig.Equal(commonConfig) {
t.Errorf("Expected running config to not change")
}
if !newConfig.Equal(&ingress.Configuration{Backends: []*ingress.Backend{{Name: "a-backend-8080"}}, Servers: servers}) {
t.Errorf("Expected new config to not change")
}
}
func TestNginxHashBucketSize(t *testing.T) {
tests := []struct {

View file

@ -307,7 +307,7 @@ func buildLoadBalancingConfig(b interface{}, fallbackLoadBalancing string) strin
// (specified through the nginx.ingress.kubernetes.io/rewrite-to annotation)
// If the annotation nginx.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 {
func buildProxyPass(host string, b interface{}, loc interface{}, dynamicConfigurationEnabled bool) string {
backends, ok := b.([]*ingress.Backend)
if !ok {
glog.Errorf("expected an '[]*ingress.Backend' type but %T was returned", b)
@ -323,14 +323,19 @@ func buildProxyPass(host string, b interface{}, loc interface{}) string {
path := location.Path
proto := "http"
upstreamName := location.Backend
upstreamName := "upstream_balancer"
if !dynamicConfigurationEnabled {
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) {
if !dynamicConfigurationEnabled && isSticky(host, location, backend.SessionAffinity.CookieSessionAffinity.Locations) {
upstreamName = fmt.Sprintf("sticky-%v", upstreamName)
}
@ -340,6 +345,7 @@ func buildProxyPass(host string, b interface{}, loc interface{}) string {
// 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

View file

@ -37,72 +37,262 @@ import (
)
var (
// TODO: add tests for secure endpoints
// TODO: add tests for SSLPassthrough
tmplFuncTestcases = map[string]struct {
Path string
Target string
Location string
ProxyPass string
AddBaseURL bool
BaseURLScheme string
Sticky bool
XForwardedPrefix bool
Path string
Target string
Location string
ProxyPass string
AddBaseURL bool
BaseURLScheme string
Sticky bool
XForwardedPrefix bool
DynamicConfigurationEnabled bool
SecureBackend bool
}{
"invalid redirect / to /": {"/", "/", "/", "proxy_pass http://upstream-name;", false, "", false, false},
"redirect / to /jenkins": {"/", "/jenkins", "~* /",
"when secure backend enabled": {
"/",
"/",
"/",
"proxy_pass https://upstream-name;",
false,
"",
false,
false,
false,
true},
"when secure backend and stickeness enabled": {
"/",
"/",
"/",
"proxy_pass https://sticky-upstream-name;",
false,
"",
true,
false,
false,
true},
"when secure backend and dynamic config enabled": {
"/",
"/",
"/",
"proxy_pass https://upstream_balancer;",
false,
"",
false,
false,
true,
true},
"when secure backend, stickeness and dynamic config enabled": {
"/",
"/",
"/",
"proxy_pass https://upstream_balancer;",
false,
"",
true,
false,
true,
true},
"invalid redirect / to / with dynamic config enabled": {
"/",
"/",
"/",
"proxy_pass http://upstream_balancer;",
false,
"",
false,
false,
true,
false},
"invalid redirect / to /": {
"/",
"/",
"/",
"proxy_pass http://upstream-name;",
false,
"",
false,
false,
false,
false},
"redirect / to /jenkins": {
"/",
"/jenkins",
"~* /",
`
rewrite /(.*) /jenkins/$1 break;
proxy_pass http://upstream-name;
`, false, "", false, false},
"redirect /something to /": {"/something", "/", `~* ^/something\/?(?<baseuri>.*)`, `
`,
false,
"",
false,
false,
false,
false},
"redirect /something to /": {
"/something",
"/",
`~* ^/something\/?(?<baseuri>.*)`,
`
rewrite /something/(.*) /$1 break;
rewrite /something / break;
proxy_pass http://upstream-name;
`, false, "", false, false},
"redirect /end-with-slash/ to /not-root": {"/end-with-slash/", "/not-root", "~* ^/end-with-slash/(?<baseuri>.*)", `
`,
false,
"",
false,
false,
false,
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, "", false, false},
"redirect /something-complex to /not-root": {"/something-complex", "/not-root", `~* ^/something-complex\/?(?<baseuri>.*)`, `
`,
false,
"",
false,
false,
false,
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, "", false, false},
"redirect / to /jenkins and rewrite": {"/", "/jenkins", "~* /", `
`,
false,
"",
false,
false,
false,
false},
"redirect / to /jenkins and rewrite": {
"/",
"/jenkins",
"~* /",
`
rewrite /(.*) /jenkins/$1 break;
proxy_pass http://upstream-name;
subs_filter '(<(?:H|h)(?:E|e)(?:A|a)(?:D|d)(?:[^">]|"[^"]*")*>)' '$1<base href="$scheme://$http_host/$baseuri">' ro;
`, true, "", false, false},
"redirect /something to / and rewrite": {"/something", "/", `~* ^/something\/?(?<baseuri>.*)`, `
`,
true,
"",
false,
false,
false,
false},
"redirect /something to / and rewrite": {
"/something",
"/",
`~* ^/something\/?(?<baseuri>.*)`,
`
rewrite /something/(.*) /$1 break;
rewrite /something / break;
proxy_pass http://upstream-name;
subs_filter '(<(?:H|h)(?:E|e)(?:A|a)(?:D|d)(?:[^">]|"[^"]*")*>)' '$1<base href="$scheme://$http_host/something/$baseuri">' ro;
`, true, "", false, false},
"redirect /end-with-slash/ to /not-root and rewrite": {"/end-with-slash/", "/not-root", `~* ^/end-with-slash/(?<baseuri>.*)`, `
`,
true,
"",
false,
false,
false,
false},
"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 '(<(?:H|h)(?:E|e)(?:A|a)(?:D|d)(?:[^">]|"[^"]*")*>)' '$1<base href="$scheme://$http_host/end-with-slash/$baseuri">' ro;
`, true, "", false, false},
"redirect /something-complex to /not-root and rewrite": {"/something-complex", "/not-root", `~* ^/something-complex\/?(?<baseuri>.*)`, `
`,
true,
"",
false,
false,
false,
false},
"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 '(<(?:H|h)(?:E|e)(?:A|a)(?:D|d)(?:[^">]|"[^"]*")*>)' '$1<base href="$scheme://$http_host/something-complex/$baseuri">' ro;
`, true, "", false, false},
"redirect /something to / and rewrite with specific scheme": {"/something", "/", `~* ^/something\/?(?<baseuri>.*)`, `
`,
true,
"",
false,
false,
false,
false},
"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 '(<(?:H|h)(?:E|e)(?:A|a)(?:D|d)(?:[^">]|"[^"]*")*>)' '$1<base href="http://$http_host/something/$baseuri">' ro;
`, true, "http", false, false},
"redirect / to /something with sticky enabled": {"/", "/something", `~* /`, `
`,
true,
"http",
false,
false,
false,
false},
"redirect / to /something with sticky enabled": {
"/",
"/something",
`~* /`,
`
rewrite /(.*) /something/$1 break;
proxy_pass http://sticky-upstream-name;
`, false, "http", true, false},
"add the X-Forwarded-Prefix header": {"/there", "/something", `~* ^/there\/?(?<baseuri>.*)`, `
`,
false,
"http",
true,
false,
false,
false},
"redirect / to /something with sticky and dynamic config enabled": {
"/",
"/something",
`~* /`,
`
rewrite /(.*) /something/$1 break;
proxy_pass http://upstream_balancer;
`,
false,
"http",
true,
false,
true,
false},
"add the X-Forwarded-Prefix header": {
"/there",
"/something",
`~* ^/there\/?(?<baseuri>.*)`,
`
rewrite /there/(.*) /something/$1 break;
proxy_set_header X-Forwarded-Prefix "/there/";
proxy_pass http://sticky-upstream-name;
`, false, "http", true, true},
`,
false,
"http",
true,
true,
false,
false},
}
)
@ -151,24 +341,25 @@ func TestBuildProxyPass(t *testing.T) {
XForwardedPrefix: tc.XForwardedPrefix,
}
backends := []*ingress.Backend{}
backend := &ingress.Backend{
Name: defaultBackend,
Secure: tc.SecureBackend,
}
if tc.Sticky {
backends = []*ingress.Backend{
{
Name: defaultBackend,
SessionAffinity: ingress.SessionAffinityConfig{
AffinityType: "cookie",
CookieSessionAffinity: ingress.CookieSessionAffinity{
Locations: map[string][]string{
defaultHost: {tc.Path},
},
},
backend.SessionAffinity = ingress.SessionAffinityConfig{
AffinityType: "cookie",
CookieSessionAffinity: ingress.CookieSessionAffinity{
Locations: map[string][]string{
defaultHost: {tc.Path},
},
},
}
}
pp := buildProxyPass(defaultHost, backends, loc)
backends := []*ingress.Backend{backend}
pp := buildProxyPass(defaultHost, backends, loc, tc.DynamicConfigurationEnabled)
if !strings.EqualFold(tc.ProxyPass, pp) {
t.Errorf("%s: expected \n'%v'\nbut returned \n'%v'", k, tc.ProxyPass, pp)
}