nginx/nginx.go (194 lines of code) (raw):

// Copyright (c) 2016-2019 Uber Technologies, Inc. // // 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 nginx import ( "bytes" "errors" "fmt" "io/ioutil" "os" "os/exec" "path" "path/filepath" "text/template" "github.com/uber/kraken/nginx/config" "github.com/uber/kraken/utils/httputil" "github.com/uber/kraken/utils/log" ) const ( _genDir = "/tmp/nginx" ) var _clientCABundle = path.Join(_genDir, "ca.crt") // Config defines nginx configuration. type Config struct { Binary string `yaml:"binary"` Root bool `yaml:"root"` // Name defines the default nginx template for each component. Name string `yaml:"name"` // TemplatePath takes precedence over Name, overwrites default template. TemplatePath string `yaml:"template_path"` CacheDir string `yaml:"cache_dir"` LogDir string `yaml:"log_dir"` // Optional log path overrides. StdoutLogPath string `yaml:"stdout_log_path"` AccessLogPath string `yaml:"access_log_path"` ErrorLogPath string `yaml:"error_log_path"` tls httputil.TLSConfig } func (c *Config) applyDefaults() error { if c.Binary == "" { c.Binary = "/usr/sbin/nginx" } if c.StdoutLogPath == "" { if c.LogDir == "" { return errors.New("one of log_dir or stdout_log_path must be set") } c.StdoutLogPath = filepath.Join(c.LogDir, "nginx-stdout.log") } if c.AccessLogPath == "" { if c.LogDir == "" { return errors.New("one of log_dir or access_log_path must be set") } c.AccessLogPath = filepath.Join(c.LogDir, "nginx-access.log") } if c.ErrorLogPath == "" { if c.LogDir == "" { return errors.New("one of log_dir or error_log_path must be set") } c.ErrorLogPath = filepath.Join(c.LogDir, "nginx-error.log") } return nil } func (c *Config) inject(params map[string]interface{}) error { for _, s := range []string{"cache_dir", "access_log_path", "error_log_path"} { if _, ok := params[s]; ok { return fmt.Errorf("invalid params: %s is reserved", s) } } params["cache_dir"] = c.CacheDir params["access_log_path"] = c.AccessLogPath params["error_log_path"] = c.ErrorLogPath return nil } // GetTemplate returns the template content. func (c *Config) getTemplate() (string, error) { if c.TemplatePath != "" { b, err := ioutil.ReadFile(c.TemplatePath) if err != nil { return "", fmt.Errorf("read template: %s", err) } return string(b), nil } tmpl, err := config.GetDefaultTemplate(c.Name) if err != nil { return "", fmt.Errorf("get default template: %s", err) } return tmpl, nil } // Build builds nginx config. func (c *Config) Build(params map[string]interface{}) ([]byte, error) { tmpl, err := c.getTemplate() if err != nil { return nil, fmt.Errorf("get template: %s", err) } if _, ok := params["client_verification"]; !ok { params["client_verification"] = config.DefaultClientVerification } site, err := populateTemplate(tmpl, params) if err != nil { return nil, fmt.Errorf("populate template: %s", err) } // Build nginx config with base template and component specific template. tmpl, err = config.GetDefaultTemplate("base") if err != nil { return nil, fmt.Errorf("get default base template: %s", err) } src, err := populateTemplate(tmpl, map[string]interface{}{ "site": string(site), "ssl_enabled": !c.tls.Server.Disabled, "ssl_certificate": c.tls.Server.Cert.Path, "ssl_certificate_key": c.tls.Server.Key.Path, "ssl_password_file": c.tls.Server.Passphrase.Path, "ssl_client_certificate": _clientCABundle, }) if err != nil { return nil, fmt.Errorf("populate base: %s", err) } return src, nil } // Option allows setting optional nginx configuration. type Option func(*Config) // WithTLS configures nginx configuration with tls. func WithTLS(tls httputil.TLSConfig) Option { return func(c *Config) { c.tls = tls } } // Run injects params into an nginx configuration template and runs it. func Run(config Config, params map[string]interface{}, opts ...Option) error { if err := config.applyDefaults(); err != nil { return fmt.Errorf("invalid config: %s", err) } if config.Name == "" && config.TemplatePath == "" { return errors.New("invalid config: name or template_path required") } if config.CacheDir == "" { return errors.New("invalid config: cache_dir required") } for _, opt := range opts { opt(&config) } // Create root directory for generated files for nginx. if err := os.MkdirAll(_genDir, 0775); err != nil { return err } if config.tls.Server.Disabled { log.Warn("Server TLS is disabled") } else { for _, s := range append( config.tls.CAs, config.tls.Server.Cert, config.tls.Server.Key, config.tls.Server.Passphrase) { if _, err := os.Stat(s.Path); err != nil { return fmt.Errorf("invalid TLS config: %s", err) } } // Concat all ca files into bundle. cabundle, err := os.Create(_clientCABundle) if err != nil { return fmt.Errorf("create cabundle: %s", err) } if err := config.tls.WriteCABundle(cabundle); err != nil { return fmt.Errorf("write cabundle: %s", err) } cabundle.Close() } if err := os.MkdirAll(config.CacheDir, 0775); err != nil { return err } if err := config.inject(params); err != nil { return err } src, err := config.Build(params) if err != nil { return fmt.Errorf("build nginx config: %s", err) } conf := filepath.Join(_genDir, config.Name) if err := ioutil.WriteFile(conf, src, 0755); err != nil { return fmt.Errorf("write src: %s", err) } stdout, err := os.OpenFile(config.StdoutLogPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { return fmt.Errorf("open stdout log: %s", err) } args := []string{config.Binary, "-g", "daemon off;", "-c", conf} if config.Root { args = append([]string{"sudo"}, args...) } cmd := exec.Command(args[0], args[1:]...) cmd.Stdout = stdout cmd.Stderr = stdout return cmd.Run() } func populateTemplate(tmpl string, args map[string]interface{}) ([]byte, error) { t, err := template.New("nginx").Parse(tmpl) if err != nil { return nil, fmt.Errorf("parse: %s", err) } out := &bytes.Buffer{} if err := t.Execute(out, args); err != nil { return nil, fmt.Errorf("exec: %s", err) } return out.Bytes(), nil } // GetServer returns a string for an nginx server directive value. func GetServer(net, addr string) string { if net == "unix" { return "unix:" + addr } return addr }