Commit bbafe336 authored by Geoff Simmons's avatar Geoff Simmons

WIP: implement TLS onload for more than one BackendConfig.

XXX: currently does not always delete haproxy backends for onload
when it should. This can be demonstrated with the tls/onload test:
deploy, verify, undeploy, then deploy again. The second deployment
fails because the haproxy backend from the first run, which should
have been removed by undeployment, still exists and is reported
by dataplane as 409 Conflict.
parent a3eb0de5
......@@ -70,7 +70,7 @@ k8s-ingress: vikingctrl
check: build
go vet $(CODE_SUBDIRS)
golint $(CODE_SUBDIRS)
go test -v ./pkg/controller/... ./pkg/varnish/...
go test -v ./pkg/controller/... ./pkg/varnish/... ./pkg/net/...
test: check
......
......@@ -51,6 +51,9 @@ func (worker *NamespaceWorker) enqueueIngsForBackendSvcs(svcs []string,
if err != nil {
return update.MakeRecoverable("%v", err)
}
worker.log.Debugf("enqueue Ingresses for BackendConfig %s/%s: "+
"found %d Ingresses in the namespace", namespace, name,
len(ings))
for _, ing := range ings {
if ing.Spec.DefaultBackend != nil {
svc2ing[ing.Spec.DefaultBackend.Service.Name] = ing
......
......@@ -987,6 +987,7 @@ func (worker *NamespaceWorker) ings2OffloaderSpec(
Namespace: svc.Namespace,
Name: svc.Name,
Secrets: make([]haproxy.SecretSpec, 0),
Onload: make(map[string]haproxy.OnloadSpec),
}
for _, ing := range ings {
for _, tls := range ing.Spec.TLS {
......@@ -1184,10 +1185,15 @@ func (worker *NamespaceWorker) addOrUpdateIng(
return update.MakeRecoverable("%v", err)
}
if worker.hController.HasOffloader(svcKey) {
worker.log.Infof("Deleting haproxy config for %s",
svcKey)
if status := worker.hController.
DeleteOffldSvc(svcKey); status.IsError() {
return status
}
} else {
worker.log.Infof("No haproxy config found for %s",
svcKey)
}
return update.MakeNoop("")
}
......@@ -1257,16 +1263,8 @@ func (worker *NamespaceWorker) addOrUpdateIng(
return update.MakeFatal("%v", err)
}
offldrSpec.Defaults = &haproxyDefSpec
if len(onlds) > 0 {
if len(onlds) > 1 {
// XXX
return update.MakeFatal(
"Multiple TLS onload configs currently not " +
"supported")
} // else {
for _, v := range onlds {
offldrSpec.Onload = v
}
for n, v := range onlds {
offldrSpec.Onload[n] = *v
}
if offldrSpec.Name == "" && len(onlds) == 0 {
worker.log.Infof("No TLS config found for Ingresses: %v",
......
......@@ -36,10 +36,13 @@ import (
"io/ioutil"
"net/http"
"net/url"
"regexp"
"strconv"
"strings"
"time"
"code.uplex.de/uplex-varnish/k8s-ingress/pkg/net"
"github.com/go-openapi/strfmt"
models "github.com/haproxytech/models/v2"
......@@ -57,22 +60,23 @@ const (
versionHdr = "Configuration-Version"
reloadIDHdr = "Reload-ID"
varnishSock = "unix@/varnish.sock"
backendSock = "unix@/run/offload/onload.sock"
backendSockPfx = "unix@"
frontend = "offloader"
onloader = "onloader"
onloaderPfx = "onldr_"
varnish = "varnish"
server = varnish
ingBackends = "ingressBackends"
ingBackendsPfx = "ingBackends_"
frontendSitePath = sitesPath + "/" + frontend
backendSitePath = sitesPath + "/" + onloader
crtPath = "/etc/ssl/private"
caBundlePath = "/run/haproxy/ca-bundle.crt"
encodingBase = 36
)
var (
port = int64(4443)
sslPort = int64(443)
roundRobin = "roundrobin"
port = int64(4443)
sslPort = int64(443)
roundRobin = "roundrobin"
nonProxyNameChars = regexp.MustCompile(`[^A-Za-z0-9_.:-]+`)
)
// ReloadStatus classifies the current state of a dataplane reload.
......@@ -132,6 +136,10 @@ type ReloadState struct {
type DataplaneError struct {
// Err encapsulates the dataplane API's error object.
Err *models.Error
// Method is the HTTP request method that led to the error response.
Method string
// URI is the request URI that let to the error response.
URI string
// Status is the HTTP response code.
Status int
// Version is the configuration-version returned in the response.
......@@ -143,7 +151,11 @@ func (err *DataplaneError) Error() string {
sb.WriteString(http.StatusText(err.Status))
sb.WriteString(" (")
sb.WriteString(strconv.Itoa(err.Status))
sb.WriteString(") version=")
sb.WriteString(") ")
sb.WriteString(err.Method)
sb.WriteRune(' ')
sb.WriteString(err.URI)
sb.WriteString(" version=")
sb.WriteString(strconv.Itoa(err.Version))
if err.Err.Code != nil {
sb.WriteString(" models.Error.Code=")
......@@ -232,16 +244,42 @@ func getSite() ([]byte, error) {
return offldSite.MarshalBinary()
}
func getOnldSite(spec *OnloadSpec) ([]byte, error) {
// XXX DRY with replNonFileChars() in package net.
func encodeChars(s string) string {
var sb strings.Builder
sb.WriteRune('_')
for _, r := range s {
sb.WriteString(strconv.FormatUint(uint64(r), encodingBase))
}
sb.WriteRune('_')
return sb.String()
}
// toProxyName converts a string to a proxy name that is legal for haproxy.cfg.
// From haproxy configuration manual section 4:
// "All proxy names must be formed from upper and lower case letters, digits,
// '-' (dash), '_' (underscore) , '.' (dot) and ':' (colon)."
func toProxyName(s string) string {
return nonProxyNameChars.ReplaceAllStringFunc(s, encodeChars)
}
func onldSitePath(s string) string {
return sitesPath + "/" + onloaderPfx + toProxyName(s)
}
func getOnldSite(name string, spec *OnloadSpec) ([]byte, error) {
// XXX: stick-table configuration
maxConn := int64(spec.MaxConn)
proxyName := onloaderPfx + toProxyName(name)
sockPath := backendSockPfx + net.UDSPath(name)
backendName := ingBackendsPfx + toProxyName(name)
site := &models.Site{
Name: onloader,
Name: proxyName,
Service: &models.SiteService{
Listeners: []*models.Bind{
{
Name: onloader,
Address: backendSock,
Name: proxyName,
Address: sockPath,
Mode: "660",
AcceptProxy: true,
},
......@@ -251,7 +289,7 @@ func getOnldSite(spec *OnloadSpec) ([]byte, error) {
},
Farms: []*models.SiteFarm{
{
Name: ingBackends,
Name: backendName,
UseAs: "default",
Mode: "tcp",
Balance: &models.Balance{
......@@ -352,6 +390,8 @@ func getTx(body []byte) (tx *models.Transaction, err error) {
func getError(resp *http.Response, body []byte) error {
dplaneErr := &DataplaneError{
Method: resp.Request.Method,
URI: resp.Request.URL.RequestURI(),
Err: &models.Error{},
Status: resp.StatusCode,
}
......@@ -447,7 +487,7 @@ func (client *DataplaneClient) FinishTx(
func (client *DataplaneClient) configTLS(
tx *models.Transaction,
path, method string,
path, method, svc string,
onldSpec *OnloadSpec,
) error {
var rdr *bytes.Reader
......@@ -458,7 +498,7 @@ func (client *DataplaneClient) configTLS(
if onldSpec == nil {
siteBytes, err = getSite()
} else {
siteBytes, err = getOnldSite(onldSpec)
siteBytes, err = getOnldSite(svc, onldSpec)
}
if err != nil {
return err
......@@ -567,7 +607,7 @@ func (client *DataplaneClient) configDefaults(
//
// A non-nil error return may wrap a DataplaneError.
func (client *DataplaneClient) AddOffldr(tx *models.Transaction) error {
return client.configTLS(tx, sitesPath, http.MethodPost, nil)
return client.configTLS(tx, sitesPath, http.MethodPost, "", nil)
}
// UpdateOffldr modifies the offloader configuration for haproxy, in
......@@ -578,7 +618,7 @@ func (client *DataplaneClient) AddOffldr(tx *models.Transaction) error {
//
// A non-nil error return may wrap a DataplaneError.
func (client *DataplaneClient) UpdateOffldr(tx *models.Transaction) error {
return client.configTLS(tx, frontendSitePath, http.MethodPut, nil)
return client.configTLS(tx, frontendSitePath, http.MethodPut, "", nil)
}
// DeleteOffldr removes the haproxy offloader configuration, in the
......@@ -586,12 +626,12 @@ func (client *DataplaneClient) UpdateOffldr(tx *models.Transaction) error {
//
// A non-nil error return may wrap a DataplaneError.
func (client *DataplaneClient) DeleteOffldr(tx *models.Transaction) error {
return client.configTLS(tx, frontendSitePath, http.MethodDelete, nil)
return client.configTLS(tx, frontendSitePath, http.MethodDelete, "",
nil)
}
// AddOnldr adds the onloader configuration for haproxy, in the
// dataplane transaction tx. instances specifies the number of servers
// in the haproxy backend.
// dataplane transaction tx.
//
// AddOnldr MUST be used if the onloader was not configured previously
// since the haproxy container was started, or after deletion.
......@@ -599,14 +639,14 @@ func (client *DataplaneClient) DeleteOffldr(tx *models.Transaction) error {
// A non-nil error return may wrap a DataplaneError.
func (client *DataplaneClient) AddOnldr(
tx *models.Transaction,
svc string,
onldSpec *OnloadSpec,
) error {
return client.configTLS(tx, sitesPath, http.MethodPost, onldSpec)
return client.configTLS(tx, sitesPath, http.MethodPost, svc, onldSpec)
}
// UpdateOnldr modifies the onloader configuration for haproxy, in the
// dataplane transaction tx. instances specifies the number of servers
// in the haproxy backend.
// dataplane transaction tx.
//
// UpdateOnldr MUST be used if the onloader was previously added with
// AddOnldr, and not removed with DeleteOnldr.
......@@ -614,17 +654,23 @@ func (client *DataplaneClient) AddOnldr(
// A non-nil error return may wrap a DataplaneError.
func (client *DataplaneClient) UpdateOnldr(
tx *models.Transaction,
svc string,
onldSpec *OnloadSpec,
) error {
return client.configTLS(tx, backendSitePath, http.MethodPut, onldSpec)
return client.configTLS(tx, onldSitePath(svc), http.MethodPut, svc,
onldSpec)
}
// DeleteOnldr removes the haproxy onloader configuration, in the
// dataplane transaction tx.
//
// A non-nil error return may wrap a DataplaneError.
func (client *DataplaneClient) DeleteOnldr(tx *models.Transaction) error {
return client.configTLS(tx, backendSitePath, http.MethodDelete, nil)
func (client *DataplaneClient) DeleteOnldr(
tx *models.Transaction,
svc string,
) error {
return client.configTLS(tx, onldSitePath(svc), http.MethodDelete, "",
nil)
}
// UpdateDefaults modifies default haproxy configuration (valid for both
......@@ -748,7 +794,7 @@ func (client *DataplaneClient) LoaderStatus() (
for _, site := range []*models.Site(sb.Sites) {
if site.Name == frontend {
offLoaded = true
} else if site.Name == onloader {
} else if strings.HasPrefix(site.Name, onloaderPfx) {
onLoaded = true
}
if offLoaded && onLoaded {
......
......@@ -87,15 +87,18 @@ type DefaultsSpec struct {
Timeouts DefaultTimeoutsSpec
}
// Spec specifies the configuration of TLS offload for haproxy. It
// includes the namespace and name of the Varnish admin Service (the
// headless k8s Service specifying ports for remote administration),
// and a list of specs for Ingress TLS Secrets.
// Spec specifies the configuration of TLS off- and onload for haproxy,
// including:
// - the namespace and name of the Varnish admin Service (the
// headless k8s Service specifying ports for remote administration),
// - a list of specs for Ingress TLS Secrets,
// - a (potentially empty) set of specifications for onload,
// - optional specifications for global timeouts.
type Spec struct {
Namespace string
Name string
Secrets []SecretSpec
Onload *OnloadSpec
Onload map[string]OnloadSpec
Defaults *DefaultsSpec
}
......@@ -348,6 +351,13 @@ func (hc *Controller) updateLoadStatus(inst *haproxyInst) error {
return nil
}
type onldSvcMap map[string]OnloadSpec
func (onldMap onldSvcMap) hasOnldSvc(svc string) bool {
_, exists := onldMap[svc]
return exists
}
func (hc *Controller) updateInstance(inst *haproxyInst, spec *Spec) error {
var err error
......@@ -367,7 +377,7 @@ func (hc *Controller) updateInstance(inst *haproxyInst, spec *Spec) error {
hc.wg.Add(1)
defer hc.wg.Done()
if len(spec.Secrets) == 0 && spec.Onload == nil {
if len(spec.Secrets) == 0 && len(spec.Onload) == 0 {
return update.MakeIncomplete(
"haproxy instance %s: no offload certificates or "+
"onload config specified", inst.name)
......@@ -413,23 +423,58 @@ func (hc *Controller) updateInstance(inst *haproxyInst, spec *Spec) error {
return err
}
}
if spec.Onload != nil && !inst.status.onLoaded {
for svcName, onld := range spec.Onload {
var onldMap onldSvcMap
if inst.spec != nil {
onldMap = onldSvcMap(inst.spec.Onload)
}
if tx.Version <= 1 || inst.spec == nil ||
inst.spec.Onload == nil {
!onldMap.hasOnldSvc(svcName) {
hc.log.Debugf("Onloader instance %s: "+
"adding TLS config %+v", inst.name,
*spec.Onload)
err = inst.dplane.AddOnldr(tx, spec.Onload)
"adding TLS config %+v for svc %s",
inst.name, onld, svcName)
err = inst.dplane.AddOnldr(tx, svcName, &onld)
} else {
hc.log.Debugf("Onloader instance %s: "+
"updating TLS config %+v", inst.name,
*spec.Onload)
err = inst.dplane.UpdateOnldr(tx, spec.Onload)
"updating TLS config %+v for svc %s", inst.name,
onld, svcName)
err = inst.dplane.UpdateOnldr(tx, svcName, &onld)
}
if err != nil {
return err
}
}
if inst.spec != nil {
for svcName, onld := range inst.spec.Onload {
if _, exists := spec.Onload[svcName]; exists {
continue
}
hc.log.Debugf("Onloader instance %s: "+
"deleting TLS config %+v for svc %s", inst.name,
onld, svcName)
if err = inst.dplane.DeleteOnldr(tx,
svcName); err != nil {
return err
}
}
}
// if len(spec.Onload) > 0 && !inst.status.onLoaded {
// if tx.Version <= 1 || inst.spec == nil ||
// inst.spec.Onload == nil {
// hc.log.Debugf("Onloader instance %s: "+
// "adding TLS config %+v", inst.name,
// *spec.Onload)
// err = inst.dplane.AddOnldr(tx, spec.Onload)
// } else {
// hc.log.Debugf("Onloader instance %s: "+
// "updating TLS config %+v", inst.name,
// *spec.Onload)
// err = inst.dplane.UpdateOnldr(tx, spec.Onload)
// }
// if err != nil {
// return err
// }
// }
if spec.Defaults == nil {
hc.log.Debugf("Instance %s: global defaults not set", inst.name)
} else {
......@@ -466,24 +511,24 @@ func (hc *Controller) updateInstance(inst *haproxyInst, spec *Spec) error {
Namespace: spec.Namespace,
Name: spec.Name,
Secrets: make([]SecretSpec, len(spec.Secrets)),
Onload: nil,
Onload: make(map[string]OnloadSpec),
Defaults: spec.Defaults,
}
for i, s := range spec.Secrets {
inst.spec.Secrets[i] = s
}
if spec.Onload != nil {
inst.spec.Onload = &OnloadSpec{
Verify: spec.Onload.Verify,
Authority: spec.Onload.Authority,
Instances: spec.Onload.Instances,
StickTblSz: spec.Onload.StickTblSz,
MaxConn: spec.Onload.MaxConn,
}
for svcName, onld := range spec.Onload {
inst.spec.Onload[svcName] = onld
}
// XXX where does this go?
// defer hc.dataplane.DeleteTx(tx)
// if spec.Onload != nil {
// inst.spec.Onload = &OnloadSpec{
// Verify: spec.Onload.Verify,
// Authority: spec.Onload.Authority,
// Instances: spec.Onload.Instances,
// StickTblSz: spec.Onload.StickTblSz,
// MaxConn: spec.Onload.MaxConn,
// }
// }
switch state.Status {
case Succeeded:
......@@ -494,7 +539,7 @@ func (hc *Controller) updateInstance(inst *haproxyInst, spec *Spec) error {
if len(spec.Secrets) > 0 {
inst.status.offLoaded = true
}
if spec.Onload != nil {
if len(spec.Onload) > 0 {
inst.status.onLoaded = true
}
return nil
......@@ -518,7 +563,7 @@ func (hc *Controller) updateInstance(inst *haproxyInst, spec *Spec) error {
if len(spec.Secrets) > 0 {
inst.status.offLoaded = true
}
if spec.Onload != nil {
if len(spec.Onload) > 0 {
inst.status.onLoaded = true
}
return nil
......@@ -612,6 +657,16 @@ func (hc *Controller) removeOffldrInstances(
continue
}
for onldSvc := range inst.spec.Onload {
hc.log.Debugf("haproxy instance %s: deleting onload "+
"config for %s", inst.name, onldSvc)
err = inst.dplane.DeleteOnldr(tx, onldSvc)
if err != nil {
errs = append(errs, inst.mkError(err))
continue
}
}
hc.log.Debugf("haproxy instance %s: finishing tx for "+
"version %d: %+v", inst.name, tx.Version, tx)
state, err := inst.dplane.FinishTx(tx)
......
......@@ -31,11 +31,29 @@
package net
import (
"crypto/sha256"
"encoding/binary"
"net"
"regexp"
"strconv"
"strings"
"syscall"
"github.com/sirupsen/logrus"
)
const (
udsPfx = "/run/offload/onld_"
udsSfx = ".sock"
encBase = 36
)
var (
udsAddr = syscall.RawSockaddrUnix{}
udsPathLen = len(udsAddr.Path)
nonFileChars = regexp.MustCompile(`[^A-Za-z0-9_.]+`)
)
// IsNonTimeoutNetErr returns true for network errors that are not
// timeouts. On syncs for deletion, the Pod may be already gone.
func IsNonTimeoutNetErr(err error, log *logrus.Logger) bool {
......@@ -47,3 +65,37 @@ func IsNonTimeoutNetErr(err error, log *logrus.Logger) bool {
log.Warnf("Non-timeout network error: %+v", err)
return true
}
func replNonFileChars(s string) string {
var sb strings.Builder
sb.WriteRune('_')
for _, r := range s {
sb.WriteString(strconv.FormatUint(uint64(r), encBase))
}
sb.WriteRune('_')
return sb.String()
}
func toFileChars(s string) string {
return nonFileChars.ReplaceAllStringFunc(s, replNonFileChars)
}
// UDSPath returns the path accessed by both Varnish and haproxy for
// the Unix domain socket used for TLS onload to the service
// designated by name.
func UDSPath(name string) string {
var sb strings.Builder
sb.WriteString(udsPfx)
fileStr := toFileChars(name)
if len(udsPfx)+len(fileStr)+len(udsSfx) < udsPathLen {
sb.WriteString(fileStr)
} else {
hash := sha256.Sum256([]byte(name))
for i := 0; i < sha256.Size; i += 8 {
n := binary.NativeEndian.Uint64((hash[i : i+8]))
sb.WriteString(strconv.FormatUint(n, encBase))
}
}
sb.WriteString(udsSfx)
return sb.String()
}
......@@ -7,7 +7,9 @@ import dynamic;
import selector;
include "bogo_backend.vcl";
backend vk8s_via { .path = "/run/offload/onload.sock"; }
backend vk8s_via_tea-svc {
.path = "/run/offload/onld_tea_19_svc.sock";
}
backend vk8s_default_coffee-6b9f5c47d7-bdt68_80 {
.host = "192.0.2.4";
......@@ -20,19 +22,19 @@ backend vk8s_default_coffee-6b9f5c47d7-l5zvl_80 {
backend vk8s_default_tea-5798f99dc5-5wj8n_80 {
.host = "192.0.2.1";
.port = "80";
.via = vk8s_via;
.via = vk8s_via_tea-svc;
.authority = "www.tea.org";
}
backend vk8s_default_tea-5798f99dc5-hn27l_80 {
.host = "192.0.2.2";
.port = "80";
.via = vk8s_via;
.via = vk8s_via_tea-svc;
.authority = "www.tea.org";
}
backend vk8s_default_tea-5798f99dc5-5wj8n_80 {
.host = "192.0.2.3";
.port = "80";
.via = vk8s_via;
.via = vk8s_via_tea-svc;
.authority = "www.tea.org";
}
......
......@@ -7,7 +7,9 @@ import dynamic;
import selector;
include "bogo_backend.vcl";
backend vk8s_via { .path = "/run/offload/onload.sock"; }
backend vk8s_via_tea-svc {
.path = "/run/offload/onld_tea_19_svc.sock";
}
backend vk8s_default_coffee-6b9f5c47d7-bdt68_80 {
.host = "192.0.2.4";
......@@ -20,17 +22,17 @@ backend vk8s_default_coffee-6b9f5c47d7-l5zvl_80 {
backend vk8s_default_tea-5798f99dc5-5wj8n_80 {
.host = "192.0.2.1";
.port = "80";
.via = vk8s_via;
.via = vk8s_via_tea-svc;
}
backend vk8s_default_tea-5798f99dc5-hn27l_80 {
.host = "192.0.2.2";
.port = "80";
.via = vk8s_via;
.via = vk8s_via_tea-svc;
}
backend vk8s_default_tea-5798f99dc5-5wj8n_80 {
.host = "192.0.2.3";
.port = "80";
.via = vk8s_via;
.via = vk8s_via_tea-svc;
}
......
......@@ -32,6 +32,8 @@ import (
"sort"
"strings"
"text/template"
"code.uplex.de/uplex-varnish/k8s-ingress/pkg/net"
)
const ingTmplSrc = `vcl 4.1;
......@@ -83,9 +85,18 @@ probe {{probeName $name}} {
{{- end}}
{{- template "ProbeDef" .IntSvcs -}}
{{- template "ProbeDef" .ExtSvcs}}
{{- if needsVia .IntSvcs .ExtSvcs }}
backend vk8s_via { .path = "/run/offload/onload.sock"; }
{{- end }}
{{- define "ViaDef"}}
{{- range $name, $svc := .}}
{{- if $svc.Via}}
backend {{viaBackendName $name}} {
.path = "{{onldUDSPath $name}}";
}
{{- end}}
{{- end}}
{{- end}}
{{- template "ViaDef" .IntSvcs -}}
{{- template "ViaDef" .ExtSvcs}}
{{range $name, $svc := .IntSvcs -}}
{{range $addr := $svc.Addresses -}}
......@@ -115,7 +126,7 @@ backend {{backendName $svc $addr}} {
.probe = {{probeName $name}};
{{- end}}
{{- if .Via}}
.via = vk8s_via;
.via = {{viaBackendName $name}};
{{- end}}
{{- if .Authority}}
.authority = "{{.Authority}}";
......@@ -203,7 +214,7 @@ sub vcl_init {
, probe = {{probeName $name}}
{{- end}}
{{- if .Via}}
, via = vk8s_via
, via = {{viaBackendName $name}}
{{- end}}
{{- end}}
);
......@@ -483,6 +494,12 @@ var vclFuncs = template.FuncMap{
"resolverName": func(svc Service) string {
return mangle(svc.Name + "_resolver")
},
"viaBackendName": func(name string) string {
return mangle("via_" + name)
},
"onldUDSPath": func(name string) string {
return net.UDSPath(name)
},
}
var ingressTmpl = template.Must(template.New(ingTmplName).
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment