Skip to content

Commit aa0f464

Browse files
isaacisaachawley
authored andcommitted
Verify after reloading
- Write config version directly in nginx config - Check config version by querying over socket - Add config version check to API - Atomic write (write and rename) for version config
1 parent 92730bc commit aa0f464

File tree

10 files changed

+315
-29
lines changed

10 files changed

+315
-29
lines changed

cmd/nginx-ingress/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,7 @@ func main() {
214214
glog.Fatalf("Error generating NGINX main config: %v", err)
215215
}
216216
ngxc.UpdateMainConfigFile(content)
217+
ngxc.UpdateConfigVersionFile()
217218

218219
nginxDone := make(chan error, 1)
219220
ngxc.Start(nginxDone)

internal/nginx/configurator.go

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1041,7 +1041,7 @@ func (cnf *Configurator) updatePlusEndpoints(ingEx *IngressEx) error {
10411041
name := getNameForUpstream(ingEx.Ingress, emptyHost, ingEx.Ingress.Spec.Backend)
10421042
endps, exists := ingEx.Endpoints[ingEx.Ingress.Spec.Backend.ServiceName+ingEx.Ingress.Spec.Backend.ServicePort.String()]
10431043
if exists {
1044-
err := cnf.nginxAPI.UpdateServers(name, endps, cfg)
1044+
err := cnf.nginxAPI.UpdateServers(name, endps, cfg, cnf.nginx.configVersion)
10451045
if err != nil {
10461046
return fmt.Errorf("Couldn't update the endpoints for %v: %v", name, err)
10471047
}
@@ -1055,7 +1055,7 @@ func (cnf *Configurator) updatePlusEndpoints(ingEx *IngressEx) error {
10551055
name := getNameForUpstream(ingEx.Ingress, rule.Host, &path.Backend)
10561056
endps, exists := ingEx.Endpoints[path.Backend.ServiceName+path.Backend.ServicePort.String()]
10571057
if exists {
1058-
err := cnf.nginxAPI.UpdateServers(name, endps, cfg)
1058+
err := cnf.nginxAPI.UpdateServers(name, endps, cfg, cnf.nginx.configVersion)
10591059
if err != nil {
10601060
return fmt.Errorf("Couldn't update the endpoints for %v: %v", name, err)
10611061
}
@@ -1117,14 +1117,14 @@ func GenerateNginxMainConfig(config *Config) *MainConfig {
11171117
SSLCiphers: config.MainServerSSLCiphers,
11181118
SSLDHParam: config.MainServerSSLDHParam,
11191119
SSLPreferServerCiphers: config.MainServerSSLPreferServerCiphers,
1120-
HTTP2: config.HTTP2,
1121-
ServerTokens: config.ServerTokens,
1122-
ProxyProtocol: config.ProxyProtocol,
1123-
WorkerProcesses: config.MainWorkerProcesses,
1124-
WorkerCPUAffinity: config.MainWorkerCPUAffinity,
1125-
WorkerShutdownTimeout: config.MainWorkerShutdownTimeout,
1126-
WorkerConnections: config.MainWorkerConnections,
1127-
WorkerRlimitNofile: config.MainWorkerRlimitNofile,
1120+
HTTP2: config.HTTP2,
1121+
ServerTokens: config.ServerTokens,
1122+
ProxyProtocol: config.ProxyProtocol,
1123+
WorkerProcesses: config.MainWorkerProcesses,
1124+
WorkerCPUAffinity: config.MainWorkerCPUAffinity,
1125+
WorkerShutdownTimeout: config.MainWorkerShutdownTimeout,
1126+
WorkerConnections: config.MainWorkerConnections,
1127+
WorkerRlimitNofile: config.MainWorkerRlimitNofile,
11281128
}
11291129
return nginxCfg
11301130
}

internal/nginx/nginx.go

Lines changed: 68 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"path"
1010

1111
"github.com/golang/glog"
12+
"github.com/nginxinc/kubernetes-ingress/internal/nginx/verify"
1213
)
1314

1415
const dhparamFilename = "dhparam.pem"
@@ -19,9 +20,12 @@ const jwkSecretFileMode = 0644
1920

2021
// Controller updates NGINX configuration, starts and reloads NGINX
2122
type Controller struct {
22-
nginxConfdPath string
23-
nginxSecretsPath string
24-
local bool
23+
nginxConfdPath string
24+
nginxSecretsPath string
25+
local bool
26+
verifyConfigGenerator *verify.ConfigGenerator
27+
verifyClient *verify.Client
28+
configVersion int
2529
}
2630

2731
// IngressNginxConfig describes an NGINX configuration
@@ -187,10 +191,18 @@ func NewUpstreamWithDefaultServer(name string) Upstream {
187191

188192
// NewNginxController creates a NGINX controller
189193
func NewNginxController(nginxConfPath string, local bool) *Controller {
194+
verifyConfigGenerator, err := verify.NewConfigGenerator()
195+
if err != nil {
196+
glog.Fatalf("error instantiating a verify.ConfigGenerator: %v", err)
197+
}
198+
190199
ngxc := Controller{
191-
nginxConfdPath: path.Join(nginxConfPath, "conf.d"),
192-
nginxSecretsPath: path.Join(nginxConfPath, "secrets"),
193-
local: local,
200+
nginxConfdPath: path.Join(nginxConfPath, "conf.d"),
201+
nginxSecretsPath: path.Join(nginxConfPath, "secrets"),
202+
local: local,
203+
verifyConfigGenerator: verifyConfigGenerator,
204+
configVersion: 0,
205+
verifyClient: verify.NewClient(),
194206
}
195207

196208
return &ngxc
@@ -271,7 +283,6 @@ func (nginx *Controller) DeleteSecretFile(name string) {
271283
glog.Warningf("Failed to delete %v: %v", filename, err)
272284
}
273285
}
274-
275286
}
276287

277288
func (nginx *Controller) getIngressNginxConfigFileName(name string) string {
@@ -284,16 +295,23 @@ func (nginx *Controller) getSecretFileName(name string) string {
284295

285296
// Reload reloads NGINX
286297
func (nginx *Controller) Reload() error {
287-
if !nginx.local {
288-
if err := shellOut("nginx -t"); err != nil {
289-
return fmt.Errorf("Invalid nginx configuration detected, not reloading: %s", err)
290-
}
291-
if err := shellOut("nginx -s reload"); err != nil {
292-
return fmt.Errorf("Reloading NGINX failed: %s", err)
293-
}
294-
} else {
295-
glog.V(3).Info("Reloading nginx")
298+
if nginx.local {
299+
glog.V(3).Info("local - skipping nginx reload")
300+
return nil
301+
}
302+
// write a new config version
303+
nginx.configVersion++
304+
nginx.UpdateConfigVersionFile()
305+
306+
glog.V(3).Infof("Reloading nginx. configVersion: %v", nginx.configVersion)
307+
if err := shellOut("nginx -s reload"); err != nil {
308+
return fmt.Errorf("nginx reload failed: %v", err)
309+
}
310+
err := nginx.verifyClient.WaitForCorrectVersion(nginx.configVersion)
311+
if err != nil {
312+
return fmt.Errorf("could not get newest config version: %v", err)
296313
}
314+
297315
return nil
298316
}
299317

@@ -399,3 +417,37 @@ func (nginx *Controller) UpdateIngressConfigFile(name string, cfg []byte) {
399417
}
400418
glog.V(3).Infof("The Ingress config file has been updated")
401419
}
420+
421+
// UpdateConfigVersionFile writes the config version file.
422+
func (nginx *Controller) UpdateConfigVersionFile() {
423+
cfg, err := nginx.verifyConfigGenerator.GenerateVersionConfig(nginx.configVersion)
424+
if err != nil {
425+
glog.Fatalf("Error generating config version content: %v", err)
426+
}
427+
428+
filename := "/etc/nginx/config-version.conf"
429+
tempname := "/etc/nginx/config-version.conf.tmp"
430+
glog.V(3).Infof("Writing config version to %v", filename)
431+
432+
if bool(glog.V(3)) || nginx.local {
433+
glog.Info(string(cfg))
434+
}
435+
436+
if !nginx.local {
437+
w, err := os.Create(tempname)
438+
if err != nil {
439+
glog.Fatalf("Failed to open %v: %v", filename, err)
440+
}
441+
_, err = w.Write(cfg)
442+
if err != nil {
443+
glog.Fatalf("Failed to write to %v: %v", filename, err)
444+
}
445+
w.Close()
446+
447+
err = os.Rename(tempname, filename)
448+
if err != nil {
449+
glog.Fatalf("failed to rename version config file: %v", err)
450+
}
451+
}
452+
glog.V(3).Infof("The config version file has been updated.")
453+
}

internal/nginx/plus/nginx_api.go

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package plus
22

33
import (
4+
"fmt"
45
"net/http"
56

67
"github.com/golang/glog"
@@ -29,13 +30,39 @@ func NewNginxAPIController(httpClient *http.Client, endpoint string, local bool)
2930
return nginx, nil
3031
}
3132

33+
// verifyConfigVersion is used to check if the worker process that the API client is connected
34+
// to is using the latest version of nginx config. This way we avoid making changes on
35+
// a worker processes that is being shut down.
36+
func verifyConfigVersion(httpClient *http.Client, configVersion int) error {
37+
req, err := http.NewRequest("GET", "http://nginx-plus-api/configVersionCheck", nil)
38+
if err != nil {
39+
return fmt.Errorf("error creating request: %v", err)
40+
}
41+
req.Header.Set("x-expected-config-version", fmt.Sprintf("%v", configVersion))
42+
resp, err := httpClient.Do(req)
43+
if err != nil {
44+
return fmt.Errorf("error doing request: %v", err)
45+
}
46+
defer resp.Body.Close()
47+
if resp.StatusCode != 200 {
48+
return fmt.Errorf("API returned non-success status: %v", resp.StatusCode)
49+
}
50+
return nil
51+
}
52+
3253
// UpdateServers updates upstream servers
33-
func (nginx *NginxAPIController) UpdateServers(upstream string, servers []string, config ServerConfig) error {
54+
func (nginx *NginxAPIController) UpdateServers(upstream string, servers []string, config ServerConfig, configVersion int) error {
3455
if nginx.local {
3556
glog.V(3).Infof("Updating endpoints of %v: %v\n", upstream, servers)
3657
return nil
3758
}
3859

60+
err := verifyConfigVersion(nginx.client.httpClient, configVersion)
61+
if err != nil {
62+
return fmt.Errorf("error verifying config version: %v", err)
63+
}
64+
glog.V(3).Infof("API has the correct config version: %v.", configVersion)
65+
3966
var upsServers []UpstreamServer
4067
for _, s := range servers {
4168
upsServers = append(upsServers, UpstreamServer{
@@ -49,7 +76,7 @@ func (nginx *NginxAPIController) UpdateServers(upstream string, servers []string
4976
added, removed, err := nginx.client.UpdateHTTPServers(upstream, upsServers)
5077
if err != nil {
5178
glog.V(3).Infof("Couldn't update servers of %v upstream: %v", upstream, err)
52-
return err
79+
return fmt.Errorf("error updating servers of %v upstream: %v", upstream, err)
5380
}
5481

5582
glog.V(3).Infof("Updated servers of %v; Added: %v, Removed: %v", upstream, added, removed)

internal/nginx/templates/nginx-plus.tmpl

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,11 +111,20 @@ http {
111111
listen unix:/var/run/nginx-plus-api.sock;
112112
access_log off;
113113

114+
# $config_version_mismatch is defined in /etc/nginx/config-version.conf
115+
location /configVersionCheck {
116+
if ($config_version_mismatch) {
117+
return 503;
118+
}
119+
return 200;
120+
}
121+
114122
location /api {
115123
api write=on;
116124
}
117125
}
118126

127+
include /etc/nginx/config-version.conf;
119128
include /etc/nginx/conf.d/*.conf;
120129
}
121130

internal/nginx/templates/nginx.tmpl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ http {
9696
}
9797
{{- end }}
9898

99-
99+
include /etc/nginx/config-version.conf;
100100
include /etc/nginx/conf.d/*.conf;
101101
}
102102

internal/nginx/verify/client.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package verify
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"io/ioutil"
7+
"net"
8+
"net/http"
9+
"strconv"
10+
"time"
11+
12+
"github.com/golang/glog"
13+
)
14+
15+
// Client is a client for verifying the config version.
16+
type Client struct {
17+
client *http.Client
18+
}
19+
20+
// NewClient returns a new client pointed at the config version socket.
21+
func NewClient() *Client {
22+
return &Client{
23+
client: &http.Client{
24+
Transport: &http.Transport{
25+
DialContext: func(_ context.Context, _, _ string) (net.Conn, error) {
26+
return net.Dial("unix", "/var/run/nginx-config-version.sock")
27+
},
28+
},
29+
},
30+
}
31+
}
32+
33+
// GetConfigVersion get version number that we put in the nginx config to verify that we're using
34+
// the correct config.
35+
func (c *Client) GetConfigVersion() (int, error) {
36+
resp, err := c.client.Get("http://config-version/configVersion")
37+
if err != nil {
38+
return 0, fmt.Errorf("error getting client: %v", err)
39+
}
40+
defer resp.Body.Close()
41+
42+
if resp.StatusCode != http.StatusOK {
43+
return 0, fmt.Errorf("non-200 response: %v", resp.StatusCode)
44+
}
45+
46+
body, err := ioutil.ReadAll(resp.Body)
47+
if err != nil {
48+
return 0, fmt.Errorf("failed to read the response body: %v", err)
49+
}
50+
v, err := strconv.Atoi(string(body))
51+
if err != nil {
52+
return 0, fmt.Errorf("error converting string to int: %v", err)
53+
}
54+
return v, nil
55+
}
56+
57+
// WaitForCorrectVersion calls the config version endpoint until it gets the expectedVersion,
58+
// which ensures that a new worker process has been started for that config version.
59+
func (c *Client) WaitForCorrectVersion(expectedVersion int) error {
60+
// This value needs tuning.
61+
maxRetries := 160
62+
sleep := 25 * time.Millisecond
63+
for i := 0; i < maxRetries; i++ {
64+
version, err := c.GetConfigVersion()
65+
if err != nil {
66+
return fmt.Errorf("unable to fetch version: %v", err)
67+
}
68+
if version == expectedVersion {
69+
glog.V(3).Infof("success, version %v ensured. iterations: %v. took: %v", expectedVersion, i, time.Duration(i)*sleep)
70+
return nil
71+
}
72+
time.Sleep(sleep)
73+
}
74+
return fmt.Errorf("could not get expected version: %v", expectedVersion)
75+
}

internal/nginx/verify/client_test.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package verify
2+
3+
import (
4+
"bytes"
5+
"io/ioutil"
6+
"net/http"
7+
"testing"
8+
)
9+
10+
type Transport struct {
11+
}
12+
13+
func (c Transport) RoundTrip(req *http.Request) (*http.Response, error) {
14+
return &http.Response{
15+
StatusCode: 200,
16+
Body: ioutil.NopCloser(bytes.NewBufferString("42")),
17+
Header: make(http.Header),
18+
}, nil
19+
}
20+
21+
func getTestHTTPClient() *http.Client {
22+
ts := Transport{}
23+
tClient := &http.Client{
24+
Transport: ts,
25+
}
26+
return tClient
27+
}
28+
29+
func TestVerifyClient(t *testing.T) {
30+
31+
c := Client{
32+
client: getTestHTTPClient(),
33+
}
34+
35+
configVersion, err := c.GetConfigVersion()
36+
if err != nil {
37+
t.Errorf("error getting config version: %v", err)
38+
}
39+
if configVersion != 42 {
40+
t.Errorf("got bad config version, expected 42 got %v", configVersion)
41+
}
42+
43+
err = c.WaitForCorrectVersion(43)
44+
if err == nil {
45+
t.Error("expected error from WaitForCorrectVersion ")
46+
}
47+
err = c.WaitForCorrectVersion(42)
48+
if err != nil {
49+
t.Errorf("error waiting for config version: %v", err)
50+
}
51+
}

0 commit comments

Comments
 (0)