Remove custom ssl code and add TLS support in Ingress rules

This commit is contained in:
Manuel de Brito Fontes 2016-03-16 11:12:45 -03:00
parent 5feb452ce4
commit 6cb0e41737
11 changed files with 190 additions and 226 deletions

View file

@ -243,8 +243,6 @@ func (lbc *loadBalancerController) sync() {
}
func (lbc *loadBalancerController) getUpstreamServers(data []interface{}) ([]nginx.Upstream, []nginx.Server) {
pems := make(map[string]string)
upstreams := make(map[string]nginx.Upstream)
servers := make(map[string]nginx.Server)
@ -297,6 +295,8 @@ func (lbc *loadBalancerController) getUpstreamServers(data []interface{}) ([]ngi
}
}
pems := lbc.getPemsFromIngress(data)
for _, rule := range ing.Spec.Rules {
var server nginx.Server
if existent, ok := servers[rule.Host]; ok {
@ -317,6 +317,18 @@ func (lbc *loadBalancerController) getUpstreamServers(data []interface{}) ([]ngi
loc := nginx.Location{Path: path.Path}
upsName := ing.GetNamespace() + "-" + path.Backend.ServiceName
svcKey := ing.GetNamespace() + "/" + path.Backend.ServiceName
_, svcExists, err := lbc.svcLister.Store.GetByKey(svcKey)
if err != nil {
glog.Infof("error getting service %v from the cache: %v", svcKey, err)
continue
}
if !svcExists {
glog.Warningf("service %v does no exists. skipping Ingress rule", svcKey)
continue
}
for _, ups := range upstreams {
if upsName == ups.Name {
loc.Upstream = ups
@ -350,6 +362,41 @@ func (lbc *loadBalancerController) getUpstreamServers(data []interface{}) ([]ngi
return aUpstreams, aServers
}
func (lbc *loadBalancerController) getPemsFromIngress(data []interface{}) map[string]string {
pems := make(map[string]string)
for _, ingIf := range data {
ing := ingIf.(*extensions.Ingress)
for _, tls := range ing.Spec.TLS {
secretName := tls.SecretName
secret, err := lbc.client.Secrets(ing.Namespace).Get(secretName)
if err != nil {
glog.Warningf("Error retriveing secret %v for ing %v: %v", secretName, ing.Name, err)
continue
}
cert, ok := secret.Data[api.TLSCertKey]
if !ok {
glog.Warningf("Secret %v has no private key", secretName)
continue
}
key, ok := secret.Data[api.TLSPrivateKeyKey]
if !ok {
glog.Warningf("Secret %v has no cert", secretName)
continue
}
pemFileName := lbc.nginx.AddOrUpdateCertAndKey(secretName, string(cert), string(key))
for _, host := range tls.Hosts {
pems[host] = pemFileName
}
}
}
return pems
}
// getEndpoints returns a list of <endpoint ip>:<port> for a given service/target port combination.
func (lbc *loadBalancerController) getEndpoints(s *api.Service, servicePort intstr.IntOrString) []nginx.UpstreamServer {
ep, err := lbc.endpLister.GetServiceEndpoints(s)
@ -415,9 +462,7 @@ func (lbc *loadBalancerController) Run() {
go lbc.svcController.Run(lbc.stopCh)
// periodic check for changes in configuration
go wait.Until(lbc.sync, 5*time.Second, wait.NeverStop)
time.Sleep(5 * time.Second)
go wait.Until(lbc.sync, 10*time.Second, wait.NeverStop)
<-lbc.stopCh
glog.Infof("shutting down NGINX loadbalancer controller")

View file

@ -5,7 +5,10 @@ function openURL(status, page)
local res, err = httpc:request_uri(page, {
path = "/",
method = "GET"
method = "GET",
headers = {
["Content-Type"] = ngx.var.httpReturnType or "text/html",
}
})
if not res then

View file

@ -56,6 +56,7 @@ server {
content_by_lua '
-- For simple singleshot requests, use the URI interface.
local http = require "resty.http"
local httpc = http.new()
local res, err = httpc:request_uri("http://example.com/helloworld", {
method = "POST",

View file

@ -67,7 +67,7 @@ end
local _M = {
_VERSION = '0.06',
_VERSION = '0.07',
}
_M._USER_AGENT = "lua-resty-http/" .. _M._VERSION .. " (Lua) ngx_lua/" .. ngx.config.ngx_lua_version
@ -196,7 +196,7 @@ function _M.parse_uri(self, uri)
m[3] = 80
end
end
if not m[4] then m[4] = "/" end
if not m[4] or "" == m[4] then m[4] = "/" end
return m, nil
end
end
@ -805,7 +805,11 @@ function _M.proxy_response(self, response, chunksize)
end
if chunk then
ngx.print(chunk)
local res, err = ngx.print(chunk)
if not res then
ngx_log(ngx_ERR, err)
break
end
end
until not chunk
end

View file

@ -1,12 +1,12 @@
package = "lua-resty-http"
version = "0.06-0"
version = "0.07-0"
source = {
url = "git://github.com/pintsized/lua-resty-http",
tag = "v0.06"
url = "git://github.com/pintsized/lua-resty-http",
tag = "v0.07"
}
description = {
summary = "Lua HTTP client cosocket driver for OpenResty / ngx_lua.",
detailed = [[
summary = "Lua HTTP client cosocket driver for OpenResty / ngx_lua.",
detailed = [[
Features an HTTP 1.0 and 1.1 streaming interface to reading
bodies using coroutines, for predictable memory usage in Lua
land. Alternative simple interface for singleshot requests
@ -17,17 +17,17 @@ description = {
Recommended by the OpenResty maintainer as a long-term
replacement for internal requests through ngx.location.capture.
]],
homepage = "https://github.com/pintsized/lua-resty-http",
license = "2-clause BSD",
maintainer = "James Hurst <james@pintsized.co.uk>"
homepage = "https://github.com/pintsized/lua-resty-http",
license = "2-clause BSD",
maintainer = "James Hurst <james@pintsized.co.uk>"
}
dependencies = {
"lua >= 5.1",
"lua >= 5.1"
}
build = {
type = "builtin",
modules = {
["resty.http"] = "lib/resty/http.lua",
["resty.http_headers"] = "lib/resty/http_headers.lua"
}
type = "builtin",
modules = {
["resty.http"] = "lib/resty/http.lua",
["resty.http_headers"] = "lib/resty/http_headers.lua"
}
}

View file

@ -0,0 +1,52 @@
# vim:set ft= ts=4 sw=4 et:
use Test::Nginx::Socket;
use Cwd qw(cwd);
plan tests => repeat_each() * (blocks() * 3);
my $pwd = cwd();
our $HttpConfig = qq{
lua_package_path "$pwd/lib/?.lua;;";
error_log logs/error.log debug;
};
$ENV{TEST_NGINX_RESOLVER} = '8.8.8.8';
no_long_string();
#no_diff();
run_tests();
__DATA__
=== TEST 1: request_uri (check the default path)
--- http_config eval: $::HttpConfig
--- config
location /lua {
content_by_lua '
local http = require "resty.http"
local httpc = http.new()
local res, err = httpc:request_uri("http://127.0.0.1:"..ngx.var.server_port)
if res and 200 == res.status then
ngx.say("OK")
else
ngx.say("FAIL")
end
';
}
location =/ {
content_by_lua '
ngx.print("OK")
';
}
--- request
GET /lua
--- response_body
OK
--- no_error_log
[error]

View file

@ -1,4 +1,4 @@
{{ $cfg := .cfg }}{{ $sslCertificates := .sslCertificates }}{{ $defErrorSvc := .defErrorSvc }}{{ $defBackend := .defBackend }}
{{ $cfg := .cfg }}{{ $defErrorSvc := .defErrorSvc }}{{ $defBackend := .defBackend }}
daemon off;
worker_processes {{ $cfg.WorkerProcesses }};
@ -17,13 +17,13 @@ http {
lua_package_path '.?.lua;./etc/nginx/lua/?.lua;/etc/nginx/lua/vendor/lua-resty-http/lib/?.lua;';
init_by_lua_block {
def_backend = "http://{{ $defBackend.ServiceName }}.{{ $defBackend.Namespace }}.svc.cluster.local:{{ $defBackend.ServicePort }}"
{{ if $defErrorSvc }}{{/* only if exists a custom error service */}}
dev_error_url = "http://{{ $defErrorSvc.ServiceName }}.{{ $defErrorSvc.Namespace }}.svc.cluster.local:{{ $defErrorSvc.ServicePort }}"
{{ else }}
dev_error_url = nil
dev_error_url = def_backend
{{ end }}
local options = {}
def_backend = "http://{{ $defBackend.ServiceName }}.{{ $defBackend.Namespace }}.svc.cluster.local:{{ $defBackend.ServicePort }}"
require("error_page")
}
@ -178,25 +178,6 @@ http {
{{ if $defErrorSvc }}{{ template "CUSTOM_ERRORS" (dict "cfg" $cfg "defErrorSvc" $defErrorSvc) }}{{ end }}
}
{{ if ge (len .sslCertificates) 1 }}
# SSL
# TODO: support more than one certificate
server {
listen 443 ssl http2 default_server;
{{ range $sslCert := .sslCertificates }}{{ if $sslCert.Default }}
# default certificate in case no match
ssl_certificate "{{ $sslCert.Cert }}";
ssl_certificate_key "{{ $sslCert.Key }}";
{{ end }}{{ end }}
location / {
proxy_pass http://{{ $defBackend.ServiceName }}.{{ $defBackend.Namespace }}.svc.cluster.local:{{ $defBackend.ServicePort }};
}
{{ if $defErrorSvc }}{{ template "CUSTOM_ERRORS" (dict "cfg" $cfg "defErrorSvc" $defErrorSvc) }}{{ end }}
}
{{ end }}
{{range $name, $upstream := .upstreams}}
upstream {{$upstream.Name}} {
least_conn;
@ -256,6 +237,17 @@ http {
}
{{ if $defErrorSvc }}{{ template "CUSTOM_ERRORS" (dict "cfg" $cfg "defErrorSvc" $defErrorSvc) }}{{ end }}
}
# default server for services without endpoints
server {
listen 8081;
location / {
content_by_lua_block {
openURL(503, dev_error_url)
}
}
}
}
# TCP services

View file

@ -17,6 +17,7 @@ limitations under the License.
package nginx
import (
"os"
"runtime"
"strconv"
"strings"
@ -27,7 +28,6 @@ import (
"k8s.io/contrib/ingress/controllers/nginx-third-party/ssl"
"k8s.io/kubernetes/pkg/client/record"
client "k8s.io/kubernetes/pkg/client/unversioned"
k8sruntime "k8s.io/kubernetes/pkg/runtime"
)
@ -220,9 +220,9 @@ type NginxManager struct {
// path to the configuration file to be used by nginx
ConfigFile string
sslCertificates []ssl.Certificate
sslDHParam string
servicesL4 []Service
sslDHParam string
servicesL4 []Service
client *client.Client
// template loaded ready to be used to generate the nginx configuration file
@ -231,8 +231,6 @@ type NginxManager struct {
// obj runtime object to be used in events
obj k8sruntime.Object
recorder record.EventRecorder
reloadLock *sync.Mutex
}
@ -276,17 +274,25 @@ func newDefaultNginxCfg() *nginxConfiguration {
// NewManager ...
func NewManager(kubeClient *client.Client, defaultSvc, customErrorSvc Service) *NginxManager {
ngx := &NginxManager{
ConfigFile: "/etc/nginx/nginx.conf",
defBackend: defaultSvc,
defCfg: newDefaultNginxCfg(),
defError: customErrorSvc,
defResolver: strings.Join(getDnsServers(), " "),
reloadLock: &sync.Mutex{},
sslDHParam: ssl.SearchDHParamFile(sslDirectory),
sslCertificates: ssl.CreateSSLCerts(sslDirectory),
ConfigFile: "/etc/nginx/nginx.conf",
defBackend: defaultSvc,
defCfg: newDefaultNginxCfg(),
defError: customErrorSvc,
defResolver: strings.Join(getDnsServers(), " "),
reloadLock: &sync.Mutex{},
}
ngx.createCertsDir(sslDirectory)
ngx.sslDHParam = ssl.SearchDHParamFile(sslDirectory)
ngx.loadTemplate()
return ngx
}
func (nginx *NginxManager) createCertsDir(base string) {
if err := os.Mkdir(base, os.ModeDir); err != nil {
glog.Fatalf("Couldn't create directory %v: %v", base, err)
}
}

View file

@ -16,13 +16,11 @@ limitations under the License.
package nginx
// NGINXController Updates NGINX configuration, starts and reloads NGINX
type NGINXController struct {
resolver string
nginxConfdPath string
nginxCertsPath string
local bool
}
import (
"os"
"github.com/golang/glog"
)
// IngressNGINXConfig describes an NGINX configuration
type IngressNGINXConfig struct {
@ -113,3 +111,25 @@ func NewUpstream(name string) Upstream {
Backends: []UpstreamServer{},
}
}
// AddOrUpdateCertAndKey creates a .pem file wth the cert and the key with the specified name
func (nginx *NginxManager) AddOrUpdateCertAndKey(name string, cert string, key string) string {
pemFileName := sslDirectory + "/" + name + ".pem"
pem, err := os.Create(pemFileName)
if err != nil {
glog.Fatalf("Couldn't create pem file %v: %v", pemFileName, err)
}
defer pem.Close()
_, err = pem.WriteString(string(key))
if err != nil {
glog.Fatalf("Couldn't write to pem file %v: %v", pemFileName, err)
}
_, err = pem.WriteString(string(cert))
if err != nil {
glog.Fatalf("Couldn't write to pem file %v: %v", pemFileName, err)
}
return pemFileName
}

View file

@ -25,12 +25,9 @@ import (
"github.com/fatih/structs"
"github.com/golang/glog"
"k8s.io/contrib/ingress/controllers/nginx-third-party/ssl"
)
var funcMap = template.FuncMap{
"getSSLHost": ssl.GetSSLHost,
"empty": func(input interface{}) bool {
check, ok := input.(string)
if ok {
@ -66,7 +63,6 @@ func (ngx *NginxManager) writeCfg(cfg *nginxConfiguration, upstreams []Upstream,
curNginxCfg := merge(toMap, fromMap)
conf := make(map[string]interface{})
conf["sslCertificates"] = ngx.sslCertificates
conf["upstreams"] = upstreams
conf["servers"] = servers
conf["tcpServices"] = servicesL4

View file

@ -17,139 +17,13 @@ limitations under the License.
package ssl
import (
"crypto/tls"
"crypto/x509"
"encoding/pem"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"strings"
"github.com/golang/glog"
)
// Certificate contains the cert, key and the list of valid hostnames
type Certificate struct {
Cert string
Key string
Cname []string
Valid bool
Default bool
}
// CreateSSLCerts reads the content of the /etc/nginx-ssl directory and
// verifies the cert and key extracting the common names for this pair
func CreateSSLCerts(baseDir string) []Certificate {
sslCerts := []Certificate{}
glog.Infof("inspecting directory %v for SSL certificates\n", baseDir)
files, _ := ioutil.ReadDir(baseDir)
for _, file := range files {
if !file.IsDir() {
continue
}
// the name of the secret could be different than the certificate file
cert, key, err := getCert(fmt.Sprintf("%v/%v", baseDir, file.Name()))
if err != nil {
glog.Errorf("error checking certificate: %v", err)
continue
}
hosts, err := checkSSLCertificate(cert, key)
if err == nil {
sslCert := Certificate{
Cert: cert,
Key: key,
Cname: hosts,
Valid: true,
}
if file.Name() == "default" {
sslCert.Default = true
}
sslCerts = append(sslCerts, sslCert)
} else {
glog.Errorf("error checking certificate: %v", err)
}
}
if len(sslCerts) == 1 {
sslCerts[0].Default = true
}
glog.Infof("ssl certificates found: %v", sslCerts)
return sslCerts
}
// checkSSLCertificate check if the certificate and key file are valid
// returning the result of the validation and the list of hostnames
// contained in the common name/s
func checkSSLCertificate(certFile, keyFile string) ([]string, error) {
_, err := tls.LoadX509KeyPair(certFile, keyFile)
if err != nil {
glog.Errorf("Error checking certificate and key file %v/%v: %v", certFile, keyFile, err)
return []string{}, err
}
pemCerts, err := ioutil.ReadFile(certFile)
if err != nil {
return []string{}, err
}
var block *pem.Block
block, pemCerts = pem.Decode(pemCerts)
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
glog.Errorf("Error checking certificate and key file %v/%v: %v", certFile, keyFile, err)
return []string{}, err
}
cn := []string{cert.Subject.CommonName}
if len(cert.DNSNames) > 0 {
cn = append(cn, cert.DNSNames...)
}
glog.Infof("DNS %v %v\n", cn, len(cn))
return cn, nil
}
func verifyHostname(certFile, host string) bool {
pemCerts, err := ioutil.ReadFile(certFile)
if err != nil {
return false
}
var block *pem.Block
block, pemCerts = pem.Decode(pemCerts)
cert, err := x509.ParseCertificate(block.Bytes)
err = cert.VerifyHostname(host)
if err == nil {
return true
}
return false
}
// GetSSLHost checks if in one of the secrets that contains SSL
// certificates could be used for the specified server name
func GetSSLHost(serverName string, certs []Certificate) Certificate {
for _, sslCert := range certs {
if verifyHostname(sslCert.Cert, serverName) {
return sslCert
}
}
return Certificate{}
}
// SearchDHParamFile iterates all the secrets mounted inside the /etc/nginx-ssl directory
// in order to find a file with the name dhparam.pem. If such file exists it will
// returns the path. If not it just returns an empty string
@ -170,32 +44,3 @@ func SearchDHParamFile(baseDir string) string {
glog.Warning("no file dhparam.pem found in secrets")
return ""
}
// getCert returns the pair cert-key if exists or an error
func getCert(certDir string) (cert string, key string, err error) {
// we search for a file with extension crt
filepath.Walk(certDir, func(path string, f os.FileInfo, _ error) error {
if !f.IsDir() {
r, err := regexp.MatchString(".crt", f.Name())
if err == nil && r {
cert = f.Name()
return nil
}
}
return nil
})
cert = fmt.Sprintf("%v/%v", certDir, cert)
if _, err := os.Stat(cert); os.IsNotExist(err) {
return "", "", fmt.Errorf("No certificate found in directory %v: %v", certDir, err)
}
key = strings.Replace(cert, ".crt", ".key", 1)
if _, err := os.Stat(key); os.IsNotExist(err) {
return "", "", fmt.Errorf("No certificate key found in directory %v: %v", certDir, err)
}
return
}