Merge branch 'master' into nginx/extauth_headers

This commit is contained in:
electroma 2017-02-27 16:28:11 -05:00 committed by GitHub
commit c8eda8f17f
89 changed files with 3309 additions and 379 deletions

View file

@ -20,6 +20,7 @@ import (
"fmt"
"io/ioutil"
"os"
"path"
"regexp"
"github.com/pkg/errors"
@ -59,8 +60,17 @@ type auth struct {
// NewParser creates a new authentication annotation parser
func NewParser(authDirectory string, sr resolver.Secret) parser.IngressAnnotation {
// TODO: check permissions required
os.MkdirAll(authDirectory, 0655)
os.MkdirAll(authDirectory, 0755)
currPath := authDirectory
for currPath != "/" {
currPath = path.Dir(currPath)
err := os.Chmod(currPath, 0755)
if err != nil {
break
}
}
return auth{sr, authDirectory}
}

View file

@ -100,7 +100,11 @@ func (a authReq) Parse(ing *extensions.Ingress) (interface{}, error) {
return nil, ing_errors.NewLocationDenied("invalid url host")
}
m, _ := parser.GetStringAnnotation(authMethod, ing)
m, err := parser.GetStringAnnotation(authMethod, ing)
if err != nil {
return nil, err
}
if len(m) != 0 && !validMethod(m) {
return nil, ing_errors.NewLocationDenied("invalid HTTP method")
}

View file

@ -28,11 +28,16 @@ import (
const (
// name of the secret
authTLSSecret = "ingress.kubernetes.io/auth-tls-secret"
annotationAuthTLSSecret = "ingress.kubernetes.io/auth-tls-secret"
annotationAuthTLSDepth = "ingress.kubernetes.io/auth-tls-verify-depth"
defaultAuthTLSDepth = 1
)
type authTLS struct {
certResolver resolver.AuthCertificate
// AuthSSLConfig contains the AuthSSLCert used for muthual autentication
// and the configured ValidationDepth
type AuthSSLConfig struct {
AuthSSLCert resolver.AuthSSLCert
ValidationDepth int `json:"validationDepth"`
}
// NewParser creates a new TLS authentication annotation parser
@ -40,29 +45,42 @@ func NewParser(resolver resolver.AuthCertificate) parser.IngressAnnotation {
return authTLS{resolver}
}
// ParseAnnotations parses the annotations contained in the ingress
// rule used to use an external URL as source for authentication
type authTLS struct {
certResolver resolver.AuthCertificate
}
// Parse parses the annotations contained in the ingress
// rule used to use a Certificate as authentication method
func (a authTLS) Parse(ing *extensions.Ingress) (interface{}, error) {
str, err := parser.GetStringAnnotation(authTLSSecret, ing)
tlsauthsecret, err := parser.GetStringAnnotation(annotationAuthTLSSecret, ing)
if err != nil {
return nil, err
return &AuthSSLConfig{}, err
}
if str == "" {
return nil, ing_errors.NewLocationDenied("an empty string is not a valid secret name")
if tlsauthsecret == "" {
return &AuthSSLConfig{}, ing_errors.NewLocationDenied("an empty string is not a valid secret name")
}
_, _, err = k8s.ParseNameNS(str)
_, _, err = k8s.ParseNameNS(tlsauthsecret)
if err != nil {
return nil, ing_errors.NewLocationDenied("an empty string is not a valid secret name")
return &AuthSSLConfig{}, ing_errors.NewLocationDenied("an empty string is not a valid secret name")
}
authCert, err := a.certResolver.GetAuthCertificate(str)
tlsdepth, err := parser.GetIntAnnotation(annotationAuthTLSDepth, ing)
if err != nil || tlsdepth == 0 {
tlsdepth = defaultAuthTLSDepth
}
authCert, err := a.certResolver.GetAuthCertificate(tlsauthsecret)
if err != nil {
return nil, ing_errors.LocationDenied{
return &AuthSSLConfig{}, ing_errors.LocationDenied{
Reason: errors.Wrap(err, "error obtaining certificate"),
}
}
return authCert, nil
return &AuthSSLConfig{
AuthSSLCert: *authCert,
ValidationDepth: tlsdepth,
}, nil
}

View file

@ -24,11 +24,13 @@ import (
)
const (
bodySize = "ingress.kubernetes.io/proxy-body-size"
connect = "ingress.kubernetes.io/proxy-connect-timeout"
send = "ingress.kubernetes.io/proxy-send-timeout"
read = "ingress.kubernetes.io/proxy-read-timeout"
bufferSize = "ingress.kubernetes.io/proxy-buffer-size"
bodySize = "ingress.kubernetes.io/proxy-body-size"
connect = "ingress.kubernetes.io/proxy-connect-timeout"
send = "ingress.kubernetes.io/proxy-send-timeout"
read = "ingress.kubernetes.io/proxy-read-timeout"
bufferSize = "ingress.kubernetes.io/proxy-buffer-size"
cookiePath = "ingress.kubernetes.io/proxy-cookie-path"
cookieDomain = "ingress.kubernetes.io/proxy-cookie-domain"
)
// Configuration returns the proxy timeout to use in the upstream server/s
@ -38,6 +40,8 @@ type Configuration struct {
SendTimeout int `json:"sendTimeout"`
ReadTimeout int `json:"readTimeout"`
BufferSize string `json:"bufferSize"`
CookieDomain string `json:"cookieDomain"`
CookiePath string `json:"cookiePath"`
}
type proxy struct {
@ -73,10 +77,20 @@ func (a proxy) Parse(ing *extensions.Ingress) (interface{}, error) {
bufs = defBackend.ProxyBufferSize
}
cp, err := parser.GetStringAnnotation(cookiePath, ing)
if err != nil || cp == "" {
cp = defBackend.ProxyCookiePath
}
cd, err := parser.GetStringAnnotation(cookieDomain, ing)
if err != nil || cd == "" {
cd = defBackend.ProxyCookieDomain
}
bs, err := parser.GetStringAnnotation(bodySize, ing)
if err != nil || bs == "" {
bs = defBackend.ProxyBodySize
}
return &Configuration{bs, ct, st, rt, bufs}, nil
return &Configuration{bs, ct, st, rt, bufs, cd, cp}, nil
}

View file

@ -52,11 +52,7 @@ func NewParser(br resolver.DefaultBackend) parser.IngressAnnotation {
// ParseAnnotations parses the annotations contained in the ingress
// rule used to rewrite the defined paths
func (a rewrite) Parse(ing *extensions.Ingress) (interface{}, error) {
rt, err := parser.GetStringAnnotation(rewriteTo, ing)
if err != nil {
return nil, err
}
rt, _ := parser.GetStringAnnotation(rewriteTo, ing)
sslRe, err := parser.GetBoolAnnotation(sslRedirect, ing)
if err != nil {
sslRe = a.backendResolver.GetDefaultBackend().SSLRedirect

View file

@ -76,8 +76,8 @@ func (m mockBackend) GetDefaultBackend() defaults.Backend {
func TestWithoutAnnotations(t *testing.T) {
ing := buildIngress()
_, err := NewParser(mockBackend{}).Parse(ing)
if err == nil {
t.Error("Expected error with ingress without annotations")
if err != nil {
t.Errorf("unexpected error with ingress without annotations: %v", err)
}
}

View file

@ -0,0 +1,118 @@
/*
Copyright 2016 The Kubernetes Authors.
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 sessionaffinity
import (
"regexp"
"github.com/golang/glog"
"k8s.io/kubernetes/pkg/apis/extensions"
"k8s.io/ingress/core/pkg/ingress/annotations/parser"
)
const (
annotationAffinityType = "ingress.kubernetes.io/affinity"
// If a cookie with this name exists,
// its value is used as an index into the list of available backends.
annotationAffinityCookieName = "ingress.kubernetes.io/session-cookie-name"
defaultAffinityCookieName = "INGRESSCOOKIE"
// This is the algorithm used by nginx to generate a value for the session cookie, if
// one isn't supplied and affinity is set to "cookie".
annotationAffinityCookieHash = "ingress.kubernetes.io/session-cookie-hash"
defaultAffinityCookieHash = "md5"
)
var (
affinityCookieHashRegex = regexp.MustCompile(`^(index|md5|sha1)$`)
)
// AffinityConfig describes the per ingress session affinity config
type AffinityConfig struct {
// The type of affinity that will be used
AffinityType string `json:"type"`
CookieConfig
}
// CookieConfig describes the Config of cookie type affinity
type CookieConfig struct {
// The name of the cookie that will be used in case of cookie affinity type.
Name string `json:"name"`
// The hash that will be used to encode the cookie in case of cookie affinity type
Hash string `json:"hash"`
}
// CookieAffinityParse gets the annotation values related to Cookie Affinity
// It also sets default values when no value or incorrect value is found
func CookieAffinityParse(ing *extensions.Ingress) *CookieConfig {
sn, err := parser.GetStringAnnotation(annotationAffinityCookieName, ing)
if err != nil || sn == "" {
glog.V(3).Infof("Ingress %v: No value found in annotation %v. Using the default %v", ing.Name, annotationAffinityCookieName, defaultAffinityCookieName)
sn = defaultAffinityCookieName
}
sh, err := parser.GetStringAnnotation(annotationAffinityCookieHash, ing)
if err != nil || !affinityCookieHashRegex.MatchString(sh) {
glog.V(3).Infof("Invalid or no annotation value found in Ingress %v: %v. Setting it to default %v", ing.Name, annotationAffinityCookieHash, defaultAffinityCookieHash)
sh = defaultAffinityCookieHash
}
return &CookieConfig{
Name: sn,
Hash: sh,
}
}
// NewParser creates a new Affinity annotation parser
func NewParser() parser.IngressAnnotation {
return affinity{}
}
type affinity struct {
}
// ParseAnnotations parses the annotations contained in the ingress
// rule used to configure the affinity directives
func (a affinity) Parse(ing *extensions.Ingress) (interface{}, error) {
var cookieAffinityConfig *CookieConfig
cookieAffinityConfig = &CookieConfig{}
// Check the type of affinity that will be used
at, err := parser.GetStringAnnotation(annotationAffinityType, ing)
if err != nil {
at = ""
}
switch at {
case "cookie":
cookieAffinityConfig = CookieAffinityParse(ing)
default:
glog.V(3).Infof("No default affinity was found for Ingress %v", ing.Name)
}
return &AffinityConfig{
AffinityType: at,
CookieConfig: *cookieAffinityConfig,
}, nil
}

View file

@ -0,0 +1,88 @@
/*
Copyright 2016 The Kubernetes Authors.
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 sessionaffinity
import (
"testing"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/apis/extensions"
"k8s.io/kubernetes/pkg/util/intstr"
)
func buildIngress() *extensions.Ingress {
defaultBackend := extensions.IngressBackend{
ServiceName: "default-backend",
ServicePort: intstr.FromInt(80),
}
return &extensions.Ingress{
ObjectMeta: api.ObjectMeta{
Name: "foo",
Namespace: api.NamespaceDefault,
},
Spec: extensions.IngressSpec{
Backend: &extensions.IngressBackend{
ServiceName: "default-backend",
ServicePort: intstr.FromInt(80),
},
Rules: []extensions.IngressRule{
{
Host: "foo.bar.com",
IngressRuleValue: extensions.IngressRuleValue{
HTTP: &extensions.HTTPIngressRuleValue{
Paths: []extensions.HTTPIngressPath{
{
Path: "/foo",
Backend: defaultBackend,
},
},
},
},
},
},
},
}
}
func TestIngressAffinityCookieConfig(t *testing.T) {
ing := buildIngress()
data := map[string]string{}
data[annotationAffinityType] = "cookie"
data[annotationAffinityCookieHash] = "sha123"
data[annotationAffinityCookieName] = "INGRESSCOOKIE"
ing.SetAnnotations(data)
affin, _ := NewParser().Parse(ing)
nginxAffinity, ok := affin.(*AffinityConfig)
if !ok {
t.Errorf("expected a Config type")
}
if nginxAffinity.AffinityType != "cookie" {
t.Errorf("expected cookie as sticky-type but returned %v", nginxAffinity.AffinityType)
}
if nginxAffinity.CookieConfig.Hash != "md5" {
t.Errorf("expected md5 as sticky-hash but returned %v", nginxAffinity.CookieConfig.Hash)
}
if nginxAffinity.CookieConfig.Name != "INGRESSCOOKIE" {
t.Errorf("expected route as sticky-name but returned %v", nginxAffinity.CookieConfig.Name)
}
}

View file

@ -0,0 +1,42 @@
/*
Copyright 2016 The Kubernetes Authors.
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 snippet
import (
"k8s.io/kubernetes/pkg/apis/extensions"
"k8s.io/ingress/core/pkg/ingress/annotations/parser"
)
const (
annotation = "ingress.kubernetes.io/configuration-snippet"
)
type snippet struct {
}
// NewParser creates a new CORS annotation parser
func NewParser() parser.IngressAnnotation {
return snippet{}
}
// Parse parses the annotations contained in the ingress rule
// used to indicate if the location/s contains a fragment of
// configuration to be included inside the paths of the rules
func (a snippet) Parse(ing *extensions.Ingress) (interface{}, error) {
return parser.GetStringAnnotation(annotation, ing)
}

View file

@ -0,0 +1,57 @@
/*
Copyright 2017 The Kubernetes Authors.
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 snippet
import (
"testing"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/apis/extensions"
)
func TestParse(t *testing.T) {
ap := NewParser()
if ap == nil {
t.Fatalf("expected a parser.IngressAnnotation but returned nil")
}
testCases := []struct {
annotations map[string]string
expected string
}{
{map[string]string{annotation: "more_headers"}, "more_headers"},
{map[string]string{annotation: "false"}, "false"},
{map[string]string{}, ""},
{nil, ""},
}
ing := &extensions.Ingress{
ObjectMeta: api.ObjectMeta{
Name: "foo",
Namespace: api.NamespaceDefault,
},
Spec: extensions.IngressSpec{},
}
for _, testCase := range testCases {
ing.SetAnnotations(testCase.annotations)
result, _ := ap.Parse(ing)
if result != testCase.expected {
t.Errorf("expected %v but returned %v, annotations: %s", testCase.expected, result, testCase.annotations)
}
}
}

View file

@ -33,6 +33,8 @@ import (
"k8s.io/ingress/core/pkg/ingress/annotations/ratelimit"
"k8s.io/ingress/core/pkg/ingress/annotations/rewrite"
"k8s.io/ingress/core/pkg/ingress/annotations/secureupstream"
"k8s.io/ingress/core/pkg/ingress/annotations/sessionaffinity"
"k8s.io/ingress/core/pkg/ingress/annotations/snippet"
"k8s.io/ingress/core/pkg/ingress/annotations/sslpassthrough"
"k8s.io/ingress/core/pkg/ingress/errors"
"k8s.io/ingress/core/pkg/ingress/resolver"
@ -51,18 +53,20 @@ type annotationExtractor struct {
func newAnnotationExtractor(cfg extractorConfig) annotationExtractor {
return annotationExtractor{
map[string]parser.IngressAnnotation{
"BasicDigestAuth": auth.NewParser(auth.AuthDirectory, cfg),
"ExternalAuth": authreq.NewParser(),
"CertificateAuth": authtls.NewParser(cfg),
"EnableCORS": cors.NewParser(),
"HealthCheck": healthcheck.NewParser(cfg),
"Whitelist": ipwhitelist.NewParser(cfg),
"UsePortInRedirects": portinredirect.NewParser(cfg),
"Proxy": proxy.NewParser(cfg),
"RateLimit": ratelimit.NewParser(),
"Redirect": rewrite.NewParser(cfg),
"SecureUpstream": secureupstream.NewParser(),
"SSLPassthrough": sslpassthrough.NewParser(),
"BasicDigestAuth": auth.NewParser(auth.AuthDirectory, cfg),
"ExternalAuth": authreq.NewParser(),
"CertificateAuth": authtls.NewParser(cfg),
"EnableCORS": cors.NewParser(),
"HealthCheck": healthcheck.NewParser(cfg),
"Whitelist": ipwhitelist.NewParser(cfg),
"UsePortInRedirects": portinredirect.NewParser(cfg),
"Proxy": proxy.NewParser(cfg),
"RateLimit": ratelimit.NewParser(),
"Redirect": rewrite.NewParser(cfg),
"SecureUpstream": secureupstream.NewParser(),
"SessionAffinity": sessionaffinity.NewParser(),
"SSLPassthrough": sslpassthrough.NewParser(),
"ConfigurationSnippet": snippet.NewParser(),
},
}
}
@ -96,9 +100,10 @@ func (e *annotationExtractor) Extract(ing *extensions.Ingress) map[string]interf
}
const (
secureUpstream = "SecureUpstream"
healthCheck = "HealthCheck"
sslPassthrough = "SSLPassthrough"
secureUpstream = "SecureUpstream"
healthCheck = "HealthCheck"
sslPassthrough = "SSLPassthrough"
sessionAffinity = "SessionAffinity"
)
func (e *annotationExtractor) SecureUpstream(ing *extensions.Ingress) bool {
@ -115,3 +120,8 @@ func (e *annotationExtractor) SSLPassthrough(ing *extensions.Ingress) bool {
val, _ := e.annotations[sslPassthrough].Parse(ing)
return val.(bool)
}
func (e *annotationExtractor) SessionAffinity(ing *extensions.Ingress) *sessionaffinity.AffinityConfig {
val, _ := e.annotations[sessionAffinity].Parse(ing)
return val.(*sessionaffinity.AffinityConfig)
}

View file

@ -28,10 +28,13 @@ import (
)
const (
annotationSecureUpstream = "ingress.kubernetes.io/secure-backends"
annotationUpsMaxFails = "ingress.kubernetes.io/upstream-max-fails"
annotationUpsFailTimeout = "ingress.kubernetes.io/upstream-fail-timeout"
annotationPassthrough = "ingress.kubernetes.io/ssl-passthrough"
annotationSecureUpstream = "ingress.kubernetes.io/secure-backends"
annotationUpsMaxFails = "ingress.kubernetes.io/upstream-max-fails"
annotationUpsFailTimeout = "ingress.kubernetes.io/upstream-fail-timeout"
annotationPassthrough = "ingress.kubernetes.io/ssl-passthrough"
annotationAffinityType = "ingress.kubernetes.io/affinity"
annotationAffinityCookieName = "ingress.kubernetes.io/session-cookie-name"
annotationAffinityCookieHash = "ingress.kubernetes.io/session-cookie-hash"
)
type mockCfg struct {
@ -179,3 +182,39 @@ func TestSSLPassthrough(t *testing.T) {
}
}
}
func TestAffinitySession(t *testing.T) {
ec := newAnnotationExtractor(mockCfg{})
ing := buildIngress()
fooAnns := []struct {
annotations map[string]string
affinitytype string
hash string
name string
}{
{map[string]string{annotationAffinityType: "cookie", annotationAffinityCookieHash: "md5", annotationAffinityCookieName: "route"}, "cookie", "md5", "route"},
{map[string]string{annotationAffinityType: "cookie", annotationAffinityCookieHash: "xpto", annotationAffinityCookieName: "route1"}, "cookie", "md5", "route1"},
{map[string]string{annotationAffinityType: "cookie", annotationAffinityCookieHash: "", annotationAffinityCookieName: ""}, "cookie", "md5", "INGRESSCOOKIE"},
{map[string]string{}, "", "", ""},
{nil, "", "", ""},
}
for _, foo := range fooAnns {
ing.SetAnnotations(foo.annotations)
r := ec.SessionAffinity(ing)
t.Logf("Testing pass %v %v %v", foo.affinitytype, foo.hash, foo.name)
if r == nil {
t.Errorf("Returned nil but expected a SessionAffinity.AffinityConfig")
continue
}
if r.CookieConfig.Hash != foo.hash {
t.Errorf("Returned %v but expected %v for Hash", r.CookieConfig.Hash, foo.hash)
}
if r.CookieConfig.Name != foo.name {
t.Errorf("Returned %v but expected %v for Name", r.CookieConfig.Name, foo.name)
}
}
}

View file

@ -98,6 +98,8 @@ func (ic *GenericController) syncSecret(k interface{}) error {
return nil
}
// getPemCertificate receives a secret, and creates a ingress.SSLCert as return.
// It parses the secret and verifies if it's a keypair, or a 'ca.crt' secret only.
func (ic *GenericController) getPemCertificate(secretName string) (*ingress.SSLCert, error) {
secretInterface, exists, err := ic.secrLister.Store.GetByKey(secretName)
if err != nil {
@ -108,19 +110,24 @@ func (ic *GenericController) getPemCertificate(secretName string) (*ingress.SSLC
}
secret := secretInterface.(*api.Secret)
cert, ok := secret.Data[api.TLSCertKey]
if !ok {
return nil, fmt.Errorf("secret named %v has no private key", secretName)
}
key, ok := secret.Data[api.TLSPrivateKeyKey]
if !ok {
return nil, fmt.Errorf("secret named %v has no cert", secretName)
}
cert, okcert := secret.Data[api.TLSCertKey]
key, okkey := secret.Data[api.TLSPrivateKeyKey]
ca := secret.Data["ca.crt"]
nsSecName := strings.Replace(secretName, "/", "-", -1)
s, err := ssl.AddOrUpdateCertAndKey(nsSecName, cert, key, ca)
var s *ingress.SSLCert
if okcert && okkey {
glog.V(3).Infof("Found certificate and private key, configuring %v as a TLS Secret", secretName)
s, err = ssl.AddOrUpdateCertAndKey(nsSecName, cert, key, ca)
} else if ca != nil {
glog.V(3).Infof("Found only ca.crt, configuring %v as an Certificate Authentication secret", secretName)
s, err = ssl.AddCertAuth(nsSecName, ca)
} else {
return nil, fmt.Errorf("No keypair or CA cert could be found in %v", secretName)
}
if err != nil {
return nil, err
}

View file

@ -76,11 +76,13 @@ type GenericController struct {
ingController *cache.Controller
endpController *cache.Controller
svcController *cache.Controller
nodeController *cache.Controller
secrController *cache.Controller
mapController *cache.Controller
ingLister cache_store.StoreToIngressLister
svcLister cache.StoreToServiceLister
nodeLister cache.StoreToNodeLister
endpLister cache.StoreToEndpointsLister
secrLister cache_store.StoreToSecretsLister
mapLister cache_store.StoreToConfigmapLister
@ -132,6 +134,7 @@ type Configuration struct {
Backend ingress.Controller
UpdateStatus bool
ElectionID string
}
// newIngressController creates an Ingress controller
@ -173,7 +176,7 @@ func newIngressController(config *Configuration) *GenericController {
DeleteFunc: func(obj interface{}) {
delIng := obj.(*extensions.Ingress)
if !IsValidClass(delIng, config.IngressClass) {
glog.Infof("ignoring add for ingress %v based on annotation %v", delIng.Name, ingressClassKey)
glog.Infof("ignoring delete for ingress %v based on annotation %v", delIng.Name, ingressClassKey)
return
}
ic.recorder.Eventf(delIng, api.EventTypeNormal, "DELETE", fmt.Sprintf("Ingress %s/%s", delIng.Namespace, delIng.Name))
@ -182,7 +185,7 @@ func newIngressController(config *Configuration) *GenericController {
UpdateFunc: func(old, cur interface{}) {
oldIng := old.(*extensions.Ingress)
curIng := cur.(*extensions.Ingress)
if !IsValidClass(curIng, config.IngressClass) {
if !IsValidClass(curIng, config.IngressClass) && !IsValidClass(oldIng, config.IngressClass) {
return
}
@ -292,11 +295,16 @@ func newIngressController(config *Configuration) *GenericController {
cache.ResourceEventHandlerFuncs{},
cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc})
ic.nodeLister.Store, ic.nodeController = cache.NewInformer(
cache.NewListWatchFromClient(ic.cfg.Client.Core().RESTClient(), "nodes", api.NamespaceAll, fields.Everything()),
&api.Node{}, ic.cfg.ResyncPeriod, eventHandler)
if config.UpdateStatus {
ic.syncStatus = status.NewStatusSyncer(status.Config{
Client: config.Client,
PublishService: ic.cfg.PublishService,
IngressLister: ic.ingLister,
ElectionID: config.ElectionID,
})
} else {
glog.Warning("Update of ingress status is disabled (flag --update-status=false was specified)")
@ -304,6 +312,15 @@ func newIngressController(config *Configuration) *GenericController {
ic.annotations = newAnnotationExtractor(ic)
ic.cfg.Backend.SetListers(ingress.StoreLister{
Ingress: ic.ingLister,
Service: ic.svcLister,
Node: ic.nodeLister,
Endpoint: ic.endpLister,
Secret: ic.secrLister,
ConfigMap: ic.mapLister,
})
return &ic
}
@ -411,29 +428,30 @@ func (ic *GenericController) sync(key interface{}) error {
return nil
}
func (ic *GenericController) getStreamServices(configmapName string, proto api.Protocol) []*ingress.Location {
func (ic *GenericController) getStreamServices(configmapName string, proto api.Protocol) []ingress.L4Service {
glog.V(3).Infof("obtaining information about stream services of type %v located in configmap %v", proto, configmapName)
if configmapName == "" {
// no configmap configured
return []*ingress.Location{}
return []ingress.L4Service{}
}
ns, name, err := k8s.ParseNameNS(configmapName)
if err != nil {
glog.Errorf("unexpected error reading configmap %v: %v", name, err)
return []*ingress.Location{}
return []ingress.L4Service{}
}
configmap, err := ic.getConfigMap(ns, name)
if err != nil {
glog.Errorf("unexpected error reading configmap %v: %v", name, err)
return []*ingress.Location{}
return []ingress.L4Service{}
}
var svcs []*ingress.Location
var svcs []ingress.L4Service
// k -> port to expose
// v -> <namespace>/<service name>:<port from service to be used>
for k, v := range configmap.Data {
_, err := strconv.Atoi(k)
externalPort, err := strconv.Atoi(k)
if err != nil {
glog.Warningf("%v is not valid as a TCP/UDP port", k)
continue
@ -476,6 +494,7 @@ func (ic *GenericController) getStreamServices(configmapName string, proto api.P
var endps []ingress.Endpoint
targetPort, err := strconv.Atoi(svcPort)
if err != nil {
glog.V(3).Infof("searching service %v/%v endpoints using the name '%v'", svcNs, svcName, svcPort)
for _, sp := range svc.Spec.Ports {
if sp.Name == svcPort {
endps = ic.getEndpoints(svc, sp.TargetPort, proto, &healthcheck.Upstream{})
@ -484,6 +503,7 @@ func (ic *GenericController) getStreamServices(configmapName string, proto api.P
}
} else {
// we need to use the TargetPort (where the endpoints are running)
glog.V(3).Infof("searching service %v/%v endpoints using the target port '%v'", svcNs, svcName, targetPort)
for _, sp := range svc.Spec.Ports {
if sp.Port == int32(targetPort) {
endps = ic.getEndpoints(svc, sp.TargetPort, proto, &healthcheck.Upstream{})
@ -492,18 +512,22 @@ func (ic *GenericController) getStreamServices(configmapName string, proto api.P
}
}
sort.Sort(ingress.EndpointByAddrPort(endps))
// tcp upstreams cannot contain empty upstreams and there is no
// default backend equivalent for TCP
// stream services cannot contain empty upstreams and there is no
// default backend equivalent
if len(endps) == 0 {
glog.Warningf("service %v/%v does not have any active endpoints", svcNs, svcName)
glog.Warningf("service %v/%v does not have any active endpoints for port %v and protocol %v", svcNs, svcName, svcPort, proto)
continue
}
svcs = append(svcs, &ingress.Location{
Path: k,
Backend: fmt.Sprintf("%v-%v-%v", svcNs, svcName, svcPort),
svcs = append(svcs, ingress.L4Service{
Port: externalPort,
Backend: ingress.L4Backend{
Name: svcName,
Namespace: svcNs,
Port: intstr.FromString(svcPort),
Protocol: proto,
},
Endpoints: endps,
})
}
@ -564,6 +588,10 @@ func (ic *GenericController) getBackendServers() ([]*ingress.Backend, []*ingress
for _, ingIf := range ings {
ing := ingIf.(*extensions.Ingress)
if !IsValidClass(ing, ic.cfg.IngressClass) {
continue
}
anns := ic.annotations.Extract(ing)
for _, rule := range ing.Spec.Rules {
@ -576,30 +604,9 @@ func (ic *GenericController) getBackendServers() ([]*ingress.Backend, []*ingress
server = servers[defServerName]
}
// use default upstream
defBackend := upstreams[defUpstreamName]
// we need to check if the spec contains the default backend
if ing.Spec.Backend != nil {
glog.V(3).Infof("ingress rule %v/%v defines a default Backend %v/%v",
ing.Namespace,
ing.Name,
ing.Spec.Backend.ServiceName,
ing.Spec.Backend.ServicePort.String())
name := fmt.Sprintf("%v-%v-%v",
ing.GetNamespace(),
ing.Spec.Backend.ServiceName,
ing.Spec.Backend.ServicePort.String())
if defUps, ok := upstreams[name]; ok {
defBackend = defUps
}
}
if rule.HTTP == nil &&
host != defServerName {
glog.V(3).Infof("ingress rule %v/%v does not contains HTTP rules. using default backend", ing.Namespace, ing.Name)
server.Locations[0].Backend = defBackend.Name
continue
}
@ -659,7 +666,6 @@ func (ic *GenericController) getBackendServers() ([]*ingress.Backend, []*ingress
glog.V(3).Infof("upstream %v does not have any active endpoints. Using default backend", value.Name)
value.Endpoints = append(value.Endpoints, newDefaultServer())
}
sort.Sort(ingress.EndpointByAddrPort(value.Endpoints))
aUpstreams = append(aUpstreams, value)
}
sort.Sort(ingress.BackendByNameServers(aUpstreams))
@ -676,16 +682,23 @@ func (ic *GenericController) getBackendServers() ([]*ingress.Backend, []*ingress
// GetAuthCertificate ...
func (ic GenericController) GetAuthCertificate(secretName string) (*resolver.AuthSSLCert, error) {
key, err := ic.GetSecret(secretName)
if err != nil {
return &resolver.AuthSSLCert{}, fmt.Errorf("unexpected error: %v", err)
}
if key != nil {
ic.secretQueue.Enqueue(key)
}
bc, exists := ic.sslCertTracker.Get(secretName)
if !exists {
return &resolver.AuthSSLCert{}, fmt.Errorf("secret %v does not exists", secretName)
}
cert := bc.(*ingress.SSLCert)
return &resolver.AuthSSLCert{
Secret: secretName,
CertFileName: cert.PemFileName,
CAFileName: cert.CAFileName,
PemSHA: cert.PemSHA,
Secret: secretName,
CAFileName: cert.CAFileName,
PemSHA: cert.PemSHA,
}, nil
}
@ -698,8 +711,13 @@ func (ic *GenericController) createUpstreams(data []interface{}) map[string]*ing
for _, ingIf := range data {
ing := ingIf.(*extensions.Ingress)
if !IsValidClass(ing, ic.cfg.IngressClass) {
continue
}
secUpstream := ic.annotations.SecureUpstream(ing)
hz := ic.annotations.HealthCheck(ing)
affinity := ic.annotations.SessionAffinity(ing)
var defBackend string
if ing.Spec.Backend != nil {
@ -739,6 +757,14 @@ func (ic *GenericController) createUpstreams(data []interface{}) map[string]*ing
if !upstreams[name].Secure {
upstreams[name].Secure = secUpstream
}
if upstreams[name].SessionAffinity.AffinityType == "" {
upstreams[name].SessionAffinity.AffinityType = affinity.AffinityType
if affinity.AffinityType == "cookie" {
upstreams[name].SessionAffinity.CookieSessionAffinity.Name = affinity.CookieConfig.Name
upstreams[name].SessionAffinity.CookieSessionAffinity.Hash = affinity.CookieConfig.Hash
}
}
svcKey := fmt.Sprintf("%v/%v", ing.GetNamespace(), path.Backend.ServiceName)
endp, err := ic.serviceEndpoints(svcKey, path.Backend.ServicePort.String(), hz)
if err != nil {
@ -782,6 +808,7 @@ func (ic *GenericController) serviceEndpoints(svcKey, backendPort string,
glog.Warningf("service %v does not have any active endpoints", svcKey)
}
sort.Sort(ingress.EndpointByAddrPort(endps))
upstreams = append(upstreams, endps...)
break
}
@ -790,7 +817,12 @@ func (ic *GenericController) serviceEndpoints(svcKey, backendPort string,
return upstreams, nil
}
func (ic *GenericController) createServers(data []interface{}, upstreams map[string]*ingress.Backend) map[string]*ingress.Server {
// createServers initializes a map that contains information about the list of
// FDQN referenced by ingress rules and the common name field in the referenced
// SSL certificates. Each server is configured with location / using a default
// backend specified by the user or the one inside the ingress spec.
func (ic *GenericController) createServers(data []interface{},
upstreams map[string]*ingress.Backend) map[string]*ingress.Server {
servers := make(map[string]*ingress.Server)
bdef := ic.GetDefaultBackend()
@ -800,10 +832,10 @@ func (ic *GenericController) createServers(data []interface{}, upstreams map[str
SendTimeout: bdef.ProxySendTimeout,
ReadTimeout: bdef.ProxyReadTimeout,
BufferSize: bdef.ProxyBufferSize,
CookieDomain: bdef.ProxyCookieDomain,
CookiePath: bdef.ProxyCookiePath,
}
dun := ic.getDefaultUpstream().Name
// This adds the Default Certificate to Default Backend and also for vhosts missing the secret
var defaultPemFileName, defaultPemSHA string
defaultCertificate, err := ic.getPemCertificate(ic.cfg.DefaultSSLCertificate)
@ -823,7 +855,7 @@ func (ic *GenericController) createServers(data []interface{}, upstreams map[str
defaultPemSHA = defaultCertificate.PemSHA
}
// default server
// initialize the default server
servers[defServerName] = &ingress.Server{
Hostname: defServerName,
SSLCertificate: defaultPemFileName,
@ -832,7 +864,7 @@ func (ic *GenericController) createServers(data []interface{}, upstreams map[str
{
Path: rootLocation,
IsDefBackend: true,
Backend: dun,
Backend: ic.getDefaultUpstream().Name,
Proxy: ngxProxy,
},
}}
@ -840,8 +872,20 @@ func (ic *GenericController) createServers(data []interface{}, upstreams map[str
// initialize all the servers
for _, ingIf := range data {
ing := ingIf.(*extensions.Ingress)
if !IsValidClass(ing, ic.cfg.IngressClass) {
continue
}
// check if ssl passthrough is configured
sslpt := ic.annotations.SSLPassthrough(ing)
dun := ic.getDefaultUpstream().Name
if ing.Spec.Backend != nil {
// replace default backend
defUpstream := fmt.Sprintf("%v-%v-%v", ing.GetNamespace(), ing.Spec.Backend.ServiceName, ing.Spec.Backend.ServicePort.String())
if backendUpstream, ok := upstreams[defUpstream]; ok {
dun = backendUpstream.Name
}
}
for _, rule := range ing.Spec.Rules {
host := rule.Host
@ -868,6 +912,9 @@ func (ic *GenericController) createServers(data []interface{}, upstreams map[str
// configure default location and SSL
for _, ingIf := range data {
ing := ingIf.(*extensions.Ingress)
if !IsValidClass(ing, ic.cfg.IngressClass) {
continue
}
for _, rule := range ing.Spec.Rules {
host := rule.Host
@ -897,22 +944,10 @@ func (ic *GenericController) createServers(data []interface{}, upstreams map[str
servers[host].SSLPemChecksum = cert.PemSHA
}
} else {
servers[host].SSLCertificate = defaultPemFileName
servers[host].SSLPemChecksum = defaultPemSHA
}
}
if ing.Spec.Backend != nil {
defUpstream := fmt.Sprintf("%v-%v-%v", ing.GetNamespace(), ing.Spec.Backend.ServiceName, ing.Spec.Backend.ServicePort.String())
if backendUpstream, ok := upstreams[defUpstream]; ok {
if host == "" || host == defServerName {
ic.recorder.Eventf(ing, api.EventTypeWarning, "MAPPING", "error: rules with Spec.Backend are allowed only with hostnames")
continue
}
servers[host].Locations[0].Backend = backendUpstream.Name
}
}
}
}
@ -1016,6 +1051,7 @@ func (ic GenericController) Start() {
go ic.ingController.Run(ic.stopCh)
go ic.endpController.Run(ic.stopCh)
go ic.svcController.Run(ic.stopCh)
go ic.nodeController.Run(ic.stopCh)
go ic.secrController.Run(ic.stopCh)
go ic.mapController.Run(ic.stopCh)

View file

@ -82,8 +82,12 @@ func NewIngressController(backend ingress.Controller) *GenericController {
updateStatus = flags.Bool("update-status", true, `Indicates if the
ingress controller should update the Ingress status IP/hostname. Default is true`)
electionID = flags.String("election-id", "ingress-controller-leader", `Election id to use for status update.`)
)
backend.OverrideFlags(flags)
flags.AddGoFlagSet(flag.CommandLine)
flags.Parse(os.Args)
@ -135,6 +139,7 @@ func NewIngressController(backend ingress.Controller) *GenericController {
config := &Configuration{
UpdateStatus: *updateStatus,
ElectionID: *electionID,
Client: kubeClient,
ResyncPeriod: *resyncPeriod,
DefaultService: *defaultSvc,

View file

@ -26,6 +26,7 @@ import (
"k8s.io/ingress/core/pkg/ingress"
"k8s.io/ingress/core/pkg/ingress/annotations/parser"
"k8s.io/ingress/core/pkg/ingress/errors"
)
// DeniedKeyName name of the key that contains the reason to deny a location
@ -92,7 +93,10 @@ func IsValidClass(ing *extensions.Ingress, class string) bool {
return true
}
cc, _ := parser.GetStringAnnotation(ingressClassKey, ing)
cc, err := parser.GetStringAnnotation(ingressClassKey, ing)
if err != nil && !errors.IsMissingAnnotations(err) {
glog.Warningf("unexpected error reading ingress annotation: %v", err)
}
if cc == "" {
return true
}

View file

@ -19,17 +19,18 @@ package controller
import (
"testing"
"reflect"
"k8s.io/ingress/core/pkg/ingress"
"k8s.io/ingress/core/pkg/ingress/annotations/auth"
"k8s.io/ingress/core/pkg/ingress/annotations/authreq"
"k8s.io/ingress/core/pkg/ingress/annotations/authtls"
"k8s.io/ingress/core/pkg/ingress/annotations/ipwhitelist"
"k8s.io/ingress/core/pkg/ingress/annotations/proxy"
"k8s.io/ingress/core/pkg/ingress/annotations/ratelimit"
"k8s.io/ingress/core/pkg/ingress/annotations/rewrite"
"k8s.io/ingress/core/pkg/ingress/resolver"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/apis/extensions"
"reflect"
)
type fakeError struct{}
@ -54,6 +55,7 @@ func TestIsValidClass(t *testing.T) {
data := map[string]string{}
data[ingressClassKey] = "custom"
ing.SetAnnotations(data)
b = IsValidClass(ing, "custom")
if !b {
t.Errorf("Expected valid class but %v returned", b)
@ -62,6 +64,10 @@ func TestIsValidClass(t *testing.T) {
if b {
t.Errorf("Expected invalid class but %v returned", b)
}
b = IsValidClass(ing, "")
if !b {
t.Errorf("Expected invalid class but %v returned", b)
}
}
func TestIsHostValid(t *testing.T) {
@ -130,7 +136,7 @@ func TestMergeLocationAnnotations(t *testing.T) {
"Redirect": rewrite.Redirect{},
"Whitelist": ipwhitelist.SourceRange{},
"Proxy": proxy.Configuration{},
"CertificateAuth": resolver.AuthSSLCert{},
"CertificateAuth": authtls.AuthSSLConfig{},
"UsePortInRedirects": true,
}

View file

@ -37,6 +37,16 @@ type Backend struct {
// http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_buffer_size)
ProxyBufferSize string `json:"proxy-buffer-size"`
// Sets a text that should be changed in the path attribute of the “Set-Cookie” header fields of
// a proxied server response.
// http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_cookie_path
ProxyCookiePath string `json:"proxy-cookie-path"`
// Sets a text that should be changed in the domain attribute of the “Set-Cookie” header fields
// of a proxied server response.
// http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_cookie_domain
ProxyCookieDomain string `json:"proxy-cookie-domain"`
// Name server/s used to resolve names of upstream servers into IP addresses.
// The file /etc/resolv.conf is used as DNS resolution configuration.
Resolver []net.IP

View file

@ -37,8 +37,6 @@ type Secret interface {
// AuthCertificate resolves a given secret name into an SSL certificate.
// The secret must contain 3 keys named:
// ca.crt: contains the certificate chain used for authentication
// tls.crt: (ignored) contains the tls certificate chain, or any other valid base64 data
// tls.key: (ignored) contains the tls secret key, or any other valid base64 data
type AuthCertificate interface {
GetAuthCertificate(string) (*AuthSSLCert, error)
}
@ -48,10 +46,6 @@ type AuthCertificate interface {
type AuthSSLCert struct {
// Secret contains the name of the secret this was fetched from
Secret string `json:"secret"`
// CertFileName contains the filename the secret's 'tls.crt' was saved to
CertFileName string `json:"certFilename"`
// KeyFileName contains the path the secret's 'tls.key'
KeyFileName string `json:"keyFilename"`
// CAFileName contains the path to the secrets 'ca.crt'
CAFileName string `json:"caFilename"`
// PemSHA contains the SHA1 hash of the 'tls.crt' value

View file

@ -0,0 +1,130 @@
/*
Copyright 2017 The Kubernetes Authors.
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 status
import (
"encoding/json"
"testing"
"time"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/unversioned"
tc "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/fake"
"k8s.io/kubernetes/pkg/client/leaderelection/resourcelock"
)
func TestGetCurrentLeaderLeaderExist(t *testing.T) {
fkER := resourcelock.LeaderElectionRecord{
HolderIdentity: "currentLeader",
LeaseDurationSeconds: 30,
AcquireTime: unversioned.Now(),
RenewTime: unversioned.Now(),
LeaderTransitions: 3,
}
leaderInfo, _ := json.Marshal(fkER)
fkEndpoints := api.Endpoints{
ObjectMeta: api.ObjectMeta{
Name: "ingress-controller-test",
Namespace: api.NamespaceSystem,
Annotations: map[string]string{
resourcelock.LeaderElectionRecordAnnotationKey: string(leaderInfo),
},
},
}
fk := tc.NewSimpleClientset(&api.EndpointsList{Items: []api.Endpoints{fkEndpoints}})
identity, endpoints, err := getCurrentLeader("ingress-controller-test", api.NamespaceSystem, fk)
if err != nil {
t.Fatalf("expected identitiy and endpoints but returned error %s", err)
}
if endpoints == nil {
t.Fatalf("returned nil but expected an endpoints")
}
if identity != "currentLeader" {
t.Fatalf("returned %v but expected %v", identity, "currentLeader")
}
}
func TestGetCurrentLeaderLeaderNotExist(t *testing.T) {
fkEndpoints := api.Endpoints{
ObjectMeta: api.ObjectMeta{
Name: "ingress-controller-test",
Namespace: api.NamespaceSystem,
Annotations: map[string]string{},
},
}
fk := tc.NewSimpleClientset(&api.EndpointsList{Items: []api.Endpoints{fkEndpoints}})
identity, endpoints, err := getCurrentLeader("ingress-controller-test", api.NamespaceSystem, fk)
if err != nil {
t.Fatalf("unexpeted error: %v", err)
}
if endpoints == nil {
t.Fatalf("returned nil but expected an endpoints")
}
if identity != "" {
t.Fatalf("returned %s but expected %s", identity, "")
}
}
func TestGetCurrentLeaderAnnotationError(t *testing.T) {
fkEndpoints := api.Endpoints{
ObjectMeta: api.ObjectMeta{
Name: "ingress-controller-test",
Namespace: api.NamespaceSystem,
Annotations: map[string]string{
resourcelock.LeaderElectionRecordAnnotationKey: "just-test-error-leader-annotation",
},
},
}
fk := tc.NewSimpleClientset(&api.EndpointsList{Items: []api.Endpoints{fkEndpoints}})
_, _, err := getCurrentLeader("ingress-controller-test", api.NamespaceSystem, fk)
if err == nil {
t.Errorf("expected error")
}
}
func TestNewElection(t *testing.T) {
fk := tc.NewSimpleClientset(&api.EndpointsList{Items: []api.Endpoints{
{
ObjectMeta: api.ObjectMeta{
Name: "ingress-controller-test",
Namespace: api.NamespaceSystem,
},
},
{
ObjectMeta: api.ObjectMeta{
Name: "ingress-controller-test-020",
Namespace: api.NamespaceSystem,
},
},
}})
ne, err := NewElection("ingress-controller-test", "startLeader", api.NamespaceSystem, 4*time.Second, func(leader string) {
// do nothing
go t.Logf("execute callback fun, leader is: %s", leader)
}, fk)
if err != nil {
t.Fatalf("unexpected error %v", err)
}
if ne == nil {
t.Fatalf("unexpected nil")
}
}

View file

@ -33,7 +33,7 @@ import (
cache_store "k8s.io/ingress/core/pkg/cache"
"k8s.io/ingress/core/pkg/k8s"
strings "k8s.io/ingress/core/pkg/strings"
"k8s.io/ingress/core/pkg/strings"
"k8s.io/ingress/core/pkg/task"
)
@ -52,6 +52,7 @@ type Config struct {
Client clientset.Interface
PublishService string
IngressLister cache_store.StoreToIngressLister
ElectionID string
}
// statusSync keeps the status IP in each Ingress rule updated executing a periodic check
@ -171,7 +172,7 @@ func NewStatusSyncer(config Config) Sync {
}
st.syncQueue = task.NewCustomTaskQueue(st.sync, st.keyfunc)
le, err := NewElection("ingress-controller-leader",
le, err := NewElection(config.ElectionID,
pod.Name, pod.Namespace, 30*time.Second,
st.callback, config.Client)
if err != nil {
@ -251,7 +252,7 @@ func (s *statusSync) updateStatus(newIPs []api.LoadBalancerIngress) {
return
}
curIPs := ing.Status.LoadBalancer.Ingress
curIPs := currIng.Status.LoadBalancer.Ingress
sort.Sort(loadBalancerIngressByIP(curIPs))
if ingressSliceEqual(newIPs, curIPs) {
glog.V(3).Infof("skipping update of Ingress %v/%v (there is no change)", currIng.Namespace, currIng.Name)

View file

@ -0,0 +1,487 @@
/*
Copyright 2017 The Kubernetes Authors.
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 status
import (
"os"
"sort"
"sync"
"testing"
"time"
cache_store "k8s.io/ingress/core/pkg/cache"
"k8s.io/ingress/core/pkg/k8s"
"k8s.io/ingress/core/pkg/task"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/apis/extensions"
"k8s.io/kubernetes/pkg/client/cache"
testclient "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/fake"
"k8s.io/kubernetes/pkg/util/sets"
)
func buildLoadBalancerIngressByIP() loadBalancerIngressByIP {
return []api.LoadBalancerIngress{
{
IP: "10.0.0.1",
Hostname: "foo1",
},
{
IP: "10.0.0.2",
Hostname: "foo2",
},
{
IP: "10.0.0.3",
Hostname: "",
},
{
IP: "",
Hostname: "foo4",
},
}
}
func buildSimpleClientSet() *testclient.Clientset {
return testclient.NewSimpleClientset(
&api.PodList{Items: []api.Pod{
{
ObjectMeta: api.ObjectMeta{
Name: "foo1",
Namespace: api.NamespaceDefault,
Labels: map[string]string{
"lable_sig": "foo_pod",
},
},
Spec: api.PodSpec{
NodeName: "foo_node_2",
},
},
{
ObjectMeta: api.ObjectMeta{
Name: "foo2",
Namespace: api.NamespaceDefault,
Labels: map[string]string{
"lable_sig": "foo_no",
},
},
},
{
ObjectMeta: api.ObjectMeta{
Name: "foo3",
Namespace: api.NamespaceSystem,
Labels: map[string]string{
"lable_sig": "foo_pod",
},
},
Spec: api.PodSpec{
NodeName: "foo_node_2",
},
},
}},
&api.ServiceList{Items: []api.Service{
{
ObjectMeta: api.ObjectMeta{
Name: "foo",
Namespace: api.NamespaceDefault,
},
Status: api.ServiceStatus{
LoadBalancer: api.LoadBalancerStatus{
Ingress: buildLoadBalancerIngressByIP(),
},
},
},
{
ObjectMeta: api.ObjectMeta{
Name: "foo_non_exist",
Namespace: api.NamespaceDefault,
},
},
}},
&api.NodeList{Items: []api.Node{
{
ObjectMeta: api.ObjectMeta{
Name: "foo_node_1",
},
Status: api.NodeStatus{
Addresses: []api.NodeAddress{
{
Type: api.NodeLegacyHostIP,
Address: "10.0.0.1",
}, {
Type: api.NodeExternalIP,
Address: "10.0.0.2",
},
},
},
},
{
ObjectMeta: api.ObjectMeta{
Name: "foo_node_2",
},
Status: api.NodeStatus{
Addresses: []api.NodeAddress{
{
Type: api.NodeLegacyHostIP,
Address: "11.0.0.1",
},
{
Type: api.NodeExternalIP,
Address: "11.0.0.2",
},
},
},
},
}},
&api.EndpointsList{Items: []api.Endpoints{
{
ObjectMeta: api.ObjectMeta{
Name: "ingress-controller-leader",
Namespace: api.NamespaceDefault,
},
}}},
&extensions.IngressList{Items: buildExtensionsIngresses()},
)
}
func fakeSynFn(interface{}) error {
return nil
}
func buildExtensionsIngresses() []extensions.Ingress {
return []extensions.Ingress{
{
ObjectMeta: api.ObjectMeta{
Name: "foo_ingress_1",
Namespace: api.NamespaceDefault,
},
Status: extensions.IngressStatus{
LoadBalancer: api.LoadBalancerStatus{
Ingress: []api.LoadBalancerIngress{
{
IP: "10.0.0.1",
Hostname: "foo1",
},
},
},
},
},
{
ObjectMeta: api.ObjectMeta{
Name: "foo_ingress_2",
Namespace: api.NamespaceDefault,
},
Status: extensions.IngressStatus{
LoadBalancer: api.LoadBalancerStatus{
Ingress: []api.LoadBalancerIngress{},
},
},
},
}
}
func buildIngressLIstener() cache_store.StoreToIngressLister {
store := cache.NewStore(cache.MetaNamespaceKeyFunc)
ids := sets.NewString("foo_ingress_non_01")
for id := range ids {
store.Add(&extensions.Ingress{
ObjectMeta: api.ObjectMeta{
Name: id,
Namespace: api.NamespaceDefault,
}})
}
store.Add(&extensions.Ingress{
ObjectMeta: api.ObjectMeta{
Name: "foo_ingress_1",
Namespace: api.NamespaceDefault,
},
Status: extensions.IngressStatus{
LoadBalancer: api.LoadBalancerStatus{
Ingress: buildLoadBalancerIngressByIP(),
},
},
})
return cache_store.StoreToIngressLister{Store: store}
}
func buildStatusSync() statusSync {
return statusSync{
pod: &k8s.PodInfo{
Name: "foo_base_pod",
Namespace: api.NamespaceDefault,
Labels: map[string]string{
"lable_sig": "foo_pod",
},
},
runLock: &sync.Mutex{},
syncQueue: task.NewTaskQueue(fakeSynFn),
Config: Config{
Client: buildSimpleClientSet(),
PublishService: api.NamespaceDefault + "/" + "foo",
IngressLister: buildIngressLIstener(),
},
}
}
func TestStatusActions(t *testing.T) {
// make sure election can be created
os.Setenv("POD_NAME", "foo1")
os.Setenv("POD_NAMESPACE", api.NamespaceDefault)
c := Config{
Client: buildSimpleClientSet(),
PublishService: "",
IngressLister: buildIngressLIstener(),
}
// create object
fkSync := NewStatusSyncer(c)
if fkSync == nil {
t.Fatalf("expected a valid Sync")
}
fk := fkSync.(statusSync)
ns := make(chan struct{})
// start it and wait for the election and syn actions
go fk.Run(ns)
// wait for the election
time.Sleep(100 * time.Millisecond)
// execute sync
fk.sync("just-test")
// PublishService is empty, so the running address is: ["11.0.0.2"]
// after updated, the ingress's ip should only be "11.0.0.2"
newIPs := []api.LoadBalancerIngress{{
IP: "11.0.0.2",
}}
fooIngress1, err1 := fk.Client.Extensions().Ingresses(api.NamespaceDefault).Get("foo_ingress_1")
if err1 != nil {
t.Fatalf("unexpected error")
}
fooIngress1CurIPs := fooIngress1.Status.LoadBalancer.Ingress
if !ingressSliceEqual(fooIngress1CurIPs, newIPs) {
t.Fatalf("returned %v but expected %v", fooIngress1CurIPs, newIPs)
}
// execute shutdown
fk.Shutdown()
// ingress should be empty
newIPs2 := []api.LoadBalancerIngress{}
fooIngress2, err2 := fk.Client.Extensions().Ingresses(api.NamespaceDefault).Get("foo_ingress_1")
if err2 != nil {
t.Fatalf("unexpected error")
}
fooIngress2CurIPs := fooIngress2.Status.LoadBalancer.Ingress
if !ingressSliceEqual(fooIngress2CurIPs, newIPs2) {
t.Fatalf("returned %v but expected %v", fooIngress2CurIPs, newIPs2)
}
// end test
ns <- struct{}{}
}
func TestCallback(t *testing.T) {
fk := buildStatusSync()
// do nothing
fk.callback("foo_base_pod")
}
func TestKeyfunc(t *testing.T) {
fk := buildStatusSync()
i := "foo_base_pod"
r, err := fk.keyfunc(i)
if err != nil {
t.Fatalf("unexpected error")
}
if r != i {
t.Errorf("returned %v but expected %v", r, i)
}
}
func TestRunningAddresessWithPublishService(t *testing.T) {
fk := buildStatusSync()
r, _ := fk.runningAddresess()
if r == nil {
t.Fatalf("returned nil but expected valid []string")
}
rl := len(r)
if len(r) != 4 {
t.Errorf("returned %v but expected %v", rl, 4)
}
}
func TestRunningAddresessWithPods(t *testing.T) {
fk := buildStatusSync()
fk.PublishService = ""
r, _ := fk.runningAddresess()
if r == nil {
t.Fatalf("returned nil but expected valid []string")
}
rl := len(r)
if len(r) != 1 {
t.Fatalf("returned %v but expected %v", rl, 1)
}
rv := r[0]
if rv != "11.0.0.2" {
t.Errorf("returned %v but expected %v", rv, "11.0.0.2")
}
}
func TestUpdateStatus(t *testing.T) {
fk := buildStatusSync()
newIPs := buildLoadBalancerIngressByIP()
sort.Sort(loadBalancerIngressByIP(newIPs))
fk.updateStatus(newIPs)
fooIngress1, err1 := fk.Client.Extensions().Ingresses(api.NamespaceDefault).Get("foo_ingress_1")
if err1 != nil {
t.Fatalf("unexpected error")
}
fooIngress1CurIPs := fooIngress1.Status.LoadBalancer.Ingress
if !ingressSliceEqual(fooIngress1CurIPs, newIPs) {
t.Fatalf("returned %v but expected %v", fooIngress1CurIPs, newIPs)
}
fooIngress2, err2 := fk.Client.Extensions().Ingresses(api.NamespaceDefault).Get("foo_ingress_2")
if err2 != nil {
t.Fatalf("unexpected error")
}
fooIngress2CurIPs := fooIngress2.Status.LoadBalancer.Ingress
if !ingressSliceEqual(fooIngress2CurIPs, []api.LoadBalancerIngress{}) {
t.Fatalf("returned %v but expected %v", fooIngress2CurIPs, []api.LoadBalancerIngress{})
}
}
func TestSliceToStatus(t *testing.T) {
fkEndpoints := []string{
"10.0.0.1",
"2001:db8::68",
"opensource-k8s-ingress",
}
r := sliceToStatus(fkEndpoints)
if r == nil {
t.Fatalf("returned nil but expected a valid []api.LoadBalancerIngress")
}
rl := len(r)
if rl != 3 {
t.Fatalf("returned %v but expected %v", rl, 3)
}
re1 := r[0]
if re1.Hostname != "opensource-k8s-ingress" {
t.Fatalf("returned %v but expected %v", re1, api.LoadBalancerIngress{Hostname: "opensource-k8s-ingress"})
}
re2 := r[1]
if re2.IP != "10.0.0.1" {
t.Fatalf("returned %v but expected %v", re2, api.LoadBalancerIngress{IP: "10.0.0.1"})
}
re3 := r[2]
if re3.IP != "2001:db8::68" {
t.Fatalf("returned %v but expected %v", re3, api.LoadBalancerIngress{IP: "2001:db8::68"})
}
}
func TestIngressSliceEqual(t *testing.T) {
fk1 := buildLoadBalancerIngressByIP()
fk2 := append(buildLoadBalancerIngressByIP(), api.LoadBalancerIngress{
IP: "10.0.0.5",
Hostname: "foo5",
})
fk3 := buildLoadBalancerIngressByIP()
fk3[0].Hostname = "foo_no_01"
fk4 := buildLoadBalancerIngressByIP()
fk4[2].IP = "11.0.0.3"
fooTests := []struct {
lhs []api.LoadBalancerIngress
rhs []api.LoadBalancerIngress
er bool
}{
{fk1, fk1, true},
{fk2, fk1, false},
{fk3, fk1, false},
{fk4, fk1, false},
{fk1, nil, false},
{nil, nil, true},
{[]api.LoadBalancerIngress{}, []api.LoadBalancerIngress{}, true},
}
for _, fooTest := range fooTests {
r := ingressSliceEqual(fooTest.lhs, fooTest.rhs)
if r != fooTest.er {
t.Errorf("returned %v but expected %v", r, fooTest.er)
}
}
}
func TestLoadBalancerIngressByIPLen(t *testing.T) {
fooTests := []struct {
ips loadBalancerIngressByIP
el int
}{
{[]api.LoadBalancerIngress{}, 0},
{buildLoadBalancerIngressByIP(), 4},
{nil, 0},
}
for _, fooTest := range fooTests {
r := fooTest.ips.Len()
if r != fooTest.el {
t.Errorf("returned %v but expected %v ", r, fooTest.el)
}
}
}
func TestLoadBalancerIngressByIPSwap(t *testing.T) {
fooTests := []struct {
ips loadBalancerIngressByIP
i int
j int
}{
{buildLoadBalancerIngressByIP(), 0, 1},
{buildLoadBalancerIngressByIP(), 2, 1},
}
for _, fooTest := range fooTests {
fooi := fooTest.ips[fooTest.i]
fooj := fooTest.ips[fooTest.j]
fooTest.ips.Swap(fooTest.i, fooTest.j)
if fooi.IP != fooTest.ips[fooTest.j].IP ||
fooj.IP != fooTest.ips[fooTest.i].IP {
t.Errorf("failed to swap for loadBalancerIngressByIP")
}
}
}
func TestLoadBalancerIngressByIPLess(t *testing.T) {
fooTests := []struct {
ips loadBalancerIngressByIP
i int
j int
er bool
}{
{buildLoadBalancerIngressByIP(), 0, 1, true},
{buildLoadBalancerIngressByIP(), 2, 1, false},
}
for _, fooTest := range fooTests {
r := fooTest.ips.Less(fooTest.i, fooTest.j)
if r != fooTest.er {
t.Errorf("returned %v but expected %v ", r, fooTest.er)
}
}
}

View file

@ -17,17 +17,22 @@ limitations under the License.
package ingress
import (
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/healthz"
"github.com/spf13/pflag"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/client/cache"
"k8s.io/kubernetes/pkg/healthz"
"k8s.io/kubernetes/pkg/util/intstr"
cache_store "k8s.io/ingress/core/pkg/cache"
"k8s.io/ingress/core/pkg/ingress/annotations/auth"
"k8s.io/ingress/core/pkg/ingress/annotations/authreq"
"k8s.io/ingress/core/pkg/ingress/annotations/authtls"
"k8s.io/ingress/core/pkg/ingress/annotations/ipwhitelist"
"k8s.io/ingress/core/pkg/ingress/annotations/proxy"
"k8s.io/ingress/core/pkg/ingress/annotations/ratelimit"
"k8s.io/ingress/core/pkg/ingress/annotations/rewrite"
"k8s.io/ingress/core/pkg/ingress/defaults"
"k8s.io/ingress/core/pkg/ingress/resolver"
)
var (
@ -81,11 +86,27 @@ type Controller interface {
OnUpdate(Configuration) ([]byte, error)
// ConfigMap content of --configmap
SetConfig(*api.ConfigMap)
// SetListers allows the access of store listers present in the generic controller
// This avoid the use of the kubernetes client.
SetListers(StoreLister)
// BackendDefaults returns the minimum settings required to configure the
// communication to endpoints
BackendDefaults() defaults.Backend
// Info returns information about the ingress controller
Info() *BackendInfo
// OverrideFlags allow the customization of the flags in the backend
OverrideFlags(*pflag.FlagSet)
}
// StoreLister returns the configured stores for ingresses, services,
// endpoints, secrets and configmaps.
type StoreLister struct {
Ingress cache_store.StoreToIngressLister
Service cache.StoreToServiceLister
Node cache.StoreToNodeLister
Endpoint cache.StoreToEndpointsLister
Secret cache_store.StoreToSecretsLister
ConfigMap cache_store.StoreToConfigmapLister
}
// BackendInfo returns information about the backend.
@ -112,10 +133,10 @@ type Configuration struct {
Servers []*Server `json:"servers"`
// TCPEndpoints contain endpoints for tcp streams handled by this backend
// +optional
TCPEndpoints []*Location `json:"tcpEndpoints,omitempty"`
TCPEndpoints []L4Service `json:"tcpEndpoints,omitempty"`
// UDPEndpoints contain endpoints for udp streams handled by this backend
// +optional
UDPEndpoints []*Location `json:"udpEndpoints,omitempty"`
UDPEndpoints []L4Service `json:"udpEndpoints,omitempty"`
// PassthroughBackend contains the backends used for SSL passthrough.
// It contains information about the associated Server Name Indication (SNI).
// +optional
@ -134,9 +155,29 @@ type Backend struct {
Secure bool `json:"secure"`
// Endpoints contains the list of endpoints currently running
Endpoints []Endpoint `json:"endpoints"`
// StickySession contains the StickyConfig object with stickness configuration
SessionAffinity SessionAffinityConfig
}
// Endpoint describes a kubernetes endpoint in an backend
// SessionAffinityConfig describes different affinity configurations for new sessions.
// Once a session is mapped to a backend based on some affinity setting, it
// retains that mapping till the backend goes down, or the ingress controller
// restarts. Exactly one of these values will be set on the upstream, since multiple
// affinity values are incompatible. Once set, the backend makes no guarantees
// about honoring updates.
type SessionAffinityConfig struct {
AffinityType string `json:"name"`
CookieSessionAffinity CookieSessionAffinity
}
// CookieSessionAffinity defines the structure used in Affinity configured by Cookies.
type CookieSessionAffinity struct {
Name string `json:"name"`
Hash string `json:"hash"`
}
// Endpoint describes a kubernetes endpoint in a backend
type Endpoint struct {
// Address IP address of the endpoint
Address string `json:"address"`
@ -233,10 +274,13 @@ type Location struct {
// CertificateAuth indicates the access to this location requires
// external authentication
// +optional
CertificateAuth resolver.AuthSSLCert `json:"certificateAuth,omitempty"`
CertificateAuth authtls.AuthSSLConfig `json:"certificateAuth,omitempty"`
// UsePortInRedirects indicates if redirects must specify the port
// +optional
UsePortInRedirects bool `json:"use-port-in-redirects"`
// ConfigurationSnippet contains additional configuration for the backend
// to be considered in the configuration of the location
ConfigurationSnippet string `json:"configuration-snippet"`
}
// SSLPassthroughBackend describes a SSL upstream server configured
@ -249,3 +293,21 @@ type SSLPassthroughBackend struct {
// Hostname returns the FQDN of the server
Hostname string `json:"hostname"`
}
// L4Service describes a L4 Ingress service.
type L4Service struct {
// Port external port to expose
Port int `json:"port"`
// Backend of the service
Backend L4Backend `json:"backend"`
// Endpoints active endpoints of the service
Endpoints []Endpoint `json:"endpoins"`
}
// L4Backend describes the kubernetes service behind L4 Ingress service
type L4Backend struct {
Port intstr.IntOrString `json:"port"`
Name string `json:"name"`
Namespace string `json:"namespace"`
Protocol api.Protocol `json:"protocol"`
}

View file

@ -37,6 +37,8 @@ func AddOrUpdateCertAndKey(name string, cert, key, ca []byte) (*ingress.SSLCert,
pemFileName := fmt.Sprintf("%v/%v", ingress.DefaultSSLDirectory, pemName)
tempPemFile, err := ioutil.TempFile(ingress.DefaultSSLDirectory, pemName)
glog.V(3).Infof("Creating temp file %v for Keypair: %v", tempPemFile.Name(), pemName)
if err != nil {
return nil, fmt.Errorf("could not create temp pem file %v: %v", pemFileName, err)
}
@ -64,12 +66,12 @@ func AddOrUpdateCertAndKey(name string, cert, key, ca []byte) (*ingress.SSLCert,
return nil, err
}
pembBock, _ := pem.Decode(pemCerts)
if pembBock == nil {
pemBlock, _ := pem.Decode(pemCerts)
if pemBlock == nil {
return nil, fmt.Errorf("No valid PEM formatted block found")
}
pemCert, err := x509.ParseCertificate(pembBock.Bytes)
pemCert, err := x509.ParseCertificate(pemBlock.Bytes)
if err != nil {
return nil, err
}
@ -97,21 +99,21 @@ func AddOrUpdateCertAndKey(name string, cert, key, ca []byte) (*ingress.SSLCert,
return nil, errors.New(oe)
}
caName := fmt.Sprintf("ca-%v.pem", name)
caFileName := fmt.Sprintf("%v/%v", ingress.DefaultSSLDirectory, caName)
f, err := os.Create(caFileName)
caFile, err := os.OpenFile(pemFileName, os.O_RDWR|os.O_APPEND, 0600)
if err != nil {
return nil, fmt.Errorf("could not create ca pem file %v: %v", caFileName, err)
return nil, fmt.Errorf("Could not open file %v for writing additional CA chains: %v", pemFileName, err)
}
defer f.Close()
_, err = f.Write(ca)
defer caFile.Close()
_, err = caFile.Write([]byte("\n"))
if err != nil {
return nil, fmt.Errorf("could not create ca pem file %v: %v", caFileName, err)
return nil, fmt.Errorf("could not append CA to cert file %v: %v", pemFileName, err)
}
f.Write([]byte("\n"))
caFile.Write(ca)
caFile.Write([]byte("\n"))
return &ingress.SSLCert{
CAFileName: caFileName,
CAFileName: pemFileName,
PemFileName: pemFileName,
PemSHA: pemSHA1(pemFileName),
CN: cn,
@ -125,6 +127,36 @@ func AddOrUpdateCertAndKey(name string, cert, key, ca []byte) (*ingress.SSLCert,
}, nil
}
// AddCertAuth creates a .pem file with the specified CAs to be used in Cert Authentication
// If it's already exists, it's clobbered.
func AddCertAuth(name string, ca []byte) (*ingress.SSLCert, error) {
caName := fmt.Sprintf("ca-%v.pem", name)
caFileName := fmt.Sprintf("%v/%v", ingress.DefaultSSLDirectory, caName)
pemCABlock, _ := pem.Decode(ca)
if pemCABlock == nil {
return nil, fmt.Errorf("No valid PEM formatted block found")
}
_, err := x509.ParseCertificate(pemCABlock.Bytes)
if err != nil {
return nil, err
}
err = ioutil.WriteFile(caFileName, ca, 0644)
if err != nil {
return nil, fmt.Errorf("could not write CA file %v: %v", caFileName, err)
}
glog.V(3).Infof("Created CA Certificate for authentication: %v", caFileName)
return &ingress.SSLCert{
CAFileName: caFileName,
PemFileName: caFileName,
PemSHA: pemSHA1(caFileName),
}, nil
}
// 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