github.com/nginxinc/kubernetes-ingress@v1.12.5/internal/nginx/manager.go (about) 1 package nginx 2 3 import ( 4 "fmt" 5 "io/ioutil" 6 "net/http" 7 "os" 8 "os/exec" 9 "path" 10 "strings" 11 "time" 12 13 "github.com/nginxinc/kubernetes-ingress/internal/metrics/collectors" 14 15 "github.com/golang/glog" 16 "github.com/nginxinc/nginx-plus-go-client/client" 17 ) 18 19 const ( 20 ReloadForEndpointsUpdate = true // ReloadForEndpointsUpdate means that is caused by an endpoints update. 21 ReloadForOtherUpdate = false // ReloadForOtherUpdate means that a reload is caused by an update for a resource(s) other than endpoints. 22 TLSSecretFileMode = 0o600 // TLSSecretFileMode defines the default filemode for files with TLS Secrets. 23 JWKSecretFileMode = 0o644 // JWKSecretFileMode defines the default filemode for files with JWK Secrets. 24 configFileMode = 0o644 25 jsonFileForOpenTracingTracer = "/var/lib/nginx/tracer-config.json" 26 nginxBinaryPath = "/usr/sbin/nginx" 27 nginxBinaryPathDebug = "/usr/sbin/nginx-debug" 28 29 appProtectPluginStartCmd = "/usr/share/ts/bin/bd-socket-plugin" 30 appProtectAgentStartCmd = "/opt/app_protect/bin/bd_agent" 31 32 // appPluginParams is the configuration of App-Protect plugin 33 appPluginParams = "tmm_count 4 proc_cpuinfo_cpu_mhz 2000000 total_xml_memory 307200000 total_umu_max_size 3129344 sys_max_account_id 1024 no_static_config" 34 35 // appProtectDebugLogConfigFileContent holds the content of the file to be written when nginx debug is enabled. It will enable NGINX App Protect debug logs 36 appProtectDebugLogConfigFileContent = "MODULE = IO_PLUGIN;\nLOG_LEVEL = TS_INFO | TS_DEBUG;\nFILE = 2;\nMODULE = ECARD_POLICY;\nLOG_LEVEL = TS_INFO | TS_DEBUG;\nFILE = 2;\n" 37 38 // appProtectLogConfigFileName is the location of the NGINX App Protect logging configuration file 39 appProtectLogConfigFileName = "/etc/app_protect/bd/logger.cfg" 40 ) 41 42 // ServerConfig holds the config data for an upstream server in NGINX Plus. 43 type ServerConfig struct { 44 MaxFails int 45 MaxConns int 46 FailTimeout string 47 SlowStart string 48 } 49 50 // The Manager interface updates NGINX configuration, starts, reloads and quits NGINX, 51 // updates NGINX Plus upstream servers. 52 type Manager interface { 53 CreateMainConfig(content []byte) 54 CreateConfig(name string, content []byte) 55 DeleteConfig(name string) 56 CreateStreamConfig(name string, content []byte) 57 DeleteStreamConfig(name string) 58 CreateTLSPassthroughHostsConfig(content []byte) 59 CreateSecret(name string, content []byte, mode os.FileMode) string 60 DeleteSecret(name string) 61 CreateAppProtectResourceFile(name string, content []byte) 62 DeleteAppProtectResourceFile(name string) 63 ClearAppProtectFolder(name string) 64 GetFilenameForSecret(name string) string 65 CreateDHParam(content string) (string, error) 66 CreateOpenTracingTracerConfig(content string) error 67 Start(done chan error) 68 Version() string 69 Reload(isEndpointsUpdate bool) error 70 Quit() 71 UpdateConfigVersionFile(openTracing bool) 72 SetPlusClients(plusClient *client.NginxClient, plusConfigVersionCheckClient *http.Client) 73 UpdateServersInPlus(upstream string, servers []string, config ServerConfig) error 74 UpdateStreamServersInPlus(upstream string, servers []string) error 75 SetOpenTracing(openTracing bool) 76 AppProtectAgentStart(apaDone chan error, debug bool) 77 AppProtectAgentQuit() 78 AppProtectPluginStart(appDone chan error) 79 AppProtectPluginQuit() 80 } 81 82 // LocalManager updates NGINX configuration, starts, reloads and quits NGINX, 83 // updates NGINX Plus upstream servers. It assumes that NGINX is running in the same container. 84 type LocalManager struct { 85 confdPath string 86 streamConfdPath string 87 secretsPath string 88 mainConfFilename string 89 configVersionFilename string 90 debug bool 91 dhparamFilename string 92 tlsPassthroughHostsFilename string 93 verifyConfigGenerator *verifyConfigGenerator 94 verifyClient *verifyClient 95 configVersion int 96 plusClient *client.NginxClient 97 plusConfigVersionCheckClient *http.Client 98 metricsCollector collectors.ManagerCollector 99 OpenTracing bool 100 appProtectPluginPid int 101 appProtectAgentPid int 102 } 103 104 // NewLocalManager creates a LocalManager. 105 func NewLocalManager(confPath string, debug bool, mc collectors.ManagerCollector, timeout time.Duration) *LocalManager { 106 verifyConfigGenerator, err := newVerifyConfigGenerator() 107 if err != nil { 108 glog.Fatalf("error instantiating a verifyConfigGenerator: %v", err) 109 } 110 111 manager := LocalManager{ 112 confdPath: path.Join(confPath, "conf.d"), 113 streamConfdPath: path.Join(confPath, "stream-conf.d"), 114 secretsPath: path.Join(confPath, "secrets"), 115 dhparamFilename: path.Join(confPath, "secrets", "dhparam.pem"), 116 mainConfFilename: path.Join(confPath, "nginx.conf"), 117 configVersionFilename: path.Join(confPath, "config-version.conf"), 118 tlsPassthroughHostsFilename: path.Join(confPath, "tls-passthrough-hosts.conf"), 119 debug: debug, 120 verifyConfigGenerator: verifyConfigGenerator, 121 configVersion: 0, 122 verifyClient: newVerifyClient(timeout), 123 metricsCollector: mc, 124 } 125 126 return &manager 127 } 128 129 // CreateMainConfig creates the main NGINX configuration file. If the file already exists, it will be overridden. 130 func (lm *LocalManager) CreateMainConfig(content []byte) { 131 glog.V(3).Infof("Writing main config to %v", lm.mainConfFilename) 132 glog.V(3).Infof(string(content)) 133 134 err := createFileAndWrite(lm.mainConfFilename, content) 135 if err != nil { 136 glog.Fatalf("Failed to write main config: %v", err) 137 } 138 } 139 140 // CreateConfig creates a configuration file. If the file already exists, it will be overridden. 141 func (lm *LocalManager) CreateConfig(name string, content []byte) { 142 createConfig(lm.getFilenameForConfig(name), content) 143 } 144 145 func createConfig(filename string, content []byte) { 146 glog.V(3).Infof("Writing config to %v", filename) 147 glog.V(3).Info(string(content)) 148 149 err := createFileAndWrite(filename, content) 150 if err != nil { 151 glog.Fatalf("Failed to write config to %v: %v", filename, err) 152 } 153 } 154 155 // DeleteConfig deletes the configuration file from the conf.d folder. 156 func (lm *LocalManager) DeleteConfig(name string) { 157 deleteConfig(lm.getFilenameForConfig(name)) 158 } 159 160 func deleteConfig(filename string) { 161 glog.V(3).Infof("Deleting config from %v", filename) 162 163 if err := os.Remove(filename); err != nil { 164 glog.Warningf("Failed to delete config from %v: %v", filename, err) 165 } 166 } 167 168 func (lm *LocalManager) getFilenameForConfig(name string) string { 169 return path.Join(lm.confdPath, name+".conf") 170 } 171 172 // CreateStreamConfig creates a configuration file for stream module. 173 // If the file already exists, it will be overridden. 174 func (lm *LocalManager) CreateStreamConfig(name string, content []byte) { 175 createConfig(lm.getFilenameForStreamConfig(name), content) 176 } 177 178 // DeleteStreamConfig deletes the configuration file from the stream-conf.d folder. 179 func (lm *LocalManager) DeleteStreamConfig(name string) { 180 deleteConfig(lm.getFilenameForStreamConfig(name)) 181 } 182 183 func (lm *LocalManager) getFilenameForStreamConfig(name string) string { 184 return path.Join(lm.streamConfdPath, name+".conf") 185 } 186 187 // CreateTLSPassthroughHostsConfig creates a configuration file with mapping between TLS Passthrough hosts and 188 // the corresponding unix sockets. 189 // If the file already exists, it will be overridden. 190 func (lm *LocalManager) CreateTLSPassthroughHostsConfig(content []byte) { 191 glog.V(3).Infof("Writing TLS Passthrough Hosts config file to %v", lm.tlsPassthroughHostsFilename) 192 createConfig(lm.tlsPassthroughHostsFilename, content) 193 } 194 195 // CreateSecret creates a secret file with the specified name, content and mode. If the file already exists, 196 // it will be overridden. 197 func (lm *LocalManager) CreateSecret(name string, content []byte, mode os.FileMode) string { 198 filename := lm.GetFilenameForSecret(name) 199 200 glog.V(3).Infof("Writing secret to %v", filename) 201 202 createFileAndWriteAtomically(filename, lm.secretsPath, mode, content) 203 204 return filename 205 } 206 207 // DeleteSecret the file with the secret. 208 func (lm *LocalManager) DeleteSecret(name string) { 209 filename := lm.GetFilenameForSecret(name) 210 211 glog.V(3).Infof("Deleting secret from %v", filename) 212 213 if err := os.Remove(filename); err != nil { 214 glog.Warningf("Failed to delete secret from %v: %v", filename, err) 215 } 216 } 217 218 // GetFilenameForSecret constructs the filename for the secret. 219 func (lm *LocalManager) GetFilenameForSecret(name string) string { 220 return path.Join(lm.secretsPath, name) 221 } 222 223 // CreateDHParam creates the servers dhparam.pem file. If the file already exists, it will be overridden. 224 func (lm *LocalManager) CreateDHParam(content string) (string, error) { 225 glog.V(3).Infof("Writing dhparam file to %v", lm.dhparamFilename) 226 227 err := createFileAndWrite(lm.dhparamFilename, []byte(content)) 228 if err != nil { 229 return lm.dhparamFilename, fmt.Errorf("Failed to write dhparam file from %v: %w", lm.dhparamFilename, err) 230 } 231 232 return lm.dhparamFilename, nil 233 } 234 235 // CreateAppProtectResourceFile writes contents of An App Protect resource to a file 236 func (lm *LocalManager) CreateAppProtectResourceFile(name string, content []byte) { 237 glog.V(3).Infof("Writing App Protect Resource to %v", name) 238 err := createFileAndWrite(name, content) 239 if err != nil { 240 glog.Fatalf("Failed to write App Protect Resource to %v: %v", name, err) 241 } 242 } 243 244 // DeleteAppProtectResourceFile removes an App Protect resource file from storage 245 func (lm *LocalManager) DeleteAppProtectResourceFile(name string) { 246 // This check is done to avoid errors in case eg. a policy is referenced, but it never became valid. 247 if _, err := os.Stat(name); !os.IsNotExist(err) { 248 if err := os.Remove(name); err != nil { 249 glog.Fatalf("Failed to delete App Protect Resource from %v: %v", name, err) 250 } 251 } 252 } 253 254 // ClearAppProtectFolder clears contents of a config folder 255 func (lm *LocalManager) ClearAppProtectFolder(name string) { 256 files, err := ioutil.ReadDir(name) 257 if err != nil { 258 glog.Fatalf("Failed to read the App Protect folder %s: %v", name, err) 259 } 260 for _, file := range files { 261 lm.DeleteAppProtectResourceFile(fmt.Sprintf("%s/%s", name, file.Name())) 262 } 263 } 264 265 // Start starts NGINX. 266 func (lm *LocalManager) Start(done chan error) { 267 glog.V(3).Info("Starting nginx") 268 269 binaryFilename := getBinaryFileName(lm.debug) 270 cmd := exec.Command(binaryFilename) 271 cmd.Stdout = os.Stdout 272 cmd.Stderr = os.Stderr 273 if err := cmd.Start(); err != nil { 274 glog.Fatalf("Failed to start nginx: %v", err) 275 } 276 277 go func() { 278 done <- cmd.Wait() 279 }() 280 err := lm.verifyClient.WaitForCorrectVersion(lm.configVersion) 281 if err != nil { 282 glog.Fatalf("Could not get newest config version: %v", err) 283 } 284 } 285 286 // Reload reloads NGINX. 287 func (lm *LocalManager) Reload(isEndpointsUpdate bool) error { 288 // write a new config version 289 lm.configVersion++ 290 lm.UpdateConfigVersionFile(lm.OpenTracing) 291 292 glog.V(3).Infof("Reloading nginx with configVersion: %v", lm.configVersion) 293 294 t1 := time.Now() 295 296 binaryFilename := getBinaryFileName(lm.debug) 297 if err := shellOut(fmt.Sprintf("%v -s %v", binaryFilename, "reload")); err != nil { 298 lm.metricsCollector.IncNginxReloadErrors() 299 return fmt.Errorf("nginx reload failed: %w", err) 300 } 301 err := lm.verifyClient.WaitForCorrectVersion(lm.configVersion) 302 if err != nil { 303 lm.metricsCollector.IncNginxReloadErrors() 304 return fmt.Errorf("could not get newest config version: %w", err) 305 } 306 307 lm.metricsCollector.IncNginxReloadCount(isEndpointsUpdate) 308 309 t2 := time.Now() 310 lm.metricsCollector.UpdateLastReloadTime(t2.Sub(t1)) 311 return nil 312 } 313 314 // Quit shutdowns NGINX gracefully. 315 func (lm *LocalManager) Quit() { 316 glog.V(3).Info("Quitting nginx") 317 318 binaryFilename := getBinaryFileName(lm.debug) 319 if err := shellOut(fmt.Sprintf("%v -s %v", binaryFilename, "quit")); err != nil { 320 glog.Fatalf("Failed to quit nginx: %v", err) 321 } 322 } 323 324 // Version returns NGINX version 325 func (lm *LocalManager) Version() string { 326 binaryFilename := getBinaryFileName(lm.debug) 327 out, err := exec.Command(binaryFilename, "-v").CombinedOutput() 328 if err != nil { 329 glog.Fatalf("Failed to get nginx version: %v", err) 330 } 331 return string(out) 332 } 333 334 // UpdateConfigVersionFile writes the config version file. 335 func (lm *LocalManager) UpdateConfigVersionFile(openTracing bool) { 336 cfg, err := lm.verifyConfigGenerator.GenerateVersionConfig(lm.configVersion, openTracing) 337 if err != nil { 338 glog.Fatalf("Error generating config version content: %v", err) 339 } 340 341 glog.V(3).Infof("Writing config version to %v", lm.configVersionFilename) 342 glog.V(3).Info(string(cfg)) 343 344 createFileAndWriteAtomically(lm.configVersionFilename, path.Dir(lm.configVersionFilename), configFileMode, cfg) 345 } 346 347 // SetPlusClients sets the necessary clients to work with NGINX Plus API. If not set, invoking the UpdateServersInPlus 348 // will fail. 349 func (lm *LocalManager) SetPlusClients(plusClient *client.NginxClient, plusConfigVersionCheckClient *http.Client) { 350 lm.plusClient = plusClient 351 lm.plusConfigVersionCheckClient = plusConfigVersionCheckClient 352 } 353 354 // UpdateServersInPlus updates NGINX Plus servers of the given upstream. 355 func (lm *LocalManager) UpdateServersInPlus(upstream string, servers []string, config ServerConfig) error { 356 err := verifyConfigVersion(lm.plusConfigVersionCheckClient, lm.configVersion) 357 if err != nil { 358 return fmt.Errorf("error verifying config version: %w", err) 359 } 360 361 glog.V(3).Infof("API has the correct config version: %v.", lm.configVersion) 362 363 var upsServers []client.UpstreamServer 364 for _, s := range servers { 365 upsServers = append(upsServers, client.UpstreamServer{ 366 Server: s, 367 MaxFails: &config.MaxFails, 368 MaxConns: &config.MaxConns, 369 FailTimeout: config.FailTimeout, 370 SlowStart: config.SlowStart, 371 }) 372 } 373 374 added, removed, updated, err := lm.plusClient.UpdateHTTPServers(upstream, upsServers) 375 if err != nil { 376 glog.V(3).Infof("Couldn't update servers of %v upstream: %v", upstream, err) 377 return fmt.Errorf("error updating servers of %v upstream: %w", upstream, err) 378 } 379 380 glog.V(3).Infof("Updated servers of %v; Added: %v, Removed: %v, Updated: %v", upstream, added, removed, updated) 381 382 return nil 383 } 384 385 // UpdateStreamServersInPlus updates NGINX Plus stream servers of the given upstream. 386 func (lm *LocalManager) UpdateStreamServersInPlus(upstream string, servers []string) error { 387 err := verifyConfigVersion(lm.plusConfigVersionCheckClient, lm.configVersion) 388 if err != nil { 389 return fmt.Errorf("error verifying config version: %w", err) 390 } 391 392 glog.V(3).Infof("API has the correct config version: %v.", lm.configVersion) 393 394 var upsServers []client.StreamUpstreamServer 395 for _, s := range servers { 396 upsServers = append(upsServers, client.StreamUpstreamServer{ 397 Server: s, 398 }) 399 } 400 401 added, removed, updated, err := lm.plusClient.UpdateStreamServers(upstream, upsServers) 402 if err != nil { 403 glog.V(3).Infof("Couldn't update stream servers of %v upstream: %v", upstream, err) 404 return fmt.Errorf("error updating stream servers of %v upstream: %w", upstream, err) 405 } 406 407 glog.V(3).Infof("Updated stream servers of %v; Added: %v, Removed: %v, Updated: %v", upstream, added, removed, updated) 408 409 return nil 410 } 411 412 // CreateOpenTracingTracerConfig creates a json configuration file for the OpenTracing tracer with the content of the string. 413 func (lm *LocalManager) CreateOpenTracingTracerConfig(content string) error { 414 glog.V(3).Infof("Writing OpenTracing tracer config file to %v", jsonFileForOpenTracingTracer) 415 err := createFileAndWrite(jsonFileForOpenTracingTracer, []byte(content)) 416 if err != nil { 417 return fmt.Errorf("Failed to write config file: %w", err) 418 } 419 420 return nil 421 } 422 423 // verifyConfigVersion is used to check if the worker process that the API client is connected 424 // to is using the latest version of nginx config. This way we avoid making changes on 425 // a worker processes that is being shut down. 426 func verifyConfigVersion(httpClient *http.Client, configVersion int) error { 427 req, err := http.NewRequest("GET", "http://nginx-plus-api/configVersionCheck", nil) 428 if err != nil { 429 return fmt.Errorf("error creating request: %w", err) 430 } 431 432 req.Header.Set("x-expected-config-version", fmt.Sprintf("%v", configVersion)) 433 434 resp, err := httpClient.Do(req) 435 if err != nil { 436 return fmt.Errorf("error doing request: %w", err) 437 } 438 defer resp.Body.Close() 439 440 if resp.StatusCode != http.StatusOK { 441 return fmt.Errorf("API returned non-success status: %v", resp.StatusCode) 442 } 443 444 return nil 445 } 446 447 // SetOpenTracing sets the value of OpenTracing for the Manager 448 func (lm *LocalManager) SetOpenTracing(openTracing bool) { 449 lm.OpenTracing = openTracing 450 } 451 452 // AppProtectAgentStart starts the AppProtect agent 453 func (lm *LocalManager) AppProtectAgentStart(apaDone chan error, debug bool) { 454 if debug { 455 glog.V(3).Info("Starting AppProtect Agent in debug mode") 456 err := os.Remove(appProtectLogConfigFileName) 457 if err != nil { 458 glog.Fatalf("Failed removing App Protect Log configuration file") 459 } 460 err = createFileAndWrite(appProtectLogConfigFileName, []byte(appProtectDebugLogConfigFileContent)) 461 if err != nil { 462 glog.Fatalf("Failed Writing App Protect Log configuration file") 463 } 464 } 465 glog.V(3).Info("Starting AppProtect Agent") 466 467 cmd := exec.Command(appProtectAgentStartCmd) 468 if err := cmd.Start(); err != nil { 469 glog.Fatalf("Failed to start AppProtect Agent: %v", err) 470 } 471 lm.appProtectAgentPid = cmd.Process.Pid 472 go func() { 473 apaDone <- cmd.Wait() 474 }() 475 } 476 477 // AppProtectAgentQuit gracefully ends AppProtect Agent. 478 func (lm *LocalManager) AppProtectAgentQuit() { 479 glog.V(3).Info("Quitting AppProtect Agent") 480 killcmd := fmt.Sprintf("kill %d", lm.appProtectAgentPid) 481 if err := shellOut(killcmd); err != nil { 482 glog.Fatalf("Failed to quit AppProtect Agent: %v", err) 483 } 484 } 485 486 // AppProtectPluginStart starts the AppProtect plugin. 487 func (lm *LocalManager) AppProtectPluginStart(appDone chan error) { 488 glog.V(3).Info("Starting AppProtect Plugin") 489 startupParams := strings.Fields(appPluginParams) 490 cmd := exec.Command(appProtectPluginStartCmd, startupParams...) 491 492 cmd.Stdout = os.Stdout 493 cmd.Stderr = os.Stdout 494 cmd.Env = os.Environ() 495 cmd.Env = append(cmd.Env, "LD_LIBRARY_PATH=/usr/lib64/bd") 496 497 if err := cmd.Start(); err != nil { 498 glog.Fatalf("Failed to start AppProtect Plugin: %v", err) 499 } 500 lm.appProtectPluginPid = cmd.Process.Pid 501 go func() { 502 appDone <- cmd.Wait() 503 }() 504 } 505 506 // AppProtectPluginQuit gracefully ends AppProtect Agent. 507 func (lm *LocalManager) AppProtectPluginQuit() { 508 glog.V(3).Info("Quitting AppProtect Plugin") 509 killcmd := fmt.Sprintf("kill %d", lm.appProtectPluginPid) 510 if err := shellOut(killcmd); err != nil { 511 glog.Fatalf("Failed to quit AppProtect Plugin: %v", err) 512 } 513 } 514 515 func getBinaryFileName(debug bool) string { 516 if debug { 517 return nginxBinaryPathDebug 518 } 519 return nginxBinaryPath 520 }