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  }