Update dependencies
This commit is contained in:
parent
bf5616c65b
commit
d6d374b28d
13962 changed files with 48226 additions and 3618880 deletions
19
pkg/file/file.go
Normal file
19
pkg/file/file.go
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
package file
|
||||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"encoding/hex"
|
||||
"io/ioutil"
|
||||
)
|
||||
|
||||
// SHA1 returns the SHA1 of a file.
|
||||
func SHA1(filename string) string {
|
||||
hasher := sha1.New()
|
||||
s, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
hasher.Write(s)
|
||||
return hex.EncodeToString(hasher.Sum(nil))
|
||||
}
|
||||
41
pkg/ingress/annotations/alias/main.go
Normal file
41
pkg/ingress/annotations/alias/main.go
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
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 alias
|
||||
|
||||
import (
|
||||
extensions "k8s.io/api/extensions/v1beta1"
|
||||
|
||||
"k8s.io/ingress-nginx/pkg/ingress/annotations/parser"
|
||||
)
|
||||
|
||||
const (
|
||||
annotation = "ingress.kubernetes.io/server-alias"
|
||||
)
|
||||
|
||||
type alias struct {
|
||||
}
|
||||
|
||||
// NewParser creates a new Alias annotation parser
|
||||
func NewParser() parser.IngressAnnotation {
|
||||
return alias{}
|
||||
}
|
||||
|
||||
// Parse parses the annotations contained in the ingress rule
|
||||
// used to add an alias to the provided hosts
|
||||
func (a alias) Parse(ing *extensions.Ingress) (interface{}, error) {
|
||||
return parser.GetStringAnnotation(annotation, ing)
|
||||
}
|
||||
60
pkg/ingress/annotations/alias/main_test.go
Normal file
60
pkg/ingress/annotations/alias/main_test.go
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
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 alias
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
api "k8s.io/api/core/v1"
|
||||
extensions "k8s.io/api/extensions/v1beta1"
|
||||
meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
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: "www.example.com"}, "www.example.com"},
|
||||
{map[string]string{annotation: "*.example.com www.example.*"}, "*.example.com www.example.*"},
|
||||
{map[string]string{annotation: `~^www\d+\.example\.com$`}, `~^www\d+\.example\.com$`},
|
||||
{map[string]string{annotation: ""}, ""},
|
||||
{map[string]string{}, ""},
|
||||
{nil, ""},
|
||||
}
|
||||
|
||||
ing := &extensions.Ingress{
|
||||
ObjectMeta: meta_v1.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
171
pkg/ingress/annotations/auth/main.go
Normal file
171
pkg/ingress/annotations/auth/main.go
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
/*
|
||||
Copyright 2015 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 auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"regexp"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
api "k8s.io/api/core/v1"
|
||||
extensions "k8s.io/api/extensions/v1beta1"
|
||||
|
||||
"k8s.io/ingress-nginx/pkg/file"
|
||||
"k8s.io/ingress-nginx/pkg/ingress/annotations/parser"
|
||||
ing_errors "k8s.io/ingress-nginx/pkg/ingress/errors"
|
||||
"k8s.io/ingress-nginx/pkg/ingress/resolver"
|
||||
)
|
||||
|
||||
const (
|
||||
authType = "ingress.kubernetes.io/auth-type"
|
||||
authSecret = "ingress.kubernetes.io/auth-secret"
|
||||
authRealm = "ingress.kubernetes.io/auth-realm"
|
||||
)
|
||||
|
||||
var (
|
||||
authTypeRegex = regexp.MustCompile(`basic|digest`)
|
||||
// AuthDirectory default directory used to store files
|
||||
// to authenticate request
|
||||
AuthDirectory = "/etc/ingress-controller/auth"
|
||||
)
|
||||
|
||||
// BasicDigest returns authentication configuration for an Ingress rule
|
||||
type BasicDigest struct {
|
||||
Type string `json:"type"`
|
||||
Realm string `json:"realm"`
|
||||
File string `json:"file"`
|
||||
Secured bool `json:"secured"`
|
||||
FileSHA string `json:"fileSha"`
|
||||
}
|
||||
|
||||
// Equal tests for equality between two BasicDigest types
|
||||
func (bd1 *BasicDigest) Equal(bd2 *BasicDigest) bool {
|
||||
if bd1 == bd2 {
|
||||
return true
|
||||
}
|
||||
if bd1 == nil || bd2 == nil {
|
||||
return false
|
||||
}
|
||||
if bd1.Type != bd2.Type {
|
||||
return false
|
||||
}
|
||||
if bd1.Realm != bd2.Realm {
|
||||
return false
|
||||
}
|
||||
if bd1.File != bd2.File {
|
||||
return false
|
||||
}
|
||||
if bd1.Secured != bd2.Secured {
|
||||
return false
|
||||
}
|
||||
if bd1.FileSHA != bd2.FileSHA {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
type auth struct {
|
||||
secretResolver resolver.Secret
|
||||
authDirectory string
|
||||
}
|
||||
|
||||
// NewParser creates a new authentication annotation parser
|
||||
func NewParser(authDirectory string, sr resolver.Secret) parser.IngressAnnotation {
|
||||
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}
|
||||
}
|
||||
|
||||
// Parse parses the annotations contained in the ingress
|
||||
// rule used to add authentication in the paths defined in the rule
|
||||
// and generated an htpasswd compatible file to be used as source
|
||||
// during the authentication process
|
||||
func (a auth) Parse(ing *extensions.Ingress) (interface{}, error) {
|
||||
at, err := parser.GetStringAnnotation(authType, ing)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !authTypeRegex.MatchString(at) {
|
||||
return nil, ing_errors.NewLocationDenied("invalid authentication type")
|
||||
}
|
||||
|
||||
s, err := parser.GetStringAnnotation(authSecret, ing)
|
||||
if err != nil {
|
||||
return nil, ing_errors.LocationDenied{
|
||||
Reason: errors.Wrap(err, "error reading secret name from annotation"),
|
||||
}
|
||||
}
|
||||
|
||||
name := fmt.Sprintf("%v/%v", ing.Namespace, s)
|
||||
secret, err := a.secretResolver.GetSecret(name)
|
||||
if err != nil {
|
||||
return nil, ing_errors.LocationDenied{
|
||||
Reason: errors.Wrapf(err, "unexpected error reading secret %v", name),
|
||||
}
|
||||
}
|
||||
|
||||
realm, _ := parser.GetStringAnnotation(authRealm, ing)
|
||||
|
||||
passFile := fmt.Sprintf("%v/%v-%v.passwd", a.authDirectory, ing.GetNamespace(), ing.GetName())
|
||||
err = dumpSecret(passFile, secret)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &BasicDigest{
|
||||
Type: at,
|
||||
Realm: realm,
|
||||
File: passFile,
|
||||
Secured: true,
|
||||
FileSHA: file.SHA1(passFile),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// dumpSecret dumps the content of a secret into a file
|
||||
// in the expected format for the specified authorization
|
||||
func dumpSecret(filename string, secret *api.Secret) error {
|
||||
val, ok := secret.Data["auth"]
|
||||
if !ok {
|
||||
return ing_errors.LocationDenied{
|
||||
Reason: errors.Errorf("the secret %v does not contain a key with value auth", secret.Name),
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: check permissions required
|
||||
err := ioutil.WriteFile(filename, val, 0777)
|
||||
if err != nil {
|
||||
return ing_errors.LocationDenied{
|
||||
Reason: errors.Wrap(err, "unexpected error creating password file"),
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
177
pkg/ingress/annotations/auth/main_test.go
Normal file
177
pkg/ingress/annotations/auth/main_test.go
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
/*
|
||||
Copyright 2015 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 auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
api "k8s.io/api/core/v1"
|
||||
extensions "k8s.io/api/extensions/v1beta1"
|
||||
meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
)
|
||||
|
||||
func buildIngress() *extensions.Ingress {
|
||||
defaultBackend := extensions.IngressBackend{
|
||||
ServiceName: "default-backend",
|
||||
ServicePort: intstr.FromInt(80),
|
||||
}
|
||||
|
||||
return &extensions.Ingress{
|
||||
ObjectMeta: meta_v1.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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type mockSecret struct {
|
||||
}
|
||||
|
||||
func (m mockSecret) GetSecret(name string) (*api.Secret, error) {
|
||||
if name != "default/demo-secret" {
|
||||
return nil, errors.Errorf("there is no secret with name %v", name)
|
||||
}
|
||||
|
||||
return &api.Secret{
|
||||
ObjectMeta: meta_v1.ObjectMeta{
|
||||
Namespace: api.NamespaceDefault,
|
||||
Name: "demo-secret",
|
||||
},
|
||||
Data: map[string][]byte{"auth": []byte("foo:$apr1$OFG3Xybp$ckL0FHDAkoXYIlH9.cysT0")},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func TestIngressWithoutAuth(t *testing.T) {
|
||||
ing := buildIngress()
|
||||
_, dir, _ := dummySecretContent(t)
|
||||
defer os.RemoveAll(dir)
|
||||
_, err := NewParser(dir, mockSecret{}).Parse(ing)
|
||||
if err == nil {
|
||||
t.Error("Expected error with ingress without annotations")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIngressAuth(t *testing.T) {
|
||||
ing := buildIngress()
|
||||
|
||||
data := map[string]string{}
|
||||
data[authType] = "basic"
|
||||
data[authSecret] = "demo-secret"
|
||||
data[authRealm] = "-realm-"
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
_, dir, _ := dummySecretContent(t)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
i, err := NewParser(dir, mockSecret{}).Parse(ing)
|
||||
if err != nil {
|
||||
t.Errorf("Uxpected error with ingress: %v", err)
|
||||
}
|
||||
auth, ok := i.(*BasicDigest)
|
||||
if !ok {
|
||||
t.Errorf("expected a BasicDigest type")
|
||||
}
|
||||
if auth.Type != "basic" {
|
||||
t.Errorf("Expected basic as auth type but returned %s", auth.Type)
|
||||
}
|
||||
if auth.Realm != "-realm-" {
|
||||
t.Errorf("Expected -realm- as realm but returned %s", auth.Realm)
|
||||
}
|
||||
if !auth.Secured {
|
||||
t.Errorf("Expected true as secured but returned %v", auth.Secured)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIngressAuthWithoutSecret(t *testing.T) {
|
||||
ing := buildIngress()
|
||||
|
||||
data := map[string]string{}
|
||||
data[authType] = "basic"
|
||||
data[authSecret] = "invalid-secret"
|
||||
data[authRealm] = "-realm-"
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
_, dir, _ := dummySecretContent(t)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
_, err := NewParser(dir, mockSecret{}).Parse(ing)
|
||||
if err == nil {
|
||||
t.Errorf("expected an error with invalid secret name")
|
||||
}
|
||||
}
|
||||
|
||||
func dummySecretContent(t *testing.T) (string, string, *api.Secret) {
|
||||
dir, err := ioutil.TempDir("", fmt.Sprintf("%v", time.Now().Unix()))
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
tmpfile, err := ioutil.TempFile("", "example-")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
defer tmpfile.Close()
|
||||
s, _ := mockSecret{}.GetSecret("default/demo-secret")
|
||||
return tmpfile.Name(), dir, s
|
||||
}
|
||||
|
||||
func TestDumpSecret(t *testing.T) {
|
||||
tmpfile, dir, s := dummySecretContent(t)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
sd := s.Data
|
||||
s.Data = nil
|
||||
|
||||
err := dumpSecret(tmpfile, s)
|
||||
if err == nil {
|
||||
t.Errorf("Expected error with secret without auth")
|
||||
}
|
||||
|
||||
s.Data = sd
|
||||
err = dumpSecret(tmpfile, s)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error creating htpasswd file %v: %v", tmpfile, err)
|
||||
}
|
||||
}
|
||||
183
pkg/ingress/annotations/authreq/main.go
Normal file
183
pkg/ingress/annotations/authreq/main.go
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
/*
|
||||
Copyright 2015 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 authreq
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
extensions "k8s.io/api/extensions/v1beta1"
|
||||
|
||||
"k8s.io/ingress-nginx/pkg/ingress/annotations/parser"
|
||||
ing_errors "k8s.io/ingress-nginx/pkg/ingress/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
// external URL that provides the authentication
|
||||
authURL = "ingress.kubernetes.io/auth-url"
|
||||
authSigninURL = "ingress.kubernetes.io/auth-signin"
|
||||
authMethod = "ingress.kubernetes.io/auth-method"
|
||||
authBody = "ingress.kubernetes.io/auth-send-body"
|
||||
authHeaders = "ingress.kubernetes.io/auth-response-headers"
|
||||
)
|
||||
|
||||
// External returns external authentication configuration for an Ingress rule
|
||||
type External struct {
|
||||
URL string `json:"url"`
|
||||
// Host contains the hostname defined in the URL
|
||||
Host string `json:"host"`
|
||||
SigninURL string `json:"signinUrl"`
|
||||
Method string `json:"method"`
|
||||
SendBody bool `json:"sendBody"`
|
||||
ResponseHeaders []string `json:"responseHeaders,omitEmpty"`
|
||||
}
|
||||
|
||||
// Equal tests for equality between two External types
|
||||
func (e1 *External) Equal(e2 *External) bool {
|
||||
if e1 == e2 {
|
||||
return true
|
||||
}
|
||||
if e1 == nil || e2 == nil {
|
||||
return false
|
||||
}
|
||||
if e1.URL != e2.URL {
|
||||
return false
|
||||
}
|
||||
if e1.Host != e2.Host {
|
||||
return false
|
||||
}
|
||||
if e1.SigninURL != e2.SigninURL {
|
||||
return false
|
||||
}
|
||||
if e1.Method != e2.Method {
|
||||
return false
|
||||
}
|
||||
if e1.SendBody != e2.SendBody {
|
||||
return false
|
||||
}
|
||||
if e1.Method != e2.Method {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, ep1 := range e1.ResponseHeaders {
|
||||
found := false
|
||||
for _, ep2 := range e2.ResponseHeaders {
|
||||
if ep1 == ep2 {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
var (
|
||||
methods = []string{"GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "CONNECT", "OPTIONS", "TRACE"}
|
||||
headerRegexp = regexp.MustCompile(`^[a-zA-Z\d\-_]+$`)
|
||||
)
|
||||
|
||||
func validMethod(method string) bool {
|
||||
if len(method) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, m := range methods {
|
||||
if method == m {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func validHeader(header string) bool {
|
||||
return headerRegexp.Match([]byte(header))
|
||||
}
|
||||
|
||||
type authReq struct {
|
||||
}
|
||||
|
||||
// NewParser creates a new authentication request annotation parser
|
||||
func NewParser() parser.IngressAnnotation {
|
||||
return authReq{}
|
||||
}
|
||||
|
||||
// ParseAnnotations parses the annotations contained in the ingress
|
||||
// rule used to use an external URL as source for authentication
|
||||
func (a authReq) Parse(ing *extensions.Ingress) (interface{}, error) {
|
||||
str, err := parser.GetStringAnnotation(authURL, ing)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if str == "" {
|
||||
return nil, ing_errors.NewLocationDenied("an empty string is not a valid URL")
|
||||
}
|
||||
|
||||
signin, _ := parser.GetStringAnnotation(authSigninURL, ing)
|
||||
|
||||
ur, err := url.Parse(str)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ur.Scheme == "" {
|
||||
return nil, ing_errors.NewLocationDenied("url scheme is empty")
|
||||
}
|
||||
if ur.Host == "" {
|
||||
return nil, ing_errors.NewLocationDenied("url host is empty")
|
||||
}
|
||||
|
||||
if strings.Contains(ur.Host, "..") {
|
||||
return nil, ing_errors.NewLocationDenied("invalid url host")
|
||||
}
|
||||
|
||||
m, _ := parser.GetStringAnnotation(authMethod, ing)
|
||||
if len(m) != 0 && !validMethod(m) {
|
||||
return nil, ing_errors.NewLocationDenied("invalid HTTP method")
|
||||
}
|
||||
|
||||
h := []string{}
|
||||
hstr, _ := parser.GetStringAnnotation(authHeaders, ing)
|
||||
if len(hstr) != 0 {
|
||||
|
||||
harr := strings.Split(hstr, ",")
|
||||
for _, header := range harr {
|
||||
header = strings.TrimSpace(header)
|
||||
if len(header) > 0 {
|
||||
if !validHeader(header) {
|
||||
return nil, ing_errors.NewLocationDenied("invalid headers list")
|
||||
}
|
||||
h = append(h, header)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sb, _ := parser.GetBoolAnnotation(authBody, ing)
|
||||
|
||||
return &External{
|
||||
URL: str,
|
||||
Host: ur.Hostname(),
|
||||
SigninURL: signin,
|
||||
Method: m,
|
||||
SendBody: sb,
|
||||
ResponseHeaders: h,
|
||||
}, nil
|
||||
}
|
||||
167
pkg/ingress/annotations/authreq/main_test.go
Normal file
167
pkg/ingress/annotations/authreq/main_test.go
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
/*
|
||||
Copyright 2015 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 authreq
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
api "k8s.io/api/core/v1"
|
||||
extensions "k8s.io/api/extensions/v1beta1"
|
||||
meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
)
|
||||
|
||||
func buildIngress() *extensions.Ingress {
|
||||
defaultBackend := extensions.IngressBackend{
|
||||
ServiceName: "default-backend",
|
||||
ServicePort: intstr.FromInt(80),
|
||||
}
|
||||
|
||||
return &extensions.Ingress{
|
||||
ObjectMeta: meta_v1.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 TestAnnotations(t *testing.T) {
|
||||
ing := buildIngress()
|
||||
|
||||
data := map[string]string{}
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
tests := []struct {
|
||||
title string
|
||||
url string
|
||||
signinURL string
|
||||
method string
|
||||
sendBody bool
|
||||
expErr bool
|
||||
}{
|
||||
{"empty", "", "", "", false, true},
|
||||
{"no scheme", "bar", "bar", "", false, true},
|
||||
{"invalid host", "http://", "http://", "", false, true},
|
||||
{"invalid host (multiple dots)", "http://foo..bar.com", "http://foo..bar.com", "", false, true},
|
||||
{"valid URL", "http://bar.foo.com/external-auth", "http://bar.foo.com/external-auth", "", false, false},
|
||||
{"valid URL - send body", "http://foo.com/external-auth", "http://foo.com/external-auth", "POST", true, false},
|
||||
{"valid URL - send body", "http://foo.com/external-auth", "http://foo.com/external-auth", "GET", true, false},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
data[authURL] = test.url
|
||||
data[authSigninURL] = test.signinURL
|
||||
data[authBody] = fmt.Sprintf("%v", test.sendBody)
|
||||
data[authMethod] = fmt.Sprintf("%v", test.method)
|
||||
|
||||
i, err := NewParser().Parse(ing)
|
||||
if test.expErr {
|
||||
if err == nil {
|
||||
t.Errorf("%v: expected error but retuned nil", test.title)
|
||||
}
|
||||
continue
|
||||
}
|
||||
u, ok := i.(*External)
|
||||
if !ok {
|
||||
t.Errorf("%v: expected an External type", test.title)
|
||||
}
|
||||
if u.URL != test.url {
|
||||
t.Errorf("%v: expected \"%v\" but \"%v\" was returned", test.title, test.url, u.URL)
|
||||
}
|
||||
if u.SigninURL != test.signinURL {
|
||||
t.Errorf("%v: expected \"%v\" but \"%v\" was returned", test.title, test.signinURL, u.SigninURL)
|
||||
}
|
||||
if u.Method != test.method {
|
||||
t.Errorf("%v: expected \"%v\" but \"%v\" was returned", test.title, test.method, u.Method)
|
||||
}
|
||||
if u.SendBody != test.sendBody {
|
||||
t.Errorf("%v: expected \"%v\" but \"%v\" was returned", test.title, test.sendBody, u.SendBody)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestHeaderAnnotations(t *testing.T) {
|
||||
ing := buildIngress()
|
||||
|
||||
data := map[string]string{}
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
tests := []struct {
|
||||
title string
|
||||
url string
|
||||
headers string
|
||||
parsedHeaders []string
|
||||
expErr bool
|
||||
}{
|
||||
{"single header", "http://goog.url", "h1", []string{"h1"}, false},
|
||||
{"nothing", "http://goog.url", "", []string{}, false},
|
||||
{"spaces", "http://goog.url", " ", []string{}, false},
|
||||
{"two headers", "http://goog.url", "1,2", []string{"1", "2"}, false},
|
||||
{"two headers and empty entries", "http://goog.url", ",1,,2,", []string{"1", "2"}, false},
|
||||
{"header with spaces", "http://goog.url", "1 2", []string{}, true},
|
||||
{"header with other bad symbols", "http://goog.url", "1+2", []string{}, true},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
data[authURL] = test.url
|
||||
data[authHeaders] = test.headers
|
||||
data[authMethod] = "GET"
|
||||
|
||||
i, err := NewParser().Parse(ing)
|
||||
if test.expErr {
|
||||
if err == nil {
|
||||
t.Errorf("%v: expected error but retuned nil", err.Error())
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
t.Log(i)
|
||||
u, ok := i.(*External)
|
||||
if !ok {
|
||||
t.Errorf("%v: expected an External type", test.title)
|
||||
continue
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(u.ResponseHeaders, test.parsedHeaders) {
|
||||
t.Errorf("%v: expected \"%v\" but \"%v\" was returned", test.title, test.headers, u.ResponseHeaders)
|
||||
}
|
||||
}
|
||||
}
|
||||
131
pkg/ingress/annotations/authtls/main.go
Normal file
131
pkg/ingress/annotations/authtls/main.go
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
/*
|
||||
Copyright 2015 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 authtls
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
extensions "k8s.io/api/extensions/v1beta1"
|
||||
|
||||
"k8s.io/ingress-nginx/pkg/ingress/annotations/parser"
|
||||
ing_errors "k8s.io/ingress-nginx/pkg/ingress/errors"
|
||||
"k8s.io/ingress-nginx/pkg/ingress/resolver"
|
||||
"k8s.io/ingress-nginx/pkg/k8s"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
const (
|
||||
// name of the secret
|
||||
annotationAuthTLSSecret = "ingress.kubernetes.io/auth-tls-secret"
|
||||
annotationAuthVerifyClient = "ingress.kubernetes.io/auth-tls-verify-client"
|
||||
annotationAuthTLSDepth = "ingress.kubernetes.io/auth-tls-verify-depth"
|
||||
annotationAuthTLSErrorPage = "ingress.kubernetes.io/auth-tls-error-page"
|
||||
defaultAuthTLSDepth = 1
|
||||
defaultAuthVerifyClient = "on"
|
||||
)
|
||||
|
||||
var (
|
||||
authVerifyClientRegex = regexp.MustCompile(`on|off|optional|optional_no_ca`)
|
||||
)
|
||||
|
||||
// AuthSSLConfig contains the AuthSSLCert used for muthual autentication
|
||||
// and the configured ValidationDepth
|
||||
type AuthSSLConfig struct {
|
||||
resolver.AuthSSLCert
|
||||
VerifyClient string `json:"verify_client"`
|
||||
ValidationDepth int `json:"validationDepth"`
|
||||
ErrorPage string `json:"errorPage"`
|
||||
}
|
||||
|
||||
// Equal tests for equality between two AuthSSLConfig types
|
||||
func (assl1 *AuthSSLConfig) Equal(assl2 *AuthSSLConfig) bool {
|
||||
if assl1 == assl2 {
|
||||
return true
|
||||
}
|
||||
if assl1 == nil || assl2 == nil {
|
||||
return false
|
||||
}
|
||||
if !(&assl1.AuthSSLCert).Equal(&assl2.AuthSSLCert) {
|
||||
return false
|
||||
}
|
||||
if assl1.VerifyClient != assl2.VerifyClient {
|
||||
return false
|
||||
}
|
||||
if assl1.ValidationDepth != assl2.ValidationDepth {
|
||||
return false
|
||||
}
|
||||
if assl1.ErrorPage != assl2.ErrorPage {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// NewParser creates a new TLS authentication annotation parser
|
||||
func NewParser(resolver resolver.AuthCertificate) parser.IngressAnnotation {
|
||||
return authTLS{resolver}
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
tlsauthsecret, err := parser.GetStringAnnotation(annotationAuthTLSSecret, ing)
|
||||
if err != nil {
|
||||
return &AuthSSLConfig{}, err
|
||||
}
|
||||
|
||||
if tlsauthsecret == "" {
|
||||
return &AuthSSLConfig{}, ing_errors.NewLocationDenied("an empty string is not a valid secret name")
|
||||
}
|
||||
|
||||
_, _, err = k8s.ParseNameNS(tlsauthsecret)
|
||||
if err != nil {
|
||||
return &AuthSSLConfig{}, ing_errors.NewLocationDenied(err.Error())
|
||||
}
|
||||
|
||||
tlsVerifyClient, err := parser.GetStringAnnotation(annotationAuthVerifyClient, ing)
|
||||
if err != nil || !authVerifyClientRegex.MatchString(tlsVerifyClient) {
|
||||
tlsVerifyClient = defaultAuthVerifyClient
|
||||
}
|
||||
|
||||
tlsdepth, err := parser.GetIntAnnotation(annotationAuthTLSDepth, ing)
|
||||
if err != nil || tlsdepth == 0 {
|
||||
tlsdepth = defaultAuthTLSDepth
|
||||
}
|
||||
|
||||
authCert, err := a.certResolver.GetAuthCertificate(tlsauthsecret)
|
||||
if err != nil {
|
||||
return &AuthSSLConfig{}, ing_errors.LocationDenied{
|
||||
Reason: errors.Wrap(err, "error obtaining certificate"),
|
||||
}
|
||||
}
|
||||
|
||||
errorpage, err := parser.GetStringAnnotation(annotationAuthTLSErrorPage, ing)
|
||||
if err != nil || errorpage == "" {
|
||||
errorpage = ""
|
||||
}
|
||||
|
||||
return &AuthSSLConfig{
|
||||
AuthSSLCert: *authCert,
|
||||
VerifyClient: tlsVerifyClient,
|
||||
ValidationDepth: tlsdepth,
|
||||
ErrorPage: errorpage,
|
||||
}, nil
|
||||
}
|
||||
108
pkg/ingress/annotations/authtls/main_test.go
Normal file
108
pkg/ingress/annotations/authtls/main_test.go
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
/*
|
||||
Copyright 2015 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 authtls
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
api "k8s.io/api/core/v1"
|
||||
extensions "k8s.io/api/extensions/v1beta1"
|
||||
meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
)
|
||||
|
||||
func buildIngress() *extensions.Ingress {
|
||||
defaultBackend := extensions.IngressBackend{
|
||||
ServiceName: "default-backend",
|
||||
ServicePort: intstr.FromInt(80),
|
||||
}
|
||||
|
||||
return &extensions.Ingress{
|
||||
ObjectMeta: meta_v1.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 TestAnnotations(t *testing.T) {
|
||||
ing := buildIngress()
|
||||
|
||||
data := map[string]string{}
|
||||
ing.SetAnnotations(data)
|
||||
/*
|
||||
tests := []struct {
|
||||
title string
|
||||
url string
|
||||
method string
|
||||
sendBody bool
|
||||
expErr bool
|
||||
}{
|
||||
{"empty", "", "", false, true},
|
||||
{"no scheme", "bar", "", false, true},
|
||||
{"invalid host", "http://", "", false, true},
|
||||
{"invalid host (multiple dots)", "http://foo..bar.com", "", false, true},
|
||||
{"valid URL", "http://bar.foo.com/external-auth", "", false, false},
|
||||
{"valid URL - send body", "http://foo.com/external-auth", "POST", true, false},
|
||||
{"valid URL - send body", "http://foo.com/external-auth", "GET", true, false},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
data[authTLSSecret] = ""
|
||||
test.title
|
||||
|
||||
u, err := ParseAnnotations(ing)
|
||||
|
||||
if test.expErr {
|
||||
if err == nil {
|
||||
t.Errorf("%v: expected error but retuned nil", test.title)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if u.URL != test.url {
|
||||
t.Errorf("%v: expected \"%v\" but \"%v\" was returned", test.title, test.url, u.URL)
|
||||
}
|
||||
if u.Method != test.method {
|
||||
t.Errorf("%v: expected \"%v\" but \"%v\" was returned", test.title, test.method, u.Method)
|
||||
}
|
||||
if u.SendBody != test.sendBody {
|
||||
t.Errorf("%v: expected \"%v\" but \"%v\" was returned", test.title, test.sendBody, u.SendBody)
|
||||
}
|
||||
}*/
|
||||
}
|
||||
55
pkg/ingress/annotations/class/main.go
Normal file
55
pkg/ingress/annotations/class/main.go
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
Copyright 2015 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 class
|
||||
|
||||
import (
|
||||
"github.com/golang/glog"
|
||||
extensions "k8s.io/api/extensions/v1beta1"
|
||||
|
||||
"k8s.io/ingress-nginx/pkg/ingress/annotations/parser"
|
||||
"k8s.io/ingress-nginx/pkg/ingress/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
// IngressKey picks a specific "class" for the Ingress.
|
||||
// The controller only processes Ingresses with this annotation either
|
||||
// unset, or set to either the configured value or the empty string.
|
||||
IngressKey = "kubernetes.io/ingress.class"
|
||||
)
|
||||
|
||||
// IsValid returns true if the given Ingress either doesn't specify
|
||||
// the ingress.class annotation, or it's set to the configured in the
|
||||
// ingress controller.
|
||||
func IsValid(ing *extensions.Ingress, controller, defClass string) bool {
|
||||
ingress, err := parser.GetStringAnnotation(IngressKey, ing)
|
||||
if err != nil && !errors.IsMissingAnnotations(err) {
|
||||
glog.Warningf("unexpected error reading ingress annotation: %v", err)
|
||||
}
|
||||
|
||||
// we have 2 valid combinations
|
||||
// 1 - ingress with default class | blank annotation on ingress
|
||||
// 2 - ingress with specific class | same annotation on ingress
|
||||
//
|
||||
// and 2 invalid combinations
|
||||
// 3 - ingress with default class | fixed annotation on ingress
|
||||
// 4 - ingress with specific class | different annotation on ingress
|
||||
if ingress == "" && controller == defClass {
|
||||
return true
|
||||
}
|
||||
|
||||
return ingress == controller
|
||||
}
|
||||
59
pkg/ingress/annotations/class/main_test.go
Normal file
59
pkg/ingress/annotations/class/main_test.go
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
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 class
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
api "k8s.io/api/core/v1"
|
||||
extensions "k8s.io/api/extensions/v1beta1"
|
||||
meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
func TestIsValidClass(t *testing.T) {
|
||||
tests := []struct {
|
||||
ingress string
|
||||
controller string
|
||||
defClass string
|
||||
isValid bool
|
||||
}{
|
||||
{"", "", "nginx", true},
|
||||
{"", "nginx", "nginx", true},
|
||||
{"nginx", "nginx", "nginx", true},
|
||||
{"custom", "custom", "nginx", true},
|
||||
{"", "killer", "nginx", false},
|
||||
{"", "", "nginx", true},
|
||||
{"custom", "nginx", "nginx", false},
|
||||
}
|
||||
|
||||
ing := &extensions.Ingress{
|
||||
ObjectMeta: meta_v1.ObjectMeta{
|
||||
Name: "foo",
|
||||
Namespace: api.NamespaceDefault,
|
||||
},
|
||||
}
|
||||
|
||||
data := map[string]string{}
|
||||
ing.SetAnnotations(data)
|
||||
for _, test := range tests {
|
||||
ing.Annotations[IngressKey] = test.ingress
|
||||
b := IsValid(ing, test.controller, test.defClass)
|
||||
if b != test.isValid {
|
||||
t.Errorf("test %v - expected %v but %v was returned", test, test.isValid, b)
|
||||
}
|
||||
}
|
||||
}
|
||||
41
pkg/ingress/annotations/clientbodybuffersize/main.go
Normal file
41
pkg/ingress/annotations/clientbodybuffersize/main.go
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
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 clientbodybuffersize
|
||||
|
||||
import (
|
||||
extensions "k8s.io/api/extensions/v1beta1"
|
||||
|
||||
"k8s.io/ingress-nginx/pkg/ingress/annotations/parser"
|
||||
)
|
||||
|
||||
const (
|
||||
annotation = "ingress.kubernetes.io/client-body-buffer-size"
|
||||
)
|
||||
|
||||
type clientBodyBufferSize struct {
|
||||
}
|
||||
|
||||
// NewParser creates a new clientBodyBufferSize annotation parser
|
||||
func NewParser() parser.IngressAnnotation {
|
||||
return clientBodyBufferSize{}
|
||||
}
|
||||
|
||||
// Parse parses the annotations contained in the ingress rule
|
||||
// used to add an client-body-buffer-size to the provided locations
|
||||
func (a clientBodyBufferSize) Parse(ing *extensions.Ingress) (interface{}, error) {
|
||||
return parser.GetStringAnnotation(annotation, ing)
|
||||
}
|
||||
59
pkg/ingress/annotations/clientbodybuffersize/main_test.go
Normal file
59
pkg/ingress/annotations/clientbodybuffersize/main_test.go
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
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 clientbodybuffersize
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
api "k8s.io/api/core/v1"
|
||||
extensions "k8s.io/api/extensions/v1beta1"
|
||||
meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
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: "8k"}, "8k"},
|
||||
{map[string]string{annotation: "16k"}, "16k"},
|
||||
{map[string]string{annotation: ""}, ""},
|
||||
{map[string]string{}, ""},
|
||||
{nil, ""},
|
||||
}
|
||||
|
||||
ing := &extensions.Ingress{
|
||||
ObjectMeta: meta_v1.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
41
pkg/ingress/annotations/cors/main.go
Normal file
41
pkg/ingress/annotations/cors/main.go
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
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 cors
|
||||
|
||||
import (
|
||||
extensions "k8s.io/api/extensions/v1beta1"
|
||||
|
||||
"k8s.io/ingress-nginx/pkg/ingress/annotations/parser"
|
||||
)
|
||||
|
||||
const (
|
||||
annotation = "ingress.kubernetes.io/enable-cors"
|
||||
)
|
||||
|
||||
type cors struct {
|
||||
}
|
||||
|
||||
// NewParser creates a new CORS annotation parser
|
||||
func NewParser() parser.IngressAnnotation {
|
||||
return cors{}
|
||||
}
|
||||
|
||||
// Parse parses the annotations contained in the ingress
|
||||
// rule used to indicate if the location/s should allows CORS
|
||||
func (a cors) Parse(ing *extensions.Ingress) (interface{}, error) {
|
||||
return parser.GetBoolAnnotation(annotation, ing)
|
||||
}
|
||||
63
pkg/ingress/annotations/cors/main_test.go
Normal file
63
pkg/ingress/annotations/cors/main_test.go
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
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 cors
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
api "k8s.io/api/core/v1"
|
||||
extensions "k8s.io/api/extensions/v1beta1"
|
||||
meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
const (
|
||||
notCorsAnnotation = "ingress.kubernetes.io/enable-not-cors"
|
||||
)
|
||||
|
||||
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 bool
|
||||
}{
|
||||
{map[string]string{annotation: "true"}, true},
|
||||
{map[string]string{annotation: "false"}, false},
|
||||
{map[string]string{notCorsAnnotation: "true"}, false},
|
||||
{map[string]string{}, false},
|
||||
{nil, false},
|
||||
}
|
||||
|
||||
ing := &extensions.Ingress{
|
||||
ObjectMeta: meta_v1.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 %t but returned %t, annotations: %s", testCase.expected, result, testCase.annotations)
|
||||
}
|
||||
}
|
||||
}
|
||||
57
pkg/ingress/annotations/defaultbackend/main.go
Normal file
57
pkg/ingress/annotations/defaultbackend/main.go
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
Copyright 2015 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 defaultbackend
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
extensions "k8s.io/api/extensions/v1beta1"
|
||||
|
||||
"k8s.io/ingress-nginx/pkg/ingress/annotations/parser"
|
||||
"k8s.io/ingress-nginx/pkg/ingress/resolver"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultBackend = "ingress.kubernetes.io/default-backend"
|
||||
)
|
||||
|
||||
type backend struct {
|
||||
serviceResolver resolver.Service
|
||||
}
|
||||
|
||||
// NewParser creates a new default backend annotation parser
|
||||
func NewParser(sr resolver.Service) parser.IngressAnnotation {
|
||||
return backend{sr}
|
||||
}
|
||||
|
||||
// Parse parses the annotations contained in the ingress to use
|
||||
// a custom default backend
|
||||
func (db backend) Parse(ing *extensions.Ingress) (interface{}, error) {
|
||||
s, err := parser.GetStringAnnotation(defaultBackend, ing)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
name := fmt.Sprintf("%v/%v", ing.Namespace, s)
|
||||
svc, err := db.serviceResolver.GetService(name)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "unexpected error reading service %v", name)
|
||||
}
|
||||
|
||||
return svc, nil
|
||||
}
|
||||
66
pkg/ingress/annotations/healthcheck/main.go
Normal file
66
pkg/ingress/annotations/healthcheck/main.go
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
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 healthcheck
|
||||
|
||||
import (
|
||||
extensions "k8s.io/api/extensions/v1beta1"
|
||||
|
||||
"k8s.io/ingress-nginx/pkg/ingress/annotations/parser"
|
||||
"k8s.io/ingress-nginx/pkg/ingress/resolver"
|
||||
)
|
||||
|
||||
const (
|
||||
upsMaxFails = "ingress.kubernetes.io/upstream-max-fails"
|
||||
upsFailTimeout = "ingress.kubernetes.io/upstream-fail-timeout"
|
||||
)
|
||||
|
||||
// Upstream returns the URL and method to use check the status of
|
||||
// the upstream server/s
|
||||
type Upstream struct {
|
||||
MaxFails int `json:"maxFails"`
|
||||
FailTimeout int `json:"failTimeout"`
|
||||
}
|
||||
|
||||
type healthCheck struct {
|
||||
backendResolver resolver.DefaultBackend
|
||||
}
|
||||
|
||||
// NewParser creates a new health check annotation parser
|
||||
func NewParser(br resolver.DefaultBackend) parser.IngressAnnotation {
|
||||
return healthCheck{br}
|
||||
}
|
||||
|
||||
// ParseAnnotations parses the annotations contained in the ingress
|
||||
// rule used to configure upstream check parameters
|
||||
func (a healthCheck) Parse(ing *extensions.Ingress) (interface{}, error) {
|
||||
defBackend := a.backendResolver.GetDefaultBackend()
|
||||
if ing.GetAnnotations() == nil {
|
||||
return &Upstream{defBackend.UpstreamMaxFails, defBackend.UpstreamFailTimeout}, nil
|
||||
}
|
||||
|
||||
mf, err := parser.GetIntAnnotation(upsMaxFails, ing)
|
||||
if err != nil {
|
||||
mf = defBackend.UpstreamMaxFails
|
||||
}
|
||||
|
||||
ft, err := parser.GetIntAnnotation(upsFailTimeout, ing)
|
||||
if err != nil {
|
||||
ft = defBackend.UpstreamFailTimeout
|
||||
}
|
||||
|
||||
return &Upstream{mf, ft}, nil
|
||||
}
|
||||
92
pkg/ingress/annotations/healthcheck/main_test.go
Normal file
92
pkg/ingress/annotations/healthcheck/main_test.go
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
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 healthcheck
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
api "k8s.io/api/core/v1"
|
||||
extensions "k8s.io/api/extensions/v1beta1"
|
||||
meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
|
||||
"k8s.io/ingress-nginx/pkg/ingress/defaults"
|
||||
)
|
||||
|
||||
func buildIngress() *extensions.Ingress {
|
||||
defaultBackend := extensions.IngressBackend{
|
||||
ServiceName: "default-backend",
|
||||
ServicePort: intstr.FromInt(80),
|
||||
}
|
||||
|
||||
return &extensions.Ingress{
|
||||
ObjectMeta: meta_v1.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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type mockBackend struct {
|
||||
}
|
||||
|
||||
func (m mockBackend) GetDefaultBackend() defaults.Backend {
|
||||
return defaults.Backend{UpstreamFailTimeout: 1}
|
||||
}
|
||||
|
||||
func TestIngressHealthCheck(t *testing.T) {
|
||||
ing := buildIngress()
|
||||
|
||||
data := map[string]string{}
|
||||
data[upsMaxFails] = "2"
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
hzi, _ := NewParser(mockBackend{}).Parse(ing)
|
||||
nginxHz, ok := hzi.(*Upstream)
|
||||
if !ok {
|
||||
t.Errorf("expected a Upstream type")
|
||||
}
|
||||
|
||||
if nginxHz.MaxFails != 2 {
|
||||
t.Errorf("expected 2 as max-fails but returned %v", nginxHz.MaxFails)
|
||||
}
|
||||
|
||||
if nginxHz.FailTimeout != 1 {
|
||||
t.Errorf("expected 0 as fail-timeout but returned %v", nginxHz.FailTimeout)
|
||||
}
|
||||
}
|
||||
113
pkg/ingress/annotations/ipwhitelist/main.go
Normal file
113
pkg/ingress/annotations/ipwhitelist/main.go
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
/*
|
||||
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 ipwhitelist
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
extensions "k8s.io/api/extensions/v1beta1"
|
||||
"k8s.io/ingress-nginx/pkg/net"
|
||||
|
||||
"k8s.io/ingress-nginx/pkg/ingress/annotations/parser"
|
||||
ing_errors "k8s.io/ingress-nginx/pkg/ingress/errors"
|
||||
"k8s.io/ingress-nginx/pkg/ingress/resolver"
|
||||
)
|
||||
|
||||
const (
|
||||
whitelist = "ingress.kubernetes.io/whitelist-source-range"
|
||||
)
|
||||
|
||||
// SourceRange returns the CIDR
|
||||
type SourceRange struct {
|
||||
CIDR []string `json:"cidr,omitEmpty"`
|
||||
}
|
||||
|
||||
// Equal tests for equality between two SourceRange types
|
||||
func (sr1 *SourceRange) Equal(sr2 *SourceRange) bool {
|
||||
if sr1 == sr2 {
|
||||
return true
|
||||
}
|
||||
if sr1 == nil || sr2 == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if len(sr1.CIDR) != len(sr2.CIDR) {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, s1l := range sr1.CIDR {
|
||||
found := false
|
||||
for _, sl2 := range sr2.CIDR {
|
||||
if s1l == sl2 {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
type ipwhitelist struct {
|
||||
backendResolver resolver.DefaultBackend
|
||||
}
|
||||
|
||||
// NewParser creates a new whitelist annotation parser
|
||||
func NewParser(br resolver.DefaultBackend) parser.IngressAnnotation {
|
||||
return ipwhitelist{br}
|
||||
}
|
||||
|
||||
// ParseAnnotations parses the annotations contained in the ingress
|
||||
// rule used to limit access to certain client addresses or networks.
|
||||
// Multiple ranges can specified using commas as separator
|
||||
// e.g. `18.0.0.0/8,56.0.0.0/8`
|
||||
func (a ipwhitelist) Parse(ing *extensions.Ingress) (interface{}, error) {
|
||||
defBackend := a.backendResolver.GetDefaultBackend()
|
||||
sort.Strings(defBackend.WhitelistSourceRange)
|
||||
|
||||
val, err := parser.GetStringAnnotation(whitelist, ing)
|
||||
// A missing annotation is not a problem, just use the default
|
||||
if err == ing_errors.ErrMissingAnnotations {
|
||||
return &SourceRange{CIDR: defBackend.WhitelistSourceRange}, nil
|
||||
}
|
||||
|
||||
values := strings.Split(val, ",")
|
||||
ipnets, ips, err := net.ParseIPNets(values...)
|
||||
if err != nil && len(ips) == 0 {
|
||||
return &SourceRange{CIDR: defBackend.WhitelistSourceRange}, ing_errors.LocationDenied{
|
||||
Reason: errors.Wrap(err, "the annotation does not contain a valid IP address or network"),
|
||||
}
|
||||
}
|
||||
|
||||
cidrs := []string{}
|
||||
for k := range ipnets {
|
||||
cidrs = append(cidrs, k)
|
||||
}
|
||||
for k := range ips {
|
||||
cidrs = append(cidrs, k)
|
||||
}
|
||||
|
||||
sort.Strings(cidrs)
|
||||
|
||||
return &SourceRange{cidrs}, nil
|
||||
}
|
||||
199
pkg/ingress/annotations/ipwhitelist/main_test.go
Normal file
199
pkg/ingress/annotations/ipwhitelist/main_test.go
Normal file
|
|
@ -0,0 +1,199 @@
|
|||
/*
|
||||
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 ipwhitelist
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
api "k8s.io/api/core/v1"
|
||||
extensions "k8s.io/api/extensions/v1beta1"
|
||||
meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
|
||||
"k8s.io/ingress-nginx/pkg/ingress/defaults"
|
||||
)
|
||||
|
||||
func buildIngress() *extensions.Ingress {
|
||||
defaultBackend := extensions.IngressBackend{
|
||||
ServiceName: "default-backend",
|
||||
ServicePort: intstr.FromInt(80),
|
||||
}
|
||||
|
||||
return &extensions.Ingress{
|
||||
ObjectMeta: meta_v1.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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type mockBackend struct {
|
||||
defaults.Backend
|
||||
}
|
||||
|
||||
func (m mockBackend) GetDefaultBackend() defaults.Backend {
|
||||
return m.Backend
|
||||
}
|
||||
|
||||
func TestParseAnnotations(t *testing.T) {
|
||||
ing := buildIngress()
|
||||
tests := map[string]struct {
|
||||
net string
|
||||
expectCidr []string
|
||||
expectErr bool
|
||||
errOut string
|
||||
}{
|
||||
"test parse a valid net": {
|
||||
net: "10.0.0.0/24",
|
||||
expectCidr: []string{"10.0.0.0/24"},
|
||||
expectErr: false,
|
||||
},
|
||||
"test parse a invalid net": {
|
||||
net: "ww",
|
||||
expectErr: true,
|
||||
errOut: "the annotation does not contain a valid IP address or network: invalid CIDR address: ww",
|
||||
},
|
||||
"test parse a empty net": {
|
||||
net: "",
|
||||
expectErr: true,
|
||||
errOut: "the annotation does not contain a valid IP address or network: invalid CIDR address: ",
|
||||
},
|
||||
"test parse multiple valid cidr": {
|
||||
net: "2.2.2.2/32,1.1.1.1/32,3.3.3.0/24",
|
||||
expectCidr: []string{"1.1.1.1/32", "2.2.2.2/32", "3.3.3.0/24"},
|
||||
expectErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for testName, test := range tests {
|
||||
data := map[string]string{}
|
||||
data[whitelist] = test.net
|
||||
ing.SetAnnotations(data)
|
||||
p := NewParser(mockBackend{})
|
||||
i, err := p.Parse(ing)
|
||||
if err != nil && !test.expectErr {
|
||||
t.Errorf("%v:unexpected error: %v", testName, err)
|
||||
}
|
||||
if test.expectErr {
|
||||
if err.Error() != test.errOut {
|
||||
t.Errorf("%v:expected error: %v but %v return", testName, test.errOut, err.Error())
|
||||
}
|
||||
}
|
||||
if !test.expectErr {
|
||||
sr, ok := i.(*SourceRange)
|
||||
if !ok {
|
||||
t.Errorf("%v:expected a SourceRange type", testName)
|
||||
}
|
||||
if !strsEquals(sr.CIDR, test.expectCidr) {
|
||||
t.Errorf("%v:expected %v CIDR but %v returned", testName, test.expectCidr, sr.CIDR)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test that when we have a whitelist set on the Backend that is used when we
|
||||
// don't have the annotation
|
||||
func TestParseAnnotationsWithDefaultConfig(t *testing.T) {
|
||||
ing := buildIngress()
|
||||
mockBackend := mockBackend{}
|
||||
mockBackend.Backend.WhitelistSourceRange = []string{"4.4.4.0/24", "1.2.3.4/32"}
|
||||
tests := map[string]struct {
|
||||
net string
|
||||
expectCidr []string
|
||||
expectErr bool
|
||||
errOut string
|
||||
}{
|
||||
"test parse a valid net": {
|
||||
net: "10.0.0.0/24",
|
||||
expectCidr: []string{"10.0.0.0/24"},
|
||||
expectErr: false,
|
||||
},
|
||||
"test parse a invalid net": {
|
||||
net: "ww",
|
||||
expectErr: true,
|
||||
errOut: "the annotation does not contain a valid IP address or network: invalid CIDR address: ww",
|
||||
},
|
||||
"test parse a empty net": {
|
||||
net: "",
|
||||
expectErr: true,
|
||||
errOut: "the annotation does not contain a valid IP address or network: invalid CIDR address: ",
|
||||
},
|
||||
"test parse multiple valid cidr": {
|
||||
net: "2.2.2.2/32,1.1.1.1/32,3.3.3.0/24",
|
||||
expectCidr: []string{"1.1.1.1/32", "2.2.2.2/32", "3.3.3.0/24"},
|
||||
expectErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for testName, test := range tests {
|
||||
data := map[string]string{}
|
||||
data[whitelist] = test.net
|
||||
ing.SetAnnotations(data)
|
||||
p := NewParser(mockBackend)
|
||||
i, err := p.Parse(ing)
|
||||
if err != nil && !test.expectErr {
|
||||
t.Errorf("%v:unexpected error: %v", testName, err)
|
||||
}
|
||||
if test.expectErr {
|
||||
if err.Error() != test.errOut {
|
||||
t.Errorf("%v:expected error: %v but %v return", testName, test.errOut, err.Error())
|
||||
}
|
||||
}
|
||||
if !test.expectErr {
|
||||
sr, ok := i.(*SourceRange)
|
||||
if !ok {
|
||||
t.Errorf("%v:expected a SourceRange type", testName)
|
||||
}
|
||||
if !strsEquals(sr.CIDR, test.expectCidr) {
|
||||
t.Errorf("%v:expected %v CIDR but %v returned", testName, test.expectCidr, sr.CIDR)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func strsEquals(a, b []string) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for i, v := range a {
|
||||
if v != b[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
102
pkg/ingress/annotations/parser/main.go
Normal file
102
pkg/ingress/annotations/parser/main.go
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
/*
|
||||
Copyright 2015 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 parser
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
extensions "k8s.io/api/extensions/v1beta1"
|
||||
|
||||
"k8s.io/ingress-nginx/pkg/ingress/errors"
|
||||
)
|
||||
|
||||
// IngressAnnotation has a method to parse annotations located in Ingress
|
||||
type IngressAnnotation interface {
|
||||
Parse(ing *extensions.Ingress) (interface{}, error)
|
||||
}
|
||||
|
||||
type ingAnnotations map[string]string
|
||||
|
||||
func (a ingAnnotations) parseBool(name string) (bool, error) {
|
||||
val, ok := a[name]
|
||||
if ok {
|
||||
b, err := strconv.ParseBool(val)
|
||||
if err != nil {
|
||||
return false, errors.NewInvalidAnnotationContent(name, val)
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
return false, errors.ErrMissingAnnotations
|
||||
}
|
||||
|
||||
func (a ingAnnotations) parseString(name string) (string, error) {
|
||||
val, ok := a[name]
|
||||
if ok {
|
||||
return val, nil
|
||||
}
|
||||
return "", errors.ErrMissingAnnotations
|
||||
}
|
||||
|
||||
func (a ingAnnotations) parseInt(name string) (int, error) {
|
||||
val, ok := a[name]
|
||||
if ok {
|
||||
i, err := strconv.Atoi(val)
|
||||
if err != nil {
|
||||
return 0, errors.NewInvalidAnnotationContent(name, val)
|
||||
}
|
||||
return i, nil
|
||||
}
|
||||
return 0, errors.ErrMissingAnnotations
|
||||
}
|
||||
|
||||
func checkAnnotation(name string, ing *extensions.Ingress) error {
|
||||
if ing == nil || len(ing.GetAnnotations()) == 0 {
|
||||
return errors.ErrMissingAnnotations
|
||||
}
|
||||
if name == "" {
|
||||
return errors.ErrInvalidAnnotationName
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetBoolAnnotation extracts a boolean from an Ingress annotation
|
||||
func GetBoolAnnotation(name string, ing *extensions.Ingress) (bool, error) {
|
||||
err := checkAnnotation(name, ing)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return ingAnnotations(ing.GetAnnotations()).parseBool(name)
|
||||
}
|
||||
|
||||
// GetStringAnnotation extracts a string from an Ingress annotation
|
||||
func GetStringAnnotation(name string, ing *extensions.Ingress) (string, error) {
|
||||
err := checkAnnotation(name, ing)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return ingAnnotations(ing.GetAnnotations()).parseString(name)
|
||||
}
|
||||
|
||||
// GetIntAnnotation extracts an int from an Ingress annotation
|
||||
func GetIntAnnotation(name string, ing *extensions.Ingress) (int, error) {
|
||||
err := checkAnnotation(name, ing)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return ingAnnotations(ing.GetAnnotations()).parseInt(name)
|
||||
}
|
||||
161
pkg/ingress/annotations/parser/main_test.go
Normal file
161
pkg/ingress/annotations/parser/main_test.go
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
/*
|
||||
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 parser
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
api "k8s.io/api/core/v1"
|
||||
extensions "k8s.io/api/extensions/v1beta1"
|
||||
meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
func buildIngress() *extensions.Ingress {
|
||||
return &extensions.Ingress{
|
||||
ObjectMeta: meta_v1.ObjectMeta{
|
||||
Name: "foo",
|
||||
Namespace: api.NamespaceDefault,
|
||||
},
|
||||
Spec: extensions.IngressSpec{},
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetBoolAnnotation(t *testing.T) {
|
||||
ing := buildIngress()
|
||||
|
||||
_, err := GetBoolAnnotation("", nil)
|
||||
if err == nil {
|
||||
t.Errorf("expected error but retuned nil")
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
field string
|
||||
value string
|
||||
exp bool
|
||||
expErr bool
|
||||
}{
|
||||
{"empty - false", "", "false", false, true},
|
||||
{"empty - true", "", "true", false, true},
|
||||
{"valid - false", "bool", "false", false, false},
|
||||
{"valid - true", "bool", "true", true, false},
|
||||
}
|
||||
|
||||
data := map[string]string{}
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
for _, test := range tests {
|
||||
data[test.field] = test.value
|
||||
|
||||
u, err := GetBoolAnnotation(test.field, ing)
|
||||
if test.expErr {
|
||||
if err == nil {
|
||||
t.Errorf("%v: expected error but retuned nil", test.name)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if u != test.exp {
|
||||
t.Errorf("%v: expected \"%v\" but \"%v\" was returned", test.name, test.exp, u)
|
||||
}
|
||||
|
||||
delete(data, test.field)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetStringAnnotation(t *testing.T) {
|
||||
ing := buildIngress()
|
||||
|
||||
_, err := GetStringAnnotation("", nil)
|
||||
if err == nil {
|
||||
t.Errorf("expected error but retuned nil")
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
field string
|
||||
value string
|
||||
exp string
|
||||
expErr bool
|
||||
}{
|
||||
{"empty - A", "", "A", "", true},
|
||||
{"empty - B", "", "B", "", true},
|
||||
{"valid - A", "string", "A", "A", false},
|
||||
{"valid - B", "string", "B", "B", false},
|
||||
}
|
||||
|
||||
data := map[string]string{}
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
for _, test := range tests {
|
||||
data[test.field] = test.value
|
||||
|
||||
s, err := GetStringAnnotation(test.field, ing)
|
||||
if test.expErr {
|
||||
if err == nil {
|
||||
t.Errorf("%v: expected error but retuned nil", test.name)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if s != test.exp {
|
||||
t.Errorf("%v: expected \"%v\" but \"%v\" was returned", test.name, test.exp, s)
|
||||
}
|
||||
|
||||
delete(data, test.field)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetIntAnnotation(t *testing.T) {
|
||||
ing := buildIngress()
|
||||
|
||||
_, err := GetIntAnnotation("", nil)
|
||||
if err == nil {
|
||||
t.Errorf("expected error but retuned nil")
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
field string
|
||||
value string
|
||||
exp int
|
||||
expErr bool
|
||||
}{
|
||||
{"empty - A", "", "1", 0, true},
|
||||
{"empty - B", "", "2", 0, true},
|
||||
{"valid - A", "string", "1", 1, false},
|
||||
{"valid - B", "string", "2", 2, false},
|
||||
}
|
||||
|
||||
data := map[string]string{}
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
for _, test := range tests {
|
||||
data[test.field] = test.value
|
||||
|
||||
s, err := GetIntAnnotation(test.field, ing)
|
||||
if test.expErr {
|
||||
if err == nil {
|
||||
t.Errorf("%v: expected error but retuned nil", test.name)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if s != test.exp {
|
||||
t.Errorf("%v: expected \"%v\" but \"%v\" was returned", test.name, test.exp, s)
|
||||
}
|
||||
|
||||
delete(data, test.field)
|
||||
}
|
||||
}
|
||||
48
pkg/ingress/annotations/portinredirect/main.go
Normal file
48
pkg/ingress/annotations/portinredirect/main.go
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
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 portinredirect
|
||||
|
||||
import (
|
||||
extensions "k8s.io/api/extensions/v1beta1"
|
||||
|
||||
"k8s.io/ingress-nginx/pkg/ingress/annotations/parser"
|
||||
"k8s.io/ingress-nginx/pkg/ingress/resolver"
|
||||
)
|
||||
|
||||
const (
|
||||
annotation = "ingress.kubernetes.io/use-port-in-redirects"
|
||||
)
|
||||
|
||||
type portInRedirect struct {
|
||||
backendResolver resolver.DefaultBackend
|
||||
}
|
||||
|
||||
// NewParser creates a new port in redirect annotation parser
|
||||
func NewParser(db resolver.DefaultBackend) parser.IngressAnnotation {
|
||||
return portInRedirect{db}
|
||||
}
|
||||
|
||||
// Parse parses the annotations contained in the ingress
|
||||
// rule used to indicate if the redirects must
|
||||
func (a portInRedirect) Parse(ing *extensions.Ingress) (interface{}, error) {
|
||||
up, err := parser.GetBoolAnnotation(annotation, ing)
|
||||
if err != nil {
|
||||
return a.backendResolver.GetDefaultBackend().UsePortInRedirects, nil
|
||||
}
|
||||
|
||||
return up, nil
|
||||
}
|
||||
121
pkg/ingress/annotations/portinredirect/main_test.go
Normal file
121
pkg/ingress/annotations/portinredirect/main_test.go
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
/*
|
||||
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 portinredirect
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
api "k8s.io/api/core/v1"
|
||||
extensions "k8s.io/api/extensions/v1beta1"
|
||||
meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
|
||||
"fmt"
|
||||
|
||||
"k8s.io/ingress-nginx/pkg/ingress/defaults"
|
||||
)
|
||||
|
||||
func buildIngress() *extensions.Ingress {
|
||||
defaultBackend := extensions.IngressBackend{
|
||||
ServiceName: "default-backend",
|
||||
ServicePort: intstr.FromInt(80),
|
||||
}
|
||||
|
||||
return &extensions.Ingress{
|
||||
ObjectMeta: meta_v1.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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type mockBackend struct {
|
||||
usePortInRedirects bool
|
||||
}
|
||||
|
||||
func (m mockBackend) GetDefaultBackend() defaults.Backend {
|
||||
return defaults.Backend{UsePortInRedirects: m.usePortInRedirects}
|
||||
}
|
||||
|
||||
func TestPortInRedirect(t *testing.T) {
|
||||
tests := []struct {
|
||||
title string
|
||||
usePort *bool
|
||||
def bool
|
||||
exp bool
|
||||
}{
|
||||
{"false - default false", newFalse(), false, false},
|
||||
{"false - default true", newFalse(), true, false},
|
||||
{"no annotation - default false", nil, false, false},
|
||||
{"no annotation - default true", nil, true, true},
|
||||
{"true - default true", newTrue(), true, true},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
ing := buildIngress()
|
||||
|
||||
data := map[string]string{}
|
||||
if test.usePort != nil {
|
||||
data[annotation] = fmt.Sprintf("%v", *test.usePort)
|
||||
}
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
i, err := NewParser(mockBackend{test.def}).Parse(ing)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error parsing a valid")
|
||||
}
|
||||
p, ok := i.(bool)
|
||||
if !ok {
|
||||
t.Errorf("expected a bool type")
|
||||
}
|
||||
|
||||
if p != test.exp {
|
||||
t.Errorf("%v: expected \"%v\" but \"%v\" was returned", test.title, test.exp, p)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func newTrue() *bool {
|
||||
b := true
|
||||
return &b
|
||||
}
|
||||
|
||||
func newFalse() *bool {
|
||||
b := false
|
||||
return &b
|
||||
}
|
||||
160
pkg/ingress/annotations/proxy/main.go
Normal file
160
pkg/ingress/annotations/proxy/main.go
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
/*
|
||||
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 proxy
|
||||
|
||||
import (
|
||||
extensions "k8s.io/api/extensions/v1beta1"
|
||||
|
||||
"k8s.io/ingress-nginx/pkg/ingress/annotations/parser"
|
||||
"k8s.io/ingress-nginx/pkg/ingress/resolver"
|
||||
)
|
||||
|
||||
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"
|
||||
cookiePath = "ingress.kubernetes.io/proxy-cookie-path"
|
||||
cookieDomain = "ingress.kubernetes.io/proxy-cookie-domain"
|
||||
nextUpstream = "ingress.kubernetes.io/proxy-next-upstream"
|
||||
passParams = "ingress.kubernetes.io/proxy-pass-params"
|
||||
requestBuffering = "ingress.kubernetes.io/proxy-request-buffering"
|
||||
)
|
||||
|
||||
// Configuration returns the proxy timeout to use in the upstream server/s
|
||||
type Configuration struct {
|
||||
BodySize string `json:"bodySize"`
|
||||
ConnectTimeout int `json:"connectTimeout"`
|
||||
SendTimeout int `json:"sendTimeout"`
|
||||
ReadTimeout int `json:"readTimeout"`
|
||||
BufferSize string `json:"bufferSize"`
|
||||
CookieDomain string `json:"cookieDomain"`
|
||||
CookiePath string `json:"cookiePath"`
|
||||
NextUpstream string `json:"nextUpstream"`
|
||||
PassParams string `json:"passParams"`
|
||||
RequestBuffering string `json:"requestBuffering"`
|
||||
}
|
||||
|
||||
// Equal tests for equality between two Configuration types
|
||||
func (l1 *Configuration) Equal(l2 *Configuration) bool {
|
||||
if l1 == l2 {
|
||||
return true
|
||||
}
|
||||
if l1 == nil || l2 == nil {
|
||||
return false
|
||||
}
|
||||
if l1.BodySize != l2.BodySize {
|
||||
return false
|
||||
}
|
||||
if l1.ConnectTimeout != l2.ConnectTimeout {
|
||||
return false
|
||||
}
|
||||
if l1.SendTimeout != l2.SendTimeout {
|
||||
return false
|
||||
}
|
||||
if l1.ReadTimeout != l2.ReadTimeout {
|
||||
return false
|
||||
}
|
||||
if l1.BufferSize != l2.BufferSize {
|
||||
return false
|
||||
}
|
||||
if l1.CookieDomain != l2.CookieDomain {
|
||||
return false
|
||||
}
|
||||
if l1.CookiePath != l2.CookiePath {
|
||||
return false
|
||||
}
|
||||
if l1.NextUpstream != l2.NextUpstream {
|
||||
return false
|
||||
}
|
||||
if l1.PassParams != l2.PassParams {
|
||||
return false
|
||||
}
|
||||
|
||||
if l1.RequestBuffering != l2.RequestBuffering {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
type proxy struct {
|
||||
backendResolver resolver.DefaultBackend
|
||||
}
|
||||
|
||||
// NewParser creates a new reverse proxy configuration annotation parser
|
||||
func NewParser(br resolver.DefaultBackend) parser.IngressAnnotation {
|
||||
return proxy{br}
|
||||
}
|
||||
|
||||
// ParseAnnotations parses the annotations contained in the ingress
|
||||
// rule used to configure upstream check parameters
|
||||
func (a proxy) Parse(ing *extensions.Ingress) (interface{}, error) {
|
||||
defBackend := a.backendResolver.GetDefaultBackend()
|
||||
ct, err := parser.GetIntAnnotation(connect, ing)
|
||||
if err != nil {
|
||||
ct = defBackend.ProxyConnectTimeout
|
||||
}
|
||||
|
||||
st, err := parser.GetIntAnnotation(send, ing)
|
||||
if err != nil {
|
||||
st = defBackend.ProxySendTimeout
|
||||
}
|
||||
|
||||
rt, err := parser.GetIntAnnotation(read, ing)
|
||||
if err != nil {
|
||||
rt = defBackend.ProxyReadTimeout
|
||||
}
|
||||
|
||||
bufs, err := parser.GetStringAnnotation(bufferSize, ing)
|
||||
if err != nil || bufs == "" {
|
||||
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
|
||||
}
|
||||
|
||||
nu, err := parser.GetStringAnnotation(nextUpstream, ing)
|
||||
if err != nil || nu == "" {
|
||||
nu = defBackend.ProxyNextUpstream
|
||||
}
|
||||
|
||||
pp, err := parser.GetStringAnnotation(passParams, ing)
|
||||
if err != nil || pp == "" {
|
||||
pp = defBackend.ProxyPassParams
|
||||
}
|
||||
|
||||
rb, err := parser.GetStringAnnotation(requestBuffering, ing)
|
||||
if err != nil || rb == "" {
|
||||
rb = defBackend.ProxyRequestBuffering
|
||||
}
|
||||
|
||||
return &Configuration{bs, ct, st, rt, bufs, cd, cp, nu, pp, rb}, nil
|
||||
}
|
||||
168
pkg/ingress/annotations/proxy/main_test.go
Normal file
168
pkg/ingress/annotations/proxy/main_test.go
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
/*
|
||||
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 proxy
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
api "k8s.io/api/core/v1"
|
||||
extensions "k8s.io/api/extensions/v1beta1"
|
||||
meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
|
||||
"k8s.io/ingress-nginx/pkg/ingress/defaults"
|
||||
)
|
||||
|
||||
func buildIngress() *extensions.Ingress {
|
||||
defaultBackend := extensions.IngressBackend{
|
||||
ServiceName: "default-backend",
|
||||
ServicePort: intstr.FromInt(80),
|
||||
}
|
||||
|
||||
return &extensions.Ingress{
|
||||
ObjectMeta: meta_v1.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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type mockBackend struct {
|
||||
}
|
||||
|
||||
func (m mockBackend) GetDefaultBackend() defaults.Backend {
|
||||
return defaults.Backend{
|
||||
UpstreamFailTimeout: 1,
|
||||
ProxyConnectTimeout: 10,
|
||||
ProxySendTimeout: 15,
|
||||
ProxyReadTimeout: 20,
|
||||
ProxyBufferSize: "10k",
|
||||
ProxyBodySize: "3k",
|
||||
ProxyNextUpstream: "error",
|
||||
ProxyPassParams: "nocanon keepalive=On",
|
||||
ProxyRequestBuffering: "on",
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxy(t *testing.T) {
|
||||
ing := buildIngress()
|
||||
|
||||
data := map[string]string{}
|
||||
data[connect] = "1"
|
||||
data[send] = "2"
|
||||
data[read] = "3"
|
||||
data[bufferSize] = "1k"
|
||||
data[bodySize] = "2k"
|
||||
data[nextUpstream] = "off"
|
||||
data[passParams] = "smax=5 max=10"
|
||||
data[requestBuffering] = "off"
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
i, err := NewParser(mockBackend{}).Parse(ing)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error parsing a valid")
|
||||
}
|
||||
p, ok := i.(*Configuration)
|
||||
if !ok {
|
||||
t.Fatalf("expected a Configuration type")
|
||||
}
|
||||
if p.ConnectTimeout != 1 {
|
||||
t.Errorf("expected 1 as connect-timeout but returned %v", p.ConnectTimeout)
|
||||
}
|
||||
if p.SendTimeout != 2 {
|
||||
t.Errorf("expected 2 as send-timeout but returned %v", p.SendTimeout)
|
||||
}
|
||||
if p.ReadTimeout != 3 {
|
||||
t.Errorf("expected 3 as read-timeout but returned %v", p.ReadTimeout)
|
||||
}
|
||||
if p.BufferSize != "1k" {
|
||||
t.Errorf("expected 1k as buffer-size but returned %v", p.BufferSize)
|
||||
}
|
||||
if p.BodySize != "2k" {
|
||||
t.Errorf("expected 2k as body-size but returned %v", p.BodySize)
|
||||
}
|
||||
if p.NextUpstream != "off" {
|
||||
t.Errorf("expected off as next-upstream but returned %v", p.NextUpstream)
|
||||
}
|
||||
if p.PassParams != "smax=5 max=10" {
|
||||
t.Errorf("expected \"smax=5 max=10\" as pass-params but returned \"%v\"", p.PassParams)
|
||||
}
|
||||
if p.RequestBuffering != "off" {
|
||||
t.Errorf("expected off as request-buffering but returned %v", p.RequestBuffering)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxyWithNoAnnotation(t *testing.T) {
|
||||
ing := buildIngress()
|
||||
|
||||
data := map[string]string{}
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
i, err := NewParser(mockBackend{}).Parse(ing)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error parsing a valid")
|
||||
}
|
||||
p, ok := i.(*Configuration)
|
||||
if !ok {
|
||||
t.Fatalf("expected a Configuration type")
|
||||
}
|
||||
if p.ConnectTimeout != 10 {
|
||||
t.Errorf("expected 10 as connect-timeout but returned %v", p.ConnectTimeout)
|
||||
}
|
||||
if p.SendTimeout != 15 {
|
||||
t.Errorf("expected 15 as send-timeout but returned %v", p.SendTimeout)
|
||||
}
|
||||
if p.ReadTimeout != 20 {
|
||||
t.Errorf("expected 20 as read-timeout but returned %v", p.ReadTimeout)
|
||||
}
|
||||
if p.BufferSize != "10k" {
|
||||
t.Errorf("expected 10k as buffer-size but returned %v", p.BufferSize)
|
||||
}
|
||||
if p.BodySize != "3k" {
|
||||
t.Errorf("expected 3k as body-size but returned %v", p.BodySize)
|
||||
}
|
||||
if p.NextUpstream != "error" {
|
||||
t.Errorf("expected error as next-upstream but returned %v", p.NextUpstream)
|
||||
}
|
||||
if p.PassParams != "nocanon keepalive=On" {
|
||||
t.Errorf("expected \"nocanon keepalive=On\" as pass-params but returned \"%v\"", p.PassParams)
|
||||
}
|
||||
if p.RequestBuffering != "on" {
|
||||
t.Errorf("expected on as request-buffering but returned %v", p.RequestBuffering)
|
||||
}
|
||||
}
|
||||
255
pkg/ingress/annotations/ratelimit/main.go
Normal file
255
pkg/ingress/annotations/ratelimit/main.go
Normal file
|
|
@ -0,0 +1,255 @@
|
|||
/*
|
||||
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 ratelimit
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
extensions "k8s.io/api/extensions/v1beta1"
|
||||
|
||||
"k8s.io/ingress-nginx/pkg/ingress/annotations/parser"
|
||||
"k8s.io/ingress-nginx/pkg/ingress/resolver"
|
||||
"k8s.io/ingress-nginx/pkg/net"
|
||||
)
|
||||
|
||||
const (
|
||||
limitIP = "ingress.kubernetes.io/limit-connections"
|
||||
limitRPS = "ingress.kubernetes.io/limit-rps"
|
||||
limitRPM = "ingress.kubernetes.io/limit-rpm"
|
||||
limitRATE = "ingress.kubernetes.io/limit-rate"
|
||||
limitRATEAFTER = "ingress.kubernetes.io/limit-rate-after"
|
||||
limitWhitelist = "ingress.kubernetes.io/limit-whitelist"
|
||||
|
||||
// allow 5 times the specified limit as burst
|
||||
defBurst = 5
|
||||
|
||||
// 1MB -> 16 thousand 64-byte states or about 8 thousand 128-byte states
|
||||
// default is 5MB
|
||||
defSharedSize = 5
|
||||
)
|
||||
|
||||
// RateLimit returns rate limit configuration for an Ingress rule limiting the
|
||||
// number of connections per IP address and/or connections per second.
|
||||
// If you both annotations are specified in a single Ingress rule, RPS limits
|
||||
// takes precedence
|
||||
type RateLimit struct {
|
||||
// Connections indicates a limit with the number of connections per IP address
|
||||
Connections Zone `json:"connections"`
|
||||
// RPS indicates a limit with the number of connections per second
|
||||
RPS Zone `json:"rps"`
|
||||
|
||||
RPM Zone `json:"rpm"`
|
||||
|
||||
LimitRate int `json:"limit-rate"`
|
||||
|
||||
LimitRateAfter int `json:"limit-rate-after"`
|
||||
|
||||
Name string `json:"name"`
|
||||
|
||||
ID string `json:"id"`
|
||||
|
||||
Whitelist []string `json:"whitelist"`
|
||||
}
|
||||
|
||||
// Equal tests for equality between two RateLimit types
|
||||
func (rt1 *RateLimit) Equal(rt2 *RateLimit) bool {
|
||||
if rt1 == rt2 {
|
||||
return true
|
||||
}
|
||||
if rt1 == nil || rt2 == nil {
|
||||
return false
|
||||
}
|
||||
if !(&rt1.Connections).Equal(&rt2.Connections) {
|
||||
return false
|
||||
}
|
||||
if !(&rt1.RPM).Equal(&rt2.RPM) {
|
||||
return false
|
||||
}
|
||||
if !(&rt1.RPS).Equal(&rt2.RPS) {
|
||||
return false
|
||||
}
|
||||
if rt1.LimitRate != rt2.LimitRate {
|
||||
return false
|
||||
}
|
||||
if rt1.LimitRateAfter != rt2.LimitRateAfter {
|
||||
return false
|
||||
}
|
||||
if rt1.ID != rt2.ID {
|
||||
return false
|
||||
}
|
||||
if rt1.Name != rt2.Name {
|
||||
return false
|
||||
}
|
||||
if len(rt1.Whitelist) != len(rt2.Whitelist) {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, r1l := range rt1.Whitelist {
|
||||
found := false
|
||||
for _, rl2 := range rt2.Whitelist {
|
||||
if r1l == rl2 {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Zone returns information about the NGINX rate limit (limit_req_zone)
|
||||
// http://nginx.org/en/docs/http/ngx_http_limit_req_module.html#limit_req_zone
|
||||
type Zone struct {
|
||||
Name string `json:"name"`
|
||||
Limit int `json:"limit"`
|
||||
Burst int `json:"burst"`
|
||||
// SharedSize amount of shared memory for the zone
|
||||
SharedSize int `json:"sharedSize"`
|
||||
}
|
||||
|
||||
// Equal tests for equality between two Zone types
|
||||
func (z1 *Zone) Equal(z2 *Zone) bool {
|
||||
if z1 == z2 {
|
||||
return true
|
||||
}
|
||||
if z1 == nil || z2 == nil {
|
||||
return false
|
||||
}
|
||||
if z1.Name != z2.Name {
|
||||
return false
|
||||
}
|
||||
if z1.Limit != z2.Limit {
|
||||
return false
|
||||
}
|
||||
if z1.Burst != z2.Burst {
|
||||
return false
|
||||
}
|
||||
if z1.SharedSize != z2.SharedSize {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
type ratelimit struct {
|
||||
backendResolver resolver.DefaultBackend
|
||||
}
|
||||
|
||||
// NewParser creates a new ratelimit annotation parser
|
||||
func NewParser(br resolver.DefaultBackend) parser.IngressAnnotation {
|
||||
return ratelimit{br}
|
||||
}
|
||||
|
||||
// ParseAnnotations parses the annotations contained in the ingress
|
||||
// rule used to rewrite the defined paths
|
||||
func (a ratelimit) Parse(ing *extensions.Ingress) (interface{}, error) {
|
||||
defBackend := a.backendResolver.GetDefaultBackend()
|
||||
lr, err := parser.GetIntAnnotation(limitRATE, ing)
|
||||
if err != nil {
|
||||
lr = defBackend.LimitRate
|
||||
}
|
||||
lra, err := parser.GetIntAnnotation(limitRATEAFTER, ing)
|
||||
if err != nil {
|
||||
lra = defBackend.LimitRateAfter
|
||||
}
|
||||
|
||||
rpm, _ := parser.GetIntAnnotation(limitRPM, ing)
|
||||
rps, _ := parser.GetIntAnnotation(limitRPS, ing)
|
||||
conn, _ := parser.GetIntAnnotation(limitIP, ing)
|
||||
|
||||
val, _ := parser.GetStringAnnotation(limitWhitelist, ing)
|
||||
|
||||
cidrs, err := parseCIDRs(val)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if rpm == 0 && rps == 0 && conn == 0 {
|
||||
return &RateLimit{
|
||||
Connections: Zone{},
|
||||
RPS: Zone{},
|
||||
RPM: Zone{},
|
||||
LimitRate: lr,
|
||||
LimitRateAfter: lra,
|
||||
}, nil
|
||||
}
|
||||
|
||||
zoneName := fmt.Sprintf("%v_%v", ing.GetNamespace(), ing.GetName())
|
||||
|
||||
return &RateLimit{
|
||||
Connections: Zone{
|
||||
Name: fmt.Sprintf("%v_conn", zoneName),
|
||||
Limit: conn,
|
||||
Burst: conn * defBurst,
|
||||
SharedSize: defSharedSize,
|
||||
},
|
||||
RPS: Zone{
|
||||
Name: fmt.Sprintf("%v_rps", zoneName),
|
||||
Limit: rps,
|
||||
Burst: rps * defBurst,
|
||||
SharedSize: defSharedSize,
|
||||
},
|
||||
RPM: Zone{
|
||||
Name: fmt.Sprintf("%v_rpm", zoneName),
|
||||
Limit: rpm,
|
||||
Burst: rpm * defBurst,
|
||||
SharedSize: defSharedSize,
|
||||
},
|
||||
LimitRate: lr,
|
||||
LimitRateAfter: lra,
|
||||
Name: zoneName,
|
||||
ID: encode(zoneName),
|
||||
Whitelist: cidrs,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func parseCIDRs(s string) ([]string, error) {
|
||||
if s == "" {
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
values := strings.Split(s, ",")
|
||||
|
||||
ipnets, ips, err := net.ParseIPNets(values...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cidrs := []string{}
|
||||
for k := range ipnets {
|
||||
cidrs = append(cidrs, k)
|
||||
}
|
||||
|
||||
for k := range ips {
|
||||
cidrs = append(cidrs, k)
|
||||
}
|
||||
|
||||
sort.Strings(cidrs)
|
||||
|
||||
return cidrs, nil
|
||||
}
|
||||
|
||||
func encode(s string) string {
|
||||
str := base64.URLEncoding.EncodeToString([]byte(s))
|
||||
return strings.Replace(str, "=", "", -1)
|
||||
}
|
||||
129
pkg/ingress/annotations/ratelimit/main_test.go
Normal file
129
pkg/ingress/annotations/ratelimit/main_test.go
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
/*
|
||||
Copyright 2015 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 ratelimit
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
api "k8s.io/api/core/v1"
|
||||
extensions "k8s.io/api/extensions/v1beta1"
|
||||
meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
"k8s.io/ingress-nginx/pkg/ingress/defaults"
|
||||
)
|
||||
|
||||
func buildIngress() *extensions.Ingress {
|
||||
defaultBackend := extensions.IngressBackend{
|
||||
ServiceName: "default-backend",
|
||||
ServicePort: intstr.FromInt(80),
|
||||
}
|
||||
|
||||
return &extensions.Ingress{
|
||||
ObjectMeta: meta_v1.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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type mockBackend struct {
|
||||
}
|
||||
|
||||
func (m mockBackend) GetDefaultBackend() defaults.Backend {
|
||||
return defaults.Backend{
|
||||
LimitRateAfter: 0,
|
||||
LimitRate: 0,
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithoutAnnotations(t *testing.T) {
|
||||
ing := buildIngress()
|
||||
_, err := NewParser(mockBackend{}).Parse(ing)
|
||||
if err != nil {
|
||||
t.Error("unexpected error with ingress without annotations")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBadRateLimiting(t *testing.T) {
|
||||
ing := buildIngress()
|
||||
|
||||
data := map[string]string{}
|
||||
data[limitIP] = "0"
|
||||
data[limitRPS] = "0"
|
||||
data[limitRPM] = "0"
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
_, err := NewParser(mockBackend{}).Parse(ing)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error with invalid limits (0)")
|
||||
}
|
||||
|
||||
data = map[string]string{}
|
||||
data[limitIP] = "5"
|
||||
data[limitRPS] = "100"
|
||||
data[limitRPM] = "10"
|
||||
data[limitRATEAFTER] = "100"
|
||||
data[limitRATE] = "10"
|
||||
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
i, err := NewParser(mockBackend{}).Parse(ing)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
rateLimit, ok := i.(*RateLimit)
|
||||
if !ok {
|
||||
t.Errorf("expected a RateLimit type")
|
||||
}
|
||||
if rateLimit.Connections.Limit != 5 {
|
||||
t.Errorf("expected 5 in limit by ip but %v was returend", rateLimit.Connections)
|
||||
}
|
||||
if rateLimit.RPS.Limit != 100 {
|
||||
t.Errorf("expected 100 in limit by rps but %v was returend", rateLimit.RPS)
|
||||
}
|
||||
if rateLimit.RPM.Limit != 10 {
|
||||
t.Errorf("expected 10 in limit by rpm but %v was returend", rateLimit.RPM)
|
||||
}
|
||||
if rateLimit.LimitRateAfter != 100 {
|
||||
t.Errorf("expected 100 in limit by limitrateafter but %v was returend", rateLimit.LimitRateAfter)
|
||||
}
|
||||
if rateLimit.LimitRate != 10 {
|
||||
t.Errorf("expected 10 in limit by limitrate but %v was returend", rateLimit.LimitRate)
|
||||
}
|
||||
}
|
||||
131
pkg/ingress/annotations/redirect/redirect.go
Normal file
131
pkg/ingress/annotations/redirect/redirect.go
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
/*
|
||||
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 redirect
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
extensions "k8s.io/api/extensions/v1beta1"
|
||||
|
||||
"k8s.io/ingress-nginx/pkg/ingress/annotations/parser"
|
||||
"k8s.io/ingress-nginx/pkg/ingress/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
permanent = "ingress.kubernetes.io/permanent-redirect"
|
||||
temporal = "ingress.kubernetes.io/temporal-redirect"
|
||||
www = "ingress.kubernetes.io/from-to-www-redirect"
|
||||
)
|
||||
|
||||
// Redirect returns the redirect configuration for an Ingress rule
|
||||
type Redirect struct {
|
||||
URL string `json:"url"`
|
||||
Code int `json:"code"`
|
||||
FromToWWW bool `json:"fromToWWW"`
|
||||
}
|
||||
|
||||
type redirect struct{}
|
||||
|
||||
// NewParser creates a new redirect annotation parser
|
||||
func NewParser() parser.IngressAnnotation {
|
||||
return redirect{}
|
||||
}
|
||||
|
||||
// Parse parses the annotations contained in the ingress
|
||||
// rule used to create a redirect in the paths defined in the rule.
|
||||
// If the Ingress containes both annotations the execution order is
|
||||
// temporal and then permanent
|
||||
func (a redirect) Parse(ing *extensions.Ingress) (interface{}, error) {
|
||||
r3w, _ := parser.GetBoolAnnotation(www, ing)
|
||||
|
||||
tr, err := parser.GetStringAnnotation(temporal, ing)
|
||||
if err != nil && !errors.IsMissingAnnotations(err) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if tr != "" {
|
||||
if err := isValidURL(tr); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Redirect{
|
||||
URL: tr,
|
||||
Code: http.StatusFound,
|
||||
FromToWWW: r3w,
|
||||
}, nil
|
||||
}
|
||||
|
||||
pr, err := parser.GetStringAnnotation(permanent, ing)
|
||||
if err != nil && !errors.IsMissingAnnotations(err) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if pr != "" {
|
||||
if err := isValidURL(pr); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Redirect{
|
||||
URL: pr,
|
||||
Code: http.StatusMovedPermanently,
|
||||
FromToWWW: r3w,
|
||||
}, nil
|
||||
}
|
||||
|
||||
if r3w {
|
||||
return &Redirect{
|
||||
FromToWWW: r3w,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return nil, errors.ErrMissingAnnotations
|
||||
}
|
||||
|
||||
// Equal tests for equality between two Redirect types
|
||||
func (r1 *Redirect) Equal(r2 *Redirect) bool {
|
||||
if r1 == r2 {
|
||||
return true
|
||||
}
|
||||
if r1 == nil || r2 == nil {
|
||||
return false
|
||||
}
|
||||
if r1.URL != r2.URL {
|
||||
return false
|
||||
}
|
||||
if r1.Code != r2.Code {
|
||||
return false
|
||||
}
|
||||
if r1.FromToWWW != r2.FromToWWW {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func isValidURL(s string) error {
|
||||
u, err := url.Parse(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(u.Scheme, "http") {
|
||||
return errors.Errorf("only http and https are valid protocols (%v)", u.Scheme)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
114
pkg/ingress/annotations/rewrite/main.go
Normal file
114
pkg/ingress/annotations/rewrite/main.go
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
/*
|
||||
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 rewrite
|
||||
|
||||
import (
|
||||
extensions "k8s.io/api/extensions/v1beta1"
|
||||
|
||||
"k8s.io/ingress-nginx/pkg/ingress/annotations/parser"
|
||||
"k8s.io/ingress-nginx/pkg/ingress/resolver"
|
||||
)
|
||||
|
||||
const (
|
||||
rewriteTo = "ingress.kubernetes.io/rewrite-target"
|
||||
addBaseURL = "ingress.kubernetes.io/add-base-url"
|
||||
baseURLScheme = "ingress.kubernetes.io/base-url-scheme"
|
||||
sslRedirect = "ingress.kubernetes.io/ssl-redirect"
|
||||
forceSSLRedirect = "ingress.kubernetes.io/force-ssl-redirect"
|
||||
appRoot = "ingress.kubernetes.io/app-root"
|
||||
)
|
||||
|
||||
// Redirect describes the per location redirect config
|
||||
type Redirect struct {
|
||||
// Target URI where the traffic must be redirected
|
||||
Target string `json:"target"`
|
||||
// AddBaseURL indicates if is required to add a base tag in the head
|
||||
// of the responses from the upstream servers
|
||||
AddBaseURL bool `json:"addBaseUrl"`
|
||||
// BaseURLScheme override for the scheme passed to the base tag
|
||||
BaseURLScheme string `json:"baseUrlScheme"`
|
||||
// SSLRedirect indicates if the location section is accessible SSL only
|
||||
SSLRedirect bool `json:"sslRedirect"`
|
||||
// ForceSSLRedirect indicates if the location section is accessible SSL only
|
||||
ForceSSLRedirect bool `json:"forceSSLRedirect"`
|
||||
// AppRoot defines the Application Root that the Controller must redirect if it's not in '/' context
|
||||
AppRoot string `json:"appRoot"`
|
||||
}
|
||||
|
||||
// Equal tests for equality between two Redirect types
|
||||
func (r1 *Redirect) Equal(r2 *Redirect) bool {
|
||||
if r1 == r2 {
|
||||
return true
|
||||
}
|
||||
if r1 == nil || r2 == nil {
|
||||
return false
|
||||
}
|
||||
if r1.Target != r2.Target {
|
||||
return false
|
||||
}
|
||||
if r1.AddBaseURL != r2.AddBaseURL {
|
||||
return false
|
||||
}
|
||||
if r1.BaseURLScheme != r2.BaseURLScheme {
|
||||
return false
|
||||
}
|
||||
if r1.SSLRedirect != r2.SSLRedirect {
|
||||
return false
|
||||
}
|
||||
if r1.ForceSSLRedirect != r2.ForceSSLRedirect {
|
||||
return false
|
||||
}
|
||||
if r1.AppRoot != r2.AppRoot {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
type rewrite struct {
|
||||
backendResolver resolver.DefaultBackend
|
||||
}
|
||||
|
||||
// NewParser creates a new reqrite annotation parser
|
||||
func NewParser(br resolver.DefaultBackend) parser.IngressAnnotation {
|
||||
return rewrite{br}
|
||||
}
|
||||
|
||||
// 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, _ := parser.GetStringAnnotation(rewriteTo, ing)
|
||||
sslRe, err := parser.GetBoolAnnotation(sslRedirect, ing)
|
||||
if err != nil {
|
||||
sslRe = a.backendResolver.GetDefaultBackend().SSLRedirect
|
||||
}
|
||||
fSslRe, err := parser.GetBoolAnnotation(forceSSLRedirect, ing)
|
||||
if err != nil {
|
||||
fSslRe = a.backendResolver.GetDefaultBackend().ForceSSLRedirect
|
||||
}
|
||||
abu, _ := parser.GetBoolAnnotation(addBaseURL, ing)
|
||||
bus, _ := parser.GetStringAnnotation(baseURLScheme, ing)
|
||||
ar, _ := parser.GetStringAnnotation(appRoot, ing)
|
||||
return &Redirect{
|
||||
Target: rt,
|
||||
AddBaseURL: abu,
|
||||
BaseURLScheme: bus,
|
||||
SSLRedirect: sslRe,
|
||||
ForceSSLRedirect: fSslRe,
|
||||
AppRoot: ar,
|
||||
}, nil
|
||||
}
|
||||
178
pkg/ingress/annotations/rewrite/main_test.go
Normal file
178
pkg/ingress/annotations/rewrite/main_test.go
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
/*
|
||||
Copyright 2015 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 rewrite
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
api "k8s.io/api/core/v1"
|
||||
extensions "k8s.io/api/extensions/v1beta1"
|
||||
meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
|
||||
"k8s.io/ingress-nginx/pkg/ingress/defaults"
|
||||
)
|
||||
|
||||
const (
|
||||
defRoute = "/demo"
|
||||
)
|
||||
|
||||
func buildIngress() *extensions.Ingress {
|
||||
defaultBackend := extensions.IngressBackend{
|
||||
ServiceName: "default-backend",
|
||||
ServicePort: intstr.FromInt(80),
|
||||
}
|
||||
|
||||
return &extensions.Ingress{
|
||||
ObjectMeta: meta_v1.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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type mockBackend struct {
|
||||
redirect bool
|
||||
}
|
||||
|
||||
func (m mockBackend) GetDefaultBackend() defaults.Backend {
|
||||
return defaults.Backend{SSLRedirect: m.redirect}
|
||||
}
|
||||
|
||||
func TestWithoutAnnotations(t *testing.T) {
|
||||
ing := buildIngress()
|
||||
_, err := NewParser(mockBackend{}).Parse(ing)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error with ingress without annotations: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRedirect(t *testing.T) {
|
||||
ing := buildIngress()
|
||||
|
||||
data := map[string]string{}
|
||||
data[rewriteTo] = defRoute
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
i, err := NewParser(mockBackend{}).Parse(ing)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error with ingress: %v", err)
|
||||
}
|
||||
redirect, ok := i.(*Redirect)
|
||||
if !ok {
|
||||
t.Errorf("expected a Redirect type")
|
||||
}
|
||||
if redirect.Target != defRoute {
|
||||
t.Errorf("Expected %v as redirect but returned %s", defRoute, redirect.Target)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSSLRedirect(t *testing.T) {
|
||||
ing := buildIngress()
|
||||
|
||||
data := map[string]string{}
|
||||
data[rewriteTo] = defRoute
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
i, _ := NewParser(mockBackend{true}).Parse(ing)
|
||||
redirect, ok := i.(*Redirect)
|
||||
if !ok {
|
||||
t.Errorf("expected a Redirect type")
|
||||
}
|
||||
if !redirect.SSLRedirect {
|
||||
t.Errorf("Expected true but returned false")
|
||||
}
|
||||
|
||||
data[sslRedirect] = "false"
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
i, _ = NewParser(mockBackend{false}).Parse(ing)
|
||||
redirect, ok = i.(*Redirect)
|
||||
if !ok {
|
||||
t.Errorf("expected a Redirect type")
|
||||
}
|
||||
if redirect.SSLRedirect {
|
||||
t.Errorf("Expected false but returned true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestForceSSLRedirect(t *testing.T) {
|
||||
ing := buildIngress()
|
||||
|
||||
data := map[string]string{}
|
||||
data[rewriteTo] = defRoute
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
i, _ := NewParser(mockBackend{true}).Parse(ing)
|
||||
redirect, ok := i.(*Redirect)
|
||||
if !ok {
|
||||
t.Errorf("expected a Redirect type")
|
||||
}
|
||||
if redirect.ForceSSLRedirect {
|
||||
t.Errorf("Expected false but returned true")
|
||||
}
|
||||
|
||||
data[forceSSLRedirect] = "true"
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
i, _ = NewParser(mockBackend{false}).Parse(ing)
|
||||
redirect, ok = i.(*Redirect)
|
||||
if !ok {
|
||||
t.Errorf("expected a Redirect type")
|
||||
}
|
||||
if !redirect.ForceSSLRedirect {
|
||||
t.Errorf("Expected true but returned false")
|
||||
}
|
||||
}
|
||||
func TestAppRoot(t *testing.T) {
|
||||
ing := buildIngress()
|
||||
|
||||
data := map[string]string{}
|
||||
data[appRoot] = "/app1"
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
i, _ := NewParser(mockBackend{true}).Parse(ing)
|
||||
redirect, ok := i.(*Redirect)
|
||||
if !ok {
|
||||
t.Errorf("expected a App Context")
|
||||
}
|
||||
if redirect.AppRoot != "/app1" {
|
||||
t.Errorf("Unexpected value got in AppRoot")
|
||||
}
|
||||
|
||||
}
|
||||
78
pkg/ingress/annotations/secureupstream/main.go
Normal file
78
pkg/ingress/annotations/secureupstream/main.go
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
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 secureupstream
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
extensions "k8s.io/api/extensions/v1beta1"
|
||||
|
||||
"k8s.io/ingress-nginx/pkg/ingress/annotations/parser"
|
||||
"k8s.io/ingress-nginx/pkg/ingress/resolver"
|
||||
)
|
||||
|
||||
const (
|
||||
secureUpstream = "ingress.kubernetes.io/secure-backends"
|
||||
secureVerifyCASecret = "ingress.kubernetes.io/secure-verify-ca-secret"
|
||||
)
|
||||
|
||||
// Secure describes SSL backend configuration
|
||||
type Secure struct {
|
||||
Secure bool `json:"secure"`
|
||||
CACert resolver.AuthSSLCert `json:"caCert"`
|
||||
}
|
||||
|
||||
type su struct {
|
||||
certResolver resolver.AuthCertificate
|
||||
}
|
||||
|
||||
// NewParser creates a new secure upstream annotation parser
|
||||
func NewParser(resolver resolver.AuthCertificate) parser.IngressAnnotation {
|
||||
return su{
|
||||
certResolver: resolver,
|
||||
}
|
||||
}
|
||||
|
||||
// Parse parses the annotations contained in the ingress
|
||||
// rule used to indicate if the upstream servers should use SSL
|
||||
func (a su) Parse(ing *extensions.Ingress) (interface{}, error) {
|
||||
s, _ := parser.GetBoolAnnotation(secureUpstream, ing)
|
||||
ca, _ := parser.GetStringAnnotation(secureVerifyCASecret, ing)
|
||||
secure := &Secure{
|
||||
Secure: s,
|
||||
CACert: resolver.AuthSSLCert{},
|
||||
}
|
||||
if !s && ca != "" {
|
||||
return secure,
|
||||
errors.Errorf("trying to use CA from secret %v/%v on a non secure backend", ing.Namespace, ca)
|
||||
}
|
||||
if ca == "" {
|
||||
return secure, nil
|
||||
}
|
||||
caCert, err := a.certResolver.GetAuthCertificate(fmt.Sprintf("%v/%v", ing.Namespace, ca))
|
||||
if err != nil {
|
||||
return secure, errors.Wrap(err, "error obtaining certificate")
|
||||
}
|
||||
if caCert == nil {
|
||||
return secure, nil
|
||||
}
|
||||
return &Secure{
|
||||
Secure: s,
|
||||
CACert: *caCert,
|
||||
}, nil
|
||||
}
|
||||
121
pkg/ingress/annotations/secureupstream/main_test.go
Normal file
121
pkg/ingress/annotations/secureupstream/main_test.go
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
/*
|
||||
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 secureupstream
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
api "k8s.io/api/core/v1"
|
||||
extensions "k8s.io/api/extensions/v1beta1"
|
||||
meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
"fmt"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
"k8s.io/ingress-nginx/pkg/ingress/resolver"
|
||||
)
|
||||
|
||||
func buildIngress() *extensions.Ingress {
|
||||
defaultBackend := extensions.IngressBackend{
|
||||
ServiceName: "default-backend",
|
||||
ServicePort: intstr.FromInt(80),
|
||||
}
|
||||
|
||||
return &extensions.Ingress{
|
||||
ObjectMeta: meta_v1.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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type mockCfg struct {
|
||||
certs map[string]resolver.AuthSSLCert
|
||||
}
|
||||
|
||||
func (cfg mockCfg) GetAuthCertificate(secret string) (*resolver.AuthSSLCert, error) {
|
||||
if cert, ok := cfg.certs[secret]; ok {
|
||||
return &cert, nil
|
||||
}
|
||||
return nil, fmt.Errorf("secret not found: %v", secret)
|
||||
}
|
||||
|
||||
func TestAnnotations(t *testing.T) {
|
||||
ing := buildIngress()
|
||||
data := map[string]string{}
|
||||
data[secureUpstream] = "true"
|
||||
data[secureVerifyCASecret] = "secure-verify-ca"
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
_, err := NewParser(mockCfg{
|
||||
certs: map[string]resolver.AuthSSLCert{
|
||||
"default/secure-verify-ca": {},
|
||||
},
|
||||
}).Parse(ing)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error on ingress: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSecretNotFound(t *testing.T) {
|
||||
ing := buildIngress()
|
||||
data := map[string]string{}
|
||||
data[secureUpstream] = "true"
|
||||
data[secureVerifyCASecret] = "secure-verify-ca"
|
||||
ing.SetAnnotations(data)
|
||||
_, err := NewParser(mockCfg{}).Parse(ing)
|
||||
if err == nil {
|
||||
t.Error("Expected secret not found error on ingress")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSecretOnNonSecure(t *testing.T) {
|
||||
ing := buildIngress()
|
||||
data := map[string]string{}
|
||||
data[secureUpstream] = "false"
|
||||
data[secureVerifyCASecret] = "secure-verify-ca"
|
||||
ing.SetAnnotations(data)
|
||||
_, err := NewParser(mockCfg{
|
||||
certs: map[string]resolver.AuthSSLCert{
|
||||
"default/secure-verify-ca": {},
|
||||
},
|
||||
}).Parse(ing)
|
||||
if err == nil {
|
||||
t.Error("Expected CA secret on non secure backend error on ingress")
|
||||
}
|
||||
}
|
||||
42
pkg/ingress/annotations/serversnippet/main.go
Normal file
42
pkg/ingress/annotations/serversnippet/main.go
Normal 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 serversnippet
|
||||
|
||||
import (
|
||||
extensions "k8s.io/api/extensions/v1beta1"
|
||||
|
||||
"k8s.io/ingress-nginx/pkg/ingress/annotations/parser"
|
||||
)
|
||||
|
||||
const (
|
||||
annotation = "ingress.kubernetes.io/server-snippet"
|
||||
)
|
||||
|
||||
type serverSnippet struct {
|
||||
}
|
||||
|
||||
// NewParser creates a new server snippet annotation parser
|
||||
func NewParser() parser.IngressAnnotation {
|
||||
return serverSnippet{}
|
||||
}
|
||||
|
||||
// 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 serverSnippet) Parse(ing *extensions.Ingress) (interface{}, error) {
|
||||
return parser.GetStringAnnotation(annotation, ing)
|
||||
}
|
||||
58
pkg/ingress/annotations/serversnippet/main_test.go
Normal file
58
pkg/ingress/annotations/serversnippet/main_test.go
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
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 serversnippet
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
api "k8s.io/api/core/v1"
|
||||
extensions "k8s.io/api/extensions/v1beta1"
|
||||
meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
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: meta_v1.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
38
pkg/ingress/annotations/serviceupstream/main.go
Normal file
38
pkg/ingress/annotations/serviceupstream/main.go
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
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 serviceupstream
|
||||
|
||||
import (
|
||||
extensions "k8s.io/api/extensions/v1beta1"
|
||||
"k8s.io/ingress-nginx/pkg/ingress/annotations/parser"
|
||||
)
|
||||
|
||||
const (
|
||||
annotationServiceUpstream = "ingress.kubernetes.io/service-upstream"
|
||||
)
|
||||
|
||||
type serviceUpstream struct {
|
||||
}
|
||||
|
||||
// NewParser creates a new serviceUpstream annotation parser
|
||||
func NewParser() parser.IngressAnnotation {
|
||||
return serviceUpstream{}
|
||||
}
|
||||
|
||||
func (s serviceUpstream) Parse(ing *extensions.Ingress) (interface{}, error) {
|
||||
return parser.GetBoolAnnotation(annotationServiceUpstream, ing)
|
||||
}
|
||||
112
pkg/ingress/annotations/serviceupstream/main_test.go
Normal file
112
pkg/ingress/annotations/serviceupstream/main_test.go
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
/*
|
||||
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 serviceupstream
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
api "k8s.io/api/core/v1"
|
||||
extensions "k8s.io/api/extensions/v1beta1"
|
||||
meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
)
|
||||
|
||||
func buildIngress() *extensions.Ingress {
|
||||
defaultBackend := extensions.IngressBackend{
|
||||
ServiceName: "default-backend",
|
||||
ServicePort: intstr.FromInt(80),
|
||||
}
|
||||
|
||||
return &extensions.Ingress{
|
||||
ObjectMeta: meta_v1.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 TestIngressAnnotationServiceUpstreamEnabled(t *testing.T) {
|
||||
ing := buildIngress()
|
||||
|
||||
data := map[string]string{}
|
||||
data[annotationServiceUpstream] = "true"
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
val, _ := NewParser().Parse(ing)
|
||||
enabled, ok := val.(bool)
|
||||
if !ok {
|
||||
t.Errorf("expected a bool type")
|
||||
}
|
||||
|
||||
if !enabled {
|
||||
t.Errorf("expected annotation value to be true, got false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIngressAnnotationServiceUpstreamSetFalse(t *testing.T) {
|
||||
ing := buildIngress()
|
||||
|
||||
// Test with explicitly set to false
|
||||
data := map[string]string{}
|
||||
data[annotationServiceUpstream] = "false"
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
val, _ := NewParser().Parse(ing)
|
||||
enabled, ok := val.(bool)
|
||||
if !ok {
|
||||
t.Errorf("expected a bool type")
|
||||
}
|
||||
|
||||
if enabled {
|
||||
t.Errorf("expected annotation value to be false, got true")
|
||||
}
|
||||
|
||||
// Test with no annotation specified, should default to false
|
||||
data = map[string]string{}
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
val, _ = NewParser().Parse(ing)
|
||||
enabled, ok = val.(bool)
|
||||
if !ok {
|
||||
t.Errorf("expected a bool type")
|
||||
}
|
||||
|
||||
if enabled {
|
||||
t.Errorf("expected annotation value to be false, got true")
|
||||
}
|
||||
}
|
||||
115
pkg/ingress/annotations/sessionaffinity/main.go
Normal file
115
pkg/ingress/annotations/sessionaffinity/main.go
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
/*
|
||||
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"
|
||||
|
||||
extensions "k8s.io/api/extensions/v1beta1"
|
||||
|
||||
"k8s.io/ingress-nginx/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) {
|
||||
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
|
||||
|
||||
}
|
||||
89
pkg/ingress/annotations/sessionaffinity/main_test.go
Normal file
89
pkg/ingress/annotations/sessionaffinity/main_test.go
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
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"
|
||||
|
||||
api "k8s.io/api/core/v1"
|
||||
extensions "k8s.io/api/extensions/v1beta1"
|
||||
meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
)
|
||||
|
||||
func buildIngress() *extensions.Ingress {
|
||||
defaultBackend := extensions.IngressBackend{
|
||||
ServiceName: "default-backend",
|
||||
ServicePort: intstr.FromInt(80),
|
||||
}
|
||||
|
||||
return &extensions.Ingress{
|
||||
ObjectMeta: meta_v1.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)
|
||||
}
|
||||
}
|
||||
42
pkg/ingress/annotations/snippet/main.go
Normal file
42
pkg/ingress/annotations/snippet/main.go
Normal 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 (
|
||||
extensions "k8s.io/api/extensions/v1beta1"
|
||||
|
||||
"k8s.io/ingress-nginx/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)
|
||||
}
|
||||
58
pkg/ingress/annotations/snippet/main_test.go
Normal file
58
pkg/ingress/annotations/snippet/main_test.go
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
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"
|
||||
|
||||
api "k8s.io/api/core/v1"
|
||||
extensions "k8s.io/api/extensions/v1beta1"
|
||||
meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
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: meta_v1.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
46
pkg/ingress/annotations/sslpassthrough/main.go
Normal file
46
pkg/ingress/annotations/sslpassthrough/main.go
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
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 sslpassthrough
|
||||
|
||||
import (
|
||||
extensions "k8s.io/api/extensions/v1beta1"
|
||||
|
||||
"k8s.io/ingress-nginx/pkg/ingress/annotations/parser"
|
||||
ing_errors "k8s.io/ingress-nginx/pkg/ingress/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
passthrough = "ingress.kubernetes.io/ssl-passthrough"
|
||||
)
|
||||
|
||||
type sslpt struct {
|
||||
}
|
||||
|
||||
// NewParser creates a new SSL passthrough annotation parser
|
||||
func NewParser() parser.IngressAnnotation {
|
||||
return sslpt{}
|
||||
}
|
||||
|
||||
// ParseAnnotations parses the annotations contained in the ingress
|
||||
// rule used to indicate if is required to configure
|
||||
func (a sslpt) Parse(ing *extensions.Ingress) (interface{}, error) {
|
||||
if ing.GetAnnotations() == nil {
|
||||
return false, ing_errors.ErrMissingAnnotations
|
||||
}
|
||||
|
||||
return parser.GetBoolAnnotation(passthrough, ing)
|
||||
}
|
||||
78
pkg/ingress/annotations/sslpassthrough/main_test.go
Normal file
78
pkg/ingress/annotations/sslpassthrough/main_test.go
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
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 sslpassthrough
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
api "k8s.io/api/core/v1"
|
||||
extensions "k8s.io/api/extensions/v1beta1"
|
||||
meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
)
|
||||
|
||||
func buildIngress() *extensions.Ingress {
|
||||
return &extensions.Ingress{
|
||||
ObjectMeta: meta_v1.ObjectMeta{
|
||||
Name: "foo",
|
||||
Namespace: api.NamespaceDefault,
|
||||
},
|
||||
Spec: extensions.IngressSpec{
|
||||
Backend: &extensions.IngressBackend{
|
||||
ServiceName: "default-backend",
|
||||
ServicePort: intstr.FromInt(80),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseAnnotations(t *testing.T) {
|
||||
ing := buildIngress()
|
||||
|
||||
_, err := NewParser().Parse(ing)
|
||||
if err == nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
data := map[string]string{}
|
||||
data[passthrough] = "true"
|
||||
ing.SetAnnotations(data)
|
||||
// test ingress using the annotation without a TLS section
|
||||
_, err = NewParser().Parse(ing)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error parsing ingress with sslpassthrough")
|
||||
}
|
||||
|
||||
// test with a valid host
|
||||
ing.Spec.TLS = []extensions.IngressTLS{
|
||||
{
|
||||
Hosts: []string{"foo.bar.com"},
|
||||
},
|
||||
}
|
||||
i, err := NewParser().Parse(ing)
|
||||
if err != nil {
|
||||
t.Errorf("expected error parsing ingress with sslpassthrough")
|
||||
}
|
||||
val, ok := i.(bool)
|
||||
if !ok {
|
||||
t.Errorf("expected a bool type")
|
||||
}
|
||||
if !val {
|
||||
t.Errorf("expected true but false returned")
|
||||
}
|
||||
}
|
||||
42
pkg/ingress/annotations/upstreamvhost/main.go
Normal file
42
pkg/ingress/annotations/upstreamvhost/main.go
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
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 upstreamvhost
|
||||
|
||||
import (
|
||||
extensions "k8s.io/api/extensions/v1beta1"
|
||||
|
||||
"k8s.io/ingress-nginx/pkg/ingress/annotations/parser"
|
||||
)
|
||||
|
||||
const (
|
||||
annotation = "ingress.kubernetes.io/upstream-vhost"
|
||||
)
|
||||
|
||||
type upstreamVhost struct {
|
||||
}
|
||||
|
||||
// NewParser creates a new upstream VHost annotation parser
|
||||
func NewParser() parser.IngressAnnotation {
|
||||
return upstreamVhost{}
|
||||
}
|
||||
|
||||
// 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 upstreamVhost) Parse(ing *extensions.Ingress) (interface{}, error) {
|
||||
return parser.GetStringAnnotation(annotation, ing)
|
||||
}
|
||||
42
pkg/ingress/annotations/vtsfilterkey/main.go
Normal file
42
pkg/ingress/annotations/vtsfilterkey/main.go
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
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 vtsfilterkey
|
||||
|
||||
import (
|
||||
extensions "k8s.io/api/extensions/v1beta1"
|
||||
|
||||
"k8s.io/ingress-nginx/pkg/ingress/annotations/parser"
|
||||
)
|
||||
|
||||
const (
|
||||
annotation = "ingress.kubernetes.io/vts-filter-key"
|
||||
)
|
||||
|
||||
type vtsFilterKey struct {
|
||||
}
|
||||
|
||||
// NewParser creates a new vts filter key annotation parser
|
||||
func NewParser() parser.IngressAnnotation {
|
||||
return vtsFilterKey{}
|
||||
}
|
||||
|
||||
// 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 vtsFilterKey) Parse(ing *extensions.Ingress) (interface{}, error) {
|
||||
return parser.GetStringAnnotation(annotation, ing)
|
||||
}
|
||||
191
pkg/ingress/controller/annotations.go
Normal file
191
pkg/ingress/controller/annotations.go
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
/*
|
||||
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 controller
|
||||
|
||||
import (
|
||||
"github.com/golang/glog"
|
||||
|
||||
extensions "k8s.io/api/extensions/v1beta1"
|
||||
"k8s.io/ingress-nginx/pkg/ingress/annotations/alias"
|
||||
"k8s.io/ingress-nginx/pkg/ingress/annotations/auth"
|
||||
"k8s.io/ingress-nginx/pkg/ingress/annotations/authreq"
|
||||
"k8s.io/ingress-nginx/pkg/ingress/annotations/authtls"
|
||||
"k8s.io/ingress-nginx/pkg/ingress/annotations/clientbodybuffersize"
|
||||
"k8s.io/ingress-nginx/pkg/ingress/annotations/cors"
|
||||
"k8s.io/ingress-nginx/pkg/ingress/annotations/defaultbackend"
|
||||
"k8s.io/ingress-nginx/pkg/ingress/annotations/healthcheck"
|
||||
"k8s.io/ingress-nginx/pkg/ingress/annotations/ipwhitelist"
|
||||
"k8s.io/ingress-nginx/pkg/ingress/annotations/parser"
|
||||
"k8s.io/ingress-nginx/pkg/ingress/annotations/portinredirect"
|
||||
"k8s.io/ingress-nginx/pkg/ingress/annotations/proxy"
|
||||
"k8s.io/ingress-nginx/pkg/ingress/annotations/ratelimit"
|
||||
"k8s.io/ingress-nginx/pkg/ingress/annotations/redirect"
|
||||
"k8s.io/ingress-nginx/pkg/ingress/annotations/rewrite"
|
||||
"k8s.io/ingress-nginx/pkg/ingress/annotations/secureupstream"
|
||||
"k8s.io/ingress-nginx/pkg/ingress/annotations/serversnippet"
|
||||
"k8s.io/ingress-nginx/pkg/ingress/annotations/serviceupstream"
|
||||
"k8s.io/ingress-nginx/pkg/ingress/annotations/sessionaffinity"
|
||||
"k8s.io/ingress-nginx/pkg/ingress/annotations/snippet"
|
||||
"k8s.io/ingress-nginx/pkg/ingress/annotations/sslpassthrough"
|
||||
"k8s.io/ingress-nginx/pkg/ingress/annotations/upstreamvhost"
|
||||
"k8s.io/ingress-nginx/pkg/ingress/annotations/vtsfilterkey"
|
||||
"k8s.io/ingress-nginx/pkg/ingress/errors"
|
||||
"k8s.io/ingress-nginx/pkg/ingress/resolver"
|
||||
)
|
||||
|
||||
type extractorConfig interface {
|
||||
resolver.AuthCertificate
|
||||
resolver.DefaultBackend
|
||||
resolver.Secret
|
||||
resolver.Service
|
||||
}
|
||||
|
||||
type annotationExtractor struct {
|
||||
secretResolver resolver.Secret
|
||||
annotations map[string]parser.IngressAnnotation
|
||||
}
|
||||
|
||||
func newAnnotationExtractor(cfg extractorConfig) annotationExtractor {
|
||||
return annotationExtractor{
|
||||
cfg,
|
||||
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(cfg),
|
||||
"Redirect": redirect.NewParser(),
|
||||
"Rewrite": rewrite.NewParser(cfg),
|
||||
"SecureUpstream": secureupstream.NewParser(cfg),
|
||||
"ServiceUpstream": serviceupstream.NewParser(),
|
||||
"SessionAffinity": sessionaffinity.NewParser(),
|
||||
"SSLPassthrough": sslpassthrough.NewParser(),
|
||||
"ConfigurationSnippet": snippet.NewParser(),
|
||||
"Alias": alias.NewParser(),
|
||||
"ClientBodyBufferSize": clientbodybuffersize.NewParser(),
|
||||
"DefaultBackend": defaultbackend.NewParser(cfg),
|
||||
"UpstreamVhost": upstreamvhost.NewParser(),
|
||||
"VtsFilterKey": vtsfilterkey.NewParser(),
|
||||
"ServerSnippet": serversnippet.NewParser(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (e *annotationExtractor) Extract(ing *extensions.Ingress) map[string]interface{} {
|
||||
anns := make(map[string]interface{})
|
||||
for name, annotationParser := range e.annotations {
|
||||
val, err := annotationParser.Parse(ing)
|
||||
glog.V(5).Infof("annotation %v in Ingress %v/%v: %v", name, ing.GetNamespace(), ing.GetName(), val)
|
||||
if err != nil {
|
||||
if errors.IsMissingAnnotations(err) {
|
||||
continue
|
||||
}
|
||||
|
||||
if !errors.IsLocationDenied(err) {
|
||||
continue
|
||||
}
|
||||
|
||||
_, alreadyDenied := anns[DeniedKeyName]
|
||||
if !alreadyDenied {
|
||||
anns[DeniedKeyName] = err
|
||||
glog.Errorf("error reading %v annotation in Ingress %v/%v: %v", name, ing.GetNamespace(), ing.GetName(), err)
|
||||
continue
|
||||
}
|
||||
|
||||
glog.V(5).Infof("error reading %v annotation in Ingress %v/%v: %v", name, ing.GetNamespace(), ing.GetName(), err)
|
||||
}
|
||||
|
||||
if val != nil {
|
||||
anns[name] = val
|
||||
}
|
||||
}
|
||||
|
||||
return anns
|
||||
}
|
||||
|
||||
const (
|
||||
secureUpstream = "SecureUpstream"
|
||||
healthCheck = "HealthCheck"
|
||||
sslPassthrough = "SSLPassthrough"
|
||||
sessionAffinity = "SessionAffinity"
|
||||
serviceUpstream = "ServiceUpstream"
|
||||
serverAlias = "Alias"
|
||||
clientBodyBufferSize = "ClientBodyBufferSize"
|
||||
certificateAuth = "CertificateAuth"
|
||||
serverSnippet = "ServerSnippet"
|
||||
)
|
||||
|
||||
func (e *annotationExtractor) ServiceUpstream(ing *extensions.Ingress) bool {
|
||||
val, _ := e.annotations[serviceUpstream].Parse(ing)
|
||||
return val.(bool)
|
||||
}
|
||||
|
||||
func (e *annotationExtractor) SecureUpstream(ing *extensions.Ingress) *secureupstream.Secure {
|
||||
val, err := e.annotations[secureUpstream].Parse(ing)
|
||||
if err != nil {
|
||||
glog.Errorf("error parsing secure upstream: %v", err)
|
||||
}
|
||||
secure := val.(*secureupstream.Secure)
|
||||
return secure
|
||||
}
|
||||
|
||||
func (e *annotationExtractor) HealthCheck(ing *extensions.Ingress) *healthcheck.Upstream {
|
||||
val, _ := e.annotations[healthCheck].Parse(ing)
|
||||
return val.(*healthcheck.Upstream)
|
||||
}
|
||||
|
||||
func (e *annotationExtractor) SSLPassthrough(ing *extensions.Ingress) bool {
|
||||
val, _ := e.annotations[sslPassthrough].Parse(ing)
|
||||
return val.(bool)
|
||||
}
|
||||
|
||||
func (e *annotationExtractor) Alias(ing *extensions.Ingress) string {
|
||||
val, _ := e.annotations[serverAlias].Parse(ing)
|
||||
return val.(string)
|
||||
}
|
||||
|
||||
func (e *annotationExtractor) ClientBodyBufferSize(ing *extensions.Ingress) string {
|
||||
val, _ := e.annotations[clientBodyBufferSize].Parse(ing)
|
||||
return val.(string)
|
||||
}
|
||||
|
||||
func (e *annotationExtractor) SessionAffinity(ing *extensions.Ingress) *sessionaffinity.AffinityConfig {
|
||||
val, _ := e.annotations[sessionAffinity].Parse(ing)
|
||||
return val.(*sessionaffinity.AffinityConfig)
|
||||
}
|
||||
|
||||
func (e *annotationExtractor) CertificateAuth(ing *extensions.Ingress) *authtls.AuthSSLConfig {
|
||||
val, err := e.annotations[certificateAuth].Parse(ing)
|
||||
if errors.IsMissingAnnotations(err) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
glog.Errorf("error parsing certificate auth: %v", err)
|
||||
}
|
||||
secure := val.(*authtls.AuthSSLConfig)
|
||||
return secure
|
||||
}
|
||||
|
||||
func (e *annotationExtractor) ServerSnippet(ing *extensions.Ingress) string {
|
||||
val, _ := e.annotations[serverSnippet].Parse(ing)
|
||||
return val.(string)
|
||||
}
|
||||
270
pkg/ingress/controller/annotations_test.go
Normal file
270
pkg/ingress/controller/annotations_test.go
Normal file
|
|
@ -0,0 +1,270 @@
|
|||
/*
|
||||
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 controller
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
apiv1 "k8s.io/api/core/v1"
|
||||
extensions "k8s.io/api/extensions/v1beta1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
|
||||
"k8s.io/ingress-nginx/pkg/ingress/defaults"
|
||||
"k8s.io/ingress-nginx/pkg/ingress/resolver"
|
||||
)
|
||||
|
||||
const (
|
||||
annotationSecureUpstream = "ingress.kubernetes.io/secure-backends"
|
||||
annotationSecureVerifyCACert = "ingress.kubernetes.io/secure-verify-ca-secret"
|
||||
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 {
|
||||
MockSecrets map[string]*apiv1.Secret
|
||||
MockServices map[string]*apiv1.Service
|
||||
}
|
||||
|
||||
func (m mockCfg) GetDefaultBackend() defaults.Backend {
|
||||
return defaults.Backend{}
|
||||
}
|
||||
|
||||
func (m mockCfg) GetSecret(name string) (*apiv1.Secret, error) {
|
||||
return m.MockSecrets[name], nil
|
||||
}
|
||||
|
||||
func (m mockCfg) GetService(name string) (*apiv1.Service, error) {
|
||||
return m.MockServices[name], nil
|
||||
}
|
||||
|
||||
func (m mockCfg) GetAuthCertificate(name string) (*resolver.AuthSSLCert, error) {
|
||||
if secret, _ := m.GetSecret(name); secret != nil {
|
||||
return &resolver.AuthSSLCert{
|
||||
Secret: name,
|
||||
CAFileName: "/opt/ca.pem",
|
||||
PemSHA: "123",
|
||||
}, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func TestAnnotationExtractor(t *testing.T) {
|
||||
ec := newAnnotationExtractor(mockCfg{})
|
||||
ing := buildIngress()
|
||||
|
||||
m := ec.Extract(ing)
|
||||
// the map at least should contains HealthCheck and Proxy information (defaults)
|
||||
if _, ok := m["HealthCheck"]; !ok {
|
||||
t.Error("expected HealthCheck annotation")
|
||||
}
|
||||
if _, ok := m["Proxy"]; !ok {
|
||||
t.Error("expected Proxy annotation")
|
||||
}
|
||||
}
|
||||
|
||||
func buildIngress() *extensions.Ingress {
|
||||
defaultBackend := extensions.IngressBackend{
|
||||
ServiceName: "default-backend",
|
||||
ServicePort: intstr.FromInt(80),
|
||||
}
|
||||
|
||||
return &extensions.Ingress{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "foo",
|
||||
Namespace: apiv1.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 TestSecureUpstream(t *testing.T) {
|
||||
ec := newAnnotationExtractor(mockCfg{})
|
||||
ing := buildIngress()
|
||||
|
||||
fooAnns := []struct {
|
||||
annotations map[string]string
|
||||
er bool
|
||||
}{
|
||||
{map[string]string{annotationSecureUpstream: "true"}, true},
|
||||
{map[string]string{annotationSecureUpstream: "false"}, false},
|
||||
{map[string]string{annotationSecureUpstream + "_no": "true"}, false},
|
||||
{map[string]string{}, false},
|
||||
{nil, false},
|
||||
}
|
||||
|
||||
for _, foo := range fooAnns {
|
||||
ing.SetAnnotations(foo.annotations)
|
||||
r := ec.SecureUpstream(ing)
|
||||
if r.Secure != foo.er {
|
||||
t.Errorf("Returned %v but expected %v", r, foo.er)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSecureVerifyCACert(t *testing.T) {
|
||||
ec := newAnnotationExtractor(mockCfg{
|
||||
MockSecrets: map[string]*apiv1.Secret{
|
||||
"default/secure-verify-ca": {
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "secure-verify-ca",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
anns := []struct {
|
||||
it int
|
||||
annotations map[string]string
|
||||
exists bool
|
||||
}{
|
||||
{1, map[string]string{annotationSecureUpstream: "true", annotationSecureVerifyCACert: "not"}, false},
|
||||
{2, map[string]string{annotationSecureUpstream: "false", annotationSecureVerifyCACert: "secure-verify-ca"}, false},
|
||||
{3, map[string]string{annotationSecureUpstream: "true", annotationSecureVerifyCACert: "secure-verify-ca"}, true},
|
||||
{4, map[string]string{annotationSecureUpstream: "true", annotationSecureVerifyCACert + "_not": "secure-verify-ca"}, false},
|
||||
{5, map[string]string{annotationSecureUpstream: "true"}, false},
|
||||
{6, map[string]string{}, false},
|
||||
{7, nil, false},
|
||||
}
|
||||
|
||||
for _, ann := range anns {
|
||||
ing := buildIngress()
|
||||
ing.SetAnnotations(ann.annotations)
|
||||
res := ec.SecureUpstream(ing)
|
||||
if (res.CACert.CAFileName != "") != ann.exists {
|
||||
t.Errorf("Expected exists was %v on iteration %v", ann.exists, ann.it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestHealthCheck(t *testing.T) {
|
||||
ec := newAnnotationExtractor(mockCfg{})
|
||||
ing := buildIngress()
|
||||
|
||||
fooAnns := []struct {
|
||||
annotations map[string]string
|
||||
eumf int
|
||||
euft int
|
||||
}{
|
||||
{map[string]string{annotationUpsMaxFails: "3", annotationUpsFailTimeout: "10"}, 3, 10},
|
||||
{map[string]string{annotationUpsMaxFails: "3"}, 3, 0},
|
||||
{map[string]string{annotationUpsFailTimeout: "10"}, 0, 10},
|
||||
{map[string]string{}, 0, 0},
|
||||
{nil, 0, 0},
|
||||
}
|
||||
|
||||
for _, foo := range fooAnns {
|
||||
ing.SetAnnotations(foo.annotations)
|
||||
r := ec.HealthCheck(ing)
|
||||
if r == nil {
|
||||
t.Errorf("Returned nil but expected a healthcheck.Upstream")
|
||||
continue
|
||||
}
|
||||
|
||||
if r.FailTimeout != foo.euft {
|
||||
t.Errorf("Returned %d but expected %d for FailTimeout", r.FailTimeout, foo.euft)
|
||||
}
|
||||
|
||||
if r.MaxFails != foo.eumf {
|
||||
t.Errorf("Returned %d but expected %d for MaxFails", r.MaxFails, foo.eumf)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSSLPassthrough(t *testing.T) {
|
||||
ec := newAnnotationExtractor(mockCfg{})
|
||||
ing := buildIngress()
|
||||
|
||||
fooAnns := []struct {
|
||||
annotations map[string]string
|
||||
er bool
|
||||
}{
|
||||
{map[string]string{annotationPassthrough: "true"}, true},
|
||||
{map[string]string{annotationPassthrough: "false"}, false},
|
||||
{map[string]string{annotationPassthrough + "_no": "true"}, false},
|
||||
{map[string]string{}, false},
|
||||
{nil, false},
|
||||
}
|
||||
|
||||
for _, foo := range fooAnns {
|
||||
ing.SetAnnotations(foo.annotations)
|
||||
r := ec.SSLPassthrough(ing)
|
||||
if r != foo.er {
|
||||
t.Errorf("Returned %v but expected %v", r, foo.er)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
173
pkg/ingress/controller/backend_ssl.go
Normal file
173
pkg/ingress/controller/backend_ssl.go
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
/*
|
||||
Copyright 2015 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 controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/golang/glog"
|
||||
|
||||
apiv1 "k8s.io/api/core/v1"
|
||||
extensions "k8s.io/api/extensions/v1beta1"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
|
||||
"k8s.io/ingress-nginx/pkg/ingress"
|
||||
"k8s.io/ingress-nginx/pkg/ingress/annotations/class"
|
||||
"k8s.io/ingress-nginx/pkg/ingress/annotations/parser"
|
||||
"k8s.io/ingress-nginx/pkg/net/ssl"
|
||||
)
|
||||
|
||||
// syncSecret keeps in sync Secrets used by Ingress rules with the files on
|
||||
// disk to allow copy of the content of the secret to disk to be used
|
||||
// by external processes.
|
||||
func (ic *GenericController) syncSecret(key string) {
|
||||
glog.V(3).Infof("starting syncing of secret %v", key)
|
||||
|
||||
cert, err := ic.getPemCertificate(key)
|
||||
if err != nil {
|
||||
glog.Warningf("error obtaining PEM from secret %v: %v", key, err)
|
||||
return
|
||||
}
|
||||
|
||||
// create certificates and add or update the item in the store
|
||||
cur, exists := ic.sslCertTracker.Get(key)
|
||||
if exists {
|
||||
s := cur.(*ingress.SSLCert)
|
||||
if reflect.DeepEqual(s, cert) {
|
||||
// no need to update
|
||||
return
|
||||
}
|
||||
glog.Infof("updating secret %v in the local store", key)
|
||||
ic.sslCertTracker.Update(key, cert)
|
||||
// this update must trigger an update
|
||||
// (like an update event from a change in Ingress)
|
||||
ic.syncQueue.Enqueue(&extensions.Ingress{})
|
||||
return
|
||||
}
|
||||
|
||||
glog.Infof("adding secret %v to the local store", key)
|
||||
ic.sslCertTracker.Add(key, cert)
|
||||
// this update must trigger an update
|
||||
// (like an update event from a change in Ingress)
|
||||
ic.syncQueue.Enqueue(&extensions.Ingress{})
|
||||
}
|
||||
|
||||
// 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) {
|
||||
secret, err := ic.listers.Secret.GetByName(secretName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error retrieving secret %v: %v", secretName, err)
|
||||
}
|
||||
|
||||
cert, okcert := secret.Data[apiv1.TLSCertKey]
|
||||
key, okkey := secret.Data[apiv1.TLSPrivateKeyKey]
|
||||
ca := secret.Data["ca.crt"]
|
||||
|
||||
// namespace/secretName -> namespace-secretName
|
||||
nsSecName := strings.Replace(secretName, "/", "-", -1)
|
||||
|
||||
var s *ingress.SSLCert
|
||||
if okcert && okkey {
|
||||
if cert == nil {
|
||||
return nil, fmt.Errorf("secret %v has no 'tls.crt'", secretName)
|
||||
}
|
||||
if key == nil {
|
||||
return nil, fmt.Errorf("secret %v has no 'tls.key'", secretName)
|
||||
}
|
||||
|
||||
// If 'ca.crt' is also present, it will allow this secret to be used in the
|
||||
// 'ingress.kubernetes.io/auth-tls-secret' annotation
|
||||
s, err = ssl.AddOrUpdateCertAndKey(nsSecName, cert, key, ca)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unexpected error creating pem file: %v", err)
|
||||
}
|
||||
|
||||
glog.V(3).Infof("found 'tls.crt' and 'tls.key', configuring %v as a TLS Secret (CN: %v)", secretName, s.CN)
|
||||
if ca != nil {
|
||||
glog.V(3).Infof("found 'ca.crt', secret %v can also be used for Certificate Authentication", secretName)
|
||||
}
|
||||
|
||||
} else if ca != nil {
|
||||
s, err = ssl.AddCertAuth(nsSecName, ca)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unexpected error creating pem file: %v", err)
|
||||
}
|
||||
|
||||
// makes this secret in 'syncSecret' to be used for Certificate Authentication
|
||||
// this does not enable Certificate Authentication
|
||||
glog.V(3).Infof("found only 'ca.crt', configuring %v as an Certificate Authentication Secret", secretName)
|
||||
|
||||
} else {
|
||||
return nil, fmt.Errorf("no keypair or CA cert could be found in %v", secretName)
|
||||
}
|
||||
|
||||
s.Name = secret.Name
|
||||
s.Namespace = secret.Namespace
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// checkMissingSecrets verify if one or more ingress rules contains a reference
|
||||
// to a secret that is not present in the local secret store.
|
||||
// In this case we call syncSecret.
|
||||
func (ic *GenericController) checkMissingSecrets() {
|
||||
for _, obj := range ic.listers.Ingress.List() {
|
||||
ing := obj.(*extensions.Ingress)
|
||||
|
||||
if !class.IsValid(ing, ic.cfg.IngressClass, ic.cfg.DefaultIngressClass) {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, tls := range ing.Spec.TLS {
|
||||
if tls.SecretName == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
key := fmt.Sprintf("%v/%v", ing.Namespace, tls.SecretName)
|
||||
if _, ok := ic.sslCertTracker.Get(key); !ok {
|
||||
ic.syncSecret(key)
|
||||
}
|
||||
}
|
||||
|
||||
key, _ := parser.GetStringAnnotation("ingress.kubernetes.io/auth-tls-secret", ing)
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, ok := ic.sslCertTracker.Get(key); !ok {
|
||||
ic.syncSecret(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sslCertTracker holds a store of referenced Secrets in Ingress rules
|
||||
type sslCertTracker struct {
|
||||
cache.ThreadSafeStore
|
||||
}
|
||||
|
||||
func newSSLCertTracker() *sslCertTracker {
|
||||
return &sslCertTracker{
|
||||
cache.NewThreadSafeStore(cache.Indexers{}, cache.Indices{}),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *sslCertTracker) DeleteAll(key string) {
|
||||
s.Delete(key)
|
||||
}
|
||||
234
pkg/ingress/controller/backend_ssl_test.go
Normal file
234
pkg/ingress/controller/backend_ssl_test.go
Normal file
|
|
@ -0,0 +1,234 @@
|
|||
/*
|
||||
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 controller
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
|
||||
apiv1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
testclient "k8s.io/client-go/kubernetes/fake"
|
||||
cache_client "k8s.io/client-go/tools/cache"
|
||||
"k8s.io/client-go/util/flowcontrol"
|
||||
|
||||
"k8s.io/ingress-nginx/pkg/ingress"
|
||||
"k8s.io/ingress-nginx/pkg/ingress/store"
|
||||
"k8s.io/ingress-nginx/pkg/task"
|
||||
"k8s.io/kubernetes/pkg/api"
|
||||
)
|
||||
|
||||
const (
|
||||
// openssl req -x509 -nodes -days 3650 -newkey rsa:2048 -keyout tls.key -out tls.crt -subj "/CN=nginxsvc/O=nginxsvc"
|
||||
tlsCrt = "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURIekNDQWdlZ0F3SUJBZ0lKQU1KZld6Mm81cWVnTUEwR0NTcUdTSWIzRFFFQkN3VUFNQ1l4RVRBUEJnTlYKQkFNTUNHNW5hVzU0YzNaak1SRXdEd1lEVlFRS0RBaHVaMmx1ZUhOMll6QWVGdzB4TnpBME1URXdNakF3TlRCYQpGdzB5TnpBME1Ea3dNakF3TlRCYU1DWXhFVEFQQmdOVkJBTU1DRzVuYVc1NGMzWmpNUkV3RHdZRFZRUUtEQWh1CloybHVlSE4yWXpDQ0FTSXdEUVlKS29aSWh2Y05BUUVCQlFBRGdnRVBBRENDQVFvQ2dnRUJBTUgzVTYvY3ArODAKU3hJRjltSnlUcGI5RzBodnhsM0JMaGdQWDBTWjZ3d1lISGJXeTh2dmlCZjVwWTdvVHd0b2FPaTN1VFNsL2RtVwpvUi9XNm9GVWM5a2l6NlNXc3p6YWRXL2l2Q21LMmxOZUFVc2gvaXY0aTAvNXlreDJRNXZUT2tVL1dra2JPOW1OCjdSVTF0QW1KT3M0T1BVc3hZZkw2cnJJUzZPYktHS2UvYUVkek9QS2NPMDJ5NUxDeHM0TFhhWDIzU1l6TG1XYVAKYVZBallrN1NRZm1xUm5mYlF4RWlpaDFQWTFRRXgxWWs0RzA0VmtHUitrSVVMaWF0L291ZjQxY0dXRTZHMTF4NQpkV1BHeS9XcGtqRGlaM0UwekdNZnJBVUZibnErN1dhRTJCRzVoUVV3ZG9SQUtWTnMzaVhLRlRkT3hoRll5bnBwCjA3cDJVNS96ZHRrQ0F3RUFBYU5RTUU0d0hRWURWUjBPQkJZRUZCL2U5UnVna0Mwc0VNTTZ6enRCSjI1U1JxalMKTUI4R0ExVWRJd1FZTUJhQUZCL2U5UnVna0Mwc0VNTTZ6enRCSjI1U1JxalNNQXdHQTFVZEV3UUZNQU1CQWY4dwpEUVlKS29aSWh2Y05BUUVMQlFBRGdnRUJBRys4MXdaSXRuMmFWSlFnejNkNmJvZW1nUXhSSHpaZDhNc1IrdFRvCnpJLy9ac1Nwc2FDR3F0TkdTaHVGKzB3TVZ4NjlpQ3lJTnJJb2J4K29NTHBsQzFQSk9uektSUUdvZEhYNFZaSUwKVlhxSFd2VStjK3ZtT0QxUEt3UjcwRi9rTXk2Yk4xMVI2amhIZ3RPZGdLKzdRczhRMVlUSC9RS2dMd3RJTFRHRwpTZlYxWFlmbnF1TXlZKzFzck00U3ZRSmRzdmFUQmJkZHE2RllpdjhXZFpIaG51ZGlSODdZcFgzOUlTSlFkOXF2CnR6OGthZTVqQVFEUWFiZnFsVWZNT1hmUnhyei96S2NvN3dMeWFMWTh1eVhEWUVIZmlHRWdablV0RjgxVlhDZUIKeU80UERBR0FuVmlXTndFM0NZcGI4RkNGelMyaVVVMDJaQWJRajlvUnYyUWNON1E9Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K"
|
||||
tlsKey = "LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2Z0lCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktnd2dnU2tBZ0VBQW9JQkFRREI5MU92M0tmdk5Fc1MKQmZaaWNrNlcvUnRJYjhaZHdTNFlEMTlFbWVzTUdCeDIxc3ZMNzRnWCthV082RThMYUdqb3Q3azBwZjNabHFFZgoxdXFCVkhQWklzK2tsck04Mm5WdjRyd3BpdHBUWGdGTElmNHIrSXRQK2NwTWRrT2IwenBGUDFwSkd6dlpqZTBWCk5iUUppVHJPRGoxTE1XSHkrcTZ5RXVqbXloaW52MmhIY3pqeW5EdE5zdVN3c2JPQzEybDl0MG1NeTVsbWoybFEKSTJKTzBrSDVxa1ozMjBNUklvb2RUMk5VQk1kV0pPQnRPRlpCa2ZwQ0ZDNG1yZjZMbitOWEJsaE9odGRjZVhWagp4c3YxcVpJdzRtZHhOTXhqSDZ3RkJXNTZ2dTFtaE5nUnVZVUZNSGFFUUNsVGJONGx5aFUzVHNZUldNcDZhZE82CmRsT2Y4M2JaQWdNQkFBRUNnZ0VBRGU1WW1XSHN3ZFpzcWQrNXdYcGFRS2Z2SkxXNmRwTmdYeVFEZ0tiWlplWDUKYldPaUFZU3pycDBra2U0SGQxZEphYVdBYk5LYk45eUV1QWUwa2hOaHVxK3dZQzdlc3JreUJCWXgwMzRBamtwTApKMzFLaHhmejBZdXNSdStialg2UFNkZnlBUnd1b1VKN1M3R3V1NXlhbDZBWU1PVmNGcHFBbjVPU0hMbFpLZnNLClN3NXZyM3NKUjNyOENNWVZoUmQ0citGam9lMXlaczJhUHl2bno5c0U3T0ZCSVRGSVBKcE4veG53VUNpWW5vSEMKV2F2TzB5RCtPeTUyN2hBQ1FwaFVMVjRaZXV2bEZwd2ZlWkZveUhnc2YrM1FxeGhpdGtJb3NGakx2Y0xlL2xjZwpSVHNRUnU5OGJNUTdSakJpYU5kaURadjBaWEMvUUMvS054SEw0bXgxTFFLQmdRRHVDY0pUM2JBZmJRY2YvSGh3CjNxRzliNE9QTXpwOTl2ajUzWU1hZHo5Vlk1dm9RR3RGeFlwbTBRVm9MR1lkQ3BHK0lXaDdVVHBMV0JUeGtMSkYKd3EwcEFmRVhmdHB0anhmcyt0OExWVUFtSXJiM2hwUjZDUjJmYjFJWVZRWUJ4dUdzN0hWMmY3NnRZMVAzSEFnNwpGTDJNTnF3ZDd5VmlsVXdSTVptcmJKV3Qwd0tCZ1FEUW1qZlgzc1NWSWZtN1FQaVQvclhSOGJMM1B3V0lNa3NOCldJTVRYeDJmaG0vd0hOL0pNdCtEK2VWbGxjSXhLMmxSYlNTQ1NwU2hzRUVsMHNxWHZUa1FFQnJxN3RFZndRWU0KbGxNbDJQb0ovV2E5c2VYSTAzWWRNeC94Vm5sbzNaUG9MUGg4UmtKekJTWkhnMlB6cCs0VmlnUklBcGdYMXo3TwpMbHg0SEVtaEl3S0JnUURES1RVdVZYL2xCQnJuV3JQVXRuT2RRU1IzNytSeENtQXZYREgxTFBlOEpxTFkxSmdlCjZFc0U2VEtwcWwwK1NrQWJ4b0JIT3QyMGtFNzdqMHJhYnpaUmZNb1NIV3N3a0RWcGtuWDBjTHpiaDNMRGxvOTkKVHFQKzUrSkRHTktIK210a3Y2bStzaFcvU3NTNHdUN3VVWjdtcXB5TEhsdGtiRXVsZlNra3B5NUJDUUtCZ0RmUwpyVk1GZUZINGI1NGV1dWJQNk5Rdi9CYVNOT2JIbnJJSmw3b2RZQTRLcWZYMXBDVnhpY01Gb3MvV2pjc2V0T1puCmNMZTFRYVVyUjZQWmp3R2dUNTd1MEdWQ1Y1QkoxVmFVKzlkTEEwNmRFMXQ4T2VQT1F2TjVkUGplalVyMDBObjIKL3VBeTVTRm1wV0hKMVh1azJ0L0V1WFNUelNQRUpEaUV5NVlRNjl0RkFvR0JBT2tDcW1jVGZGYlpPTjJRK2JqdgpvVmQvSFpLR3YrbEhqcm5maVlhODVxcUszdWJmb0FSNGppR3V3TThqc3hZZW8vb0hQdHJDTkxINndsYlZNTUFGCmlRZG80ZUF3S0xxRHo1MUx4U2hMckwzUUtNQ1FuZVhkT0VjaEdqSW9zRG5Zekd5RTBpSzJGbWNvWHVSQU1QOHgKWDFreUlkazdENDFxSjQ5WlM1OEdBbXlLCi0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K"
|
||||
tlscaName = "ca.crt"
|
||||
)
|
||||
|
||||
type MockQueue struct {
|
||||
cache_client.Store
|
||||
Synced bool
|
||||
}
|
||||
|
||||
func (f *MockQueue) HasSynced() bool {
|
||||
return f.Synced
|
||||
}
|
||||
|
||||
func (f *MockQueue) AddIfNotPresent(obj interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *MockQueue) Pop(process cache_client.PopProcessFunc) (interface{}, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (f *MockQueue) Close() {
|
||||
// just mock
|
||||
}
|
||||
|
||||
func buildSimpleClientSetForBackendSSL() *testclient.Clientset {
|
||||
return testclient.NewSimpleClientset()
|
||||
}
|
||||
|
||||
func buildIngListenerForBackendSSL() store.IngressLister {
|
||||
ingLister := store.IngressLister{}
|
||||
ingLister.Store = cache_client.NewStore(cache_client.DeletionHandlingMetaNamespaceKeyFunc)
|
||||
return ingLister
|
||||
}
|
||||
|
||||
func buildSecretForBackendSSL() *apiv1.Secret {
|
||||
return &apiv1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "foo_secret",
|
||||
Namespace: api.NamespaceDefault,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func buildSecrListerForBackendSSL() store.SecretLister {
|
||||
secrLister := store.SecretLister{}
|
||||
secrLister.Store = cache_client.NewStore(cache_client.DeletionHandlingMetaNamespaceKeyFunc)
|
||||
|
||||
return secrLister
|
||||
}
|
||||
|
||||
func buildListers() *ingress.StoreLister {
|
||||
sl := &ingress.StoreLister{}
|
||||
sl.Ingress.Store = buildIngListenerForBackendSSL()
|
||||
sl.Secret.Store = buildSecrListerForBackendSSL()
|
||||
return sl
|
||||
}
|
||||
|
||||
func buildControllerForBackendSSL() cache_client.Controller {
|
||||
cfg := &cache_client.Config{
|
||||
Queue: &MockQueue{Synced: true},
|
||||
}
|
||||
|
||||
return cache_client.New(cfg)
|
||||
}
|
||||
|
||||
func buildGenericControllerForBackendSSL() *GenericController {
|
||||
gc := &GenericController{
|
||||
syncRateLimiter: flowcontrol.NewTokenBucketRateLimiter(0.3, 1),
|
||||
cfg: &Configuration{
|
||||
Client: buildSimpleClientSetForBackendSSL(),
|
||||
},
|
||||
listers: buildListers(),
|
||||
sslCertTracker: newSSLCertTracker(),
|
||||
}
|
||||
|
||||
gc.syncQueue = task.NewTaskQueue(gc.syncIngress)
|
||||
return gc
|
||||
}
|
||||
|
||||
func buildCrtKeyAndCA() ([]byte, []byte, []byte, error) {
|
||||
// prepare
|
||||
td, err := ioutil.TempDir("", "ssl")
|
||||
if err != nil {
|
||||
return nil, nil, nil, fmt.Errorf("error occurs while creating temp directory: %v", err)
|
||||
}
|
||||
ingress.DefaultSSLDirectory = td
|
||||
|
||||
dCrt, err := base64.StdEncoding.DecodeString(tlsCrt)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
dKey, err := base64.StdEncoding.DecodeString(tlsKey)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
dCa := dCrt
|
||||
|
||||
return dCrt, dKey, dCa, nil
|
||||
}
|
||||
|
||||
func TestSyncSecret(t *testing.T) {
|
||||
// prepare for test
|
||||
dCrt, dKey, dCa, err := buildCrtKeyAndCA()
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
foos := []struct {
|
||||
tn string
|
||||
secretName string
|
||||
Data map[string][]byte
|
||||
expectSuccess bool
|
||||
}{
|
||||
{"getPemCertificate_error", "default/foo_secret", map[string][]byte{api.TLSPrivateKeyKey: dKey}, false},
|
||||
{"normal_test", "default/foo_secret", map[string][]byte{api.TLSCertKey: dCrt, api.TLSPrivateKeyKey: dKey, tlscaName: dCa}, true},
|
||||
}
|
||||
|
||||
for _, foo := range foos {
|
||||
t.Run(foo.tn, func(t *testing.T) {
|
||||
ic := buildGenericControllerForBackendSSL()
|
||||
|
||||
// init secret for getPemCertificate
|
||||
secret := buildSecretForBackendSSL()
|
||||
secret.SetNamespace("default")
|
||||
secret.SetName("foo_secret")
|
||||
secret.Data = foo.Data
|
||||
ic.listers.Secret.Add(secret)
|
||||
|
||||
key := "default/foo_secret"
|
||||
// for add
|
||||
ic.syncSecret(key)
|
||||
if foo.expectSuccess {
|
||||
// validate
|
||||
_, exist := ic.sslCertTracker.Get(key)
|
||||
if !exist {
|
||||
t.Errorf("Failed to sync secret: %s", foo.secretName)
|
||||
} else {
|
||||
// for update
|
||||
ic.syncSecret(key)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetPemCertificate(t *testing.T) {
|
||||
// prepare
|
||||
dCrt, dKey, dCa, err := buildCrtKeyAndCA()
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
foos := []struct {
|
||||
tn string
|
||||
secretName string
|
||||
Data map[string][]byte
|
||||
eErr bool
|
||||
}{
|
||||
{"sceret_not_exist", "default/foo_secret_not_exist", nil, true},
|
||||
{"data_not_complete_all_not_exist", "default/foo_secret", map[string][]byte{}, true},
|
||||
{"data_not_complete_TLSCertKey_not_exist", "default/foo_secret", map[string][]byte{api.TLSPrivateKeyKey: dKey, tlscaName: dCa}, false},
|
||||
{"data_not_complete_TLSCertKeyAndCA_not_exist", "default/foo_secret", map[string][]byte{api.TLSPrivateKeyKey: dKey}, true},
|
||||
{"data_not_complete_TLSPrivateKeyKey_not_exist", "default/foo_secret", map[string][]byte{api.TLSCertKey: dCrt, tlscaName: dCa}, false},
|
||||
{"data_not_complete_TLSPrivateKeyKeyAndCA_not_exist", "default/foo_secret", map[string][]byte{api.TLSCertKey: dCrt}, true},
|
||||
{"data_not_complete_CA_not_exist", "default/foo_secret", map[string][]byte{api.TLSCertKey: dCrt, api.TLSPrivateKeyKey: dKey}, false},
|
||||
{"normal_test", "default/foo_secret", map[string][]byte{api.TLSCertKey: dCrt, api.TLSPrivateKeyKey: dKey, tlscaName: dCa}, false},
|
||||
}
|
||||
|
||||
for _, foo := range foos {
|
||||
t.Run(foo.tn, func(t *testing.T) {
|
||||
ic := buildGenericControllerForBackendSSL()
|
||||
secret := buildSecretForBackendSSL()
|
||||
secret.Data = foo.Data
|
||||
ic.listers.Secret.Add(secret)
|
||||
sslCert, err := ic.getPemCertificate(foo.secretName)
|
||||
|
||||
if foo.eErr {
|
||||
if err == nil {
|
||||
t.Fatal("Expected error")
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if sslCert == nil {
|
||||
t.Error("Expected an ingress.SSLCert")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
1262
pkg/ingress/controller/controller.go
Normal file
1262
pkg/ingress/controller/controller.go
Normal file
File diff suppressed because it is too large
Load diff
319
pkg/ingress/controller/launch.go
Normal file
319
pkg/ingress/controller/launch.go
Normal file
|
|
@ -0,0 +1,319 @@
|
|||
package controller
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/pprof"
|
||||
"os"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/golang/glog"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
apiv1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apiserver/pkg/server/healthz"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
|
||||
|
||||
"k8s.io/ingress-nginx/pkg/ingress"
|
||||
"k8s.io/ingress-nginx/pkg/k8s"
|
||||
)
|
||||
|
||||
// NewIngressController returns a configured Ingress controller
|
||||
func NewIngressController(backend ingress.Controller) *GenericController {
|
||||
var (
|
||||
flags = pflag.NewFlagSet("", pflag.ExitOnError)
|
||||
|
||||
apiserverHost = flags.String("apiserver-host", "", "The address of the Kubernetes Apiserver "+
|
||||
"to connect to in the format of protocol://address:port, e.g., "+
|
||||
"http://localhost:8080. If not specified, the assumption is that the binary runs inside a "+
|
||||
"Kubernetes cluster and local discovery is attempted.")
|
||||
kubeConfigFile = flags.String("kubeconfig", "", "Path to kubeconfig file with authorization and master location information.")
|
||||
|
||||
defaultSvc = flags.String("default-backend-service", "",
|
||||
`Service used to serve a 404 page for the default backend. Takes the form
|
||||
namespace/name. The controller uses the first node port of this Service for
|
||||
the default backend.`)
|
||||
|
||||
ingressClass = flags.String("ingress-class", "",
|
||||
`Name of the ingress class to route through this controller.`)
|
||||
|
||||
configMap = flags.String("configmap", "",
|
||||
`Name of the ConfigMap that contains the custom configuration to use`)
|
||||
|
||||
publishSvc = flags.String("publish-service", "",
|
||||
`Service fronting the ingress controllers. Takes the form
|
||||
namespace/name. The controller will set the endpoint records on the
|
||||
ingress objects to reflect those on the service.`)
|
||||
|
||||
tcpConfigMapName = flags.String("tcp-services-configmap", "",
|
||||
`Name of the ConfigMap that contains the definition of the TCP services to expose.
|
||||
The key in the map indicates the external port to be used. The value is the name of the
|
||||
service with the format namespace/serviceName and the port of the service could be a
|
||||
number of the name of the port.
|
||||
The ports 80 and 443 are not allowed as external ports. This ports are reserved for the backend`)
|
||||
|
||||
udpConfigMapName = flags.String("udp-services-configmap", "",
|
||||
`Name of the ConfigMap that contains the definition of the UDP services to expose.
|
||||
The key in the map indicates the external port to be used. The value is the name of the
|
||||
service with the format namespace/serviceName and the port of the service could be a
|
||||
number of the name of the port.`)
|
||||
|
||||
resyncPeriod = flags.Duration("sync-period", 600*time.Second,
|
||||
`Relist and confirm cloud resources this often. Default is 10 minutes`)
|
||||
|
||||
watchNamespace = flags.String("watch-namespace", apiv1.NamespaceAll,
|
||||
`Namespace to watch for Ingress. Default is to watch all namespaces`)
|
||||
|
||||
healthzPort = flags.Int("healthz-port", 10254, "port for healthz endpoint.")
|
||||
|
||||
profiling = flags.Bool("profiling", true, `Enable profiling via web interface host:port/debug/pprof/`)
|
||||
|
||||
defSSLCertificate = flags.String("default-ssl-certificate", "", `Name of the secret
|
||||
that contains a SSL certificate to be used as default for a HTTPS catch-all server`)
|
||||
|
||||
defHealthzURL = flags.String("health-check-path", "/healthz", `Defines
|
||||
the URL to be used as health check inside in the default server in NGINX.`)
|
||||
|
||||
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.`)
|
||||
|
||||
forceIsolation = flags.Bool("force-namespace-isolation", false,
|
||||
`Force namespace isolation. This flag is required to avoid the reference of secrets or
|
||||
configmaps located in a different namespace than the specified in the flag --watch-namespace.`)
|
||||
|
||||
disableNodeList = flags.Bool("disable-node-list", false,
|
||||
`Disable querying nodes. If --force-namespace-isolation is true, this should also be set.`)
|
||||
|
||||
updateStatusOnShutdown = flags.Bool("update-status-on-shutdown", true, `Indicates if the
|
||||
ingress controller should update the Ingress status IP/hostname when the controller
|
||||
is being stopped. Default is true`)
|
||||
|
||||
sortBackends = flags.Bool("sort-backends", false,
|
||||
`Defines if backends and it's endpoints should be sorted`)
|
||||
)
|
||||
|
||||
flags.AddGoFlagSet(flag.CommandLine)
|
||||
backend.ConfigureFlags(flags)
|
||||
flags.Parse(os.Args)
|
||||
backend.OverrideFlags(flags)
|
||||
|
||||
flag.Set("logtostderr", "true")
|
||||
|
||||
glog.Info(backend.Info())
|
||||
|
||||
if *ingressClass != "" {
|
||||
glog.Infof("Watching for ingress class: %s", *ingressClass)
|
||||
}
|
||||
|
||||
if *defaultSvc == "" {
|
||||
glog.Fatalf("Please specify --default-backend-service")
|
||||
}
|
||||
|
||||
kubeClient, err := createApiserverClient(*apiserverHost, *kubeConfigFile)
|
||||
if err != nil {
|
||||
handleFatalInitError(err)
|
||||
}
|
||||
|
||||
ns, name, err := k8s.ParseNameNS(*defaultSvc)
|
||||
if err != nil {
|
||||
glog.Fatalf("invalid format for service %v: %v", *defaultSvc, err)
|
||||
}
|
||||
|
||||
_, err = kubeClient.Core().Services(ns).Get(name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "cannot get services in the namespace") {
|
||||
glog.Fatalf("✖ It seems the cluster it is running with Authorization enabled (like RBAC) and there is no permissions for the ingress controller. Please check the configuration")
|
||||
}
|
||||
glog.Fatalf("no service with name %v found: %v", *defaultSvc, err)
|
||||
}
|
||||
glog.Infof("validated %v as the default backend", *defaultSvc)
|
||||
|
||||
if *publishSvc != "" {
|
||||
ns, name, err := k8s.ParseNameNS(*publishSvc)
|
||||
if err != nil {
|
||||
glog.Fatalf("invalid service format: %v", err)
|
||||
}
|
||||
|
||||
svc, err := kubeClient.CoreV1().Services(ns).Get(name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
glog.Fatalf("unexpected error getting information about service %v: %v", *publishSvc, err)
|
||||
}
|
||||
|
||||
if len(svc.Status.LoadBalancer.Ingress) == 0 {
|
||||
if len(svc.Spec.ExternalIPs) > 0 {
|
||||
glog.Infof("service %v validated as assigned with externalIP", *publishSvc)
|
||||
} else {
|
||||
// We could poll here, but we instead just exit and rely on k8s to restart us
|
||||
glog.Fatalf("service %s does not (yet) have ingress points", *publishSvc)
|
||||
}
|
||||
} else {
|
||||
glog.Infof("service %v validated as source of Ingress status", *publishSvc)
|
||||
}
|
||||
}
|
||||
|
||||
if *watchNamespace != "" {
|
||||
_, err = kubeClient.CoreV1().Namespaces().Get(*watchNamespace, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
glog.Fatalf("no watchNamespace with name %v found: %v", *watchNamespace, err)
|
||||
}
|
||||
}
|
||||
|
||||
if resyncPeriod.Seconds() < 10 {
|
||||
glog.Fatalf("resync period (%vs) is too low", resyncPeriod.Seconds())
|
||||
}
|
||||
|
||||
err = os.MkdirAll(ingress.DefaultSSLDirectory, 0655)
|
||||
if err != nil {
|
||||
glog.Errorf("Failed to mkdir SSL directory: %v", err)
|
||||
}
|
||||
|
||||
config := &Configuration{
|
||||
UpdateStatus: *updateStatus,
|
||||
ElectionID: *electionID,
|
||||
Client: kubeClient,
|
||||
ResyncPeriod: *resyncPeriod,
|
||||
DefaultService: *defaultSvc,
|
||||
IngressClass: *ingressClass,
|
||||
DefaultIngressClass: backend.DefaultIngressClass(),
|
||||
Namespace: *watchNamespace,
|
||||
ConfigMapName: *configMap,
|
||||
TCPConfigMapName: *tcpConfigMapName,
|
||||
UDPConfigMapName: *udpConfigMapName,
|
||||
DefaultSSLCertificate: *defSSLCertificate,
|
||||
DefaultHealthzURL: *defHealthzURL,
|
||||
PublishService: *publishSvc,
|
||||
Backend: backend,
|
||||
ForceNamespaceIsolation: *forceIsolation,
|
||||
DisableNodeList: *disableNodeList,
|
||||
UpdateStatusOnShutdown: *updateStatusOnShutdown,
|
||||
SortBackends: *sortBackends,
|
||||
}
|
||||
|
||||
ic := newIngressController(config)
|
||||
go registerHandlers(*profiling, *healthzPort, ic)
|
||||
return ic
|
||||
}
|
||||
|
||||
func registerHandlers(enableProfiling bool, port int, ic *GenericController) {
|
||||
mux := http.NewServeMux()
|
||||
// expose health check endpoint (/healthz)
|
||||
healthz.InstallHandler(mux,
|
||||
healthz.PingHealthz,
|
||||
ic.cfg.Backend,
|
||||
)
|
||||
|
||||
mux.Handle("/metrics", promhttp.Handler())
|
||||
|
||||
mux.HandleFunc("/build", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
b, _ := json.Marshal(ic.Info())
|
||||
w.Write(b)
|
||||
})
|
||||
|
||||
mux.HandleFunc("/stop", func(w http.ResponseWriter, r *http.Request) {
|
||||
err := syscall.Kill(syscall.Getpid(), syscall.SIGTERM)
|
||||
if err != nil {
|
||||
glog.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
if enableProfiling {
|
||||
mux.HandleFunc("/debug/pprof/", pprof.Index)
|
||||
mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
|
||||
mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
|
||||
mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
|
||||
}
|
||||
|
||||
server := &http.Server{
|
||||
Addr: fmt.Sprintf(":%v", port),
|
||||
Handler: mux,
|
||||
}
|
||||
glog.Fatal(server.ListenAndServe())
|
||||
}
|
||||
|
||||
const (
|
||||
// High enough QPS to fit all expected use cases. QPS=0 is not set here, because
|
||||
// client code is overriding it.
|
||||
defaultQPS = 1e6
|
||||
// High enough Burst to fit all expected use cases. Burst=0 is not set here, because
|
||||
// client code is overriding it.
|
||||
defaultBurst = 1e6
|
||||
)
|
||||
|
||||
// buildConfigFromFlags builds REST config based on master URL and kubeconfig path.
|
||||
// If both of them are empty then in cluster config is used.
|
||||
func buildConfigFromFlags(masterURL, kubeconfigPath string) (*rest.Config, error) {
|
||||
if kubeconfigPath == "" && masterURL == "" {
|
||||
kubeconfig, err := rest.InClusterConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return kubeconfig, nil
|
||||
}
|
||||
|
||||
return clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
|
||||
&clientcmd.ClientConfigLoadingRules{ExplicitPath: kubeconfigPath},
|
||||
&clientcmd.ConfigOverrides{
|
||||
ClusterInfo: clientcmdapi.Cluster{
|
||||
Server: masterURL,
|
||||
},
|
||||
}).ClientConfig()
|
||||
}
|
||||
|
||||
// createApiserverClient creates new Kubernetes Apiserver client. When kubeconfig or apiserverHost param is empty
|
||||
// the function assumes that it is running inside a Kubernetes cluster and attempts to
|
||||
// discover the Apiserver. Otherwise, it connects to the Apiserver specified.
|
||||
//
|
||||
// apiserverHost param is in the format of protocol://address:port/pathPrefix, e.g.http://localhost:8001.
|
||||
// kubeConfig location of kubeconfig file
|
||||
func createApiserverClient(apiserverHost string, kubeConfig string) (*kubernetes.Clientset, error) {
|
||||
cfg, err := buildConfigFromFlags(apiserverHost, kubeConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cfg.QPS = defaultQPS
|
||||
cfg.Burst = defaultBurst
|
||||
cfg.ContentType = "application/vnd.kubernetes.protobuf"
|
||||
|
||||
glog.Infof("Creating API client for %s", cfg.Host)
|
||||
|
||||
client, err := kubernetes.NewForConfig(cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
v, err := client.Discovery().ServerVersion()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
glog.Infof("Running in Kubernetes Cluster version v%v.%v (%v) - git (%v) commit %v - platform %v",
|
||||
v.Major, v.Minor, v.GitVersion, v.GitTreeState, v.GitCommit, v.Platform)
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles fatal init error that prevents server from doing any work. Prints verbose error
|
||||
* message and quits the server.
|
||||
*/
|
||||
func handleFatalInitError(err error) {
|
||||
glog.Fatalf("Error while initializing connection to Kubernetes apiserver. "+
|
||||
"This most likely means that the cluster is misconfigured (e.g., it has "+
|
||||
"invalid apiserver certificates or service accounts configuration). Reason: %s\n"+
|
||||
"Refer to the troubleshooting guide for more information: "+
|
||||
"https://github.com/kubernetes/ingress/blob/master/docs/troubleshooting.md", err)
|
||||
}
|
||||
236
pkg/ingress/controller/listers.go
Normal file
236
pkg/ingress/controller/listers.go
Normal file
|
|
@ -0,0 +1,236 @@
|
|||
/*
|
||||
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 controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/golang/glog"
|
||||
|
||||
apiv1 "k8s.io/api/core/v1"
|
||||
extensions "k8s.io/api/extensions/v1beta1"
|
||||
"k8s.io/apimachinery/pkg/fields"
|
||||
"k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
fcache "k8s.io/client-go/tools/cache/testing"
|
||||
|
||||
"k8s.io/ingress-nginx/pkg/ingress"
|
||||
"k8s.io/ingress-nginx/pkg/ingress/annotations/class"
|
||||
"k8s.io/ingress-nginx/pkg/ingress/annotations/parser"
|
||||
)
|
||||
|
||||
type cacheController struct {
|
||||
Ingress cache.Controller
|
||||
Endpoint cache.Controller
|
||||
Service cache.Controller
|
||||
Node cache.Controller
|
||||
Secret cache.Controller
|
||||
Configmap cache.Controller
|
||||
}
|
||||
|
||||
func (c *cacheController) Run(stopCh chan struct{}) {
|
||||
go c.Ingress.Run(stopCh)
|
||||
go c.Endpoint.Run(stopCh)
|
||||
go c.Service.Run(stopCh)
|
||||
go c.Node.Run(stopCh)
|
||||
go c.Secret.Run(stopCh)
|
||||
go c.Configmap.Run(stopCh)
|
||||
|
||||
// Wait for all involved caches to be synced, before processing items from the queue is started
|
||||
if !cache.WaitForCacheSync(stopCh,
|
||||
c.Ingress.HasSynced,
|
||||
c.Endpoint.HasSynced,
|
||||
c.Service.HasSynced,
|
||||
c.Node.HasSynced,
|
||||
c.Secret.HasSynced,
|
||||
c.Configmap.HasSynced,
|
||||
) {
|
||||
runtime.HandleError(fmt.Errorf("Timed out waiting for caches to sync"))
|
||||
}
|
||||
}
|
||||
|
||||
func (ic *GenericController) createListers(disableNodeLister bool) (*ingress.StoreLister, *cacheController) {
|
||||
// from here to the end of the method all the code is just boilerplate
|
||||
// required to watch Ingress, Secrets, ConfigMaps and Endoints.
|
||||
// This is used to detect new content, updates or removals and act accordingly
|
||||
ingEventHandler := cache.ResourceEventHandlerFuncs{
|
||||
AddFunc: func(obj interface{}) {
|
||||
addIng := obj.(*extensions.Ingress)
|
||||
if !class.IsValid(addIng, ic.cfg.IngressClass, ic.cfg.DefaultIngressClass) {
|
||||
a, _ := parser.GetStringAnnotation(class.IngressKey, addIng)
|
||||
glog.Infof("ignoring add for ingress %v based on annotation %v with value %v", addIng.Name, class.IngressKey, a)
|
||||
return
|
||||
}
|
||||
ic.recorder.Eventf(addIng, apiv1.EventTypeNormal, "CREATE", fmt.Sprintf("Ingress %s/%s", addIng.Namespace, addIng.Name))
|
||||
ic.syncQueue.Enqueue(obj)
|
||||
},
|
||||
DeleteFunc: func(obj interface{}) {
|
||||
delIng, ok := obj.(*extensions.Ingress)
|
||||
if !ok {
|
||||
// If we reached here it means the ingress was deleted but its final state is unrecorded.
|
||||
tombstone, ok := obj.(cache.DeletedFinalStateUnknown)
|
||||
if !ok {
|
||||
glog.Errorf("couldn't get object from tombstone %#v", obj)
|
||||
return
|
||||
}
|
||||
delIng, ok = tombstone.Obj.(*extensions.Ingress)
|
||||
if !ok {
|
||||
glog.Errorf("Tombstone contained object that is not an Ingress: %#v", obj)
|
||||
return
|
||||
}
|
||||
}
|
||||
if !class.IsValid(delIng, ic.cfg.IngressClass, ic.cfg.DefaultIngressClass) {
|
||||
glog.Infof("ignoring delete for ingress %v based on annotation %v", delIng.Name, class.IngressKey)
|
||||
return
|
||||
}
|
||||
ic.recorder.Eventf(delIng, apiv1.EventTypeNormal, "DELETE", fmt.Sprintf("Ingress %s/%s", delIng.Namespace, delIng.Name))
|
||||
ic.syncQueue.Enqueue(obj)
|
||||
},
|
||||
UpdateFunc: func(old, cur interface{}) {
|
||||
oldIng := old.(*extensions.Ingress)
|
||||
curIng := cur.(*extensions.Ingress)
|
||||
validOld := class.IsValid(oldIng, ic.cfg.IngressClass, ic.cfg.DefaultIngressClass)
|
||||
validCur := class.IsValid(curIng, ic.cfg.IngressClass, ic.cfg.DefaultIngressClass)
|
||||
if !validOld && validCur {
|
||||
glog.Infof("creating ingress %v based on annotation %v", curIng.Name, class.IngressKey)
|
||||
ic.recorder.Eventf(curIng, apiv1.EventTypeNormal, "CREATE", fmt.Sprintf("Ingress %s/%s", curIng.Namespace, curIng.Name))
|
||||
} else if validOld && !validCur {
|
||||
glog.Infof("removing ingress %v based on annotation %v", curIng.Name, class.IngressKey)
|
||||
ic.recorder.Eventf(curIng, apiv1.EventTypeNormal, "DELETE", fmt.Sprintf("Ingress %s/%s", curIng.Namespace, curIng.Name))
|
||||
} else if validCur && !reflect.DeepEqual(old, cur) {
|
||||
ic.recorder.Eventf(curIng, apiv1.EventTypeNormal, "UPDATE", fmt.Sprintf("Ingress %s/%s", curIng.Namespace, curIng.Name))
|
||||
}
|
||||
|
||||
ic.syncQueue.Enqueue(cur)
|
||||
},
|
||||
}
|
||||
|
||||
secrEventHandler := cache.ResourceEventHandlerFuncs{
|
||||
UpdateFunc: func(old, cur interface{}) {
|
||||
if !reflect.DeepEqual(old, cur) {
|
||||
sec := cur.(*apiv1.Secret)
|
||||
key := fmt.Sprintf("%v/%v", sec.Namespace, sec.Name)
|
||||
ic.syncSecret(key)
|
||||
}
|
||||
},
|
||||
DeleteFunc: func(obj interface{}) {
|
||||
sec, ok := obj.(*apiv1.Secret)
|
||||
if !ok {
|
||||
// If we reached here it means the secret was deleted but its final state is unrecorded.
|
||||
tombstone, ok := obj.(cache.DeletedFinalStateUnknown)
|
||||
if !ok {
|
||||
glog.Errorf("couldn't get object from tombstone %#v", obj)
|
||||
return
|
||||
}
|
||||
sec, ok = tombstone.Obj.(*apiv1.Secret)
|
||||
if !ok {
|
||||
glog.Errorf("Tombstone contained object that is not a Secret: %#v", obj)
|
||||
return
|
||||
}
|
||||
}
|
||||
key := fmt.Sprintf("%v/%v", sec.Namespace, sec.Name)
|
||||
ic.sslCertTracker.DeleteAll(key)
|
||||
ic.syncQueue.Enqueue(key)
|
||||
},
|
||||
}
|
||||
|
||||
eventHandler := cache.ResourceEventHandlerFuncs{
|
||||
AddFunc: func(obj interface{}) {
|
||||
ic.syncQueue.Enqueue(obj)
|
||||
},
|
||||
DeleteFunc: func(obj interface{}) {
|
||||
ic.syncQueue.Enqueue(obj)
|
||||
},
|
||||
UpdateFunc: func(old, cur interface{}) {
|
||||
oep := old.(*apiv1.Endpoints)
|
||||
ocur := cur.(*apiv1.Endpoints)
|
||||
if !reflect.DeepEqual(ocur.Subsets, oep.Subsets) {
|
||||
ic.syncQueue.Enqueue(cur)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
mapEventHandler := cache.ResourceEventHandlerFuncs{
|
||||
AddFunc: func(obj interface{}) {
|
||||
upCmap := obj.(*apiv1.ConfigMap)
|
||||
mapKey := fmt.Sprintf("%s/%s", upCmap.Namespace, upCmap.Name)
|
||||
if mapKey == ic.cfg.ConfigMapName {
|
||||
glog.V(2).Infof("adding configmap %v to backend", mapKey)
|
||||
ic.cfg.Backend.SetConfig(upCmap)
|
||||
ic.setForceReload(true)
|
||||
}
|
||||
},
|
||||
UpdateFunc: func(old, cur interface{}) {
|
||||
if !reflect.DeepEqual(old, cur) {
|
||||
upCmap := cur.(*apiv1.ConfigMap)
|
||||
mapKey := fmt.Sprintf("%s/%s", upCmap.Namespace, upCmap.Name)
|
||||
if mapKey == ic.cfg.ConfigMapName {
|
||||
glog.V(2).Infof("updating configmap backend (%v)", mapKey)
|
||||
ic.cfg.Backend.SetConfig(upCmap)
|
||||
ic.setForceReload(true)
|
||||
}
|
||||
// updates to configuration configmaps can trigger an update
|
||||
if mapKey == ic.cfg.ConfigMapName || mapKey == ic.cfg.TCPConfigMapName || mapKey == ic.cfg.UDPConfigMapName {
|
||||
ic.recorder.Eventf(upCmap, apiv1.EventTypeNormal, "UPDATE", fmt.Sprintf("ConfigMap %v", mapKey))
|
||||
ic.syncQueue.Enqueue(cur)
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
watchNs := apiv1.NamespaceAll
|
||||
if ic.cfg.ForceNamespaceIsolation && ic.cfg.Namespace != apiv1.NamespaceAll {
|
||||
watchNs = ic.cfg.Namespace
|
||||
}
|
||||
|
||||
lister := &ingress.StoreLister{}
|
||||
|
||||
controller := &cacheController{}
|
||||
|
||||
lister.Ingress.Store, controller.Ingress = cache.NewInformer(
|
||||
cache.NewListWatchFromClient(ic.cfg.Client.ExtensionsV1beta1().RESTClient(), "ingresses", ic.cfg.Namespace, fields.Everything()),
|
||||
&extensions.Ingress{}, ic.cfg.ResyncPeriod, ingEventHandler)
|
||||
|
||||
lister.Endpoint.Store, controller.Endpoint = cache.NewInformer(
|
||||
cache.NewListWatchFromClient(ic.cfg.Client.CoreV1().RESTClient(), "endpoints", ic.cfg.Namespace, fields.Everything()),
|
||||
&apiv1.Endpoints{}, ic.cfg.ResyncPeriod, eventHandler)
|
||||
|
||||
lister.Secret.Store, controller.Secret = cache.NewInformer(
|
||||
cache.NewListWatchFromClient(ic.cfg.Client.CoreV1().RESTClient(), "secrets", watchNs, fields.Everything()),
|
||||
&apiv1.Secret{}, ic.cfg.ResyncPeriod, secrEventHandler)
|
||||
|
||||
lister.ConfigMap.Store, controller.Configmap = cache.NewInformer(
|
||||
cache.NewListWatchFromClient(ic.cfg.Client.CoreV1().RESTClient(), "configmaps", watchNs, fields.Everything()),
|
||||
&apiv1.ConfigMap{}, ic.cfg.ResyncPeriod, mapEventHandler)
|
||||
|
||||
lister.Service.Store, controller.Service = cache.NewInformer(
|
||||
cache.NewListWatchFromClient(ic.cfg.Client.CoreV1().RESTClient(), "services", ic.cfg.Namespace, fields.Everything()),
|
||||
&apiv1.Service{}, ic.cfg.ResyncPeriod, cache.ResourceEventHandlerFuncs{})
|
||||
|
||||
var nodeListerWatcher cache.ListerWatcher
|
||||
if disableNodeLister {
|
||||
nodeListerWatcher = fcache.NewFakeControllerSource()
|
||||
} else {
|
||||
nodeListerWatcher = cache.NewListWatchFromClient(ic.cfg.Client.CoreV1().RESTClient(), "nodes", apiv1.NamespaceAll, fields.Everything())
|
||||
}
|
||||
lister.Node.Store, controller.Node = cache.NewInformer(
|
||||
nodeListerWatcher,
|
||||
&apiv1.Node{}, ic.cfg.ResyncPeriod, cache.ResourceEventHandlerFuncs{})
|
||||
|
||||
return lister, controller
|
||||
}
|
||||
84
pkg/ingress/controller/metrics.go
Normal file
84
pkg/ingress/controller/metrics.go
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
Copyright 2015 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 controller
|
||||
|
||||
import (
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
|
||||
"k8s.io/ingress-nginx/pkg/ingress"
|
||||
)
|
||||
|
||||
const (
|
||||
ns = "ingress_controller"
|
||||
operation = "count"
|
||||
reloadLabel = "reloads"
|
||||
sslLabelExpire = "ssl_expire_time_seconds"
|
||||
sslLabelHost = "host"
|
||||
)
|
||||
|
||||
func init() {
|
||||
prometheus.MustRegister(reloadOperation)
|
||||
prometheus.MustRegister(reloadOperationErrors)
|
||||
prometheus.MustRegister(sslExpireTime)
|
||||
|
||||
}
|
||||
|
||||
var (
|
||||
reloadOperation = prometheus.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Namespace: ns,
|
||||
Name: "success",
|
||||
Help: "Cumulative number of Ingress controller reload operations",
|
||||
},
|
||||
[]string{operation},
|
||||
)
|
||||
reloadOperationErrors = prometheus.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Namespace: ns,
|
||||
Name: "errors",
|
||||
Help: "Cumulative number of Ingress controller errors during reload operations",
|
||||
},
|
||||
[]string{operation},
|
||||
)
|
||||
sslExpireTime = prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Namespace: ns,
|
||||
Name: sslLabelExpire,
|
||||
Help: "Number of seconds since 1970 to the SSL Certificate expire. An example to check if this " +
|
||||
"certificate will expire in 10 days is: \"ingress_controller_ssl_expire_time_seconds < (time() + (10 * 24 * 3600))\"",
|
||||
},
|
||||
[]string{sslLabelHost},
|
||||
)
|
||||
)
|
||||
|
||||
func incReloadCount() {
|
||||
reloadOperation.WithLabelValues(reloadLabel).Inc()
|
||||
}
|
||||
|
||||
func incReloadErrorCount() {
|
||||
reloadOperationErrors.WithLabelValues(reloadLabel).Inc()
|
||||
}
|
||||
|
||||
func setSSLExpireTime(servers []*ingress.Server) {
|
||||
|
||||
for _, s := range servers {
|
||||
if s.Hostname != defServerName {
|
||||
sslExpireTime.WithLabelValues(s.Hostname).Set(float64(s.SSLExpireTime.Unix()))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
55
pkg/ingress/controller/util.go
Normal file
55
pkg/ingress/controller/util.go
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
Copyright 2015 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 controller
|
||||
|
||||
import (
|
||||
"github.com/golang/glog"
|
||||
|
||||
"github.com/imdario/mergo"
|
||||
|
||||
api "k8s.io/api/core/v1"
|
||||
|
||||
"k8s.io/ingress-nginx/pkg/ingress"
|
||||
)
|
||||
|
||||
// DeniedKeyName name of the key that contains the reason to deny a location
|
||||
const DeniedKeyName = "Denied"
|
||||
|
||||
// newUpstream creates an upstream without servers.
|
||||
func newUpstream(name string) *ingress.Backend {
|
||||
return &ingress.Backend{
|
||||
Name: name,
|
||||
Endpoints: []ingress.Endpoint{},
|
||||
Service: &api.Service{},
|
||||
SessionAffinity: ingress.SessionAffinityConfig{
|
||||
CookieSessionAffinity: ingress.CookieSessionAffinity{
|
||||
Locations: make(map[string][]string),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func mergeLocationAnnotations(loc *ingress.Location, anns map[string]interface{}) {
|
||||
if _, ok := anns[DeniedKeyName]; ok {
|
||||
loc.Denied = anns[DeniedKeyName].(error)
|
||||
}
|
||||
delete(anns, DeniedKeyName)
|
||||
err := mergo.Map(loc, anns)
|
||||
if err != nil {
|
||||
glog.Errorf("unexpected error merging extracted annotations in location type: %v", err)
|
||||
}
|
||||
}
|
||||
82
pkg/ingress/controller/util_test.go
Normal file
82
pkg/ingress/controller/util_test.go
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
Copyright 2015 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 controller
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"k8s.io/ingress-nginx/pkg/ingress"
|
||||
"k8s.io/ingress-nginx/pkg/ingress/annotations/auth"
|
||||
"k8s.io/ingress-nginx/pkg/ingress/annotations/authreq"
|
||||
"k8s.io/ingress-nginx/pkg/ingress/annotations/ipwhitelist"
|
||||
"k8s.io/ingress-nginx/pkg/ingress/annotations/proxy"
|
||||
"k8s.io/ingress-nginx/pkg/ingress/annotations/ratelimit"
|
||||
"k8s.io/ingress-nginx/pkg/ingress/annotations/redirect"
|
||||
"k8s.io/ingress-nginx/pkg/ingress/annotations/rewrite"
|
||||
)
|
||||
|
||||
type fakeError struct{}
|
||||
|
||||
func (fe *fakeError) Error() string {
|
||||
return "fakeError"
|
||||
}
|
||||
|
||||
func TestMergeLocationAnnotations(t *testing.T) {
|
||||
// initial parameters
|
||||
loc := ingress.Location{}
|
||||
annotations := map[string]interface{}{
|
||||
"Path": "/checkpath",
|
||||
"IsDefBackend": true,
|
||||
"Backend": "foo_backend",
|
||||
"BasicDigestAuth": auth.BasicDigest{},
|
||||
DeniedKeyName: &fakeError{},
|
||||
"EnableCORS": true,
|
||||
"ExternalAuth": authreq.External{},
|
||||
"RateLimit": ratelimit.RateLimit{},
|
||||
"Redirect": redirect.Redirect{},
|
||||
"Rewrite": rewrite.Redirect{},
|
||||
"Whitelist": ipwhitelist.SourceRange{},
|
||||
"Proxy": proxy.Configuration{},
|
||||
"UsePortInRedirects": true,
|
||||
}
|
||||
|
||||
// create test table
|
||||
type fooMergeLocationAnnotationsStruct struct {
|
||||
fName string
|
||||
er interface{}
|
||||
}
|
||||
fooTests := []fooMergeLocationAnnotationsStruct{}
|
||||
for name, value := range annotations {
|
||||
fva := fooMergeLocationAnnotationsStruct{name, value}
|
||||
fooTests = append(fooTests, fva)
|
||||
}
|
||||
|
||||
// execute test
|
||||
mergeLocationAnnotations(&loc, annotations)
|
||||
|
||||
// check result
|
||||
for _, foo := range fooTests {
|
||||
fv := reflect.ValueOf(loc).FieldByName(foo.fName).Interface()
|
||||
if !reflect.DeepEqual(fv, foo.er) {
|
||||
t.Errorf("Returned %v but expected %v for the field %s", fv, foo.er, foo.fName)
|
||||
}
|
||||
}
|
||||
if _, ok := annotations[DeniedKeyName]; ok {
|
||||
t.Errorf("%s should be removed after mergeLocationAnnotations", DeniedKeyName)
|
||||
}
|
||||
}
|
||||
109
pkg/ingress/defaults/main.go
Normal file
109
pkg/ingress/defaults/main.go
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
package defaults
|
||||
|
||||
import "net"
|
||||
|
||||
// Backend defines the mandatory configuration that an Ingress controller must provide
|
||||
// The reason of this requirements is the annotations are generic. If some implementation do not supports
|
||||
// one or more annotations it just can provides defaults
|
||||
type Backend struct {
|
||||
// AppRoot contains the AppRoot for apps that doesn't exposes its content in the 'root' context
|
||||
AppRoot string `json:"app-root"`
|
||||
|
||||
// enables which HTTP codes should be passed for processing with the error_page directive
|
||||
// http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_intercept_errors
|
||||
// http://nginx.org/en/docs/http/ngx_http_core_module.html#error_page
|
||||
// By default this is disabled
|
||||
CustomHTTPErrors []int `json:"custom-http-errors,-"`
|
||||
|
||||
// http://nginx.org/en/docs/http/ngx_http_core_module.html#client_max_body_size
|
||||
// Sets the maximum allowed size of the client request body
|
||||
ProxyBodySize string `json:"proxy-body-size"`
|
||||
|
||||
// Defines a timeout for establishing a connection with a proxied server.
|
||||
// It should be noted that this timeout cannot usually exceed 75 seconds.
|
||||
// http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_connect_timeout
|
||||
ProxyConnectTimeout int `json:"proxy-connect-timeout"`
|
||||
|
||||
// Timeout in seconds for reading a response from the proxied server. The timeout is set only between
|
||||
// two successive read operations, not for the transmission of the whole response
|
||||
// http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_read_timeout
|
||||
ProxyReadTimeout int `json:"proxy-read-timeout"`
|
||||
|
||||
// Timeout in seconds for transmitting a request to the proxied server. The timeout is set only between
|
||||
// two successive write operations, not for the transmission of the whole request.
|
||||
// http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_send_timeout
|
||||
ProxySendTimeout int `json:"proxy-send-timeout"`
|
||||
|
||||
// Sets the size of the buffer used for reading the first part of the response received from the
|
||||
// proxied server. This part usually contains a small response header.
|
||||
// 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"`
|
||||
|
||||
// Specifies in which cases a request should be passed to the next server.
|
||||
// http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_next_upstream
|
||||
ProxyNextUpstream string `json:"proxy-next-upstream"`
|
||||
|
||||
// Parameters for proxy-pass directive (eg. Apache web server).
|
||||
ProxyPassParams string `json:"proxy-pass-params"`
|
||||
|
||||
// Enables or disables buffering of a client request body.
|
||||
// http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_request_buffering
|
||||
ProxyRequestBuffering string `json:"proxy-request-buffering"`
|
||||
|
||||
// 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
|
||||
|
||||
// SkipAccessLogURLs sets a list of URLs that should not appear in the NGINX access log
|
||||
// This is useful with urls like `/health` or `health-check` that make "complex" reading the logs
|
||||
// By default this list is empty
|
||||
SkipAccessLogURLs []string `json:"skip-access-log-urls,-"`
|
||||
|
||||
// Enables or disables the redirect (301) to the HTTPS port
|
||||
SSLRedirect bool `json:"ssl-redirect"`
|
||||
|
||||
// Enables or disables the redirect (301) to the HTTPS port even without TLS cert
|
||||
// This is useful if doing SSL offloading outside of cluster eg AWS ELB
|
||||
ForceSSLRedirect bool `json:"force-ssl-redirect"`
|
||||
|
||||
// Enables or disables the specification of port in redirects
|
||||
// Default: false
|
||||
UsePortInRedirects bool `json:"use-port-in-redirects"`
|
||||
|
||||
// Number of unsuccessful attempts to communicate with the server that should happen in the
|
||||
// duration set by the fail_timeout parameter to consider the server unavailable
|
||||
// http://nginx.org/en/docs/http/ngx_http_upstream_module.html#upstream
|
||||
// Default: 0, ie use platform liveness probe
|
||||
UpstreamMaxFails int `json:"upstream-max-fails"`
|
||||
|
||||
// Time during which the specified number of unsuccessful attempts to communicate with
|
||||
// the server should happen to consider the server unavailable
|
||||
// http://nginx.org/en/docs/http/ngx_http_upstream_module.html#upstream
|
||||
// Default: 0, ie use platform liveness probe
|
||||
UpstreamFailTimeout int `json:"upstream-fail-timeout"`
|
||||
|
||||
// WhitelistSourceRange allows limiting access to certain client addresses
|
||||
// http://nginx.org/en/docs/http/ngx_http_access_module.html
|
||||
WhitelistSourceRange []string `json:"whitelist-source-range,-"`
|
||||
|
||||
// Limits the rate of response transmission to a client.
|
||||
// The rate is specified in bytes per second. The zero value disables rate limiting.
|
||||
// The limit is set per a request, and so if a client simultaneously opens two connections,
|
||||
// the overall rate will be twice as much as the specified limit.
|
||||
// http://nginx.org/en/docs/http/ngx_http_core_module.html#limit_rate
|
||||
LimitRate int `json:"limit-rate"`
|
||||
|
||||
// Sets the initial amount after which the further transmission of a response to a client will be rate limited.
|
||||
// http://nginx.org/en/docs/http/ngx_http_core_module.html#limit_rate_after
|
||||
LimitRateAfter int `json:"limit-rate-after"`
|
||||
}
|
||||
71
pkg/ingress/doc.go
Normal file
71
pkg/ingress/doc.go
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
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 ingress
|
||||
|
||||
// This package contains the interface is required to implement to build an Ingress controller
|
||||
// A dummy implementation could be
|
||||
//
|
||||
// func main() {
|
||||
// dc := newDummyController()
|
||||
// controller.NewIngressController(dc)
|
||||
// glog.Infof("shutting down Ingress controller...")
|
||||
// }
|
||||
//
|
||||
//where newDummyController returns and implementation of the Controller interface:
|
||||
//
|
||||
// func newDummyController() ingress.Controller {
|
||||
// return &DummyController{}
|
||||
// }
|
||||
//
|
||||
// type DummyController struct {
|
||||
// }
|
||||
//
|
||||
// func (dc DummyController) Reload(data []byte) ([]byte, error) {
|
||||
// err := ioutil.WriteFile("/arbitrary-path", data, 0644)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
//
|
||||
// return exec.Command("some command", "--reload").CombinedOutput()
|
||||
// }
|
||||
//
|
||||
// func (dc DummyController) Test(file string) *exec.Cmd {
|
||||
// return exec.Command("some command", "--config-file", file)
|
||||
// }
|
||||
//
|
||||
// func (dc DummyController) OnUpdate(*api.ConfigMap, Configuration) ([]byte, error) {
|
||||
// return []byte(`<string containing a configuration file>`)
|
||||
// }
|
||||
//
|
||||
// func (dc DummyController) BackendDefaults() defaults.Backend {
|
||||
// return ingress.NewStandardDefaults()
|
||||
// }
|
||||
//
|
||||
// func (n DummyController) Name() string {
|
||||
// return "dummy Controller"
|
||||
// }
|
||||
//
|
||||
// func (n DummyController) Check(_ *http.Request) error {
|
||||
// return nil
|
||||
// }
|
||||
//
|
||||
// func (dc DummyController) Info() *BackendInfo {
|
||||
// Name: "dummy",
|
||||
// Release: "0.0.0",
|
||||
// Build: "git-00000000",
|
||||
// Repository: "git://foo.bar.com",
|
||||
// }
|
||||
93
pkg/ingress/errors/errors.go
Normal file
93
pkg/ingress/errors/errors.go
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
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 errors
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrMissingAnnotations the ingress rule does not contain annotations
|
||||
// This is an error only when annotations are being parsed
|
||||
ErrMissingAnnotations = errors.New("ingress rule without annotations")
|
||||
|
||||
// ErrInvalidAnnotationName the ingress rule does contains an invalid
|
||||
// annotation name
|
||||
ErrInvalidAnnotationName = errors.New("invalid annotation name")
|
||||
)
|
||||
|
||||
// NewInvalidAnnotationContent returns a new InvalidContent error
|
||||
func NewInvalidAnnotationContent(name string, val interface{}) error {
|
||||
return InvalidContent{
|
||||
Name: fmt.Sprintf("the annotation %v does not contain a valid value (%v)", name, val),
|
||||
}
|
||||
}
|
||||
|
||||
// NewLocationDenied returns a new LocationDenied error
|
||||
func NewLocationDenied(reason string) error {
|
||||
return LocationDenied{
|
||||
Reason: errors.Errorf("Location denied, reason: %v", reason),
|
||||
}
|
||||
}
|
||||
|
||||
// InvalidContent error
|
||||
type InvalidContent struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
func (e InvalidContent) Error() string {
|
||||
return e.Name
|
||||
}
|
||||
|
||||
// LocationDenied error
|
||||
type LocationDenied struct {
|
||||
Reason error
|
||||
}
|
||||
|
||||
func (e LocationDenied) Error() string {
|
||||
return e.Reason.Error()
|
||||
}
|
||||
|
||||
// IsLocationDenied checks if the err is an error which
|
||||
// indicates a location should return HTTP code 503
|
||||
func IsLocationDenied(e error) bool {
|
||||
_, ok := e.(LocationDenied)
|
||||
return ok
|
||||
}
|
||||
|
||||
// IsMissingAnnotations checks if the err is an error which
|
||||
// indicates the ingress does not contain annotations
|
||||
func IsMissingAnnotations(e error) bool {
|
||||
return e == ErrMissingAnnotations
|
||||
}
|
||||
|
||||
// IsInvalidContent checks if the err is an error which
|
||||
// indicates an annotations value is not valid
|
||||
func IsInvalidContent(e error) bool {
|
||||
_, ok := e.(InvalidContent)
|
||||
return ok
|
||||
}
|
||||
|
||||
func New(m string) error {
|
||||
return errors.New(m)
|
||||
}
|
||||
|
||||
func Errorf(format string, args ...interface{}) error {
|
||||
return errors.Errorf(format, args)
|
||||
}
|
||||
52
pkg/ingress/errors/errors_test.go
Normal file
52
pkg/ingress/errors/errors_test.go
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
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 errors
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestIsLocationDenied(t *testing.T) {
|
||||
err := NewLocationDenied("demo")
|
||||
if !IsLocationDenied(err) {
|
||||
t.Error("expected true")
|
||||
}
|
||||
if IsLocationDenied(nil) {
|
||||
t.Error("expected false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsMissingAnnotations(t *testing.T) {
|
||||
if !IsMissingAnnotations(ErrMissingAnnotations) {
|
||||
t.Error("expected true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidContent(t *testing.T) {
|
||||
if IsInvalidContent(ErrMissingAnnotations) {
|
||||
t.Error("expected false")
|
||||
}
|
||||
err := NewInvalidAnnotationContent("demo", "")
|
||||
if !IsInvalidContent(err) {
|
||||
t.Error("expected true")
|
||||
}
|
||||
if IsInvalidContent(nil) {
|
||||
t.Error("expected false")
|
||||
}
|
||||
err = NewLocationDenied("demo")
|
||||
if IsInvalidContent(err) {
|
||||
t.Error("expected false")
|
||||
}
|
||||
}
|
||||
74
pkg/ingress/resolver/main.go
Normal file
74
pkg/ingress/resolver/main.go
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
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 resolver
|
||||
|
||||
import (
|
||||
apiv1 "k8s.io/api/core/v1"
|
||||
|
||||
"k8s.io/ingress-nginx/pkg/ingress/defaults"
|
||||
)
|
||||
|
||||
// DefaultBackend has a method that returns the backend
|
||||
// that must be used as default
|
||||
type DefaultBackend interface {
|
||||
GetDefaultBackend() defaults.Backend
|
||||
}
|
||||
|
||||
// Secret has a method that searches for secrets contenating
|
||||
// the namespace and name using a the character /
|
||||
type Secret interface {
|
||||
GetSecret(string) (*apiv1.Secret, error)
|
||||
}
|
||||
|
||||
// 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
|
||||
type AuthCertificate interface {
|
||||
GetAuthCertificate(string) (*AuthSSLCert, error)
|
||||
}
|
||||
|
||||
// Service has a method that searches for services contenating
|
||||
// the namespace and name using a the character /
|
||||
type Service interface {
|
||||
GetService(string) (*apiv1.Service, error)
|
||||
}
|
||||
|
||||
// AuthSSLCert contains the necessary information to do certificate based
|
||||
// authentication of an ingress location
|
||||
type AuthSSLCert struct {
|
||||
// Secret contains the name of the secret this was fetched from
|
||||
Secret string `json:"secret"`
|
||||
// CAFileName contains the path to the secrets 'ca.crt'
|
||||
CAFileName string `json:"caFilename"`
|
||||
// PemSHA contains the SHA1 hash of the 'ca.crt' or combinations of (tls.crt, tls.key, tls.crt) depending on certs in secret
|
||||
PemSHA string `json:"pemSha"`
|
||||
}
|
||||
|
||||
// Equal tests for equality between two AuthSSLCert types
|
||||
func (asslc1 *AuthSSLCert) Equal(assl2 *AuthSSLCert) bool {
|
||||
if asslc1.Secret != assl2.Secret {
|
||||
return false
|
||||
}
|
||||
if asslc1.CAFileName != assl2.CAFileName {
|
||||
return false
|
||||
}
|
||||
if asslc1.PemSHA != assl2.PemSHA {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
50
pkg/ingress/sort_ingress.go
Normal file
50
pkg/ingress/sort_ingress.go
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
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 ingress
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"time"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
// SSLCert describes a SSL certificate to be used in a server
|
||||
type SSLCert struct {
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
Certificate *x509.Certificate `json:"certificate,omitempty"`
|
||||
// CAFileName contains the path to the file with the root certificate
|
||||
CAFileName string `json:"caFileName"`
|
||||
// PemFileName contains the path to the file with the certificate and key concatenated
|
||||
PemFileName string `json:"pemFileName"`
|
||||
// FullChainPemFileName contains the path to the file with the certificate and key concatenated
|
||||
// This certificate contains the full chain (ca + intermediates + cert)
|
||||
FullChainPemFileName string `json:"fullChainPemFileName"`
|
||||
// PemSHA contains the sha1 of the pem file.
|
||||
// This is used to detect changes in the secret that contains the certificates
|
||||
PemSHA string `json:"pemSha"`
|
||||
// CN contains all the common names defined in the SSL certificate
|
||||
CN []string `json:"cn"`
|
||||
// ExpiresTime contains the expiration of this SSL certificate in timestamp format
|
||||
ExpireTime time.Time `json:"expires"`
|
||||
}
|
||||
|
||||
// GetObjectKind implements the ObjectKind interface as a noop
|
||||
func (s SSLCert) GetObjectKind() schema.ObjectKind {
|
||||
return schema.EmptyObjectKind
|
||||
}
|
||||
38
pkg/ingress/sort_ingress_test.go
Normal file
38
pkg/ingress/sort_ingress_test.go
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
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 ingress
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
func TestGetObjectKindForSSLCert(t *testing.T) {
|
||||
fk := &SSLCert{
|
||||
ObjectMeta: metav1.ObjectMeta{},
|
||||
CAFileName: "ca_file",
|
||||
PemFileName: "pemfile",
|
||||
PemSHA: "pem_sha",
|
||||
CN: []string{},
|
||||
}
|
||||
|
||||
r := fk.GetObjectKind()
|
||||
if r == nil {
|
||||
t.Errorf("Returned nil but expected a valid ObjectKind")
|
||||
}
|
||||
}
|
||||
399
pkg/ingress/status/status.go
Normal file
399
pkg/ingress/status/status.go
Normal file
|
|
@ -0,0 +1,399 @@
|
|||
/*
|
||||
Copyright 2015 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 (
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/golang/glog"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
pool "gopkg.in/go-playground/pool.v3"
|
||||
apiv1 "k8s.io/api/core/v1"
|
||||
extensions "k8s.io/api/extensions/v1beta1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
clientset "k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/kubernetes/scheme"
|
||||
"k8s.io/client-go/tools/leaderelection"
|
||||
"k8s.io/client-go/tools/leaderelection/resourcelock"
|
||||
"k8s.io/client-go/tools/record"
|
||||
"k8s.io/kubernetes/pkg/kubelet/util/sliceutils"
|
||||
|
||||
"k8s.io/ingress-nginx/pkg/ingress/annotations/class"
|
||||
"k8s.io/ingress-nginx/pkg/ingress/store"
|
||||
"k8s.io/ingress-nginx/pkg/k8s"
|
||||
"k8s.io/ingress-nginx/pkg/task"
|
||||
)
|
||||
|
||||
const (
|
||||
updateInterval = 60 * time.Second
|
||||
)
|
||||
|
||||
// Sync ...
|
||||
type Sync interface {
|
||||
Run(stopCh <-chan struct{})
|
||||
Shutdown()
|
||||
}
|
||||
|
||||
// Config ...
|
||||
type Config struct {
|
||||
Client clientset.Interface
|
||||
|
||||
PublishService string
|
||||
|
||||
ElectionID string
|
||||
|
||||
UpdateStatusOnShutdown bool
|
||||
|
||||
IngressLister store.IngressLister
|
||||
|
||||
DefaultIngressClass string
|
||||
IngressClass string
|
||||
|
||||
// CustomIngressStatus allows to set custom values in Ingress status
|
||||
CustomIngressStatus func(*extensions.Ingress) []apiv1.LoadBalancerIngress
|
||||
}
|
||||
|
||||
// statusSync keeps the status IP in each Ingress rule updated executing a periodic check
|
||||
// in all the defined rules. To simplify the process leader election is used so the update
|
||||
// is executed only in one node (Ingress controllers can be scaled to more than one)
|
||||
// If the controller is running with the flag --publish-service (with a valid service)
|
||||
// the IP address behind the service is used, if not the source is the IP/s of the node/s
|
||||
type statusSync struct {
|
||||
Config
|
||||
// pod contains runtime information about this pod
|
||||
pod *k8s.PodInfo
|
||||
|
||||
elector *leaderelection.LeaderElector
|
||||
// workqueue used to keep in sync the status IP/s
|
||||
// in the Ingress rules
|
||||
syncQueue *task.Queue
|
||||
}
|
||||
|
||||
// Run starts the loop to keep the status in sync
|
||||
func (s statusSync) Run(stopCh <-chan struct{}) {
|
||||
go s.elector.Run()
|
||||
go wait.Forever(s.update, updateInterval)
|
||||
go s.syncQueue.Run(time.Second, stopCh)
|
||||
<-stopCh
|
||||
}
|
||||
|
||||
func (s *statusSync) update() {
|
||||
// send a dummy object to the queue to force a sync
|
||||
s.syncQueue.Enqueue("sync status")
|
||||
}
|
||||
|
||||
// Shutdown stop the sync. In case the instance is the leader it will remove the current IP
|
||||
// if there is no other instances running.
|
||||
func (s statusSync) Shutdown() {
|
||||
go s.syncQueue.Shutdown()
|
||||
// remove IP from Ingress
|
||||
if !s.elector.IsLeader() {
|
||||
return
|
||||
}
|
||||
|
||||
if !s.UpdateStatusOnShutdown {
|
||||
glog.Warningf("skipping update of status of Ingress rules")
|
||||
return
|
||||
}
|
||||
|
||||
glog.Infof("updating status of Ingress rules (remove)")
|
||||
|
||||
addrs, err := s.runningAddresses()
|
||||
if err != nil {
|
||||
glog.Errorf("error obtaining running IPs: %v", addrs)
|
||||
return
|
||||
}
|
||||
|
||||
if len(addrs) > 1 {
|
||||
// leave the job to the next leader
|
||||
glog.Infof("leaving status update for next leader (%v)", len(addrs))
|
||||
return
|
||||
}
|
||||
|
||||
if s.isRunningMultiplePods() {
|
||||
glog.V(2).Infof("skipping Ingress status update (multiple pods running - another one will be elected as master)")
|
||||
return
|
||||
}
|
||||
|
||||
glog.Infof("removing address from ingress status (%v)", addrs)
|
||||
s.updateStatus([]apiv1.LoadBalancerIngress{})
|
||||
}
|
||||
|
||||
func (s *statusSync) sync(key interface{}) error {
|
||||
if s.syncQueue.IsShuttingDown() {
|
||||
glog.V(2).Infof("skipping Ingress status update (shutting down in progress)")
|
||||
return nil
|
||||
}
|
||||
|
||||
if !s.elector.IsLeader() {
|
||||
glog.V(2).Infof("skipping Ingress status update (I am not the current leader)")
|
||||
return nil
|
||||
}
|
||||
|
||||
addrs, err := s.runningAddresses()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.updateStatus(sliceToStatus(addrs))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s statusSync) keyfunc(input interface{}) (interface{}, error) {
|
||||
return input, nil
|
||||
}
|
||||
|
||||
// NewStatusSyncer returns a new Sync instance
|
||||
func NewStatusSyncer(config Config) Sync {
|
||||
pod, err := k8s.GetPodDetails(config.Client)
|
||||
if err != nil {
|
||||
glog.Fatalf("unexpected error obtaining pod information: %v", err)
|
||||
}
|
||||
|
||||
st := statusSync{
|
||||
pod: pod,
|
||||
|
||||
Config: config,
|
||||
}
|
||||
st.syncQueue = task.NewCustomTaskQueue(st.sync, st.keyfunc)
|
||||
|
||||
// we need to use the defined ingress class to allow multiple leaders
|
||||
// in order to update information about ingress status
|
||||
electionID := fmt.Sprintf("%v-%v", config.ElectionID, config.DefaultIngressClass)
|
||||
if config.IngressClass != "" {
|
||||
electionID = fmt.Sprintf("%v-%v", config.ElectionID, config.IngressClass)
|
||||
}
|
||||
|
||||
callbacks := leaderelection.LeaderCallbacks{
|
||||
OnStartedLeading: func(stop <-chan struct{}) {
|
||||
glog.V(2).Infof("I am the new status update leader")
|
||||
},
|
||||
OnStoppedLeading: func() {
|
||||
glog.V(2).Infof("I am not status update leader anymore")
|
||||
},
|
||||
OnNewLeader: func(identity string) {
|
||||
glog.Infof("new leader elected: %v", identity)
|
||||
},
|
||||
}
|
||||
|
||||
broadcaster := record.NewBroadcaster()
|
||||
hostname, _ := os.Hostname()
|
||||
|
||||
recorder := broadcaster.NewRecorder(scheme.Scheme, apiv1.EventSource{
|
||||
Component: "ingress-leader-elector",
|
||||
Host: hostname,
|
||||
})
|
||||
|
||||
lock := resourcelock.ConfigMapLock{
|
||||
ConfigMapMeta: metav1.ObjectMeta{Namespace: pod.Namespace, Name: electionID},
|
||||
Client: config.Client.CoreV1(),
|
||||
LockConfig: resourcelock.ResourceLockConfig{
|
||||
Identity: pod.Name,
|
||||
EventRecorder: recorder,
|
||||
},
|
||||
}
|
||||
|
||||
ttl := 30 * time.Second
|
||||
le, err := leaderelection.NewLeaderElector(leaderelection.LeaderElectionConfig{
|
||||
Lock: &lock,
|
||||
LeaseDuration: ttl,
|
||||
RenewDeadline: ttl / 2,
|
||||
RetryPeriod: ttl / 4,
|
||||
Callbacks: callbacks,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
glog.Fatalf("unexpected error starting leader election: %v", err)
|
||||
}
|
||||
|
||||
st.elector = le
|
||||
return st
|
||||
}
|
||||
|
||||
// runningAddresses returns a list of IP addresses and/or FQDN where the
|
||||
// ingress controller is currently running
|
||||
func (s *statusSync) runningAddresses() ([]string, error) {
|
||||
if s.PublishService != "" {
|
||||
ns, name, _ := k8s.ParseNameNS(s.PublishService)
|
||||
svc, err := s.Client.CoreV1().Services(ns).Get(name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
addrs := []string{}
|
||||
for _, ip := range svc.Status.LoadBalancer.Ingress {
|
||||
if ip.IP == "" {
|
||||
addrs = append(addrs, ip.Hostname)
|
||||
} else {
|
||||
addrs = append(addrs, ip.IP)
|
||||
}
|
||||
}
|
||||
for _, ip := range svc.Spec.ExternalIPs {
|
||||
addrs = append(addrs, ip)
|
||||
}
|
||||
|
||||
return addrs, nil
|
||||
}
|
||||
|
||||
// get information about all the pods running the ingress controller
|
||||
pods, err := s.Client.CoreV1().Pods(s.pod.Namespace).List(metav1.ListOptions{
|
||||
LabelSelector: labels.SelectorFromSet(s.pod.Labels).String(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
addrs := []string{}
|
||||
for _, pod := range pods.Items {
|
||||
name := k8s.GetNodeIP(s.Client, pod.Spec.NodeName)
|
||||
if !sliceutils.StringInSlice(name, addrs) {
|
||||
addrs = append(addrs, name)
|
||||
}
|
||||
}
|
||||
return addrs, nil
|
||||
}
|
||||
|
||||
func (s *statusSync) isRunningMultiplePods() bool {
|
||||
pods, err := s.Client.CoreV1().Pods(s.pod.Namespace).List(metav1.ListOptions{
|
||||
LabelSelector: labels.SelectorFromSet(s.pod.Labels).String(),
|
||||
})
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return len(pods.Items) > 1
|
||||
}
|
||||
|
||||
// sliceToStatus converts a slice of IP and/or hostnames to LoadBalancerIngress
|
||||
func sliceToStatus(endpoints []string) []apiv1.LoadBalancerIngress {
|
||||
lbi := []apiv1.LoadBalancerIngress{}
|
||||
for _, ep := range endpoints {
|
||||
if net.ParseIP(ep) == nil {
|
||||
lbi = append(lbi, apiv1.LoadBalancerIngress{Hostname: ep})
|
||||
} else {
|
||||
lbi = append(lbi, apiv1.LoadBalancerIngress{IP: ep})
|
||||
}
|
||||
}
|
||||
|
||||
sort.SliceStable(lbi, func(a, b int) bool {
|
||||
return lbi[a].IP < lbi[b].IP
|
||||
})
|
||||
|
||||
return lbi
|
||||
}
|
||||
|
||||
// updateStatus changes the status information of Ingress rules
|
||||
// If the backend function CustomIngressStatus returns a value different
|
||||
// of nil then it uses the returned value or the newIngressPoint values
|
||||
func (s *statusSync) updateStatus(newIngressPoint []apiv1.LoadBalancerIngress) {
|
||||
ings := s.IngressLister.List()
|
||||
|
||||
p := pool.NewLimited(10)
|
||||
defer p.Close()
|
||||
|
||||
batch := p.Batch()
|
||||
|
||||
for _, cur := range ings {
|
||||
ing := cur.(*extensions.Ingress)
|
||||
|
||||
if !class.IsValid(ing, s.Config.IngressClass, s.Config.DefaultIngressClass) {
|
||||
continue
|
||||
}
|
||||
|
||||
batch.Queue(runUpdate(ing, newIngressPoint, s.Client, s.CustomIngressStatus))
|
||||
}
|
||||
|
||||
batch.QueueComplete()
|
||||
batch.WaitAll()
|
||||
}
|
||||
|
||||
func runUpdate(ing *extensions.Ingress, status []apiv1.LoadBalancerIngress,
|
||||
client clientset.Interface,
|
||||
statusFunc func(*extensions.Ingress) []apiv1.LoadBalancerIngress) pool.WorkFunc {
|
||||
return func(wu pool.WorkUnit) (interface{}, error) {
|
||||
if wu.IsCancelled() {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
addrs := status
|
||||
ca := statusFunc(ing)
|
||||
if ca != nil {
|
||||
addrs = ca
|
||||
}
|
||||
sort.SliceStable(addrs, lessLoadBalancerIngress(addrs))
|
||||
|
||||
curIPs := ing.Status.LoadBalancer.Ingress
|
||||
sort.SliceStable(curIPs, lessLoadBalancerIngress(curIPs))
|
||||
|
||||
if ingressSliceEqual(addrs, curIPs) {
|
||||
glog.V(3).Infof("skipping update of Ingress %v/%v (no change)", ing.Namespace, ing.Name)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
ingClient := client.Extensions().Ingresses(ing.Namespace)
|
||||
|
||||
currIng, err := ingClient.Get(ing.Name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, fmt.Sprintf("unexpected error searching Ingress %v/%v", ing.Namespace, ing.Name))
|
||||
}
|
||||
|
||||
glog.Infof("updating Ingress %v/%v status to %v", currIng.Namespace, currIng.Name, addrs)
|
||||
currIng.Status.LoadBalancer.Ingress = addrs
|
||||
_, err = ingClient.UpdateStatus(currIng)
|
||||
if err != nil {
|
||||
glog.Warningf("error updating ingress rule: %v", err)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
func lessLoadBalancerIngress(addrs []apiv1.LoadBalancerIngress) func(int, int) bool {
|
||||
return func(a, b int) bool {
|
||||
switch strings.Compare(addrs[a].Hostname, addrs[b].Hostname) {
|
||||
case -1:
|
||||
return true
|
||||
case 1:
|
||||
return false
|
||||
}
|
||||
return addrs[a].IP < addrs[b].IP
|
||||
}
|
||||
}
|
||||
|
||||
func ingressSliceEqual(lhs, rhs []apiv1.LoadBalancerIngress) bool {
|
||||
if len(lhs) != len(rhs) {
|
||||
return false
|
||||
}
|
||||
|
||||
for i := range lhs {
|
||||
if lhs[i].IP != rhs[i].IP {
|
||||
return false
|
||||
}
|
||||
if lhs[i].Hostname != rhs[i].Hostname {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
463
pkg/ingress/status/status_test.go
Normal file
463
pkg/ingress/status/status_test.go
Normal file
|
|
@ -0,0 +1,463 @@
|
|||
/*
|
||||
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"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
apiv1 "k8s.io/api/core/v1"
|
||||
extensions "k8s.io/api/extensions/v1beta1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
testclient "k8s.io/client-go/kubernetes/fake"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
"k8s.io/kubernetes/pkg/api"
|
||||
|
||||
"k8s.io/ingress-nginx/pkg/ingress/annotations/class"
|
||||
"k8s.io/ingress-nginx/pkg/ingress/store"
|
||||
"k8s.io/ingress-nginx/pkg/k8s"
|
||||
"k8s.io/ingress-nginx/pkg/task"
|
||||
)
|
||||
|
||||
func buildLoadBalancerIngressByIP() []apiv1.LoadBalancerIngress {
|
||||
return []apiv1.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(
|
||||
&apiv1.PodList{Items: []apiv1.Pod{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "foo1",
|
||||
Namespace: apiv1.NamespaceDefault,
|
||||
Labels: map[string]string{
|
||||
"lable_sig": "foo_pod",
|
||||
},
|
||||
},
|
||||
Spec: apiv1.PodSpec{
|
||||
NodeName: "foo_node_2",
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "foo2",
|
||||
Namespace: apiv1.NamespaceDefault,
|
||||
Labels: map[string]string{
|
||||
"lable_sig": "foo_no",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "foo3",
|
||||
Namespace: api.NamespaceSystem,
|
||||
Labels: map[string]string{
|
||||
"lable_sig": "foo_pod",
|
||||
},
|
||||
},
|
||||
Spec: apiv1.PodSpec{
|
||||
NodeName: "foo_node_2",
|
||||
},
|
||||
},
|
||||
}},
|
||||
&apiv1.ServiceList{Items: []apiv1.Service{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "foo",
|
||||
Namespace: apiv1.NamespaceDefault,
|
||||
},
|
||||
Status: apiv1.ServiceStatus{
|
||||
LoadBalancer: apiv1.LoadBalancerStatus{
|
||||
Ingress: buildLoadBalancerIngressByIP(),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "foo_non_exist",
|
||||
Namespace: apiv1.NamespaceDefault,
|
||||
},
|
||||
},
|
||||
}},
|
||||
&apiv1.NodeList{Items: []apiv1.Node{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "foo_node_1",
|
||||
},
|
||||
Status: apiv1.NodeStatus{
|
||||
Addresses: []apiv1.NodeAddress{
|
||||
{
|
||||
Type: apiv1.NodeInternalIP,
|
||||
Address: "10.0.0.1",
|
||||
}, {
|
||||
Type: apiv1.NodeExternalIP,
|
||||
Address: "10.0.0.2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "foo_node_2",
|
||||
},
|
||||
Status: apiv1.NodeStatus{
|
||||
Addresses: []apiv1.NodeAddress{
|
||||
{
|
||||
Type: apiv1.NodeInternalIP,
|
||||
Address: "11.0.0.1",
|
||||
},
|
||||
{
|
||||
Type: apiv1.NodeExternalIP,
|
||||
Address: "11.0.0.2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}},
|
||||
&apiv1.EndpointsList{Items: []apiv1.Endpoints{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "ingress-controller-leader",
|
||||
Namespace: apiv1.NamespaceDefault,
|
||||
SelfLink: "/api/v1/namespaces/default/endpoints/ingress-controller-leader",
|
||||
},
|
||||
}}},
|
||||
&extensions.IngressList{Items: buildExtensionsIngresses()},
|
||||
)
|
||||
}
|
||||
|
||||
func fakeSynFn(interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildExtensionsIngresses() []extensions.Ingress {
|
||||
return []extensions.Ingress{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "foo_ingress_1",
|
||||
Namespace: apiv1.NamespaceDefault,
|
||||
},
|
||||
Status: extensions.IngressStatus{
|
||||
LoadBalancer: apiv1.LoadBalancerStatus{
|
||||
Ingress: []apiv1.LoadBalancerIngress{
|
||||
{
|
||||
IP: "10.0.0.1",
|
||||
Hostname: "foo1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "foo_ingress_different_class",
|
||||
Namespace: api.NamespaceDefault,
|
||||
Annotations: map[string]string{
|
||||
class.IngressKey: "no-nginx",
|
||||
},
|
||||
},
|
||||
Status: extensions.IngressStatus{
|
||||
LoadBalancer: apiv1.LoadBalancerStatus{
|
||||
Ingress: []apiv1.LoadBalancerIngress{
|
||||
{
|
||||
IP: "0.0.0.0",
|
||||
Hostname: "foo.bar.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "foo_ingress_2",
|
||||
Namespace: apiv1.NamespaceDefault,
|
||||
},
|
||||
Status: extensions.IngressStatus{
|
||||
LoadBalancer: apiv1.LoadBalancerStatus{
|
||||
Ingress: []apiv1.LoadBalancerIngress{},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func buildIngressListener() store.IngressLister {
|
||||
s := cache.NewStore(cache.MetaNamespaceKeyFunc)
|
||||
s.Add(&extensions.Ingress{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "foo_ingress_non_01",
|
||||
Namespace: apiv1.NamespaceDefault,
|
||||
}})
|
||||
s.Add(&extensions.Ingress{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "foo_ingress_1",
|
||||
Namespace: apiv1.NamespaceDefault,
|
||||
},
|
||||
Status: extensions.IngressStatus{
|
||||
LoadBalancer: apiv1.LoadBalancerStatus{
|
||||
Ingress: buildLoadBalancerIngressByIP(),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return store.IngressLister{Store: s}
|
||||
}
|
||||
|
||||
func buildStatusSync() statusSync {
|
||||
return statusSync{
|
||||
pod: &k8s.PodInfo{
|
||||
Name: "foo_base_pod",
|
||||
Namespace: apiv1.NamespaceDefault,
|
||||
Labels: map[string]string{
|
||||
"lable_sig": "foo_pod",
|
||||
},
|
||||
},
|
||||
syncQueue: task.NewTaskQueue(fakeSynFn),
|
||||
Config: Config{
|
||||
Client: buildSimpleClientSet(),
|
||||
PublishService: apiv1.NamespaceDefault + "/" + "foo",
|
||||
IngressLister: buildIngressListener(),
|
||||
CustomIngressStatus: func(*extensions.Ingress) []apiv1.LoadBalancerIngress {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestStatusActions(t *testing.T) {
|
||||
// make sure election can be created
|
||||
os.Setenv("POD_NAME", "foo1")
|
||||
os.Setenv("POD_NAMESPACE", apiv1.NamespaceDefault)
|
||||
c := Config{
|
||||
Client: buildSimpleClientSet(),
|
||||
PublishService: "",
|
||||
IngressLister: buildIngressListener(),
|
||||
DefaultIngressClass: "nginx",
|
||||
IngressClass: "",
|
||||
UpdateStatusOnShutdown: true,
|
||||
CustomIngressStatus: func(*extensions.Ingress) []apiv1.LoadBalancerIngress {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
// 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 := []apiv1.LoadBalancerIngress{{
|
||||
IP: "11.0.0.2",
|
||||
}}
|
||||
fooIngress1, err1 := fk.Client.Extensions().Ingresses(apiv1.NamespaceDefault).Get("foo_ingress_1", metav1.GetOptions{})
|
||||
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 := []apiv1.LoadBalancerIngress{}
|
||||
fooIngress2, err2 := fk.Client.Extensions().Ingresses(apiv1.NamespaceDefault).Get("foo_ingress_1", metav1.GetOptions{})
|
||||
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)
|
||||
}
|
||||
|
||||
oic, err := fk.Client.Extensions().Ingresses(api.NamespaceDefault).Get("foo_ingress_different_class", metav1.GetOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error")
|
||||
}
|
||||
if oic.Status.LoadBalancer.Ingress[0].IP != "0.0.0.0" && oic.Status.LoadBalancer.Ingress[0].Hostname != "foo.bar.com" {
|
||||
t.Fatalf("invalid ingress status for rule with different class")
|
||||
}
|
||||
|
||||
// end test
|
||||
ns <- struct{}{}
|
||||
}
|
||||
|
||||
func TestCallback(t *testing.T) {
|
||||
buildStatusSync()
|
||||
}
|
||||
|
||||
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.runningAddresses()
|
||||
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.runningAddresses()
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
TODO: this test requires a refactoring
|
||||
func TestUpdateStatus(t *testing.T) {
|
||||
fk := buildStatusSync()
|
||||
newIPs := buildLoadBalancerIngressByIP()
|
||||
fk.updateStatus(newIPs)
|
||||
|
||||
fooIngress1, err1 := fk.Client.Extensions().Ingresses(apiv1.NamespaceDefault).Get("foo_ingress_1", metav1.GetOptions{})
|
||||
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(apiv1.NamespaceDefault).Get("foo_ingress_2", metav1.GetOptions{})
|
||||
if err2 != nil {
|
||||
t.Fatalf("unexpected error")
|
||||
}
|
||||
fooIngress2CurIPs := fooIngress2.Status.LoadBalancer.Ingress
|
||||
if !ingressSliceEqual(fooIngress2CurIPs, []apiv1.LoadBalancerIngress{}) {
|
||||
t.Fatalf("returned %v but expected %v", fooIngress2CurIPs, []apiv1.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 []apiv1.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, apiv1.LoadBalancerIngress{Hostname: "opensource-k8s-ingress"})
|
||||
}
|
||||
re2 := r[1]
|
||||
if re2.IP != "10.0.0.1" {
|
||||
t.Fatalf("returned %v but expected %v", re2, apiv1.LoadBalancerIngress{IP: "10.0.0.1"})
|
||||
}
|
||||
re3 := r[2]
|
||||
if re3.IP != "2001:db8::68" {
|
||||
t.Fatalf("returned %v but expected %v", re3, apiv1.LoadBalancerIngress{IP: "2001:db8::68"})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIngressSliceEqual(t *testing.T) {
|
||||
fk1 := buildLoadBalancerIngressByIP()
|
||||
fk2 := append(buildLoadBalancerIngressByIP(), apiv1.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 []apiv1.LoadBalancerIngress
|
||||
rhs []apiv1.LoadBalancerIngress
|
||||
er bool
|
||||
}{
|
||||
{fk1, fk1, true},
|
||||
{fk2, fk1, false},
|
||||
{fk3, fk1, false},
|
||||
{fk4, fk1, false},
|
||||
{fk1, nil, false},
|
||||
{nil, nil, true},
|
||||
{[]apiv1.LoadBalancerIngress{}, []apiv1.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
102
pkg/ingress/store/main.go
Normal file
102
pkg/ingress/store/main.go
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
/*
|
||||
Copyright 2015 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 store
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
apiv1 "k8s.io/api/core/v1"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
)
|
||||
|
||||
// IngressLister makes a Store that lists Ingress.
|
||||
type IngressLister struct {
|
||||
cache.Store
|
||||
}
|
||||
|
||||
// SecretsLister makes a Store that lists Secrets.
|
||||
type SecretLister struct {
|
||||
cache.Store
|
||||
}
|
||||
|
||||
// GetByName searches for a secret in the local secrets Store
|
||||
func (sl *SecretLister) GetByName(name string) (*apiv1.Secret, error) {
|
||||
s, exists, err := sl.GetByKey(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("secret %v was not found", name)
|
||||
}
|
||||
return s.(*apiv1.Secret), nil
|
||||
}
|
||||
|
||||
// ConfigMapLister makes a Store that lists Configmaps.
|
||||
type ConfigMapLister struct {
|
||||
cache.Store
|
||||
}
|
||||
|
||||
// GetByName searches for a configmap in the local configmaps Store
|
||||
func (cml *ConfigMapLister) GetByName(name string) (*apiv1.ConfigMap, error) {
|
||||
s, exists, err := cml.GetByKey(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("configmap %v was not found", name)
|
||||
}
|
||||
return s.(*apiv1.ConfigMap), nil
|
||||
}
|
||||
|
||||
// ServiceLister makes a Store that lists Services.
|
||||
type ServiceLister struct {
|
||||
cache.Store
|
||||
}
|
||||
|
||||
// GetByName searches for a service in the local secrets Store
|
||||
func (sl *ServiceLister) GetByName(name string) (*apiv1.Service, error) {
|
||||
s, exists, err := sl.GetByKey(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("service %v was not found", name)
|
||||
}
|
||||
return s.(*apiv1.Service), nil
|
||||
}
|
||||
|
||||
// NodeLister makes a Store that lists Nodes.
|
||||
type NodeLister struct {
|
||||
cache.Store
|
||||
}
|
||||
|
||||
// EndpointLister makes a Store that lists Endpoints.
|
||||
type EndpointLister struct {
|
||||
cache.Store
|
||||
}
|
||||
|
||||
// GetServiceEndpoints returns the endpoints of a service, matched on service name.
|
||||
func (s *EndpointLister) GetServiceEndpoints(svc *apiv1.Service) (ep apiv1.Endpoints, err error) {
|
||||
for _, m := range s.Store.List() {
|
||||
ep = *m.(*apiv1.Endpoints)
|
||||
if svc.Name == ep.Name && svc.Namespace == ep.Namespace {
|
||||
return ep, nil
|
||||
}
|
||||
}
|
||||
err = fmt.Errorf("could not find endpoints for service: %v", svc.Name)
|
||||
return
|
||||
}
|
||||
73
pkg/ingress/type_equals_test.go
Normal file
73
pkg/ingress/type_equals_test.go
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
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 ingress
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEqualConfiguration(t *testing.T) {
|
||||
ap, _ := filepath.Abs("../../tests/manifests/configuration-a.json")
|
||||
a, err := readJSON(ap)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error reading JSON file: %v", err)
|
||||
}
|
||||
|
||||
bp, _ := filepath.Abs("../../tests/manifests/configuration-b.json")
|
||||
b, err := readJSON(bp)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error reading JSON file: %v", err)
|
||||
}
|
||||
|
||||
cp, _ := filepath.Abs("../../tests/manifests/configuration-c.json")
|
||||
c, err := readJSON(cp)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error reading JSON file: %v", err)
|
||||
}
|
||||
|
||||
if !a.Equal(b) {
|
||||
t.Errorf("expected equal configurations (configuration-a.json and configuration-b.json)")
|
||||
}
|
||||
|
||||
if !b.Equal(a) {
|
||||
t.Errorf("expected equal configurations (configuration-b.json and configuration-a.json)")
|
||||
}
|
||||
|
||||
if a.Equal(c) {
|
||||
t.Errorf("expected equal configurations (configuration-a.json and configuration-c.json)")
|
||||
}
|
||||
}
|
||||
|
||||
func readJSON(p string) (*Configuration, error) {
|
||||
f, err := os.Open(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var c Configuration
|
||||
|
||||
d := json.NewDecoder(f)
|
||||
err = d.Decode(&c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &c, nil
|
||||
}
|
||||
376
pkg/ingress/types.go
Normal file
376
pkg/ingress/types.go
Normal file
|
|
@ -0,0 +1,376 @@
|
|||
/*
|
||||
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 ingress
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
apiv1 "k8s.io/api/core/v1"
|
||||
extensions "k8s.io/api/extensions/v1beta1"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
"k8s.io/apiserver/pkg/server/healthz"
|
||||
|
||||
"k8s.io/ingress-nginx/pkg/ingress/annotations/auth"
|
||||
"k8s.io/ingress-nginx/pkg/ingress/annotations/authreq"
|
||||
"k8s.io/ingress-nginx/pkg/ingress/annotations/authtls"
|
||||
"k8s.io/ingress-nginx/pkg/ingress/annotations/ipwhitelist"
|
||||
"k8s.io/ingress-nginx/pkg/ingress/annotations/proxy"
|
||||
"k8s.io/ingress-nginx/pkg/ingress/annotations/ratelimit"
|
||||
"k8s.io/ingress-nginx/pkg/ingress/annotations/redirect"
|
||||
"k8s.io/ingress-nginx/pkg/ingress/annotations/rewrite"
|
||||
"k8s.io/ingress-nginx/pkg/ingress/defaults"
|
||||
"k8s.io/ingress-nginx/pkg/ingress/resolver"
|
||||
"k8s.io/ingress-nginx/pkg/ingress/store"
|
||||
)
|
||||
|
||||
var (
|
||||
// DefaultSSLDirectory defines the location where the SSL certificates will be generated
|
||||
// This directory contains all the SSL certificates that are specified in Ingress rules.
|
||||
// The name of each file is <namespace>-<secret name>.pem. The content is the concatenated
|
||||
// certificate and key.
|
||||
DefaultSSLDirectory = "/ingress-controller/ssl"
|
||||
)
|
||||
|
||||
// Controller holds the methods to handle an Ingress backend
|
||||
// TODO (#18): Make sure this is sufficiently supportive of other backends.
|
||||
type Controller interface {
|
||||
// HealthzChecker returns is a named healthz check that returns the ingress
|
||||
// controller status
|
||||
healthz.HealthzChecker
|
||||
|
||||
// OnUpdate callback invoked from the sync queue https://k8s.io/ingress/core/blob/master/pkg/ingress/controller/controller.go#L387
|
||||
// when an update occurs. This is executed frequently because Ingress
|
||||
// controllers watches changes in:
|
||||
// - Ingresses: main work
|
||||
// - Secrets: referenced from Ingress rules with TLS configured
|
||||
// - ConfigMaps: where the controller reads custom configuration
|
||||
// - Services: referenced from Ingress rules and required to obtain
|
||||
// information about ports and annotations
|
||||
// - Endpoints: referenced from Services and what the backend uses
|
||||
// to route traffic
|
||||
// Any update to services, endpoints, secrets (only those referenced from Ingress)
|
||||
// and ingress trigger the execution.
|
||||
// Notifications of type Add, Update and Delete:
|
||||
// https://github.com/kubernetes/kubernetes/blob/master/pkg/client/cache/controller.go#L164
|
||||
//
|
||||
// Configuration returns the translation from Ingress rules containing
|
||||
// information about all the upstreams (service endpoints ) "virtual"
|
||||
// servers (FQDN) and all the locations inside each server. Each
|
||||
// location contains information about all the annotations were configured
|
||||
// https://k8s.io/ingress/core/blob/master/pkg/ingress/types.go#L83
|
||||
// The backend returns an error if was not possible to update the configuration.
|
||||
//
|
||||
OnUpdate(Configuration) error
|
||||
// ConfigMap content of --configmap
|
||||
SetConfig(*apiv1.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
|
||||
// ConfigureFlags allow to configure more flags before the parsing of
|
||||
// command line arguments
|
||||
ConfigureFlags(*pflag.FlagSet)
|
||||
// OverrideFlags allow the customization of the flags in the backend
|
||||
OverrideFlags(*pflag.FlagSet)
|
||||
// DefaultIngressClass just return the default ingress class
|
||||
DefaultIngressClass() string
|
||||
// UpdateIngressStatus custom callback used to update the status in an Ingress rule
|
||||
// This allows custom implementations
|
||||
// If the function returns nil the standard functions will be executed.
|
||||
UpdateIngressStatus(*extensions.Ingress) []apiv1.LoadBalancerIngress
|
||||
// DefaultEndpoint returns the Endpoint to use as default when the
|
||||
// referenced service does not exists. This should return the content
|
||||
// of to the default backend
|
||||
DefaultEndpoint() Endpoint
|
||||
}
|
||||
|
||||
// StoreLister returns the configured stores for ingresses, services,
|
||||
// endpoints, secrets and configmaps.
|
||||
type StoreLister struct {
|
||||
Ingress store.IngressLister
|
||||
Service store.ServiceLister
|
||||
Node store.NodeLister
|
||||
Endpoint store.EndpointLister
|
||||
Secret store.SecretLister
|
||||
ConfigMap store.ConfigMapLister
|
||||
}
|
||||
|
||||
// BackendInfo returns information about the backend.
|
||||
// This fields contains information that helps to track issues or to
|
||||
// map the running ingress controller to source code
|
||||
type BackendInfo struct {
|
||||
// Name returns the name of the backend implementation
|
||||
Name string `json:"name"`
|
||||
// Release returns the running version (semver)
|
||||
Release string `json:"release"`
|
||||
// Build returns information about the git commit
|
||||
Build string `json:"build"`
|
||||
// Repository return information about the git repository
|
||||
Repository string `json:"repository"`
|
||||
}
|
||||
|
||||
// Configuration holds the definition of all the parts required to describe all
|
||||
// ingresses reachable by the ingress controller (using a filter by namespace)
|
||||
type Configuration struct {
|
||||
// Backends are a list of backends used by all the Ingress rules in the
|
||||
// ingress controller. This list includes the default backend
|
||||
Backends []*Backend `json:"backends,omitEmpty"`
|
||||
// Servers
|
||||
Servers []*Server `json:"servers,omitEmpty"`
|
||||
// TCPEndpoints contain endpoints for tcp streams handled by this backend
|
||||
// +optional
|
||||
TCPEndpoints []L4Service `json:"tcpEndpoints,omitempty"`
|
||||
// UDPEndpoints contain endpoints for udp streams handled by this backend
|
||||
// +optional
|
||||
UDPEndpoints []L4Service `json:"udpEndpoints,omitempty"`
|
||||
// PassthroughBackend contains the backends used for SSL passthrough.
|
||||
// It contains information about the associated Server Name Indication (SNI).
|
||||
// +optional
|
||||
PassthroughBackends []*SSLPassthroughBackend `json:"passthroughBackends,omitempty"`
|
||||
}
|
||||
|
||||
// Backend describes one or more remote server/s (endpoints) associated with a service
|
||||
// +k8s:deepcopy-gen=true
|
||||
type Backend struct {
|
||||
// Name represents an unique apiv1.Service name formatted as <namespace>-<name>-<port>
|
||||
Name string `json:"name"`
|
||||
Service *apiv1.Service `json:"service,omitempty"`
|
||||
Port intstr.IntOrString `json:"port"`
|
||||
// This indicates if the communication protocol between the backend and the endpoint is HTTP or HTTPS
|
||||
// Allowing the use of HTTPS
|
||||
// The endpoint/s must provide a TLS connection.
|
||||
// The certificate used in the endpoint cannot be a self signed certificate
|
||||
Secure bool `json:"secure"`
|
||||
// SecureCACert has the filename and SHA1 of the certificate authorities used to validate
|
||||
// a secured connection to the backend
|
||||
SecureCACert resolver.AuthSSLCert `json:"secureCACert"`
|
||||
// SSLPassthrough indicates that Ingress controller will delegate TLS termination to the endpoints.
|
||||
SSLPassthrough bool `json:"sslPassthrough"`
|
||||
// Endpoints contains the list of endpoints currently running
|
||||
Endpoints []Endpoint `json:"endpoints,omitempty"`
|
||||
// StickySessionAffinitySession contains the StickyConfig object with stickness configuration
|
||||
SessionAffinity SessionAffinityConfig `json:"sessionAffinityConfig"`
|
||||
}
|
||||
|
||||
// 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.
|
||||
// +k8s:deepcopy-gen=true
|
||||
type SessionAffinityConfig struct {
|
||||
AffinityType string `json:"name"`
|
||||
CookieSessionAffinity CookieSessionAffinity `json:"cookieSessionAffinity"`
|
||||
}
|
||||
|
||||
// CookieSessionAffinity defines the structure used in Affinity configured by Cookies.
|
||||
// +k8s:deepcopy-gen=true
|
||||
type CookieSessionAffinity struct {
|
||||
Name string `json:"name"`
|
||||
Hash string `json:"hash"`
|
||||
Locations map[string][]string `json:"locations,omitempty"`
|
||||
}
|
||||
|
||||
// Endpoint describes a kubernetes endpoint in a backend
|
||||
// +k8s:deepcopy-gen=true
|
||||
type Endpoint struct {
|
||||
// Address IP address of the endpoint
|
||||
Address string `json:"address"`
|
||||
// Port number of the TCP port
|
||||
Port string `json:"port"`
|
||||
// MaxFails returns the number of unsuccessful attempts to communicate
|
||||
// allowed before this should be considered dow.
|
||||
// Setting 0 indicates that the check is performed by a Kubernetes probe
|
||||
MaxFails int `json:"maxFails"`
|
||||
// FailTimeout returns the time in seconds during which the specified number
|
||||
// of unsuccessful attempts to communicate with the server should happen
|
||||
// to consider the endpoint unavailable
|
||||
FailTimeout int `json:"failTimeout"`
|
||||
// Target returns a reference to the object providing the endpoint
|
||||
Target *apiv1.ObjectReference `json:"target,omipempty"`
|
||||
}
|
||||
|
||||
// Server describes a website
|
||||
type Server struct {
|
||||
// Hostname returns the FQDN of the server
|
||||
Hostname string `json:"hostname"`
|
||||
// SSLPassthrough indicates if the TLS termination is realized in
|
||||
// the server or in the remote endpoint
|
||||
SSLPassthrough bool `json:"sslPassthrough"`
|
||||
// SSLCertificate path to the SSL certificate on disk
|
||||
SSLCertificate string `json:"sslCertificate"`
|
||||
// SSLFullChainCertificate path to the SSL certificate on disk
|
||||
// This certificate contains the full chain (ca + intermediates + cert)
|
||||
SSLFullChainCertificate string `json:"sslFullChainCertificate"`
|
||||
// SSLExpireTime has the expire date of this certificate
|
||||
SSLExpireTime time.Time `json:"sslExpireTime"`
|
||||
// SSLPemChecksum returns the checksum of the certificate file on disk.
|
||||
// There is no restriction in the hash generator. This checksim can be
|
||||
// used to determine if the secret changed without the use of file
|
||||
// system notifications
|
||||
SSLPemChecksum string `json:"sslPemChecksum"`
|
||||
// Locations list of URIs configured in the server.
|
||||
Locations []*Location `json:"locations,omitempty"`
|
||||
// Alias return the alias of the server name
|
||||
Alias string `json:"alias,omitempty"`
|
||||
// RedirectFromToWWW returns if a redirect to/from prefix www is required
|
||||
RedirectFromToWWW bool `json:"redirectFromToWWW,omitempty"`
|
||||
// CertificateAuth indicates the this server requires mutual authentication
|
||||
// +optional
|
||||
CertificateAuth authtls.AuthSSLConfig `json:"certificateAuth"`
|
||||
|
||||
// ServerSnippet returns the snippet of server
|
||||
// +optional
|
||||
ServerSnippet string `json:"serverSnippet"`
|
||||
}
|
||||
|
||||
// Location describes an URI inside a server.
|
||||
// Also contains additional information about annotations in the Ingress.
|
||||
//
|
||||
// Important:
|
||||
// The implementation of annotations is optional
|
||||
//
|
||||
// In some cases when more than one annotations is defined a particular order in the execution
|
||||
// is required.
|
||||
// The chain in the execution order of annotations should be:
|
||||
// - Whitelist
|
||||
// - RateLimit
|
||||
// - BasicDigestAuth
|
||||
// - ExternalAuth
|
||||
// - Redirect
|
||||
type Location struct {
|
||||
// Path is an extended POSIX regex as defined by IEEE Std 1003.1,
|
||||
// (i.e this follows the egrep/unix syntax, not the perl syntax)
|
||||
// matched against the path of an incoming request. Currently it can
|
||||
// contain characters disallowed from the conventional "path"
|
||||
// part of a URL as defined by RFC 3986. Paths must begin with
|
||||
// a '/'. If unspecified, the path defaults to a catch all sending
|
||||
// traffic to the backend.
|
||||
Path string `json:"path"`
|
||||
// IsDefBackend indicates if service specified in the Ingress
|
||||
// contains active endpoints or not. Returning true means the location
|
||||
// uses the default backend.
|
||||
IsDefBackend bool `json:"isDefBackend"`
|
||||
// Ingress returns the ingress from which this location was generated
|
||||
Ingress *extensions.Ingress `json:"ingress"`
|
||||
// Backend describes the name of the backend to use.
|
||||
Backend string `json:"backend"`
|
||||
// Service describes the referenced services from the ingress
|
||||
Service *apiv1.Service `json:"service,omitempty"`
|
||||
// Port describes to which port from the service
|
||||
Port intstr.IntOrString `json:"port"`
|
||||
// Overwrite the Host header passed into the backend. Defaults to
|
||||
// vhost of the incoming request.
|
||||
// +optional
|
||||
UpstreamVhost string `json:"upstream-vhost"`
|
||||
// BasicDigestAuth returns authentication configuration for
|
||||
// an Ingress rule.
|
||||
// +optional
|
||||
BasicDigestAuth auth.BasicDigest `json:"basicDigestAuth,omitempty"`
|
||||
// Denied returns an error when this location cannot not be allowed
|
||||
// Requesting a denied location should return HTTP code 403.
|
||||
Denied error `json:"denied,omitempty"`
|
||||
// EnableCORS indicates if path must support CORS
|
||||
// +optional
|
||||
EnableCORS bool `json:"enableCors,omitempty"`
|
||||
// ExternalAuth indicates the access to this location requires
|
||||
// authentication using an external provider
|
||||
// +optional
|
||||
ExternalAuth authreq.External `json:"externalAuth,omitempty"`
|
||||
// RateLimit describes a limit in the number of connections per IP
|
||||
// address or connections per second.
|
||||
// The Redirect annotation precedes RateLimit
|
||||
// +optional
|
||||
RateLimit ratelimit.RateLimit `json:"rateLimit,omitempty"`
|
||||
// Redirect describes a temporal o permanent redirection this location.
|
||||
// +optional
|
||||
Redirect redirect.Redirect `json:"redirect,omitempty"`
|
||||
// Rewrite describes the redirection this location.
|
||||
// +optional
|
||||
Rewrite rewrite.Redirect `json:"rewrite,omitempty"`
|
||||
// Whitelist indicates only connections from certain client
|
||||
// addresses or networks are allowed.
|
||||
// +optional
|
||||
Whitelist ipwhitelist.SourceRange `json:"whitelist,omitempty"`
|
||||
// Proxy contains information about timeouts and buffer sizes
|
||||
// to be used in connections against endpoints
|
||||
// +optional
|
||||
Proxy proxy.Configuration `json:"proxy,omitempty"`
|
||||
// UsePortInRedirects indicates if redirects must specify the port
|
||||
// +optional
|
||||
UsePortInRedirects bool `json:"usePortInRedirects"`
|
||||
// VtsFilterKey contains the vts filter key on the location level
|
||||
// https://github.com/vozlt/nginx-module-vts#vhost_traffic_status_filter_by_set_key
|
||||
// +optional
|
||||
VtsFilterKey string `json:"vtsFilterKey,omitempty"`
|
||||
// ConfigurationSnippet contains additional configuration for the backend
|
||||
// to be considered in the configuration of the location
|
||||
ConfigurationSnippet string `json:"configurationSnippet"`
|
||||
// ClientBodyBufferSize allows for the configuration of the client body
|
||||
// buffer size for a specific location.
|
||||
// +optional
|
||||
ClientBodyBufferSize string `json:"clientBodyBufferSize,omitempty"`
|
||||
// DefaultBackend allows the use of a custom default backend for this location.
|
||||
// +optional
|
||||
DefaultBackend *apiv1.Service `json:"defaultBackend,omitempty"`
|
||||
}
|
||||
|
||||
// SSLPassthroughBackend describes a SSL upstream server configured
|
||||
// as passthrough (no TLS termination in the ingress controller)
|
||||
// The endpoints must provide the TLS termination exposing the required SSL certificate.
|
||||
// The ingress controller only pipes the underlying TCP connection
|
||||
type SSLPassthroughBackend struct {
|
||||
Service *apiv1.Service `json:"service,omitEmpty"`
|
||||
Port intstr.IntOrString `json:"port"`
|
||||
// Backend describes the endpoints to use.
|
||||
Backend string `json:"namespace,omitempty"`
|
||||
// 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,omitEmpty"`
|
||||
}
|
||||
|
||||
// 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 apiv1.Protocol `json:"protocol"`
|
||||
// +optional
|
||||
ProxyProtocol ProxyProtocol `json:"proxyProtocol"`
|
||||
}
|
||||
|
||||
// ProxyProtocol describes the proxy protocol configuration
|
||||
type ProxyProtocol struct {
|
||||
Decode bool `json:"decode"`
|
||||
Encode bool `json:"encode"`
|
||||
}
|
||||
481
pkg/ingress/types_equals.go
Normal file
481
pkg/ingress/types_equals.go
Normal file
|
|
@ -0,0 +1,481 @@
|
|||
/*
|
||||
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 ingress
|
||||
|
||||
// Equal tests for equality between two BackendInfo types
|
||||
func (bi1 *BackendInfo) Equal(bi2 *BackendInfo) bool {
|
||||
if bi1 == bi2 {
|
||||
return true
|
||||
}
|
||||
if bi1 == nil || bi2 == nil {
|
||||
return false
|
||||
}
|
||||
if bi1.Name != bi2.Name {
|
||||
return false
|
||||
}
|
||||
if bi1.Release != bi2.Release {
|
||||
return false
|
||||
}
|
||||
if bi1.Build != bi2.Build {
|
||||
return false
|
||||
}
|
||||
if bi1.Repository != bi2.Repository {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Equal tests for equality between two Configuration types
|
||||
func (c1 *Configuration) Equal(c2 *Configuration) bool {
|
||||
if c1 == c2 {
|
||||
return true
|
||||
}
|
||||
if c1 == nil || c2 == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if len(c1.Backends) != len(c2.Backends) {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, c1b := range c1.Backends {
|
||||
found := false
|
||||
for _, c2b := range c2.Backends {
|
||||
if c1b.Equal(c2b) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if len(c1.Servers) != len(c2.Servers) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Servers are sorted
|
||||
for idx, c1s := range c1.Servers {
|
||||
if !c1s.Equal(c2.Servers[idx]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if len(c1.TCPEndpoints) != len(c2.TCPEndpoints) {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, tcp1 := range c1.TCPEndpoints {
|
||||
found := false
|
||||
for _, tcp2 := range c2.TCPEndpoints {
|
||||
if (&tcp1).Equal(&tcp2) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if len(c1.UDPEndpoints) != len(c2.UDPEndpoints) {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, udp1 := range c1.UDPEndpoints {
|
||||
found := false
|
||||
for _, udp2 := range c2.UDPEndpoints {
|
||||
if (&udp1).Equal(&udp2) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if len(c1.PassthroughBackends) != len(c2.PassthroughBackends) {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, ptb1 := range c1.PassthroughBackends {
|
||||
found := false
|
||||
for _, ptb2 := range c2.PassthroughBackends {
|
||||
if ptb1.Equal(ptb2) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Equal tests for equality between two Backend types
|
||||
func (b1 *Backend) Equal(b2 *Backend) bool {
|
||||
if b1 == b2 {
|
||||
return true
|
||||
}
|
||||
if b1 == nil || b2 == nil {
|
||||
return false
|
||||
}
|
||||
if b1.Name != b2.Name {
|
||||
return false
|
||||
}
|
||||
|
||||
if b1.Service != b2.Service {
|
||||
if b1.Service == nil || b2.Service == nil {
|
||||
return false
|
||||
}
|
||||
if b1.Service.GetNamespace() != b2.Service.GetNamespace() {
|
||||
return false
|
||||
}
|
||||
if b1.Service.GetName() != b2.Service.GetName() {
|
||||
return false
|
||||
}
|
||||
if b1.Service.GetResourceVersion() != b2.Service.GetResourceVersion() {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if b1.Port != b2.Port {
|
||||
return false
|
||||
}
|
||||
if b1.Secure != b2.Secure {
|
||||
return false
|
||||
}
|
||||
if !(&b1.SecureCACert).Equal(&b2.SecureCACert) {
|
||||
return false
|
||||
}
|
||||
if b1.SSLPassthrough != b2.SSLPassthrough {
|
||||
return false
|
||||
}
|
||||
if !(&b1.SessionAffinity).Equal(&b2.SessionAffinity) {
|
||||
return false
|
||||
}
|
||||
|
||||
if len(b1.Endpoints) != len(b2.Endpoints) {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, udp1 := range b1.Endpoints {
|
||||
found := false
|
||||
for _, udp2 := range b2.Endpoints {
|
||||
if (&udp1).Equal(&udp2) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Equal tests for equality between two SessionAffinityConfig types
|
||||
func (sac1 *SessionAffinityConfig) Equal(sac2 *SessionAffinityConfig) bool {
|
||||
if sac1 == sac2 {
|
||||
return true
|
||||
}
|
||||
if sac1 == nil || sac2 == nil {
|
||||
return false
|
||||
}
|
||||
if sac1.AffinityType != sac2.AffinityType {
|
||||
return false
|
||||
}
|
||||
if !(&sac1.CookieSessionAffinity).Equal(&sac2.CookieSessionAffinity) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Equal tests for equality between two CookieSessionAffinity types
|
||||
func (csa1 *CookieSessionAffinity) Equal(csa2 *CookieSessionAffinity) bool {
|
||||
if csa1 == csa2 {
|
||||
return true
|
||||
}
|
||||
if csa1 == nil || csa2 == nil {
|
||||
return false
|
||||
}
|
||||
if csa1.Name != csa2.Name {
|
||||
return false
|
||||
}
|
||||
if csa1.Hash != csa2.Hash {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Equal checks the equality against an Endpoint
|
||||
func (e1 *Endpoint) Equal(e2 *Endpoint) bool {
|
||||
if e1 == e2 {
|
||||
return true
|
||||
}
|
||||
if e1 == nil || e2 == nil {
|
||||
return false
|
||||
}
|
||||
if e1.Address != e2.Address {
|
||||
return false
|
||||
}
|
||||
if e1.Port != e2.Port {
|
||||
return false
|
||||
}
|
||||
if e1.MaxFails != e2.MaxFails {
|
||||
return false
|
||||
}
|
||||
if e1.FailTimeout != e2.FailTimeout {
|
||||
return false
|
||||
}
|
||||
|
||||
if e1.Target != e2.Target {
|
||||
if e1.Target == nil || e2.Target == nil {
|
||||
return false
|
||||
}
|
||||
if e1.Target.UID != e2.Target.UID {
|
||||
return false
|
||||
}
|
||||
if e1.Target.ResourceVersion != e2.Target.ResourceVersion {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Equal tests for equality between two Server types
|
||||
func (s1 *Server) Equal(s2 *Server) bool {
|
||||
if s1 == s2 {
|
||||
return true
|
||||
}
|
||||
if s1 == nil || s2 == nil {
|
||||
return false
|
||||
}
|
||||
if s1.Hostname != s2.Hostname {
|
||||
return false
|
||||
}
|
||||
if s1.Alias != s2.Alias {
|
||||
return false
|
||||
}
|
||||
if s1.SSLPassthrough != s2.SSLPassthrough {
|
||||
return false
|
||||
}
|
||||
if s1.SSLCertificate != s2.SSLCertificate {
|
||||
return false
|
||||
}
|
||||
if s1.SSLPemChecksum != s2.SSLPemChecksum {
|
||||
return false
|
||||
}
|
||||
if !(&s1.CertificateAuth).Equal(&s2.CertificateAuth) {
|
||||
return false
|
||||
}
|
||||
if s1.RedirectFromToWWW != s2.RedirectFromToWWW {
|
||||
return false
|
||||
}
|
||||
|
||||
if len(s1.Locations) != len(s2.Locations) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Location are sorted
|
||||
for idx, s1l := range s1.Locations {
|
||||
if !s1l.Equal(s2.Locations[idx]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Equal tests for equality between two Location types
|
||||
func (l1 *Location) Equal(l2 *Location) bool {
|
||||
if l1 == l2 {
|
||||
return true
|
||||
}
|
||||
if l1 == nil || l2 == nil {
|
||||
return false
|
||||
}
|
||||
if l1.Path != l2.Path {
|
||||
return false
|
||||
}
|
||||
if l1.IsDefBackend != l2.IsDefBackend {
|
||||
return false
|
||||
}
|
||||
if l1.Backend != l2.Backend {
|
||||
return false
|
||||
}
|
||||
|
||||
if l1.Service != l2.Service {
|
||||
if l1.Service == nil || l2.Service == nil {
|
||||
return false
|
||||
}
|
||||
if l1.Service.GetNamespace() != l2.Service.GetNamespace() {
|
||||
return false
|
||||
}
|
||||
if l1.Service.GetName() != l2.Service.GetName() {
|
||||
return false
|
||||
}
|
||||
if l1.Service.GetResourceVersion() != l2.Service.GetResourceVersion() {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if l1.Port.StrVal != l2.Port.StrVal {
|
||||
return false
|
||||
}
|
||||
if !(&l1.BasicDigestAuth).Equal(&l2.BasicDigestAuth) {
|
||||
return false
|
||||
}
|
||||
if l1.Denied != l2.Denied {
|
||||
return false
|
||||
}
|
||||
if l1.EnableCORS != l2.EnableCORS {
|
||||
return false
|
||||
}
|
||||
if !(&l1.ExternalAuth).Equal(&l2.ExternalAuth) {
|
||||
return false
|
||||
}
|
||||
if !(&l1.RateLimit).Equal(&l2.RateLimit) {
|
||||
return false
|
||||
}
|
||||
if !(&l1.Redirect).Equal(&l2.Redirect) {
|
||||
return false
|
||||
}
|
||||
if !(&l1.Rewrite).Equal(&l2.Rewrite) {
|
||||
return false
|
||||
}
|
||||
if !(&l1.Whitelist).Equal(&l2.Whitelist) {
|
||||
return false
|
||||
}
|
||||
if !(&l1.Proxy).Equal(&l2.Proxy) {
|
||||
return false
|
||||
}
|
||||
if l1.UsePortInRedirects != l2.UsePortInRedirects {
|
||||
return false
|
||||
}
|
||||
if l1.ConfigurationSnippet != l2.ConfigurationSnippet {
|
||||
return false
|
||||
}
|
||||
if l1.ClientBodyBufferSize != l2.ClientBodyBufferSize {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Equal tests for equality between two SSLPassthroughBackend types
|
||||
func (ptb1 *SSLPassthroughBackend) Equal(ptb2 *SSLPassthroughBackend) bool {
|
||||
if ptb1 == ptb2 {
|
||||
return true
|
||||
}
|
||||
if ptb1 == nil || ptb2 == nil {
|
||||
return false
|
||||
}
|
||||
if ptb1.Backend != ptb2.Backend {
|
||||
return false
|
||||
}
|
||||
if ptb1.Hostname != ptb2.Hostname {
|
||||
return false
|
||||
}
|
||||
if ptb1.Port != ptb2.Port {
|
||||
return false
|
||||
}
|
||||
|
||||
if ptb1.Service != ptb2.Service {
|
||||
if ptb1.Service == nil || ptb2.Service == nil {
|
||||
return false
|
||||
}
|
||||
if ptb1.Service.GetNamespace() != ptb2.Service.GetNamespace() {
|
||||
return false
|
||||
}
|
||||
if ptb1.Service.GetName() != ptb2.Service.GetName() {
|
||||
return false
|
||||
}
|
||||
if ptb1.Service.GetResourceVersion() != ptb2.Service.GetResourceVersion() {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Equal tests for equality between two L4Service types
|
||||
func (e1 *L4Service) Equal(e2 *L4Service) bool {
|
||||
if e1 == e2 {
|
||||
return true
|
||||
}
|
||||
if e1 == nil || e2 == nil {
|
||||
return false
|
||||
}
|
||||
if e1.Port != e2.Port {
|
||||
return false
|
||||
}
|
||||
if !(&e1.Backend).Equal(&e2.Backend) {
|
||||
return false
|
||||
}
|
||||
if len(e1.Endpoints) != len(e2.Endpoints) {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, ep1 := range e1.Endpoints {
|
||||
found := false
|
||||
for _, ep2 := range e2.Endpoints {
|
||||
if (&ep1).Equal(&ep2) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Equal tests for equality between two L4Backend types
|
||||
func (l4b1 *L4Backend) Equal(l4b2 *L4Backend) bool {
|
||||
if l4b1 == l4b2 {
|
||||
return true
|
||||
}
|
||||
if l4b1 == nil || l4b2 == nil {
|
||||
return false
|
||||
}
|
||||
if l4b1.Port != l4b2.Port {
|
||||
return false
|
||||
}
|
||||
if l4b1.Name != l4b2.Name {
|
||||
return false
|
||||
}
|
||||
if l4b1.Namespace != l4b2.Namespace {
|
||||
return false
|
||||
}
|
||||
if l4b1.Protocol != l4b2.Protocol {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
116
pkg/ingress/zz_generated.types.go
Normal file
116
pkg/ingress/zz_generated.types.go
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
// +build !ignore_autogenerated
|
||||
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
// This file was autogenerated by deepcopy-gen. Do not edit it manually!
|
||||
|
||||
package ingress
|
||||
|
||||
import (
|
||||
v1 "k8s.io/api/core/v1"
|
||||
conversion "k8s.io/apimachinery/pkg/conversion"
|
||||
reflect "reflect"
|
||||
)
|
||||
|
||||
// GetGeneratedDeepCopyFuncs returns the generated funcs, since we aren't registering them.
|
||||
func GetGeneratedDeepCopyFuncs() []conversion.GeneratedDeepCopyFunc {
|
||||
return []conversion.GeneratedDeepCopyFunc{
|
||||
{Fn: DeepCopy__Backend, InType: reflect.TypeOf(&Backend{})},
|
||||
{Fn: DeepCopy__CookieSessionAffinity, InType: reflect.TypeOf(&CookieSessionAffinity{})},
|
||||
{Fn: DeepCopy__Endpoint, InType: reflect.TypeOf(&Endpoint{})},
|
||||
{Fn: DeepCopy__SessionAffinityConfig, InType: reflect.TypeOf(&SessionAffinityConfig{})},
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy__Backend is an autogenerated deepcopy function.
|
||||
func DeepCopy__Backend(in interface{}, out interface{}, c *conversion.Cloner) error {
|
||||
{
|
||||
in := in.(*Backend)
|
||||
out := out.(*Backend)
|
||||
*out = *in
|
||||
if in.Service != nil {
|
||||
in, out := &in.Service, &out.Service
|
||||
if newVal, err := c.DeepCopy(*in); err != nil {
|
||||
return err
|
||||
} else {
|
||||
*out = newVal.(*v1.Service)
|
||||
}
|
||||
}
|
||||
if in.Endpoints != nil {
|
||||
in, out := &in.Endpoints, &out.Endpoints
|
||||
*out = make([]Endpoint, len(*in))
|
||||
for i := range *in {
|
||||
if err := DeepCopy__Endpoint(&(*in)[i], &(*out)[i], c); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := DeepCopy__SessionAffinityConfig(&in.SessionAffinity, &out.SessionAffinity, c); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy__CookieSessionAffinity is an autogenerated deepcopy function.
|
||||
func DeepCopy__CookieSessionAffinity(in interface{}, out interface{}, c *conversion.Cloner) error {
|
||||
{
|
||||
in := in.(*CookieSessionAffinity)
|
||||
out := out.(*CookieSessionAffinity)
|
||||
*out = *in
|
||||
if in.Locations != nil {
|
||||
in, out := &in.Locations, &out.Locations
|
||||
*out = make(map[string][]string)
|
||||
for key, val := range *in {
|
||||
if newVal, err := c.DeepCopy(&val); err != nil {
|
||||
return err
|
||||
} else {
|
||||
(*out)[key] = *newVal.(*[]string)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy__Endpoint is an autogenerated deepcopy function.
|
||||
func DeepCopy__Endpoint(in interface{}, out interface{}, c *conversion.Cloner) error {
|
||||
{
|
||||
in := in.(*Endpoint)
|
||||
out := out.(*Endpoint)
|
||||
*out = *in
|
||||
if in.Target != nil {
|
||||
in, out := &in.Target, &out.Target
|
||||
*out = new(v1.ObjectReference)
|
||||
**out = **in
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy__SessionAffinityConfig is an autogenerated deepcopy function.
|
||||
func DeepCopy__SessionAffinityConfig(in interface{}, out interface{}, c *conversion.Cloner) error {
|
||||
{
|
||||
in := in.(*SessionAffinityConfig)
|
||||
out := out.(*SessionAffinityConfig)
|
||||
*out = *in
|
||||
if err := DeepCopy__CookieSessionAffinity(&in.CookieSessionAffinity, &out.CookieSessionAffinity, c); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
93
pkg/k8s/main.go
Normal file
93
pkg/k8s/main.go
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
Copyright 2015 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 k8s
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
apiv1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
clientset "k8s.io/client-go/kubernetes"
|
||||
)
|
||||
|
||||
// ParseNameNS parses a string searching a namespace and name
|
||||
func ParseNameNS(input string) (string, string, error) {
|
||||
nsName := strings.Split(input, "/")
|
||||
if len(nsName) != 2 {
|
||||
return "", "", fmt.Errorf("invalid format (namespace/name) found in '%v'", input)
|
||||
}
|
||||
|
||||
return nsName[0], nsName[1], nil
|
||||
}
|
||||
|
||||
// GetNodeIP returns the IP address of a node in the cluster
|
||||
func GetNodeIP(kubeClient clientset.Interface, name string) string {
|
||||
var externalIP string
|
||||
node, err := kubeClient.Core().Nodes().Get(name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return externalIP
|
||||
}
|
||||
|
||||
for _, address := range node.Status.Addresses {
|
||||
if address.Type == apiv1.NodeExternalIP {
|
||||
if address.Address != "" {
|
||||
externalIP = address.Address
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if externalIP == "" && address.Type == apiv1.NodeInternalIP {
|
||||
externalIP = address.Address
|
||||
}
|
||||
}
|
||||
return externalIP
|
||||
}
|
||||
|
||||
// PodInfo contains runtime information about the pod running the Ingres controller
|
||||
type PodInfo struct {
|
||||
Name string
|
||||
Namespace string
|
||||
NodeIP string
|
||||
// Labels selectors of the running pod
|
||||
// This is used to search for other Ingress controller pods
|
||||
Labels map[string]string
|
||||
}
|
||||
|
||||
// GetPodDetails returns runtime information about the pod:
|
||||
// name, namespace and IP of the node where it is running
|
||||
func GetPodDetails(kubeClient clientset.Interface) (*PodInfo, error) {
|
||||
podName := os.Getenv("POD_NAME")
|
||||
podNs := os.Getenv("POD_NAMESPACE")
|
||||
|
||||
if podName == "" || podNs == "" {
|
||||
return nil, fmt.Errorf("unable to get POD information (missing POD_NAME or POD_NAMESPACE environment variable")
|
||||
}
|
||||
|
||||
pod, _ := kubeClient.Core().Pods(podNs).Get(podName, metav1.GetOptions{})
|
||||
if pod == nil {
|
||||
return nil, fmt.Errorf("unable to get POD information")
|
||||
}
|
||||
|
||||
return &PodInfo{
|
||||
Name: podName,
|
||||
Namespace: podNs,
|
||||
NodeIP: GetNodeIP(kubeClient, pod.Spec.NodeName),
|
||||
Labels: pod.GetLabels(),
|
||||
}, nil
|
||||
}
|
||||
241
pkg/k8s/main_test.go
Normal file
241
pkg/k8s/main_test.go
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
/*
|
||||
Copyright 2015 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 k8s
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
apiv1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
testclient "k8s.io/client-go/kubernetes/fake"
|
||||
)
|
||||
|
||||
func TestParseNameNS(t *testing.T) {
|
||||
tests := []struct {
|
||||
title string
|
||||
input string
|
||||
ns string
|
||||
name string
|
||||
expErr bool
|
||||
}{
|
||||
{"empty string", "", "", "", true},
|
||||
{"demo", "demo", "", "", true},
|
||||
{"kube-system", "kube-system", "", "", true},
|
||||
{"default/kube-system", "default/kube-system", "default", "kube-system", false},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
ns, name, err := ParseNameNS(test.input)
|
||||
if test.expErr {
|
||||
if err == nil {
|
||||
t.Errorf("%v: expected error but returned nil", test.title)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if test.ns != ns {
|
||||
t.Errorf("%v: expected %v but returned %v", test.title, test.ns, ns)
|
||||
}
|
||||
if test.name != name {
|
||||
t.Errorf("%v: expected %v but returned %v", test.title, test.name, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetNodeIP(t *testing.T) {
|
||||
fKNodes := []struct {
|
||||
cs *testclient.Clientset
|
||||
n string
|
||||
ea string
|
||||
}{
|
||||
// empty node list
|
||||
{testclient.NewSimpleClientset(), "demo", ""},
|
||||
|
||||
// node not exist
|
||||
{testclient.NewSimpleClientset(&apiv1.NodeList{Items: []apiv1.Node{{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "demo",
|
||||
},
|
||||
Status: apiv1.NodeStatus{
|
||||
Addresses: []apiv1.NodeAddress{
|
||||
{
|
||||
Type: apiv1.NodeInternalIP,
|
||||
Address: "10.0.0.1",
|
||||
},
|
||||
},
|
||||
},
|
||||
}}}), "notexistnode", ""},
|
||||
|
||||
// node exist
|
||||
{testclient.NewSimpleClientset(&apiv1.NodeList{Items: []apiv1.Node{{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "demo",
|
||||
},
|
||||
Status: apiv1.NodeStatus{
|
||||
Addresses: []apiv1.NodeAddress{
|
||||
{
|
||||
Type: apiv1.NodeInternalIP,
|
||||
Address: "10.0.0.1",
|
||||
},
|
||||
},
|
||||
},
|
||||
}}}), "demo", "10.0.0.1"},
|
||||
|
||||
// search the correct node
|
||||
{testclient.NewSimpleClientset(&apiv1.NodeList{Items: []apiv1.Node{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "demo1",
|
||||
},
|
||||
Status: apiv1.NodeStatus{
|
||||
Addresses: []apiv1.NodeAddress{
|
||||
{
|
||||
Type: apiv1.NodeInternalIP,
|
||||
Address: "10.0.0.1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "demo2",
|
||||
},
|
||||
Status: apiv1.NodeStatus{
|
||||
Addresses: []apiv1.NodeAddress{
|
||||
{
|
||||
Type: apiv1.NodeInternalIP,
|
||||
Address: "10.0.0.2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}}), "demo2", "10.0.0.2"},
|
||||
|
||||
// get NodeExternalIP
|
||||
{testclient.NewSimpleClientset(&apiv1.NodeList{Items: []apiv1.Node{{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "demo",
|
||||
},
|
||||
Status: apiv1.NodeStatus{
|
||||
Addresses: []apiv1.NodeAddress{
|
||||
{
|
||||
Type: apiv1.NodeInternalIP,
|
||||
Address: "10.0.0.1",
|
||||
}, {
|
||||
Type: apiv1.NodeExternalIP,
|
||||
Address: "10.0.0.2",
|
||||
},
|
||||
},
|
||||
},
|
||||
}}}), "demo", "10.0.0.2"},
|
||||
|
||||
// get NodeInternalIP
|
||||
{testclient.NewSimpleClientset(&apiv1.NodeList{Items: []apiv1.Node{{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "demo",
|
||||
},
|
||||
Status: apiv1.NodeStatus{
|
||||
Addresses: []apiv1.NodeAddress{
|
||||
{
|
||||
Type: apiv1.NodeExternalIP,
|
||||
Address: "",
|
||||
}, {
|
||||
Type: apiv1.NodeInternalIP,
|
||||
Address: "10.0.0.2",
|
||||
},
|
||||
},
|
||||
},
|
||||
}}}), "demo", "10.0.0.2"},
|
||||
}
|
||||
|
||||
for _, fk := range fKNodes {
|
||||
address := GetNodeIP(fk.cs, fk.n)
|
||||
if address != fk.ea {
|
||||
t.Errorf("expected %s, but returned %s", fk.ea, address)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetPodDetails(t *testing.T) {
|
||||
// POD_NAME & POD_NAMESPACE not exist
|
||||
os.Setenv("POD_NAME", "")
|
||||
os.Setenv("POD_NAMESPACE", "")
|
||||
_, err1 := GetPodDetails(testclient.NewSimpleClientset())
|
||||
if err1 == nil {
|
||||
t.Errorf("expected an error but returned nil")
|
||||
}
|
||||
|
||||
// POD_NAME not exist
|
||||
os.Setenv("POD_NAME", "")
|
||||
os.Setenv("POD_NAMESPACE", apiv1.NamespaceDefault)
|
||||
_, err2 := GetPodDetails(testclient.NewSimpleClientset())
|
||||
if err2 == nil {
|
||||
t.Errorf("expected an error but returned nil")
|
||||
}
|
||||
|
||||
// POD_NAMESPACE not exist
|
||||
os.Setenv("POD_NAME", "testpod")
|
||||
os.Setenv("POD_NAMESPACE", "")
|
||||
_, err3 := GetPodDetails(testclient.NewSimpleClientset())
|
||||
if err3 == nil {
|
||||
t.Errorf("expected an error but returned nil")
|
||||
}
|
||||
|
||||
// POD not exist
|
||||
os.Setenv("POD_NAME", "testpod")
|
||||
os.Setenv("POD_NAMESPACE", apiv1.NamespaceDefault)
|
||||
_, err4 := GetPodDetails(testclient.NewSimpleClientset())
|
||||
if err4 == nil {
|
||||
t.Errorf("expected an error but returned nil")
|
||||
}
|
||||
|
||||
// success to get PodInfo
|
||||
fkClient := testclient.NewSimpleClientset(
|
||||
&apiv1.PodList{Items: []apiv1.Pod{{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "testpod",
|
||||
Namespace: apiv1.NamespaceDefault,
|
||||
Labels: map[string]string{
|
||||
"first": "first_label",
|
||||
"second": "second_label",
|
||||
},
|
||||
},
|
||||
}}},
|
||||
&apiv1.NodeList{Items: []apiv1.Node{{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "demo",
|
||||
},
|
||||
Status: apiv1.NodeStatus{
|
||||
Addresses: []apiv1.NodeAddress{
|
||||
{
|
||||
Type: apiv1.NodeInternalIP,
|
||||
Address: "10.0.0.1",
|
||||
},
|
||||
},
|
||||
},
|
||||
}}})
|
||||
|
||||
epi, err5 := GetPodDetails(fkClient)
|
||||
if err5 != nil {
|
||||
t.Errorf("expected a PodInfo but returned error")
|
||||
return
|
||||
}
|
||||
|
||||
if epi == nil {
|
||||
t.Errorf("expected a PodInfo but returned nil")
|
||||
}
|
||||
}
|
||||
58
pkg/net/dns/dns.go
Normal file
58
pkg/net/dns/dns.go
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
Copyright 2015 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 dns
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/golang/glog"
|
||||
)
|
||||
|
||||
var defResolvConf = "/etc/resolv.conf"
|
||||
|
||||
// GetSystemNameServers returns the list of nameservers located in the file /etc/resolv.conf
|
||||
func GetSystemNameServers() ([]net.IP, error) {
|
||||
var nameservers []net.IP
|
||||
file, err := ioutil.ReadFile(defResolvConf)
|
||||
if err != nil {
|
||||
return nameservers, err
|
||||
}
|
||||
|
||||
// Lines of the form "nameserver 1.2.3.4" accumulate.
|
||||
lines := strings.Split(string(file), "\n")
|
||||
for l := range lines {
|
||||
trimmed := strings.TrimSpace(lines[l])
|
||||
if len(trimmed) == 0 || trimmed[0] == '#' || trimmed[0] == ';' {
|
||||
continue
|
||||
}
|
||||
fields := strings.Fields(trimmed)
|
||||
if len(fields) < 2 {
|
||||
continue
|
||||
}
|
||||
if fields[0] == "nameserver" {
|
||||
ip := net.ParseIP(fields[1])
|
||||
if ip != nil {
|
||||
nameservers = append(nameservers, ip)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
glog.V(3).Infof("nameservers IP address/es to use: %v", nameservers)
|
||||
return nameservers, nil
|
||||
}
|
||||
63
pkg/net/dns/dns_test.go
Normal file
63
pkg/net/dns/dns_test.go
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
Copyright 2015 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 dns
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetDNSServers(t *testing.T) {
|
||||
s, err := GetSystemNameServers()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error reading /etc/resolv.conf file: %v", err)
|
||||
}
|
||||
if len(s) < 1 {
|
||||
t.Error("expected at least 1 nameserver in /etc/resolv.conf")
|
||||
}
|
||||
|
||||
file, err := ioutil.TempFile("", "fw")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
defer file.Close()
|
||||
defer os.Remove(file.Name())
|
||||
|
||||
ioutil.WriteFile(file.Name(), []byte(`
|
||||
# comment
|
||||
; comment
|
||||
nameserver 2001:4860:4860::8844
|
||||
nameserver 2001:4860:4860::8888
|
||||
nameserver 8.8.8.8
|
||||
`), 0644)
|
||||
|
||||
defResolvConf = file.Name()
|
||||
s, err = GetSystemNameServers()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error reading /etc/resolv.conf file: %v", err)
|
||||
}
|
||||
if len(s) < 3 {
|
||||
t.Errorf("expected at 3 nameservers but %v returned", len(s))
|
||||
}
|
||||
|
||||
eip := net.ParseIP("2001:4860:4860::8844")
|
||||
if !s[0].Equal(eip) {
|
||||
t.Errorf("expected %v as nameservers but %v returned", eip, s[0])
|
||||
}
|
||||
}
|
||||
53
pkg/net/ipnet.go
Normal file
53
pkg/net/ipnet.go
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
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 net
|
||||
|
||||
import (
|
||||
"net"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// IPNet maps string to net.IPNet.
|
||||
type IPNet map[string]*net.IPNet
|
||||
|
||||
// IP maps string to net.IP.
|
||||
type IP map[string]net.IP
|
||||
|
||||
// ParseIPNets parses string slice to IPNet.
|
||||
func ParseIPNets(specs ...string) (IPNet, IP, error) {
|
||||
ipnetset := make(IPNet)
|
||||
ipset := make(IP)
|
||||
|
||||
for _, spec := range specs {
|
||||
spec = strings.TrimSpace(spec)
|
||||
_, ipnet, err := net.ParseCIDR(spec)
|
||||
if err != nil {
|
||||
ip := net.ParseIP(spec)
|
||||
if ip == nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
i := ip.String()
|
||||
ipset[i] = ip
|
||||
continue
|
||||
}
|
||||
|
||||
k := ipnet.String()
|
||||
ipnetset[k] = ipnet
|
||||
}
|
||||
|
||||
return ipnetset, ipset, nil
|
||||
}
|
||||
34
pkg/net/ipnet_test.go
Normal file
34
pkg/net/ipnet_test.go
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
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 net
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewIPSet(t *testing.T) {
|
||||
ipsets, ips, err := ParseIPNets("1.0.0.0", "2.0.0.0/8", "3.0.0.0/8")
|
||||
if err != nil {
|
||||
t.Errorf("error parsing IPNets: %v", err)
|
||||
}
|
||||
if len(ipsets) != 2 {
|
||||
t.Errorf("Expected len=2: %d", len(ipsets))
|
||||
}
|
||||
if len(ips) != 1 {
|
||||
t.Errorf("Expected len=1: %d", len(ips))
|
||||
}
|
||||
}
|
||||
24
pkg/net/net.go
Normal file
24
pkg/net/net.go
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
Copyright 2015 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 net
|
||||
|
||||
import _net "net"
|
||||
|
||||
// IsIPV6 checks if the input contains a valid IPV6 address
|
||||
func IsIPV6(ip _net.IP) bool {
|
||||
return ip.To4() == nil
|
||||
}
|
||||
43
pkg/net/net_test.go
Normal file
43
pkg/net/net_test.go
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
Copyright 2015 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 net
|
||||
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestIsIPV6(t *testing.T) {
|
||||
tests := []struct {
|
||||
in net.IP
|
||||
isIPV6 bool
|
||||
}{
|
||||
{net.ParseIP("2001:4860:4860::8844"), true},
|
||||
{net.ParseIP("2001:4860:4860::8888"), true},
|
||||
{net.ParseIP("0:0:0:0:0:ffff:c868:8165"), true},
|
||||
{net.ParseIP("2001:db8:85a3::8a2e:370:7334"), true},
|
||||
{net.ParseIP("::1"), true},
|
||||
{net.ParseIP("8.8.8.8"), false},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
isIPV6 := IsIPV6(test.in)
|
||||
if isIPV6 && !test.isIPV6 {
|
||||
t.Fatalf("%v expected %v but returned %v", test.in, test.isIPV6, isIPV6)
|
||||
}
|
||||
}
|
||||
}
|
||||
420
pkg/net/ssl/ssl.go
Normal file
420
pkg/net/ssl/ssl.go
Normal file
|
|
@ -0,0 +1,420 @@
|
|||
/*
|
||||
Copyright 2015 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 ssl
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/asn1"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"math/big"
|
||||
"net"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/golang/glog"
|
||||
"github.com/zakjan/cert-chain-resolver/certUtil"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
|
||||
"k8s.io/ingress-nginx/pkg/file"
|
||||
"k8s.io/ingress-nginx/pkg/ingress"
|
||||
)
|
||||
|
||||
var (
|
||||
oidExtensionSubjectAltName = asn1.ObjectIdentifier{2, 5, 29, 17}
|
||||
)
|
||||
|
||||
// AddOrUpdateCertAndKey creates a .pem file wth the cert and the key with the specified name
|
||||
func AddOrUpdateCertAndKey(name string, cert, key, ca []byte) (*ingress.SSLCert, error) {
|
||||
pemName := fmt.Sprintf("%v.pem", name)
|
||||
pemFileName := fmt.Sprintf("%v/%v", ingress.DefaultSSLDirectory, pemName)
|
||||
fullChainPemFileName := fmt.Sprintf("%v/%v-full-chain.pem", ingress.DefaultSSLDirectory, name)
|
||||
|
||||
tempPemFile, err := ioutil.TempFile(ingress.DefaultSSLDirectory, pemName)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not create temp pem file %v: %v", pemFileName, err)
|
||||
}
|
||||
glog.V(3).Infof("Creating temp file %v for Keypair: %v", tempPemFile.Name(), pemName)
|
||||
|
||||
_, err = tempPemFile.Write(cert)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not write to pem file %v: %v", tempPemFile.Name(), err)
|
||||
}
|
||||
_, err = tempPemFile.Write([]byte("\n"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not write to pem file %v: %v", tempPemFile.Name(), err)
|
||||
}
|
||||
_, err = tempPemFile.Write(key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not write to pem file %v: %v", tempPemFile.Name(), err)
|
||||
}
|
||||
|
||||
err = tempPemFile.Close()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not close temp pem file %v: %v", tempPemFile.Name(), err)
|
||||
}
|
||||
|
||||
pemCerts, err := ioutil.ReadFile(tempPemFile.Name())
|
||||
if err != nil {
|
||||
_ = os.Remove(tempPemFile.Name())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pemBlock, _ := pem.Decode(pemCerts)
|
||||
if pemBlock == nil {
|
||||
_ = os.Remove(tempPemFile.Name())
|
||||
return nil, fmt.Errorf("no valid PEM formatted block found")
|
||||
}
|
||||
|
||||
// If the file does not start with 'BEGIN CERTIFICATE' it's invalid and must not be used.
|
||||
if pemBlock.Type != "CERTIFICATE" {
|
||||
_ = os.Remove(tempPemFile.Name())
|
||||
return nil, fmt.Errorf("certificate %v contains invalid data, and must be created with 'kubectl create secret tls'", name)
|
||||
}
|
||||
|
||||
pemCert, err := x509.ParseCertificate(pemBlock.Bytes)
|
||||
if err != nil {
|
||||
_ = os.Remove(tempPemFile.Name())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
//Ensure that certificate and private key have a matching public key
|
||||
if _, err := tls.X509KeyPair(cert, key); err != nil {
|
||||
_ = os.Remove(tempPemFile.Name())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cn := sets.NewString(pemCert.Subject.CommonName)
|
||||
for _, dns := range pemCert.DNSNames {
|
||||
if !cn.Has(dns) {
|
||||
cn.Insert(dns)
|
||||
}
|
||||
}
|
||||
|
||||
if len(pemCert.Extensions) > 0 {
|
||||
glog.V(3).Info("parsing ssl certificate extensions")
|
||||
for _, ext := range getExtension(pemCert, oidExtensionSubjectAltName) {
|
||||
dns, _, _, err := parseSANExtension(ext.Value)
|
||||
if err != nil {
|
||||
glog.Warningf("unexpected error parsing certificate extensions: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, dns := range dns {
|
||||
if !cn.Has(dns) {
|
||||
cn.Insert(dns)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err = os.Rename(tempPemFile.Name(), pemFileName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not move temp pem file %v to destination %v: %v", tempPemFile.Name(), pemFileName, err)
|
||||
}
|
||||
|
||||
if len(ca) > 0 {
|
||||
bundle := x509.NewCertPool()
|
||||
bundle.AppendCertsFromPEM(ca)
|
||||
opts := x509.VerifyOptions{
|
||||
Roots: bundle,
|
||||
}
|
||||
|
||||
_, err := pemCert.Verify(opts)
|
||||
if err != nil {
|
||||
oe := fmt.Sprintf("failed to verify certificate chain: \n\t%s\n", err)
|
||||
return nil, errors.New(oe)
|
||||
}
|
||||
|
||||
caFile, err := os.OpenFile(pemFileName, os.O_RDWR|os.O_APPEND, 0600)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not open file %v for writing additional CA chains: %v", pemFileName, err)
|
||||
}
|
||||
|
||||
defer caFile.Close()
|
||||
_, err = caFile.Write([]byte("\n"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not append CA to cert file %v: %v", pemFileName, err)
|
||||
}
|
||||
caFile.Write(ca)
|
||||
caFile.Write([]byte("\n"))
|
||||
|
||||
return &ingress.SSLCert{
|
||||
Certificate: pemCert,
|
||||
CAFileName: pemFileName,
|
||||
PemFileName: pemFileName,
|
||||
PemSHA: file.SHA1(pemFileName),
|
||||
CN: cn.List(),
|
||||
ExpireTime: pemCert.NotAfter,
|
||||
}, nil
|
||||
}
|
||||
|
||||
s := &ingress.SSLCert{
|
||||
Certificate: pemCert,
|
||||
PemFileName: pemFileName,
|
||||
PemSHA: file.SHA1(pemFileName),
|
||||
CN: cn.List(),
|
||||
ExpireTime: pemCert.NotAfter,
|
||||
}
|
||||
|
||||
err = fullChainCert(pemFileName, fullChainPemFileName)
|
||||
if err != nil {
|
||||
glog.Errorf("unexpected error generating SSL certificate with full chain: %v", err)
|
||||
return s, nil
|
||||
}
|
||||
|
||||
s.FullChainPemFileName = fullChainPemFileName
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func getExtension(c *x509.Certificate, id asn1.ObjectIdentifier) []pkix.Extension {
|
||||
var exts []pkix.Extension
|
||||
for _, ext := range c.Extensions {
|
||||
if ext.Id.Equal(id) {
|
||||
exts = append(exts, ext)
|
||||
}
|
||||
}
|
||||
return exts
|
||||
}
|
||||
|
||||
func parseSANExtension(value []byte) (dnsNames, emailAddresses []string, ipAddresses []net.IP, err error) {
|
||||
// RFC 5280, 4.2.1.6
|
||||
|
||||
// SubjectAltName ::= GeneralNames
|
||||
//
|
||||
// GeneralNames ::= SEQUENCE SIZE (1..MAX) OF GeneralName
|
||||
//
|
||||
// GeneralName ::= CHOICE {
|
||||
// otherName [0] OtherName,
|
||||
// rfc822Name [1] IA5String,
|
||||
// dNSName [2] IA5String,
|
||||
// x400Address [3] ORAddress,
|
||||
// directoryName [4] Name,
|
||||
// ediPartyName [5] EDIPartyName,
|
||||
// uniformResourceIdentifier [6] IA5String,
|
||||
// iPAddress [7] OCTET STRING,
|
||||
// registeredID [8] OBJECT IDENTIFIER }
|
||||
var seq asn1.RawValue
|
||||
var rest []byte
|
||||
if rest, err = asn1.Unmarshal(value, &seq); err != nil {
|
||||
return
|
||||
} else if len(rest) != 0 {
|
||||
err = errors.New("x509: trailing data after X.509 extension")
|
||||
return
|
||||
}
|
||||
if !seq.IsCompound || seq.Tag != 16 || seq.Class != 0 {
|
||||
err = asn1.StructuralError{Msg: "bad SAN sequence"}
|
||||
return
|
||||
}
|
||||
|
||||
rest = seq.Bytes
|
||||
for len(rest) > 0 {
|
||||
var v asn1.RawValue
|
||||
rest, err = asn1.Unmarshal(rest, &v)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
switch v.Tag {
|
||||
case 1:
|
||||
emailAddresses = append(emailAddresses, string(v.Bytes))
|
||||
case 2:
|
||||
dnsNames = append(dnsNames, string(v.Bytes))
|
||||
case 7:
|
||||
switch len(v.Bytes) {
|
||||
case net.IPv4len, net.IPv6len:
|
||||
ipAddresses = append(ipAddresses, v.Bytes)
|
||||
default:
|
||||
err = errors.New("x509: certificate contained IP address of length " + strconv.Itoa(len(v.Bytes)))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
// If the first certificate does not start with 'BEGIN CERTIFICATE' it's invalid and must not be used.
|
||||
if pemCABlock.Type != "CERTIFICATE" {
|
||||
return nil, fmt.Errorf("CA file %v contains invalid data, and must be created only with PEM formated certificates", name)
|
||||
}
|
||||
|
||||
_, 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: file.SHA1(caFileName),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// AddOrUpdateDHParam creates a dh parameters file with the specified name
|
||||
func AddOrUpdateDHParam(name string, dh []byte) (string, error) {
|
||||
pemName := fmt.Sprintf("%v.pem", name)
|
||||
pemFileName := fmt.Sprintf("%v/%v", ingress.DefaultSSLDirectory, pemName)
|
||||
|
||||
tempPemFile, err := ioutil.TempFile(ingress.DefaultSSLDirectory, pemName)
|
||||
|
||||
glog.V(3).Infof("Creating temp file %v for DH param: %v", tempPemFile.Name(), pemName)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("could not create temp pem file %v: %v", pemFileName, err)
|
||||
}
|
||||
|
||||
_, err = tempPemFile.Write(dh)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("could not write to pem file %v: %v", tempPemFile.Name(), err)
|
||||
}
|
||||
|
||||
err = tempPemFile.Close()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("could not close temp pem file %v: %v", tempPemFile.Name(), err)
|
||||
}
|
||||
|
||||
pemCerts, err := ioutil.ReadFile(tempPemFile.Name())
|
||||
if err != nil {
|
||||
_ = os.Remove(tempPemFile.Name())
|
||||
return "", err
|
||||
}
|
||||
|
||||
pemBlock, _ := pem.Decode(pemCerts)
|
||||
if pemBlock == nil {
|
||||
_ = os.Remove(tempPemFile.Name())
|
||||
return "", fmt.Errorf("no valid PEM formatted block found")
|
||||
}
|
||||
|
||||
// If the file does not start with 'BEGIN DH PARAMETERS' it's invalid and must not be used.
|
||||
if pemBlock.Type != "DH PARAMETERS" {
|
||||
_ = os.Remove(tempPemFile.Name())
|
||||
return "", fmt.Errorf("certificate %v contains invalid data", name)
|
||||
}
|
||||
|
||||
err = os.Rename(tempPemFile.Name(), pemFileName)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("could not move temp pem file %v to destination %v: %v", tempPemFile.Name(), pemFileName, err)
|
||||
}
|
||||
|
||||
return pemFileName, nil
|
||||
}
|
||||
|
||||
// GetFakeSSLCert creates a Self Signed Certificate
|
||||
// Based in the code https://golang.org/src/crypto/tls/generate_cert.go
|
||||
func GetFakeSSLCert() ([]byte, []byte) {
|
||||
|
||||
var priv interface{}
|
||||
var err error
|
||||
|
||||
priv, err = rsa.GenerateKey(rand.Reader, 2048)
|
||||
|
||||
if err != nil {
|
||||
glog.Fatalf("failed to generate fake private key: %s", err)
|
||||
}
|
||||
|
||||
notBefore := time.Now()
|
||||
// This certificate is valid for 365 days
|
||||
notAfter := notBefore.Add(365 * 24 * time.Hour)
|
||||
|
||||
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
|
||||
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
|
||||
|
||||
if err != nil {
|
||||
glog.Fatalf("failed to generate fake serial number: %s", err)
|
||||
}
|
||||
|
||||
template := x509.Certificate{
|
||||
SerialNumber: serialNumber,
|
||||
Subject: pkix.Name{
|
||||
Organization: []string{"Acme Co"},
|
||||
CommonName: "Kubernetes Ingress Controller Fake Certificate",
|
||||
},
|
||||
NotBefore: notBefore,
|
||||
NotAfter: notAfter,
|
||||
|
||||
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||
BasicConstraintsValid: true,
|
||||
DNSNames: []string{"ingress.local"},
|
||||
}
|
||||
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.(*rsa.PrivateKey).PublicKey, priv)
|
||||
if err != nil {
|
||||
glog.Fatalf("Failed to create fake certificate: %s", err)
|
||||
}
|
||||
|
||||
cert := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
|
||||
|
||||
key := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv.(*rsa.PrivateKey))})
|
||||
|
||||
return cert, key
|
||||
}
|
||||
|
||||
func fullChainCert(in, out string) error {
|
||||
inputFile, err := os.Open(in)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data, err := ioutil.ReadAll(inputFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cert, err := certUtil.DecodeCertificate(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
certs, err := certUtil.FetchCertificateChain(cert)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
certs, err = certUtil.AddRootCA(certs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data = certUtil.EncodeCertificates(certs)
|
||||
return ioutil.WriteFile(out, data, 0644)
|
||||
}
|
||||
151
pkg/net/ssl/ssl_test.go
Normal file
151
pkg/net/ssl/ssl_test.go
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
/*
|
||||
Copyright 2015 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 ssl
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
certutil "k8s.io/client-go/util/cert"
|
||||
"k8s.io/client-go/util/cert/triple"
|
||||
|
||||
"k8s.io/ingress-nginx/pkg/ingress"
|
||||
)
|
||||
|
||||
// generateRSACerts generates a self signed certificate using a self generated ca
|
||||
func generateRSACerts(host string) (*triple.KeyPair, *triple.KeyPair, error) {
|
||||
ca, err := triple.NewCA("self-sign-ca")
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
key, err := certutil.NewPrivateKey()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("unable to create a server private key: %v", err)
|
||||
}
|
||||
|
||||
config := certutil.Config{
|
||||
CommonName: host,
|
||||
Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny},
|
||||
}
|
||||
cert, err := certutil.NewSignedCert(config, key, ca.Cert, ca.Key)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("unable to sign the server certificate: %v", err)
|
||||
}
|
||||
|
||||
return &triple.KeyPair{
|
||||
Key: key,
|
||||
Cert: cert,
|
||||
}, ca, nil
|
||||
}
|
||||
|
||||
func TestAddOrUpdateCertAndKey(t *testing.T) {
|
||||
td, err := ioutil.TempDir("", "ssl")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error creating temporal directory: %v", err)
|
||||
}
|
||||
ingress.DefaultSSLDirectory = td
|
||||
|
||||
cert, _, err := generateRSACerts("echoheaders")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error creating SSL certificate: %v", err)
|
||||
}
|
||||
|
||||
name := fmt.Sprintf("test-%v", time.Now().UnixNano())
|
||||
|
||||
c := certutil.EncodeCertPEM(cert.Cert)
|
||||
k := certutil.EncodePrivateKeyPEM(cert.Key)
|
||||
|
||||
ngxCert, err := AddOrUpdateCertAndKey(name, c, k, []byte{})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error checking SSL certificate: %v", err)
|
||||
}
|
||||
|
||||
if ngxCert.PemFileName == "" {
|
||||
t.Fatalf("expected path to pem file but returned empty")
|
||||
}
|
||||
|
||||
if len(ngxCert.CN) == 0 {
|
||||
t.Fatalf("expected at least one cname but none returned")
|
||||
}
|
||||
|
||||
if ngxCert.CN[0] != "echoheaders" {
|
||||
t.Fatalf("expected cname echoheaders but %v returned", ngxCert.CN[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestCACert(t *testing.T) {
|
||||
td, err := ioutil.TempDir("", "ssl")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error creating temporal directory: %v", err)
|
||||
}
|
||||
ingress.DefaultSSLDirectory = td
|
||||
|
||||
cert, CA, err := generateRSACerts("echoheaders")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error creating SSL certificate: %v", err)
|
||||
}
|
||||
|
||||
name := fmt.Sprintf("test-%v", time.Now().UnixNano())
|
||||
|
||||
c := certutil.EncodeCertPEM(cert.Cert)
|
||||
k := certutil.EncodePrivateKeyPEM(cert.Key)
|
||||
ca := certutil.EncodeCertPEM(CA.Cert)
|
||||
|
||||
ngxCert, err := AddOrUpdateCertAndKey(name, c, k, ca)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error checking SSL certificate: %v", err)
|
||||
}
|
||||
if ngxCert.CAFileName == "" {
|
||||
t.Fatalf("expected a valid CA file name")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetFakeSSLCert(t *testing.T) {
|
||||
k, c := GetFakeSSLCert()
|
||||
if len(k) == 0 {
|
||||
t.Fatalf("expected a valid key")
|
||||
}
|
||||
if len(c) == 0 {
|
||||
t.Fatalf("expected a valid certificate")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddCertAuth(t *testing.T) {
|
||||
td, err := ioutil.TempDir("", "ssl")
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error creating temporal directory: %v", err)
|
||||
}
|
||||
ingress.DefaultSSLDirectory = td
|
||||
|
||||
cn := "demo-ca"
|
||||
_, ca, err := generateRSACerts(cn)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error creating SSL certificate: %v", err)
|
||||
}
|
||||
c := certutil.EncodeCertPEM(ca.Cert)
|
||||
ic, err := AddCertAuth(cn, c)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error creating SSL certificate: %v", err)
|
||||
}
|
||||
if ic.CAFileName == "" {
|
||||
t.Fatalf("expected a valid CA file name")
|
||||
}
|
||||
}
|
||||
529
pkg/nginx/config/config.go
Normal file
529
pkg/nginx/config/config.go
Normal file
|
|
@ -0,0 +1,529 @@
|
|||
/*
|
||||
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 config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"strconv"
|
||||
|
||||
"github.com/golang/glog"
|
||||
|
||||
apiv1 "k8s.io/api/core/v1"
|
||||
|
||||
"k8s.io/ingress-nginx/pkg/ingress"
|
||||
"k8s.io/ingress-nginx/pkg/ingress/defaults"
|
||||
)
|
||||
|
||||
const (
|
||||
// http://nginx.org/en/docs/http/ngx_http_core_module.html#client_max_body_size
|
||||
// Sets the maximum allowed size of the client request body
|
||||
bodySize = "1m"
|
||||
|
||||
// http://nginx.org/en/docs/ngx_core_module.html#error_log
|
||||
// Configures logging level [debug | info | notice | warn | error | crit | alert | emerg]
|
||||
// Log levels above are listed in the order of increasing severity
|
||||
errorLevel = "notice"
|
||||
|
||||
// HTTP Strict Transport Security (often abbreviated as HSTS) is a security feature (HTTP header)
|
||||
// that tell browsers that it should only be communicated with using HTTPS, instead of using HTTP.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/Security/HTTP_strict_transport_security
|
||||
// max-age is the time, in seconds, that the browser should remember that this site is only to be accessed using HTTPS.
|
||||
hstsMaxAge = "15724800"
|
||||
|
||||
gzipTypes = "application/atom+xml application/javascript application/x-javascript application/json application/rss+xml application/vnd.ms-fontobject application/x-font-ttf application/x-web-app-manifest+json application/xhtml+xml application/xml font/opentype image/svg+xml image/x-icon text/css text/plain text/x-component"
|
||||
|
||||
logFormatUpstream = `%v - [$the_real_ip] - $remote_user [$time_local] "$request" $status $body_bytes_sent "$http_referer" "$http_user_agent" $request_length $request_time [$proxy_upstream_name] $upstream_addr $upstream_response_length $upstream_response_time $upstream_status`
|
||||
|
||||
logFormatStream = `[$time_local] $protocol $status $bytes_sent $bytes_received $session_time`
|
||||
|
||||
// http://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_buffer_size
|
||||
// Sets the size of the buffer used for sending data.
|
||||
// 4k helps NGINX to improve TLS Time To First Byte (TTTFB)
|
||||
// https://www.igvita.com/2013/12/16/optimizing-nginx-tls-time-to-first-byte/
|
||||
sslBufferSize = "4k"
|
||||
|
||||
// Enabled ciphers list to enabled. The ciphers are specified in the format understood by the OpenSSL library
|
||||
// http://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_ciphers
|
||||
sslCiphers = "ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA"
|
||||
|
||||
// SSL enabled protocols to use
|
||||
// http://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_protocols
|
||||
sslProtocols = "TLSv1 TLSv1.1 TLSv1.2"
|
||||
|
||||
// Time during which a client may reuse the session parameters stored in a cache.
|
||||
// http://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_session_timeout
|
||||
sslSessionTimeout = "10m"
|
||||
|
||||
// Size of the SSL shared cache between all worker processes.
|
||||
// http://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_session_cache
|
||||
sslSessionCacheSize = "10m"
|
||||
|
||||
// Default setting for load balancer algorithm
|
||||
defaultLoadBalancerAlgorithm = "least_conn"
|
||||
|
||||
// Parameters for a shared memory zone that will keep states for various keys.
|
||||
// http://nginx.org/en/docs/http/ngx_http_limit_conn_module.html#limit_conn_zone
|
||||
defaultLimitConnZoneVariable = "$binary_remote_addr"
|
||||
)
|
||||
|
||||
// Configuration represents the content of nginx.conf file
|
||||
type Configuration struct {
|
||||
defaults.Backend `json:",squash"`
|
||||
|
||||
// Sets the name of the configmap that contains the headers to pass to the client
|
||||
AddHeaders string `json:"add-headers,omitempty"`
|
||||
|
||||
// AllowBackendServerHeader enables the return of the header Server from the backend
|
||||
// instead of the generic nginx string.
|
||||
// http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_hide_header
|
||||
// By default this is disabled
|
||||
AllowBackendServerHeader bool `json:"allow-backend-server-header"`
|
||||
|
||||
// AccessLogPath sets the path of the access logs if enabled
|
||||
// http://nginx.org/en/docs/http/ngx_http_log_module.html#access_log
|
||||
// By default access logs go to /var/log/nginx/access.log
|
||||
AccessLogPath string `json:"access-log-path,omitempty"`
|
||||
|
||||
// ErrorLogPath sets the path of the error logs
|
||||
// http://nginx.org/en/docs/ngx_core_module.html#error_log
|
||||
// By default error logs go to /var/log/nginx/error.log
|
||||
ErrorLogPath string `json:"error-log-path,omitempty"`
|
||||
|
||||
// EnableDynamicTLSRecords enables dynamic TLS record sizes
|
||||
// https://blog.cloudflare.com/optimizing-tls-over-tcp-to-reduce-latency
|
||||
// By default this is enabled
|
||||
EnableDynamicTLSRecords bool `json:"enable-dynamic-tls-records"`
|
||||
|
||||
// ClientHeaderBufferSize allows to configure a custom buffer
|
||||
// size for reading client request header
|
||||
// http://nginx.org/en/docs/http/ngx_http_core_module.html#client_header_buffer_size
|
||||
ClientHeaderBufferSize string `json:"client-header-buffer-size"`
|
||||
|
||||
// Defines a timeout for reading client request header, in seconds
|
||||
// http://nginx.org/en/docs/http/ngx_http_core_module.html#client_header_timeout
|
||||
ClientHeaderTimeout int `json:"client-header-timeout,omitempty"`
|
||||
|
||||
// Sets buffer size for reading client request body
|
||||
// http://nginx.org/en/docs/http/ngx_http_core_module.html#client_body_buffer_size
|
||||
ClientBodyBufferSize string `json:"client-body-buffer-size,omitempty"`
|
||||
|
||||
// Defines a timeout for reading client request body, in seconds
|
||||
// http://nginx.org/en/docs/http/ngx_http_core_module.html#client_body_timeout
|
||||
ClientBodyTimeout int `json:"client-body-timeout,omitempty"`
|
||||
|
||||
// DisableAccessLog disables the Access Log globally from NGINX ingress controller
|
||||
//http://nginx.org/en/docs/http/ngx_http_log_module.html
|
||||
DisableAccessLog bool `json:"disable-access-log,omitempty"`
|
||||
|
||||
// DisableIpv6 disable listening on ipv6 address
|
||||
DisableIpv6 bool `json:"disable-ipv6,omitempty"`
|
||||
|
||||
// EnableUnderscoresInHeaders enables underscores in header names
|
||||
// http://nginx.org/en/docs/http/ngx_http_core_module.html#underscores_in_headers
|
||||
// By default this is disabled
|
||||
EnableUnderscoresInHeaders bool `json:"enable-underscores-in-headers"`
|
||||
|
||||
// IgnoreInvalidHeaders set if header fields with invalid names should be ignored
|
||||
// http://nginx.org/en/docs/http/ngx_http_core_module.html#ignore_invalid_headers
|
||||
// By default this is enabled
|
||||
IgnoreInvalidHeaders bool `json:"ignore-invalid-headers"`
|
||||
|
||||
// EnableVtsStatus allows the replacement of the default status page with a third party module named
|
||||
// nginx-module-vts - https://github.com/vozlt/nginx-module-vts
|
||||
// By default this is disabled
|
||||
EnableVtsStatus bool `json:"enable-vts-status,omitempty"`
|
||||
|
||||
// Vts config on http level
|
||||
// Description: Sets parameters for a shared memory zone that will keep states for various keys. The cache is shared between all worker processe
|
||||
// https://github.com/vozlt/nginx-module-vts#vhost_traffic_status_zone
|
||||
// Default value is 10m
|
||||
VtsStatusZoneSize string `json:"vts-status-zone-size,omitempty"`
|
||||
|
||||
// Vts config on http level
|
||||
// Description: Enables the keys by user defined variable. The key is a key string to calculate traffic.
|
||||
// The name is a group string to calculate traffic. The key and name can contain variables such as $host,
|
||||
// $server_name. The name's group belongs to filterZones if specified. The key's group belongs to serverZones
|
||||
// if not specified second argument name. The example with geoip module is as follows:
|
||||
// https://github.com/vozlt/nginx-module-vts#vhost_traffic_status_filter_by_set_key
|
||||
// Default value is $geoip_country_code country::*
|
||||
VtsDefaultFilterKey string `json:"vts-default-filter-key,omitempty"`
|
||||
|
||||
// RetryNonIdempotent since 1.9.13 NGINX will not retry non-idempotent requests (POST, LOCK, PATCH)
|
||||
// in case of an error. The previous behavior can be restored using the value true
|
||||
RetryNonIdempotent bool `json:"retry-non-idempotent"`
|
||||
|
||||
// http://nginx.org/en/docs/ngx_core_module.html#error_log
|
||||
// Configures logging level [debug | info | notice | warn | error | crit | alert | emerg]
|
||||
// Log levels above are listed in the order of increasing severity
|
||||
ErrorLogLevel string `json:"error-log-level,omitempty"`
|
||||
|
||||
// https://nginx.org/en/docs/http/ngx_http_v2_module.html#http2_max_field_size
|
||||
// HTTP2MaxFieldSize Limits the maximum size of an HPACK-compressed request header field
|
||||
HTTP2MaxFieldSize string `json:"http2-max-field-size,omitempty"`
|
||||
|
||||
// https://nginx.org/en/docs/http/ngx_http_v2_module.html#http2_max_header_size
|
||||
// HTTP2MaxHeaderSize Limits the maximum size of the entire request header list after HPACK decompression
|
||||
HTTP2MaxHeaderSize string `json:"http2-max-header-size,omitempty"`
|
||||
|
||||
// Enables or disables the header HSTS in servers running SSL
|
||||
HSTS bool `json:"hsts,omitempty"`
|
||||
|
||||
// Enables or disables the use of HSTS in all the subdomains of the servername
|
||||
// Default: true
|
||||
HSTSIncludeSubdomains bool `json:"hsts-include-subdomains,omitempty"`
|
||||
|
||||
// HTTP Strict Transport Security (often abbreviated as HSTS) is a security feature (HTTP header)
|
||||
// that tell browsers that it should only be communicated with using HTTPS, instead of using HTTP.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/Security/HTTP_strict_transport_security
|
||||
// max-age is the time, in seconds, that the browser should remember that this site is only to be
|
||||
// accessed using HTTPS.
|
||||
HSTSMaxAge string `json:"hsts-max-age,omitempty"`
|
||||
|
||||
// Enables or disables the preload attribute in HSTS feature
|
||||
HSTSPreload bool `json:"hsts-preload,omitempty"`
|
||||
|
||||
// Time during which a keep-alive client connection will stay open on the server side.
|
||||
// The zero value disables keep-alive client connections
|
||||
// http://nginx.org/en/docs/http/ngx_http_core_module.html#keepalive_timeout
|
||||
KeepAlive int `json:"keep-alive,omitempty"`
|
||||
|
||||
// Sets the maximum number of requests that can be served through one keep-alive connection.
|
||||
// http://nginx.org/en/docs/http/ngx_http_core_module.html#keepalive_requests
|
||||
KeepAliveRequests int `json:"keep-alive-requests,omitempty"`
|
||||
|
||||
// LargeClientHeaderBuffers Sets the maximum number and size of buffers used for reading
|
||||
// large client request header.
|
||||
// http://nginx.org/en/docs/http/ngx_http_core_module.html#large_client_header_buffers
|
||||
// Default: 4 8k
|
||||
LargeClientHeaderBuffers string `json:"large-client-header-buffers"`
|
||||
|
||||
// Enable json escaping
|
||||
// http://nginx.org/en/docs/http/ngx_http_log_module.html#log_format
|
||||
LogFormatEscapeJSON bool `json:"log-format-escape-json,omitempty"`
|
||||
|
||||
// Customize upstream log_format
|
||||
// http://nginx.org/en/docs/http/ngx_http_log_module.html#log_format
|
||||
LogFormatUpstream string `json:"log-format-upstream,omitempty"`
|
||||
|
||||
// Customize stream log_format
|
||||
// http://nginx.org/en/docs/http/ngx_http_log_module.html#log_format
|
||||
LogFormatStream string `json:"log-format-stream,omitempty"`
|
||||
|
||||
// Maximum number of simultaneous connections that can be opened by each worker process
|
||||
// http://nginx.org/en/docs/ngx_core_module.html#worker_connections
|
||||
MaxWorkerConnections int `json:"max-worker-connections,omitempty"`
|
||||
|
||||
// Sets the bucket size for the map variables hash tables.
|
||||
// Default value depends on the processor’s cache line size.
|
||||
// http://nginx.org/en/docs/http/ngx_http_map_module.html#map_hash_bucket_size
|
||||
MapHashBucketSize int `json:"map-hash-bucket-size,omitempty"`
|
||||
|
||||
// If UseProxyProtocol is enabled ProxyRealIPCIDR defines the default the IP/network address
|
||||
// of your external load balancer
|
||||
ProxyRealIPCIDR []string `json:"proxy-real-ip-cidr,omitempty"`
|
||||
|
||||
// Sets the name of the configmap that contains the headers to pass to the backend
|
||||
ProxySetHeaders string `json:"proxy-set-headers,omitempty"`
|
||||
|
||||
// Maximum size of the server names hash tables used in server names, map directive’s values,
|
||||
// MIME types, names of request header strings, etcd.
|
||||
// http://nginx.org/en/docs/hash.html
|
||||
// http://nginx.org/en/docs/http/ngx_http_core_module.html#server_names_hash_max_size
|
||||
ServerNameHashMaxSize int `json:"server-name-hash-max-size,omitempty"`
|
||||
|
||||
// Size of the bucket for the server names hash tables
|
||||
// http://nginx.org/en/docs/hash.html
|
||||
// http://nginx.org/en/docs/http/ngx_http_core_module.html#server_names_hash_bucket_size
|
||||
ServerNameHashBucketSize int `json:"server-name-hash-bucket-size,omitempty"`
|
||||
|
||||
// Size of the bucket for the proxy headers hash tables
|
||||
// http://nginx.org/en/docs/hash.html
|
||||
// https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_headers_hash_max_size
|
||||
ProxyHeadersHashMaxSize int `json:"proxy-headers-hash-max-size,omitempty"`
|
||||
|
||||
// Maximum size of the bucket for the proxy headers hash tables
|
||||
// http://nginx.org/en/docs/hash.html
|
||||
// https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_headers_hash_bucket_size
|
||||
ProxyHeadersHashBucketSize int `json:"proxy-headers-hash-bucket-size,omitempty"`
|
||||
|
||||
// Enables or disables emitting nginx version in error messages and in the “Server” response header field.
|
||||
// http://nginx.org/en/docs/http/ngx_http_core_module.html#server_tokens
|
||||
// Default: true
|
||||
ShowServerTokens bool `json:"server-tokens"`
|
||||
|
||||
// Enabled ciphers list to enabled. The ciphers are specified in the format understood by
|
||||
// the OpenSSL library
|
||||
// http://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_ciphers
|
||||
SSLCiphers string `json:"ssl-ciphers,omitempty"`
|
||||
|
||||
// Specifies a curve for ECDHE ciphers.
|
||||
// http://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_ecdh_curve
|
||||
SSLECDHCurve string `json:"ssl-ecdh-curve,omitempty"`
|
||||
|
||||
// The secret that contains Diffie-Hellman key to help with "Perfect Forward Secrecy"
|
||||
// https://www.openssl.org/docs/manmaster/apps/dhparam.html
|
||||
// https://wiki.mozilla.org/Security/Server_Side_TLS#DHE_handshake_and_dhparam
|
||||
// http://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_dhparam
|
||||
SSLDHParam string `json:"ssl-dh-param,omitempty"`
|
||||
|
||||
// SSL enabled protocols to use
|
||||
// http://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_protocols
|
||||
SSLProtocols string `json:"ssl-protocols,omitempty"`
|
||||
|
||||
// Enables or disables the use of shared SSL cache among worker processes.
|
||||
// http://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_session_cache
|
||||
SSLSessionCache bool `json:"ssl-session-cache,omitempty"`
|
||||
|
||||
// Size of the SSL shared cache between all worker processes.
|
||||
// http://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_session_cache
|
||||
SSLSessionCacheSize string `json:"ssl-session-cache-size,omitempty"`
|
||||
|
||||
// Enables or disables session resumption through TLS session tickets.
|
||||
// http://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_session_tickets
|
||||
SSLSessionTickets bool `json:"ssl-session-tickets,omitempty"`
|
||||
|
||||
// Time during which a client may reuse the session parameters stored in a cache.
|
||||
// http://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_session_timeout
|
||||
SSLSessionTimeout string `json:"ssl-session-timeout,omitempty"`
|
||||
|
||||
// http://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_buffer_size
|
||||
// Sets the size of the buffer used for sending data.
|
||||
// 4k helps NGINX to improve TLS Time To First Byte (TTTFB)
|
||||
// https://www.igvita.com/2013/12/16/optimizing-nginx-tls-time-to-first-byte/
|
||||
SSLBufferSize string `json:"ssl-buffer-size,omitempty"`
|
||||
|
||||
// Enables or disables the use of the PROXY protocol to receive client connection
|
||||
// (real IP address) information passed through proxy servers and load balancers
|
||||
// such as HAproxy and Amazon Elastic Load Balancer (ELB).
|
||||
// https://www.nginx.com/resources/admin-guide/proxy-protocol/
|
||||
UseProxyProtocol bool `json:"use-proxy-protocol,omitempty"`
|
||||
|
||||
// Enables or disables the use of the nginx module that compresses responses using the "gzip" method
|
||||
// http://nginx.org/en/docs/http/ngx_http_gzip_module.html
|
||||
UseGzip bool `json:"use-gzip,omitempty"`
|
||||
|
||||
// Enables or disables the HTTP/2 support in secure connections
|
||||
// http://nginx.org/en/docs/http/ngx_http_v2_module.html
|
||||
// Default: true
|
||||
UseHTTP2 bool `json:"use-http2,omitempty"`
|
||||
|
||||
// MIME types in addition to "text/html" to compress. The special value “*” matches any MIME type.
|
||||
// Responses with the “text/html” type are always compressed if UseGzip is enabled
|
||||
GzipTypes string `json:"gzip-types,omitempty"`
|
||||
|
||||
// Defines the number of worker processes. By default auto means number of available CPU cores
|
||||
// http://nginx.org/en/docs/ngx_core_module.html#worker_processes
|
||||
WorkerProcesses string `json:"worker-processes,omitempty"`
|
||||
|
||||
// Defines a timeout for a graceful shutdown of worker processes
|
||||
// http://nginx.org/en/docs/ngx_core_module.html#worker_shutdown_timeout
|
||||
WorkerShutdownTimeout string `json:"worker-shutdown-timeout,omitempty"`
|
||||
|
||||
// Defines the load balancing algorithm to use. The deault is round-robin
|
||||
LoadBalanceAlgorithm string `json:"load-balance,omitempty"`
|
||||
|
||||
// Sets the bucket size for the variables hash table.
|
||||
// http://nginx.org/en/docs/http/ngx_http_map_module.html#variables_hash_bucket_size
|
||||
VariablesHashBucketSize int `json:"variables-hash-bucket-size,omitempty"`
|
||||
|
||||
// Sets the maximum size of the variables hash table.
|
||||
// http://nginx.org/en/docs/http/ngx_http_map_module.html#variables_hash_max_size
|
||||
VariablesHashMaxSize int `json:"variables-hash-max-size,omitempty"`
|
||||
|
||||
// Activates the cache for connections to upstream servers.
|
||||
// The connections parameter sets the maximum number of idle keepalive connections to
|
||||
// upstream servers that are preserved in the cache of each worker process. When this
|
||||
// number is exceeded, the least recently used connections are closed.
|
||||
// http://nginx.org/en/docs/http/ngx_http_upstream_module.html#keepalive
|
||||
// Default: 32
|
||||
UpstreamKeepaliveConnections int `json:"upstream-keepalive-connections,omitempty"`
|
||||
|
||||
// Sets the maximum size of the variables hash table.
|
||||
// http://nginx.org/en/docs/http/ngx_http_map_module.html#variables_hash_max_size
|
||||
LimitConnZoneVariable string `json:"limit-conn-zone-variable,omitempty"`
|
||||
|
||||
// Sets the timeout between two successive read or write operations on client or proxied server connections.
|
||||
// If no data is transmitted within this time, the connection is closed.
|
||||
// http://nginx.org/en/docs/stream/ngx_stream_proxy_module.html#proxy_timeout
|
||||
ProxyStreamTimeout string `json:"proxy-stream-timeout,omitempty"`
|
||||
|
||||
// Sets the ipv4 addresses on which the server will accept requests.
|
||||
BindAddressIpv4 []string `json:"bind-address-ipv4,omitempty"`
|
||||
|
||||
// Sets the ipv6 addresses on which the server will accept requests.
|
||||
BindAddressIpv6 []string `json:"bind-address-ipv6,omitempty"`
|
||||
|
||||
// Sets the header field for identifying the originating IP address of a client
|
||||
// Default is X-Forwarded-For
|
||||
ForwardedForHeader string `json:"forwarded-for-header,omitempty"`
|
||||
|
||||
// EnableOpentracing enables the nginx Opentracing extension
|
||||
// https://github.com/rnburn/nginx-opentracing
|
||||
// By default this is disabled
|
||||
EnableOpentracing bool `json:"enable-opentracing"`
|
||||
|
||||
// ZipkinCollectorHost specifies the host to use when uploading traces
|
||||
ZipkinCollectorHost string `json:"zipkin-collector-host"`
|
||||
|
||||
// ZipkinCollectorPort specifies the port to use when uploading traces
|
||||
ZipkinCollectorPort int `json:"zipkin-collector-port"`
|
||||
|
||||
// ZipkinServiceName specifies the service name to use for any traces created
|
||||
// Default: nginx
|
||||
ZipkinServiceName string `json:"zipkin-service-name"`
|
||||
|
||||
// HTTPSnippet adds custom configuration to the http section of the nginx configuration
|
||||
HTTPSnippet string `json:"http-snippet"`
|
||||
|
||||
// ServerSnippet adds custom configuration to all the servers in the nginx configuration
|
||||
ServerSnippet string `json:"server-snippet"`
|
||||
|
||||
// LocationSnippet adds custom configuration to all the locations in the nginx configuration
|
||||
LocationSnippet string `json:"location-snippet"`
|
||||
}
|
||||
|
||||
// NewDefault returns the default nginx configuration
|
||||
func NewDefault() Configuration {
|
||||
defIPCIDR := make([]string, 0)
|
||||
defIPCIDR = append(defIPCIDR, "0.0.0.0/0")
|
||||
defBindAddress := make([]string, 0)
|
||||
cfg := Configuration{
|
||||
AllowBackendServerHeader: false,
|
||||
AccessLogPath: "/var/log/nginx/access.log",
|
||||
ErrorLogPath: "/var/log/nginx/error.log",
|
||||
ClientHeaderBufferSize: "1k",
|
||||
ClientHeaderTimeout: 60,
|
||||
ClientBodyBufferSize: "8k",
|
||||
ClientBodyTimeout: 60,
|
||||
EnableDynamicTLSRecords: true,
|
||||
EnableUnderscoresInHeaders: false,
|
||||
ErrorLogLevel: errorLevel,
|
||||
ForwardedForHeader: "X-Forwarded-For",
|
||||
HTTP2MaxFieldSize: "4k",
|
||||
HTTP2MaxHeaderSize: "16k",
|
||||
HSTS: true,
|
||||
HSTSIncludeSubdomains: true,
|
||||
HSTSMaxAge: hstsMaxAge,
|
||||
HSTSPreload: false,
|
||||
IgnoreInvalidHeaders: true,
|
||||
GzipTypes: gzipTypes,
|
||||
KeepAlive: 75,
|
||||
KeepAliveRequests: 100,
|
||||
LargeClientHeaderBuffers: "4 8k",
|
||||
LogFormatEscapeJSON: false,
|
||||
LogFormatStream: logFormatStream,
|
||||
LogFormatUpstream: logFormatUpstream,
|
||||
MaxWorkerConnections: 16384,
|
||||
MapHashBucketSize: 64,
|
||||
ProxyRealIPCIDR: defIPCIDR,
|
||||
ServerNameHashMaxSize: 1024,
|
||||
ProxyHeadersHashMaxSize: 512,
|
||||
ProxyHeadersHashBucketSize: 64,
|
||||
ShowServerTokens: true,
|
||||
SSLBufferSize: sslBufferSize,
|
||||
SSLCiphers: sslCiphers,
|
||||
SSLECDHCurve: "auto",
|
||||
SSLProtocols: sslProtocols,
|
||||
SSLSessionCache: true,
|
||||
SSLSessionCacheSize: sslSessionCacheSize,
|
||||
SSLSessionTickets: true,
|
||||
SSLSessionTimeout: sslSessionTimeout,
|
||||
UseGzip: true,
|
||||
WorkerProcesses: strconv.Itoa(runtime.NumCPU()),
|
||||
WorkerShutdownTimeout: "10s",
|
||||
LoadBalanceAlgorithm: defaultLoadBalancerAlgorithm,
|
||||
VtsStatusZoneSize: "10m",
|
||||
VtsDefaultFilterKey: "$geoip_country_code country::*",
|
||||
VariablesHashBucketSize: 64,
|
||||
VariablesHashMaxSize: 2048,
|
||||
UseHTTP2: true,
|
||||
ProxyStreamTimeout: "600s",
|
||||
Backend: defaults.Backend{
|
||||
ProxyBodySize: bodySize,
|
||||
ProxyConnectTimeout: 5,
|
||||
ProxyReadTimeout: 60,
|
||||
ProxySendTimeout: 60,
|
||||
ProxyBufferSize: "4k",
|
||||
ProxyCookieDomain: "off",
|
||||
ProxyCookiePath: "off",
|
||||
ProxyNextUpstream: "error timeout invalid_header http_502 http_503 http_504",
|
||||
ProxyRequestBuffering: "on",
|
||||
SSLRedirect: true,
|
||||
CustomHTTPErrors: []int{},
|
||||
WhitelistSourceRange: []string{},
|
||||
SkipAccessLogURLs: []string{},
|
||||
LimitRate: 0,
|
||||
LimitRateAfter: 0,
|
||||
},
|
||||
UpstreamKeepaliveConnections: 32,
|
||||
LimitConnZoneVariable: defaultLimitConnZoneVariable,
|
||||
BindAddressIpv4: defBindAddress,
|
||||
BindAddressIpv6: defBindAddress,
|
||||
ZipkinCollectorPort: 9411,
|
||||
ZipkinServiceName: "nginx",
|
||||
}
|
||||
|
||||
if glog.V(5) {
|
||||
cfg.ErrorLogLevel = "debug"
|
||||
}
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
||||
// BuildLogFormatUpstream format the log_format upstream using
|
||||
// proxy_protocol_addr as remote client address if UseProxyProtocol
|
||||
// is enabled.
|
||||
func (cfg Configuration) BuildLogFormatUpstream() string {
|
||||
if cfg.LogFormatUpstream == logFormatUpstream {
|
||||
return fmt.Sprintf(cfg.LogFormatUpstream, "$the_real_ip")
|
||||
}
|
||||
|
||||
return cfg.LogFormatUpstream
|
||||
}
|
||||
|
||||
// TemplateConfig contains the nginx configuration to render the file nginx.conf
|
||||
type TemplateConfig struct {
|
||||
ProxySetHeaders map[string]string
|
||||
AddHeaders map[string]string
|
||||
MaxOpenFiles int
|
||||
BacklogSize int
|
||||
Backends []*ingress.Backend
|
||||
PassthroughBackends []*ingress.SSLPassthroughBackend
|
||||
Servers []*ingress.Server
|
||||
TCPBackends []ingress.L4Service
|
||||
UDPBackends []ingress.L4Service
|
||||
HealthzURI string
|
||||
CustomErrors bool
|
||||
Cfg Configuration
|
||||
IsIPV6Enabled bool
|
||||
IsSSLPassthroughEnabled bool
|
||||
RedirectServers map[string]string
|
||||
ListenPorts *ListenPorts
|
||||
PublishService *apiv1.Service
|
||||
}
|
||||
|
||||
// ListenPorts describe the ports required to run the
|
||||
// NGINX Ingress controller
|
||||
type ListenPorts struct {
|
||||
HTTP int
|
||||
HTTPS int
|
||||
Status int
|
||||
Health int
|
||||
Default int
|
||||
SSLProxy int
|
||||
}
|
||||
46
pkg/nginx/config/config_test.go
Normal file
46
pkg/nginx/config/config_test.go
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
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 config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBuildLogFormatUpstream(t *testing.T) {
|
||||
|
||||
testCases := []struct {
|
||||
useProxyProtocol bool // use proxy protocol
|
||||
curLogFormat string
|
||||
expected string
|
||||
}{
|
||||
{true, logFormatUpstream, fmt.Sprintf(logFormatUpstream, "$the_real_ip")},
|
||||
{false, logFormatUpstream, fmt.Sprintf(logFormatUpstream, "$the_real_ip")},
|
||||
{true, "my-log-format", "my-log-format"},
|
||||
{false, "john-log-format", "john-log-format"},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
cfg := NewDefault()
|
||||
cfg.UseProxyProtocol = testCase.useProxyProtocol
|
||||
cfg.LogFormatUpstream = testCase.curLogFormat
|
||||
result := cfg.BuildLogFormatUpstream()
|
||||
if result != testCase.expected {
|
||||
t.Errorf(" expected %v but return %v", testCase.expected, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
97
pkg/nginx/controller/metrics.go
Normal file
97
pkg/nginx/controller/metrics.go
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
/*
|
||||
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 controller
|
||||
|
||||
import (
|
||||
"github.com/golang/glog"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
|
||||
"k8s.io/ingress-nginx/pkg/nginx/metric/collector"
|
||||
)
|
||||
|
||||
const (
|
||||
ngxStatusPath = "/nginx_status"
|
||||
ngxVtsPath = "/nginx_status/format/json"
|
||||
)
|
||||
|
||||
func (n *NGINXController) setupMonitor(sm statusModule) {
|
||||
csm := n.statusModule
|
||||
if csm != sm {
|
||||
glog.Infof("changing prometheus collector from %v to %v", csm, sm)
|
||||
n.stats.stop(csm)
|
||||
n.stats.start(sm)
|
||||
n.statusModule = sm
|
||||
}
|
||||
}
|
||||
|
||||
type statsCollector struct {
|
||||
process prometheus.Collector
|
||||
basic collector.Stopable
|
||||
vts collector.Stopable
|
||||
|
||||
namespace string
|
||||
watchClass string
|
||||
|
||||
port int
|
||||
}
|
||||
|
||||
func (s *statsCollector) stop(sm statusModule) {
|
||||
switch sm {
|
||||
case defaultStatusModule:
|
||||
s.basic.Stop()
|
||||
prometheus.Unregister(s.basic)
|
||||
case vtsStatusModule:
|
||||
s.vts.Stop()
|
||||
prometheus.Unregister(s.vts)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *statsCollector) start(sm statusModule) {
|
||||
switch sm {
|
||||
case defaultStatusModule:
|
||||
s.basic = collector.NewNginxStatus(s.namespace, s.watchClass, s.port, ngxStatusPath)
|
||||
prometheus.Register(s.basic)
|
||||
break
|
||||
case vtsStatusModule:
|
||||
s.vts = collector.NewNGINXVTSCollector(s.namespace, s.watchClass, s.port, ngxVtsPath)
|
||||
prometheus.Register(s.vts)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func newStatsCollector(ns, class, binary string, port int) *statsCollector {
|
||||
glog.Infof("starting new nginx stats collector for Ingress controller running in namespace %v (class %v)", ns, class)
|
||||
glog.Infof("collector extracting information from port %v", port)
|
||||
pc, err := collector.NewNamedProcess(true, collector.BinaryNameMatcher{
|
||||
Name: "nginx",
|
||||
Binary: binary,
|
||||
})
|
||||
if err != nil {
|
||||
glog.Fatalf("unexpected error registering nginx collector: %v", err)
|
||||
}
|
||||
err = prometheus.Register(pc)
|
||||
if err != nil {
|
||||
glog.Fatalf("unexpected error registering nginx collector: %v", err)
|
||||
}
|
||||
|
||||
return &statsCollector{
|
||||
namespace: ns,
|
||||
watchClass: class,
|
||||
process: pc,
|
||||
port: port,
|
||||
}
|
||||
}
|
||||
799
pkg/nginx/controller/nginx.go
Normal file
799
pkg/nginx/controller/nginx.go
Normal file
|
|
@ -0,0 +1,799 @@
|
|||
/*
|
||||
Copyright 2015 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 controller
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/golang/glog"
|
||||
"github.com/mitchellh/go-ps"
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
proxyproto "github.com/armon/go-proxyproto"
|
||||
"github.com/ncabatoff/process-exporter/proc"
|
||||
apiv1 "k8s.io/api/core/v1"
|
||||
extensions "k8s.io/api/extensions/v1beta1"
|
||||
|
||||
"k8s.io/ingress-nginx/pkg/ingress"
|
||||
"k8s.io/ingress-nginx/pkg/ingress/controller"
|
||||
"k8s.io/ingress-nginx/pkg/ingress/defaults"
|
||||
"k8s.io/ingress-nginx/pkg/net/dns"
|
||||
"k8s.io/ingress-nginx/pkg/net/ssl"
|
||||
"k8s.io/ingress-nginx/pkg/nginx/config"
|
||||
ngx_template "k8s.io/ingress-nginx/pkg/nginx/template"
|
||||
"k8s.io/ingress-nginx/version"
|
||||
)
|
||||
|
||||
type statusModule string
|
||||
|
||||
const (
|
||||
ngxHealthPath = "/healthz"
|
||||
|
||||
defaultStatusModule statusModule = "default"
|
||||
vtsStatusModule statusModule = "vts"
|
||||
|
||||
defUpstreamName = "upstream-default-backend"
|
||||
)
|
||||
|
||||
var (
|
||||
tmplPath = "/etc/nginx/template/nginx.tmpl"
|
||||
cfgPath = "/etc/nginx/nginx.conf"
|
||||
nginxBinary = "/usr/sbin/nginx"
|
||||
defIngressClass = "nginx"
|
||||
)
|
||||
|
||||
// NewNGINXController creates a new NGINX Ingress controller.
|
||||
// If the environment variable NGINX_BINARY exists it will be used
|
||||
// as source for nginx commands
|
||||
func NewNGINXController() *NGINXController {
|
||||
ngx := os.Getenv("NGINX_BINARY")
|
||||
if ngx == "" {
|
||||
ngx = nginxBinary
|
||||
}
|
||||
|
||||
h, err := dns.GetSystemNameServers()
|
||||
if err != nil {
|
||||
glog.Warningf("unexpected error reading system nameservers: %v", err)
|
||||
}
|
||||
|
||||
n := &NGINXController{
|
||||
binary: ngx,
|
||||
configmap: &apiv1.ConfigMap{},
|
||||
isIPV6Enabled: isIPv6Enabled(),
|
||||
resolver: h,
|
||||
ports: &config.ListenPorts{},
|
||||
backendDefaults: config.NewDefault().Backend,
|
||||
}
|
||||
|
||||
var onChange func()
|
||||
onChange = func() {
|
||||
template, err := ngx_template.NewTemplate(tmplPath, onChange)
|
||||
if err != nil {
|
||||
// this error is different from the rest because it must be clear why nginx is not working
|
||||
glog.Errorf(`
|
||||
-------------------------------------------------------------------------------
|
||||
Error loading new template : %v
|
||||
-------------------------------------------------------------------------------
|
||||
`, err)
|
||||
return
|
||||
}
|
||||
|
||||
n.t.Close()
|
||||
n.t = template
|
||||
glog.Info("new NGINX template loaded")
|
||||
}
|
||||
|
||||
ngxTpl, err := ngx_template.NewTemplate(tmplPath, onChange)
|
||||
if err != nil {
|
||||
glog.Fatalf("invalid NGINX template: %v", err)
|
||||
}
|
||||
|
||||
n.t = ngxTpl
|
||||
|
||||
return n
|
||||
}
|
||||
|
||||
// NGINXController ...
|
||||
type NGINXController struct {
|
||||
controller *controller.GenericController
|
||||
t *ngx_template.Template
|
||||
|
||||
configmap *apiv1.ConfigMap
|
||||
|
||||
storeLister *ingress.StoreLister
|
||||
|
||||
binary string
|
||||
resolver []net.IP
|
||||
|
||||
cmdArgs []string
|
||||
|
||||
stats *statsCollector
|
||||
statusModule statusModule
|
||||
|
||||
// returns true if IPV6 is enabled in the pod
|
||||
isIPV6Enabled bool
|
||||
|
||||
// returns true if proxy protocol es enabled
|
||||
isProxyProtocolEnabled bool
|
||||
|
||||
isSSLPassthroughEnabled bool
|
||||
|
||||
isShuttingDown bool
|
||||
|
||||
proxy *proxy
|
||||
|
||||
ports *config.ListenPorts
|
||||
|
||||
backendDefaults defaults.Backend
|
||||
}
|
||||
|
||||
// Start start a new NGINX master process running in foreground.
|
||||
func (n *NGINXController) Start() {
|
||||
n.isShuttingDown = false
|
||||
|
||||
n.controller = controller.NewIngressController(n)
|
||||
go n.controller.Start()
|
||||
|
||||
done := make(chan error, 1)
|
||||
cmd := exec.Command(n.binary, "-c", cfgPath)
|
||||
|
||||
// put nginx in another process group to prevent it
|
||||
// to receive signals meant for the controller
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
Setpgid: true,
|
||||
Pgid: 0,
|
||||
}
|
||||
|
||||
glog.Info("starting NGINX process...")
|
||||
n.start(cmd, done)
|
||||
|
||||
// if the nginx master process dies the workers continue to process requests,
|
||||
// passing checks but in case of updates in ingress no updates will be
|
||||
// reflected in the nginx configuration which can lead to confusion and report
|
||||
// issues because of this behavior.
|
||||
// To avoid this issue we restart nginx in case of errors.
|
||||
for {
|
||||
err := <-done
|
||||
|
||||
if n.isShuttingDown {
|
||||
break
|
||||
}
|
||||
|
||||
if exitError, ok := err.(*exec.ExitError); ok {
|
||||
waitStatus := exitError.Sys().(syscall.WaitStatus)
|
||||
glog.Warningf(`
|
||||
-------------------------------------------------------------------------------
|
||||
NGINX master process died (%v): %v
|
||||
-------------------------------------------------------------------------------
|
||||
`, waitStatus.ExitStatus(), err)
|
||||
}
|
||||
cmd.Process.Release()
|
||||
cmd = exec.Command(n.binary, "-c", cfgPath)
|
||||
// we wait until the workers are killed
|
||||
for {
|
||||
conn, err := net.DialTimeout("tcp", "127.0.0.1:80", 1*time.Second)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
conn.Close()
|
||||
// kill nginx worker processes
|
||||
fs, err := proc.NewFS("/proc")
|
||||
procs, _ := fs.FS.AllProcs()
|
||||
for _, p := range procs {
|
||||
pn, err := p.Comm()
|
||||
if err != nil {
|
||||
glog.Errorf("unexpected error obtaining process information: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
if pn == "nginx" {
|
||||
osp, err := os.FindProcess(p.PID)
|
||||
if err != nil {
|
||||
glog.Errorf("unexpected error obtaining process information: %v", err)
|
||||
continue
|
||||
}
|
||||
osp.Signal(syscall.SIGQUIT)
|
||||
}
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
// restart a new nginx master process if the controller
|
||||
// is not being stopped
|
||||
n.start(cmd, done)
|
||||
}
|
||||
}
|
||||
|
||||
// Stop gracefully stops the NGINX master process.
|
||||
func (n *NGINXController) Stop() error {
|
||||
n.isShuttingDown = true
|
||||
n.controller.Stop()
|
||||
|
||||
// Send stop signal to Nginx
|
||||
glog.Info("stopping NGINX process...")
|
||||
cmd := exec.Command(n.binary, "-c", cfgPath, "-s", "quit")
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Wait for the Nginx process disappear
|
||||
waitForNginxShutdown()
|
||||
glog.Info("NGINX process has stopped")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *NGINXController) start(cmd *exec.Cmd, done chan error) {
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Start(); err != nil {
|
||||
glog.Fatalf("nginx error: %v", err)
|
||||
done <- err
|
||||
return
|
||||
}
|
||||
|
||||
n.cmdArgs = cmd.Args
|
||||
|
||||
go func() {
|
||||
done <- cmd.Wait()
|
||||
}()
|
||||
}
|
||||
|
||||
// BackendDefaults returns the nginx defaults
|
||||
func (n NGINXController) BackendDefaults() defaults.Backend {
|
||||
return n.backendDefaults
|
||||
}
|
||||
|
||||
// printDiff returns the difference between the running configuration
|
||||
// and the new one
|
||||
func (n NGINXController) printDiff(data []byte) {
|
||||
if !glog.V(2) {
|
||||
return
|
||||
}
|
||||
|
||||
in, err := os.Open(cfgPath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
src, err := ioutil.ReadAll(in)
|
||||
in.Close()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !bytes.Equal(src, data) {
|
||||
tmpfile, err := ioutil.TempFile("", "nginx-cfg-diff")
|
||||
if err != nil {
|
||||
glog.Errorf("error creating temporal file: %s", err)
|
||||
return
|
||||
}
|
||||
defer tmpfile.Close()
|
||||
err = ioutil.WriteFile(tmpfile.Name(), data, 0644)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
diffOutput, err := diff(src, data)
|
||||
if err != nil {
|
||||
glog.Errorf("error computing diff: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
glog.Infof("NGINX configuration diff\n")
|
||||
glog.Infof("%v", string(diffOutput))
|
||||
|
||||
os.Remove(tmpfile.Name())
|
||||
}
|
||||
}
|
||||
|
||||
// Info return build information
|
||||
func (n NGINXController) Info() *ingress.BackendInfo {
|
||||
return &ingress.BackendInfo{
|
||||
Name: "NGINX",
|
||||
Release: version.RELEASE,
|
||||
Build: version.COMMIT,
|
||||
Repository: version.REPO,
|
||||
}
|
||||
}
|
||||
|
||||
// DefaultEndpoint returns the default endpoint to be use as default server that returns 404.
|
||||
func (n NGINXController) DefaultEndpoint() ingress.Endpoint {
|
||||
return ingress.Endpoint{
|
||||
Address: "127.0.0.1",
|
||||
Port: fmt.Sprintf("%v", n.ports.Default),
|
||||
Target: &apiv1.ObjectReference{},
|
||||
}
|
||||
}
|
||||
|
||||
// ConfigureFlags allow to configure more flags before the parsing of
|
||||
// command line arguments
|
||||
func (n *NGINXController) ConfigureFlags(flags *pflag.FlagSet) {
|
||||
flags.BoolVar(&n.isSSLPassthroughEnabled, "enable-ssl-passthrough", false, `Enable SSL passthrough feature. Default is disabled`)
|
||||
flags.IntVar(&n.ports.HTTP, "http-port", 80, `Indicates the port to use for HTTP traffic`)
|
||||
flags.IntVar(&n.ports.HTTPS, "https-port", 443, `Indicates the port to use for HTTPS traffic`)
|
||||
flags.IntVar(&n.ports.Status, "status-port", 18080, `Indicates the TCP port to use for exposing the nginx status page`)
|
||||
flags.IntVar(&n.ports.SSLProxy, "ssl-passtrough-proxy-port", 442, `Default port to use internally for SSL when SSL Passthgough is enabled`)
|
||||
flags.IntVar(&n.ports.Default, "default-server-port", 8181, `Default port to use for exposing the default server (catch all)`)
|
||||
}
|
||||
|
||||
// OverrideFlags customize NGINX controller flags
|
||||
func (n *NGINXController) OverrideFlags(flags *pflag.FlagSet) {
|
||||
// we check port collisions
|
||||
if !isPortAvailable(n.ports.HTTP) {
|
||||
glog.Fatalf("Port %v is already in use. Please check the flag --http-port", n.ports.HTTP)
|
||||
}
|
||||
if !isPortAvailable(n.ports.HTTPS) {
|
||||
glog.Fatalf("Port %v is already in use. Please check the flag --https-port", n.ports.HTTPS)
|
||||
}
|
||||
if !isPortAvailable(n.ports.Status) {
|
||||
glog.Fatalf("Port %v is already in use. Please check the flag --status-port", n.ports.Status)
|
||||
}
|
||||
if !isPortAvailable(n.ports.Default) {
|
||||
glog.Fatalf("Port %v is already in use. Please check the flag --default-server-port", n.ports.Default)
|
||||
}
|
||||
|
||||
ic, _ := flags.GetString("ingress-class")
|
||||
wc, _ := flags.GetString("watch-namespace")
|
||||
|
||||
if ic == "" {
|
||||
ic = defIngressClass
|
||||
}
|
||||
|
||||
if ic != defIngressClass {
|
||||
glog.Warningf("only Ingress with class %v will be processed by this ingress controller", ic)
|
||||
}
|
||||
|
||||
flags.Set("ingress-class", ic)
|
||||
|
||||
h, _ := flags.GetInt("healthz-port")
|
||||
n.ports.Health = h
|
||||
|
||||
n.stats = newStatsCollector(wc, ic, n.binary, n.ports.Status)
|
||||
|
||||
if n.isSSLPassthroughEnabled {
|
||||
if !isPortAvailable(n.ports.SSLProxy) {
|
||||
glog.Fatalf("Port %v is already in use. Please check the flag --ssl-passtrough-proxy-port", n.ports.SSLProxy)
|
||||
}
|
||||
|
||||
glog.Info("starting TLS proxy for SSL passthrough")
|
||||
n.proxy = &proxy{
|
||||
Default: &server{
|
||||
Hostname: "localhost",
|
||||
IP: "127.0.0.1",
|
||||
Port: n.ports.SSLProxy,
|
||||
ProxyProtocol: true,
|
||||
},
|
||||
}
|
||||
|
||||
listener, err := net.Listen("tcp", fmt.Sprintf(":%v", n.ports.HTTPS))
|
||||
if err != nil {
|
||||
glog.Fatalf("%v", err)
|
||||
}
|
||||
|
||||
proxyList := &proxyproto.Listener{Listener: listener}
|
||||
|
||||
// start goroutine that accepts tcp connections in port 443
|
||||
go func() {
|
||||
for {
|
||||
var conn net.Conn
|
||||
var err error
|
||||
|
||||
if n.isProxyProtocolEnabled {
|
||||
// we need to wrap the listener in order to decode
|
||||
// proxy protocol before handling the connection
|
||||
conn, err = proxyList.Accept()
|
||||
} else {
|
||||
conn, err = listener.Accept()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
glog.Warningf("unexpected error accepting tcp connection: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
glog.V(3).Infof("remote address %s to local %s", conn.RemoteAddr(), conn.LocalAddr())
|
||||
go n.proxy.Handle(conn)
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
// DefaultIngressClass just return the default ingress class
|
||||
func (n NGINXController) DefaultIngressClass() string {
|
||||
return defIngressClass
|
||||
}
|
||||
|
||||
// testTemplate checks if the NGINX configuration inside the byte array is valid
|
||||
// running the command "nginx -t" using a temporal file.
|
||||
func (n NGINXController) testTemplate(cfg []byte) error {
|
||||
if len(cfg) == 0 {
|
||||
return fmt.Errorf("invalid nginx configuration (empty)")
|
||||
}
|
||||
tmpfile, err := ioutil.TempFile("", "nginx-cfg")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tmpfile.Close()
|
||||
err = ioutil.WriteFile(tmpfile.Name(), cfg, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
out, err := exec.Command(n.binary, "-t", "-c", tmpfile.Name()).CombinedOutput()
|
||||
if err != nil {
|
||||
// this error is different from the rest because it must be clear why nginx is not working
|
||||
oe := fmt.Sprintf(`
|
||||
-------------------------------------------------------------------------------
|
||||
Error: %v
|
||||
%v
|
||||
-------------------------------------------------------------------------------
|
||||
`, err, string(out))
|
||||
return errors.New(oe)
|
||||
}
|
||||
|
||||
os.Remove(tmpfile.Name())
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetConfig sets the configured configmap
|
||||
func (n *NGINXController) SetConfig(cmap *apiv1.ConfigMap) {
|
||||
n.configmap = cmap
|
||||
n.isProxyProtocolEnabled = false
|
||||
|
||||
m := map[string]string{}
|
||||
if cmap != nil {
|
||||
m = cmap.Data
|
||||
}
|
||||
|
||||
val, ok := m["use-proxy-protocol"]
|
||||
if ok {
|
||||
b, err := strconv.ParseBool(val)
|
||||
if err == nil {
|
||||
n.isProxyProtocolEnabled = b
|
||||
}
|
||||
}
|
||||
|
||||
n.backendDefaults = ngx_template.ReadConfig(m).Backend
|
||||
}
|
||||
|
||||
// SetListers sets the configured store listers in the generic ingress controller
|
||||
func (n *NGINXController) SetListers(lister *ingress.StoreLister) {
|
||||
n.storeLister = lister
|
||||
}
|
||||
|
||||
// UpdateIngressStatus custom Ingress status update
|
||||
func (n *NGINXController) UpdateIngressStatus(*extensions.Ingress) []apiv1.LoadBalancerIngress {
|
||||
return nil
|
||||
}
|
||||
|
||||
// OnUpdate is called by syncQueue in https://github.com/kubernetes/ingress/blob/master/core/pkg/ingress/controller/controller.go#L426
|
||||
// periodically to keep the configuration in sync.
|
||||
//
|
||||
// convert configmap to custom configuration object (different in each implementation)
|
||||
// write the custom template (the complexity depends on the implementation)
|
||||
// write the configuration file
|
||||
// returning nill implies the backend will be reloaded.
|
||||
// if an error is returned means requeue the update
|
||||
func (n *NGINXController) OnUpdate(ingressCfg ingress.Configuration) error {
|
||||
cfg := ngx_template.ReadConfig(n.configmap.Data)
|
||||
cfg.Resolver = n.resolver
|
||||
|
||||
servers := []*server{}
|
||||
for _, pb := range ingressCfg.PassthroughBackends {
|
||||
svc := pb.Service
|
||||
if svc == nil {
|
||||
glog.Warningf("missing service for PassthroughBackends %v", pb.Backend)
|
||||
continue
|
||||
}
|
||||
port, err := strconv.Atoi(pb.Port.String())
|
||||
if err != nil {
|
||||
for _, sp := range svc.Spec.Ports {
|
||||
if sp.Name == pb.Port.String() {
|
||||
port = int(sp.Port)
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for _, sp := range svc.Spec.Ports {
|
||||
if sp.Port == int32(port) {
|
||||
port = int(sp.Port)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//TODO: Allow PassthroughBackends to specify they support proxy-protocol
|
||||
servers = append(servers, &server{
|
||||
Hostname: pb.Hostname,
|
||||
IP: svc.Spec.ClusterIP,
|
||||
Port: port,
|
||||
ProxyProtocol: false,
|
||||
})
|
||||
}
|
||||
|
||||
if n.isSSLPassthroughEnabled {
|
||||
n.proxy.ServerList = servers
|
||||
}
|
||||
|
||||
// we need to check if the status module configuration changed
|
||||
if cfg.EnableVtsStatus {
|
||||
n.setupMonitor(vtsStatusModule)
|
||||
} else {
|
||||
n.setupMonitor(defaultStatusModule)
|
||||
}
|
||||
|
||||
// NGINX cannot resize the hash tables used to store server names.
|
||||
// For this reason we check if the defined size defined is correct
|
||||
// for the FQDN defined in the ingress rules adjusting the value
|
||||
// if is required.
|
||||
// https://trac.nginx.org/nginx/ticket/352
|
||||
// https://trac.nginx.org/nginx/ticket/631
|
||||
var longestName int
|
||||
var serverNameBytes int
|
||||
redirectServers := make(map[string]string)
|
||||
for _, srv := range ingressCfg.Servers {
|
||||
if longestName < len(srv.Hostname) {
|
||||
longestName = len(srv.Hostname)
|
||||
}
|
||||
serverNameBytes += len(srv.Hostname)
|
||||
if srv.RedirectFromToWWW {
|
||||
var n string
|
||||
if strings.HasPrefix(srv.Hostname, "www.") {
|
||||
n = strings.TrimLeft(srv.Hostname, "www.")
|
||||
} else {
|
||||
n = fmt.Sprintf("www.%v", srv.Hostname)
|
||||
}
|
||||
glog.V(3).Infof("creating redirect from %v to %v", srv.Hostname, n)
|
||||
if _, ok := redirectServers[n]; !ok {
|
||||
found := false
|
||||
for _, esrv := range ingressCfg.Servers {
|
||||
if esrv.Hostname == n {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
redirectServers[n] = srv.Hostname
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if cfg.ServerNameHashBucketSize == 0 {
|
||||
nameHashBucketSize := nginxHashBucketSize(longestName)
|
||||
glog.V(3).Infof("adjusting ServerNameHashBucketSize variable to %v", nameHashBucketSize)
|
||||
cfg.ServerNameHashBucketSize = nameHashBucketSize
|
||||
}
|
||||
serverNameHashMaxSize := nextPowerOf2(serverNameBytes)
|
||||
if cfg.ServerNameHashMaxSize < serverNameHashMaxSize {
|
||||
glog.V(3).Infof("adjusting ServerNameHashMaxSize variable to %v", serverNameHashMaxSize)
|
||||
cfg.ServerNameHashMaxSize = serverNameHashMaxSize
|
||||
}
|
||||
|
||||
// the limit of open files is per worker process
|
||||
// and we leave some room to avoid consuming all the FDs available
|
||||
wp, err := strconv.Atoi(cfg.WorkerProcesses)
|
||||
glog.V(3).Infof("number of worker processes: %v", wp)
|
||||
if err != nil {
|
||||
wp = 1
|
||||
}
|
||||
maxOpenFiles := (sysctlFSFileMax() / wp) - 1024
|
||||
glog.V(3).Infof("maximum number of open file descriptors : %v", sysctlFSFileMax())
|
||||
if maxOpenFiles < 1024 {
|
||||
// this means the value of RLIMIT_NOFILE is too low.
|
||||
maxOpenFiles = 1024
|
||||
}
|
||||
|
||||
setHeaders := map[string]string{}
|
||||
if cfg.ProxySetHeaders != "" {
|
||||
cmap, exists, err := n.storeLister.ConfigMap.GetByKey(cfg.ProxySetHeaders)
|
||||
if err != nil {
|
||||
glog.Warningf("unexpected error reading configmap %v: %v", cfg.ProxySetHeaders, err)
|
||||
}
|
||||
|
||||
if exists {
|
||||
setHeaders = cmap.(*apiv1.ConfigMap).Data
|
||||
}
|
||||
}
|
||||
|
||||
addHeaders := map[string]string{}
|
||||
if cfg.AddHeaders != "" {
|
||||
cmap, exists, err := n.storeLister.ConfigMap.GetByKey(cfg.AddHeaders)
|
||||
if err != nil {
|
||||
glog.Warningf("unexpected error reading configmap %v: %v", cfg.AddHeaders, err)
|
||||
}
|
||||
|
||||
if exists {
|
||||
addHeaders = cmap.(*apiv1.ConfigMap).Data
|
||||
}
|
||||
}
|
||||
|
||||
sslDHParam := ""
|
||||
if cfg.SSLDHParam != "" {
|
||||
secretName := cfg.SSLDHParam
|
||||
s, exists, err := n.storeLister.Secret.GetByKey(secretName)
|
||||
if err != nil {
|
||||
glog.Warningf("unexpected error reading secret %v: %v", secretName, err)
|
||||
}
|
||||
|
||||
if exists {
|
||||
secret := s.(*apiv1.Secret)
|
||||
nsSecName := strings.Replace(secretName, "/", "-", -1)
|
||||
|
||||
dh, ok := secret.Data["dhparam.pem"]
|
||||
if ok {
|
||||
pemFileName, err := ssl.AddOrUpdateDHParam(nsSecName, dh)
|
||||
if err != nil {
|
||||
glog.Warningf("unexpected error adding or updating dhparam %v file: %v", nsSecName, err)
|
||||
} else {
|
||||
sslDHParam = pemFileName
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cfg.SSLDHParam = sslDHParam
|
||||
|
||||
tc := config.TemplateConfig{
|
||||
ProxySetHeaders: setHeaders,
|
||||
AddHeaders: addHeaders,
|
||||
MaxOpenFiles: maxOpenFiles,
|
||||
BacklogSize: sysctlSomaxconn(),
|
||||
Backends: ingressCfg.Backends,
|
||||
PassthroughBackends: ingressCfg.PassthroughBackends,
|
||||
Servers: ingressCfg.Servers,
|
||||
TCPBackends: ingressCfg.TCPEndpoints,
|
||||
UDPBackends: ingressCfg.UDPEndpoints,
|
||||
HealthzURI: ngxHealthPath,
|
||||
CustomErrors: len(cfg.CustomHTTPErrors) > 0,
|
||||
Cfg: cfg,
|
||||
IsIPV6Enabled: n.isIPV6Enabled && !cfg.DisableIpv6,
|
||||
RedirectServers: redirectServers,
|
||||
IsSSLPassthroughEnabled: n.isSSLPassthroughEnabled,
|
||||
ListenPorts: n.ports,
|
||||
PublishService: n.controller.GetPublishService(),
|
||||
}
|
||||
|
||||
content, err := n.t.Write(tc)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = n.testTemplate(content)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
n.printDiff(content)
|
||||
|
||||
err = ioutil.WriteFile(cfgPath, content, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
o, err := exec.Command(n.binary, "-s", "reload", "-c", cfgPath).CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("%v\n%v", err, string(o))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// nginxHashBucketSize computes the correct nginx hash_bucket_size for a hash with the given longest key
|
||||
func nginxHashBucketSize(longestString int) int {
|
||||
// See https://github.com/kubernetes/ingress/issues/623 for an explanation
|
||||
wordSize := 8 // Assume 64 bit CPU
|
||||
n := longestString + 2
|
||||
aligned := (n + wordSize - 1) & ^(wordSize - 1)
|
||||
rawSize := wordSize + wordSize + aligned
|
||||
return nextPowerOf2(rawSize)
|
||||
}
|
||||
|
||||
// Name returns the healthcheck name
|
||||
func (n NGINXController) Name() string {
|
||||
return "Ingress Controller"
|
||||
}
|
||||
|
||||
// Check returns if the nginx healthz endpoint is returning ok (status code 200)
|
||||
func (n NGINXController) Check(_ *http.Request) error {
|
||||
res, err := http.Get(fmt.Sprintf("http://localhost:%v%v", n.ports.Status, ngxHealthPath))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != 200 {
|
||||
return fmt.Errorf("ingress controller is not healthy")
|
||||
}
|
||||
|
||||
// check the nginx master process is running
|
||||
fs, err := proc.NewFS("/proc")
|
||||
if err != nil {
|
||||
glog.Errorf("%v", err)
|
||||
return err
|
||||
}
|
||||
f, err := ioutil.ReadFile("/run/nginx.pid")
|
||||
if err != nil {
|
||||
glog.Errorf("%v", err)
|
||||
return err
|
||||
}
|
||||
pid, err := strconv.Atoi(strings.TrimRight(string(f), "\r\n"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = fs.NewProc(int(pid))
|
||||
if err != nil {
|
||||
glog.Errorf("%v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// http://graphics.stanford.edu/~seander/bithacks.html#RoundUpPowerOf2
|
||||
// https://play.golang.org/p/TVSyCcdxUh
|
||||
func nextPowerOf2(v int) int {
|
||||
v--
|
||||
v |= v >> 1
|
||||
v |= v >> 2
|
||||
v |= v >> 4
|
||||
v |= v >> 8
|
||||
v |= v >> 16
|
||||
v++
|
||||
|
||||
return v
|
||||
}
|
||||
|
||||
func isIPv6Enabled() bool {
|
||||
cmd := exec.Command("test", "-f", "/proc/net/if_inet6")
|
||||
return cmd.Run() == nil
|
||||
}
|
||||
|
||||
// isNginxRunning returns true if a process with the name 'nginx' is found
|
||||
func isNginxProcessPresent() bool {
|
||||
processes, _ := ps.Processes()
|
||||
for _, p := range processes {
|
||||
if p.Executable() == "nginx" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func waitForNginxShutdown() {
|
||||
timer := time.NewTicker(time.Second * 1)
|
||||
defer timer.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-timer.C:
|
||||
if !isNginxProcessPresent() {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
59
pkg/nginx/controller/nginx_test.go
Normal file
59
pkg/nginx/controller/nginx_test.go
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
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 controller
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestNginxHashBucketSize(t *testing.T) {
|
||||
tests := []struct {
|
||||
n int
|
||||
expected int
|
||||
}{
|
||||
{0, 32},
|
||||
{1, 32},
|
||||
{2, 32},
|
||||
{3, 32},
|
||||
// ...
|
||||
{13, 32},
|
||||
{14, 32},
|
||||
{15, 64},
|
||||
{16, 64},
|
||||
// ...
|
||||
{45, 64},
|
||||
{46, 64},
|
||||
{47, 128},
|
||||
{48, 128},
|
||||
// ...
|
||||
// ...
|
||||
{109, 128},
|
||||
{110, 128},
|
||||
{111, 256},
|
||||
{112, 256},
|
||||
// ...
|
||||
{237, 256},
|
||||
{238, 256},
|
||||
{239, 512},
|
||||
{240, 512},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
actual := nginxHashBucketSize(test.n)
|
||||
if actual != test.expected {
|
||||
t.Errorf("Test nginxHashBucketSize(%d): expected %d but returned %d", test.n, test.expected, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
110
pkg/nginx/controller/tcp.go
Normal file
110
pkg/nginx/controller/tcp.go
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
|
||||
"github.com/golang/glog"
|
||||
|
||||
"github.com/paultag/sniff/parser"
|
||||
)
|
||||
|
||||
type server struct {
|
||||
Hostname string
|
||||
IP string
|
||||
Port int
|
||||
ProxyProtocol bool
|
||||
}
|
||||
|
||||
type proxy struct {
|
||||
ServerList []*server
|
||||
Default *server
|
||||
}
|
||||
|
||||
func (p *proxy) Get(host string) *server {
|
||||
if p.ServerList == nil {
|
||||
return p.Default
|
||||
}
|
||||
|
||||
for _, s := range p.ServerList {
|
||||
if s.Hostname == host {
|
||||
return s
|
||||
}
|
||||
}
|
||||
|
||||
return p.Default
|
||||
}
|
||||
|
||||
func (p *proxy) Handle(conn net.Conn) {
|
||||
defer conn.Close()
|
||||
data := make([]byte, 4096)
|
||||
|
||||
length, err := conn.Read(data)
|
||||
if err != nil {
|
||||
glog.V(4).Infof("error reading the first 4k of the connection: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
proxy := p.Default
|
||||
hostname, err := parser.GetHostname(data[:])
|
||||
if err == nil {
|
||||
glog.V(4).Infof("parsed hostname from TLS Client Hello: %s", hostname)
|
||||
proxy = p.Get(hostname)
|
||||
}
|
||||
|
||||
if proxy == nil {
|
||||
glog.V(4).Infof("there is no configured proxy for SSL connections")
|
||||
return
|
||||
}
|
||||
|
||||
clientConn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", proxy.IP, proxy.Port))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer clientConn.Close()
|
||||
|
||||
if proxy.ProxyProtocol {
|
||||
//Write out the proxy-protocol header
|
||||
localAddr := conn.LocalAddr().(*net.TCPAddr)
|
||||
remoteAddr := conn.RemoteAddr().(*net.TCPAddr)
|
||||
protocol := "UNKNOWN"
|
||||
if remoteAddr.IP.To4() != nil {
|
||||
protocol = "TCP4"
|
||||
} else if remoteAddr.IP.To16() != nil {
|
||||
protocol = "TCP6"
|
||||
}
|
||||
proxyProtocolHeader := fmt.Sprintf("PROXY %s %s %s %d %d\r\n", protocol, remoteAddr.IP.String(), localAddr.IP.String(), remoteAddr.Port, localAddr.Port)
|
||||
glog.V(4).Infof("Writing proxy protocol header - %s", proxyProtocolHeader)
|
||||
_, err = fmt.Fprintf(clientConn, proxyProtocolHeader)
|
||||
}
|
||||
if err != nil {
|
||||
glog.Errorf("unexpected error writing proxy-protocol header: %s", err)
|
||||
clientConn.Close()
|
||||
} else {
|
||||
_, err = clientConn.Write(data[:length])
|
||||
if err != nil {
|
||||
glog.Errorf("unexpected error writing first 4k of proxy data: %s", err)
|
||||
clientConn.Close()
|
||||
}
|
||||
}
|
||||
|
||||
pipe(clientConn, conn)
|
||||
}
|
||||
|
||||
func pipe(client, server net.Conn) {
|
||||
doCopy := func(s, c net.Conn, cancel chan<- bool) {
|
||||
io.Copy(s, c)
|
||||
cancel <- true
|
||||
}
|
||||
|
||||
cancel := make(chan bool, 2)
|
||||
|
||||
go doCopy(server, client, cancel)
|
||||
go doCopy(client, server, cancel)
|
||||
|
||||
select {
|
||||
case <-cancel:
|
||||
return
|
||||
}
|
||||
}
|
||||
87
pkg/nginx/controller/utils.go
Normal file
87
pkg/nginx/controller/utils.go
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
Copyright 2015 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 controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"syscall"
|
||||
|
||||
"github.com/golang/glog"
|
||||
|
||||
"k8s.io/kubernetes/pkg/util/sysctl"
|
||||
)
|
||||
|
||||
// sysctlSomaxconn returns the value of net.core.somaxconn, i.e.
|
||||
// maximum number of connections that can be queued for acceptance
|
||||
// http://nginx.org/en/docs/http/ngx_http_core_module.html#listen
|
||||
func sysctlSomaxconn() int {
|
||||
maxConns, err := sysctl.New().GetSysctl("net/core/somaxconn")
|
||||
if err != nil || maxConns < 512 {
|
||||
glog.V(3).Infof("system net.core.somaxconn=%v (using system default)", maxConns)
|
||||
return 511
|
||||
}
|
||||
|
||||
return maxConns
|
||||
}
|
||||
|
||||
// sysctlFSFileMax returns the value of fs.file-max, i.e.
|
||||
// maximum number of open file descriptors
|
||||
func sysctlFSFileMax() int {
|
||||
var rLimit syscall.Rlimit
|
||||
err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rLimit)
|
||||
if err != nil {
|
||||
glog.Errorf("unexpected error reading system maximum number of open file descriptors (RLIMIT_NOFILE): %v", err)
|
||||
// returning 0 means don't render the value
|
||||
return 0
|
||||
}
|
||||
return int(rLimit.Max)
|
||||
}
|
||||
|
||||
func diff(b1, b2 []byte) ([]byte, error) {
|
||||
f1, err := ioutil.TempFile("", "a")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f1.Close()
|
||||
defer os.Remove(f1.Name())
|
||||
|
||||
f2, err := ioutil.TempFile("", "b")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f2.Close()
|
||||
defer os.Remove(f2.Name())
|
||||
|
||||
f1.Write(b1)
|
||||
f2.Write(b2)
|
||||
|
||||
out, _ := exec.Command("diff", "-u", f1.Name(), f2.Name()).CombinedOutput()
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func isPortAvailable(p int) bool {
|
||||
ln, err := net.Listen("tcp", fmt.Sprintf(":%v", p))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
ln.Close()
|
||||
return true
|
||||
}
|
||||
41
pkg/nginx/controller/utils_test.go
Normal file
41
pkg/nginx/controller/utils_test.go
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
Copyright 2015 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 controller
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestDiff(t *testing.T) {
|
||||
tests := []struct {
|
||||
a []byte
|
||||
b []byte
|
||||
empty bool
|
||||
}{
|
||||
{[]byte(""), []byte(""), true},
|
||||
{[]byte("a"), []byte("a"), true},
|
||||
{[]byte("a"), []byte("b"), false},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
b, err := diff(test.a, test.b)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error returned: %v", err)
|
||||
}
|
||||
if len(b) == 0 && !test.empty {
|
||||
t.Fatalf("expected empty but returned %s", b)
|
||||
}
|
||||
}
|
||||
}
|
||||
151
pkg/nginx/metric/collector/nginx.go
Normal file
151
pkg/nginx/metric/collector/nginx.go
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
/*
|
||||
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 collector
|
||||
|
||||
import (
|
||||
"github.com/golang/glog"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
type (
|
||||
nginxStatusCollector struct {
|
||||
scrapeChan chan scrapeRequest
|
||||
ngxHealthPort int
|
||||
ngxVtsPath string
|
||||
data *nginxStatusData
|
||||
watchNamespace string
|
||||
ingressClass string
|
||||
}
|
||||
|
||||
nginxStatusData struct {
|
||||
active *prometheus.Desc
|
||||
accepted *prometheus.Desc
|
||||
handled *prometheus.Desc
|
||||
requests *prometheus.Desc
|
||||
reading *prometheus.Desc
|
||||
writing *prometheus.Desc
|
||||
waiting *prometheus.Desc
|
||||
}
|
||||
)
|
||||
|
||||
// NewNginxStatus returns a new prometheus collector the default nginx status module
|
||||
func NewNginxStatus(watchNamespace, ingressClass string, ngxHealthPort int, ngxVtsPath string) Stopable {
|
||||
|
||||
p := nginxStatusCollector{
|
||||
scrapeChan: make(chan scrapeRequest),
|
||||
ngxHealthPort: ngxHealthPort,
|
||||
ngxVtsPath: ngxVtsPath,
|
||||
watchNamespace: watchNamespace,
|
||||
ingressClass: ingressClass,
|
||||
}
|
||||
|
||||
p.data = &nginxStatusData{
|
||||
active: prometheus.NewDesc(
|
||||
prometheus.BuildFQName(ns, "", "active_connections"),
|
||||
"total number of active connections",
|
||||
[]string{"ingress_class", "namespace"}, nil),
|
||||
|
||||
accepted: prometheus.NewDesc(
|
||||
prometheus.BuildFQName(ns, "", "accepted_connections"),
|
||||
"total number of accepted client connections",
|
||||
[]string{"ingress_class", "namespace"}, nil),
|
||||
|
||||
handled: prometheus.NewDesc(
|
||||
prometheus.BuildFQName(ns, "", "handled_connections"),
|
||||
"total number of handled connections",
|
||||
[]string{"ingress_class", "namespace"}, nil),
|
||||
|
||||
requests: prometheus.NewDesc(
|
||||
prometheus.BuildFQName(ns, "", "total_requests"),
|
||||
"total number of client requests",
|
||||
[]string{"ingress_class", "namespace"}, nil),
|
||||
|
||||
reading: prometheus.NewDesc(
|
||||
prometheus.BuildFQName(ns, "", "current_reading_connections"),
|
||||
"current number of connections where nginx is reading the request header",
|
||||
[]string{"ingress_class", "namespace"}, nil),
|
||||
|
||||
writing: prometheus.NewDesc(
|
||||
prometheus.BuildFQName(ns, "", "current_writing_connections"),
|
||||
"current number of connections where nginx is writing the response back to the client",
|
||||
[]string{"ingress_class", "namespace"}, nil),
|
||||
|
||||
waiting: prometheus.NewDesc(
|
||||
prometheus.BuildFQName(ns, "", "current_waiting_connections"),
|
||||
"current number of idle client connections waiting for a request",
|
||||
[]string{"ingress_class", "namespace"}, nil),
|
||||
}
|
||||
|
||||
go p.start()
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
// Describe implements prometheus.Collector.
|
||||
func (p nginxStatusCollector) Describe(ch chan<- *prometheus.Desc) {
|
||||
ch <- p.data.active
|
||||
ch <- p.data.accepted
|
||||
ch <- p.data.handled
|
||||
ch <- p.data.requests
|
||||
ch <- p.data.reading
|
||||
ch <- p.data.writing
|
||||
ch <- p.data.waiting
|
||||
}
|
||||
|
||||
// Collect implements prometheus.Collector.
|
||||
func (p nginxStatusCollector) Collect(ch chan<- prometheus.Metric) {
|
||||
req := scrapeRequest{results: ch, done: make(chan struct{})}
|
||||
p.scrapeChan <- req
|
||||
<-req.done
|
||||
}
|
||||
|
||||
func (p nginxStatusCollector) start() {
|
||||
for req := range p.scrapeChan {
|
||||
ch := req.results
|
||||
p.scrape(ch)
|
||||
req.done <- struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
func (p nginxStatusCollector) Stop() {
|
||||
close(p.scrapeChan)
|
||||
}
|
||||
|
||||
// nginxStatusCollector scrape the nginx status
|
||||
func (p nginxStatusCollector) scrape(ch chan<- prometheus.Metric) {
|
||||
s, err := getNginxStatus(p.ngxHealthPort, p.ngxVtsPath)
|
||||
if err != nil {
|
||||
glog.Warningf("unexpected error obtaining nginx status info: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(p.data.active,
|
||||
prometheus.GaugeValue, float64(s.Active), p.ingressClass, p.watchNamespace)
|
||||
ch <- prometheus.MustNewConstMetric(p.data.accepted,
|
||||
prometheus.GaugeValue, float64(s.Accepted), p.ingressClass, p.watchNamespace)
|
||||
ch <- prometheus.MustNewConstMetric(p.data.handled,
|
||||
prometheus.GaugeValue, float64(s.Handled), p.ingressClass, p.watchNamespace)
|
||||
ch <- prometheus.MustNewConstMetric(p.data.requests,
|
||||
prometheus.GaugeValue, float64(s.Requests), p.ingressClass, p.watchNamespace)
|
||||
ch <- prometheus.MustNewConstMetric(p.data.reading,
|
||||
prometheus.GaugeValue, float64(s.Reading), p.ingressClass, p.watchNamespace)
|
||||
ch <- prometheus.MustNewConstMetric(p.data.writing,
|
||||
prometheus.GaugeValue, float64(s.Writing), p.ingressClass, p.watchNamespace)
|
||||
ch <- prometheus.MustNewConstMetric(p.data.waiting,
|
||||
prometheus.GaugeValue, float64(s.Waiting), p.ingressClass, p.watchNamespace)
|
||||
}
|
||||
174
pkg/nginx/metric/collector/process.go
Normal file
174
pkg/nginx/metric/collector/process.go
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
/*
|
||||
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 collector
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/golang/glog"
|
||||
|
||||
common "github.com/ncabatoff/process-exporter"
|
||||
"github.com/ncabatoff/process-exporter/proc"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
// BinaryNameMatcher ...
|
||||
type BinaryNameMatcher struct {
|
||||
Name string
|
||||
Binary string
|
||||
}
|
||||
|
||||
// MatchAndName returns false if the match failed, otherwise
|
||||
// true and the resulting name.
|
||||
func (em BinaryNameMatcher) MatchAndName(nacl common.NameAndCmdline) (bool, string) {
|
||||
if len(nacl.Cmdline) == 0 {
|
||||
return false, ""
|
||||
}
|
||||
cmd := filepath.Base(em.Binary)
|
||||
return em.Name == cmd, ""
|
||||
}
|
||||
|
||||
type namedProcessData struct {
|
||||
numProcs *prometheus.Desc
|
||||
cpuSecs *prometheus.Desc
|
||||
readBytes *prometheus.Desc
|
||||
writeBytes *prometheus.Desc
|
||||
memResidentbytes *prometheus.Desc
|
||||
memVirtualbytes *prometheus.Desc
|
||||
startTime *prometheus.Desc
|
||||
}
|
||||
|
||||
type namedProcess struct {
|
||||
*proc.Grouper
|
||||
|
||||
scrapeChan chan scrapeRequest
|
||||
fs *proc.FS
|
||||
data namedProcessData
|
||||
}
|
||||
|
||||
// NewNamedProcess returns a new prometheus collector for the nginx process
|
||||
func NewNamedProcess(children bool, mn common.MatchNamer) (prometheus.Collector, error) {
|
||||
fs, err := proc.NewFS("/proc")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p := namedProcess{
|
||||
scrapeChan: make(chan scrapeRequest),
|
||||
Grouper: proc.NewGrouper(children, mn),
|
||||
fs: fs,
|
||||
}
|
||||
_, err = p.Update(p.fs.AllProcs())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
p.data = namedProcessData{
|
||||
numProcs: prometheus.NewDesc(
|
||||
"num_procs",
|
||||
"number of processes",
|
||||
nil, nil),
|
||||
|
||||
cpuSecs: prometheus.NewDesc(
|
||||
"cpu_seconds_total",
|
||||
"Cpu usage in seconds",
|
||||
nil, nil),
|
||||
|
||||
readBytes: prometheus.NewDesc(
|
||||
"read_bytes_total",
|
||||
"number of bytes read",
|
||||
nil, nil),
|
||||
|
||||
writeBytes: prometheus.NewDesc(
|
||||
"write_bytes_total",
|
||||
"number of bytes written",
|
||||
nil, nil),
|
||||
|
||||
memResidentbytes: prometheus.NewDesc(
|
||||
"resident_memory_bytes",
|
||||
"number of bytes of memory in use",
|
||||
nil, nil),
|
||||
|
||||
memVirtualbytes: prometheus.NewDesc(
|
||||
"virtual_memory_bytes",
|
||||
"number of bytes of memory in use",
|
||||
nil, nil),
|
||||
|
||||
startTime: prometheus.NewDesc(
|
||||
"oldest_start_time_seconds",
|
||||
"start time in seconds since 1970/01/01",
|
||||
nil, nil),
|
||||
}
|
||||
|
||||
go p.start()
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// Describe implements prometheus.Collector.
|
||||
func (p namedProcess) Describe(ch chan<- *prometheus.Desc) {
|
||||
ch <- p.data.cpuSecs
|
||||
ch <- p.data.numProcs
|
||||
ch <- p.data.readBytes
|
||||
ch <- p.data.writeBytes
|
||||
ch <- p.data.memResidentbytes
|
||||
ch <- p.data.memVirtualbytes
|
||||
ch <- p.data.startTime
|
||||
}
|
||||
|
||||
// Collect implements prometheus.Collector.
|
||||
func (p namedProcess) Collect(ch chan<- prometheus.Metric) {
|
||||
req := scrapeRequest{results: ch, done: make(chan struct{})}
|
||||
p.scrapeChan <- req
|
||||
<-req.done
|
||||
}
|
||||
|
||||
func (p namedProcess) start() {
|
||||
for req := range p.scrapeChan {
|
||||
ch := req.results
|
||||
p.scrape(ch)
|
||||
req.done <- struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
func (p namedProcess) Stop() {
|
||||
close(p.scrapeChan)
|
||||
}
|
||||
|
||||
func (p namedProcess) scrape(ch chan<- prometheus.Metric) {
|
||||
_, err := p.Update(p.fs.AllProcs())
|
||||
if err != nil {
|
||||
glog.Warningf("unexpected error obtaining nginx process info: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, gcounts := range p.Groups() {
|
||||
ch <- prometheus.MustNewConstMetric(p.data.numProcs,
|
||||
prometheus.GaugeValue, float64(gcounts.Procs))
|
||||
ch <- prometheus.MustNewConstMetric(p.data.memResidentbytes,
|
||||
prometheus.GaugeValue, float64(gcounts.Memresident))
|
||||
ch <- prometheus.MustNewConstMetric(p.data.memVirtualbytes,
|
||||
prometheus.GaugeValue, float64(gcounts.Memvirtual))
|
||||
ch <- prometheus.MustNewConstMetric(p.data.startTime,
|
||||
prometheus.GaugeValue, float64(gcounts.OldestStartTime.Unix()))
|
||||
ch <- prometheus.MustNewConstMetric(p.data.cpuSecs,
|
||||
prometheus.CounterValue, gcounts.Cpu)
|
||||
ch <- prometheus.MustNewConstMetric(p.data.readBytes,
|
||||
prometheus.CounterValue, float64(gcounts.ReadBytes))
|
||||
ch <- prometheus.MustNewConstMetric(p.data.writeBytes,
|
||||
prometheus.CounterValue, float64(gcounts.WriteBytes))
|
||||
}
|
||||
}
|
||||
30
pkg/nginx/metric/collector/scrape.go
Normal file
30
pkg/nginx/metric/collector/scrape.go
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
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 collector
|
||||
|
||||
import "github.com/prometheus/client_golang/prometheus"
|
||||
|
||||
// Stopable defines a prometheus collector that can be stopped
|
||||
type Stopable interface {
|
||||
prometheus.Collector
|
||||
Stop()
|
||||
}
|
||||
|
||||
type scrapeRequest struct {
|
||||
results chan<- prometheus.Metric
|
||||
done chan struct{}
|
||||
}
|
||||
225
pkg/nginx/metric/collector/status.go
Normal file
225
pkg/nginx/metric/collector/status.go
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
/*
|
||||
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 collector
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
|
||||
"github.com/golang/glog"
|
||||
)
|
||||
|
||||
var (
|
||||
ac = regexp.MustCompile(`Active connections: (\d+)`)
|
||||
sahr = regexp.MustCompile(`(\d+)\s(\d+)\s(\d+)`)
|
||||
reading = regexp.MustCompile(`Reading: (\d+)`)
|
||||
writing = regexp.MustCompile(`Writing: (\d+)`)
|
||||
waiting = regexp.MustCompile(`Waiting: (\d+)`)
|
||||
)
|
||||
|
||||
type basicStatus struct {
|
||||
// Active total number of active connections
|
||||
Active int
|
||||
// Accepted total number of accepted client connections
|
||||
Accepted int
|
||||
// Handled total number of handled connections. Generally, the parameter value is the same as accepts unless some resource limits have been reached (for example, the worker_connections limit).
|
||||
Handled int
|
||||
// Requests total number of client requests.
|
||||
Requests int
|
||||
// Reading current number of connections where nginx is reading the request header.
|
||||
Reading int
|
||||
// Writing current number of connections where nginx is writing the response back to the client.
|
||||
Writing int
|
||||
// Waiting current number of idle client connections waiting for a request.
|
||||
Waiting int
|
||||
}
|
||||
|
||||
// https://github.com/vozlt/nginx-module-vts
|
||||
type vts struct {
|
||||
NginxVersion string `json:"nginxVersion"`
|
||||
LoadMsec int `json:"loadMsec"`
|
||||
NowMsec int `json:"nowMsec"`
|
||||
// Total connections and requests(same as stub_status_module in NGINX)
|
||||
Connections connections `json:"connections"`
|
||||
// Traffic(in/out) and request and response counts and cache hit ratio per each server zone
|
||||
ServerZones map[string]serverZone `json:"serverZones"`
|
||||
// Traffic(in/out) and request and response counts and cache hit ratio per each server zone filtered through
|
||||
// the vhost_traffic_status_filter_by_set_key directive
|
||||
FilterZones map[string]map[string]filterZone `json:"filterZones"`
|
||||
// Traffic(in/out) and request and response counts per server in each upstream group
|
||||
UpstreamZones map[string][]upstreamZone `json:"upstreamZones"`
|
||||
}
|
||||
|
||||
type serverZone struct {
|
||||
RequestCounter float64 `json:"requestCounter"`
|
||||
InBytes float64 `json:"inBytes"`
|
||||
OutBytes float64 `json:"outBytes"`
|
||||
Responses response `json:"responses"`
|
||||
Cache cache `json:"cache"`
|
||||
}
|
||||
|
||||
type filterZone struct {
|
||||
RequestCounter float64 `json:"requestCounter"`
|
||||
InBytes float64 `json:"inBytes"`
|
||||
OutBytes float64 `json:"outBytes"`
|
||||
Cache cache `json:"cache"`
|
||||
Responses response `json:"responses"`
|
||||
}
|
||||
|
||||
type upstreamZone struct {
|
||||
Responses response `json:"responses"`
|
||||
Server string `json:"server"`
|
||||
RequestCounter float64 `json:"requestCounter"`
|
||||
InBytes float64 `json:"inBytes"`
|
||||
OutBytes float64 `json:"outBytes"`
|
||||
ResponseMsec float64 `json:"responseMsec"`
|
||||
Weight float64 `json:"weight"`
|
||||
MaxFails float64 `json:"maxFails"`
|
||||
FailTimeout float64 `json:"failTimeout"`
|
||||
Backup BoolToFloat64 `json:"backup"`
|
||||
Down BoolToFloat64 `json:"down"`
|
||||
}
|
||||
|
||||
type cache struct {
|
||||
Miss float64 `json:"miss"`
|
||||
Bypass float64 `json:"bypass"`
|
||||
Expired float64 `json:"expired"`
|
||||
Stale float64 `json:"stale"`
|
||||
Updating float64 `json:"updating"`
|
||||
Revalidated float64 `json:"revalidated"`
|
||||
Hit float64 `json:"hit"`
|
||||
Scarce float64 `json:"scarce"`
|
||||
}
|
||||
|
||||
type response struct {
|
||||
OneXx float64 `json:"1xx"`
|
||||
TwoXx float64 `json:"2xx"`
|
||||
TheeXx float64 `json:"3xx"`
|
||||
FourXx float64 `json:"4xx"`
|
||||
FiveXx float64 `json:"5xx"`
|
||||
}
|
||||
|
||||
type connections struct {
|
||||
Active float64 `json:"active"`
|
||||
Reading float64 `json:"reading"`
|
||||
Writing float64 `json:"writing"`
|
||||
Waiting float64 `json:"waiting"`
|
||||
Accepted float64 `json:"accepted"`
|
||||
Handled float64 `json:"handled"`
|
||||
Requests float64 `json:"requests"`
|
||||
}
|
||||
|
||||
// BoolToFloat64 ...
|
||||
type BoolToFloat64 float64
|
||||
|
||||
// UnmarshalJSON ...
|
||||
func (bit BoolToFloat64) UnmarshalJSON(data []byte) error {
|
||||
asString := string(data)
|
||||
if asString == "1" || asString == "true" {
|
||||
bit = 1
|
||||
} else if asString == "0" || asString == "false" {
|
||||
bit = 0
|
||||
} else {
|
||||
return fmt.Errorf(fmt.Sprintf("boolean unmarshal error: invalid input %s", asString))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getNginxStatus(port int, path string) (*basicStatus, error) {
|
||||
url := fmt.Sprintf("http://localhost:%v%v", port, path)
|
||||
glog.V(3).Infof("start scraping url: %v", url)
|
||||
|
||||
data, err := httpBody(url)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unexpected error scraping nginx status page: %v", err)
|
||||
}
|
||||
|
||||
return parse(string(data)), nil
|
||||
}
|
||||
|
||||
func httpBody(url string) ([]byte, error) {
|
||||
resp, err := http.DefaultClient.Get(url)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unexpected error scraping nginx : %v", err)
|
||||
}
|
||||
|
||||
data, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unexpected error scraping nginx (%v)", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 400 {
|
||||
return nil, fmt.Errorf("unexpected error scraping nginx (status %v)", resp.StatusCode)
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func getNginxVtsMetrics(port int, path string) (*vts, error) {
|
||||
url := fmt.Sprintf("http://localhost:%v%v", port, path)
|
||||
glog.V(3).Infof("start scraping url: %v", url)
|
||||
|
||||
data, err := httpBody(url)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unexpected error scraping nginx vts (%v)", err)
|
||||
}
|
||||
|
||||
var vts *vts
|
||||
err = json.Unmarshal(data, &vts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unexpected error json unmarshal (%v)", err)
|
||||
}
|
||||
glog.V(3).Infof("scrape returned : %v", vts)
|
||||
return vts, nil
|
||||
}
|
||||
|
||||
func parse(data string) *basicStatus {
|
||||
acr := ac.FindStringSubmatch(data)
|
||||
sahrr := sahr.FindStringSubmatch(data)
|
||||
readingr := reading.FindStringSubmatch(data)
|
||||
writingr := writing.FindStringSubmatch(data)
|
||||
waitingr := waiting.FindStringSubmatch(data)
|
||||
|
||||
return &basicStatus{
|
||||
toInt(acr, 1),
|
||||
toInt(sahrr, 1),
|
||||
toInt(sahrr, 2),
|
||||
toInt(sahrr, 3),
|
||||
toInt(readingr, 1),
|
||||
toInt(writingr, 1),
|
||||
toInt(waitingr, 1),
|
||||
}
|
||||
}
|
||||
|
||||
func toInt(data []string, pos int) int {
|
||||
if len(data) == 0 {
|
||||
return 0
|
||||
}
|
||||
if pos > len(data) {
|
||||
return 0
|
||||
}
|
||||
if v, err := strconv.Atoi(data[pos]); err == nil {
|
||||
return v
|
||||
}
|
||||
return 0
|
||||
}
|
||||
72
pkg/nginx/metric/collector/status_test.go
Normal file
72
pkg/nginx/metric/collector/status_test.go
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
Copyright 2015 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 collector
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/kylelemons/godebug/pretty"
|
||||
)
|
||||
|
||||
func TestParseStatus(t *testing.T) {
|
||||
tests := []struct {
|
||||
in string
|
||||
out *basicStatus
|
||||
}{
|
||||
{`Active connections: 43
|
||||
server accepts handled requests
|
||||
7368 7368 10993
|
||||
Reading: 0 Writing: 5 Waiting: 38`,
|
||||
&basicStatus{43, 7368, 7368, 10993, 0, 5, 38},
|
||||
},
|
||||
{`Active connections: 0
|
||||
server accepts handled requests
|
||||
1 7 0
|
||||
Reading: A Writing: B Waiting: 38`,
|
||||
&basicStatus{0, 1, 7, 0, 0, 0, 38},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
r := parse(test.in)
|
||||
if diff := pretty.Compare(r, test.out); diff != "" {
|
||||
t.Logf("%v", diff)
|
||||
t.Fatalf("expected %v but returned %v", test.out, r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestToint(t *testing.T) {
|
||||
tests := []struct {
|
||||
in []string
|
||||
pos int
|
||||
exp int
|
||||
}{
|
||||
{[]string{}, 0, 0},
|
||||
{[]string{}, 1, 0},
|
||||
{[]string{"A"}, 0, 0},
|
||||
{[]string{"1"}, 0, 1},
|
||||
{[]string{"a", "2"}, 1, 2},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
v := toInt(test.in, test.pos)
|
||||
if v != test.exp {
|
||||
t.Fatalf("expected %v but returned %v", test.exp, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
273
pkg/nginx/metric/collector/vts.go
Normal file
273
pkg/nginx/metric/collector/vts.go
Normal file
|
|
@ -0,0 +1,273 @@
|
|||
/*
|
||||
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 collector
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
"github.com/golang/glog"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
const ns = "nginx"
|
||||
|
||||
type (
|
||||
vtsCollector struct {
|
||||
scrapeChan chan scrapeRequest
|
||||
port int
|
||||
path string
|
||||
data *vtsData
|
||||
watchNamespace string
|
||||
ingressClass string
|
||||
}
|
||||
|
||||
vtsData struct {
|
||||
bytes *prometheus.Desc
|
||||
cache *prometheus.Desc
|
||||
connections *prometheus.Desc
|
||||
responses *prometheus.Desc
|
||||
requests *prometheus.Desc
|
||||
filterZoneBytes *prometheus.Desc
|
||||
filterZoneResponses *prometheus.Desc
|
||||
filterZoneCache *prometheus.Desc
|
||||
upstreamBackup *prometheus.Desc
|
||||
upstreamBytes *prometheus.Desc
|
||||
upstreamDown *prometheus.Desc
|
||||
upstreamFailTimeout *prometheus.Desc
|
||||
upstreamMaxFails *prometheus.Desc
|
||||
upstreamResponses *prometheus.Desc
|
||||
upstreamRequests *prometheus.Desc
|
||||
upstreamResponseMsec *prometheus.Desc
|
||||
upstreamWeight *prometheus.Desc
|
||||
}
|
||||
)
|
||||
|
||||
// NewNGINXVTSCollector returns a new prometheus collector for the VTS module
|
||||
func NewNGINXVTSCollector(watchNamespace, ingressClass string, port int, path string) Stopable {
|
||||
|
||||
p := vtsCollector{
|
||||
scrapeChan: make(chan scrapeRequest),
|
||||
port: port,
|
||||
path: path,
|
||||
watchNamespace: watchNamespace,
|
||||
ingressClass: ingressClass,
|
||||
}
|
||||
|
||||
p.data = &vtsData{
|
||||
bytes: prometheus.NewDesc(
|
||||
prometheus.BuildFQName(ns, "", "bytes_total"),
|
||||
"Nginx bytes count",
|
||||
[]string{"ingress_class", "namespace", "server_zone", "direction"}, nil),
|
||||
|
||||
cache: prometheus.NewDesc(
|
||||
prometheus.BuildFQName(ns, "", "cache_total"),
|
||||
"Nginx cache count",
|
||||
[]string{"ingress_class", "namespace", "server_zone", "type"}, nil),
|
||||
|
||||
connections: prometheus.NewDesc(
|
||||
prometheus.BuildFQName(ns, "", "connections_total"),
|
||||
"Nginx connections count",
|
||||
[]string{"ingress_class", "namespace", "type"}, nil),
|
||||
|
||||
responses: prometheus.NewDesc(
|
||||
prometheus.BuildFQName(ns, "", "responses_total"),
|
||||
"The number of responses with status codes 1xx, 2xx, 3xx, 4xx, and 5xx.",
|
||||
[]string{"ingress_class", "namespace", "server_zone", "status_code"}, nil),
|
||||
|
||||
requests: prometheus.NewDesc(
|
||||
prometheus.BuildFQName(ns, "", "requests_total"),
|
||||
"The total number of requested client connections.",
|
||||
[]string{"ingress_class", "namespace", "server_zone"}, nil),
|
||||
|
||||
filterZoneBytes: prometheus.NewDesc(
|
||||
prometheus.BuildFQName(ns, "", "filterzone_bytes_total"),
|
||||
"Nginx bytes count",
|
||||
[]string{"ingress_class", "namespace", "server_zone", "key", "direction"}, nil),
|
||||
|
||||
filterZoneResponses: prometheus.NewDesc(
|
||||
prometheus.BuildFQName(ns, "", "filterzone_responses_total"),
|
||||
"The number of responses with status codes 1xx, 2xx, 3xx, 4xx, and 5xx.",
|
||||
[]string{"ingress_class", "namespace", "server_zone", "key", "status_code"}, nil),
|
||||
|
||||
filterZoneCache: prometheus.NewDesc(
|
||||
prometheus.BuildFQName(ns, "", "filterzone_cache_total"),
|
||||
"Nginx cache count",
|
||||
[]string{"ingress_class", "namespace", "server_zone", "key", "type"}, nil),
|
||||
|
||||
upstreamBackup: prometheus.NewDesc(
|
||||
prometheus.BuildFQName(ns, "", "upstream_backup"),
|
||||
"Current backup setting of the server.",
|
||||
[]string{"ingress_class", "namespace", "upstream", "server"}, nil),
|
||||
|
||||
upstreamBytes: prometheus.NewDesc(
|
||||
prometheus.BuildFQName(ns, "", "upstream_bytes_total"),
|
||||
"The total number of bytes sent to this server.",
|
||||
[]string{"ingress_class", "namespace", "upstream", "server", "direction"}, nil),
|
||||
|
||||
upstreamDown: prometheus.NewDesc(
|
||||
prometheus.BuildFQName(ns, "", "vts_upstream_down_total"),
|
||||
"Current down setting of the server.",
|
||||
[]string{"ingress_class", "namespace", "upstream", "server"}, nil),
|
||||
|
||||
upstreamFailTimeout: prometheus.NewDesc(
|
||||
prometheus.BuildFQName(ns, "", "upstream_fail_timeout"),
|
||||
"Current fail_timeout setting of the server.",
|
||||
[]string{"ingress_class", "namespace", "upstream", "server"}, nil),
|
||||
|
||||
upstreamMaxFails: prometheus.NewDesc(
|
||||
prometheus.BuildFQName(ns, "", "upstream_maxfails"),
|
||||
"Current max_fails setting of the server.",
|
||||
[]string{"ingress_class", "namespace", "upstream", "server"}, nil),
|
||||
|
||||
upstreamResponses: prometheus.NewDesc(
|
||||
prometheus.BuildFQName(ns, "", "upstream_responses_total"),
|
||||
"The number of upstream responses with status codes 1xx, 2xx, 3xx, 4xx, and 5xx.",
|
||||
[]string{"ingress_class", "namespace", "upstream", "server", "status_code"}, nil),
|
||||
|
||||
upstreamRequests: prometheus.NewDesc(
|
||||
prometheus.BuildFQName(ns, "", "upstream_requests_total"),
|
||||
"The total number of client connections forwarded to this server.",
|
||||
[]string{"ingress_class", "namespace", "upstream", "server"}, nil),
|
||||
|
||||
upstreamResponseMsec: prometheus.NewDesc(
|
||||
prometheus.BuildFQName(ns, "", "upstream_response_msecs_avg"),
|
||||
"The average of only upstream response processing times in milliseconds.",
|
||||
[]string{"ingress_class", "namespace", "upstream", "server"}, nil),
|
||||
|
||||
upstreamWeight: prometheus.NewDesc(
|
||||
prometheus.BuildFQName(ns, "", "upstream_weight"),
|
||||
"Current upstream weight setting of the server.",
|
||||
[]string{"ingress_class", "namespace", "upstream", "server"}, nil),
|
||||
}
|
||||
|
||||
go p.start()
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
// Describe implements prometheus.Collector.
|
||||
func (p vtsCollector) Describe(ch chan<- *prometheus.Desc) {
|
||||
ch <- p.data.bytes
|
||||
ch <- p.data.cache
|
||||
ch <- p.data.connections
|
||||
ch <- p.data.requests
|
||||
ch <- p.data.responses
|
||||
ch <- p.data.upstreamBackup
|
||||
ch <- p.data.upstreamBytes
|
||||
ch <- p.data.upstreamDown
|
||||
ch <- p.data.upstreamFailTimeout
|
||||
ch <- p.data.upstreamMaxFails
|
||||
ch <- p.data.upstreamRequests
|
||||
ch <- p.data.upstreamResponseMsec
|
||||
ch <- p.data.upstreamResponses
|
||||
ch <- p.data.upstreamWeight
|
||||
ch <- p.data.filterZoneBytes
|
||||
ch <- p.data.filterZoneCache
|
||||
ch <- p.data.filterZoneResponses
|
||||
}
|
||||
|
||||
// Collect implements prometheus.Collector.
|
||||
func (p vtsCollector) Collect(ch chan<- prometheus.Metric) {
|
||||
req := scrapeRequest{results: ch, done: make(chan struct{})}
|
||||
p.scrapeChan <- req
|
||||
<-req.done
|
||||
}
|
||||
|
||||
func (p vtsCollector) start() {
|
||||
for req := range p.scrapeChan {
|
||||
ch := req.results
|
||||
p.scrapeVts(ch)
|
||||
req.done <- struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
func (p vtsCollector) Stop() {
|
||||
close(p.scrapeChan)
|
||||
}
|
||||
|
||||
// scrapeVts scrape nginx vts metrics
|
||||
func (p vtsCollector) scrapeVts(ch chan<- prometheus.Metric) {
|
||||
nginxMetrics, err := getNginxVtsMetrics(p.port, p.path)
|
||||
if err != nil {
|
||||
glog.Warningf("unexpected error obtaining nginx status info: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
reflectMetrics(&nginxMetrics.Connections, p.data.connections, ch, p.ingressClass, p.watchNamespace)
|
||||
|
||||
for name, zones := range nginxMetrics.UpstreamZones {
|
||||
for pos, value := range zones {
|
||||
reflectMetrics(&zones[pos].Responses, p.data.upstreamResponses, ch, p.ingressClass, p.watchNamespace, name, value.Server)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(p.data.upstreamRequests,
|
||||
prometheus.CounterValue, zones[pos].RequestCounter, p.ingressClass, p.watchNamespace, name, value.Server)
|
||||
ch <- prometheus.MustNewConstMetric(p.data.upstreamDown,
|
||||
prometheus.CounterValue, float64(zones[pos].Down), p.ingressClass, p.watchNamespace, name, value.Server)
|
||||
ch <- prometheus.MustNewConstMetric(p.data.upstreamWeight,
|
||||
prometheus.CounterValue, zones[pos].Weight, p.ingressClass, p.watchNamespace, name, value.Server)
|
||||
ch <- prometheus.MustNewConstMetric(p.data.upstreamResponseMsec,
|
||||
prometheus.CounterValue, zones[pos].ResponseMsec, p.ingressClass, p.watchNamespace, name, value.Server)
|
||||
ch <- prometheus.MustNewConstMetric(p.data.upstreamBackup,
|
||||
prometheus.CounterValue, float64(zones[pos].Backup), p.ingressClass, p.watchNamespace, name, value.Server)
|
||||
ch <- prometheus.MustNewConstMetric(p.data.upstreamFailTimeout,
|
||||
prometheus.CounterValue, zones[pos].FailTimeout, p.ingressClass, p.watchNamespace, name, value.Server)
|
||||
ch <- prometheus.MustNewConstMetric(p.data.upstreamMaxFails,
|
||||
prometheus.CounterValue, zones[pos].MaxFails, p.ingressClass, p.watchNamespace, name, value.Server)
|
||||
ch <- prometheus.MustNewConstMetric(p.data.upstreamBytes,
|
||||
prometheus.CounterValue, zones[pos].InBytes, p.ingressClass, p.watchNamespace, name, value.Server, "in")
|
||||
ch <- prometheus.MustNewConstMetric(p.data.upstreamBytes,
|
||||
prometheus.CounterValue, zones[pos].OutBytes, p.ingressClass, p.watchNamespace, name, value.Server, "out")
|
||||
}
|
||||
}
|
||||
|
||||
for name, zone := range nginxMetrics.ServerZones {
|
||||
reflectMetrics(&zone.Responses, p.data.responses, ch, p.ingressClass, p.watchNamespace, name)
|
||||
reflectMetrics(&zone.Cache, p.data.cache, ch, p.ingressClass, p.watchNamespace, name)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(p.data.requests,
|
||||
prometheus.CounterValue, zone.RequestCounter, p.ingressClass, p.watchNamespace, name)
|
||||
ch <- prometheus.MustNewConstMetric(p.data.bytes,
|
||||
prometheus.CounterValue, zone.InBytes, p.ingressClass, p.watchNamespace, name, "in")
|
||||
ch <- prometheus.MustNewConstMetric(p.data.bytes,
|
||||
prometheus.CounterValue, zone.OutBytes, p.ingressClass, p.watchNamespace, name, "out")
|
||||
}
|
||||
|
||||
for serverZone, keys := range nginxMetrics.FilterZones {
|
||||
for name, zone := range keys {
|
||||
reflectMetrics(&zone.Responses, p.data.filterZoneResponses, ch, p.ingressClass, p.watchNamespace, serverZone, name)
|
||||
reflectMetrics(&zone.Cache, p.data.filterZoneCache, ch, p.ingressClass, p.watchNamespace, serverZone, name)
|
||||
|
||||
ch <- prometheus.MustNewConstMetric(p.data.filterZoneBytes,
|
||||
prometheus.CounterValue, zone.InBytes, p.ingressClass, p.watchNamespace, serverZone, name, "in")
|
||||
ch <- prometheus.MustNewConstMetric(p.data.filterZoneBytes,
|
||||
prometheus.CounterValue, zone.OutBytes, p.ingressClass, p.watchNamespace, serverZone, name, "out")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func reflectMetrics(value interface{}, desc *prometheus.Desc, ch chan<- prometheus.Metric, labels ...string) {
|
||||
val := reflect.ValueOf(value).Elem()
|
||||
|
||||
for i := 0; i < val.NumField(); i++ {
|
||||
tag := val.Type().Field(i).Tag
|
||||
l := append(labels, tag.Get("json"))
|
||||
ch <- prometheus.MustNewConstMetric(desc,
|
||||
prometheus.CounterValue, val.Field(i).Interface().(float64),
|
||||
l...)
|
||||
}
|
||||
}
|
||||
137
pkg/nginx/template/configmap.go
Normal file
137
pkg/nginx/template/configmap.go
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
/*
|
||||
Copyright 2015 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 template
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/golang/glog"
|
||||
|
||||
"github.com/mitchellh/mapstructure"
|
||||
|
||||
ing_net "k8s.io/ingress-nginx/pkg/net"
|
||||
"k8s.io/ingress-nginx/pkg/nginx/config"
|
||||
)
|
||||
|
||||
const (
|
||||
customHTTPErrors = "custom-http-errors"
|
||||
skipAccessLogUrls = "skip-access-log-urls"
|
||||
whitelistSourceRange = "whitelist-source-range"
|
||||
proxyRealIPCIDR = "proxy-real-ip-cidr"
|
||||
bindAddress = "bind-address"
|
||||
)
|
||||
|
||||
// ReadConfig obtains the configuration defined by the user merged with the defaults.
|
||||
func ReadConfig(src map[string]string) config.Configuration {
|
||||
conf := map[string]string{}
|
||||
if src != nil {
|
||||
// we need to copy the configmap data because the content is altered
|
||||
for k, v := range src {
|
||||
conf[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
errors := make([]int, 0)
|
||||
skipUrls := make([]string, 0)
|
||||
whitelist := make([]string, 0)
|
||||
proxylist := make([]string, 0)
|
||||
bindAddressIpv4List := make([]string, 0)
|
||||
bindAddressIpv6List := make([]string, 0)
|
||||
|
||||
if val, ok := conf[customHTTPErrors]; ok {
|
||||
delete(conf, customHTTPErrors)
|
||||
for _, i := range strings.Split(val, ",") {
|
||||
j, err := strconv.Atoi(i)
|
||||
if err != nil {
|
||||
glog.Warningf("%v is not a valid http code: %v", i, err)
|
||||
} else {
|
||||
errors = append(errors, j)
|
||||
}
|
||||
}
|
||||
}
|
||||
if val, ok := conf[skipAccessLogUrls]; ok {
|
||||
delete(conf, skipAccessLogUrls)
|
||||
skipUrls = strings.Split(val, ",")
|
||||
}
|
||||
if val, ok := conf[whitelistSourceRange]; ok {
|
||||
delete(conf, whitelistSourceRange)
|
||||
whitelist = append(whitelist, strings.Split(val, ",")...)
|
||||
}
|
||||
if val, ok := conf[proxyRealIPCIDR]; ok {
|
||||
delete(conf, proxyRealIPCIDR)
|
||||
proxylist = append(proxylist, strings.Split(val, ",")...)
|
||||
} else {
|
||||
proxylist = append(proxylist, "0.0.0.0/0")
|
||||
}
|
||||
if val, ok := conf[bindAddress]; ok {
|
||||
delete(conf, bindAddress)
|
||||
for _, i := range strings.Split(val, ",") {
|
||||
ns := net.ParseIP(i)
|
||||
if ns != nil {
|
||||
if ing_net.IsIPV6(ns) {
|
||||
bindAddressIpv6List = append(bindAddressIpv6List, fmt.Sprintf("[%v]", ns))
|
||||
} else {
|
||||
bindAddressIpv4List = append(bindAddressIpv4List, fmt.Sprintf("%v", ns))
|
||||
}
|
||||
} else {
|
||||
glog.Warningf("%v is not a valid textual representation of an IP address", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
to := config.NewDefault()
|
||||
to.CustomHTTPErrors = filterErrors(errors)
|
||||
to.SkipAccessLogURLs = skipUrls
|
||||
to.WhitelistSourceRange = whitelist
|
||||
to.ProxyRealIPCIDR = proxylist
|
||||
to.BindAddressIpv4 = bindAddressIpv4List
|
||||
to.BindAddressIpv6 = bindAddressIpv6List
|
||||
|
||||
config := &mapstructure.DecoderConfig{
|
||||
Metadata: nil,
|
||||
WeaklyTypedInput: true,
|
||||
Result: &to,
|
||||
TagName: "json",
|
||||
}
|
||||
|
||||
decoder, err := mapstructure.NewDecoder(config)
|
||||
if err != nil {
|
||||
glog.Warningf("unexpected error merging defaults: %v", err)
|
||||
}
|
||||
err = decoder.Decode(conf)
|
||||
if err != nil {
|
||||
glog.Warningf("unexpected error merging defaults: %v", err)
|
||||
}
|
||||
|
||||
return to
|
||||
}
|
||||
|
||||
func filterErrors(codes []int) []int {
|
||||
var fa []int
|
||||
for _, code := range codes {
|
||||
if code > 299 && code < 600 {
|
||||
fa = append(fa, code)
|
||||
} else {
|
||||
glog.Warningf("error code %v is not valid for custom error pages", code)
|
||||
}
|
||||
}
|
||||
|
||||
return fa
|
||||
}
|
||||
95
pkg/nginx/template/configmap_test.go
Normal file
95
pkg/nginx/template/configmap_test.go
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
/*
|
||||
Copyright 2015 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 template
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/kylelemons/godebug/pretty"
|
||||
|
||||
"k8s.io/ingress-nginx/pkg/nginx/config"
|
||||
)
|
||||
|
||||
func TestFilterErrors(t *testing.T) {
|
||||
e := filterErrors([]int{200, 300, 345, 500, 555, 999})
|
||||
if len(e) != 4 {
|
||||
t.Errorf("expected 4 elements but %v returned", len(e))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeConfigMapToStruct(t *testing.T) {
|
||||
conf := map[string]string{
|
||||
"custom-http-errors": "300,400,demo",
|
||||
"proxy-read-timeout": "1",
|
||||
"proxy-send-timeout": "2",
|
||||
"skip-access-log-urls": "/log,/demo,/test",
|
||||
"use-proxy-protocol": "true",
|
||||
"disable-access-log": "true",
|
||||
"access-log-path": "/var/log/test/access.log",
|
||||
"error-log-path": "/var/log/test/error.log",
|
||||
"use-gzip": "true",
|
||||
"enable-dynamic-tls-records": "false",
|
||||
"gzip-types": "text/html",
|
||||
"proxy-real-ip-cidr": "1.1.1.1/8,2.2.2.2/24",
|
||||
"bind-address": "1.1.1.1,2.2.2.2,3.3.3,2001:db8:a0b:12f0::1,3731:54:65fe:2::a7,33:33:33::33::33",
|
||||
"worker-shutdown-timeout": "99s",
|
||||
}
|
||||
def := config.NewDefault()
|
||||
def.CustomHTTPErrors = []int{300, 400}
|
||||
def.DisableAccessLog = true
|
||||
def.AccessLogPath = "/var/log/test/access.log"
|
||||
def.ErrorLogPath = "/var/log/test/error.log"
|
||||
def.SkipAccessLogURLs = []string{"/log", "/demo", "/test"}
|
||||
def.ProxyReadTimeout = 1
|
||||
def.ProxySendTimeout = 2
|
||||
def.EnableDynamicTLSRecords = false
|
||||
def.UseProxyProtocol = true
|
||||
def.GzipTypes = "text/html"
|
||||
def.ProxyRealIPCIDR = []string{"1.1.1.1/8", "2.2.2.2/24"}
|
||||
def.BindAddressIpv4 = []string{"1.1.1.1", "2.2.2.2"}
|
||||
def.BindAddressIpv6 = []string{"[2001:db8:a0b:12f0::1]", "[3731:54:65fe:2::a7]"}
|
||||
def.WorkerShutdownTimeout = "99s"
|
||||
|
||||
to := ReadConfig(conf)
|
||||
if diff := pretty.Compare(to, def); diff != "" {
|
||||
t.Errorf("unexpected diff: (-got +want)\n%s", diff)
|
||||
}
|
||||
|
||||
def = config.NewDefault()
|
||||
to = ReadConfig(map[string]string{})
|
||||
if diff := pretty.Compare(to, def); diff != "" {
|
||||
t.Errorf("unexpected diff: (-got +want)\n%s", diff)
|
||||
}
|
||||
|
||||
def = config.NewDefault()
|
||||
def.WhitelistSourceRange = []string{"1.1.1.1/32"}
|
||||
to = ReadConfig(map[string]string{
|
||||
"whitelist-source-range": "1.1.1.1/32",
|
||||
})
|
||||
|
||||
if diff := pretty.Compare(to, def); diff != "" {
|
||||
t.Errorf("unexpected diff: (-got +want)\n%s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultLoadBalance(t *testing.T) {
|
||||
conf := map[string]string{}
|
||||
to := ReadConfig(conf)
|
||||
if to.LoadBalanceAlgorithm != "least_conn" {
|
||||
t.Errorf("default load balance algorithm wrong")
|
||||
}
|
||||
}
|
||||
690
pkg/nginx/template/template.go
Normal file
690
pkg/nginx/template/template.go
Normal file
|
|
@ -0,0 +1,690 @@
|
|||
/*
|
||||
Copyright 2015 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 template
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
text_template "text/template"
|
||||
|
||||
"github.com/golang/glog"
|
||||
|
||||
"github.com/pborman/uuid"
|
||||
|
||||
extensions "k8s.io/api/extensions/v1beta1"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/ingress-nginx/pkg/ingress"
|
||||
"k8s.io/ingress-nginx/pkg/ingress/annotations/ratelimit"
|
||||
ing_net "k8s.io/ingress-nginx/pkg/net"
|
||||
"k8s.io/ingress-nginx/pkg/nginx/config"
|
||||
"k8s.io/ingress-nginx/pkg/watch"
|
||||
)
|
||||
|
||||
const (
|
||||
slash = "/"
|
||||
defBufferSize = 65535
|
||||
)
|
||||
|
||||
// Template ...
|
||||
type Template struct {
|
||||
tmpl *text_template.Template
|
||||
fw watch.FileWatcher
|
||||
s int
|
||||
}
|
||||
|
||||
//NewTemplate returns a new Template instance or an
|
||||
//error if the specified template file contains errors
|
||||
func NewTemplate(file string, onChange func()) (*Template, error) {
|
||||
tmpl, err := text_template.New("nginx.tmpl").Funcs(funcMap).ParseFiles(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fw, err := watch.NewFileWatcher(file, onChange)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Template{
|
||||
tmpl: tmpl,
|
||||
fw: fw,
|
||||
s: defBufferSize,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Close removes the file watcher
|
||||
func (t *Template) Close() {
|
||||
t.fw.Close()
|
||||
}
|
||||
|
||||
// Write populates a buffer using a template with NGINX configuration
|
||||
// and the servers and upstreams created by Ingress rules
|
||||
func (t *Template) Write(conf config.TemplateConfig) ([]byte, error) {
|
||||
tmplBuf := bytes.NewBuffer(make([]byte, 0, t.s))
|
||||
outCmdBuf := bytes.NewBuffer(make([]byte, 0, t.s))
|
||||
|
||||
defer func() {
|
||||
if t.s < tmplBuf.Cap() {
|
||||
glog.V(2).Infof("adjusting template buffer size from %v to %v", t.s, tmplBuf.Cap())
|
||||
t.s = tmplBuf.Cap()
|
||||
}
|
||||
}()
|
||||
|
||||
if glog.V(3) {
|
||||
b, err := json.Marshal(conf)
|
||||
if err != nil {
|
||||
glog.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
glog.Infof("NGINX configuration: %v", string(b))
|
||||
}
|
||||
|
||||
err := t.tmpl.Execute(tmplBuf, conf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// squeezes multiple adjacent empty lines to be single
|
||||
// spaced this is to avoid the use of regular expressions
|
||||
cmd := exec.Command("/ingress-controller/clean-nginx-conf.sh")
|
||||
cmd.Stdin = tmplBuf
|
||||
cmd.Stdout = outCmdBuf
|
||||
if err := cmd.Run(); err != nil {
|
||||
glog.Warningf("unexpected error cleaning template: %v", err)
|
||||
return tmplBuf.Bytes(), nil
|
||||
}
|
||||
|
||||
return outCmdBuf.Bytes(), nil
|
||||
}
|
||||
|
||||
var (
|
||||
funcMap = text_template.FuncMap{
|
||||
"empty": func(input interface{}) bool {
|
||||
check, ok := input.(string)
|
||||
if ok {
|
||||
return len(check) == 0
|
||||
}
|
||||
return true
|
||||
},
|
||||
"buildLocation": buildLocation,
|
||||
"buildAuthLocation": buildAuthLocation,
|
||||
"buildAuthResponseHeaders": buildAuthResponseHeaders,
|
||||
"buildProxyPass": buildProxyPass,
|
||||
"filterRateLimits": filterRateLimits,
|
||||
"buildRateLimitZones": buildRateLimitZones,
|
||||
"buildRateLimit": buildRateLimit,
|
||||
"buildResolvers": buildResolvers,
|
||||
"buildUpstreamName": buildUpstreamName,
|
||||
"isLocationAllowed": isLocationAllowed,
|
||||
"buildLogFormatUpstream": buildLogFormatUpstream,
|
||||
"buildDenyVariable": buildDenyVariable,
|
||||
"getenv": os.Getenv,
|
||||
"contains": strings.Contains,
|
||||
"hasPrefix": strings.HasPrefix,
|
||||
"hasSuffix": strings.HasSuffix,
|
||||
"toUpper": strings.ToUpper,
|
||||
"toLower": strings.ToLower,
|
||||
"formatIP": formatIP,
|
||||
"buildNextUpstream": buildNextUpstream,
|
||||
"getIngressInformation": getIngressInformation,
|
||||
"serverConfig": func(all config.TemplateConfig, server *ingress.Server) interface{} {
|
||||
return struct{ First, Second interface{} }{all, server}
|
||||
},
|
||||
"isValidClientBodyBufferSize": isValidClientBodyBufferSize,
|
||||
"buildForwardedFor": buildForwardedFor,
|
||||
"buildAuthSignURL": buildAuthSignURL,
|
||||
}
|
||||
)
|
||||
|
||||
// formatIP will wrap IPv6 addresses in [] and return IPv4 addresses
|
||||
// without modification. If the input cannot be parsed as an IP address
|
||||
// it is returned without modification.
|
||||
func formatIP(input string) string {
|
||||
ip := net.ParseIP(input)
|
||||
if ip == nil {
|
||||
return input
|
||||
}
|
||||
if v4 := ip.To4(); v4 != nil {
|
||||
return input
|
||||
}
|
||||
return fmt.Sprintf("[%s]", input)
|
||||
}
|
||||
|
||||
// buildResolvers returns the resolvers reading the /etc/resolv.conf file
|
||||
func buildResolvers(input interface{}) string {
|
||||
// NGINX need IPV6 addresses to be surrounded by brackets
|
||||
nss, ok := input.([]net.IP)
|
||||
if !ok {
|
||||
glog.Errorf("expected a '[]net.IP' type but %T was returned", input)
|
||||
return ""
|
||||
}
|
||||
|
||||
if len(nss) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
r := []string{"resolver"}
|
||||
for _, ns := range nss {
|
||||
if ing_net.IsIPV6(ns) {
|
||||
r = append(r, fmt.Sprintf("[%v]", ns))
|
||||
} else {
|
||||
r = append(r, fmt.Sprintf("%v", ns))
|
||||
}
|
||||
}
|
||||
r = append(r, "valid=30s;")
|
||||
|
||||
return strings.Join(r, " ")
|
||||
}
|
||||
|
||||
// buildLocation produces the location string, if the ingress has redirects
|
||||
// (specified through the ingress.kubernetes.io/rewrite-to annotation)
|
||||
func buildLocation(input interface{}) string {
|
||||
location, ok := input.(*ingress.Location)
|
||||
if !ok {
|
||||
glog.Errorf("expected an '*ingress.Location' type but %T was returned", input)
|
||||
return slash
|
||||
}
|
||||
|
||||
path := location.Path
|
||||
if len(location.Rewrite.Target) > 0 && location.Rewrite.Target != path {
|
||||
if path == slash {
|
||||
return fmt.Sprintf("~* %s", path)
|
||||
}
|
||||
// baseuri regex will parse basename from the given location
|
||||
baseuri := `(?<baseuri>.*)`
|
||||
if !strings.HasSuffix(path, slash) {
|
||||
// Not treat the slash after "location path" as a part of baseuri
|
||||
baseuri = fmt.Sprintf(`\/?%s`, baseuri)
|
||||
}
|
||||
return fmt.Sprintf(`~* ^%s%s`, path, baseuri)
|
||||
}
|
||||
|
||||
return path
|
||||
}
|
||||
|
||||
// TODO: Needs Unit Tests
|
||||
func buildAuthLocation(input interface{}) string {
|
||||
location, ok := input.(*ingress.Location)
|
||||
if !ok {
|
||||
glog.Errorf("expected an '*ingress.Location' type but %T was returned", input)
|
||||
return ""
|
||||
}
|
||||
|
||||
if location.ExternalAuth.URL == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
str := base64.URLEncoding.EncodeToString([]byte(location.Path))
|
||||
// avoid locations containing the = char
|
||||
str = strings.Replace(str, "=", "", -1)
|
||||
return fmt.Sprintf("/_external-auth-%v", str)
|
||||
}
|
||||
|
||||
func buildAuthResponseHeaders(input interface{}) []string {
|
||||
location, ok := input.(*ingress.Location)
|
||||
res := []string{}
|
||||
if !ok {
|
||||
glog.Errorf("expected an '*ingress.Location' type but %T was returned", input)
|
||||
return res
|
||||
}
|
||||
|
||||
if len(location.ExternalAuth.ResponseHeaders) == 0 {
|
||||
return res
|
||||
}
|
||||
|
||||
for i, h := range location.ExternalAuth.ResponseHeaders {
|
||||
hvar := strings.ToLower(h)
|
||||
hvar = strings.NewReplacer("-", "_").Replace(hvar)
|
||||
res = append(res, fmt.Sprintf("auth_request_set $authHeader%v $upstream_http_%v;", i, hvar))
|
||||
res = append(res, fmt.Sprintf("proxy_set_header '%v' $authHeader%v;", h, i))
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func buildLogFormatUpstream(input interface{}) string {
|
||||
cfg, ok := input.(config.Configuration)
|
||||
if !ok {
|
||||
glog.Errorf("expected a 'config.Configuration' type but %T was returned", input)
|
||||
return ""
|
||||
}
|
||||
|
||||
return cfg.BuildLogFormatUpstream()
|
||||
}
|
||||
|
||||
// buildProxyPass produces the proxy pass string, if the ingress has redirects
|
||||
// (specified through the ingress.kubernetes.io/rewrite-to annotation)
|
||||
// If the annotation ingress.kubernetes.io/add-base-url:"true" is specified it will
|
||||
// add a base tag in the head of the response from the service
|
||||
func buildProxyPass(host string, b interface{}, loc interface{}) string {
|
||||
backends, ok := b.([]*ingress.Backend)
|
||||
if !ok {
|
||||
glog.Errorf("expected an '[]*ingress.Backend' type but %T was returned", b)
|
||||
return ""
|
||||
}
|
||||
|
||||
location, ok := loc.(*ingress.Location)
|
||||
if !ok {
|
||||
glog.Errorf("expected a '*ingress.Location' type but %T was returned", loc)
|
||||
return ""
|
||||
}
|
||||
|
||||
path := location.Path
|
||||
proto := "http"
|
||||
|
||||
upstreamName := location.Backend
|
||||
for _, backend := range backends {
|
||||
if backend.Name == location.Backend {
|
||||
if backend.Secure || backend.SSLPassthrough {
|
||||
proto = "https"
|
||||
}
|
||||
|
||||
if isSticky(host, location, backend.SessionAffinity.CookieSessionAffinity.Locations) {
|
||||
upstreamName = fmt.Sprintf("sticky-%v", upstreamName)
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// defProxyPass returns the default proxy_pass, just the name of the upstream
|
||||
defProxyPass := fmt.Sprintf("proxy_pass %s://%s;", proto, upstreamName)
|
||||
// if the path in the ingress rule is equals to the target: no special rewrite
|
||||
if path == location.Rewrite.Target {
|
||||
return defProxyPass
|
||||
}
|
||||
|
||||
if !strings.HasSuffix(path, slash) {
|
||||
path = fmt.Sprintf("%s/", path)
|
||||
}
|
||||
|
||||
if len(location.Rewrite.Target) > 0 {
|
||||
abu := ""
|
||||
if location.Rewrite.AddBaseURL {
|
||||
// path has a slash suffix, so that it can be connected with baseuri directly
|
||||
bPath := fmt.Sprintf("%s%s", path, "$baseuri")
|
||||
if len(location.Rewrite.BaseURLScheme) > 0 {
|
||||
abu = fmt.Sprintf(`subs_filter '<head(.*)>' '<head$1><base href="%v://$http_host%v">' r;
|
||||
subs_filter '<HEAD(.*)>' '<HEAD$1><base href="%v://$http_host%v">' r;
|
||||
`, location.Rewrite.BaseURLScheme, bPath, location.Rewrite.BaseURLScheme, bPath)
|
||||
} else {
|
||||
abu = fmt.Sprintf(`subs_filter '<head(.*)>' '<head$1><base href="$scheme://$http_host%v">' r;
|
||||
subs_filter '<HEAD(.*)>' '<HEAD$1><base href="$scheme://$http_host%v">' r;
|
||||
`, bPath, bPath)
|
||||
}
|
||||
}
|
||||
|
||||
if location.Rewrite.Target == slash {
|
||||
// special case redirect to /
|
||||
// ie /something to /
|
||||
return fmt.Sprintf(`
|
||||
rewrite %s(.*) /$1 break;
|
||||
rewrite %s / break;
|
||||
proxy_pass %s://%s;
|
||||
%v`, path, location.Path, proto, upstreamName, abu)
|
||||
}
|
||||
|
||||
return fmt.Sprintf(`
|
||||
rewrite %s(.*) %s/$1 break;
|
||||
proxy_pass %s://%s;
|
||||
%v`, path, location.Rewrite.Target, proto, upstreamName, abu)
|
||||
}
|
||||
|
||||
// default proxy_pass
|
||||
return defProxyPass
|
||||
}
|
||||
|
||||
// TODO: Needs Unit Tests
|
||||
func filterRateLimits(input interface{}) []ratelimit.RateLimit {
|
||||
ratelimits := []ratelimit.RateLimit{}
|
||||
found := sets.String{}
|
||||
|
||||
servers, ok := input.([]*ingress.Server)
|
||||
if !ok {
|
||||
glog.Errorf("expected a '[]ratelimit.RateLimit' type but %T was returned", input)
|
||||
return ratelimits
|
||||
}
|
||||
for _, server := range servers {
|
||||
for _, loc := range server.Locations {
|
||||
if loc.RateLimit.ID != "" && !found.Has(loc.RateLimit.ID) {
|
||||
found.Insert(loc.RateLimit.ID)
|
||||
ratelimits = append(ratelimits, loc.RateLimit)
|
||||
}
|
||||
}
|
||||
}
|
||||
return ratelimits
|
||||
}
|
||||
|
||||
// TODO: Needs Unit Tests
|
||||
// buildRateLimitZones produces an array of limit_conn_zone in order to allow
|
||||
// rate limiting of request. Each Ingress rule could have up to three zones, one
|
||||
// for connection limit by IP address, one for limiting requests per minute, and
|
||||
// one for limiting requests per second.
|
||||
func buildRateLimitZones(input interface{}) []string {
|
||||
zones := sets.String{}
|
||||
|
||||
servers, ok := input.([]*ingress.Server)
|
||||
if !ok {
|
||||
glog.Errorf("expected a '[]*ingress.Server' type but %T was returned", input)
|
||||
return zones.List()
|
||||
}
|
||||
|
||||
for _, server := range servers {
|
||||
for _, loc := range server.Locations {
|
||||
if loc.RateLimit.Connections.Limit > 0 {
|
||||
zone := fmt.Sprintf("limit_conn_zone $limit_%s zone=%v:%vm;",
|
||||
loc.RateLimit.ID,
|
||||
loc.RateLimit.Connections.Name,
|
||||
loc.RateLimit.Connections.SharedSize)
|
||||
if !zones.Has(zone) {
|
||||
zones.Insert(zone)
|
||||
}
|
||||
}
|
||||
|
||||
if loc.RateLimit.RPM.Limit > 0 {
|
||||
zone := fmt.Sprintf("limit_req_zone $limit_%s zone=%v:%vm rate=%vr/m;",
|
||||
loc.RateLimit.ID,
|
||||
loc.RateLimit.RPM.Name,
|
||||
loc.RateLimit.RPM.SharedSize,
|
||||
loc.RateLimit.RPM.Limit)
|
||||
if !zones.Has(zone) {
|
||||
zones.Insert(zone)
|
||||
}
|
||||
}
|
||||
|
||||
if loc.RateLimit.RPS.Limit > 0 {
|
||||
zone := fmt.Sprintf("limit_req_zone $limit_%s zone=%v:%vm rate=%vr/s;",
|
||||
loc.RateLimit.ID,
|
||||
loc.RateLimit.RPS.Name,
|
||||
loc.RateLimit.RPS.SharedSize,
|
||||
loc.RateLimit.RPS.Limit)
|
||||
if !zones.Has(zone) {
|
||||
zones.Insert(zone)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return zones.List()
|
||||
}
|
||||
|
||||
// buildRateLimit produces an array of limit_req to be used inside the Path of
|
||||
// Ingress rules. The order: connections by IP first, then RPS, and RPM last.
|
||||
func buildRateLimit(input interface{}) []string {
|
||||
limits := []string{}
|
||||
|
||||
loc, ok := input.(*ingress.Location)
|
||||
if !ok {
|
||||
glog.Errorf("expected an '*ingress.Location' type but %T was returned", input)
|
||||
return limits
|
||||
}
|
||||
|
||||
if loc.RateLimit.Connections.Limit > 0 {
|
||||
limit := fmt.Sprintf("limit_conn %v %v;",
|
||||
loc.RateLimit.Connections.Name, loc.RateLimit.Connections.Limit)
|
||||
limits = append(limits, limit)
|
||||
}
|
||||
|
||||
if loc.RateLimit.RPS.Limit > 0 {
|
||||
limit := fmt.Sprintf("limit_req zone=%v burst=%v nodelay;",
|
||||
loc.RateLimit.RPS.Name, loc.RateLimit.RPS.Burst)
|
||||
limits = append(limits, limit)
|
||||
}
|
||||
|
||||
if loc.RateLimit.RPM.Limit > 0 {
|
||||
limit := fmt.Sprintf("limit_req zone=%v burst=%v nodelay;",
|
||||
loc.RateLimit.RPM.Name, loc.RateLimit.RPM.Burst)
|
||||
limits = append(limits, limit)
|
||||
}
|
||||
|
||||
if loc.RateLimit.LimitRateAfter > 0 {
|
||||
limit := fmt.Sprintf("limit_rate_after %vk;",
|
||||
loc.RateLimit.LimitRateAfter)
|
||||
limits = append(limits, limit)
|
||||
}
|
||||
|
||||
if loc.RateLimit.LimitRate > 0 {
|
||||
limit := fmt.Sprintf("limit_rate %vk;",
|
||||
loc.RateLimit.LimitRate)
|
||||
limits = append(limits, limit)
|
||||
}
|
||||
|
||||
return limits
|
||||
}
|
||||
|
||||
func isLocationAllowed(input interface{}) bool {
|
||||
loc, ok := input.(*ingress.Location)
|
||||
if !ok {
|
||||
glog.Errorf("expected an '*ingress.Location' type but %T was returned", input)
|
||||
return false
|
||||
}
|
||||
|
||||
return loc.Denied == nil
|
||||
}
|
||||
|
||||
var (
|
||||
denyPathSlugMap = map[string]string{}
|
||||
)
|
||||
|
||||
// buildDenyVariable returns a nginx variable for a location in a
|
||||
// server to be used in the whitelist check
|
||||
// This method uses a unique id generator library to reduce the
|
||||
// size of the string to be used as a variable in nginx to avoid
|
||||
// issue with the size of the variable bucket size directive
|
||||
func buildDenyVariable(a interface{}) string {
|
||||
l, ok := a.(string)
|
||||
if !ok {
|
||||
glog.Errorf("expected a 'string' type but %T was returned", a)
|
||||
return ""
|
||||
}
|
||||
|
||||
if _, ok := denyPathSlugMap[l]; !ok {
|
||||
denyPathSlugMap[l] = buildRandomUUID()
|
||||
}
|
||||
|
||||
return fmt.Sprintf("$deny_%v", denyPathSlugMap[l])
|
||||
}
|
||||
|
||||
// TODO: Needs Unit Tests
|
||||
func buildUpstreamName(host string, b interface{}, loc interface{}) string {
|
||||
|
||||
backends, ok := b.([]*ingress.Backend)
|
||||
if !ok {
|
||||
glog.Errorf("expected an '[]*ingress.Backend' type but %T was returned", b)
|
||||
return ""
|
||||
}
|
||||
|
||||
location, ok := loc.(*ingress.Location)
|
||||
if !ok {
|
||||
glog.Errorf("expected a '*ingress.Location' type but %T was returned", loc)
|
||||
return ""
|
||||
}
|
||||
|
||||
upstreamName := location.Backend
|
||||
|
||||
for _, backend := range backends {
|
||||
if backend.Name == location.Backend {
|
||||
if backend.SessionAffinity.AffinityType == "cookie" &&
|
||||
isSticky(host, location, backend.SessionAffinity.CookieSessionAffinity.Locations) {
|
||||
upstreamName = fmt.Sprintf("sticky-%v", upstreamName)
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return upstreamName
|
||||
}
|
||||
|
||||
// TODO: Needs Unit Tests
|
||||
func isSticky(host string, loc *ingress.Location, stickyLocations map[string][]string) bool {
|
||||
if _, ok := stickyLocations[host]; ok {
|
||||
for _, sl := range stickyLocations[host] {
|
||||
if sl == loc.Path {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func buildNextUpstream(input interface{}) string {
|
||||
nextUpstream, ok := input.(string)
|
||||
if !ok {
|
||||
glog.Errorf("expected a 'string' type but %T was returned", input)
|
||||
return ""
|
||||
}
|
||||
|
||||
parts := strings.Split(nextUpstream, " ")
|
||||
|
||||
nextUpstreamCodes := make([]string, 0, len(parts))
|
||||
for _, v := range parts {
|
||||
if v != "" && v != "non_idempotent" {
|
||||
nextUpstreamCodes = append(nextUpstreamCodes, v)
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(nextUpstreamCodes, " ")
|
||||
}
|
||||
|
||||
// buildRandomUUID return a random string to be used in the template
|
||||
func buildRandomUUID() string {
|
||||
s := uuid.New()
|
||||
return strings.Replace(s, "-", "", -1)
|
||||
}
|
||||
|
||||
func isValidClientBodyBufferSize(input interface{}) bool {
|
||||
s, ok := input.(string)
|
||||
if !ok {
|
||||
glog.Errorf("expected an 'string' type but %T was returned", input)
|
||||
return false
|
||||
}
|
||||
|
||||
if s == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
_, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
sLowercase := strings.ToLower(s)
|
||||
|
||||
kCheck := strings.TrimSuffix(sLowercase, "k")
|
||||
_, err := strconv.Atoi(kCheck)
|
||||
if err == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
mCheck := strings.TrimSuffix(sLowercase, "m")
|
||||
_, err = strconv.Atoi(mCheck)
|
||||
if err == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
glog.Errorf("client-body-buffer-size '%v' was provided in an incorrect format, hence it will not be set.", s)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
type ingressInformation struct {
|
||||
Namespace string
|
||||
Rule string
|
||||
Service string
|
||||
Annotations map[string]string
|
||||
}
|
||||
|
||||
func getIngressInformation(i, p interface{}) *ingressInformation {
|
||||
ing, ok := i.(*extensions.Ingress)
|
||||
if !ok {
|
||||
glog.Errorf("expected an '*extensions.Ingress' type but %T was returned", i)
|
||||
return &ingressInformation{}
|
||||
}
|
||||
|
||||
path, ok := p.(string)
|
||||
if !ok {
|
||||
glog.Errorf("expected a 'string' type but %T was returned", p)
|
||||
return &ingressInformation{}
|
||||
}
|
||||
|
||||
if ing == nil {
|
||||
return &ingressInformation{}
|
||||
}
|
||||
|
||||
info := &ingressInformation{
|
||||
Namespace: ing.GetNamespace(),
|
||||
Rule: ing.GetName(),
|
||||
Annotations: ing.Annotations,
|
||||
}
|
||||
|
||||
if ing.Spec.Backend != nil {
|
||||
info.Service = ing.Spec.Backend.ServiceName
|
||||
}
|
||||
|
||||
for _, rule := range ing.Spec.Rules {
|
||||
if rule.HTTP == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, rPath := range rule.HTTP.Paths {
|
||||
if path == rPath.Path {
|
||||
info.Service = rPath.Backend.ServiceName
|
||||
return info
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return info
|
||||
}
|
||||
|
||||
func buildForwardedFor(input interface{}) string {
|
||||
s, ok := input.(string)
|
||||
if !ok {
|
||||
glog.Errorf("expected a 'string' type but %T was returned", input)
|
||||
return ""
|
||||
}
|
||||
|
||||
ffh := strings.Replace(s, "-", "_", -1)
|
||||
ffh = strings.ToLower(ffh)
|
||||
return fmt.Sprintf("$http_%v", ffh)
|
||||
}
|
||||
|
||||
func buildAuthSignURL(input interface{}) string {
|
||||
s, ok := input.(string)
|
||||
if !ok {
|
||||
glog.Errorf("expected an 'string' type but %T was returned", input)
|
||||
return ""
|
||||
}
|
||||
|
||||
u, _ := url.Parse(s)
|
||||
q := u.Query()
|
||||
if len(q) == 0 {
|
||||
return fmt.Sprintf("%v?rd=$scheme://$http_host$request_uri", s)
|
||||
}
|
||||
|
||||
if q.Get("rd") != "" {
|
||||
return s
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%v&rd=$scheme://$http_host$request_uri", s)
|
||||
}
|
||||
372
pkg/nginx/template/template_test.go
Normal file
372
pkg/nginx/template/template_test.go
Normal file
|
|
@ -0,0 +1,372 @@
|
|||
/*
|
||||
Copyright 2015 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 template
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"os"
|
||||
"path"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"k8s.io/ingress-nginx/pkg/ingress"
|
||||
"k8s.io/ingress-nginx/pkg/ingress/annotations/authreq"
|
||||
"k8s.io/ingress-nginx/pkg/ingress/annotations/rewrite"
|
||||
"k8s.io/ingress-nginx/pkg/nginx/config"
|
||||
)
|
||||
|
||||
var (
|
||||
// TODO: add tests for secure endpoints
|
||||
tmplFuncTestcases = map[string]struct {
|
||||
Path string
|
||||
Target string
|
||||
Location string
|
||||
ProxyPass string
|
||||
AddBaseURL bool
|
||||
BaseURLScheme string
|
||||
}{
|
||||
"invalid redirect / to /": {"/", "/", "/", "proxy_pass http://upstream-name;", false, ""},
|
||||
"redirect / to /jenkins": {"/", "/jenkins", "~* /",
|
||||
`
|
||||
rewrite /(.*) /jenkins/$1 break;
|
||||
proxy_pass http://upstream-name;
|
||||
`, false, ""},
|
||||
"redirect /something to /": {"/something", "/", `~* ^/something\/?(?<baseuri>.*)`, `
|
||||
rewrite /something/(.*) /$1 break;
|
||||
rewrite /something / break;
|
||||
proxy_pass http://upstream-name;
|
||||
`, false, ""},
|
||||
"redirect /end-with-slash/ to /not-root": {"/end-with-slash/", "/not-root", "~* ^/end-with-slash/(?<baseuri>.*)", `
|
||||
rewrite /end-with-slash/(.*) /not-root/$1 break;
|
||||
proxy_pass http://upstream-name;
|
||||
`, false, ""},
|
||||
"redirect /something-complex to /not-root": {"/something-complex", "/not-root", `~* ^/something-complex\/?(?<baseuri>.*)`, `
|
||||
rewrite /something-complex/(.*) /not-root/$1 break;
|
||||
proxy_pass http://upstream-name;
|
||||
`, false, ""},
|
||||
"redirect / to /jenkins and rewrite": {"/", "/jenkins", "~* /", `
|
||||
rewrite /(.*) /jenkins/$1 break;
|
||||
proxy_pass http://upstream-name;
|
||||
subs_filter '<head(.*)>' '<head$1><base href="$scheme://$http_host/$baseuri">' r;
|
||||
subs_filter '<HEAD(.*)>' '<HEAD$1><base href="$scheme://$http_host/$baseuri">' r;
|
||||
`, true, ""},
|
||||
"redirect /something to / and rewrite": {"/something", "/", `~* ^/something\/?(?<baseuri>.*)`, `
|
||||
rewrite /something/(.*) /$1 break;
|
||||
rewrite /something / break;
|
||||
proxy_pass http://upstream-name;
|
||||
subs_filter '<head(.*)>' '<head$1><base href="$scheme://$http_host/something/$baseuri">' r;
|
||||
subs_filter '<HEAD(.*)>' '<HEAD$1><base href="$scheme://$http_host/something/$baseuri">' r;
|
||||
`, true, ""},
|
||||
"redirect /end-with-slash/ to /not-root and rewrite": {"/end-with-slash/", "/not-root", `~* ^/end-with-slash/(?<baseuri>.*)`, `
|
||||
rewrite /end-with-slash/(.*) /not-root/$1 break;
|
||||
proxy_pass http://upstream-name;
|
||||
subs_filter '<head(.*)>' '<head$1><base href="$scheme://$http_host/end-with-slash/$baseuri">' r;
|
||||
subs_filter '<HEAD(.*)>' '<HEAD$1><base href="$scheme://$http_host/end-with-slash/$baseuri">' r;
|
||||
`, true, ""},
|
||||
"redirect /something-complex to /not-root and rewrite": {"/something-complex", "/not-root", `~* ^/something-complex\/?(?<baseuri>.*)`, `
|
||||
rewrite /something-complex/(.*) /not-root/$1 break;
|
||||
proxy_pass http://upstream-name;
|
||||
subs_filter '<head(.*)>' '<head$1><base href="$scheme://$http_host/something-complex/$baseuri">' r;
|
||||
subs_filter '<HEAD(.*)>' '<HEAD$1><base href="$scheme://$http_host/something-complex/$baseuri">' r;
|
||||
`, true, ""},
|
||||
"redirect /something to / and rewrite with specific scheme": {"/something", "/", `~* ^/something\/?(?<baseuri>.*)`, `
|
||||
rewrite /something/(.*) /$1 break;
|
||||
rewrite /something / break;
|
||||
proxy_pass http://upstream-name;
|
||||
subs_filter '<head(.*)>' '<head$1><base href="http://$http_host/something/$baseuri">' r;
|
||||
subs_filter '<HEAD(.*)>' '<HEAD$1><base href="http://$http_host/something/$baseuri">' r;
|
||||
`, true, "http"},
|
||||
}
|
||||
)
|
||||
|
||||
func TestFormatIP(t *testing.T) {
|
||||
cases := map[string]struct {
|
||||
Input, Output string
|
||||
}{
|
||||
"ipv4-localhost": {"127.0.0.1", "127.0.0.1"},
|
||||
"ipv4-internet": {"8.8.8.8", "8.8.8.8"},
|
||||
"ipv6-localhost": {"::1", "[::1]"},
|
||||
"ipv6-internet": {"2001:4860:4860::8888", "[2001:4860:4860::8888]"},
|
||||
"invalid-ip": {"nonsense", "nonsense"},
|
||||
"empty-ip": {"", ""},
|
||||
}
|
||||
for k, tc := range cases {
|
||||
res := formatIP(tc.Input)
|
||||
if res != tc.Output {
|
||||
t.Errorf("%s: called formatIp('%s'); expected '%v' but returned '%v'", k, tc.Input, tc.Output, res)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildLocation(t *testing.T) {
|
||||
for k, tc := range tmplFuncTestcases {
|
||||
loc := &ingress.Location{
|
||||
Path: tc.Path,
|
||||
Rewrite: rewrite.Redirect{Target: tc.Target, AddBaseURL: tc.AddBaseURL},
|
||||
}
|
||||
|
||||
newLoc := buildLocation(loc)
|
||||
if tc.Location != newLoc {
|
||||
t.Errorf("%s: expected '%v' but returned %v", k, tc.Location, newLoc)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildProxyPass(t *testing.T) {
|
||||
for k, tc := range tmplFuncTestcases {
|
||||
loc := &ingress.Location{
|
||||
Path: tc.Path,
|
||||
Rewrite: rewrite.Redirect{Target: tc.Target, AddBaseURL: tc.AddBaseURL, BaseURLScheme: tc.BaseURLScheme},
|
||||
Backend: "upstream-name",
|
||||
}
|
||||
|
||||
pp := buildProxyPass("", []*ingress.Backend{}, loc)
|
||||
if !strings.EqualFold(tc.ProxyPass, pp) {
|
||||
t.Errorf("%s: expected \n'%v'\nbut returned \n'%v'", k, tc.ProxyPass, pp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildAuthResponseHeaders(t *testing.T) {
|
||||
loc := &ingress.Location{
|
||||
ExternalAuth: authreq.External{ResponseHeaders: []string{"h1", "H-With-Caps-And-Dashes"}},
|
||||
}
|
||||
headers := buildAuthResponseHeaders(loc)
|
||||
expected := []string{
|
||||
"auth_request_set $authHeader0 $upstream_http_h1;",
|
||||
"proxy_set_header 'h1' $authHeader0;",
|
||||
"auth_request_set $authHeader1 $upstream_http_h_with_caps_and_dashes;",
|
||||
"proxy_set_header 'H-With-Caps-And-Dashes' $authHeader1;",
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(expected, headers) {
|
||||
t.Errorf("Expected \n'%v'\nbut returned \n'%v'", expected, headers)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTemplateWithData(t *testing.T) {
|
||||
pwd, _ := os.Getwd()
|
||||
f, err := os.Open(path.Join(pwd, "../tests/data/config.json"))
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error reading json file: %v", err)
|
||||
}
|
||||
defer f.Close()
|
||||
data, err := ioutil.ReadFile(f.Name())
|
||||
if err != nil {
|
||||
t.Error("unexpected error reading json file: ", err)
|
||||
}
|
||||
var dat config.TemplateConfig
|
||||
if err := json.Unmarshal(data, &dat); err != nil {
|
||||
t.Errorf("unexpected error unmarshalling json: %v", err)
|
||||
}
|
||||
if dat.ListenPorts == nil {
|
||||
dat.ListenPorts = &config.ListenPorts{}
|
||||
}
|
||||
tf, err := os.Open(path.Join(pwd, "../rootfs/etc/nginx/template/nginx.tmpl"))
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error reading json file: %v", err)
|
||||
}
|
||||
defer tf.Close()
|
||||
|
||||
ngxTpl, err := NewTemplate(tf.Name(), func() {})
|
||||
if err != nil {
|
||||
t.Errorf("invalid NGINX template: %v", err)
|
||||
}
|
||||
|
||||
_, err = ngxTpl.Write(dat)
|
||||
if err != nil {
|
||||
t.Errorf("invalid NGINX template: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkTemplateWithData(b *testing.B) {
|
||||
pwd, _ := os.Getwd()
|
||||
f, err := os.Open(path.Join(pwd, "../../test/data/config.json"))
|
||||
if err != nil {
|
||||
b.Errorf("unexpected error reading json file: %v", err)
|
||||
}
|
||||
defer f.Close()
|
||||
data, err := ioutil.ReadFile(f.Name())
|
||||
if err != nil {
|
||||
b.Error("unexpected error reading json file: ", err)
|
||||
}
|
||||
var dat config.TemplateConfig
|
||||
if err := json.Unmarshal(data, &dat); err != nil {
|
||||
b.Errorf("unexpected error unmarshalling json: %v", err)
|
||||
}
|
||||
|
||||
tf, err := os.Open(path.Join(pwd, "../../rootfs/etc/nginx/template/nginx.tmpl"))
|
||||
if err != nil {
|
||||
b.Errorf("unexpected error reading json file: %v", err)
|
||||
}
|
||||
defer tf.Close()
|
||||
|
||||
ngxTpl, err := NewTemplate(tf.Name(), func() {})
|
||||
if err != nil {
|
||||
b.Errorf("invalid NGINX template: %v", err)
|
||||
}
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
ngxTpl.Write(dat)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildDenyVariable(t *testing.T) {
|
||||
a := buildDenyVariable("host1.example.com_/.well-known/acme-challenge")
|
||||
b := buildDenyVariable("host1.example.com_/.well-known/acme-challenge")
|
||||
if !reflect.DeepEqual(a, b) {
|
||||
t.Errorf("Expected '%v' but returned '%v'", a, b)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildClientBodyBufferSize(t *testing.T) {
|
||||
a := isValidClientBodyBufferSize("1000")
|
||||
if a != true {
|
||||
t.Errorf("Expected '%v' but returned '%v'", true, a)
|
||||
}
|
||||
b := isValidClientBodyBufferSize("1000k")
|
||||
if b != true {
|
||||
t.Errorf("Expected '%v' but returned '%v'", true, b)
|
||||
}
|
||||
c := isValidClientBodyBufferSize("1000m")
|
||||
if c != true {
|
||||
t.Errorf("Expected '%v' but returned '%v'", true, c)
|
||||
}
|
||||
d := isValidClientBodyBufferSize("1000km")
|
||||
if d != false {
|
||||
t.Errorf("Expected '%v' but returned '%v'", false, d)
|
||||
}
|
||||
e := isValidClientBodyBufferSize("1000mk")
|
||||
if e != false {
|
||||
t.Errorf("Expected '%v' but returned '%v'", false, e)
|
||||
}
|
||||
f := isValidClientBodyBufferSize("1000kk")
|
||||
if f != false {
|
||||
t.Errorf("Expected '%v' but returned '%v'", false, f)
|
||||
}
|
||||
g := isValidClientBodyBufferSize("1000mm")
|
||||
if g != false {
|
||||
t.Errorf("Expected '%v' but returned '%v'", false, g)
|
||||
}
|
||||
h := isValidClientBodyBufferSize(nil)
|
||||
if h != false {
|
||||
t.Errorf("Expected '%v' but returned '%v'", false, h)
|
||||
}
|
||||
i := isValidClientBodyBufferSize("")
|
||||
if i != false {
|
||||
t.Errorf("Expected '%v' but returned '%v'", false, i)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsLocationAllowed(t *testing.T) {
|
||||
loc := ingress.Location{
|
||||
Denied: nil,
|
||||
}
|
||||
|
||||
isAllowed := isLocationAllowed(&loc)
|
||||
if !isAllowed {
|
||||
t.Errorf("Expected '%v' but returned '%v'", true, isAllowed)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildForwardedFor(t *testing.T) {
|
||||
inputStr := "X-Forwarded-For"
|
||||
outputStr := buildForwardedFor(inputStr)
|
||||
|
||||
validStr := "$http_x_forwarded_for"
|
||||
|
||||
if outputStr != validStr {
|
||||
t.Errorf("Expected '%v' but returned '%v'", validStr, outputStr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildResolvers(t *testing.T) {
|
||||
ipOne := net.ParseIP("192.0.0.1")
|
||||
ipTwo := net.ParseIP("2001:db8:1234:0000:0000:0000:0000:0000")
|
||||
ipList := []net.IP{ipOne, ipTwo}
|
||||
|
||||
validResolver := "resolver 192.0.0.1 [2001:db8:1234::] valid=30s;"
|
||||
resolver := buildResolvers(ipList)
|
||||
|
||||
if resolver != validResolver {
|
||||
t.Errorf("Expected '%v' but returned '%v'", validResolver, resolver)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildNextUpstream(t *testing.T) {
|
||||
nextUpstream := "timeout http_500 http_502 non_idempotent"
|
||||
validNextUpstream := "timeout http_500 http_502"
|
||||
|
||||
buildNextUpstream := buildNextUpstream(nextUpstream)
|
||||
|
||||
if buildNextUpstream != validNextUpstream {
|
||||
t.Errorf("Expected '%v' but returned '%v'", validNextUpstream, buildNextUpstream)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildRateLimit(t *testing.T) {
|
||||
loc := &ingress.Location{}
|
||||
|
||||
loc.RateLimit.Connections.Name = "con"
|
||||
loc.RateLimit.Connections.Limit = 1
|
||||
|
||||
loc.RateLimit.RPS.Name = "rps"
|
||||
loc.RateLimit.RPS.Limit = 1
|
||||
loc.RateLimit.RPS.Burst = 1
|
||||
|
||||
loc.RateLimit.RPM.Name = "rpm"
|
||||
loc.RateLimit.RPM.Limit = 2
|
||||
loc.RateLimit.RPM.Burst = 2
|
||||
|
||||
loc.RateLimit.LimitRateAfter = 1
|
||||
loc.RateLimit.LimitRate = 1
|
||||
|
||||
validLimits := []string{
|
||||
"limit_conn con 1;",
|
||||
"limit_req zone=rps burst=1 nodelay;",
|
||||
"limit_req zone=rpm burst=2 nodelay;",
|
||||
"limit_rate_after 1k;",
|
||||
"limit_rate 1k;",
|
||||
}
|
||||
|
||||
limits := buildRateLimit(loc)
|
||||
|
||||
for i, limit := range limits {
|
||||
if limit != validLimits[i] {
|
||||
t.Errorf("Expected '%v' but returned '%v'", validLimits, limits)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildAuthSignURL(t *testing.T) {
|
||||
cases := map[string]struct {
|
||||
Input, Output string
|
||||
}{
|
||||
"default url": {"http://google.com", "http://google.com?rd=$scheme://$http_host$request_uri"},
|
||||
"with random field": {"http://google.com?cat=0", "http://google.com?cat=0&rd=$scheme://$http_host$request_uri"},
|
||||
"with rd field": {"http://google.com?cat&rd=$request", "http://google.com?cat&rd=$request"},
|
||||
}
|
||||
for k, tc := range cases {
|
||||
res := buildAuthSignURL(tc.Input)
|
||||
if res != tc.Output {
|
||||
t.Errorf("%s: called buildAuthSignURL('%s'); expected '%v' but returned '%v'", k, tc.Input, tc.Output, res)
|
||||
}
|
||||
}
|
||||
}
|
||||
168
pkg/task/queue.go
Normal file
168
pkg/task/queue.go
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
/*
|
||||
Copyright 2015 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 task
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/golang/glog"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
"k8s.io/client-go/util/workqueue"
|
||||
)
|
||||
|
||||
var (
|
||||
keyFunc = cache.DeletionHandlingMetaNamespaceKeyFunc
|
||||
)
|
||||
|
||||
// Queue manages a time work queue through an independent worker that invokes the
|
||||
// given sync function for every work item inserted.
|
||||
// The queue uses an internal timestamp that allows the removal of certain elements
|
||||
// which timestamp is older than the last successful get operation.
|
||||
type Queue struct {
|
||||
// queue is the work queue the worker polls
|
||||
queue workqueue.RateLimitingInterface
|
||||
// sync is called for each item in the queue
|
||||
sync func(interface{}) error
|
||||
// workerDone is closed when the worker exits
|
||||
workerDone chan bool
|
||||
|
||||
fn func(obj interface{}) (interface{}, error)
|
||||
|
||||
lastSync int64
|
||||
}
|
||||
|
||||
// Element represents one item of the queue
|
||||
type Element struct {
|
||||
Key interface{}
|
||||
Timestamp int64
|
||||
}
|
||||
|
||||
// Run ...
|
||||
func (t *Queue) Run(period time.Duration, stopCh <-chan struct{}) {
|
||||
wait.Until(t.worker, period, stopCh)
|
||||
}
|
||||
|
||||
// Enqueue enqueues ns/name of the given api object in the task queue.
|
||||
func (t *Queue) Enqueue(obj interface{}) {
|
||||
if t.IsShuttingDown() {
|
||||
glog.Errorf("queue has been shutdown, failed to enqueue: %v", obj)
|
||||
return
|
||||
}
|
||||
|
||||
ts := time.Now().UnixNano()
|
||||
glog.V(3).Infof("queuing item %v", obj)
|
||||
key, err := t.fn(obj)
|
||||
if err != nil {
|
||||
glog.Errorf("%v", err)
|
||||
return
|
||||
}
|
||||
t.queue.Add(Element{
|
||||
Key: key,
|
||||
Timestamp: ts,
|
||||
})
|
||||
}
|
||||
|
||||
func (t *Queue) defaultKeyFunc(obj interface{}) (interface{}, error) {
|
||||
key, err := keyFunc(obj)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("could not get key for object %+v: %v", obj, err)
|
||||
}
|
||||
|
||||
return key, nil
|
||||
}
|
||||
|
||||
// worker processes work in the queue through sync.
|
||||
func (t *Queue) worker() {
|
||||
for {
|
||||
key, quit := t.queue.Get()
|
||||
if quit {
|
||||
if !isClosed(t.workerDone) {
|
||||
close(t.workerDone)
|
||||
}
|
||||
return
|
||||
}
|
||||
ts := time.Now().UnixNano()
|
||||
|
||||
item := key.(Element)
|
||||
if t.lastSync > item.Timestamp {
|
||||
glog.V(3).Infof("skipping %v sync (%v > %v)", item.Key, t.lastSync, item.Timestamp)
|
||||
t.queue.Forget(key)
|
||||
t.queue.Done(key)
|
||||
continue
|
||||
}
|
||||
|
||||
glog.V(3).Infof("syncing %v", item.Key)
|
||||
if err := t.sync(key); err != nil {
|
||||
glog.Warningf("requeuing %v, err %v", item.Key, err)
|
||||
t.queue.AddRateLimited(Element{
|
||||
Key: item.Key,
|
||||
Timestamp: time.Now().UnixNano(),
|
||||
})
|
||||
} else {
|
||||
t.queue.Forget(key)
|
||||
t.lastSync = ts
|
||||
}
|
||||
|
||||
t.queue.Done(key)
|
||||
}
|
||||
}
|
||||
|
||||
func isClosed(ch <-chan bool) bool {
|
||||
select {
|
||||
case <-ch:
|
||||
return true
|
||||
default:
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Shutdown shuts down the work queue and waits for the worker to ACK
|
||||
func (t *Queue) Shutdown() {
|
||||
t.queue.ShutDown()
|
||||
<-t.workerDone
|
||||
}
|
||||
|
||||
// IsShuttingDown returns if the method Shutdown was invoked
|
||||
func (t *Queue) IsShuttingDown() bool {
|
||||
return t.queue.ShuttingDown()
|
||||
}
|
||||
|
||||
// NewTaskQueue creates a new task queue with the given sync function.
|
||||
// The sync function is called for every element inserted into the queue.
|
||||
func NewTaskQueue(syncFn func(interface{}) error) *Queue {
|
||||
return NewCustomTaskQueue(syncFn, nil)
|
||||
}
|
||||
|
||||
// NewCustomTaskQueue ...
|
||||
func NewCustomTaskQueue(syncFn func(interface{}) error, fn func(interface{}) (interface{}, error)) *Queue {
|
||||
q := &Queue{
|
||||
queue: workqueue.NewRateLimitingQueue(workqueue.DefaultControllerRateLimiter()),
|
||||
sync: syncFn,
|
||||
workerDone: make(chan bool),
|
||||
fn: fn,
|
||||
}
|
||||
|
||||
if fn == nil {
|
||||
q.fn = q.defaultKeyFunc
|
||||
}
|
||||
|
||||
return q
|
||||
}
|
||||
159
pkg/task/queue_test.go
Normal file
159
pkg/task/queue_test.go
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
/*
|
||||
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 task
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
var sr uint32
|
||||
|
||||
type mockEnqueueObj struct {
|
||||
k string
|
||||
v string
|
||||
}
|
||||
|
||||
func mockSynFn(interface{}) error {
|
||||
// sr will be plus one times after enqueue
|
||||
atomic.AddUint32(&sr, 1)
|
||||
return nil
|
||||
}
|
||||
|
||||
func mockKeyFn(interface{}) (interface{}, error) {
|
||||
return mockEnqueueObj{
|
||||
k: "static_key",
|
||||
v: "static_value",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func mockErrorKeyFn(interface{}) (interface{}, error) {
|
||||
return nil, fmt.Errorf("failed to get key")
|
||||
}
|
||||
|
||||
func TestShutdown(t *testing.T) {
|
||||
q := NewTaskQueue(mockSynFn)
|
||||
stopCh := make(chan struct{})
|
||||
// run queue
|
||||
go q.Run(10*time.Second, stopCh)
|
||||
q.Shutdown()
|
||||
s := q.IsShuttingDown()
|
||||
if !s {
|
||||
t.Errorf("queue should be shutdown")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnqueueSuccess(t *testing.T) {
|
||||
// initialize result
|
||||
atomic.StoreUint32(&sr, 0)
|
||||
q := NewCustomTaskQueue(mockSynFn, mockKeyFn)
|
||||
stopCh := make(chan struct{})
|
||||
// run queue
|
||||
go q.Run(5*time.Second, stopCh)
|
||||
// mock object whichi will be enqueue
|
||||
mo := mockEnqueueObj{
|
||||
k: "testKey",
|
||||
v: "testValue",
|
||||
}
|
||||
q.Enqueue(mo)
|
||||
// wait for 'mockSynFn'
|
||||
time.Sleep(time.Millisecond * 10)
|
||||
if atomic.LoadUint32(&sr) != 1 {
|
||||
t.Errorf("sr should be 1, but is %d", sr)
|
||||
}
|
||||
|
||||
// shutdown queue before exit
|
||||
q.Shutdown()
|
||||
}
|
||||
|
||||
func TestEnqueueFailed(t *testing.T) {
|
||||
// initialize result
|
||||
atomic.StoreUint32(&sr, 0)
|
||||
q := NewCustomTaskQueue(mockSynFn, mockKeyFn)
|
||||
stopCh := make(chan struct{})
|
||||
// run queue
|
||||
go q.Run(5*time.Second, stopCh)
|
||||
// mock object whichi will be enqueue
|
||||
mo := mockEnqueueObj{
|
||||
k: "testKey",
|
||||
v: "testValue",
|
||||
}
|
||||
|
||||
// shutdown queue before enqueue
|
||||
q.Shutdown()
|
||||
// wait for shutdown
|
||||
time.Sleep(time.Millisecond * 10)
|
||||
q.Enqueue(mo)
|
||||
// wait for 'mockSynFn'
|
||||
time.Sleep(time.Millisecond * 10)
|
||||
// queue is shutdown, so mockSynFn should not be executed, so the result should be 0
|
||||
if atomic.LoadUint32(&sr) != 0 {
|
||||
t.Errorf("queue has been shutdown, so sr should be 0, but is %d", sr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnqueueKeyError(t *testing.T) {
|
||||
// initialize result
|
||||
atomic.StoreUint32(&sr, 0)
|
||||
q := NewCustomTaskQueue(mockSynFn, mockErrorKeyFn)
|
||||
stopCh := make(chan struct{})
|
||||
// run queue
|
||||
go q.Run(5*time.Second, stopCh)
|
||||
// mock object whichi will be enqueue
|
||||
mo := mockEnqueueObj{
|
||||
k: "testKey",
|
||||
v: "testValue",
|
||||
}
|
||||
|
||||
q.Enqueue(mo)
|
||||
// wait for 'mockSynFn'
|
||||
time.Sleep(time.Millisecond * 10)
|
||||
// key error, so the result should be 0
|
||||
if atomic.LoadUint32(&sr) != 0 {
|
||||
t.Errorf("error occurs while get key, so sr should be 0, but is %d", sr)
|
||||
}
|
||||
// shutdown queue before exit
|
||||
q.Shutdown()
|
||||
}
|
||||
|
||||
func TestSkipEnqueue(t *testing.T) {
|
||||
// initialize result
|
||||
atomic.StoreUint32(&sr, 0)
|
||||
q := NewCustomTaskQueue(mockSynFn, mockKeyFn)
|
||||
stopCh := make(chan struct{})
|
||||
// run queue
|
||||
go q.Run(time.Second, stopCh)
|
||||
// mock object whichi will be enqueue
|
||||
mo := mockEnqueueObj{
|
||||
k: "testKey",
|
||||
v: "testValue",
|
||||
}
|
||||
q.Enqueue(mo)
|
||||
q.Enqueue(mo)
|
||||
q.Enqueue(mo)
|
||||
q.Enqueue(mo)
|
||||
// wait for 'mockSynFn'
|
||||
time.Sleep(time.Millisecond * 10)
|
||||
if atomic.LoadUint32(&sr) != 1 {
|
||||
t.Errorf("sr should be 1, but is %d", sr)
|
||||
}
|
||||
|
||||
// shutdown queue before exit
|
||||
q.Shutdown()
|
||||
}
|
||||
76
pkg/watch/file_watcher.go
Normal file
76
pkg/watch/file_watcher.go
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
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 watch
|
||||
|
||||
import (
|
||||
"log"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/fsnotify.v1"
|
||||
)
|
||||
|
||||
// FileWatcher defines a watch over a file
|
||||
type FileWatcher struct {
|
||||
file string
|
||||
watcher *fsnotify.Watcher
|
||||
// onEvent callback to be invoked after the file being watched changes
|
||||
onEvent func()
|
||||
}
|
||||
|
||||
// NewFileWatcher creates a new FileWatcher
|
||||
func NewFileWatcher(file string, onEvent func()) (FileWatcher, error) {
|
||||
fw := FileWatcher{
|
||||
file: file,
|
||||
onEvent: onEvent,
|
||||
}
|
||||
err := fw.watch()
|
||||
return fw, err
|
||||
}
|
||||
|
||||
// Close ends the watch
|
||||
func (f *FileWatcher) Close() error {
|
||||
return f.watcher.Close()
|
||||
}
|
||||
|
||||
// watch creates a fsnotify watcher for a file and create of write events
|
||||
func (f *FileWatcher) watch() error {
|
||||
watcher, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f.watcher = watcher
|
||||
|
||||
dir, file := path.Split(f.file)
|
||||
go func(file string) {
|
||||
for {
|
||||
select {
|
||||
case event := <-watcher.Events:
|
||||
if (event.Op&fsnotify.Write == fsnotify.Write ||
|
||||
event.Op&fsnotify.Create == fsnotify.Create) &&
|
||||
strings.HasSuffix(event.Name, file) {
|
||||
f.onEvent()
|
||||
}
|
||||
case err := <-watcher.Errors:
|
||||
if err != nil {
|
||||
log.Printf("error watching file: %v\n", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}(file)
|
||||
return watcher.Add(dir)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue