Add validation to all annotations
This commit is contained in:
parent
49674631ef
commit
f8eb6b1879
89 changed files with 3457 additions and 569 deletions
|
@ -27,19 +27,44 @@ import (
|
|||
"k8s.io/ingress-nginx/internal/ingress/resolver"
|
||||
)
|
||||
|
||||
const (
|
||||
serverAliasAnnotation = "server-alias"
|
||||
)
|
||||
|
||||
var aliasAnnotation = parser.Annotation{
|
||||
Group: "alias",
|
||||
Annotations: parser.AnnotationFields{
|
||||
serverAliasAnnotation: {
|
||||
Validator: parser.ValidateArrayOfServerName,
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskLow, // Low, as it allows regexes but on a very limited set
|
||||
Documentation: `this annotation can be used to define additional server
|
||||
aliases for this Ingress`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
type alias struct {
|
||||
r resolver.Resolver
|
||||
r resolver.Resolver
|
||||
annotationConfig parser.Annotation
|
||||
}
|
||||
|
||||
// NewParser creates a new Alias annotation parser
|
||||
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
|
||||
return alias{r}
|
||||
return alias{
|
||||
r: r,
|
||||
annotationConfig: aliasAnnotation,
|
||||
}
|
||||
}
|
||||
|
||||
func (a alias) GetDocumentation() parser.AnnotationFields {
|
||||
return a.annotationConfig.Annotations
|
||||
}
|
||||
|
||||
// Parse parses the annotations contained in the ingress rule
|
||||
// used to add an alias to the provided hosts
|
||||
func (a alias) Parse(ing *networking.Ingress) (interface{}, error) {
|
||||
val, err := parser.GetStringAnnotation("server-alias", ing)
|
||||
val, err := parser.GetStringAnnotation(serverAliasAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
return []string{}, err
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@ import (
|
|||
"k8s.io/ingress-nginx/internal/ingress/resolver"
|
||||
)
|
||||
|
||||
var annotation = parser.GetAnnotationWithPrefix("server-alias")
|
||||
var annotation = parser.GetAnnotationWithPrefix(serverAliasAnnotation)
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
ap := NewParser(&resolver.Mock{})
|
||||
|
@ -38,14 +38,16 @@ func TestParse(t *testing.T) {
|
|||
testCases := []struct {
|
||||
annotations map[string]string
|
||||
expected []string
|
||||
wantErr bool
|
||||
}{
|
||||
{map[string]string{annotation: "a.com, b.com, , c.com"}, []string{"a.com", "b.com", "c.com"}},
|
||||
{map[string]string{annotation: "www.example.com"}, []string{"www.example.com"}},
|
||||
{map[string]string{annotation: "*.example.com,www.example.*"}, []string{"*.example.com", "www.example.*"}},
|
||||
{map[string]string{annotation: `~^www\d+\.example\.com$`}, []string{`~^www\d+\.example\.com$`}},
|
||||
{map[string]string{annotation: ""}, []string{}},
|
||||
{map[string]string{}, []string{}},
|
||||
{nil, []string{}},
|
||||
{map[string]string{annotation: "a.com, b.com, , c.com"}, []string{"a.com", "b.com", "c.com"}, false},
|
||||
{map[string]string{annotation: "www.example.com"}, []string{"www.example.com"}, false},
|
||||
{map[string]string{annotation: "*.example.com,www.example.*"}, []string{"*.example.com", "www.example.*"}, false},
|
||||
{map[string]string{annotation: `~^www\d+\.example\.com$`}, []string{`~^www\d+\.example\.com$`}, false},
|
||||
{map[string]string{annotation: `www.xpto;lala`}, []string{}, true},
|
||||
{map[string]string{annotation: ""}, []string{}, true},
|
||||
{map[string]string{}, []string{}, true},
|
||||
{nil, []string{}, true},
|
||||
}
|
||||
|
||||
ing := &networking.Ingress{
|
||||
|
@ -58,7 +60,10 @@ func TestParse(t *testing.T) {
|
|||
|
||||
for _, testCase := range testCases {
|
||||
ing.SetAnnotations(testCase.annotations)
|
||||
result, _ := ap.Parse(ing)
|
||||
result, err := ap.Parse(ing)
|
||||
if (err != nil) != testCase.wantErr {
|
||||
t.Errorf("ParseAliasAnnotation() annotation: %s, error = %v, wantErr %v", testCase.annotations, err, testCase.wantErr)
|
||||
}
|
||||
if !reflect.DeepEqual(result, testCase.expected) {
|
||||
t.Errorf("expected %v but returned %v, annotations: %s", testCase.expected, result, testCase.annotations)
|
||||
}
|
||||
|
|
|
@ -44,8 +44,8 @@ import (
|
|||
"k8s.io/ingress-nginx/internal/ingress/annotations/fastcgi"
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/globalratelimit"
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/http2pushpreload"
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/ipallowlist"
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/ipdenylist"
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/ipwhitelist"
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/loadbalancing"
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/log"
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/mirror"
|
||||
|
@ -109,7 +109,6 @@ type Ingress struct {
|
|||
UpstreamHashBy upstreamhashby.Config
|
||||
LoadBalancing string
|
||||
UpstreamVhost string
|
||||
Whitelist ipwhitelist.SourceRange
|
||||
Denylist ipdenylist.SourceRange
|
||||
XForwardedPrefix string
|
||||
SSLCipher sslcipher.Config
|
||||
|
@ -117,6 +116,7 @@ type Ingress struct {
|
|||
ModSecurity modsecurity.Config
|
||||
Mirror mirror.Config
|
||||
StreamSnippet string
|
||||
Allowlist ipallowlist.SourceRange
|
||||
}
|
||||
|
||||
// Extractor defines the annotation parsers to be used in the extraction of annotations
|
||||
|
@ -159,7 +159,7 @@ func NewAnnotationExtractor(cfg resolver.Resolver) Extractor {
|
|||
"UpstreamHashBy": upstreamhashby.NewParser(cfg),
|
||||
"LoadBalancing": loadbalancing.NewParser(cfg),
|
||||
"UpstreamVhost": upstreamvhost.NewParser(cfg),
|
||||
"Whitelist": ipwhitelist.NewParser(cfg),
|
||||
"Allowlist": ipallowlist.NewParser(cfg),
|
||||
"Denylist": ipdenylist.NewParser(cfg),
|
||||
"XForwardedPrefix": xforwardedprefix.NewParser(cfg),
|
||||
"SSLCipher": sslcipher.NewParser(cfg),
|
||||
|
@ -173,7 +173,7 @@ func NewAnnotationExtractor(cfg resolver.Resolver) Extractor {
|
|||
}
|
||||
|
||||
// Extract extracts the annotations from an Ingress
|
||||
func (e Extractor) Extract(ing *networking.Ingress) *Ingress {
|
||||
func (e Extractor) Extract(ing *networking.Ingress) (*Ingress, error) {
|
||||
pia := &Ingress{
|
||||
ObjectMeta: ing.ObjectMeta,
|
||||
}
|
||||
|
@ -183,6 +183,10 @@ func (e Extractor) Extract(ing *networking.Ingress) *Ingress {
|
|||
val, err := annotationParser.Parse(ing)
|
||||
klog.V(5).InfoS("Parsing Ingress annotation", "name", name, "ingress", klog.KObj(ing), "value", val)
|
||||
if err != nil {
|
||||
if errors.IsValidationError(err) {
|
||||
klog.ErrorS(err, "ingress contains invalid annotation value")
|
||||
return nil, err
|
||||
}
|
||||
if errors.IsMissingAnnotations(err) {
|
||||
continue
|
||||
}
|
||||
|
@ -220,5 +224,5 @@ func (e Extractor) Extract(ing *networking.Ingress) *Ingress {
|
|||
klog.ErrorS(err, "unexpected error merging extracted annotations")
|
||||
}
|
||||
|
||||
return pia
|
||||
return pia, nil
|
||||
}
|
||||
|
|
|
@ -134,8 +134,11 @@ func TestSSLPassthrough(t *testing.T) {
|
|||
|
||||
for _, foo := range fooAnns {
|
||||
ing.SetAnnotations(foo.annotations)
|
||||
r := ec.Extract(ing).SSLPassthrough
|
||||
if r != foo.er {
|
||||
r, err := ec.Extract(ing)
|
||||
if err != nil {
|
||||
t.Errorf("Errors should be null: %v", err)
|
||||
}
|
||||
if r.SSLPassthrough != foo.er {
|
||||
t.Errorf("Returned %v but expected %v", r, foo.er)
|
||||
}
|
||||
}
|
||||
|
@ -158,8 +161,11 @@ func TestUpstreamHashBy(t *testing.T) {
|
|||
|
||||
for _, foo := range fooAnns {
|
||||
ing.SetAnnotations(foo.annotations)
|
||||
r := ec.Extract(ing).UpstreamHashBy.UpstreamHashBy
|
||||
if r != foo.er {
|
||||
r, err := ec.Extract(ing)
|
||||
if err != nil {
|
||||
t.Errorf("error should be null: %v", err)
|
||||
}
|
||||
if r.UpstreamHashBy.UpstreamHashBy != foo.er {
|
||||
t.Errorf("Returned %v but expected %v", r, foo.er)
|
||||
}
|
||||
}
|
||||
|
@ -185,7 +191,11 @@ func TestAffinitySession(t *testing.T) {
|
|||
|
||||
for _, foo := range fooAnns {
|
||||
ing.SetAnnotations(foo.annotations)
|
||||
r := ec.Extract(ing).SessionAffinity
|
||||
rann, err := ec.Extract(ing)
|
||||
if err != nil {
|
||||
t.Errorf("error should be null: %v", err)
|
||||
}
|
||||
r := rann.SessionAffinity
|
||||
t.Logf("Testing pass %v %v", foo.affinitytype, foo.cookiename)
|
||||
|
||||
if r.Type != foo.affinitytype {
|
||||
|
@ -228,7 +238,11 @@ func TestCors(t *testing.T) {
|
|||
|
||||
for _, foo := range fooAnns {
|
||||
ing.SetAnnotations(foo.annotations)
|
||||
r := ec.Extract(ing).CorsConfig
|
||||
rann, err := ec.Extract(ing)
|
||||
if err != nil {
|
||||
t.Errorf("error should be null: %v", err)
|
||||
}
|
||||
r := rann.CorsConfig
|
||||
t.Logf("Testing pass %v %v %v %v %v", foo.corsenabled, foo.methods, foo.headers, foo.origin, foo.credentials)
|
||||
|
||||
if r.CorsEnabled != foo.corsenabled {
|
||||
|
@ -277,7 +291,11 @@ func TestCustomHTTPErrors(t *testing.T) {
|
|||
|
||||
for _, foo := range fooAnns {
|
||||
ing.SetAnnotations(foo.annotations)
|
||||
r := ec.Extract(ing).CustomHTTPErrors
|
||||
rann, err := ec.Extract(ing)
|
||||
if err != nil {
|
||||
t.Errorf("error should be null: %v", err)
|
||||
}
|
||||
r := rann.CustomHTTPErrors
|
||||
|
||||
// Check that expected codes were created
|
||||
for i := range foo.er {
|
||||
|
|
|
@ -32,13 +32,56 @@ import (
|
|||
"k8s.io/ingress-nginx/pkg/util/file"
|
||||
)
|
||||
|
||||
const (
|
||||
authSecretTypeAnnotation = "auth-secret-type" //#nosec G101
|
||||
authRealmAnnotation = "auth-realm"
|
||||
authTypeAnnotation = "auth-type"
|
||||
// This should be exported as it is imported by other packages
|
||||
AuthSecretAnnotation = "auth-secret" //#nosec G101
|
||||
)
|
||||
|
||||
var (
|
||||
authTypeRegex = regexp.MustCompile(`basic|digest`)
|
||||
authTypeRegex = regexp.MustCompile(`basic|digest`)
|
||||
authSecretTypeRegex = regexp.MustCompile(`auth-file|auth-map`)
|
||||
|
||||
// AuthDirectory default directory used to store files
|
||||
// to authenticate request
|
||||
AuthDirectory = "/etc/ingress-controller/auth"
|
||||
)
|
||||
|
||||
var AuthSecretConfig = parser.AnnotationConfig{
|
||||
Validator: parser.ValidateRegex(*parser.BasicCharsRegex, true),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskMedium, // Medium as it allows a subset of chars
|
||||
Documentation: `This annotation defines the name of the Secret that contains the usernames and passwords which are granted access to the paths defined in the Ingress rules. `,
|
||||
}
|
||||
|
||||
var authSecretAnnotations = parser.Annotation{
|
||||
Group: "authentication",
|
||||
Annotations: parser.AnnotationFields{
|
||||
AuthSecretAnnotation: AuthSecretConfig,
|
||||
authSecretTypeAnnotation: {
|
||||
Validator: parser.ValidateRegex(*authSecretTypeRegex, true),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation what is the format of auth-secret value. Can be "auth-file" that defines the content of an htpasswd file, or "auth-map" where each key
|
||||
is a user and each value is the password.`,
|
||||
},
|
||||
authRealmAnnotation: {
|
||||
Validator: parser.ValidateRegex(*parser.CharsWithSpace, false),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskMedium, // Medium as it allows a subset of chars
|
||||
Documentation: `This annotation defines the realm (message) that should be shown to user when authentication is requested.`,
|
||||
},
|
||||
authTypeAnnotation: {
|
||||
Validator: parser.ValidateRegex(*authTypeRegex, true),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation defines the basic authentication type. Should be "basic" or "digest"`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const (
|
||||
fileAuth = "auth-file"
|
||||
mapAuth = "auth-map"
|
||||
|
@ -85,13 +128,18 @@ func (bd1 *Config) Equal(bd2 *Config) bool {
|
|||
}
|
||||
|
||||
type auth struct {
|
||||
r resolver.Resolver
|
||||
authDirectory string
|
||||
r resolver.Resolver
|
||||
authDirectory string
|
||||
annotationConfig parser.Annotation
|
||||
}
|
||||
|
||||
// NewParser creates a new authentication annotation parser
|
||||
func NewParser(authDirectory string, r resolver.Resolver) parser.IngressAnnotation {
|
||||
return auth{r, authDirectory}
|
||||
return auth{
|
||||
r: r,
|
||||
authDirectory: authDirectory,
|
||||
annotationConfig: authSecretAnnotations,
|
||||
}
|
||||
}
|
||||
|
||||
// Parse parses the annotations contained in the ingress
|
||||
|
@ -99,7 +147,7 @@ func NewParser(authDirectory string, r resolver.Resolver) parser.IngressAnnotati
|
|||
// and generated an htpasswd compatible file to be used as source
|
||||
// during the authentication process
|
||||
func (a auth) Parse(ing *networking.Ingress) (interface{}, error) {
|
||||
at, err := parser.GetStringAnnotation("auth-type", ing)
|
||||
at, err := parser.GetStringAnnotation(authTypeAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -109,12 +157,15 @@ func (a auth) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
}
|
||||
|
||||
var secretType string
|
||||
secretType, err = parser.GetStringAnnotation("auth-secret-type", ing)
|
||||
secretType, err = parser.GetStringAnnotation(authSecretTypeAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
if ing_errors.IsValidationError(err) {
|
||||
return nil, err
|
||||
}
|
||||
secretType = fileAuth
|
||||
}
|
||||
|
||||
s, err := parser.GetStringAnnotation("auth-secret", ing)
|
||||
s, err := parser.GetStringAnnotation(AuthSecretAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
return nil, ing_errors.LocationDenied{
|
||||
Reason: fmt.Errorf("error reading secret name from annotation: %w", err),
|
||||
|
@ -131,6 +182,12 @@ func (a auth) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
if sns == "" {
|
||||
sns = ing.Namespace
|
||||
}
|
||||
// We don't accept different namespaces for secrets.
|
||||
if sns != ing.Namespace {
|
||||
return nil, ing_errors.LocationDenied{
|
||||
Reason: fmt.Errorf("cross namespace usage of secrets is not allowed"),
|
||||
}
|
||||
}
|
||||
|
||||
name := fmt.Sprintf("%v/%v", sns, sname)
|
||||
secret, err := a.r.GetSecret(name)
|
||||
|
@ -140,7 +197,10 @@ func (a auth) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
}
|
||||
}
|
||||
|
||||
realm, _ := parser.GetStringAnnotation("auth-realm", ing)
|
||||
realm, err := parser.GetStringAnnotation(authRealmAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if ing_errors.IsValidationError(err) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
passFilename := fmt.Sprintf("%v/%v-%v-%v.passwd", a.authDirectory, ing.GetNamespace(), ing.UID, secret.UID)
|
||||
|
||||
|
@ -210,3 +270,7 @@ func dumpSecretAuthMap(filename string, secret *api.Secret) error {
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a auth) GetDocumentation() parser.AnnotationFields {
|
||||
return a.annotationConfig.Annotations
|
||||
}
|
||||
|
|
|
@ -106,13 +106,72 @@ func TestIngressAuthBadAuthType(t *testing.T) {
|
|||
ing := buildIngress()
|
||||
|
||||
data := map[string]string{}
|
||||
data[parser.GetAnnotationWithPrefix("auth-type")] = "invalid"
|
||||
data[parser.GetAnnotationWithPrefix(authTypeAnnotation)] = "invalid"
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
_, dir, _ := dummySecretContent(t)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
expected := ing_errors.NewLocationDenied("invalid authentication type")
|
||||
expected := ing_errors.NewValidationError("nginx.ingress.kubernetes.io/auth-type")
|
||||
_, err := NewParser(dir, &mockSecret{}).Parse(ing)
|
||||
if err.Error() != expected.Error() {
|
||||
t.Errorf("expected '%v' but got '%v'", expected, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIngressInvalidRealm(t *testing.T) {
|
||||
ing := buildIngress()
|
||||
|
||||
data := map[string]string{}
|
||||
data[parser.GetAnnotationWithPrefix(authTypeAnnotation)] = "basic"
|
||||
data[parser.GetAnnotationWithPrefix(authRealmAnnotation)] = "something weird ; location trying to { break }"
|
||||
data[parser.GetAnnotationWithPrefix(AuthSecretAnnotation)] = "demo-secret"
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
_, dir, _ := dummySecretContent(t)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
expected := ing_errors.NewValidationError("nginx.ingress.kubernetes.io/auth-realm")
|
||||
_, err := NewParser(dir, &mockSecret{}).Parse(ing)
|
||||
if err.Error() != expected.Error() {
|
||||
t.Errorf("expected '%v' but got '%v'", expected, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIngressInvalidDifferentNamespace(t *testing.T) {
|
||||
ing := buildIngress()
|
||||
|
||||
data := map[string]string{}
|
||||
data[parser.GetAnnotationWithPrefix(authTypeAnnotation)] = "basic"
|
||||
data[parser.GetAnnotationWithPrefix(AuthSecretAnnotation)] = "otherns/demo-secret"
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
_, dir, _ := dummySecretContent(t)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
expected := ing_errors.LocationDenied{
|
||||
Reason: errors.New("cross namespace usage of secrets is not allowed"),
|
||||
}
|
||||
_, err := NewParser(dir, &mockSecret{}).Parse(ing)
|
||||
if err.Error() != expected.Error() {
|
||||
t.Errorf("expected '%v' but got '%v'", expected, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIngressInvalidSecretName(t *testing.T) {
|
||||
ing := buildIngress()
|
||||
|
||||
data := map[string]string{}
|
||||
data[parser.GetAnnotationWithPrefix(authTypeAnnotation)] = "basic"
|
||||
data[parser.GetAnnotationWithPrefix(AuthSecretAnnotation)] = "demo-secret;xpto"
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
_, dir, _ := dummySecretContent(t)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
expected := ing_errors.LocationDenied{
|
||||
Reason: errors.New("error reading secret name from annotation: annotation nginx.ingress.kubernetes.io/auth-secret contains invalid value"),
|
||||
}
|
||||
_, err := NewParser(dir, &mockSecret{}).Parse(ing)
|
||||
if err.Error() != expected.Error() {
|
||||
t.Errorf("expected '%v' but got '%v'", expected, err)
|
||||
|
@ -123,7 +182,7 @@ func TestInvalidIngressAuthNoSecret(t *testing.T) {
|
|||
ing := buildIngress()
|
||||
|
||||
data := map[string]string{}
|
||||
data[parser.GetAnnotationWithPrefix("auth-type")] = "basic"
|
||||
data[parser.GetAnnotationWithPrefix(authTypeAnnotation)] = "basic"
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
_, dir, _ := dummySecretContent(t)
|
||||
|
@ -142,9 +201,9 @@ func TestIngressAuth(t *testing.T) {
|
|||
ing := buildIngress()
|
||||
|
||||
data := map[string]string{}
|
||||
data[parser.GetAnnotationWithPrefix("auth-type")] = "basic"
|
||||
data[parser.GetAnnotationWithPrefix("auth-secret")] = "demo-secret"
|
||||
data[parser.GetAnnotationWithPrefix("auth-realm")] = "-realm-"
|
||||
data[parser.GetAnnotationWithPrefix(authTypeAnnotation)] = "basic"
|
||||
data[parser.GetAnnotationWithPrefix(AuthSecretAnnotation)] = "demo-secret"
|
||||
data[parser.GetAnnotationWithPrefix(authRealmAnnotation)] = "-realm-"
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
_, dir, _ := dummySecretContent(t)
|
||||
|
@ -173,9 +232,9 @@ func TestIngressAuthWithoutSecret(t *testing.T) {
|
|||
ing := buildIngress()
|
||||
|
||||
data := map[string]string{}
|
||||
data[parser.GetAnnotationWithPrefix("auth-type")] = "basic"
|
||||
data[parser.GetAnnotationWithPrefix("auth-secret")] = "invalid-secret"
|
||||
data[parser.GetAnnotationWithPrefix("auth-realm")] = "-realm-"
|
||||
data[parser.GetAnnotationWithPrefix(authTypeAnnotation)] = "basic"
|
||||
data[parser.GetAnnotationWithPrefix(AuthSecretAnnotation)] = "invalid-secret"
|
||||
data[parser.GetAnnotationWithPrefix(authRealmAnnotation)] = "-realm-"
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
_, dir, _ := dummySecretContent(t)
|
||||
|
@ -191,10 +250,10 @@ func TestIngressAuthInvalidSecretKey(t *testing.T) {
|
|||
ing := buildIngress()
|
||||
|
||||
data := map[string]string{}
|
||||
data[parser.GetAnnotationWithPrefix("auth-type")] = "basic"
|
||||
data[parser.GetAnnotationWithPrefix("auth-secret")] = "demo-secret"
|
||||
data[parser.GetAnnotationWithPrefix("auth-secret-type")] = "invalid-type"
|
||||
data[parser.GetAnnotationWithPrefix("auth-realm")] = "-realm-"
|
||||
data[parser.GetAnnotationWithPrefix(authTypeAnnotation)] = "basic"
|
||||
data[parser.GetAnnotationWithPrefix(AuthSecretAnnotation)] = "demo-secret"
|
||||
data[parser.GetAnnotationWithPrefix(authSecretTypeAnnotation)] = "invalid-type"
|
||||
data[parser.GetAnnotationWithPrefix(authRealmAnnotation)] = "-realm-"
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
_, dir, _ := dummySecretContent(t)
|
||||
|
|
|
@ -24,13 +24,127 @@ import (
|
|||
"k8s.io/klog/v2"
|
||||
|
||||
networking "k8s.io/api/networking/v1"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/parser"
|
||||
"k8s.io/ingress-nginx/internal/ingress/errors"
|
||||
ing_errors "k8s.io/ingress-nginx/internal/ingress/errors"
|
||||
"k8s.io/ingress-nginx/internal/ingress/resolver"
|
||||
"k8s.io/ingress-nginx/pkg/util/sets"
|
||||
)
|
||||
|
||||
const (
|
||||
authReqURLAnnotation = "auth-url"
|
||||
authReqMethodAnnotation = "auth-method"
|
||||
authReqSigninAnnotation = "auth-signin"
|
||||
authReqSigninRedirParamAnnotation = "auth-signin-redirect-param"
|
||||
authReqSnippetAnnotation = "auth-snippet"
|
||||
authReqCacheKeyAnnotation = "auth-cache-key"
|
||||
authReqKeepaliveAnnotation = "auth-keepalive"
|
||||
authReqKeepaliveRequestsAnnotation = "auth-keepalive-requests"
|
||||
authReqKeepaliveTimeout = "auth-keepalive-timeout"
|
||||
authReqCacheDuration = "auth-cache-duration"
|
||||
authReqResponseHeadersAnnotation = "auth-response-headers"
|
||||
authReqProxySetHeadersAnnotation = "auth-proxy-set-headers"
|
||||
authReqRequestRedirectAnnotation = "auth-request-redirect"
|
||||
authReqAlwaysSetCookieAnnotation = "auth-always-set-cookie"
|
||||
|
||||
// This should be exported as it is imported by other packages
|
||||
AuthSecretAnnotation = "auth-secret"
|
||||
)
|
||||
|
||||
var authReqAnnotations = parser.Annotation{
|
||||
Group: "authentication",
|
||||
Annotations: parser.AnnotationFields{
|
||||
authReqURLAnnotation: {
|
||||
Validator: parser.ValidateRegex(*parser.URLWithNginxVariableRegex, true),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskHigh,
|
||||
Documentation: `This annotation allows to indicate the URL where the HTTP request should be sent`,
|
||||
},
|
||||
authReqMethodAnnotation: {
|
||||
Validator: parser.ValidateRegex(*methodsRegex, true),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation allows to specify the HTTP method to use`,
|
||||
},
|
||||
authReqSigninAnnotation: {
|
||||
Validator: parser.ValidateRegex(*parser.URLWithNginxVariableRegex, true),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskHigh,
|
||||
Documentation: `This annotation allows to specify the location of the error page`,
|
||||
},
|
||||
authReqSigninRedirParamAnnotation: {
|
||||
Validator: parser.ValidateRegex(*parser.URLIsValidRegex, true),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskMedium,
|
||||
Documentation: `This annotation allows to specify the URL parameter in the error page which should contain the original URL for a failed signin request`,
|
||||
},
|
||||
authReqSnippetAnnotation: {
|
||||
Validator: parser.ValidateNull,
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskCritical,
|
||||
Documentation: `This annotation allows to specify a custom snippet to use with external authentication`,
|
||||
},
|
||||
authReqCacheKeyAnnotation: {
|
||||
Validator: parser.ValidateRegex(*parser.NGINXVariable, true),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskMedium,
|
||||
Documentation: `This annotation enables caching for auth requests.`,
|
||||
},
|
||||
authReqKeepaliveAnnotation: {
|
||||
Validator: parser.ValidateInt,
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation specifies the maximum number of keepalive connections to auth-url. Only takes effect when no variables are used in the host part of the URL`,
|
||||
},
|
||||
authReqKeepaliveRequestsAnnotation: {
|
||||
Validator: parser.ValidateInt,
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation defines the maximum number of requests that can be served through one keepalive connection`,
|
||||
},
|
||||
authReqKeepaliveTimeout: {
|
||||
Validator: parser.ValidateInt,
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation specifies a duration in seconds which an idle keepalive connection to an upstream server will stay open`,
|
||||
},
|
||||
authReqCacheDuration: {
|
||||
Validator: parser.ValidateRegex(*parser.ExtendedChars, false),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskMedium,
|
||||
Documentation: `This annotation allows to specify a caching time for auth responses based on their response codes, e.g. 200 202 30m`,
|
||||
},
|
||||
authReqResponseHeadersAnnotation: {
|
||||
Validator: parser.ValidateRegex(*parser.HeadersVariable, true),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskMedium,
|
||||
Documentation: `This annotation sets the headers to pass to backend once authentication request completes. They should be separated by comma.`,
|
||||
},
|
||||
authReqProxySetHeadersAnnotation: {
|
||||
Validator: parser.ValidateRegex(*parser.BasicCharsRegex, true),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskMedium,
|
||||
Documentation: `This annotation sets the name of a ConfigMap that specifies headers to pass to the authentication service.
|
||||
Only ConfigMaps on the same namespace are allowed`,
|
||||
},
|
||||
authReqRequestRedirectAnnotation: {
|
||||
Validator: parser.ValidateRegex(*parser.URLIsValidRegex, true),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskMedium,
|
||||
Documentation: `This annotation allows to specify the X-Auth-Request-Redirect header value`,
|
||||
},
|
||||
authReqAlwaysSetCookieAnnotation: {
|
||||
Validator: parser.ValidateBool,
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation enables setting a cookie returned by auth request.
|
||||
By default, the cookie will be set only if an upstream reports with the code 200, 201, 204, 206, 301, 302, 303, 304, 307, or 308`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Config returns external authentication configuration for an Ingress rule
|
||||
type Config struct {
|
||||
URL string `json:"url"`
|
||||
|
@ -121,7 +235,7 @@ func (e1 *Config) Equal(e2 *Config) bool {
|
|||
}
|
||||
|
||||
var (
|
||||
methods = []string{"GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "CONNECT", "OPTIONS", "TRACE"}
|
||||
methodsRegex = regexp.MustCompile("(GET|HEAD|POST|PUT|PATCH|DELETE|CONNECT|OPTIONS|TRACE)")
|
||||
headerRegexp = regexp.MustCompile(`^[a-zA-Z\d\-_]+$`)
|
||||
statusCodeRegex = regexp.MustCompile(`^[\d]{3}$`)
|
||||
durationRegex = regexp.MustCompile(`^[\d]+(ms|s|m|h|d|w|M|y)$`) // see http://nginx.org/en/docs/syntax.html
|
||||
|
@ -129,16 +243,7 @@ var (
|
|||
|
||||
// ValidMethod checks is the provided string a valid HTTP method
|
||||
func ValidMethod(method string) bool {
|
||||
if len(method) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, m := range methods {
|
||||
if method == m {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
return methodsRegex.MatchString(method)
|
||||
}
|
||||
|
||||
// ValidHeader checks is the provided string satisfies the header's name regex
|
||||
|
@ -173,19 +278,23 @@ func ValidCacheDuration(duration string) bool {
|
|||
}
|
||||
|
||||
type authReq struct {
|
||||
r resolver.Resolver
|
||||
r resolver.Resolver
|
||||
annotationConfig parser.Annotation
|
||||
}
|
||||
|
||||
// NewParser creates a new authentication request annotation parser
|
||||
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
|
||||
return authReq{r}
|
||||
return authReq{
|
||||
r: r,
|
||||
annotationConfig: authReqAnnotations,
|
||||
}
|
||||
}
|
||||
|
||||
// ParseAnnotations parses the annotations contained in the ingress
|
||||
// rule used to use an Config URL as source for authentication
|
||||
func (a authReq) Parse(ing *networking.Ingress) (interface{}, error) {
|
||||
// Required Parameters
|
||||
urlString, err := parser.GetStringAnnotation("auth-url", ing)
|
||||
urlString, err := parser.GetStringAnnotation(authReqURLAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -195,33 +304,44 @@ func (a authReq) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
return nil, ing_errors.LocationDenied{Reason: fmt.Errorf("could not parse auth-url annotation: %v", err)}
|
||||
}
|
||||
|
||||
authMethod, _ := parser.GetStringAnnotation("auth-method", ing)
|
||||
if len(authMethod) != 0 && !ValidMethod(authMethod) {
|
||||
return nil, ing_errors.NewLocationDenied("invalid HTTP method")
|
||||
authMethod, err := parser.GetStringAnnotation(authReqMethodAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
if errors.IsValidationError(err) {
|
||||
return nil, ing_errors.NewLocationDenied("invalid HTTP method")
|
||||
}
|
||||
}
|
||||
|
||||
// Optional Parameters
|
||||
signIn, err := parser.GetStringAnnotation("auth-signin", ing)
|
||||
signIn, err := parser.GetStringAnnotation(authReqSigninAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
if errors.IsValidationError(err) {
|
||||
klog.Warningf("%s value is invalid: %s", authReqSigninAnnotation, err)
|
||||
}
|
||||
klog.V(3).InfoS("auth-signin annotation is undefined and will not be set")
|
||||
}
|
||||
|
||||
signInRedirectParam, err := parser.GetStringAnnotation("auth-signin-redirect-param", ing)
|
||||
signInRedirectParam, err := parser.GetStringAnnotation(authReqSigninRedirParamAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
if errors.IsValidationError(err) {
|
||||
klog.Warningf("%s value is invalid: %s", authReqSigninRedirParamAnnotation, err)
|
||||
}
|
||||
klog.V(3).Infof("auth-signin-redirect-param annotation is undefined and will not be set")
|
||||
}
|
||||
|
||||
authSnippet, err := parser.GetStringAnnotation("auth-snippet", ing)
|
||||
authSnippet, err := parser.GetStringAnnotation(authReqSnippetAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
klog.V(3).InfoS("auth-snippet annotation is undefined and will not be set")
|
||||
}
|
||||
|
||||
authCacheKey, err := parser.GetStringAnnotation("auth-cache-key", ing)
|
||||
authCacheKey, err := parser.GetStringAnnotation(authReqCacheKeyAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
if errors.IsValidationError(err) {
|
||||
klog.Warningf("%s value is invalid: %s", authReqCacheKeyAnnotation, err)
|
||||
}
|
||||
klog.V(3).InfoS("auth-cache-key annotation is undefined and will not be set")
|
||||
}
|
||||
|
||||
keepaliveConnections, err := parser.GetIntAnnotation("auth-keepalive", ing)
|
||||
keepaliveConnections, err := parser.GetIntAnnotation(authReqKeepaliveAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
klog.V(3).InfoS("auth-keepalive annotation is undefined and will be set to its default value")
|
||||
keepaliveConnections = defaultKeepaliveConnections
|
||||
|
@ -238,9 +358,9 @@ func (a authReq) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
}
|
||||
}
|
||||
|
||||
keepaliveRequests, err := parser.GetIntAnnotation("auth-keepalive-requests", ing)
|
||||
keepaliveRequests, err := parser.GetIntAnnotation(authReqKeepaliveRequestsAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
klog.V(3).InfoS("auth-keepalive-requests annotation is undefined and will be set to its default value")
|
||||
klog.V(3).InfoS("auth-keepalive-requests annotation is undefined or invalid and will be set to its default value")
|
||||
keepaliveRequests = defaultKeepaliveRequests
|
||||
}
|
||||
if keepaliveRequests <= 0 {
|
||||
|
@ -248,7 +368,7 @@ func (a authReq) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
keepaliveConnections = 0
|
||||
}
|
||||
|
||||
keepaliveTimeout, err := parser.GetIntAnnotation("auth-keepalive-timeout", ing)
|
||||
keepaliveTimeout, err := parser.GetIntAnnotation(authReqKeepaliveTimeout, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
klog.V(3).InfoS("auth-keepalive-timeout annotation is undefined and will be set to its default value")
|
||||
keepaliveTimeout = defaultKeepaliveTimeout
|
||||
|
@ -258,14 +378,20 @@ func (a authReq) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
keepaliveConnections = 0
|
||||
}
|
||||
|
||||
durstr, _ := parser.GetStringAnnotation("auth-cache-duration", ing)
|
||||
durstr, err := parser.GetStringAnnotation(authReqCacheDuration, ing, a.annotationConfig.Annotations)
|
||||
if err != nil && errors.IsValidationError(err) {
|
||||
return nil, fmt.Errorf("%s contains invalid value", authReqCacheDuration)
|
||||
}
|
||||
authCacheDuration, err := ParseStringToCacheDurations(durstr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
responseHeaders := []string{}
|
||||
hstr, _ := parser.GetStringAnnotation("auth-response-headers", ing)
|
||||
hstr, err := parser.GetStringAnnotation(authReqResponseHeadersAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil && errors.IsValidationError(err) {
|
||||
return nil, ing_errors.NewLocationDenied("validation error")
|
||||
}
|
||||
if len(hstr) != 0 {
|
||||
harr := strings.Split(hstr, ",")
|
||||
for _, header := range harr {
|
||||
|
@ -279,9 +405,26 @@ func (a authReq) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
}
|
||||
}
|
||||
|
||||
proxySetHeaderMap, err := parser.GetStringAnnotation("auth-proxy-set-headers", ing)
|
||||
proxySetHeaderMap, err := parser.GetStringAnnotation(authReqProxySetHeadersAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
klog.V(3).InfoS("auth-set-proxy-headers annotation is undefined and will not be set")
|
||||
klog.V(3).InfoS("auth-set-proxy-headers annotation is undefined and will not be set", "err", err)
|
||||
}
|
||||
|
||||
cns, _, err := cache.SplitMetaNamespaceKey(proxySetHeaderMap)
|
||||
if err != nil {
|
||||
return nil, ing_errors.LocationDenied{
|
||||
Reason: fmt.Errorf("error reading configmap name %s from annotation: %w", proxySetHeaderMap, err),
|
||||
}
|
||||
}
|
||||
|
||||
if cns == "" {
|
||||
cns = ing.Namespace
|
||||
}
|
||||
// We don't accept different namespaces for secrets.
|
||||
if cns != ing.Namespace {
|
||||
return nil, ing_errors.LocationDenied{
|
||||
Reason: fmt.Errorf("cross namespace usage of secrets is not allowed"),
|
||||
}
|
||||
}
|
||||
|
||||
var proxySetHeaders map[string]string
|
||||
|
@ -301,9 +444,15 @@ func (a authReq) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
proxySetHeaders = proxySetHeadersMapContents.Data
|
||||
}
|
||||
|
||||
requestRedirect, _ := parser.GetStringAnnotation("auth-request-redirect", ing)
|
||||
requestRedirect, err := parser.GetStringAnnotation(authReqRequestRedirectAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil && errors.IsValidationError(err) {
|
||||
return nil, fmt.Errorf("%s is invalid: %w", authReqRequestRedirectAnnotation, err)
|
||||
}
|
||||
|
||||
alwaysSetCookie, _ := parser.GetBoolAnnotation("auth-always-set-cookie", ing)
|
||||
alwaysSetCookie, err := parser.GetBoolAnnotation(authReqAlwaysSetCookieAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil && errors.IsValidationError(err) {
|
||||
return nil, fmt.Errorf("%s is invalid: %w", authReqAlwaysSetCookieAnnotation, err)
|
||||
}
|
||||
|
||||
return &Config{
|
||||
URL: urlString,
|
||||
|
@ -348,3 +497,7 @@ func ParseStringToCacheDurations(input string) ([]string, error) {
|
|||
}
|
||||
return authCacheDuration, nil
|
||||
}
|
||||
|
||||
func (a authReq) GetDocumentation() parser.AnnotationFields {
|
||||
return a.annotationConfig.Annotations
|
||||
}
|
||||
|
|
|
@ -192,11 +192,13 @@ func TestHeaderAnnotations(t *testing.T) {
|
|||
i, err := NewParser(&resolver.Mock{}).Parse(ing)
|
||||
if test.expErr {
|
||||
if err == nil {
|
||||
t.Error("expected error but retuned nil")
|
||||
t.Errorf("%v expected error but retuned nil", test.title)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("no error was expected but %v happened in %s", err, test.title)
|
||||
}
|
||||
u, ok := i.(*Config)
|
||||
if !ok {
|
||||
t.Errorf("%v: expected an External type", test.title)
|
||||
|
|
|
@ -23,23 +23,47 @@ import (
|
|||
"k8s.io/ingress-nginx/internal/ingress/resolver"
|
||||
)
|
||||
|
||||
const (
|
||||
enableGlobalAuthAnnotation = "enable-global-auth"
|
||||
)
|
||||
|
||||
var globalAuthAnnotations = parser.Annotation{
|
||||
Group: "authentication",
|
||||
Annotations: parser.AnnotationFields{
|
||||
enableGlobalAuthAnnotation: {
|
||||
Validator: parser.ValidateBool,
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `Defines if the gloabl external authentication should be enabled.`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
type authReqGlobal struct {
|
||||
r resolver.Resolver
|
||||
r resolver.Resolver
|
||||
annotationConfig parser.Annotation
|
||||
}
|
||||
|
||||
// NewParser creates a new authentication request annotation parser
|
||||
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
|
||||
return authReqGlobal{r}
|
||||
return authReqGlobal{
|
||||
r: r,
|
||||
annotationConfig: globalAuthAnnotations,
|
||||
}
|
||||
}
|
||||
|
||||
// ParseAnnotations parses the annotations contained in the ingress
|
||||
// rule used to enable or disable global external authentication
|
||||
func (a authReqGlobal) Parse(ing *networking.Ingress) (interface{}, error) {
|
||||
|
||||
enableGlobalAuth, err := parser.GetBoolAnnotation("enable-global-auth", ing)
|
||||
enableGlobalAuth, err := parser.GetBoolAnnotation(enableGlobalAuthAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
enableGlobalAuth = true
|
||||
}
|
||||
|
||||
return enableGlobalAuth, nil
|
||||
}
|
||||
|
||||
func (a authReqGlobal) GetDocumentation() parser.AnnotationFields {
|
||||
return a.annotationConfig.Annotations
|
||||
}
|
||||
|
|
|
@ -32,13 +32,64 @@ import (
|
|||
const (
|
||||
defaultAuthTLSDepth = 1
|
||||
defaultAuthVerifyClient = "on"
|
||||
|
||||
annotationAuthTLSSecret = "auth-tls-secret" //#nosec G101
|
||||
annotationAuthTLSVerifyClient = "auth-tls-verify-client"
|
||||
annotationAuthTLSVerifyDepth = "auth-tls-verify-depth"
|
||||
annotationAuthTLSErrorPage = "auth-tls-error-page"
|
||||
annotationAuthTLSPassCertToUpstream = "auth-tls-pass-certificate-to-upstream" //#nosec G101
|
||||
annotationAuthTLSMatchCN = "auth-tls-match-cn"
|
||||
)
|
||||
|
||||
var (
|
||||
regexChars = regexp.QuoteMeta(`()|=`)
|
||||
authVerifyClientRegex = regexp.MustCompile(`on|off|optional|optional_no_ca`)
|
||||
commonNameRegex = regexp.MustCompile(`CN=`)
|
||||
commonNameRegex = regexp.MustCompile(`^CN=[/\-.\_\~a-zA-Z0-9` + regexChars + `]*$`)
|
||||
redirectRegex = regexp.MustCompile(`^((https?://)?[A-Za-z0-9\-\.]*(:[0-9]+)?/[A-Za-z0-9\-\.]*)?$`)
|
||||
)
|
||||
|
||||
var authTLSAnnotations = parser.Annotation{
|
||||
Group: "authentication",
|
||||
Annotations: parser.AnnotationFields{
|
||||
annotationAuthTLSSecret: {
|
||||
Validator: parser.ValidateRegex(*parser.BasicCharsRegex, true),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskMedium, // Medium as it allows a subset of chars
|
||||
Documentation: `This annotation defines the secret that contains the certificate chain of allowed certs`,
|
||||
},
|
||||
annotationAuthTLSVerifyClient: {
|
||||
Validator: parser.ValidateRegex(*authVerifyClientRegex, true),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskMedium, // Medium as it allows a subset of chars
|
||||
Documentation: `This annotation enables verification of client certificates. Can be "on", "off", "optional" or "optional_no_ca"`,
|
||||
},
|
||||
annotationAuthTLSVerifyDepth: {
|
||||
Validator: parser.ValidateInt,
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation defines validation depth between the provided client certificate and the Certification Authority chain.`,
|
||||
},
|
||||
annotationAuthTLSErrorPage: {
|
||||
Validator: parser.ValidateRegex(*redirectRegex, true),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskHigh,
|
||||
Documentation: `This annotation defines the URL/Page that user should be redirected in case of a Certificate Authentication Error`,
|
||||
},
|
||||
annotationAuthTLSPassCertToUpstream: {
|
||||
Validator: parser.ValidateBool,
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation defines if the received certificates should be passed or not to the upstream server in the header "ssl-client-cert"`,
|
||||
},
|
||||
annotationAuthTLSMatchCN: {
|
||||
Validator: parser.ValidateRegex(*commonNameRegex, true),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskHigh,
|
||||
Documentation: `This annotation adds a sanity check for the CN of the client certificate that is sent over using a string / regex starting with "CN="`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Config contains the AuthSSLCert used for mutual authentication
|
||||
// and the configured ValidationDepth
|
||||
type Config struct {
|
||||
|
@ -80,11 +131,15 @@ func (assl1 *Config) Equal(assl2 *Config) bool {
|
|||
|
||||
// NewParser creates a new TLS authentication annotation parser
|
||||
func NewParser(resolver resolver.Resolver) parser.IngressAnnotation {
|
||||
return authTLS{resolver}
|
||||
return authTLS{
|
||||
r: resolver,
|
||||
annotationConfig: authTLSAnnotations,
|
||||
}
|
||||
}
|
||||
|
||||
type authTLS struct {
|
||||
r resolver.Resolver
|
||||
r resolver.Resolver
|
||||
annotationConfig parser.Annotation
|
||||
}
|
||||
|
||||
// Parse parses the annotations contained in the ingress
|
||||
|
@ -93,15 +148,21 @@ func (a authTLS) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
var err error
|
||||
config := &Config{}
|
||||
|
||||
tlsauthsecret, err := parser.GetStringAnnotation("auth-tls-secret", ing)
|
||||
tlsauthsecret, err := parser.GetStringAnnotation(annotationAuthTLSSecret, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
return &Config{}, err
|
||||
}
|
||||
|
||||
_, _, err = k8s.ParseNameNS(tlsauthsecret)
|
||||
ns, _, err := k8s.ParseNameNS(tlsauthsecret)
|
||||
if err != nil {
|
||||
return &Config{}, ing_errors.NewLocationDenied(err.Error())
|
||||
}
|
||||
if ns == "" {
|
||||
ns = ing.Namespace
|
||||
}
|
||||
if ns != ing.Namespace {
|
||||
return &Config{}, ing_errors.NewLocationDenied("cross namespace secrets are not supported")
|
||||
}
|
||||
|
||||
authCert, err := a.r.GetAuthCertificate(tlsauthsecret)
|
||||
if err != nil {
|
||||
|
@ -110,30 +171,45 @@ func (a authTLS) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
}
|
||||
config.AuthSSLCert = *authCert
|
||||
|
||||
config.VerifyClient, err = parser.GetStringAnnotation("auth-tls-verify-client", ing)
|
||||
config.VerifyClient, err = parser.GetStringAnnotation(annotationAuthTLSVerifyClient, ing, a.annotationConfig.Annotations)
|
||||
// We can set a default value here in case of validation error
|
||||
if err != nil || !authVerifyClientRegex.MatchString(config.VerifyClient) {
|
||||
config.VerifyClient = defaultAuthVerifyClient
|
||||
}
|
||||
|
||||
config.ValidationDepth, err = parser.GetIntAnnotation("auth-tls-verify-depth", ing)
|
||||
config.ValidationDepth, err = parser.GetIntAnnotation(annotationAuthTLSVerifyDepth, ing, a.annotationConfig.Annotations)
|
||||
// We can set a default value here in case of validation error
|
||||
if err != nil || config.ValidationDepth == 0 {
|
||||
config.ValidationDepth = defaultAuthTLSDepth
|
||||
}
|
||||
|
||||
config.ErrorPage, err = parser.GetStringAnnotation("auth-tls-error-page", ing)
|
||||
config.ErrorPage, err = parser.GetStringAnnotation(annotationAuthTLSErrorPage, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
if ing_errors.IsValidationError(err) {
|
||||
return &Config{}, err
|
||||
}
|
||||
config.ErrorPage = ""
|
||||
}
|
||||
|
||||
config.PassCertToUpstream, err = parser.GetBoolAnnotation("auth-tls-pass-certificate-to-upstream", ing)
|
||||
config.PassCertToUpstream, err = parser.GetBoolAnnotation(annotationAuthTLSPassCertToUpstream, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
if ing_errors.IsValidationError(err) {
|
||||
return &Config{}, err
|
||||
}
|
||||
config.PassCertToUpstream = false
|
||||
}
|
||||
|
||||
config.MatchCN, err = parser.GetStringAnnotation("auth-tls-match-cn", ing)
|
||||
if err != nil || !commonNameRegex.MatchString(config.MatchCN) {
|
||||
config.MatchCN, err = parser.GetStringAnnotation(annotationAuthTLSMatchCN, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
if ing_errors.IsValidationError(err) {
|
||||
return &Config{}, err
|
||||
}
|
||||
config.MatchCN = ""
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func (a authTLS) GetDocumentation() parser.AnnotationFields {
|
||||
return a.annotationConfig.Annotations
|
||||
}
|
||||
|
|
|
@ -93,7 +93,7 @@ func TestAnnotations(t *testing.T) {
|
|||
ing := buildIngress()
|
||||
data := map[string]string{}
|
||||
|
||||
data[parser.GetAnnotationWithPrefix("auth-tls-secret")] = "default/demo-secret"
|
||||
data[parser.GetAnnotationWithPrefix(annotationAuthTLSSecret)] = "default/demo-secret"
|
||||
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
|
@ -132,11 +132,11 @@ func TestAnnotations(t *testing.T) {
|
|||
t.Errorf("expected empty string, but got %v", u.MatchCN)
|
||||
}
|
||||
|
||||
data[parser.GetAnnotationWithPrefix("auth-tls-verify-client")] = "off"
|
||||
data[parser.GetAnnotationWithPrefix("auth-tls-verify-depth")] = "2"
|
||||
data[parser.GetAnnotationWithPrefix("auth-tls-error-page")] = "ok.com/error"
|
||||
data[parser.GetAnnotationWithPrefix("auth-tls-pass-certificate-to-upstream")] = "true"
|
||||
data[parser.GetAnnotationWithPrefix("auth-tls-match-cn")] = "CN=hello-app"
|
||||
data[parser.GetAnnotationWithPrefix(annotationAuthTLSVerifyClient)] = "off"
|
||||
data[parser.GetAnnotationWithPrefix(annotationAuthTLSVerifyDepth)] = "2"
|
||||
data[parser.GetAnnotationWithPrefix(annotationAuthTLSErrorPage)] = "ok.com/error"
|
||||
data[parser.GetAnnotationWithPrefix(annotationAuthTLSPassCertToUpstream)] = "true"
|
||||
data[parser.GetAnnotationWithPrefix(annotationAuthTLSMatchCN)] = "CN=(hello-app|ok|goodbye)"
|
||||
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
|
@ -165,8 +165,8 @@ func TestAnnotations(t *testing.T) {
|
|||
if u.PassCertToUpstream != true {
|
||||
t.Errorf("expected %v but got %v", true, u.PassCertToUpstream)
|
||||
}
|
||||
if u.MatchCN != "CN=hello-app" {
|
||||
t.Errorf("expected %v but got %v", "CN=hello-app", u.MatchCN)
|
||||
if u.MatchCN != "CN=(hello-app|ok|goodbye)" {
|
||||
t.Errorf("expected %v but got %v", "CN=(hello-app|ok|goodbye)", u.MatchCN)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -182,15 +182,24 @@ func TestInvalidAnnotations(t *testing.T) {
|
|||
}
|
||||
|
||||
// Invalid NameSpace
|
||||
data[parser.GetAnnotationWithPrefix("auth-tls-secret")] = "demo-secret"
|
||||
data[parser.GetAnnotationWithPrefix(annotationAuthTLSSecret)] = "demo-secret"
|
||||
ing.SetAnnotations(data)
|
||||
_, err = NewParser(fakeSecret).Parse(ing)
|
||||
if err == nil {
|
||||
t.Errorf("Expected error with ingress but got nil")
|
||||
}
|
||||
|
||||
// Invalid Cross NameSpace
|
||||
data[parser.GetAnnotationWithPrefix(annotationAuthTLSSecret)] = "nondefault/demo-secret"
|
||||
ing.SetAnnotations(data)
|
||||
_, err = NewParser(fakeSecret).Parse(ing)
|
||||
expErr := errors.NewLocationDenied("cross namespace secrets are not supported")
|
||||
if err.Error() != expErr.Error() {
|
||||
t.Errorf("received error is different from cross namespace error: %s Expected %s", err, expErr)
|
||||
}
|
||||
|
||||
// Invalid Auth Certificate
|
||||
data[parser.GetAnnotationWithPrefix("auth-tls-secret")] = "default/invalid-demo-secret"
|
||||
data[parser.GetAnnotationWithPrefix(annotationAuthTLSSecret)] = "default/invalid-demo-secret"
|
||||
ing.SetAnnotations(data)
|
||||
_, err = NewParser(fakeSecret).Parse(ing)
|
||||
if err == nil {
|
||||
|
@ -198,11 +207,38 @@ func TestInvalidAnnotations(t *testing.T) {
|
|||
}
|
||||
|
||||
// Invalid optional Annotations
|
||||
data[parser.GetAnnotationWithPrefix("auth-tls-secret")] = "default/demo-secret"
|
||||
data[parser.GetAnnotationWithPrefix("auth-tls-verify-client")] = "w00t"
|
||||
data[parser.GetAnnotationWithPrefix("auth-tls-verify-depth")] = "abcd"
|
||||
data[parser.GetAnnotationWithPrefix("auth-tls-pass-certificate-to-upstream")] = "nahh"
|
||||
data[parser.GetAnnotationWithPrefix("auth-tls-match-cn")] = "<script>nope</script>"
|
||||
data[parser.GetAnnotationWithPrefix(annotationAuthTLSSecret)] = "default/demo-secret"
|
||||
|
||||
data[parser.GetAnnotationWithPrefix(annotationAuthTLSVerifyClient)] = "w00t"
|
||||
ing.SetAnnotations(data)
|
||||
_, err = NewParser(fakeSecret).Parse(ing)
|
||||
if err != nil {
|
||||
t.Errorf("Error should be nil and verify client should be defaulted")
|
||||
}
|
||||
|
||||
data[parser.GetAnnotationWithPrefix(annotationAuthTLSVerifyDepth)] = "abcd"
|
||||
ing.SetAnnotations(data)
|
||||
_, err = NewParser(fakeSecret).Parse(ing)
|
||||
if err != nil {
|
||||
t.Errorf("Error should be nil and verify depth should be defaulted")
|
||||
}
|
||||
|
||||
data[parser.GetAnnotationWithPrefix(annotationAuthTLSPassCertToUpstream)] = "nahh"
|
||||
ing.SetAnnotations(data)
|
||||
_, err = NewParser(fakeSecret).Parse(ing)
|
||||
if err == nil {
|
||||
t.Errorf("Expected error with ingress but got nil")
|
||||
}
|
||||
delete(data, parser.GetAnnotationWithPrefix(annotationAuthTLSPassCertToUpstream))
|
||||
|
||||
data[parser.GetAnnotationWithPrefix(annotationAuthTLSMatchCN)] = "<script>nope</script>"
|
||||
ing.SetAnnotations(data)
|
||||
_, err = NewParser(fakeSecret).Parse(ing)
|
||||
if err == nil {
|
||||
t.Errorf("Expected error with ingress CN but got nil")
|
||||
}
|
||||
delete(data, parser.GetAnnotationWithPrefix(annotationAuthTLSMatchCN))
|
||||
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
i, err := NewParser(fakeSecret).Parse(ing)
|
||||
|
|
|
@ -17,48 +17,66 @@ limitations under the License.
|
|||
package backendprotocol
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
networking "k8s.io/api/networking/v1"
|
||||
"k8s.io/klog/v2"
|
||||
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/parser"
|
||||
"k8s.io/ingress-nginx/internal/ingress/errors"
|
||||
"k8s.io/ingress-nginx/internal/ingress/resolver"
|
||||
)
|
||||
|
||||
// HTTP protocol
|
||||
const HTTP = "HTTP"
|
||||
|
||||
var (
|
||||
validProtocols = regexp.MustCompile(`^(AUTO_HTTP|HTTP|HTTPS|GRPC|GRPCS|FCGI)$`)
|
||||
validProtocols = []string{"auto_http", "http", "https", "ajp", "grpc", "grpcs", "fcgi"}
|
||||
)
|
||||
|
||||
const (
|
||||
http = "HTTP"
|
||||
backendProtocolAnnotation = "backend-protocol"
|
||||
)
|
||||
|
||||
var backendProtocolConfig = parser.Annotation{
|
||||
Group: "backend",
|
||||
Annotations: parser.AnnotationFields{
|
||||
backendProtocolAnnotation: {
|
||||
Validator: parser.ValidateOptions(validProtocols, false, true),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow, // Low, as it allows just a set of options
|
||||
Documentation: `this annotation can be used to define which protocol should
|
||||
be used to communicate with backends`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
type backendProtocol struct {
|
||||
r resolver.Resolver
|
||||
r resolver.Resolver
|
||||
annotationConfig parser.Annotation
|
||||
}
|
||||
|
||||
// NewParser creates a new backend protocol annotation parser
|
||||
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
|
||||
return backendProtocol{r}
|
||||
return backendProtocol{
|
||||
r: r,
|
||||
annotationConfig: backendProtocolConfig,
|
||||
}
|
||||
}
|
||||
|
||||
func (a backendProtocol) GetDocumentation() parser.AnnotationFields {
|
||||
return a.annotationConfig.Annotations
|
||||
}
|
||||
|
||||
// ParseAnnotations parses the annotations contained in the ingress
|
||||
// rule used to indicate the backend protocol.
|
||||
func (a backendProtocol) Parse(ing *networking.Ingress) (interface{}, error) {
|
||||
if ing.GetAnnotations() == nil {
|
||||
return HTTP, nil
|
||||
return http, nil
|
||||
}
|
||||
|
||||
proto, err := parser.GetStringAnnotation("backend-protocol", ing)
|
||||
proto, err := parser.GetStringAnnotation(backendProtocolAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
return HTTP, nil
|
||||
}
|
||||
|
||||
proto = strings.TrimSpace(strings.ToUpper(proto))
|
||||
if !validProtocols.MatchString(proto) {
|
||||
klog.Warningf("Protocol %v is not a valid value for the backend-protocol annotation. Using HTTP as protocol", proto)
|
||||
return HTTP, nil
|
||||
if errors.IsValidationError(err) {
|
||||
klog.Warningf("validation error %s. Using HTTP as protocol", err)
|
||||
}
|
||||
return http, nil
|
||||
}
|
||||
|
||||
return proto, nil
|
||||
|
|
|
@ -77,7 +77,7 @@ func TestParseInvalidAnnotations(t *testing.T) {
|
|||
}
|
||||
|
||||
// Test invalid annotation set
|
||||
data[parser.GetAnnotationWithPrefix("backend-protocol")] = "INVALID"
|
||||
data[parser.GetAnnotationWithPrefix(backendProtocolAnnotation)] = "INVALID"
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
i, err = NewParser(&resolver.Mock{}).Parse(ing)
|
||||
|
@ -97,7 +97,7 @@ func TestParseAnnotations(t *testing.T) {
|
|||
ing := buildIngress()
|
||||
|
||||
data := map[string]string{}
|
||||
data[parser.GetAnnotationWithPrefix("backend-protocol")] = "HTTPS"
|
||||
data[parser.GetAnnotationWithPrefix(backendProtocolAnnotation)] = " HTTPS "
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
i, err := NewParser(&resolver.Mock{}).Parse(ing)
|
||||
|
|
|
@ -18,14 +18,82 @@ package canary
|
|||
|
||||
import (
|
||||
networking "k8s.io/api/networking/v1"
|
||||
"k8s.io/klog/v2"
|
||||
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/parser"
|
||||
"k8s.io/ingress-nginx/internal/ingress/errors"
|
||||
"k8s.io/ingress-nginx/internal/ingress/resolver"
|
||||
)
|
||||
|
||||
const (
|
||||
canaryAnnotation = "canary"
|
||||
canaryWeightAnnotation = "canary-weight"
|
||||
canaryWeightTotalAnnotation = "canary-weight-total"
|
||||
canaryByHeaderAnnotation = "canary-by-header"
|
||||
canaryByHeaderValueAnnotation = "canary-by-header-value"
|
||||
canaryByHeaderPatternAnnotation = "canary-by-header-pattern"
|
||||
canaryByCookieAnnotation = "canary-by-cookie"
|
||||
)
|
||||
|
||||
var CanaryAnnotations = parser.Annotation{
|
||||
Group: "canary",
|
||||
Annotations: parser.AnnotationFields{
|
||||
canaryAnnotation: {
|
||||
Validator: parser.ValidateBool,
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation enables the Ingress spec to act as an alternative service for requests to route to depending on the rules applied`,
|
||||
},
|
||||
canaryWeightAnnotation: {
|
||||
Validator: parser.ValidateInt,
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation defines the integer based (0 - ) percent of random requests that should be routed to the service specified in the canary Ingress`,
|
||||
},
|
||||
canaryWeightTotalAnnotation: {
|
||||
Validator: parser.ValidateInt,
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation The total weight of traffic. If unspecified, it defaults to 100`,
|
||||
},
|
||||
canaryByHeaderAnnotation: {
|
||||
Validator: parser.ValidateRegex(*parser.BasicCharsRegex, true),
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskMedium,
|
||||
Documentation: `This annotation defines the header that should be used for notifying the Ingress to route the request to the service specified in the Canary Ingress.
|
||||
When the request header is set to 'always', it will be routed to the canary. When the header is set to 'never', it will never be routed to the canary.
|
||||
For any other value, the header will be ignored and the request compared against the other canary rules by precedence`,
|
||||
},
|
||||
canaryByHeaderValueAnnotation: {
|
||||
Validator: parser.ValidateRegex(*parser.BasicCharsRegex, true),
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskMedium,
|
||||
Documentation: `This annotation defines the header value to match for notifying the Ingress to route the request to the service specified in the Canary Ingress.
|
||||
When the request header is set to this value, it will be routed to the canary. For any other header value, the header will be ignored and the request compared against the other canary rules by precedence.
|
||||
This annotation has to be used together with 'canary-by-header'. The annotation is an extension of the 'canary-by-header' to allow customizing the header value instead of using hardcoded values.
|
||||
It doesn't have any effect if the 'canary-by-header' annotation is not defined`,
|
||||
},
|
||||
canaryByHeaderPatternAnnotation: {
|
||||
Validator: parser.ValidateRegex(*parser.IsValidRegex, false),
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskMedium,
|
||||
Documentation: `This annotation works the same way as canary-by-header-value except it does PCRE Regex matching.
|
||||
Note that when 'canary-by-header-value' is set this annotation will be ignored.
|
||||
When the given Regex causes error during request processing, the request will be considered as not matching.`,
|
||||
},
|
||||
canaryByCookieAnnotation: {
|
||||
Validator: parser.ValidateRegex(*parser.BasicCharsRegex, true),
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskMedium,
|
||||
Documentation: `This annotation defines the cookie that should be used for notifying the Ingress to route the request to the service specified in the Canary Ingress.
|
||||
When the cookie is set to 'always', it will be routed to the canary. When the cookie is set to 'never', it will never be routed to the canary`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
type canary struct {
|
||||
r resolver.Resolver
|
||||
r resolver.Resolver
|
||||
annotationConfig parser.Annotation
|
||||
}
|
||||
|
||||
// Config returns the configuration rules for setting up the Canary
|
||||
|
@ -41,7 +109,10 @@ type Config struct {
|
|||
|
||||
// NewParser parses the ingress for canary related annotations
|
||||
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
|
||||
return canary{r}
|
||||
return canary{
|
||||
r: r,
|
||||
annotationConfig: CanaryAnnotations,
|
||||
}
|
||||
}
|
||||
|
||||
// Parse parses the annotations contained in the ingress
|
||||
|
@ -50,45 +121,70 @@ func (c canary) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
config := &Config{}
|
||||
var err error
|
||||
|
||||
config.Enabled, err = parser.GetBoolAnnotation("canary", ing)
|
||||
config.Enabled, err = parser.GetBoolAnnotation(canaryAnnotation, ing, c.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
if errors.IsValidationError(err) {
|
||||
klog.Warningf("%s is invalid, defaulting to 'false'", canaryAnnotation)
|
||||
}
|
||||
config.Enabled = false
|
||||
}
|
||||
|
||||
config.Weight, err = parser.GetIntAnnotation("canary-weight", ing)
|
||||
config.Weight, err = parser.GetIntAnnotation(canaryWeightAnnotation, ing, c.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
if errors.IsValidationError(err) {
|
||||
klog.Warningf("%s is invalid, defaulting to '0'", canaryWeightAnnotation)
|
||||
}
|
||||
config.Weight = 0
|
||||
}
|
||||
|
||||
config.WeightTotal, err = parser.GetIntAnnotation("canary-weight-total", ing)
|
||||
config.WeightTotal, err = parser.GetIntAnnotation(canaryWeightTotalAnnotation, ing, c.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
if errors.IsValidationError(err) {
|
||||
klog.Warningf("%s is invalid, defaulting to '100'", canaryWeightTotalAnnotation)
|
||||
}
|
||||
config.WeightTotal = 100
|
||||
}
|
||||
|
||||
config.Header, err = parser.GetStringAnnotation("canary-by-header", ing)
|
||||
config.Header, err = parser.GetStringAnnotation(canaryByHeaderAnnotation, ing, c.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
if errors.IsValidationError(err) {
|
||||
klog.Warningf("%s is invalid, defaulting to ''", canaryByHeaderAnnotation)
|
||||
}
|
||||
config.Header = ""
|
||||
}
|
||||
|
||||
config.HeaderValue, err = parser.GetStringAnnotation("canary-by-header-value", ing)
|
||||
config.HeaderValue, err = parser.GetStringAnnotation(canaryByHeaderValueAnnotation, ing, c.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
if errors.IsValidationError(err) {
|
||||
klog.Warningf("%s is invalid, defaulting to ''", canaryByHeaderValueAnnotation)
|
||||
}
|
||||
config.HeaderValue = ""
|
||||
}
|
||||
|
||||
config.HeaderPattern, err = parser.GetStringAnnotation("canary-by-header-pattern", ing)
|
||||
config.HeaderPattern, err = parser.GetStringAnnotation(canaryByHeaderPatternAnnotation, ing, c.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
if errors.IsValidationError(err) {
|
||||
klog.Warningf("%s is invalid, defaulting to ''", canaryByHeaderPatternAnnotation)
|
||||
}
|
||||
config.HeaderPattern = ""
|
||||
}
|
||||
|
||||
config.Cookie, err = parser.GetStringAnnotation("canary-by-cookie", ing)
|
||||
config.Cookie, err = parser.GetStringAnnotation(canaryByCookieAnnotation, ing, c.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
if errors.IsValidationError(err) {
|
||||
klog.Warningf("%s is invalid, defaulting to ''", canaryByCookieAnnotation)
|
||||
}
|
||||
config.Cookie = ""
|
||||
}
|
||||
|
||||
if !config.Enabled && (config.Weight > 0 || len(config.Header) > 0 || len(config.HeaderValue) > 0 || len(config.Cookie) > 0 ||
|
||||
len(config.HeaderPattern) > 0) {
|
||||
return nil, errors.NewInvalidAnnotationConfiguration("canary", "configured but not enabled")
|
||||
return nil, errors.NewInvalidAnnotationConfiguration(canaryAnnotation, "configured but not enabled")
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func (c canary) GetDocumentation() parser.AnnotationFields {
|
||||
return c.annotationConfig.Annotations
|
||||
}
|
||||
|
|
|
@ -23,17 +23,44 @@ import (
|
|||
"k8s.io/ingress-nginx/internal/ingress/resolver"
|
||||
)
|
||||
|
||||
const (
|
||||
clientBodyBufferSizeAnnotation = "client-body-buffer-size"
|
||||
)
|
||||
|
||||
var clientBodyBufferSizeConfig = parser.Annotation{
|
||||
Group: "backend",
|
||||
Annotations: parser.AnnotationFields{
|
||||
clientBodyBufferSizeAnnotation: {
|
||||
Validator: parser.ValidateRegex(*parser.SizeRegex, true),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow, // Low, as it allows just a set of options
|
||||
Documentation: `Sets buffer size for reading client request body per location.
|
||||
In case the request body is larger than the buffer, the whole body or only its part is written to a temporary file.
|
||||
By default, buffer size is equal to two memory pages. This is 8K on x86, other 32-bit platforms, and x86-64.
|
||||
It is usually 16K on other 64-bit platforms. This annotation is applied to each location provided in the ingress rule.`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
type clientBodyBufferSize struct {
|
||||
r resolver.Resolver
|
||||
r resolver.Resolver
|
||||
annotationConfig parser.Annotation
|
||||
}
|
||||
|
||||
// NewParser creates a new clientBodyBufferSize annotation parser
|
||||
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
|
||||
return clientBodyBufferSize{r}
|
||||
return clientBodyBufferSize{
|
||||
r: r,
|
||||
annotationConfig: clientBodyBufferSizeConfig,
|
||||
}
|
||||
}
|
||||
|
||||
func (cbbs clientBodyBufferSize) GetDocumentation() parser.AnnotationFields {
|
||||
return cbbs.annotationConfig.Annotations
|
||||
}
|
||||
|
||||
// Parse parses the annotations contained in the ingress rule
|
||||
// used to add an client-body-buffer-size to the provided locations
|
||||
func (cbbs clientBodyBufferSize) Parse(ing *networking.Ingress) (interface{}, error) {
|
||||
return parser.GetStringAnnotation("client-body-buffer-size", ing)
|
||||
return parser.GetStringAnnotation(clientBodyBufferSizeAnnotation, ing, cbbs.annotationConfig.Annotations)
|
||||
}
|
||||
|
|
|
@ -39,6 +39,9 @@ func TestParse(t *testing.T) {
|
|||
}{
|
||||
{map[string]string{annotation: "8k"}, "8k"},
|
||||
{map[string]string{annotation: "16k"}, "16k"},
|
||||
{map[string]string{annotation: "10000"}, "10000"},
|
||||
{map[string]string{annotation: "16R"}, ""},
|
||||
{map[string]string{annotation: "16kkk"}, ""},
|
||||
{map[string]string{annotation: ""}, ""},
|
||||
{map[string]string{}, ""},
|
||||
{nil, ""},
|
||||
|
|
|
@ -17,12 +17,34 @@ limitations under the License.
|
|||
package connection
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
networking "k8s.io/api/networking/v1"
|
||||
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/parser"
|
||||
"k8s.io/ingress-nginx/internal/ingress/resolver"
|
||||
)
|
||||
|
||||
const (
|
||||
connectionProxyHeaderAnnotation = "connection-proxy-header"
|
||||
)
|
||||
|
||||
var (
|
||||
validConnectionHeaderValue = regexp.MustCompile(`^(close|keep-alive)$`)
|
||||
)
|
||||
|
||||
var connectionHeadersAnnotations = parser.Annotation{
|
||||
Group: "backend",
|
||||
Annotations: parser.AnnotationFields{
|
||||
connectionProxyHeaderAnnotation: {
|
||||
Validator: parser.ValidateRegex(*validConnectionHeaderValue, true),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation allows setting a specific value for "proxy_set_header Connection" directive. Right now it is restricted to "close" or "keep-alive"`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Config returns the connection header configuration for an Ingress rule
|
||||
type Config struct {
|
||||
Header string `json:"header"`
|
||||
|
@ -30,18 +52,22 @@ type Config struct {
|
|||
}
|
||||
|
||||
type connection struct {
|
||||
r resolver.Resolver
|
||||
r resolver.Resolver
|
||||
annotationConfig parser.Annotation
|
||||
}
|
||||
|
||||
// NewParser creates a new port in redirect annotation parser
|
||||
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
|
||||
return connection{r}
|
||||
return connection{
|
||||
r: r,
|
||||
annotationConfig: connectionHeadersAnnotations,
|
||||
}
|
||||
}
|
||||
|
||||
// Parse parses the annotations contained in the ingress
|
||||
// rule used to indicate if the connection header should be overridden.
|
||||
func (a connection) Parse(ing *networking.Ingress) (interface{}, error) {
|
||||
cp, err := parser.GetStringAnnotation("connection-proxy-header", ing)
|
||||
cp, err := parser.GetStringAnnotation(connectionProxyHeaderAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
return &Config{
|
||||
Enabled: false,
|
||||
|
@ -70,3 +96,7 @@ func (r1 *Config) Equal(r2 *Config) bool {
|
|||
|
||||
return true
|
||||
}
|
||||
|
||||
func (a connection) GetDocumentation() parser.AnnotationFields {
|
||||
return a.annotationConfig.Annotations
|
||||
}
|
||||
|
|
|
@ -37,10 +37,12 @@ func TestParse(t *testing.T) {
|
|||
testCases := []struct {
|
||||
annotations map[string]string
|
||||
expected *Config
|
||||
expectErr bool
|
||||
}{
|
||||
{map[string]string{annotation: "keep-alive"}, &Config{Enabled: true, Header: "keep-alive"}},
|
||||
{map[string]string{}, &Config{Enabled: false}},
|
||||
{nil, &Config{Enabled: false}},
|
||||
{map[string]string{annotation: "keep-alive"}, &Config{Enabled: true, Header: "keep-alive"}, false},
|
||||
{map[string]string{annotation: "not-allowed-value"}, &Config{Enabled: false}, true},
|
||||
{map[string]string{}, &Config{Enabled: false}, true},
|
||||
{nil, &Config{Enabled: false}, true},
|
||||
}
|
||||
|
||||
ing := &networking.Ingress{
|
||||
|
@ -53,11 +55,17 @@ func TestParse(t *testing.T) {
|
|||
|
||||
for _, testCase := range testCases {
|
||||
ing.SetAnnotations(testCase.annotations)
|
||||
i, _ := ap.Parse(ing)
|
||||
p, _ := i.(*Config)
|
||||
|
||||
i, err := ap.Parse(ing)
|
||||
if (err != nil) != testCase.expectErr {
|
||||
t.Fatalf("expected error: %t got error: %t err value: %s. %+v", testCase.expectErr, err != nil, err, testCase.annotations)
|
||||
}
|
||||
p, ok := i.(*Config)
|
||||
if !ok {
|
||||
t.Fatalf("expected a Config type")
|
||||
}
|
||||
if !p.Equal(testCase.expected) {
|
||||
t.Errorf("expected %v but returned %v, annotations: %s", testCase.expected, p, testCase.annotations)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@ import (
|
|||
"k8s.io/klog/v2"
|
||||
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/parser"
|
||||
"k8s.io/ingress-nginx/internal/ingress/errors"
|
||||
"k8s.io/ingress-nginx/internal/ingress/resolver"
|
||||
)
|
||||
|
||||
|
@ -38,20 +39,87 @@ var (
|
|||
// Regex are defined here to prevent information leak, if user tries to set anything not valid
|
||||
// that could cause the Response to contain some internal value/variable (like returning $pid, $upstream_addr, etc)
|
||||
// Origin must contain a http/s Origin (including or not the port) or the value '*'
|
||||
// This Regex is composed of the following:
|
||||
// * Sets a group that can be (https?://)?*?.something.com:port?
|
||||
// * Allows this to be repeated as much as possible, and separated by comma
|
||||
// Otherwise it should be '*'
|
||||
corsOriginRegexValidator = regexp.MustCompile(`^((((https?://)?(\*\.)?[A-Za-z0-9\-\.]*(:[0-9]+)?,?)+)|\*)?$`)
|
||||
// corsOriginRegex defines the regex for validation inside Parse
|
||||
corsOriginRegex = regexp.MustCompile(`^(https?://(\*\.)?[A-Za-z0-9\-\.]*(:[0-9]+)?|\*)?$`)
|
||||
// Method must contain valid methods list (PUT, GET, POST, BLA)
|
||||
// May contain or not spaces between each verb
|
||||
corsMethodsRegex = regexp.MustCompile(`^([A-Za-z]+,?\s?)+$`)
|
||||
// Headers must contain valid values only (X-HEADER12, X-ABC)
|
||||
// May contain or not spaces between each Header
|
||||
corsHeadersRegex = regexp.MustCompile(`^([A-Za-z0-9\-\_]+,?\s?)+$`)
|
||||
// Expose Headers must contain valid values only (*, X-HEADER12, X-ABC)
|
||||
// May contain or not spaces between each Header
|
||||
corsExposeHeadersRegex = regexp.MustCompile(`^(([A-Za-z0-9\-\_]+|\*),?\s?)+$`)
|
||||
)
|
||||
|
||||
const (
|
||||
corsEnableAnnotation = "enable-cors"
|
||||
corsAllowOriginAnnotation = "cors-allow-origin"
|
||||
corsAllowHeadersAnnotation = "cors-allow-headers"
|
||||
corsAllowMethodsAnnotation = "cors-allow-methods"
|
||||
corsAllowCredentialsAnnotation = "cors-allow-credentials" //#nosec G101
|
||||
corsExposeHeadersAnnotation = "cors-expose-headers"
|
||||
corsMaxAgeAnnotation = "cors-max-age"
|
||||
)
|
||||
|
||||
var corsAnnotation = parser.Annotation{
|
||||
Group: "cors",
|
||||
Annotations: parser.AnnotationFields{
|
||||
corsEnableAnnotation: {
|
||||
Validator: parser.ValidateBool,
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation enables Cross-Origin Resource Sharing (CORS) in an Ingress rule`,
|
||||
},
|
||||
corsAllowOriginAnnotation: {
|
||||
Validator: parser.ValidateRegex(*corsOriginRegexValidator, true),
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskMedium,
|
||||
Documentation: `This annotation controls what's the accepted Origin for CORS.
|
||||
This is a multi-valued field, separated by ','. It must follow this format: http(s)://origin-site.com or http(s)://origin-site.com:port
|
||||
It also supports single level wildcard subdomains and follows this format: http(s)://*.foo.bar, http(s)://*.bar.foo:8080 or http(s)://*.abc.bar.foo:9000`,
|
||||
},
|
||||
corsAllowHeadersAnnotation: {
|
||||
Validator: parser.ValidateRegex(*parser.HeadersVariable, true),
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskMedium,
|
||||
Documentation: `This annotation controls which headers are accepted.
|
||||
This is a multi-valued field, separated by ',' and accepts letters, numbers, _ and -`,
|
||||
},
|
||||
corsAllowMethodsAnnotation: {
|
||||
Validator: parser.ValidateRegex(*corsMethodsRegex, true),
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskMedium,
|
||||
Documentation: `This annotation controls which methods are accepted.
|
||||
This is a multi-valued field, separated by ',' and accepts only letters (upper and lower case)`,
|
||||
},
|
||||
corsAllowCredentialsAnnotation: {
|
||||
Validator: parser.ValidateBool,
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation controls if credentials can be passed during CORS operations.`,
|
||||
},
|
||||
corsExposeHeadersAnnotation: {
|
||||
Validator: parser.ValidateRegex(*corsExposeHeadersRegex, true),
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskMedium,
|
||||
Documentation: `This annotation controls which headers are exposed to response.
|
||||
This is a multi-valued field, separated by ',' and accepts letters, numbers, _, - and *.`,
|
||||
},
|
||||
corsMaxAgeAnnotation: {
|
||||
Validator: parser.ValidateInt,
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation controls how long, in seconds, preflight requests can be cached.`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
type cors struct {
|
||||
r resolver.Resolver
|
||||
r resolver.Resolver
|
||||
annotationConfig parser.Annotation
|
||||
}
|
||||
|
||||
// Config contains the Cors configuration to be used in the Ingress
|
||||
|
@ -67,7 +135,10 @@ type Config struct {
|
|||
|
||||
// NewParser creates a new CORS annotation parser
|
||||
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
|
||||
return cors{r}
|
||||
return cors{
|
||||
r: r,
|
||||
annotationConfig: corsAnnotation,
|
||||
}
|
||||
}
|
||||
|
||||
// Equal tests for equality between two External types
|
||||
|
@ -116,13 +187,16 @@ func (c cors) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
var err error
|
||||
config := &Config{}
|
||||
|
||||
config.CorsEnabled, err = parser.GetBoolAnnotation("enable-cors", ing)
|
||||
config.CorsEnabled, err = parser.GetBoolAnnotation(corsEnableAnnotation, ing, c.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
if errors.IsValidationError(err) {
|
||||
klog.Warningf("enable-cors is invalid, defaulting to 'false'")
|
||||
}
|
||||
config.CorsEnabled = false
|
||||
}
|
||||
|
||||
config.CorsAllowOrigin = []string{}
|
||||
unparsedOrigins, err := parser.GetStringAnnotation("cors-allow-origin", ing)
|
||||
unparsedOrigins, err := parser.GetStringAnnotation(corsAllowOriginAnnotation, ing, c.annotationConfig.Annotations)
|
||||
if err == nil {
|
||||
origins := strings.Split(unparsedOrigins, ",")
|
||||
for _, origin := range origins {
|
||||
|
@ -140,33 +214,48 @@ func (c cors) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
klog.Infof("Current config.corsAllowOrigin %v", config.CorsAllowOrigin)
|
||||
}
|
||||
} else {
|
||||
if errors.IsValidationError(err) {
|
||||
klog.Warningf("cors-allow-origin is invalid, defaulting to '*'")
|
||||
}
|
||||
config.CorsAllowOrigin = []string{"*"}
|
||||
}
|
||||
|
||||
config.CorsAllowHeaders, err = parser.GetStringAnnotation("cors-allow-headers", ing)
|
||||
if err != nil || !corsHeadersRegex.MatchString(config.CorsAllowHeaders) {
|
||||
config.CorsAllowHeaders, err = parser.GetStringAnnotation(corsAllowHeadersAnnotation, ing, c.annotationConfig.Annotations)
|
||||
if err != nil || !parser.HeadersVariable.MatchString(config.CorsAllowHeaders) {
|
||||
config.CorsAllowHeaders = defaultCorsHeaders
|
||||
}
|
||||
|
||||
config.CorsAllowMethods, err = parser.GetStringAnnotation("cors-allow-methods", ing)
|
||||
config.CorsAllowMethods, err = parser.GetStringAnnotation(corsAllowMethodsAnnotation, ing, c.annotationConfig.Annotations)
|
||||
if err != nil || !corsMethodsRegex.MatchString(config.CorsAllowMethods) {
|
||||
config.CorsAllowMethods = defaultCorsMethods
|
||||
}
|
||||
|
||||
config.CorsAllowCredentials, err = parser.GetBoolAnnotation("cors-allow-credentials", ing)
|
||||
config.CorsAllowCredentials, err = parser.GetBoolAnnotation(corsAllowCredentialsAnnotation, ing, c.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
if errors.IsValidationError(err) {
|
||||
if errors.IsValidationError(err) {
|
||||
klog.Warningf("cors-allow-credentials is invalid, defaulting to 'true'")
|
||||
}
|
||||
}
|
||||
config.CorsAllowCredentials = true
|
||||
}
|
||||
|
||||
config.CorsExposeHeaders, err = parser.GetStringAnnotation("cors-expose-headers", ing)
|
||||
config.CorsExposeHeaders, err = parser.GetStringAnnotation(corsExposeHeadersAnnotation, ing, c.annotationConfig.Annotations)
|
||||
if err != nil || !corsExposeHeadersRegex.MatchString(config.CorsExposeHeaders) {
|
||||
config.CorsExposeHeaders = ""
|
||||
}
|
||||
|
||||
config.CorsMaxAge, err = parser.GetIntAnnotation("cors-max-age", ing)
|
||||
config.CorsMaxAge, err = parser.GetIntAnnotation(corsMaxAgeAnnotation, ing, c.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
if errors.IsValidationError(err) {
|
||||
klog.Warningf("cors-max-age is invalid, defaulting to %d", defaultCorsMaxAge)
|
||||
}
|
||||
config.CorsMaxAge = defaultCorsMaxAge
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func (c cors) GetDocumentation() parser.AnnotationFields {
|
||||
return c.annotationConfig.Annotations
|
||||
}
|
||||
|
|
|
@ -75,13 +75,13 @@ func TestIngressCorsConfigValid(t *testing.T) {
|
|||
data := map[string]string{}
|
||||
|
||||
// Valid
|
||||
data[parser.GetAnnotationWithPrefix("enable-cors")] = "true"
|
||||
data[parser.GetAnnotationWithPrefix("cors-allow-headers")] = "DNT,X-CustomHeader, Keep-Alive,User-Agent"
|
||||
data[parser.GetAnnotationWithPrefix("cors-allow-credentials")] = "false"
|
||||
data[parser.GetAnnotationWithPrefix("cors-allow-methods")] = "GET, PATCH"
|
||||
data[parser.GetAnnotationWithPrefix("cors-allow-origin")] = "https://origin123.test.com:4443"
|
||||
data[parser.GetAnnotationWithPrefix("cors-expose-headers")] = "*, X-CustomResponseHeader"
|
||||
data[parser.GetAnnotationWithPrefix("cors-max-age")] = "600"
|
||||
data[parser.GetAnnotationWithPrefix(corsEnableAnnotation)] = "true"
|
||||
data[parser.GetAnnotationWithPrefix(corsAllowHeadersAnnotation)] = "DNT,X-CustomHeader, Keep-Alive,User-Agent"
|
||||
data[parser.GetAnnotationWithPrefix(corsAllowCredentialsAnnotation)] = "false"
|
||||
data[parser.GetAnnotationWithPrefix(corsAllowMethodsAnnotation)] = "GET, PATCH"
|
||||
data[parser.GetAnnotationWithPrefix(corsAllowOriginAnnotation)] = "https://origin123.test.com:4443"
|
||||
data[parser.GetAnnotationWithPrefix(corsExposeHeadersAnnotation)] = "*, X-CustomResponseHeader"
|
||||
data[parser.GetAnnotationWithPrefix(corsMaxAgeAnnotation)] = "600"
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
corst, err := NewParser(&resolver.Mock{}).Parse(ing)
|
||||
|
@ -95,31 +95,31 @@ func TestIngressCorsConfigValid(t *testing.T) {
|
|||
}
|
||||
|
||||
if !nginxCors.CorsEnabled {
|
||||
t.Errorf("expected %v but returned %v", data[parser.GetAnnotationWithPrefix("enable-cors")], nginxCors.CorsEnabled)
|
||||
t.Errorf("expected %v but returned %v", data[parser.GetAnnotationWithPrefix(corsEnableAnnotation)], nginxCors.CorsEnabled)
|
||||
}
|
||||
|
||||
if nginxCors.CorsAllowCredentials {
|
||||
t.Errorf("expected %v but returned %v", data[parser.GetAnnotationWithPrefix("cors-allow-credentials")], nginxCors.CorsAllowCredentials)
|
||||
t.Errorf("expected %v but returned %v", data[parser.GetAnnotationWithPrefix(corsAllowCredentialsAnnotation)], nginxCors.CorsAllowCredentials)
|
||||
}
|
||||
|
||||
if nginxCors.CorsAllowHeaders != "DNT,X-CustomHeader, Keep-Alive,User-Agent" {
|
||||
t.Errorf("expected %v but returned %v", data[parser.GetAnnotationWithPrefix("cors-allow-headers")], nginxCors.CorsAllowHeaders)
|
||||
t.Errorf("expected %v but returned %v", data[parser.GetAnnotationWithPrefix(corsAllowHeadersAnnotation)], nginxCors.CorsAllowHeaders)
|
||||
}
|
||||
|
||||
if nginxCors.CorsAllowMethods != "GET, PATCH" {
|
||||
t.Errorf("expected %v but returned %v", data[parser.GetAnnotationWithPrefix("cors-allow-methods")], nginxCors.CorsAllowMethods)
|
||||
t.Errorf("expected %v but returned %v", data[parser.GetAnnotationWithPrefix(corsAllowMethodsAnnotation)], nginxCors.CorsAllowMethods)
|
||||
}
|
||||
|
||||
if nginxCors.CorsAllowOrigin[0] != "https://origin123.test.com:4443" {
|
||||
t.Errorf("expected %v but returned %v", data[parser.GetAnnotationWithPrefix("cors-allow-origin")], nginxCors.CorsAllowOrigin)
|
||||
t.Errorf("expected %v but returned %v", data[parser.GetAnnotationWithPrefix(corsAllowOriginAnnotation)], nginxCors.CorsAllowOrigin)
|
||||
}
|
||||
|
||||
if nginxCors.CorsExposeHeaders != "*, X-CustomResponseHeader" {
|
||||
t.Errorf("expected %v but returned %v", data[parser.GetAnnotationWithPrefix("cors-expose-headers")], nginxCors.CorsExposeHeaders)
|
||||
t.Errorf("expected %v but returned %v", data[parser.GetAnnotationWithPrefix(corsExposeHeadersAnnotation)], nginxCors.CorsExposeHeaders)
|
||||
}
|
||||
|
||||
if nginxCors.CorsMaxAge != 600 {
|
||||
t.Errorf("expected %v but returned %v", data[parser.GetAnnotationWithPrefix("cors-max-age")], nginxCors.CorsMaxAge)
|
||||
t.Errorf("expected %v but returned %v", data[parser.GetAnnotationWithPrefix(corsMaxAgeAnnotation)], nginxCors.CorsMaxAge)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -129,13 +129,13 @@ func TestIngressCorsConfigInvalid(t *testing.T) {
|
|||
data := map[string]string{}
|
||||
|
||||
// Valid
|
||||
data[parser.GetAnnotationWithPrefix("enable-cors")] = "yes"
|
||||
data[parser.GetAnnotationWithPrefix("cors-allow-headers")] = "@alright, #ingress"
|
||||
data[parser.GetAnnotationWithPrefix("cors-allow-credentials")] = "no"
|
||||
data[parser.GetAnnotationWithPrefix("cors-allow-methods")] = "GET, PATCH, $nginx"
|
||||
data[parser.GetAnnotationWithPrefix("cors-allow-origin")] = "origin123.test.com:4443"
|
||||
data[parser.GetAnnotationWithPrefix("cors-expose-headers")] = "@alright, #ingress"
|
||||
data[parser.GetAnnotationWithPrefix("cors-max-age")] = "abcd"
|
||||
data[parser.GetAnnotationWithPrefix(corsEnableAnnotation)] = "yes"
|
||||
data[parser.GetAnnotationWithPrefix(corsAllowHeadersAnnotation)] = "@alright, #ingress"
|
||||
data[parser.GetAnnotationWithPrefix(corsAllowCredentialsAnnotation)] = "no"
|
||||
data[parser.GetAnnotationWithPrefix(corsAllowMethodsAnnotation)] = "GET, PATCH, $nginx"
|
||||
data[parser.GetAnnotationWithPrefix(corsAllowOriginAnnotation)] = "origin123.test.com:4443"
|
||||
data[parser.GetAnnotationWithPrefix(corsExposeHeadersAnnotation)] = "@alright, #ingress"
|
||||
data[parser.GetAnnotationWithPrefix(corsMaxAgeAnnotation)] = "abcd"
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
corst, err := NewParser(&resolver.Mock{}).Parse(ing)
|
||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||
package customhttperrors
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
|
@ -26,19 +27,46 @@ import (
|
|||
"k8s.io/ingress-nginx/internal/ingress/resolver"
|
||||
)
|
||||
|
||||
const (
|
||||
customHTTPErrorsAnnotation = "custom-http-errors"
|
||||
)
|
||||
|
||||
var (
|
||||
// We accept anything between 400 and 599, on a comma separated.
|
||||
arrayOfHTTPErrors = regexp.MustCompile(`^(?:[4,5][0-9][0-9],?)*$`)
|
||||
)
|
||||
|
||||
var customHTTPErrorsAnnotations = parser.Annotation{
|
||||
Group: "backend",
|
||||
Annotations: parser.AnnotationFields{
|
||||
customHTTPErrorsAnnotation: {
|
||||
Validator: parser.ValidateRegex(*arrayOfHTTPErrors, true),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `If a default backend annotation is specified on the ingress, the errors code specified on this annotation
|
||||
will be routed to that annotation's default backend service. Otherwise they will be routed to the global default backend.
|
||||
A comma-separated list of error codes is accepted (anything between 400 and 599, like 403, 503)`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
type customhttperrors struct {
|
||||
r resolver.Resolver
|
||||
r resolver.Resolver
|
||||
annotationConfig parser.Annotation
|
||||
}
|
||||
|
||||
// NewParser creates a new custom http errors annotation parser
|
||||
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
|
||||
return customhttperrors{r}
|
||||
return customhttperrors{
|
||||
r: r,
|
||||
annotationConfig: customHTTPErrorsAnnotations,
|
||||
}
|
||||
}
|
||||
|
||||
// Parse parses the annotations contained in the ingress to use
|
||||
// custom http errors
|
||||
func (e customhttperrors) Parse(ing *networking.Ingress) (interface{}, error) {
|
||||
c, err := parser.GetStringAnnotation("custom-http-errors", ing)
|
||||
c, err := parser.GetStringAnnotation("custom-http-errors", ing, e.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -55,3 +83,7 @@ func (e customhttperrors) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
|
||||
return codes, nil
|
||||
}
|
||||
|
||||
func (e customhttperrors) GetDocumentation() parser.AnnotationFields {
|
||||
return e.annotationConfig.Annotations
|
||||
}
|
||||
|
|
|
@ -25,19 +25,40 @@ import (
|
|||
"k8s.io/ingress-nginx/internal/ingress/resolver"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultBackendAnnotation = "default-backend"
|
||||
)
|
||||
|
||||
var defaultBackendAnnotations = parser.Annotation{
|
||||
Group: "backend",
|
||||
Annotations: parser.AnnotationFields{
|
||||
defaultBackendAnnotation: {
|
||||
Validator: parser.ValidateServiceName,
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This service will be used to handle the response when the configured service in the Ingress rule does not have any active endpoints.
|
||||
It will also be used to handle the error responses if both this annotation and the custom-http-errors annotation are set.`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
type backend struct {
|
||||
r resolver.Resolver
|
||||
r resolver.Resolver
|
||||
annotationConfig parser.Annotation
|
||||
}
|
||||
|
||||
// NewParser creates a new default backend annotation parser
|
||||
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
|
||||
return backend{r}
|
||||
return backend{
|
||||
r: r,
|
||||
annotationConfig: defaultBackendAnnotations,
|
||||
}
|
||||
}
|
||||
|
||||
// Parse parses the annotations contained in the ingress to use
|
||||
// a custom default backend
|
||||
func (db backend) Parse(ing *networking.Ingress) (interface{}, error) {
|
||||
s, err := parser.GetStringAnnotation("default-backend", ing)
|
||||
s, err := parser.GetStringAnnotation(defaultBackendAnnotation, ing, db.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -50,3 +71,7 @@ func (db backend) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
|
||||
return svc, nil
|
||||
}
|
||||
|
||||
func (db backend) GetDocumentation() parser.AnnotationFields {
|
||||
return db.annotationConfig.Annotations
|
||||
}
|
||||
|
|
|
@ -91,21 +91,51 @@ func (m mockService) GetService(name string) (*api.Service, error) {
|
|||
func TestAnnotations(t *testing.T) {
|
||||
ing := buildIngress()
|
||||
|
||||
data := map[string]string{}
|
||||
data[parser.GetAnnotationWithPrefix("default-backend")] = "demo-service"
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
fakeService := &mockService{}
|
||||
i, err := NewParser(fakeService).Parse(ing)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error %v", err)
|
||||
tests := map[string]struct {
|
||||
expectErr bool
|
||||
serviceName string
|
||||
}{
|
||||
"valid name": {
|
||||
serviceName: "demo-service",
|
||||
expectErr: false,
|
||||
},
|
||||
"not in backend": {
|
||||
serviceName: "demo1-service",
|
||||
expectErr: true,
|
||||
},
|
||||
"invalid dns name": {
|
||||
serviceName: "demo-service.something.tld",
|
||||
expectErr: true,
|
||||
},
|
||||
"invalid name": {
|
||||
serviceName: "something/xpto",
|
||||
expectErr: true,
|
||||
},
|
||||
"invalid characters": {
|
||||
serviceName: "something;xpto",
|
||||
expectErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
svc, ok := i.(*api.Service)
|
||||
if !ok {
|
||||
t.Errorf("expected *api.Service but got %v", svc)
|
||||
}
|
||||
if svc.Name != "demo-service" {
|
||||
t.Errorf("expected %v but got %v", "demo-service", svc.Name)
|
||||
for _, test := range tests {
|
||||
data := map[string]string{}
|
||||
data[parser.GetAnnotationWithPrefix(defaultBackendAnnotation)] = test.serviceName
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
fakeService := &mockService{}
|
||||
i, err := NewParser(fakeService).Parse(ing)
|
||||
if (err != nil) != test.expectErr {
|
||||
t.Errorf("expected error: %t got error: %t err value: %s. %+v", test.expectErr, err != nil, err, i)
|
||||
}
|
||||
|
||||
if !test.expectErr {
|
||||
svc, ok := i.(*api.Service)
|
||||
if !ok {
|
||||
t.Errorf("expected *api.Service but got %v", svc)
|
||||
}
|
||||
if svc.Name != test.serviceName {
|
||||
t.Errorf("expected %v but got %v", test.serviceName, svc.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,8 +22,10 @@ import (
|
|||
"time"
|
||||
|
||||
networking "k8s.io/api/networking/v1"
|
||||
"k8s.io/klog/v2"
|
||||
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/parser"
|
||||
"k8s.io/ingress-nginx/internal/ingress/errors"
|
||||
ing_errors "k8s.io/ingress-nginx/internal/ingress/errors"
|
||||
"k8s.io/ingress-nginx/internal/ingress/resolver"
|
||||
"k8s.io/ingress-nginx/internal/net"
|
||||
|
@ -32,6 +34,46 @@ import (
|
|||
|
||||
const defaultKey = "$remote_addr"
|
||||
|
||||
const (
|
||||
globalRateLimitAnnotation = "global-rate-limit"
|
||||
globalRateLimitWindowAnnotation = "global-rate-limit-window"
|
||||
globalRateLimitKeyAnnotation = "global-rate-limit-key"
|
||||
globalRateLimitIgnoredCidrsAnnotation = "global-rate-limit-ignored-cidrs"
|
||||
)
|
||||
|
||||
var globalRateLimitAnnotationConfig = parser.Annotation{
|
||||
Group: "ratelimit",
|
||||
Annotations: parser.AnnotationFields{
|
||||
globalRateLimitAnnotation: {
|
||||
Validator: parser.ValidateInt,
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation configures maximum allowed number of requests per window`,
|
||||
},
|
||||
globalRateLimitWindowAnnotation: {
|
||||
Validator: parser.ValidateDuration,
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `Configures a time window (i.e 1m) that the limit is applied`,
|
||||
},
|
||||
globalRateLimitKeyAnnotation: {
|
||||
Validator: parser.ValidateRegex(*parser.NGINXVariable, true),
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskHigh,
|
||||
Documentation: `This annotation Configures a key for counting the samples. Defaults to $remote_addr.
|
||||
You can also combine multiple NGINX variables here, like ${remote_addr}-${http_x_api_client} which would mean the limit will be applied to
|
||||
requests coming from the same API client (indicated by X-API-Client HTTP request header) with the same source IP address`,
|
||||
},
|
||||
globalRateLimitIgnoredCidrsAnnotation: {
|
||||
Validator: parser.ValidateCIDRs,
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskMedium,
|
||||
Documentation: `This annotation defines a comma separated list of IPs and CIDRs to match client IP against.
|
||||
When there's a match request is not considered for rate limiting.`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Config encapsulates all global rate limit attributes
|
||||
type Config struct {
|
||||
Namespace string `json:"namespace"`
|
||||
|
@ -63,12 +105,16 @@ func (l *Config) Equal(r *Config) bool {
|
|||
}
|
||||
|
||||
type globalratelimit struct {
|
||||
r resolver.Resolver
|
||||
r resolver.Resolver
|
||||
annotationConfig parser.Annotation
|
||||
}
|
||||
|
||||
// NewParser creates a new globalratelimit annotation parser
|
||||
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
|
||||
return globalratelimit{r}
|
||||
return globalratelimit{
|
||||
r: r,
|
||||
annotationConfig: globalRateLimitAnnotationConfig,
|
||||
}
|
||||
}
|
||||
|
||||
// Parse extracts globalratelimit annotations from the given ingress
|
||||
|
@ -76,8 +122,16 @@ func NewParser(r resolver.Resolver) parser.IngressAnnotation {
|
|||
func (a globalratelimit) Parse(ing *networking.Ingress) (interface{}, error) {
|
||||
config := &Config{}
|
||||
|
||||
limit, _ := parser.GetIntAnnotation("global-rate-limit", ing)
|
||||
rawWindowSize, _ := parser.GetStringAnnotation("global-rate-limit-window", ing)
|
||||
limit, err := parser.GetIntAnnotation(globalRateLimitAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil && errors.IsInvalidContent(err) {
|
||||
return nil, err
|
||||
}
|
||||
rawWindowSize, err := parser.GetStringAnnotation(globalRateLimitWindowAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil && errors.IsValidationError(err) {
|
||||
return config, ing_errors.LocationDenied{
|
||||
Reason: fmt.Errorf("failed to parse 'global-rate-limit-window' value: %w", err),
|
||||
}
|
||||
}
|
||||
|
||||
if limit == 0 || len(rawWindowSize) == 0 {
|
||||
return config, nil
|
||||
|
@ -90,12 +144,18 @@ func (a globalratelimit) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
}
|
||||
}
|
||||
|
||||
key, _ := parser.GetStringAnnotation("global-rate-limit-key", ing)
|
||||
key, err := parser.GetStringAnnotation(globalRateLimitKeyAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
klog.Warningf("invalid %s, defaulting to %s", globalRateLimitKeyAnnotation, defaultKey)
|
||||
}
|
||||
if len(key) == 0 {
|
||||
key = defaultKey
|
||||
}
|
||||
|
||||
rawIgnoredCIDRs, _ := parser.GetStringAnnotation("global-rate-limit-ignored-cidrs", ing)
|
||||
rawIgnoredCIDRs, err := parser.GetStringAnnotation(globalRateLimitIgnoredCidrsAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil && errors.IsInvalidContent(err) {
|
||||
return nil, err
|
||||
}
|
||||
ignoredCIDRs, err := net.ParseCIDRs(rawIgnoredCIDRs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -109,3 +169,7 @@ func (a globalratelimit) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func (a globalratelimit) GetDocumentation() parser.AnnotationFields {
|
||||
return a.annotationConfig.Annotations
|
||||
}
|
||||
|
|
|
@ -149,6 +149,22 @@ func TestGlobalRateLimiting(t *testing.T) {
|
|||
},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"global-rate-limit-complex-key",
|
||||
map[string]string{
|
||||
annRateLimit: "100",
|
||||
annRateLimitWindow: "2m",
|
||||
annRateLimitKey: "${http_x_api_user}${otherinfo}",
|
||||
},
|
||||
&Config{
|
||||
Namespace: expectedUID,
|
||||
Limit: 100,
|
||||
WindowSize: 120,
|
||||
Key: "${http_x_api_user}${otherinfo}",
|
||||
IgnoredCIDRs: make([]string, 0),
|
||||
},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"incorrect duration for window",
|
||||
map[string]string{
|
||||
|
@ -157,8 +173,8 @@ func TestGlobalRateLimiting(t *testing.T) {
|
|||
annRateLimitKey: "$http_x_api_user",
|
||||
},
|
||||
&Config{},
|
||||
ing_errors.LocationDenied{
|
||||
Reason: fmt.Errorf("failed to parse 'global-rate-limit-window' value: time: unknown unit \"mb\" in duration \"2mb\""),
|
||||
ing_errors.ValidationError{
|
||||
Reason: fmt.Errorf("failed to parse 'global-rate-limit-window' value: annotation nginx.ingress.kubernetes.io/global-rate-limit-window contains invalid value"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -168,7 +184,7 @@ func TestGlobalRateLimiting(t *testing.T) {
|
|||
|
||||
i, actualErr := NewParser(mockBackend{}).Parse(ing)
|
||||
if (testCase.expectedErr == nil || actualErr == nil) && testCase.expectedErr != actualErr {
|
||||
t.Errorf("expected error 'nil' but got '%v'", actualErr)
|
||||
t.Errorf("%s expected error '%v' but got '%v'", testCase.title, testCase.expectedErr, actualErr)
|
||||
} else if testCase.expectedErr != nil && actualErr != nil &&
|
||||
testCase.expectedErr.Error() != actualErr.Error() {
|
||||
t.Errorf("expected error '%v' but got '%v'", testCase.expectedErr, actualErr)
|
||||
|
|
|
@ -23,17 +23,41 @@ import (
|
|||
"k8s.io/ingress-nginx/internal/ingress/resolver"
|
||||
)
|
||||
|
||||
const (
|
||||
http2PushPreloadAnnotation = "http2-push-preload"
|
||||
)
|
||||
|
||||
var http2PushPreloadAnnotations = parser.Annotation{
|
||||
Group: "", // TODO: TBD
|
||||
Annotations: parser.AnnotationFields{
|
||||
http2PushPreloadAnnotation: {
|
||||
Validator: parser.ValidateBool,
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `Enables automatic conversion of preload links specified in the “Link” response header fields into push requests`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
type http2PushPreload struct {
|
||||
r resolver.Resolver
|
||||
r resolver.Resolver
|
||||
annotationConfig parser.Annotation
|
||||
}
|
||||
|
||||
// NewParser creates a new http2PushPreload annotation parser
|
||||
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
|
||||
return http2PushPreload{r}
|
||||
return http2PushPreload{
|
||||
r: r,
|
||||
annotationConfig: http2PushPreloadAnnotations,
|
||||
}
|
||||
}
|
||||
|
||||
// Parse parses the annotations contained in the ingress rule
|
||||
// used to add http2 push preload to the server
|
||||
func (h2pp http2PushPreload) Parse(ing *networking.Ingress) (interface{}, error) {
|
||||
return parser.GetBoolAnnotation("http2-push-preload", ing)
|
||||
return parser.GetBoolAnnotation(http2PushPreloadAnnotation, ing, h2pp.annotationConfig.Annotations)
|
||||
}
|
||||
|
||||
func (h2pp http2PushPreload) GetDocumentation() parser.AnnotationFields {
|
||||
return h2pp.annotationConfig.Annotations
|
||||
}
|
||||
|
|
|
@ -23,11 +23,12 @@ import (
|
|||
networking "k8s.io/api/networking/v1"
|
||||
meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/parser"
|
||||
"k8s.io/ingress-nginx/internal/ingress/errors"
|
||||
"k8s.io/ingress-nginx/internal/ingress/resolver"
|
||||
)
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
annotation := parser.GetAnnotationWithPrefix("http2-push-preload")
|
||||
annotation := parser.GetAnnotationWithPrefix(http2PushPreloadAnnotation)
|
||||
ap := NewParser(&resolver.Mock{})
|
||||
if ap == nil {
|
||||
t.Fatalf("expected a parser.IngressAnnotation but returned nil")
|
||||
|
@ -36,12 +37,14 @@ func TestParse(t *testing.T) {
|
|||
testCases := []struct {
|
||||
annotations map[string]string
|
||||
expected bool
|
||||
expectErr bool
|
||||
}{
|
||||
{map[string]string{annotation: "true"}, true},
|
||||
{map[string]string{annotation: "1"}, true},
|
||||
{map[string]string{annotation: ""}, false},
|
||||
{map[string]string{}, false},
|
||||
{nil, false},
|
||||
{map[string]string{annotation: "true"}, true, false},
|
||||
{map[string]string{annotation: "1"}, true, false},
|
||||
{map[string]string{annotation: "xpto"}, false, true},
|
||||
{map[string]string{annotation: ""}, false, false},
|
||||
{map[string]string{}, false, false},
|
||||
{nil, false, false},
|
||||
}
|
||||
|
||||
ing := &networking.Ingress{
|
||||
|
@ -54,7 +57,10 @@ func TestParse(t *testing.T) {
|
|||
|
||||
for _, testCase := range testCases {
|
||||
ing.SetAnnotations(testCase.annotations)
|
||||
result, _ := ap.Parse(ing)
|
||||
result, err := ap.Parse(ing)
|
||||
if ((err != nil) != testCase.expectErr) && !errors.IsInvalidContent(err) && !errors.IsMissingAnnotations(err) {
|
||||
t.Fatalf("expected error: %t got error: %t err value: %s. %+v", testCase.expectErr, err != nil, err, testCase.annotations)
|
||||
}
|
||||
if result != testCase.expected {
|
||||
t.Errorf("expected %v but returned %v, annotations: %s", testCase.expected, result, testCase.annotations)
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
package ipwhitelist
|
||||
package ipallowlist
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
@ -30,6 +30,24 @@ import (
|
|||
"k8s.io/ingress-nginx/pkg/util/sets"
|
||||
)
|
||||
|
||||
const (
|
||||
ipWhitelistAnnotation = "whitelist-source-range"
|
||||
ipAllowlistAnnotation = "allowlist-source-range"
|
||||
)
|
||||
|
||||
var allowlistAnnotations = parser.Annotation{
|
||||
Group: "acl",
|
||||
Annotations: parser.AnnotationFields{
|
||||
ipAllowlistAnnotation: {
|
||||
Validator: parser.ValidateCIDRs,
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskMedium, // Failure on parsing this may cause undesired access
|
||||
Documentation: `This annotation allows setting a list of IPs and networks allowed to access this Location`,
|
||||
AnnotationAliases: []string{ipWhitelistAnnotation},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// SourceRange returns the CIDR
|
||||
type SourceRange struct {
|
||||
CIDR []string `json:"cidr,omitempty"`
|
||||
|
@ -47,36 +65,47 @@ func (sr1 *SourceRange) Equal(sr2 *SourceRange) bool {
|
|||
return sets.StringElementsMatch(sr1.CIDR, sr2.CIDR)
|
||||
}
|
||||
|
||||
type ipwhitelist struct {
|
||||
r resolver.Resolver
|
||||
type ipallowlist struct {
|
||||
r resolver.Resolver
|
||||
annotationConfig parser.Annotation
|
||||
}
|
||||
|
||||
// NewParser creates a new whitelist annotation parser
|
||||
// NewParser creates a new ipallowlist annotation parser
|
||||
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
|
||||
return ipwhitelist{r}
|
||||
return ipallowlist{
|
||||
r: r,
|
||||
annotationConfig: allowlistAnnotations,
|
||||
}
|
||||
}
|
||||
|
||||
// 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 *networking.Ingress) (interface{}, error) {
|
||||
func (a ipallowlist) Parse(ing *networking.Ingress) (interface{}, error) {
|
||||
defBackend := a.r.GetDefaultBackend()
|
||||
|
||||
defaultWhitelistSourceRange := make([]string, len(defBackend.WhitelistSourceRange))
|
||||
copy(defaultWhitelistSourceRange, defBackend.WhitelistSourceRange)
|
||||
sort.Strings(defaultWhitelistSourceRange)
|
||||
defaultAllowlistSourceRange := make([]string, len(defBackend.WhitelistSourceRange))
|
||||
copy(defaultAllowlistSourceRange, defBackend.WhitelistSourceRange)
|
||||
sort.Strings(defaultAllowlistSourceRange)
|
||||
|
||||
val, err := parser.GetStringAnnotation("whitelist-source-range", ing)
|
||||
val, err := parser.GetStringAnnotation(ipAllowlistAnnotation, ing, a.annotationConfig.Annotations)
|
||||
// A missing annotation is not a problem, just use the default
|
||||
if err == ing_errors.ErrMissingAnnotations {
|
||||
return &SourceRange{CIDR: defaultWhitelistSourceRange}, nil
|
||||
if err != nil {
|
||||
if err == ing_errors.ErrMissingAnnotations {
|
||||
return &SourceRange{CIDR: defaultAllowlistSourceRange}, nil
|
||||
}
|
||||
|
||||
return &SourceRange{CIDR: defaultAllowlistSourceRange}, ing_errors.LocationDenied{
|
||||
Reason: err,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
values := strings.Split(val, ",")
|
||||
ipnets, ips, err := net.ParseIPNets(values...)
|
||||
if err != nil && len(ips) == 0 {
|
||||
return &SourceRange{CIDR: defaultWhitelistSourceRange}, ing_errors.LocationDenied{
|
||||
return &SourceRange{CIDR: defaultAllowlistSourceRange}, ing_errors.LocationDenied{
|
||||
Reason: fmt.Errorf("the annotation does not contain a valid IP address or network: %w", err),
|
||||
}
|
||||
}
|
||||
|
@ -93,3 +122,7 @@ func (a ipwhitelist) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
|
||||
return &SourceRange{cidrs}, nil
|
||||
}
|
||||
|
||||
func (a ipallowlist) GetDocumentation() parser.AnnotationFields {
|
||||
return a.annotationConfig.Annotations
|
||||
}
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
package ipwhitelist
|
||||
package ipallowlist
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
@ -86,12 +86,12 @@ func TestParseAnnotations(t *testing.T) {
|
|||
"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",
|
||||
errOut: "annotation nginx.ingress.kubernetes.io/allowlist-source-range contains invalid value",
|
||||
},
|
||||
"test parse a empty net": {
|
||||
net: "",
|
||||
expectErr: true,
|
||||
errOut: "the annotation does not contain a valid IP address or network: invalid CIDR address: ",
|
||||
errOut: "the annotation nginx.ingress.kubernetes.io/allowlist-source-range does not contain a valid value ()",
|
||||
},
|
||||
"test parse multiple valid cidr": {
|
||||
net: "2.2.2.2/32,1.1.1.1/32,3.3.3.0/24",
|
||||
|
@ -102,16 +102,16 @@ func TestParseAnnotations(t *testing.T) {
|
|||
|
||||
for testName, test := range tests {
|
||||
data := map[string]string{}
|
||||
data[parser.GetAnnotationWithPrefix("whitelist-source-range")] = test.net
|
||||
data[parser.GetAnnotationWithPrefix(ipAllowlistAnnotation)] = test.net
|
||||
ing.SetAnnotations(data)
|
||||
p := NewParser(&resolver.Mock{})
|
||||
i, err := p.Parse(ing)
|
||||
if err != nil && !test.expectErr {
|
||||
t.Errorf("%v:unexpected error: %v", testName, err)
|
||||
if (err != nil) != test.expectErr {
|
||||
t.Errorf("%s expected error: %t got error: %t err value: %s. %+v", testName, test.expectErr, err != nil, err, i)
|
||||
}
|
||||
if test.expectErr {
|
||||
if test.expectErr && err != nil {
|
||||
if err.Error() != test.errOut {
|
||||
t.Errorf("%v:expected error: %v but %v return", testName, test.errOut, err.Error())
|
||||
t.Errorf("expected error %s but got %s", test.errOut, err)
|
||||
}
|
||||
}
|
||||
if !test.expectErr {
|
||||
|
@ -137,7 +137,7 @@ func (m mockBackend) GetDefaultBackend() defaults.Backend {
|
|||
}
|
||||
}
|
||||
|
||||
// Test that when we have a whitelist set on the Backend that is used when we
|
||||
// Test that when we have a allowlist set on the Backend that is used when we
|
||||
// don't have the annotation
|
||||
func TestParseAnnotationsWithDefaultConfig(t *testing.T) {
|
||||
ing := buildIngress()
|
||||
|
@ -158,12 +158,12 @@ func TestParseAnnotationsWithDefaultConfig(t *testing.T) {
|
|||
"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",
|
||||
errOut: "annotation nginx.ingress.kubernetes.io/allowlist-source-range contains invalid value",
|
||||
},
|
||||
"test parse a empty net": {
|
||||
net: "",
|
||||
expectErr: true,
|
||||
errOut: "the annotation does not contain a valid IP address or network: invalid CIDR address: ",
|
||||
errOut: "the annotation nginx.ingress.kubernetes.io/allowlist-source-range does not contain a valid value ()",
|
||||
},
|
||||
"test parse multiple valid cidr": {
|
||||
net: "2.2.2.2/32,1.1.1.1/32,3.3.3.0/24",
|
||||
|
@ -174,16 +174,67 @@ func TestParseAnnotationsWithDefaultConfig(t *testing.T) {
|
|||
|
||||
for testName, test := range tests {
|
||||
data := map[string]string{}
|
||||
data[parser.GetAnnotationWithPrefix("whitelist-source-range")] = test.net
|
||||
data[parser.GetAnnotationWithPrefix(ipAllowlistAnnotation)] = 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 (err != nil) != test.expectErr {
|
||||
t.Errorf("expected error: %t got error: %t err value: %s. %+v", test.expectErr, err != nil, err, i)
|
||||
}
|
||||
if test.expectErr {
|
||||
if test.expectErr && err != nil {
|
||||
if err.Error() != test.errOut {
|
||||
t.Errorf("%v:expected error: %v but %v return", testName, test.errOut, err.Error())
|
||||
t.Errorf("expected error %s but got %s", test.errOut, err)
|
||||
}
|
||||
}
|
||||
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 TestLegacyAnnotation(t *testing.T) {
|
||||
ing := buildIngress()
|
||||
|
||||
mockBackend := mockBackend{}
|
||||
|
||||
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 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[parser.GetAnnotationWithPrefix(ipWhitelistAnnotation)] = test.net
|
||||
ing.SetAnnotations(data)
|
||||
p := NewParser(mockBackend)
|
||||
i, err := p.Parse(ing)
|
||||
if (err != nil) != test.expectErr {
|
||||
t.Errorf("expected error: %t got error: %t err value: %s. %+v", test.expectErr, err != nil, err, i)
|
||||
}
|
||||
if test.expectErr && err != nil {
|
||||
if err.Error() != test.errOut {
|
||||
t.Errorf("expected error %s but got %s", test.errOut, err)
|
||||
}
|
||||
}
|
||||
if !test.expectErr {
|
|
@ -30,6 +30,22 @@ import (
|
|||
"k8s.io/ingress-nginx/pkg/util/sets"
|
||||
)
|
||||
|
||||
const (
|
||||
ipDenylistAnnotation = "denylist-source-range"
|
||||
)
|
||||
|
||||
var denylistAnnotations = parser.Annotation{
|
||||
Group: "acl",
|
||||
Annotations: parser.AnnotationFields{
|
||||
ipDenylistAnnotation: {
|
||||
Validator: parser.ValidateCIDRs,
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskMedium, // Failure on parsing this may cause undesired access
|
||||
Documentation: `This annotation allows setting a list of IPs and networks that should be blocked to access this Location`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// SourceRange returns the CIDR
|
||||
type SourceRange struct {
|
||||
CIDR []string `json:"cidr,omitempty"`
|
||||
|
@ -48,12 +64,16 @@ func (sr1 *SourceRange) Equal(sr2 *SourceRange) bool {
|
|||
}
|
||||
|
||||
type ipdenylist struct {
|
||||
r resolver.Resolver
|
||||
r resolver.Resolver
|
||||
annotationConfig parser.Annotation
|
||||
}
|
||||
|
||||
// NewParser creates a new denylist annotation parser
|
||||
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
|
||||
return ipdenylist{r}
|
||||
return ipdenylist{
|
||||
r: r,
|
||||
annotationConfig: denylistAnnotations,
|
||||
}
|
||||
}
|
||||
|
||||
// ParseAnnotations parses the annotations contained in the ingress
|
||||
|
@ -67,10 +87,16 @@ func (a ipdenylist) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
copy(defaultDenylistSourceRange, defBackend.DenylistSourceRange)
|
||||
sort.Strings(defaultDenylistSourceRange)
|
||||
|
||||
val, err := parser.GetStringAnnotation("denylist-source-range", ing)
|
||||
// A missing annotation is not a problem, just use the default
|
||||
if err == ing_errors.ErrMissingAnnotations {
|
||||
return &SourceRange{CIDR: defaultDenylistSourceRange}, nil
|
||||
val, err := parser.GetStringAnnotation(ipDenylistAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
if err == ing_errors.ErrMissingAnnotations {
|
||||
return &SourceRange{CIDR: defaultDenylistSourceRange}, nil
|
||||
}
|
||||
|
||||
return &SourceRange{CIDR: defaultDenylistSourceRange}, ing_errors.LocationDenied{
|
||||
Reason: err,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
values := strings.Split(val, ",")
|
||||
|
@ -93,3 +119,7 @@ func (a ipdenylist) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
|
||||
return &SourceRange{cidrs}, nil
|
||||
}
|
||||
|
||||
func (a ipdenylist) GetDocumentation() parser.AnnotationFields {
|
||||
return a.annotationConfig.Annotations
|
||||
}
|
||||
|
|
|
@ -86,17 +86,17 @@ func TestParseAnnotations(t *testing.T) {
|
|||
"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",
|
||||
errOut: "annotation nginx.ingress.kubernetes.io/denylist-source-range contains invalid value",
|
||||
},
|
||||
"test parse a empty net": {
|
||||
net: "",
|
||||
expectErr: true,
|
||||
errOut: "the annotation does not contain a valid IP address or network: invalid CIDR address: ",
|
||||
errOut: "the annotation nginx.ingress.kubernetes.io/denylist-source-range does not contain a valid value ()",
|
||||
},
|
||||
"test parse a malicious escaped string": {
|
||||
net: `10.0.0.0/8"rm /tmp",11.0.0.0/8`,
|
||||
expectErr: true,
|
||||
errOut: `the annotation does not contain a valid IP address or network: invalid CIDR address: 10.0.0.0/8"rm /tmp"`,
|
||||
errOut: `annotation nginx.ingress.kubernetes.io/denylist-source-range contains invalid value`,
|
||||
},
|
||||
"test parse multiple valid cidr": {
|
||||
net: "2.2.2.2/32,1.1.1.1/32,3.3.3.0/24",
|
||||
|
@ -107,16 +107,16 @@ func TestParseAnnotations(t *testing.T) {
|
|||
|
||||
for testName, test := range tests {
|
||||
data := map[string]string{}
|
||||
data[parser.GetAnnotationWithPrefix("denylist-source-range")] = test.net
|
||||
data[parser.GetAnnotationWithPrefix(ipDenylistAnnotation)] = test.net
|
||||
ing.SetAnnotations(data)
|
||||
p := NewParser(&resolver.Mock{})
|
||||
i, err := p.Parse(ing)
|
||||
if err != nil && !test.expectErr {
|
||||
t.Errorf("%v:unexpected error: %v", testName, err)
|
||||
if (err != nil) != test.expectErr {
|
||||
t.Errorf("expected error: %t got error: %t err value: %s. %+v", test.expectErr, err != nil, err, i)
|
||||
}
|
||||
if test.expectErr {
|
||||
if test.expectErr && err != nil {
|
||||
if err.Error() != test.errOut {
|
||||
t.Errorf("%v:expected error: %v but %v return", testName, test.errOut, err.Error())
|
||||
t.Errorf("expected error %s but got %s", test.errOut, err)
|
||||
}
|
||||
}
|
||||
if !test.expectErr {
|
||||
|
@ -163,12 +163,12 @@ func TestParseAnnotationsWithDefaultConfig(t *testing.T) {
|
|||
"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",
|
||||
errOut: "annotation nginx.ingress.kubernetes.io/denylist-source-range contains invalid value",
|
||||
},
|
||||
"test parse a empty net": {
|
||||
net: "",
|
||||
expectErr: true,
|
||||
errOut: "the annotation does not contain a valid IP address or network: invalid CIDR address: ",
|
||||
errOut: "the annotation nginx.ingress.kubernetes.io/denylist-source-range does not contain a valid value ()",
|
||||
},
|
||||
"test parse multiple valid cidr": {
|
||||
net: "2.2.2.2/32,1.1.1.1/32,3.3.3.0/24",
|
||||
|
@ -179,16 +179,16 @@ func TestParseAnnotationsWithDefaultConfig(t *testing.T) {
|
|||
|
||||
for testName, test := range tests {
|
||||
data := map[string]string{}
|
||||
data[parser.GetAnnotationWithPrefix("denylist-source-range")] = test.net
|
||||
data[parser.GetAnnotationWithPrefix(ipDenylistAnnotation)] = 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 (err != nil) != test.expectErr {
|
||||
t.Errorf("expected error: %t got error: %t err value: %s. %+v", test.expectErr, err != nil, err, i)
|
||||
}
|
||||
if test.expectErr {
|
||||
if test.expectErr && err != nil {
|
||||
if err.Error() != test.errOut {
|
||||
t.Errorf("%v:expected error: %v but %v return", testName, test.errOut, err.Error())
|
||||
t.Errorf("expected error %s but got %s", test.errOut, err)
|
||||
}
|
||||
}
|
||||
if !test.expectErr {
|
||||
|
|
|
@ -23,18 +23,47 @@ import (
|
|||
"k8s.io/ingress-nginx/internal/ingress/resolver"
|
||||
)
|
||||
|
||||
type loadbalancing struct {
|
||||
r resolver.Resolver
|
||||
// LB Alghorithms are defined in https://github.com/kubernetes/ingress-nginx/blob/d3e75b056f77be54e01bdb18675f1bb46caece31/rootfs/etc/nginx/lua/balancer.lua#L28
|
||||
|
||||
const (
|
||||
loadBalanceAlghoritmAnnotation = "load-balance"
|
||||
)
|
||||
|
||||
var loadBalanceAlghoritms = []string{"round_robin", "chash", "chashsubset", "sticky_balanced", "sticky_persistent", "ewma"}
|
||||
|
||||
var loadBalanceAnnotations = parser.Annotation{
|
||||
Group: "backend",
|
||||
Annotations: parser.AnnotationFields{
|
||||
loadBalanceAlghoritmAnnotation: {
|
||||
Validator: parser.ValidateOptions(loadBalanceAlghoritms, true, true),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation allows setting the load balancing alghorithm that should be used. If none is specified, defaults to
|
||||
the default configured by Ingress admin, otherwise to round_robin`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// NewParser creates a new CORS annotation parser
|
||||
type loadbalancing struct {
|
||||
r resolver.Resolver
|
||||
annotationConfig parser.Annotation
|
||||
}
|
||||
|
||||
// NewParser creates a new Load Balancer annotation parser
|
||||
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
|
||||
return loadbalancing{r}
|
||||
return loadbalancing{
|
||||
r: r,
|
||||
annotationConfig: loadBalanceAnnotations,
|
||||
}
|
||||
}
|
||||
|
||||
// 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 loadbalancing) Parse(ing *networking.Ingress) (interface{}, error) {
|
||||
return parser.GetStringAnnotation("load-balance", ing)
|
||||
return parser.GetStringAnnotation(loadBalanceAlghoritmAnnotation, ing, a.annotationConfig.Annotations)
|
||||
}
|
||||
|
||||
func (a loadbalancing) GetDocumentation() parser.AnnotationFields {
|
||||
return a.annotationConfig.Annotations
|
||||
}
|
||||
|
|
|
@ -38,7 +38,8 @@ func TestParse(t *testing.T) {
|
|||
annotations map[string]string
|
||||
expected string
|
||||
}{
|
||||
{map[string]string{annotation: "ip_hash"}, "ip_hash"},
|
||||
{map[string]string{annotation: "ewma"}, "ewma"},
|
||||
{map[string]string{annotation: "ip_hash"}, ""}, // This is invalid and should not return anything
|
||||
{map[string]string{}, ""},
|
||||
{nil, ""},
|
||||
}
|
||||
|
|
|
@ -23,8 +23,32 @@ import (
|
|||
"k8s.io/ingress-nginx/internal/ingress/resolver"
|
||||
)
|
||||
|
||||
const (
|
||||
enableAccessLogAnnotation = "enable-access-log"
|
||||
enableRewriteLogAnnotation = "enable-rewrite-log"
|
||||
)
|
||||
|
||||
var logAnnotations = parser.Annotation{
|
||||
Group: "log",
|
||||
Annotations: parser.AnnotationFields{
|
||||
enableAccessLogAnnotation: {
|
||||
Validator: parser.ValidateBool,
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This configuration setting allows you to control if this location should generate an access_log`,
|
||||
},
|
||||
enableRewriteLogAnnotation: {
|
||||
Validator: parser.ValidateBool,
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This configuration setting allows you to control if this location should generate logs from the rewrite feature usage`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
type log struct {
|
||||
r resolver.Resolver
|
||||
r resolver.Resolver
|
||||
annotationConfig parser.Annotation
|
||||
}
|
||||
|
||||
// Config contains the configuration to be used in the Ingress
|
||||
|
@ -48,7 +72,10 @@ func (bd1 *Config) Equal(bd2 *Config) bool {
|
|||
|
||||
// NewParser creates a new log annotations parser
|
||||
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
|
||||
return log{r}
|
||||
return log{
|
||||
r: r,
|
||||
annotationConfig: logAnnotations,
|
||||
}
|
||||
}
|
||||
|
||||
// Parse parses the annotations contained in the ingress
|
||||
|
@ -57,15 +84,19 @@ func (l log) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
var err error
|
||||
config := &Config{}
|
||||
|
||||
config.Access, err = parser.GetBoolAnnotation("enable-access-log", ing)
|
||||
config.Access, err = parser.GetBoolAnnotation(enableAccessLogAnnotation, ing, l.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
config.Access = true
|
||||
}
|
||||
|
||||
config.Rewrite, err = parser.GetBoolAnnotation("enable-rewrite-log", ing)
|
||||
config.Rewrite, err = parser.GetBoolAnnotation(enableRewriteLogAnnotation, ing, l.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
config.Rewrite = false
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func (l log) GetDocumentation() parser.AnnotationFields {
|
||||
return l.annotationConfig.Annotations
|
||||
}
|
||||
|
|
|
@ -73,7 +73,7 @@ func TestIngressAccessLogConfig(t *testing.T) {
|
|||
ing := buildIngress()
|
||||
|
||||
data := map[string]string{}
|
||||
data[parser.GetAnnotationWithPrefix("enable-access-log")] = "false"
|
||||
data[parser.GetAnnotationWithPrefix(enableAccessLogAnnotation)] = "false"
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
log, _ := NewParser(&resolver.Mock{}).Parse(ing)
|
||||
|
@ -91,7 +91,7 @@ func TestIngressRewriteLogConfig(t *testing.T) {
|
|||
ing := buildIngress()
|
||||
|
||||
data := map[string]string{}
|
||||
data[parser.GetAnnotationWithPrefix("enable-rewrite-log")] = "true"
|
||||
data[parser.GetAnnotationWithPrefix(enableRewriteLogAnnotation)] = "true"
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
log, _ := NewParser(&resolver.Mock{}).Parse(ing)
|
||||
|
@ -104,3 +104,21 @@ func TestIngressRewriteLogConfig(t *testing.T) {
|
|||
t.Errorf("expected rewrite log to be enabled but it is disabled")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidBoolConfig(t *testing.T) {
|
||||
ing := buildIngress()
|
||||
|
||||
data := map[string]string{}
|
||||
data[parser.GetAnnotationWithPrefix(enableRewriteLogAnnotation)] = "blo"
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
log, _ := NewParser(&resolver.Mock{}).Parse(ing)
|
||||
nginxLogs, ok := log.(*Config)
|
||||
if !ok {
|
||||
t.Errorf("expected a Config type")
|
||||
}
|
||||
|
||||
if !nginxLogs.Access {
|
||||
t.Errorf("expected access log to be enabled due to invalid config, but it is disabled")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,13 +18,50 @@ package mirror
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
networking "k8s.io/api/networking/v1"
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/parser"
|
||||
"k8s.io/ingress-nginx/internal/ingress/errors"
|
||||
"k8s.io/ingress-nginx/internal/ingress/resolver"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
mirrorRequestBodyAnnotation = "mirror-request-body"
|
||||
mirrorTargetAnnotation = "mirror-target"
|
||||
mirrorHostAnnotation = "mirror-host"
|
||||
)
|
||||
|
||||
var (
|
||||
OnOffRegex = regexp.MustCompile(`^(on|off)$`)
|
||||
)
|
||||
|
||||
var mirrorAnnotation = parser.Annotation{
|
||||
Group: "mirror",
|
||||
Annotations: parser.AnnotationFields{
|
||||
mirrorRequestBodyAnnotation: {
|
||||
Validator: parser.ValidateRegex(*OnOffRegex, true),
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation defines if the request-body should be sent to the mirror backend. Can be 'on' or 'off'`,
|
||||
},
|
||||
mirrorTargetAnnotation: {
|
||||
Validator: parser.ValidateServerName,
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskHigh,
|
||||
Documentation: `This annotation enables a request to be mirrored to a mirror backend.`,
|
||||
},
|
||||
mirrorHostAnnotation: {
|
||||
Validator: parser.ValidateServerName,
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskHigh,
|
||||
Documentation: `This annotation defines if a specific Host header should be set for mirrored request.`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Config returns the mirror to use in a given location
|
||||
type Config struct {
|
||||
Source string `json:"source"`
|
||||
|
@ -63,12 +100,16 @@ func (m1 *Config) Equal(m2 *Config) bool {
|
|||
}
|
||||
|
||||
type mirror struct {
|
||||
r resolver.Resolver
|
||||
r resolver.Resolver
|
||||
annotationConfig parser.Annotation
|
||||
}
|
||||
|
||||
// NewParser creates a new mirror configuration annotation parser
|
||||
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
|
||||
return mirror{r}
|
||||
return mirror{
|
||||
r: r,
|
||||
annotationConfig: mirrorAnnotation,
|
||||
}
|
||||
}
|
||||
|
||||
// ParseAnnotations parses the annotations contained in the ingress
|
||||
|
@ -79,19 +120,29 @@ func (a mirror) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
}
|
||||
|
||||
var err error
|
||||
config.RequestBody, err = parser.GetStringAnnotation("mirror-request-body", ing)
|
||||
config.RequestBody, err = parser.GetStringAnnotation(mirrorRequestBodyAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil || config.RequestBody != "off" {
|
||||
if errors.IsValidationError(err) {
|
||||
klog.Warningf("annotation %s contains invalid value", mirrorRequestBodyAnnotation)
|
||||
}
|
||||
config.RequestBody = "on"
|
||||
}
|
||||
|
||||
config.Target, err = parser.GetStringAnnotation("mirror-target", ing)
|
||||
config.Target, err = parser.GetStringAnnotation(mirrorTargetAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
config.Target = ""
|
||||
config.Source = ""
|
||||
if errors.IsValidationError(err) {
|
||||
klog.Warningf("annotation %s contains invalid value, defaulting", mirrorTargetAnnotation)
|
||||
} else {
|
||||
config.Target = ""
|
||||
config.Source = ""
|
||||
}
|
||||
}
|
||||
|
||||
config.Host, err = parser.GetStringAnnotation("mirror-host", ing)
|
||||
config.Host, err = parser.GetStringAnnotation(mirrorHostAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
if errors.IsValidationError(err) {
|
||||
klog.Warningf("annotation %s contains invalid value, defaulting", mirrorHostAnnotation)
|
||||
}
|
||||
if config.Target != "" {
|
||||
target := strings.Split(config.Target, "$")
|
||||
|
||||
|
@ -106,3 +157,7 @@ func (a mirror) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func (a mirror) GetDocumentation() parser.AnnotationFields {
|
||||
return a.annotationConfig.Annotations
|
||||
}
|
||||
|
|
|
@ -94,13 +94,13 @@ func TestParse(t *testing.T) {
|
|||
Source: ngxURI,
|
||||
RequestBody: "on",
|
||||
Target: "http://some.test.env.com",
|
||||
Host: "someInvalidParm.%^&*()_=!@#'\"",
|
||||
Host: "some.test.env.com",
|
||||
}},
|
||||
{map[string]string{backendURL: "http://some.test.env.com", host: "_sbrubles-i\"@xpto:12345"}, &Config{
|
||||
Source: ngxURI,
|
||||
RequestBody: "on",
|
||||
Target: "http://some.test.env.com",
|
||||
Host: "_sbrubles-i\"@xpto:12345",
|
||||
Host: "some.test.env.com",
|
||||
}},
|
||||
}
|
||||
|
||||
|
@ -115,9 +115,12 @@ func TestParse(t *testing.T) {
|
|||
|
||||
for _, testCase := range testCases {
|
||||
ing.SetAnnotations(testCase.annotations)
|
||||
result, _ := ap.Parse(ing)
|
||||
result, err := ap.Parse(ing)
|
||||
if err != nil {
|
||||
t.Errorf(err.Error())
|
||||
}
|
||||
if !reflect.DeepEqual(result, testCase.expected) {
|
||||
t.Errorf("expected %v but returned %v, annotations: %s", testCase.expected, result, testCase.annotations)
|
||||
t.Errorf("expected %+v but returned %+v, annotations: %s", testCase.expected, result, testCase.annotations)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,9 +19,48 @@ package modsecurity
|
|||
import (
|
||||
networking "k8s.io/api/networking/v1"
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/parser"
|
||||
"k8s.io/ingress-nginx/internal/ingress/errors"
|
||||
"k8s.io/ingress-nginx/internal/ingress/resolver"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
modsecEnableAnnotation = "enable-modsecurity"
|
||||
modsecEnableOwaspCoreAnnotation = "enable-owasp-core-rules"
|
||||
modesecTransactionIdAnnotation = "modsecurity-transaction-id"
|
||||
modsecSnippetAnnotation = "modsecurity-snippet"
|
||||
)
|
||||
|
||||
var modsecurityAnnotation = parser.Annotation{
|
||||
Group: "modsecurity",
|
||||
Annotations: parser.AnnotationFields{
|
||||
modsecEnableAnnotation: {
|
||||
Validator: parser.ValidateBool,
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation enables ModSecurity`,
|
||||
},
|
||||
modsecEnableOwaspCoreAnnotation: {
|
||||
Validator: parser.ValidateBool,
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation enables the OWASP Core Rule Set`,
|
||||
},
|
||||
modesecTransactionIdAnnotation: {
|
||||
Validator: parser.ValidateRegex(*parser.NGINXVariable, true),
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskHigh,
|
||||
Documentation: `This annotation enables passing an NGINX variable to ModSecurity.`,
|
||||
},
|
||||
modsecSnippetAnnotation: {
|
||||
Validator: parser.ValidateNull,
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskCritical,
|
||||
Documentation: `This annotation enables adding a specific snippet configuration for ModSecurity`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Config contains ModSecurity Configuration items
|
||||
type Config struct {
|
||||
Enable bool `json:"enable-modsecurity"`
|
||||
|
@ -60,11 +99,15 @@ func (modsec1 *Config) Equal(modsec2 *Config) bool {
|
|||
|
||||
// NewParser creates a new ModSecurity annotation parser
|
||||
func NewParser(resolver resolver.Resolver) parser.IngressAnnotation {
|
||||
return modSecurity{resolver}
|
||||
return modSecurity{
|
||||
r: resolver,
|
||||
annotationConfig: modsecurityAnnotation,
|
||||
}
|
||||
}
|
||||
|
||||
type modSecurity struct {
|
||||
r resolver.Resolver
|
||||
r resolver.Resolver
|
||||
annotationConfig parser.Annotation
|
||||
}
|
||||
|
||||
// Parse parses the annotations contained in the ingress
|
||||
|
@ -74,26 +117,39 @@ func (a modSecurity) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
config := &Config{}
|
||||
|
||||
config.EnableSet = true
|
||||
config.Enable, err = parser.GetBoolAnnotation("enable-modsecurity", ing)
|
||||
config.Enable, err = parser.GetBoolAnnotation(modsecEnableAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
if errors.IsInvalidContent(err) {
|
||||
klog.Warningf("annotation %s contains invalid directive, defaulting to false", modsecEnableAnnotation)
|
||||
}
|
||||
config.Enable = false
|
||||
config.EnableSet = false
|
||||
}
|
||||
|
||||
config.OWASPRules, err = parser.GetBoolAnnotation("enable-owasp-core-rules", ing)
|
||||
config.OWASPRules, err = parser.GetBoolAnnotation(modsecEnableOwaspCoreAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
if errors.IsInvalidContent(err) {
|
||||
klog.Warningf("annotation %s contains invalid directive, defaulting to false", modsecEnableOwaspCoreAnnotation)
|
||||
}
|
||||
config.OWASPRules = false
|
||||
}
|
||||
|
||||
config.TransactionID, err = parser.GetStringAnnotation("modsecurity-transaction-id", ing)
|
||||
config.TransactionID, err = parser.GetStringAnnotation(modesecTransactionIdAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
if errors.IsInvalidContent(err) {
|
||||
klog.Warningf("annotation %s contains invalid directive, defaulting", modesecTransactionIdAnnotation)
|
||||
}
|
||||
config.TransactionID = ""
|
||||
}
|
||||
|
||||
config.Snippet, err = parser.GetStringAnnotation("modsecurity-snippet", ing)
|
||||
config.Snippet, err = parser.GetStringAnnotation("modsecurity-snippet", ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
config.Snippet = ""
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func (a modSecurity) GetDocumentation() parser.AnnotationFields {
|
||||
return a.annotationConfig.Annotations
|
||||
}
|
||||
|
|
|
@ -17,14 +17,51 @@ limitations under the License.
|
|||
package opentelemetry
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
networking "k8s.io/api/networking/v1"
|
||||
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/parser"
|
||||
"k8s.io/ingress-nginx/internal/ingress/errors"
|
||||
"k8s.io/ingress-nginx/internal/ingress/resolver"
|
||||
)
|
||||
|
||||
const (
|
||||
enableOpenTelemetryAnnotation = "enable-opentelemetry"
|
||||
otelTrustSpanAnnotation = "opentelemetry-trust-incoming-span"
|
||||
otelOperationNameAnnotation = "opentelemetry-operation-name"
|
||||
)
|
||||
|
||||
var regexOperationName = regexp.MustCompile(`^[A-Za-z0-9_\-]*$`)
|
||||
|
||||
var otelAnnotations = parser.Annotation{
|
||||
Group: "opentelemetry",
|
||||
Annotations: parser.AnnotationFields{
|
||||
enableOpenTelemetryAnnotation: {
|
||||
Validator: parser.ValidateBool,
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation defines if Open Telemetry collector should be enable for this location. OpenTelemetry should
|
||||
already be configured by Ingress administrator`,
|
||||
},
|
||||
otelTrustSpanAnnotation: {
|
||||
Validator: parser.ValidateBool,
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation enables or disables using spans from incoming requests as parent for created ones`,
|
||||
},
|
||||
otelOperationNameAnnotation: {
|
||||
Validator: parser.ValidateRegex(*regexOperationName, true),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskMedium,
|
||||
Documentation: `This annotation defines what operation name should be added to the span`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
type opentelemetry struct {
|
||||
r resolver.Resolver
|
||||
r resolver.Resolver
|
||||
annotationConfig parser.Annotation
|
||||
}
|
||||
|
||||
// Config contains the configuration to be used in the Ingress
|
||||
|
@ -64,13 +101,16 @@ func (bd1 *Config) Equal(bd2 *Config) bool {
|
|||
|
||||
// NewParser creates a new serviceUpstream annotation parser
|
||||
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
|
||||
return opentelemetry{r}
|
||||
return opentelemetry{
|
||||
r: r,
|
||||
annotationConfig: otelAnnotations,
|
||||
}
|
||||
}
|
||||
|
||||
// Parse parses the annotations to look for opentelemetry configurations
|
||||
func (c opentelemetry) Parse(ing *networking.Ingress) (interface{}, error) {
|
||||
cfg := Config{}
|
||||
enabled, err := parser.GetBoolAnnotation("enable-opentelemetry", ing)
|
||||
enabled, err := parser.GetBoolAnnotation(enableOpenTelemetryAnnotation, ing, c.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
return &cfg, nil
|
||||
}
|
||||
|
@ -80,10 +120,13 @@ func (c opentelemetry) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
return &cfg, nil
|
||||
}
|
||||
|
||||
trustEnabled, err := parser.GetBoolAnnotation("opentelemetry-trust-incoming-span", ing)
|
||||
trustEnabled, err := parser.GetBoolAnnotation(otelTrustSpanAnnotation, ing, c.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
operationName, err := parser.GetStringAnnotation("opentelemetry-operation-name", ing)
|
||||
operationName, err := parser.GetStringAnnotation(otelOperationNameAnnotation, ing, c.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
if errors.IsValidationError(err) {
|
||||
return nil, err
|
||||
}
|
||||
return &cfg, nil
|
||||
}
|
||||
cfg.OperationName = operationName
|
||||
|
@ -92,10 +135,17 @@ func (c opentelemetry) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
|
||||
cfg.TrustSet = true
|
||||
cfg.TrustEnabled = trustEnabled
|
||||
operationName, err := parser.GetStringAnnotation("opentelemetry-operation-name", ing)
|
||||
operationName, err := parser.GetStringAnnotation(otelOperationNameAnnotation, ing, c.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
if errors.IsValidationError(err) {
|
||||
return nil, err
|
||||
}
|
||||
return &cfg, nil
|
||||
}
|
||||
cfg.OperationName = operationName
|
||||
return &cfg, nil
|
||||
}
|
||||
|
||||
func (c opentelemetry) GetDocumentation() parser.AnnotationFields {
|
||||
return c.annotationConfig.Annotations
|
||||
}
|
||||
|
|
|
@ -73,7 +73,7 @@ func TestIngressAnnotationOpentelemetrySetTrue(t *testing.T) {
|
|||
ing := buildIngress()
|
||||
|
||||
data := map[string]string{}
|
||||
data[parser.GetAnnotationWithPrefix("enable-opentelemetry")] = "true"
|
||||
data[parser.GetAnnotationWithPrefix(enableOpenTelemetryAnnotation)] = "true"
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
val, _ := NewParser(&resolver.Mock{}).Parse(ing)
|
||||
|
@ -100,7 +100,7 @@ func TestIngressAnnotationOpentelemetrySetFalse(t *testing.T) {
|
|||
|
||||
// Test with explicitly set to false
|
||||
data := map[string]string{}
|
||||
data[parser.GetAnnotationWithPrefix("enable-opentelemetry")] = "false"
|
||||
data[parser.GetAnnotationWithPrefix(enableOpenTelemetryAnnotation)] = "false"
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
val, _ := NewParser(&resolver.Mock{}).Parse(ing)
|
||||
|
@ -123,12 +123,15 @@ func TestIngressAnnotationOpentelemetryTrustSetTrue(t *testing.T) {
|
|||
|
||||
data := map[string]string{}
|
||||
opName := "foo-op"
|
||||
data[parser.GetAnnotationWithPrefix("enable-opentelemetry")] = "true"
|
||||
data[parser.GetAnnotationWithPrefix("opentelemetry-trust-incoming-span")] = "true"
|
||||
data[parser.GetAnnotationWithPrefix("opentelemetry-operation-name")] = opName
|
||||
data[parser.GetAnnotationWithPrefix(enableOpenTelemetryAnnotation)] = "true"
|
||||
data[parser.GetAnnotationWithPrefix(otelTrustSpanAnnotation)] = "true"
|
||||
data[parser.GetAnnotationWithPrefix(otelOperationNameAnnotation)] = opName
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
val, _ := NewParser(&resolver.Mock{}).Parse(ing)
|
||||
val, err := NewParser(&resolver.Mock{}).Parse(ing)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
openTelemetry, ok := val.(*Config)
|
||||
if !ok {
|
||||
t.Errorf("expected a Config type")
|
||||
|
@ -155,6 +158,21 @@ func TestIngressAnnotationOpentelemetryTrustSetTrue(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestIngressAnnotationOpentelemetryWithBadOpName(t *testing.T) {
|
||||
ing := buildIngress()
|
||||
|
||||
data := map[string]string{}
|
||||
opName := "fooxpto_123$la;"
|
||||
data[parser.GetAnnotationWithPrefix(enableOpenTelemetryAnnotation)] = "true"
|
||||
data[parser.GetAnnotationWithPrefix(otelOperationNameAnnotation)] = opName
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
_, err := NewParser(&resolver.Mock{}).Parse(ing)
|
||||
if err == nil {
|
||||
t.Fatalf("This operation should return an error but no error was returned")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIngressAnnotationOpentelemetryUnset(t *testing.T) {
|
||||
ing := buildIngress()
|
||||
|
||||
|
|
|
@ -23,8 +23,33 @@ import (
|
|||
"k8s.io/ingress-nginx/internal/ingress/resolver"
|
||||
)
|
||||
|
||||
const (
|
||||
enableOpentracingAnnotation = "enable-opentracing"
|
||||
opentracingTrustSpanAnnotation = "opentracing-trust-incoming-span"
|
||||
)
|
||||
|
||||
var opentracingAnnotations = parser.Annotation{
|
||||
Group: "opentracing",
|
||||
Annotations: parser.AnnotationFields{
|
||||
enableOpentracingAnnotation: {
|
||||
Validator: parser.ValidateBool,
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation defines if Opentracing collector should be enable for this location. Opentracing should
|
||||
already be configured by Ingress administrator`,
|
||||
},
|
||||
opentracingTrustSpanAnnotation: {
|
||||
Validator: parser.ValidateBool,
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation enables or disables using spans from incoming requests as parent for created ones`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
type opentracing struct {
|
||||
r resolver.Resolver
|
||||
r resolver.Resolver
|
||||
annotationConfig parser.Annotation
|
||||
}
|
||||
|
||||
// Config contains the configuration to be used in the Ingress
|
||||
|
@ -58,19 +83,26 @@ func (bd1 *Config) Equal(bd2 *Config) bool {
|
|||
|
||||
// NewParser creates a new serviceUpstream annotation parser
|
||||
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
|
||||
return opentracing{r}
|
||||
return opentracing{
|
||||
r: r,
|
||||
annotationConfig: opentracingAnnotations,
|
||||
}
|
||||
}
|
||||
|
||||
func (s opentracing) Parse(ing *networking.Ingress) (interface{}, error) {
|
||||
enabled, err := parser.GetBoolAnnotation("enable-opentracing", ing)
|
||||
enabled, err := parser.GetBoolAnnotation(enableOpentracingAnnotation, ing, s.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
return &Config{}, nil
|
||||
}
|
||||
|
||||
trustSpan, err := parser.GetBoolAnnotation("opentracing-trust-incoming-span", ing)
|
||||
trustSpan, err := parser.GetBoolAnnotation(opentracingTrustSpanAnnotation, ing, s.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
return &Config{Set: true, Enabled: enabled}, nil
|
||||
}
|
||||
|
||||
return &Config{Set: true, Enabled: enabled, TrustSet: true, TrustEnabled: trustSpan}, nil
|
||||
}
|
||||
|
||||
func (s opentracing) GetDocumentation() parser.AnnotationFields {
|
||||
return s.annotationConfig.Annotations
|
||||
}
|
||||
|
|
|
@ -73,7 +73,7 @@ func TestIngressAnnotationOpentracingSetTrue(t *testing.T) {
|
|||
ing := buildIngress()
|
||||
|
||||
data := map[string]string{}
|
||||
data[parser.GetAnnotationWithPrefix("enable-opentracing")] = "true"
|
||||
data[parser.GetAnnotationWithPrefix(enableOpentracingAnnotation)] = "true"
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
val, _ := NewParser(&resolver.Mock{}).Parse(ing)
|
||||
|
@ -92,7 +92,7 @@ func TestIngressAnnotationOpentracingSetFalse(t *testing.T) {
|
|||
|
||||
// Test with explicitly set to false
|
||||
data := map[string]string{}
|
||||
data[parser.GetAnnotationWithPrefix("enable-opentracing")] = "false"
|
||||
data[parser.GetAnnotationWithPrefix(enableOpentracingAnnotation)] = "false"
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
val, _ := NewParser(&resolver.Mock{}).Parse(ing)
|
||||
|
@ -110,8 +110,8 @@ func TestIngressAnnotationOpentracingTrustSetTrue(t *testing.T) {
|
|||
ing := buildIngress()
|
||||
|
||||
data := map[string]string{}
|
||||
data[parser.GetAnnotationWithPrefix("enable-opentracing")] = "true"
|
||||
data[parser.GetAnnotationWithPrefix("opentracing-trust-incoming-span")] = "true"
|
||||
data[parser.GetAnnotationWithPrefix(enableOpentracingAnnotation)] = "true"
|
||||
data[parser.GetAnnotationWithPrefix(opentracingTrustSpanAnnotation)] = "true"
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
val, _ := NewParser(&resolver.Mock{}).Parse(ing)
|
||||
|
|
|
@ -29,20 +29,75 @@ import (
|
|||
)
|
||||
|
||||
// DefaultAnnotationsPrefix defines the common prefix used in the nginx ingress controller
|
||||
const DefaultAnnotationsPrefix = "nginx.ingress.kubernetes.io"
|
||||
const (
|
||||
DefaultAnnotationsPrefix = "nginx.ingress.kubernetes.io"
|
||||
)
|
||||
|
||||
var (
|
||||
// AnnotationsPrefix is the mutable attribute that the controller explicitly refers to
|
||||
AnnotationsPrefix = DefaultAnnotationsPrefix
|
||||
)
|
||||
|
||||
// AnnotationGroup defines the group that this annotation may belong
|
||||
// eg.: Security, Snippets, Rewrite, etc
|
||||
type AnnotationGroup string
|
||||
|
||||
// AnnotationScope defines which scope this annotation applies. May be to the whole
|
||||
// ingress, per location, etc
|
||||
type AnnotationScope string
|
||||
|
||||
var (
|
||||
AnnotationScopeLocation AnnotationScope = "location"
|
||||
AnnotationScopeIngress AnnotationScope = "ingress"
|
||||
)
|
||||
|
||||
// AnnotationRisk is a subset of risk that an annotation may represent.
|
||||
// Based on the Risk, the admin will be able to allow or disallow users to set it
|
||||
// on their ingress objects
|
||||
type AnnotationRisk string
|
||||
|
||||
type AnnotationFields map[string]AnnotationConfig
|
||||
|
||||
// AnnotationConfig defines the configuration that a single annotation field
|
||||
// has, with the Validator and the documentation of this field.
|
||||
type AnnotationConfig struct {
|
||||
// Validator defines a function to validate the annotation value
|
||||
Validator AnnotationValidator
|
||||
// Documentation defines a user facing documentation for this annotation. This
|
||||
// field will be used to auto generate documentations
|
||||
Documentation string
|
||||
// Risk defines a risk of this annotation being exposed to the user. Annotations
|
||||
// with bool fields, or to set timeout are usually low risk. Annotations that allows
|
||||
// string input without a limited set of options may represent a high risk
|
||||
Risk AnnotationRisk
|
||||
|
||||
// Scope defines which scope this annotation applies, may be to location, to an Ingress object, etc
|
||||
Scope AnnotationScope
|
||||
|
||||
// AnnotationAliases defines other names this annotation may have.
|
||||
AnnotationAliases []string
|
||||
}
|
||||
|
||||
// Annotation defines an annotation feature an Ingress may have.
|
||||
// It should contain the internal resolver, and all the annotations
|
||||
// with configs and Validators that should be used for each Annotation
|
||||
type Annotation struct {
|
||||
// Annotations contains all the annotations that belong to this feature
|
||||
Annotations AnnotationFields
|
||||
// Group defines which annotation group this feature belongs to
|
||||
Group AnnotationGroup
|
||||
}
|
||||
|
||||
// IngressAnnotation has a method to parse annotations located in Ingress
|
||||
type IngressAnnotation interface {
|
||||
Parse(ing *networking.Ingress) (interface{}, error)
|
||||
GetDocumentation() AnnotationFields
|
||||
}
|
||||
|
||||
type ingAnnotations map[string]string
|
||||
|
||||
// TODO: We already parse all of this on checkAnnotation and can just do a parse over the
|
||||
// value
|
||||
func (a ingAnnotations) parseBool(name string) (bool, error) {
|
||||
val, ok := a[name]
|
||||
if ok {
|
||||
|
@ -92,21 +147,9 @@ func (a ingAnnotations) parseFloat32(name string) (float32, error) {
|
|||
return 0, errors.ErrMissingAnnotations
|
||||
}
|
||||
|
||||
func checkAnnotation(name string, ing *networking.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 *networking.Ingress) (bool, error) {
|
||||
v := GetAnnotationWithPrefix(name)
|
||||
err := checkAnnotation(v, ing)
|
||||
func GetBoolAnnotation(name string, ing *networking.Ingress, fields AnnotationFields) (bool, error) {
|
||||
v, err := checkAnnotation(name, ing, fields)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
@ -114,9 +157,8 @@ func GetBoolAnnotation(name string, ing *networking.Ingress) (bool, error) {
|
|||
}
|
||||
|
||||
// GetStringAnnotation extracts a string from an Ingress annotation
|
||||
func GetStringAnnotation(name string, ing *networking.Ingress) (string, error) {
|
||||
v := GetAnnotationWithPrefix(name)
|
||||
err := checkAnnotation(v, ing)
|
||||
func GetStringAnnotation(name string, ing *networking.Ingress, fields AnnotationFields) (string, error) {
|
||||
v, err := checkAnnotation(name, ing, fields)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
@ -125,9 +167,8 @@ func GetStringAnnotation(name string, ing *networking.Ingress) (string, error) {
|
|||
}
|
||||
|
||||
// GetIntAnnotation extracts an int from an Ingress annotation
|
||||
func GetIntAnnotation(name string, ing *networking.Ingress) (int, error) {
|
||||
v := GetAnnotationWithPrefix(name)
|
||||
err := checkAnnotation(v, ing)
|
||||
func GetIntAnnotation(name string, ing *networking.Ingress, fields AnnotationFields) (int, error) {
|
||||
v, err := checkAnnotation(name, ing, fields)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
@ -135,9 +176,8 @@ func GetIntAnnotation(name string, ing *networking.Ingress) (int, error) {
|
|||
}
|
||||
|
||||
// GetFloatAnnotation extracts a float32 from an Ingress annotation
|
||||
func GetFloatAnnotation(name string, ing *networking.Ingress) (float32, error) {
|
||||
v := GetAnnotationWithPrefix(name)
|
||||
err := checkAnnotation(v, ing)
|
||||
func GetFloatAnnotation(name string, ing *networking.Ingress, fields AnnotationFields) (float32, error) {
|
||||
v, err := checkAnnotation(name, ing, fields)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
|
|
@ -38,7 +38,7 @@ func buildIngress() *networking.Ingress {
|
|||
func TestGetBoolAnnotation(t *testing.T) {
|
||||
ing := buildIngress()
|
||||
|
||||
_, err := GetBoolAnnotation("", nil)
|
||||
_, err := GetBoolAnnotation("", nil, nil)
|
||||
if err == nil {
|
||||
t.Errorf("expected error but retuned nil")
|
||||
}
|
||||
|
@ -59,8 +59,8 @@ func TestGetBoolAnnotation(t *testing.T) {
|
|||
|
||||
for _, test := range tests {
|
||||
data[GetAnnotationWithPrefix(test.field)] = test.value
|
||||
|
||||
u, err := GetBoolAnnotation(test.field, ing)
|
||||
ing.SetAnnotations(data)
|
||||
u, err := GetBoolAnnotation(test.field, ing, nil)
|
||||
if test.expErr {
|
||||
if err == nil {
|
||||
t.Errorf("%v: expected error but retuned nil", test.name)
|
||||
|
@ -68,7 +68,7 @@ func TestGetBoolAnnotation(t *testing.T) {
|
|||
continue
|
||||
}
|
||||
if u != test.exp {
|
||||
t.Errorf("%v: expected \"%v\" but \"%v\" was returned", test.name, test.exp, u)
|
||||
t.Errorf("%v: expected \"%v\" but \"%v\" was returned, %+v", test.name, test.exp, u, ing)
|
||||
}
|
||||
|
||||
delete(data, test.field)
|
||||
|
@ -78,7 +78,7 @@ func TestGetBoolAnnotation(t *testing.T) {
|
|||
func TestGetStringAnnotation(t *testing.T) {
|
||||
ing := buildIngress()
|
||||
|
||||
_, err := GetStringAnnotation("", nil)
|
||||
_, err := GetStringAnnotation("", nil, nil)
|
||||
if err == nil {
|
||||
t.Errorf("expected error but none returned")
|
||||
}
|
||||
|
@ -109,7 +109,7 @@ rewrite (?i)/arcgis/services/Utilities/Geometry/GeometryServer(.*)$ /arcgis/serv
|
|||
for _, test := range tests {
|
||||
data[GetAnnotationWithPrefix(test.field)] = test.value
|
||||
|
||||
s, err := GetStringAnnotation(test.field, ing)
|
||||
s, err := GetStringAnnotation(test.field, ing, nil)
|
||||
if test.expErr {
|
||||
if err == nil {
|
||||
t.Errorf("%v: expected error but none returned", test.name)
|
||||
|
@ -133,7 +133,7 @@ rewrite (?i)/arcgis/services/Utilities/Geometry/GeometryServer(.*)$ /arcgis/serv
|
|||
func TestGetFloatAnnotation(t *testing.T) {
|
||||
ing := buildIngress()
|
||||
|
||||
_, err := GetFloatAnnotation("", nil)
|
||||
_, err := GetFloatAnnotation("", nil, nil)
|
||||
if err == nil {
|
||||
t.Errorf("expected error but retuned nil")
|
||||
}
|
||||
|
@ -156,7 +156,7 @@ func TestGetFloatAnnotation(t *testing.T) {
|
|||
for _, test := range tests {
|
||||
data[GetAnnotationWithPrefix(test.field)] = test.value
|
||||
|
||||
s, err := GetFloatAnnotation(test.field, ing)
|
||||
s, err := GetFloatAnnotation(test.field, ing, nil)
|
||||
if test.expErr {
|
||||
if err == nil {
|
||||
t.Errorf("%v: expected error but retuned nil", test.name)
|
||||
|
@ -174,7 +174,7 @@ func TestGetFloatAnnotation(t *testing.T) {
|
|||
func TestGetIntAnnotation(t *testing.T) {
|
||||
ing := buildIngress()
|
||||
|
||||
_, err := GetIntAnnotation("", nil)
|
||||
_, err := GetIntAnnotation("", nil, nil)
|
||||
if err == nil {
|
||||
t.Errorf("expected error but retuned nil")
|
||||
}
|
||||
|
@ -196,7 +196,7 @@ func TestGetIntAnnotation(t *testing.T) {
|
|||
for _, test := range tests {
|
||||
data[GetAnnotationWithPrefix(test.field)] = test.value
|
||||
|
||||
s, err := GetIntAnnotation(test.field, ing)
|
||||
s, err := GetIntAnnotation(test.field, ing, nil)
|
||||
if test.expErr {
|
||||
if err == nil {
|
||||
t.Errorf("%v: expected error but retuned nil", test.name)
|
||||
|
@ -224,6 +224,7 @@ func TestStringToURL(t *testing.T) {
|
|||
}{
|
||||
{"empty", "", "url scheme is empty", nil, true},
|
||||
{"no scheme", "bar", "url scheme is empty", nil, true},
|
||||
{"invalid parse", "://lala.com", "://lala.com is not a valid URL: parse \"://lala.com\": missing protocol scheme", nil, true},
|
||||
{"invalid host", "http://", "url host is empty", nil, true},
|
||||
{"invalid host (multiple dots)", "http://foo..bar.com", "invalid url host", nil, true},
|
||||
{"valid URL", validURL, "", validParsedURL, false},
|
||||
|
|
224
internal/ingress/annotations/parser/validators.go
Normal file
224
internal/ingress/annotations/parser/validators.go
Normal file
|
@ -0,0 +1,224 @@
|
|||
/*
|
||||
Copyright 2023 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 (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
networking "k8s.io/api/networking/v1"
|
||||
machineryvalidation "k8s.io/apimachinery/pkg/api/validation"
|
||||
"k8s.io/ingress-nginx/internal/ingress/errors"
|
||||
"k8s.io/ingress-nginx/internal/net"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
type AnnotationValidator func(string) error
|
||||
|
||||
const (
|
||||
AnnotationRiskLow AnnotationRisk = "Low"
|
||||
AnnotationRiskMedium AnnotationRisk = "Medium"
|
||||
AnnotationRiskHigh AnnotationRisk = "High"
|
||||
AnnotationRiskCritical AnnotationRisk = "Critical"
|
||||
)
|
||||
|
||||
var (
|
||||
alphaNumericChars = `\-\.\_\~a-zA-Z0-9\/:`
|
||||
extendedAlphaNumeric = alphaNumericChars + ", "
|
||||
regexEnabledChars = regexp.QuoteMeta(`^$[](){}*+?|&=\`)
|
||||
urlEnabledChars = regexp.QuoteMeta(`:?&=`)
|
||||
)
|
||||
|
||||
// IsValidRegex checks if the tested string can be used as a regex, but without any weird character.
|
||||
// It includes regex characters for paths that may contain regexes
|
||||
var IsValidRegex = regexp.MustCompile("^[/" + alphaNumericChars + regexEnabledChars + "]*$")
|
||||
|
||||
// SizeRegex validates sizes understood by NGINX, like 1000, 100k, 1000M
|
||||
var SizeRegex = regexp.MustCompile("^(?i)[0-9]+[bkmg]?$")
|
||||
|
||||
// URLRegex is used to validate a URL but with only a specific set of characters:
|
||||
// It is alphanumericChar + ":", "?", "&"
|
||||
// A valid URL would be proto://something.com:port/something?arg=param
|
||||
var (
|
||||
// URLIsValidRegex is used on full URLs, containing query strings (:, ? and &)
|
||||
URLIsValidRegex = regexp.MustCompile("^[" + alphaNumericChars + urlEnabledChars + "]*$")
|
||||
// BasicChars is alphanumeric and ".", "-", "_", "~" and ":", usually used on simple host:port/path composition
|
||||
BasicCharsRegex = regexp.MustCompile("^[/" + alphaNumericChars + "]*$")
|
||||
// ExtendedChars is alphanumeric and ".", "-", "_", "~" and ":" plus "," and spaces, usually used on simple host:port/path composition
|
||||
ExtendedChars = regexp.MustCompile("^[/" + extendedAlphaNumeric + "]*$")
|
||||
// CharsWithSpace is like basic chars, but includes the space character
|
||||
CharsWithSpace = regexp.MustCompile("^[/" + alphaNumericChars + " ]*$")
|
||||
// NGINXVariable allows entries with alphanumeric characters, -, _ and the special "$"
|
||||
NGINXVariable = regexp.MustCompile(`^[A-Za-z0-9\-\_\$\{\}]*$`)
|
||||
// RegexPathWithCapture allows entries that SHOULD start with "/" and may contain alphanumeric + capture
|
||||
// character for regex based paths, like /something/$1/anything/$2
|
||||
RegexPathWithCapture = regexp.MustCompile(`^/[` + alphaNumericChars + `\/\$]*$`)
|
||||
// HeadersVariable defines a regex that allows headers separated by comma
|
||||
HeadersVariable = regexp.MustCompile(`^[A-Za-z0-9-_, ]*$`)
|
||||
// URLWithNginxVariableRegex defines a url that can contain nginx variables.
|
||||
// It is a risky operation
|
||||
URLWithNginxVariableRegex = regexp.MustCompile("^[" + alphaNumericChars + urlEnabledChars + "$]*$")
|
||||
)
|
||||
|
||||
// ValidateArrayOfServerName validates if all fields on a Server name annotation are
|
||||
// regexes. They can be *.something*, ~^www\d+\.example\.com$ but not fancy character
|
||||
func ValidateArrayOfServerName(value string) error {
|
||||
for _, fqdn := range strings.Split(value, ",") {
|
||||
if err := ValidateServerName(fqdn); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ValidateServerName(value string) error {
|
||||
value = strings.TrimSpace(value)
|
||||
if !IsValidRegex.MatchString(value) {
|
||||
return fmt.Errorf("value %s is invalid server name", value)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateRegex receives a regex as an argument and uses it to validate
|
||||
// the value of the field.
|
||||
// Annotation can define if the spaces should be trimmed before validating the value
|
||||
func ValidateRegex(regex regexp.Regexp, removeSpace bool) AnnotationValidator {
|
||||
return func(s string) error {
|
||||
if removeSpace {
|
||||
s = strings.ReplaceAll(s, " ", "")
|
||||
}
|
||||
if !regex.MatchString(s) {
|
||||
return fmt.Errorf("value %s is invalid", s)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateOptions receives an array of valid options that can be the value of annotation.
|
||||
// If no valid option is found, it will return an error
|
||||
func ValidateOptions(options []string, caseSensitive bool, trimSpace bool) AnnotationValidator {
|
||||
return func(s string) error {
|
||||
if trimSpace {
|
||||
s = strings.TrimSpace(s)
|
||||
}
|
||||
if !caseSensitive {
|
||||
s = strings.ToLower(s)
|
||||
}
|
||||
for _, option := range options {
|
||||
if s == option {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("value does not match any valid option")
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateBool validates if the specified value is a bool
|
||||
func ValidateBool(value string) error {
|
||||
_, err := strconv.ParseBool(value)
|
||||
return err
|
||||
}
|
||||
|
||||
// ValidateInt validates if the specified value is an integer
|
||||
func ValidateInt(value string) error {
|
||||
_, err := strconv.Atoi(value)
|
||||
return err
|
||||
}
|
||||
|
||||
// ValidateInt validates if the specified value is an array of IPs and CIDRs
|
||||
func ValidateCIDRs(value string) error {
|
||||
_, err := net.ParseCIDRs(value)
|
||||
return err
|
||||
}
|
||||
|
||||
// ValidateDuration validates if the specified value is a valid time
|
||||
func ValidateDuration(value string) error {
|
||||
_, err := time.ParseDuration(value)
|
||||
return err
|
||||
}
|
||||
|
||||
// ValidateNull always return null values and should not be widely used.
|
||||
// It is used on the "snippet" annotations, as it is up to the admin to allow its
|
||||
// usage, knowing it can be critical!
|
||||
func ValidateNull(value string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateServiceName validates if a provided service name is a valid string
|
||||
func ValidateServiceName(value string) error {
|
||||
errs := machineryvalidation.NameIsDNS1035Label(value, false)
|
||||
if len(errs) != 0 {
|
||||
return fmt.Errorf("annotation does not contain a valid service name: %+v", errs)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkAnnotations will check each annotation for:
|
||||
// 1 - Does it contain the internal validation and docs config?
|
||||
// 2 - Does the ingress contains annotations? (validate null pointers)
|
||||
// 3 - Does it contains a validator? Should it contain a validator (not containing is a bug!)
|
||||
// 4 - Does the annotation contain aliases? So we should use if the alias is defined an the annotation not.
|
||||
// 4 - Runs the validator on the value
|
||||
// It will return the full annotation name if all is fine
|
||||
func checkAnnotation(name string, ing *networking.Ingress, fields AnnotationFields) (string, error) {
|
||||
var validateFunc AnnotationValidator
|
||||
if fields != nil {
|
||||
config, ok := fields[name]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("annotation does not contain a valid internal configuration, this is an Ingress Controller issue! Please raise an issue on github.com/kubernetes/ingress-nginx")
|
||||
}
|
||||
validateFunc = config.Validator
|
||||
}
|
||||
|
||||
if ing == nil || len(ing.GetAnnotations()) == 0 {
|
||||
return "", errors.ErrMissingAnnotations
|
||||
}
|
||||
|
||||
annotationFullName := GetAnnotationWithPrefix(name)
|
||||
if annotationFullName == "" {
|
||||
return "", errors.ErrInvalidAnnotationName
|
||||
}
|
||||
|
||||
annotationValue := ing.GetAnnotations()[annotationFullName]
|
||||
if fields != nil {
|
||||
if validateFunc == nil {
|
||||
return "", fmt.Errorf("annotation does not contain a validator. This is an ingress-controller bug. Please open an issue")
|
||||
}
|
||||
if annotationValue == "" {
|
||||
for _, annotationAlias := range fields[name].AnnotationAliases {
|
||||
tempAnnotationFullName := GetAnnotationWithPrefix(annotationAlias)
|
||||
if aliasVal := ing.GetAnnotations()[tempAnnotationFullName]; aliasVal != "" {
|
||||
annotationValue = aliasVal
|
||||
annotationFullName = tempAnnotationFullName
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
// We don't run validation against empty values
|
||||
if annotationValue != "" {
|
||||
if err := validateFunc(annotationValue); err != nil {
|
||||
klog.Warningf("validation error on ingress %s/%s: annotation %s contains invalid value %s", ing.GetNamespace(), ing.GetName(), name, annotationValue)
|
||||
return "", errors.NewValidationError(annotationFullName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return annotationFullName, nil
|
||||
}
|
223
internal/ingress/annotations/parser/validators_test.go
Normal file
223
internal/ingress/annotations/parser/validators_test.go
Normal file
|
@ -0,0 +1,223 @@
|
|||
/*
|
||||
Copyright 2023 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 (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
networking "k8s.io/api/networking/v1"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
func TestValidateArrayOfServerName(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
value string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "should accept common name",
|
||||
value: "something.com,anything.com",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "should accept wildcard name",
|
||||
value: "*.something.com,otherthing.com",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "should allow names with spaces between array and some regexes",
|
||||
value: `~^www\d+\.example\.com$,something.com`,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "should allow names with regexes",
|
||||
value: `http://some.test.env.com:2121/$someparam=1&$someotherparam=2`,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "should allow names with wildcard in middle common name",
|
||||
value: "*.so*mething.com,bla.com",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "should deny names with weird characters",
|
||||
value: "something.com,lolo;xpto.com,nothing.com",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if err := ValidateArrayOfServerName(tt.value); (err != nil) != tt.wantErr {
|
||||
t.Errorf("ValidateArrayOfServerName() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_checkAnnotation(t *testing.T) {
|
||||
type args struct {
|
||||
name string
|
||||
ing *networking.Ingress
|
||||
fields AnnotationFields
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "null ingress should error",
|
||||
want: "",
|
||||
args: args{
|
||||
name: "some-random-annotation",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "not having a validator for a specific annotation is a bug",
|
||||
want: "",
|
||||
args: args{
|
||||
name: "some-new-invalid-annotation",
|
||||
ing: &networking.Ingress{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Annotations: map[string]string{
|
||||
GetAnnotationWithPrefix("some-new-invalid-annotation"): "xpto",
|
||||
},
|
||||
},
|
||||
},
|
||||
fields: AnnotationFields{
|
||||
"otherannotation": AnnotationConfig{
|
||||
Validator: func(value string) error { return nil },
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "annotationconfig found and no validation func defined on annotation is a bug",
|
||||
want: "",
|
||||
args: args{
|
||||
name: "some-new-invalid-annotation",
|
||||
ing: &networking.Ingress{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Annotations: map[string]string{
|
||||
GetAnnotationWithPrefix("some-new-invalid-annotation"): "xpto",
|
||||
},
|
||||
},
|
||||
},
|
||||
fields: AnnotationFields{
|
||||
"some-new-invalid-annotation": AnnotationConfig{},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "no annotation can turn into a null pointer and should fail",
|
||||
want: "",
|
||||
args: args{
|
||||
name: "some-new-invalid-annotation",
|
||||
ing: &networking.Ingress{
|
||||
ObjectMeta: v1.ObjectMeta{},
|
||||
},
|
||||
fields: AnnotationFields{
|
||||
"some-new-invalid-annotation": AnnotationConfig{},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "no AnnotationField config should bypass validations",
|
||||
want: GetAnnotationWithPrefix("some-valid-annotation"),
|
||||
args: args{
|
||||
name: "some-valid-annotation",
|
||||
ing: &networking.Ingress{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Annotations: map[string]string{
|
||||
GetAnnotationWithPrefix("some-valid-annotation"): "xpto",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "annotation with invalid value should fail",
|
||||
want: "",
|
||||
args: args{
|
||||
name: "some-new-annotation",
|
||||
ing: &networking.Ingress{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Annotations: map[string]string{
|
||||
GetAnnotationWithPrefix("some-new-annotation"): "xpto1",
|
||||
},
|
||||
},
|
||||
},
|
||||
fields: AnnotationFields{
|
||||
"some-new-annotation": AnnotationConfig{
|
||||
Validator: func(value string) error {
|
||||
if value != "xpto" {
|
||||
return fmt.Errorf("this is an error")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "annotation with valid value should pass",
|
||||
want: GetAnnotationWithPrefix("some-other-annotation"),
|
||||
args: args{
|
||||
name: "some-other-annotation",
|
||||
ing: &networking.Ingress{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Annotations: map[string]string{
|
||||
GetAnnotationWithPrefix("some-other-annotation"): "xpto",
|
||||
},
|
||||
},
|
||||
},
|
||||
fields: AnnotationFields{
|
||||
"some-other-annotation": AnnotationConfig{
|
||||
Validator: func(value string) error {
|
||||
if value != "xpto" {
|
||||
return fmt.Errorf("this is an error")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := checkAnnotation(tt.args.name, tt.args.ing, tt.args.fields)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("checkAnnotation() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("checkAnnotation() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -23,22 +23,46 @@ import (
|
|||
"k8s.io/ingress-nginx/internal/ingress/resolver"
|
||||
)
|
||||
|
||||
const (
|
||||
portsInRedirectAnnotation = "use-port-in-redirects"
|
||||
)
|
||||
|
||||
var portsInRedirectAnnotations = parser.Annotation{
|
||||
Group: "redirect",
|
||||
Annotations: parser.AnnotationFields{
|
||||
portsInRedirectAnnotation: {
|
||||
Validator: parser.ValidateBool,
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow, // Low, as it allows just a set of options
|
||||
Documentation: `Enables or disables specifying the port in absolute redirects issued by nginx.`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
type portInRedirect struct {
|
||||
r resolver.Resolver
|
||||
r resolver.Resolver
|
||||
annotationConfig parser.Annotation
|
||||
}
|
||||
|
||||
// NewParser creates a new port in redirect annotation parser
|
||||
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
|
||||
return portInRedirect{r}
|
||||
return portInRedirect{
|
||||
r: r,
|
||||
annotationConfig: portsInRedirectAnnotations,
|
||||
}
|
||||
}
|
||||
|
||||
// Parse parses the annotations contained in the ingress
|
||||
// rule used to indicate if the redirects must
|
||||
func (a portInRedirect) Parse(ing *networking.Ingress) (interface{}, error) {
|
||||
up, err := parser.GetBoolAnnotation("use-port-in-redirects", ing)
|
||||
up, err := parser.GetBoolAnnotation(portsInRedirectAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
return a.r.GetDefaultBackend().UsePortInRedirects, nil
|
||||
}
|
||||
|
||||
return up, nil
|
||||
}
|
||||
|
||||
func (a portInRedirect) GetDocumentation() parser.AnnotationFields {
|
||||
return a.annotationConfig.Annotations
|
||||
}
|
||||
|
|
|
@ -17,7 +17,6 @@ limitations under the License.
|
|||
package portinredirect
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
api "k8s.io/api/core/v1"
|
||||
|
@ -84,23 +83,24 @@ func (m mockBackend) GetDefaultBackend() defaults.Backend {
|
|||
func TestPortInRedirect(t *testing.T) {
|
||||
tests := []struct {
|
||||
title string
|
||||
usePort *bool
|
||||
usePort string
|
||||
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},
|
||||
{"false - default false", "false", false, false},
|
||||
{"false - default true", "false", true, false},
|
||||
{"no annotation - default false", "", false, false},
|
||||
{"no annotation - default false", "not-a-bool", false, false},
|
||||
{"no annotation - default true", "", true, true},
|
||||
{"true - default true", "true", true, true},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
ing := buildIngress()
|
||||
|
||||
data := map[string]string{}
|
||||
if test.usePort != nil {
|
||||
data[parser.GetAnnotationWithPrefix("use-port-in-redirects")] = fmt.Sprintf("%v", *test.usePort)
|
||||
if test.usePort != "" {
|
||||
data[parser.GetAnnotationWithPrefix(portsInRedirectAnnotation)] = test.usePort
|
||||
}
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
|
@ -118,13 +118,3 @@ func TestPortInRedirect(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func newTrue() *bool {
|
||||
b := true
|
||||
return &b
|
||||
}
|
||||
|
||||
func newFalse() *bool {
|
||||
b := false
|
||||
return &b
|
||||
}
|
||||
|
|
|
@ -17,12 +17,150 @@ limitations under the License.
|
|||
package proxy
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
networking "k8s.io/api/networking/v1"
|
||||
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/parser"
|
||||
"k8s.io/ingress-nginx/internal/ingress/resolver"
|
||||
)
|
||||
|
||||
const (
|
||||
proxyConnectTimeoutAnnotation = "proxy-connect-timeout"
|
||||
proxySendTimeoutAnnotation = "proxy-send-timeout"
|
||||
proxyReadTimeoutAnnotation = "proxy-read-timeout"
|
||||
proxyBuffersNumberAnnotation = "proxy-buffers-number"
|
||||
proxyBufferSizeAnnotation = "proxy-buffer-size"
|
||||
proxyCookiePathAnnotation = "proxy-cookie-path"
|
||||
proxyCookieDomainAnnotation = "proxy-cookie-domain"
|
||||
proxyBodySizeAnnotation = "proxy-body-size"
|
||||
proxyNextUpstreamAnnotation = "proxy-next-upstream"
|
||||
proxyNextUpstreamTimeoutAnnotation = "proxy-next-upstream-timeout"
|
||||
proxyNextUpstreamTriesAnnotation = "proxy-next-upstream-tries"
|
||||
proxyRequestBufferingAnnotation = "proxy-request-buffering"
|
||||
proxyRedirectFromAnnotation = "proxy-redirect-from"
|
||||
proxyRedirectToAnnotation = "proxy-redirect-to"
|
||||
proxyBufferingAnnotation = "proxy-buffering"
|
||||
proxyHTTPVersionAnnotation = "proxy-http-version"
|
||||
proxyMaxTempFileSizeAnnotation = "proxy-max-temp-file-size"
|
||||
)
|
||||
|
||||
var (
|
||||
validUpstreamAnnotation = regexp.MustCompile(`^((error|timeout|invalid_header|http_500|http_502|http_503|http_504|http_403|http_404|http_429|non_idempotent|off)\s?)+$`)
|
||||
)
|
||||
|
||||
var proxyAnnotations = parser.Annotation{
|
||||
Group: "backend",
|
||||
Annotations: parser.AnnotationFields{
|
||||
proxyConnectTimeoutAnnotation: {
|
||||
Validator: parser.ValidateInt,
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation allows setting the timeout in seconds of the connect operation to the backend.`,
|
||||
},
|
||||
proxySendTimeoutAnnotation: {
|
||||
Validator: parser.ValidateInt,
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation allows setting the timeout in seconds of the send operation to the backend.`,
|
||||
},
|
||||
proxyReadTimeoutAnnotation: {
|
||||
Validator: parser.ValidateInt,
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation allows setting the timeout in seconds of the read operation to the backend.`,
|
||||
},
|
||||
proxyBuffersNumberAnnotation: {
|
||||
Validator: parser.ValidateInt,
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation sets the number of the buffers in proxy_buffers used for reading the first part of the response received from the proxied server.
|
||||
By default proxy buffers number is set as 4`,
|
||||
},
|
||||
proxyBufferSizeAnnotation: {
|
||||
Validator: parser.ValidateRegex(*parser.SizeRegex, true),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation sets the size of the buffer proxy_buffer_size used for reading the first part of the response received from the proxied server.
|
||||
By default proxy buffer size is set as "4k".`,
|
||||
},
|
||||
proxyCookiePathAnnotation: {
|
||||
Validator: parser.ValidateRegex(*parser.URLIsValidRegex, true),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskMedium,
|
||||
Documentation: `This annotation sets a text that should be changed in the path attribute of the "Set-Cookie" header fields of a proxied server response.`,
|
||||
},
|
||||
proxyCookieDomainAnnotation: {
|
||||
Validator: parser.ValidateRegex(*parser.BasicCharsRegex, true),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskMedium,
|
||||
Documentation: `This annotation ets a text that should be changed in the domain attribute of the "Set-Cookie" header fields of a proxied server response.`,
|
||||
},
|
||||
proxyBodySizeAnnotation: {
|
||||
Validator: parser.ValidateRegex(*parser.SizeRegex, true),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskMedium,
|
||||
Documentation: `This annotation allows setting the maximum allowed size of a client request body.`,
|
||||
},
|
||||
proxyNextUpstreamAnnotation: {
|
||||
Validator: parser.ValidateRegex(*validUpstreamAnnotation, false),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskMedium,
|
||||
Documentation: `This annotation defines when the next upstream should be used.
|
||||
This annotation reflect the directive https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_next_upstream
|
||||
and only the allowed values on upstream are allowed here.`,
|
||||
},
|
||||
proxyNextUpstreamTimeoutAnnotation: {
|
||||
Validator: parser.ValidateInt,
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation limits the time during which a request can be passed to the next server`,
|
||||
},
|
||||
proxyNextUpstreamTriesAnnotation: {
|
||||
Validator: parser.ValidateInt,
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation limits the number of possible tries for passing a request to the next server`,
|
||||
},
|
||||
proxyRequestBufferingAnnotation: {
|
||||
Validator: parser.ValidateOptions([]string{"on", "off"}, true, true),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation enables or disables buffering of a client request body.`,
|
||||
},
|
||||
proxyRedirectFromAnnotation: {
|
||||
Validator: parser.ValidateRegex(*parser.URLIsValidRegex, true),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskMedium,
|
||||
Documentation: `The annotations proxy-redirect-from and proxy-redirect-to will set the first and second parameters of NGINX's proxy_redirect directive respectively`,
|
||||
},
|
||||
proxyRedirectToAnnotation: {
|
||||
Validator: parser.ValidateRegex(*parser.URLIsValidRegex, true),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskMedium,
|
||||
Documentation: `The annotations proxy-redirect-from and proxy-redirect-to will set the first and second parameters of NGINX's proxy_redirect directive respectively`,
|
||||
},
|
||||
proxyBufferingAnnotation: {
|
||||
Validator: parser.ValidateOptions([]string{"on", "off"}, true, true),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation enables or disables buffering of responses from the proxied server. It can be "on" or "off"`,
|
||||
},
|
||||
proxyHTTPVersionAnnotation: {
|
||||
Validator: parser.ValidateOptions([]string{"1.0", "1.1"}, true, true),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotations sets the HTTP protocol version for proxying. Can be "1.0" or "1.1".`,
|
||||
},
|
||||
proxyMaxTempFileSizeAnnotation: {
|
||||
Validator: parser.ValidateRegex(*parser.SizeRegex, true),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation defines the maximum size of a temporary file when buffering responses.`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Config returns the proxy timeout to use in the upstream server/s
|
||||
type Config struct {
|
||||
BodySize string `json:"bodySize"`
|
||||
|
@ -109,12 +247,15 @@ func (l1 *Config) Equal(l2 *Config) bool {
|
|||
}
|
||||
|
||||
type proxy struct {
|
||||
r resolver.Resolver
|
||||
r resolver.Resolver
|
||||
annotationConfig parser.Annotation
|
||||
}
|
||||
|
||||
// NewParser creates a new reverse proxy configuration annotation parser
|
||||
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
|
||||
return proxy{r}
|
||||
return proxy{r: r,
|
||||
annotationConfig: proxyAnnotations,
|
||||
}
|
||||
}
|
||||
|
||||
// ParseAnnotations parses the annotations contained in the ingress
|
||||
|
@ -125,90 +266,94 @@ func (a proxy) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
|
||||
var err error
|
||||
|
||||
config.ConnectTimeout, err = parser.GetIntAnnotation("proxy-connect-timeout", ing)
|
||||
config.ConnectTimeout, err = parser.GetIntAnnotation(proxyConnectTimeoutAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
config.ConnectTimeout = defBackend.ProxyConnectTimeout
|
||||
}
|
||||
|
||||
config.SendTimeout, err = parser.GetIntAnnotation("proxy-send-timeout", ing)
|
||||
config.SendTimeout, err = parser.GetIntAnnotation(proxySendTimeoutAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
config.SendTimeout = defBackend.ProxySendTimeout
|
||||
}
|
||||
|
||||
config.ReadTimeout, err = parser.GetIntAnnotation("proxy-read-timeout", ing)
|
||||
config.ReadTimeout, err = parser.GetIntAnnotation(proxyReadTimeoutAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
config.ReadTimeout = defBackend.ProxyReadTimeout
|
||||
}
|
||||
|
||||
config.BuffersNumber, err = parser.GetIntAnnotation("proxy-buffers-number", ing)
|
||||
config.BuffersNumber, err = parser.GetIntAnnotation(proxyBuffersNumberAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
config.BuffersNumber = defBackend.ProxyBuffersNumber
|
||||
}
|
||||
|
||||
config.BufferSize, err = parser.GetStringAnnotation("proxy-buffer-size", ing)
|
||||
config.BufferSize, err = parser.GetStringAnnotation(proxyBufferSizeAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
config.BufferSize = defBackend.ProxyBufferSize
|
||||
}
|
||||
|
||||
config.CookiePath, err = parser.GetStringAnnotation("proxy-cookie-path", ing)
|
||||
config.CookiePath, err = parser.GetStringAnnotation(proxyCookiePathAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
config.CookiePath = defBackend.ProxyCookiePath
|
||||
}
|
||||
|
||||
config.CookieDomain, err = parser.GetStringAnnotation("proxy-cookie-domain", ing)
|
||||
config.CookieDomain, err = parser.GetStringAnnotation(proxyCookieDomainAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
config.CookieDomain = defBackend.ProxyCookieDomain
|
||||
}
|
||||
|
||||
config.BodySize, err = parser.GetStringAnnotation("proxy-body-size", ing)
|
||||
config.BodySize, err = parser.GetStringAnnotation(proxyBodySizeAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
config.BodySize = defBackend.ProxyBodySize
|
||||
}
|
||||
|
||||
config.NextUpstream, err = parser.GetStringAnnotation("proxy-next-upstream", ing)
|
||||
config.NextUpstream, err = parser.GetStringAnnotation(proxyNextUpstreamAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
config.NextUpstream = defBackend.ProxyNextUpstream
|
||||
}
|
||||
|
||||
config.NextUpstreamTimeout, err = parser.GetIntAnnotation("proxy-next-upstream-timeout", ing)
|
||||
config.NextUpstreamTimeout, err = parser.GetIntAnnotation(proxyNextUpstreamTimeoutAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
config.NextUpstreamTimeout = defBackend.ProxyNextUpstreamTimeout
|
||||
}
|
||||
|
||||
config.NextUpstreamTries, err = parser.GetIntAnnotation("proxy-next-upstream-tries", ing)
|
||||
config.NextUpstreamTries, err = parser.GetIntAnnotation(proxyNextUpstreamTriesAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
config.NextUpstreamTries = defBackend.ProxyNextUpstreamTries
|
||||
}
|
||||
|
||||
config.RequestBuffering, err = parser.GetStringAnnotation("proxy-request-buffering", ing)
|
||||
config.RequestBuffering, err = parser.GetStringAnnotation(proxyRequestBufferingAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
config.RequestBuffering = defBackend.ProxyRequestBuffering
|
||||
}
|
||||
|
||||
config.ProxyRedirectFrom, err = parser.GetStringAnnotation("proxy-redirect-from", ing)
|
||||
config.ProxyRedirectFrom, err = parser.GetStringAnnotation(proxyRedirectFromAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
config.ProxyRedirectFrom = defBackend.ProxyRedirectFrom
|
||||
}
|
||||
|
||||
config.ProxyRedirectTo, err = parser.GetStringAnnotation("proxy-redirect-to", ing)
|
||||
config.ProxyRedirectTo, err = parser.GetStringAnnotation(proxyRedirectToAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
config.ProxyRedirectTo = defBackend.ProxyRedirectTo
|
||||
}
|
||||
|
||||
config.ProxyBuffering, err = parser.GetStringAnnotation("proxy-buffering", ing)
|
||||
config.ProxyBuffering, err = parser.GetStringAnnotation(proxyBufferingAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
config.ProxyBuffering = defBackend.ProxyBuffering
|
||||
}
|
||||
|
||||
config.ProxyHTTPVersion, err = parser.GetStringAnnotation("proxy-http-version", ing)
|
||||
config.ProxyHTTPVersion, err = parser.GetStringAnnotation(proxyHTTPVersionAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
config.ProxyHTTPVersion = defBackend.ProxyHTTPVersion
|
||||
}
|
||||
|
||||
config.ProxyMaxTempFileSize, err = parser.GetStringAnnotation("proxy-max-temp-file-size", ing)
|
||||
config.ProxyMaxTempFileSize, err = parser.GetStringAnnotation(proxyMaxTempFileSizeAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
config.ProxyMaxTempFileSize = defBackend.ProxyMaxTempFileSize
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func (a proxy) GetDocumentation() parser.AnnotationFields {
|
||||
return a.annotationConfig.Annotations
|
||||
}
|
||||
|
|
|
@ -161,6 +161,74 @@ func TestProxy(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestProxyComplex(t *testing.T) {
|
||||
ing := buildIngress()
|
||||
|
||||
data := map[string]string{}
|
||||
data[parser.GetAnnotationWithPrefix("proxy-connect-timeout")] = "1"
|
||||
data[parser.GetAnnotationWithPrefix("proxy-send-timeout")] = "2"
|
||||
data[parser.GetAnnotationWithPrefix("proxy-read-timeout")] = "3"
|
||||
data[parser.GetAnnotationWithPrefix("proxy-buffers-number")] = "8"
|
||||
data[parser.GetAnnotationWithPrefix("proxy-buffer-size")] = "1k"
|
||||
data[parser.GetAnnotationWithPrefix("proxy-body-size")] = "2k"
|
||||
data[parser.GetAnnotationWithPrefix("proxy-next-upstream")] = "error http_502"
|
||||
data[parser.GetAnnotationWithPrefix("proxy-next-upstream-timeout")] = "5"
|
||||
data[parser.GetAnnotationWithPrefix("proxy-next-upstream-tries")] = "3"
|
||||
data[parser.GetAnnotationWithPrefix("proxy-request-buffering")] = "off"
|
||||
data[parser.GetAnnotationWithPrefix("proxy-buffering")] = "on"
|
||||
data[parser.GetAnnotationWithPrefix("proxy-http-version")] = "1.0"
|
||||
data[parser.GetAnnotationWithPrefix("proxy-max-temp-file-size")] = "128k"
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
i, err := NewParser(mockBackend{}).Parse(ing)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error parsing a valid")
|
||||
}
|
||||
p, ok := i.(*Config)
|
||||
if !ok {
|
||||
t.Fatalf("expected a Config 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.BuffersNumber != 8 {
|
||||
t.Errorf("expected 8 as proxy-buffers-number but returned %v", p.BuffersNumber)
|
||||
}
|
||||
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 != "error http_502" {
|
||||
t.Errorf("expected off as next-upstream but returned %v", p.NextUpstream)
|
||||
}
|
||||
if p.NextUpstreamTimeout != 5 {
|
||||
t.Errorf("expected 5 as next-upstream-timeout but returned %v", p.NextUpstreamTimeout)
|
||||
}
|
||||
if p.NextUpstreamTries != 3 {
|
||||
t.Errorf("expected 3 as next-upstream-tries but returned %v", p.NextUpstreamTries)
|
||||
}
|
||||
if p.RequestBuffering != "off" {
|
||||
t.Errorf("expected off as request-buffering but returned %v", p.RequestBuffering)
|
||||
}
|
||||
if p.ProxyBuffering != "on" {
|
||||
t.Errorf("expected on as proxy-buffering but returned %v", p.ProxyBuffering)
|
||||
}
|
||||
if p.ProxyHTTPVersion != "1.0" {
|
||||
t.Errorf("expected 1.0 as proxy-http-version but returned %v", p.ProxyHTTPVersion)
|
||||
}
|
||||
if p.ProxyMaxTempFileSize != "128k" {
|
||||
t.Errorf("expected 128k as proxy-max-temp-file-size but returned %v", p.ProxyMaxTempFileSize)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxyWithNoAnnotation(t *testing.T) {
|
||||
ing := buildIngress()
|
||||
|
||||
|
|
|
@ -24,9 +24,11 @@ import (
|
|||
|
||||
networking "k8s.io/api/networking/v1"
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/parser"
|
||||
"k8s.io/ingress-nginx/internal/ingress/errors"
|
||||
ing_errors "k8s.io/ingress-nginx/internal/ingress/errors"
|
||||
"k8s.io/ingress-nginx/internal/ingress/resolver"
|
||||
"k8s.io/ingress-nginx/internal/k8s"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -39,9 +41,73 @@ const (
|
|||
|
||||
var (
|
||||
proxySSLOnOffRegex = regexp.MustCompile(`^(on|off)$`)
|
||||
proxySSLProtocolRegex = regexp.MustCompile(`^(SSLv2|SSLv3|TLSv1|TLSv1\.1|TLSv1\.2|TLSv1\.3)$`)
|
||||
proxySSLProtocolRegex = regexp.MustCompile(`^(SSLv2|SSLv3|TLSv1|TLSv1\.1|TLSv1\.2|TLSv1\.3| )*$`)
|
||||
proxySSLCiphersRegex = regexp.MustCompile(`^[A-Za-z0-9\+\:\_\-\!]*$`)
|
||||
)
|
||||
|
||||
const (
|
||||
proxySSLSecretAnnotation = "proxy-ssl-secret"
|
||||
proxySSLCiphersAnnotation = "proxy-ssl-ciphers"
|
||||
proxySSLProtocolsAnnotation = "proxy-ssl-protocols"
|
||||
proxySSLNameAnnotation = "proxy-ssl-name"
|
||||
proxySSLVerifyAnnotation = "proxy-ssl-verify"
|
||||
proxySSLVerifyDepthAnnotation = "proxy-ssl-verify-depth"
|
||||
proxySSLServerNameAnnotation = "proxy-ssl-server-name"
|
||||
)
|
||||
|
||||
var proxySSLAnnotation = parser.Annotation{
|
||||
Group: "proxy",
|
||||
Annotations: parser.AnnotationFields{
|
||||
proxySSLSecretAnnotation: {
|
||||
Validator: parser.ValidateRegex(*parser.BasicCharsRegex, true),
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskMedium,
|
||||
Documentation: `This annotation specifies a Secret with the certificate tls.crt, key tls.key in PEM format used for authentication to a proxied HTTPS server.
|
||||
It should also contain trusted CA certificates ca.crt in PEM format used to verify the certificate of the proxied HTTPS server.
|
||||
This annotation expects the Secret name in the form "namespace/secretName"
|
||||
Just secrets on the same namespace of the ingress can be used.`,
|
||||
},
|
||||
proxySSLCiphersAnnotation: {
|
||||
Validator: parser.ValidateRegex(*proxySSLCiphersRegex, true),
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskMedium,
|
||||
Documentation: `This annotation Specifies the enabled ciphers for requests to a proxied HTTPS server.
|
||||
The ciphers are specified in the format understood by the OpenSSL library.`,
|
||||
},
|
||||
proxySSLProtocolsAnnotation: {
|
||||
Validator: parser.ValidateRegex(*proxySSLProtocolRegex, true),
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation enables the specified protocols for requests to a proxied HTTPS server.`,
|
||||
},
|
||||
proxySSLNameAnnotation: {
|
||||
Validator: parser.ValidateServerName,
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskHigh,
|
||||
Documentation: `This annotation allows to set proxy_ssl_name. This allows overriding the server name used to verify the certificate of the proxied HTTPS server.
|
||||
This value is also passed through SNI when a connection is established to the proxied HTTPS server.`,
|
||||
},
|
||||
proxySSLVerifyAnnotation: {
|
||||
Validator: parser.ValidateRegex(*proxySSLOnOffRegex, true),
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation enables or disables verification of the proxied HTTPS server certificate. (default: off)`,
|
||||
},
|
||||
proxySSLVerifyDepthAnnotation: {
|
||||
Validator: parser.ValidateInt,
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation Sets the verification depth in the proxied HTTPS server certificates chain. (default: 1).`,
|
||||
},
|
||||
proxySSLServerNameAnnotation: {
|
||||
Validator: parser.ValidateRegex(*proxySSLOnOffRegex, true),
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation enables passing of the server name through TLS Server Name Indication extension (SNI, RFC 6066) when establishing a connection with the proxied HTTPS server.`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Config contains the AuthSSLCert used for mutual authentication
|
||||
// and the configured VerifyDepth
|
||||
type Config struct {
|
||||
|
@ -85,11 +151,14 @@ func (pssl1 *Config) Equal(pssl2 *Config) bool {
|
|||
|
||||
// NewParser creates a new TLS authentication annotation parser
|
||||
func NewParser(resolver resolver.Resolver) parser.IngressAnnotation {
|
||||
return proxySSL{resolver}
|
||||
return proxySSL{
|
||||
r: resolver,
|
||||
annotationConfig: proxySSLAnnotation}
|
||||
}
|
||||
|
||||
type proxySSL struct {
|
||||
r resolver.Resolver
|
||||
r resolver.Resolver
|
||||
annotationConfig parser.Annotation
|
||||
}
|
||||
|
||||
func sortProtocols(protocols string) string {
|
||||
|
@ -120,16 +189,20 @@ func (p proxySSL) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
var err error
|
||||
config := &Config{}
|
||||
|
||||
proxysslsecret, err := parser.GetStringAnnotation("proxy-ssl-secret", ing)
|
||||
proxysslsecret, err := parser.GetStringAnnotation(proxySSLSecretAnnotation, ing, p.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
return &Config{}, err
|
||||
}
|
||||
|
||||
_, _, err = k8s.ParseNameNS(proxysslsecret)
|
||||
ns, _, err := k8s.ParseNameNS(proxysslsecret)
|
||||
if err != nil {
|
||||
return &Config{}, ing_errors.NewLocationDenied(err.Error())
|
||||
}
|
||||
|
||||
if ns != ing.Namespace {
|
||||
return &Config{}, ing_errors.NewLocationDenied("cross namespace secrets are not supported")
|
||||
}
|
||||
|
||||
proxyCert, err := p.r.GetAuthCertificate(proxysslsecret)
|
||||
if err != nil {
|
||||
e := fmt.Errorf("error obtaining certificate: %w", err)
|
||||
|
@ -137,37 +210,50 @@ func (p proxySSL) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
}
|
||||
config.AuthSSLCert = *proxyCert
|
||||
|
||||
config.Ciphers, err = parser.GetStringAnnotation("proxy-ssl-ciphers", ing)
|
||||
config.Ciphers, err = parser.GetStringAnnotation(proxySSLCiphersAnnotation, ing, p.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
if errors.IsValidationError(err) {
|
||||
klog.Warningf("invalid value passed to proxy-ssl-ciphers, defaulting to %s", defaultProxySSLCiphers)
|
||||
}
|
||||
config.Ciphers = defaultProxySSLCiphers
|
||||
}
|
||||
|
||||
config.Protocols, err = parser.GetStringAnnotation("proxy-ssl-protocols", ing)
|
||||
config.Protocols, err = parser.GetStringAnnotation(proxySSLProtocolsAnnotation, ing, p.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
if errors.IsValidationError(err) {
|
||||
klog.Warningf("invalid value passed to proxy-ssl-protocolos, defaulting to %s", defaultProxySSLProtocols)
|
||||
}
|
||||
config.Protocols = defaultProxySSLProtocols
|
||||
} else {
|
||||
config.Protocols = sortProtocols(config.Protocols)
|
||||
}
|
||||
|
||||
config.ProxySSLName, err = parser.GetStringAnnotation("proxy-ssl-name", ing)
|
||||
config.ProxySSLName, err = parser.GetStringAnnotation(proxySSLNameAnnotation, ing, p.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
if errors.IsValidationError(err) {
|
||||
klog.Warningf("invalid value passed to proxy-ssl-name, defaulting to empty")
|
||||
}
|
||||
config.ProxySSLName = ""
|
||||
}
|
||||
|
||||
config.Verify, err = parser.GetStringAnnotation("proxy-ssl-verify", ing)
|
||||
config.Verify, err = parser.GetStringAnnotation(proxySSLVerifyAnnotation, ing, p.annotationConfig.Annotations)
|
||||
if err != nil || !proxySSLOnOffRegex.MatchString(config.Verify) {
|
||||
config.Verify = defaultProxySSLVerify
|
||||
}
|
||||
|
||||
config.VerifyDepth, err = parser.GetIntAnnotation("proxy-ssl-verify-depth", ing)
|
||||
config.VerifyDepth, err = parser.GetIntAnnotation(proxySSLVerifyDepthAnnotation, ing, p.annotationConfig.Annotations)
|
||||
if err != nil || config.VerifyDepth == 0 {
|
||||
config.VerifyDepth = defaultProxySSLVerifyDepth
|
||||
}
|
||||
|
||||
config.ProxySSLServerName, err = parser.GetStringAnnotation("proxy-ssl-server-name", ing)
|
||||
config.ProxySSLServerName, err = parser.GetStringAnnotation(proxySSLServerNameAnnotation, ing, p.annotationConfig.Annotations)
|
||||
if err != nil || !proxySSLOnOffRegex.MatchString(config.ProxySSLServerName) {
|
||||
config.ProxySSLServerName = defaultProxySSLServerName
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func (p proxySSL) GetDocumentation() parser.AnnotationFields {
|
||||
return p.annotationConfig.Annotations
|
||||
}
|
||||
|
|
|
@ -93,7 +93,7 @@ func TestAnnotations(t *testing.T) {
|
|||
ing := buildIngress()
|
||||
data := map[string]string{}
|
||||
|
||||
data[parser.GetAnnotationWithPrefix("proxy-ssl-secret")] = "default/demo-secret"
|
||||
data[parser.GetAnnotationWithPrefix(proxySSLSecretAnnotation)] = "default/demo-secret"
|
||||
data[parser.GetAnnotationWithPrefix("proxy-ssl-ciphers")] = "HIGH:-SHA"
|
||||
data[parser.GetAnnotationWithPrefix("proxy-ssl-name")] = "$host"
|
||||
data[parser.GetAnnotationWithPrefix("proxy-ssl-protocols")] = "TLSv1.3 SSLv2 TLSv1 TLSv1.2"
|
||||
|
|
|
@ -24,6 +24,7 @@ import (
|
|||
networking "k8s.io/api/networking/v1"
|
||||
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/parser"
|
||||
"k8s.io/ingress-nginx/internal/ingress/errors"
|
||||
"k8s.io/ingress-nginx/internal/ingress/resolver"
|
||||
"k8s.io/ingress-nginx/internal/net"
|
||||
"k8s.io/ingress-nginx/pkg/util/sets"
|
||||
|
@ -58,7 +59,7 @@ type Config struct {
|
|||
|
||||
ID string `json:"id"`
|
||||
|
||||
Whitelist []string `json:"whitelist"`
|
||||
Allowlist []string `json:"allowlist"`
|
||||
}
|
||||
|
||||
// Equal tests for equality between two RateLimit types
|
||||
|
@ -90,11 +91,11 @@ func (rt1 *Config) Equal(rt2 *Config) bool {
|
|||
if rt1.Name != rt2.Name {
|
||||
return false
|
||||
}
|
||||
if len(rt1.Whitelist) != len(rt2.Whitelist) {
|
||||
if len(rt1.Allowlist) != len(rt2.Allowlist) {
|
||||
return false
|
||||
}
|
||||
|
||||
return sets.StringElementsMatch(rt1.Whitelist, rt2.Whitelist)
|
||||
return sets.StringElementsMatch(rt1.Allowlist, rt2.Allowlist)
|
||||
}
|
||||
|
||||
// Zone returns information about the NGINX rate limit (limit_req_zone)
|
||||
|
@ -131,43 +132,121 @@ func (z1 *Zone) Equal(z2 *Zone) bool {
|
|||
return true
|
||||
}
|
||||
|
||||
const (
|
||||
limitRateAnnotation = "limit-rate"
|
||||
limitRateAfterAnnotation = "limit-rate-after"
|
||||
limitRateRPMAnnotation = "limit-rpm"
|
||||
limitRateRPSAnnotation = "limit-rps"
|
||||
limitRateConnectionsAnnotation = "limit-connections"
|
||||
limitRateBurstMultiplierAnnotation = "limit-burst-multiplier"
|
||||
limitWhitelistAnnotation = "limit-whitelist" // This annotation is an alias for limit-allowlist
|
||||
limitAllowlistAnnotation = "limit-allowlist"
|
||||
)
|
||||
|
||||
var rateLimitAnnotations = parser.Annotation{
|
||||
Group: "rate-limit",
|
||||
Annotations: parser.AnnotationFields{
|
||||
limitRateAnnotation: {
|
||||
Validator: parser.ValidateInt,
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow, // Low, as it allows just a set of options
|
||||
Documentation: `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.
|
||||
References: https://nginx.org/en/docs/http/ngx_http_core_module.html#limit_rate`,
|
||||
},
|
||||
limitRateAfterAnnotation: {
|
||||
Validator: parser.ValidateInt,
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow, // Low, as it allows just a set of options
|
||||
Documentation: `Sets the initial amount after which the further transmission of a response to a client will be rate limited.`,
|
||||
},
|
||||
limitRateRPMAnnotation: {
|
||||
Validator: parser.ValidateInt,
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow, // Low, as it allows just a set of options
|
||||
Documentation: `Requests per minute that will be allowed.`,
|
||||
},
|
||||
limitRateRPSAnnotation: {
|
||||
Validator: parser.ValidateInt,
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow, // Low, as it allows just a set of options
|
||||
Documentation: `Requests per second that will be allowed.`,
|
||||
},
|
||||
limitRateConnectionsAnnotation: {
|
||||
Validator: parser.ValidateInt,
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow, // Low, as it allows just a set of options
|
||||
Documentation: `Number of connections that will be allowed`,
|
||||
},
|
||||
limitRateBurstMultiplierAnnotation: {
|
||||
Validator: parser.ValidateInt,
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow, // Low, as it allows just a set of options
|
||||
Documentation: `Burst multiplier for a limit-rate enabled location.`,
|
||||
},
|
||||
limitAllowlistAnnotation: {
|
||||
Validator: parser.ValidateCIDRs,
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow, // Low, as it allows just a set of options
|
||||
Documentation: `List of CIDR/IP addresses that will not be rate-limited.`,
|
||||
AnnotationAliases: []string{limitWhitelistAnnotation},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
type ratelimit struct {
|
||||
r resolver.Resolver
|
||||
r resolver.Resolver
|
||||
annotationConfig parser.Annotation
|
||||
}
|
||||
|
||||
// NewParser creates a new ratelimit annotation parser
|
||||
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
|
||||
return ratelimit{r}
|
||||
return ratelimit{
|
||||
r: r,
|
||||
annotationConfig: rateLimitAnnotations,
|
||||
}
|
||||
}
|
||||
|
||||
// ParseAnnotations parses the annotations contained in the ingress
|
||||
// rule used to rewrite the defined paths
|
||||
func (a ratelimit) Parse(ing *networking.Ingress) (interface{}, error) {
|
||||
defBackend := a.r.GetDefaultBackend()
|
||||
lr, err := parser.GetIntAnnotation("limit-rate", ing)
|
||||
lr, err := parser.GetIntAnnotation(limitRateAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
lr = defBackend.LimitRate
|
||||
}
|
||||
lra, err := parser.GetIntAnnotation("limit-rate-after", ing)
|
||||
lra, err := parser.GetIntAnnotation(limitRateAfterAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
lra = defBackend.LimitRateAfter
|
||||
}
|
||||
|
||||
rpm, _ := parser.GetIntAnnotation("limit-rpm", ing)
|
||||
rps, _ := parser.GetIntAnnotation("limit-rps", ing)
|
||||
conn, _ := parser.GetIntAnnotation("limit-connections", ing)
|
||||
burstMultiplier, err := parser.GetIntAnnotation("limit-burst-multiplier", ing)
|
||||
rpm, err := parser.GetIntAnnotation(limitRateRPMAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil && errors.IsValidationError(err) {
|
||||
return nil, err
|
||||
}
|
||||
rps, err := parser.GetIntAnnotation(limitRateRPSAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil && errors.IsValidationError(err) {
|
||||
return nil, err
|
||||
}
|
||||
conn, err := parser.GetIntAnnotation(limitRateConnectionsAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil && errors.IsValidationError(err) {
|
||||
return nil, err
|
||||
}
|
||||
burstMultiplier, err := parser.GetIntAnnotation(limitRateBurstMultiplierAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
burstMultiplier = defBurst
|
||||
}
|
||||
|
||||
val, _ := parser.GetStringAnnotation("limit-whitelist", ing)
|
||||
|
||||
cidrs, err := net.ParseCIDRs(val)
|
||||
if err != nil {
|
||||
val, err := parser.GetStringAnnotation(limitAllowlistAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil && errors.IsValidationError(err) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cidrs, errCidr := net.ParseCIDRs(val)
|
||||
if errCidr != nil {
|
||||
return nil, errCidr
|
||||
}
|
||||
|
||||
if rpm == 0 && rps == 0 && conn == 0 {
|
||||
return &Config{
|
||||
Connections: Zone{},
|
||||
|
@ -203,7 +282,7 @@ func (a ratelimit) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
LimitRateAfter: lra,
|
||||
Name: zoneName,
|
||||
ID: encode(zoneName),
|
||||
Whitelist: cidrs,
|
||||
Allowlist: cidrs,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -211,3 +290,7 @@ func encode(s string) string {
|
|||
str := base64.URLEncoding.EncodeToString([]byte(s))
|
||||
return strings.Replace(str, "=", "", -1)
|
||||
}
|
||||
|
||||
func (a ratelimit) GetDocumentation() parser.AnnotationFields {
|
||||
return a.annotationConfig.Annotations
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ import (
|
|||
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/parser"
|
||||
"k8s.io/ingress-nginx/internal/ingress/defaults"
|
||||
"k8s.io/ingress-nginx/internal/ingress/errors"
|
||||
"k8s.io/ingress-nginx/internal/ingress/resolver"
|
||||
)
|
||||
|
||||
|
@ -85,8 +86,8 @@ func (m mockBackend) GetDefaultBackend() defaults.Backend {
|
|||
func TestWithoutAnnotations(t *testing.T) {
|
||||
ing := buildIngress()
|
||||
_, err := NewParser(mockBackend{}).Parse(ing)
|
||||
if err != nil {
|
||||
t.Error("unexpected error with ingress without annotations")
|
||||
if err != nil && !errors.IsMissingAnnotations(err) {
|
||||
t.Errorf("unexpected error with ingress without annotations: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -94,22 +95,22 @@ func TestRateLimiting(t *testing.T) {
|
|||
ing := buildIngress()
|
||||
|
||||
data := map[string]string{}
|
||||
data[parser.GetAnnotationWithPrefix("limit-connections")] = "0"
|
||||
data[parser.GetAnnotationWithPrefix("limit-rps")] = "0"
|
||||
data[parser.GetAnnotationWithPrefix("limit-rpm")] = "0"
|
||||
data[parser.GetAnnotationWithPrefix(limitRateConnectionsAnnotation)] = "0"
|
||||
data[parser.GetAnnotationWithPrefix(limitRateRPSAnnotation)] = "0"
|
||||
data[parser.GetAnnotationWithPrefix(limitRateRPMAnnotation)] = "0"
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
_, err := NewParser(mockBackend{}).Parse(ing)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error with invalid limits (0)")
|
||||
t.Errorf("unexpected error with invalid limits (0): %s", err)
|
||||
}
|
||||
|
||||
data = map[string]string{}
|
||||
data[parser.GetAnnotationWithPrefix("limit-connections")] = "5"
|
||||
data[parser.GetAnnotationWithPrefix("limit-rps")] = "100"
|
||||
data[parser.GetAnnotationWithPrefix("limit-rpm")] = "10"
|
||||
data[parser.GetAnnotationWithPrefix("limit-rate-after")] = "100"
|
||||
data[parser.GetAnnotationWithPrefix("limit-rate")] = "10"
|
||||
data[parser.GetAnnotationWithPrefix(limitRateConnectionsAnnotation)] = "5"
|
||||
data[parser.GetAnnotationWithPrefix(limitRateRPSAnnotation)] = "100"
|
||||
data[parser.GetAnnotationWithPrefix(limitRateRPMAnnotation)] = "10"
|
||||
data[parser.GetAnnotationWithPrefix(limitRateAfterAnnotation)] = "100"
|
||||
data[parser.GetAnnotationWithPrefix(limitRateAnnotation)] = "10"
|
||||
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
|
@ -147,12 +148,12 @@ func TestRateLimiting(t *testing.T) {
|
|||
}
|
||||
|
||||
data = map[string]string{}
|
||||
data[parser.GetAnnotationWithPrefix("limit-connections")] = "5"
|
||||
data[parser.GetAnnotationWithPrefix("limit-rps")] = "100"
|
||||
data[parser.GetAnnotationWithPrefix("limit-rpm")] = "10"
|
||||
data[parser.GetAnnotationWithPrefix("limit-rate-after")] = "100"
|
||||
data[parser.GetAnnotationWithPrefix("limit-rate")] = "10"
|
||||
data[parser.GetAnnotationWithPrefix("limit-burst-multiplier")] = "3"
|
||||
data[parser.GetAnnotationWithPrefix(limitRateConnectionsAnnotation)] = "5"
|
||||
data[parser.GetAnnotationWithPrefix(limitRateRPSAnnotation)] = "100"
|
||||
data[parser.GetAnnotationWithPrefix(limitRateRPMAnnotation)] = "10"
|
||||
data[parser.GetAnnotationWithPrefix(limitRateAfterAnnotation)] = "100"
|
||||
data[parser.GetAnnotationWithPrefix(limitRateAnnotation)] = "10"
|
||||
data[parser.GetAnnotationWithPrefix(limitRateBurstMultiplierAnnotation)] = "3"
|
||||
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
|
@ -189,3 +190,61 @@ func TestRateLimiting(t *testing.T) {
|
|||
t.Errorf("expected 10 in limit by limitrate but %v was returned", rateLimit.LimitRate)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnnotationCIDR(t *testing.T) {
|
||||
ing := buildIngress()
|
||||
|
||||
data := map[string]string{}
|
||||
data[parser.GetAnnotationWithPrefix(limitRateConnectionsAnnotation)] = "5"
|
||||
data[parser.GetAnnotationWithPrefix(limitAllowlistAnnotation)] = "192.168.0.5, 192.168.50.32/24"
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
i, err := NewParser(mockBackend{}).Parse(ing)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
rateLimit, ok := i.(*Config)
|
||||
if !ok {
|
||||
t.Errorf("expected a RateLimit type")
|
||||
}
|
||||
if len(rateLimit.Allowlist) != 2 {
|
||||
t.Errorf("expected 2 cidrs in limit by ip but %v was returned", len(rateLimit.Allowlist))
|
||||
}
|
||||
|
||||
data = map[string]string{}
|
||||
data[parser.GetAnnotationWithPrefix(limitRateConnectionsAnnotation)] = "5"
|
||||
data[parser.GetAnnotationWithPrefix(limitWhitelistAnnotation)] = "192.168.0.5, 192.168.50.32/24, 10.10.10.1"
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
i, err = NewParser(mockBackend{}).Parse(ing)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
rateLimit, ok = i.(*Config)
|
||||
if !ok {
|
||||
t.Errorf("expected a RateLimit type")
|
||||
}
|
||||
if len(rateLimit.Allowlist) != 3 {
|
||||
t.Errorf("expected 3 cidrs in limit by ip but %v was returned", len(rateLimit.Allowlist))
|
||||
}
|
||||
|
||||
// Parent annotation surpasses any alias
|
||||
data = map[string]string{}
|
||||
data[parser.GetAnnotationWithPrefix(limitRateConnectionsAnnotation)] = "5"
|
||||
data[parser.GetAnnotationWithPrefix(limitWhitelistAnnotation)] = "192.168.0.5, 192.168.50.32/24, 10.10.10.1"
|
||||
data[parser.GetAnnotationWithPrefix(limitAllowlistAnnotation)] = "192.168.0.9"
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
i, err = NewParser(mockBackend{}).Parse(ing)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
rateLimit, ok = i.(*Config)
|
||||
if !ok {
|
||||
t.Errorf("expected a RateLimit type")
|
||||
}
|
||||
if len(rateLimit.Allowlist) != 1 {
|
||||
t.Errorf("expected 1 cidrs in limit by ip but %v was returned", len(rateLimit.Allowlist))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,13 +37,56 @@ type Config struct {
|
|||
FromToWWW bool `json:"fromToWWW"`
|
||||
}
|
||||
|
||||
const (
|
||||
fromToWWWRedirAnnotation = "from-to-www-redirect"
|
||||
temporalRedirectAnnotation = "temporal-redirect"
|
||||
permanentRedirectAnnotation = "permanent-redirect"
|
||||
permanentRedirectAnnotationCode = "permanent-redirect-code"
|
||||
)
|
||||
|
||||
var redirectAnnotations = parser.Annotation{
|
||||
Group: "redirect",
|
||||
Annotations: parser.AnnotationFields{
|
||||
fromToWWWRedirAnnotation: {
|
||||
Validator: parser.ValidateBool,
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow, // Low, as it allows just a set of options
|
||||
Documentation: `In some scenarios is required to redirect from www.domain.com to domain.com or vice versa. To enable this feature use this annotation.`,
|
||||
},
|
||||
temporalRedirectAnnotation: {
|
||||
Validator: parser.ValidateRegex(*parser.URLIsValidRegex, false),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskMedium, // Medium, as it allows arbitrary URLs that needs to be validated
|
||||
Documentation: `This annotation allows you to return a temporal redirect (Return Code 302) instead of sending data to the upstream.
|
||||
For example setting this annotation to https://www.google.com would redirect everything to Google with a Return Code of 302 (Moved Temporarily).`,
|
||||
},
|
||||
permanentRedirectAnnotation: {
|
||||
Validator: parser.ValidateRegex(*parser.URLIsValidRegex, false),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskMedium, // Medium, as it allows arbitrary URLs that needs to be validated
|
||||
Documentation: `This annotation allows to return a permanent redirect (Return Code 301) instead of sending data to the upstream.
|
||||
For example setting this annotation https://www.google.com would redirect everything to Google with a code 301`,
|
||||
},
|
||||
permanentRedirectAnnotationCode: {
|
||||
Validator: parser.ValidateInt,
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow, // Low, as it allows just a set of options
|
||||
Documentation: `This annotation allows you to modify the status code used for permanent redirects.`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
type redirect struct {
|
||||
r resolver.Resolver
|
||||
r resolver.Resolver
|
||||
annotationConfig parser.Annotation
|
||||
}
|
||||
|
||||
// NewParser creates a new redirect annotation parser
|
||||
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
|
||||
return redirect{r}
|
||||
return redirect{
|
||||
r: r,
|
||||
annotationConfig: redirectAnnotations,
|
||||
}
|
||||
}
|
||||
|
||||
// Parse parses the annotations contained in the ingress
|
||||
|
@ -51,9 +94,12 @@ func NewParser(r resolver.Resolver) parser.IngressAnnotation {
|
|||
// If the Ingress contains both annotations the execution order is
|
||||
// temporal and then permanent
|
||||
func (r redirect) Parse(ing *networking.Ingress) (interface{}, error) {
|
||||
r3w, _ := parser.GetBoolAnnotation("from-to-www-redirect", ing)
|
||||
r3w, err := parser.GetBoolAnnotation(fromToWWWRedirAnnotation, ing, r.annotationConfig.Annotations)
|
||||
if err != nil && !errors.IsMissingAnnotations(err) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tr, err := parser.GetStringAnnotation("temporal-redirect", ing)
|
||||
tr, err := parser.GetStringAnnotation(temporalRedirectAnnotation, ing, r.annotationConfig.Annotations)
|
||||
if err != nil && !errors.IsMissingAnnotations(err) {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -70,12 +116,12 @@ func (r redirect) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
}, nil
|
||||
}
|
||||
|
||||
pr, err := parser.GetStringAnnotation("permanent-redirect", ing)
|
||||
pr, err := parser.GetStringAnnotation(permanentRedirectAnnotation, ing, r.annotationConfig.Annotations)
|
||||
if err != nil && !errors.IsMissingAnnotations(err) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
prc, err := parser.GetIntAnnotation("permanent-redirect-code", ing)
|
||||
prc, err := parser.GetIntAnnotation(permanentRedirectAnnotationCode, ing, r.annotationConfig.Annotations)
|
||||
if err != nil && !errors.IsMissingAnnotations(err) {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -127,3 +173,7 @@ func isValidURL(s string) error {
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a redirect) GetDocumentation() parser.AnnotationFields {
|
||||
return a.annotationConfig.Annotations
|
||||
}
|
||||
|
|
|
@ -43,7 +43,7 @@ func TestPermanentRedirectWithDefaultCode(t *testing.T) {
|
|||
ing := new(networking.Ingress)
|
||||
|
||||
data := make(map[string]string, 1)
|
||||
data[parser.GetAnnotationWithPrefix("permanent-redirect")] = defRedirectURL
|
||||
data[parser.GetAnnotationWithPrefix(permanentRedirectAnnotation)] = defRedirectURL
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
i, err := rp.Parse(ing)
|
||||
|
@ -81,8 +81,8 @@ func TestPermanentRedirectWithCustomCode(t *testing.T) {
|
|||
ing := new(networking.Ingress)
|
||||
|
||||
data := make(map[string]string, 2)
|
||||
data[parser.GetAnnotationWithPrefix("permanent-redirect")] = defRedirectURL
|
||||
data[parser.GetAnnotationWithPrefix("permanent-redirect-code")] = strconv.Itoa(tc.input)
|
||||
data[parser.GetAnnotationWithPrefix(permanentRedirectAnnotation)] = defRedirectURL
|
||||
data[parser.GetAnnotationWithPrefix(permanentRedirectAnnotationCode)] = strconv.Itoa(tc.input)
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
i, err := rp.Parse(ing)
|
||||
|
@ -112,8 +112,8 @@ func TestTemporalRedirect(t *testing.T) {
|
|||
ing := new(networking.Ingress)
|
||||
|
||||
data := make(map[string]string, 1)
|
||||
data[parser.GetAnnotationWithPrefix("from-to-www-redirect")] = "true"
|
||||
data[parser.GetAnnotationWithPrefix("temporal-redirect")] = defRedirectURL
|
||||
data[parser.GetAnnotationWithPrefix(fromToWWWRedirAnnotation)] = "true"
|
||||
data[parser.GetAnnotationWithPrefix(temporalRedirectAnnotation)] = defRedirectURL
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
i, err := rp.Parse(ing)
|
||||
|
|
|
@ -27,6 +27,59 @@ import (
|
|||
"k8s.io/ingress-nginx/internal/ingress/resolver"
|
||||
)
|
||||
|
||||
const (
|
||||
rewriteTargetAnnotation = "rewrite-target"
|
||||
sslRedirectAnnotation = "ssl-redirect"
|
||||
preserveTrailingSlashAnnotation = "preserve-trailing-slash"
|
||||
forceSSLRedirectAnnotation = "force-ssl-redirect"
|
||||
useRegexAnnotation = "use-regex"
|
||||
appRootAnnotation = "app-root"
|
||||
)
|
||||
|
||||
var rewriteAnnotations = parser.Annotation{
|
||||
Group: "rewrite",
|
||||
Annotations: parser.AnnotationFields{
|
||||
rewriteTargetAnnotation: {
|
||||
Validator: parser.ValidateRegex(*parser.RegexPathWithCapture, false),
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskMedium,
|
||||
Documentation: `This annotation allows to specify the target URI where the traffic must be redirected. It can contain regular characters and captured
|
||||
groups specified as '$1', '$2', etc.`,
|
||||
},
|
||||
sslRedirectAnnotation: {
|
||||
Validator: parser.ValidateBool,
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation defines if the location section is only accessible via SSL`,
|
||||
},
|
||||
preserveTrailingSlashAnnotation: {
|
||||
Validator: parser.ValidateBool,
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskMedium,
|
||||
Documentation: `This annotation defines if the trailing slash should be preserved in the URI with 'ssl-redirect'`,
|
||||
},
|
||||
forceSSLRedirectAnnotation: {
|
||||
Validator: parser.ValidateBool,
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskMedium,
|
||||
Documentation: `This annotation forces the redirection to HTTPS even if the Ingress is not TLS Enabled`,
|
||||
},
|
||||
useRegexAnnotation: {
|
||||
Validator: parser.ValidateBool,
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation defines if the paths defined on an Ingress use regular expressions. To use regex on path
|
||||
the pathType should also be defined as 'ImplementationSpecific'.`,
|
||||
},
|
||||
appRootAnnotation: {
|
||||
Validator: parser.ValidateRegex(*parser.RegexPathWithCapture, false),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskMedium,
|
||||
Documentation: `This annotation defines the Application Root that the Controller must redirect if it's in / context`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Config describes the per location redirect config
|
||||
type Config struct {
|
||||
// Target URI where the traffic must be redirected
|
||||
|
@ -71,12 +124,16 @@ func (r1 *Config) Equal(r2 *Config) bool {
|
|||
}
|
||||
|
||||
type rewrite struct {
|
||||
r resolver.Resolver
|
||||
r resolver.Resolver
|
||||
annotationConfig parser.Annotation
|
||||
}
|
||||
|
||||
// NewParser creates a new rewrite annotation parser
|
||||
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
|
||||
return rewrite{r}
|
||||
return rewrite{
|
||||
r: r,
|
||||
annotationConfig: rewriteAnnotations,
|
||||
}
|
||||
}
|
||||
|
||||
// ParseAnnotations parses the annotations contained in the ingress
|
||||
|
@ -85,24 +142,45 @@ func (a rewrite) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
var err error
|
||||
config := &Config{}
|
||||
|
||||
config.Target, _ = parser.GetStringAnnotation("rewrite-target", ing)
|
||||
config.SSLRedirect, err = parser.GetBoolAnnotation("ssl-redirect", ing)
|
||||
config.Target, err = parser.GetStringAnnotation(rewriteTargetAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
if errors.IsValidationError(err) {
|
||||
klog.Warningf("%sis invalid, defaulting to empty", rewriteTargetAnnotation)
|
||||
}
|
||||
config.Target = ""
|
||||
}
|
||||
config.SSLRedirect, err = parser.GetBoolAnnotation(sslRedirectAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
if errors.IsValidationError(err) {
|
||||
klog.Warningf("%sis invalid, defaulting to '%s'", sslRedirectAnnotation, a.r.GetDefaultBackend().SSLRedirect)
|
||||
}
|
||||
config.SSLRedirect = a.r.GetDefaultBackend().SSLRedirect
|
||||
}
|
||||
config.PreserveTrailingSlash, err = parser.GetBoolAnnotation("preserve-trailing-slash", ing)
|
||||
config.PreserveTrailingSlash, err = parser.GetBoolAnnotation(preserveTrailingSlashAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
if errors.IsValidationError(err) {
|
||||
klog.Warningf("%sis invalid, defaulting to '%s'", preserveTrailingSlashAnnotation, a.r.GetDefaultBackend().PreserveTrailingSlash)
|
||||
}
|
||||
config.PreserveTrailingSlash = a.r.GetDefaultBackend().PreserveTrailingSlash
|
||||
}
|
||||
|
||||
config.ForceSSLRedirect, err = parser.GetBoolAnnotation("force-ssl-redirect", ing)
|
||||
config.ForceSSLRedirect, err = parser.GetBoolAnnotation(forceSSLRedirectAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
if errors.IsValidationError(err) {
|
||||
klog.Warningf("%sis invalid, defaulting to '%s'", forceSSLRedirectAnnotation, a.r.GetDefaultBackend().ForceSSLRedirect)
|
||||
}
|
||||
config.ForceSSLRedirect = a.r.GetDefaultBackend().ForceSSLRedirect
|
||||
}
|
||||
|
||||
config.UseRegex, _ = parser.GetBoolAnnotation("use-regex", ing)
|
||||
config.UseRegex, err = parser.GetBoolAnnotation(useRegexAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
if errors.IsValidationError(err) {
|
||||
klog.Warningf("%sis invalid, defaulting to 'false'", useRegexAnnotation)
|
||||
}
|
||||
config.UseRegex = false
|
||||
}
|
||||
|
||||
config.AppRoot, err = parser.GetStringAnnotation("app-root", ing)
|
||||
config.AppRoot, err = parser.GetStringAnnotation(appRootAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
if !errors.IsMissingAnnotations(err) && !errors.IsInvalidContent(err) {
|
||||
klog.Warningf("Annotation app-root contains an invalid value: %v", err)
|
||||
|
@ -126,3 +204,7 @@ func (a rewrite) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func (a rewrite) GetDocumentation() parser.AnnotationFields {
|
||||
return a.annotationConfig.Annotations
|
||||
}
|
||||
|
|
|
@ -129,6 +129,30 @@ func TestSSLRedirect(t *testing.T) {
|
|||
t.Errorf("Expected true but returned false")
|
||||
}
|
||||
|
||||
data[parser.GetAnnotationWithPrefix("rewrite-target")] = "/xpto/$1/abc/$2"
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
i, _ = NewParser(mockBackend{redirect: true}).Parse(ing)
|
||||
redirect, ok = i.(*Config)
|
||||
if !ok {
|
||||
t.Errorf("expected a Redirect type")
|
||||
}
|
||||
if redirect.Target != "/xpto/$1/abc/$2" {
|
||||
t.Errorf("Expected /xpto/$1/abc/$2 but returned %s", redirect.Target)
|
||||
}
|
||||
|
||||
data[parser.GetAnnotationWithPrefix("rewrite-target")] = "/xpto/xas{445}"
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
i, _ = NewParser(mockBackend{redirect: true}).Parse(ing)
|
||||
redirect, ok = i.(*Config)
|
||||
if !ok {
|
||||
t.Errorf("expected a Redirect type")
|
||||
}
|
||||
if redirect.Target != "" {
|
||||
t.Errorf("Expected empty rewrite target but returned %s", redirect.Target)
|
||||
}
|
||||
|
||||
data[parser.GetAnnotationWithPrefix("ssl-redirect")] = "false"
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
|
|
|
@ -23,18 +23,40 @@ import (
|
|||
"k8s.io/ingress-nginx/internal/ingress/resolver"
|
||||
)
|
||||
|
||||
const (
|
||||
satisfyAnnotation = "satisfy"
|
||||
)
|
||||
|
||||
var satisfyAnnotations = parser.Annotation{
|
||||
Group: "authentication",
|
||||
Annotations: parser.AnnotationFields{
|
||||
satisfyAnnotation: {
|
||||
Validator: parser.ValidateOptions([]string{"any", "all"}, true, true),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `By default, a request would need to satisfy all authentication requirements in order to be allowed.
|
||||
By using this annotation, requests that satisfy either any or all authentication requirements are allowed, based on the configuration value.
|
||||
Valid options are "all" and "any"`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
type satisfy struct {
|
||||
r resolver.Resolver
|
||||
r resolver.Resolver
|
||||
annotationConfig parser.Annotation
|
||||
}
|
||||
|
||||
// NewParser creates a new SATISFY annotation parser
|
||||
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
|
||||
return satisfy{r}
|
||||
return satisfy{
|
||||
r: r,
|
||||
annotationConfig: satisfyAnnotations,
|
||||
}
|
||||
}
|
||||
|
||||
// Parse parses annotation contained in the ingress
|
||||
func (s satisfy) Parse(ing *networking.Ingress) (interface{}, error) {
|
||||
satisfy, err := parser.GetStringAnnotation("satisfy", ing)
|
||||
satisfy, err := parser.GetStringAnnotation(satisfyAnnotation, ing, s.annotationConfig.Annotations)
|
||||
|
||||
if err != nil || (satisfy != "any" && satisfy != "all") {
|
||||
satisfy = ""
|
||||
|
@ -42,3 +64,7 @@ func (s satisfy) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
|
||||
return satisfy, nil
|
||||
}
|
||||
|
||||
func (s satisfy) GetDocumentation() parser.AnnotationFields {
|
||||
return s.annotationConfig.Annotations
|
||||
}
|
||||
|
|
|
@ -83,7 +83,7 @@ func TestSatisfyParser(t *testing.T) {
|
|||
annotations := map[string]string{}
|
||||
|
||||
for input, expected := range data {
|
||||
annotations[parser.GetAnnotationWithPrefix("satisfy")] = input
|
||||
annotations[parser.GetAnnotationWithPrefix(satisfyAnnotation)] = input
|
||||
ing.SetAnnotations(annotations)
|
||||
|
||||
satisfyt, err := NewParser(&resolver.Mock{}).Parse(ing)
|
||||
|
|
|
@ -23,18 +23,42 @@ import (
|
|||
"k8s.io/ingress-nginx/internal/ingress/resolver"
|
||||
)
|
||||
|
||||
const (
|
||||
serverSnippetAnnotation = "server-snippet"
|
||||
)
|
||||
|
||||
var serverSnippetAnnotations = parser.Annotation{
|
||||
Group: "snippets",
|
||||
Annotations: parser.AnnotationFields{
|
||||
serverSnippetAnnotation: {
|
||||
Validator: parser.ValidateNull,
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskCritical, // Critical, this annotation is not validated at all and allows arbitrary configutations
|
||||
Documentation: `This annotation allows setting a custom NGINX configuration on a server block. This annotation does not contain any validation and it's usage is not recommended!`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
type serverSnippet struct {
|
||||
r resolver.Resolver
|
||||
r resolver.Resolver
|
||||
annotationConfig parser.Annotation
|
||||
}
|
||||
|
||||
// NewParser creates a new server snippet annotation parser
|
||||
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
|
||||
return serverSnippet{r}
|
||||
return serverSnippet{
|
||||
r: r,
|
||||
annotationConfig: serverSnippetAnnotations,
|
||||
}
|
||||
}
|
||||
|
||||
// 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 *networking.Ingress) (interface{}, error) {
|
||||
return parser.GetStringAnnotation("server-snippet", ing)
|
||||
return parser.GetStringAnnotation(serverSnippetAnnotation, ing, a.annotationConfig.Annotations)
|
||||
}
|
||||
|
||||
func (a serverSnippet) GetDocumentation() parser.AnnotationFields {
|
||||
return a.annotationConfig.Annotations
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@ import (
|
|||
)
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
annotation := parser.GetAnnotationWithPrefix("server-snippet")
|
||||
annotation := parser.GetAnnotationWithPrefix(serverSnippetAnnotation)
|
||||
|
||||
ap := NewParser(&resolver.Mock{})
|
||||
if ap == nil {
|
||||
|
|
|
@ -24,19 +24,39 @@ import (
|
|||
"k8s.io/ingress-nginx/internal/ingress/resolver"
|
||||
)
|
||||
|
||||
const (
|
||||
serviceUpstreamAnnotation = "service-upstream"
|
||||
)
|
||||
|
||||
var serviceUpstreamAnnotations = parser.Annotation{
|
||||
Group: "backend",
|
||||
Annotations: parser.AnnotationFields{
|
||||
serviceUpstreamAnnotation: {
|
||||
Validator: parser.ValidateBool,
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskLow, // Critical, this annotation is not validated at all and allows arbitrary configutations
|
||||
Documentation: `This annotation makes NGINX use Service's Cluster IP and Port instead of Endpoints as the backend endpoints`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
type serviceUpstream struct {
|
||||
r resolver.Resolver
|
||||
r resolver.Resolver
|
||||
annotationConfig parser.Annotation
|
||||
}
|
||||
|
||||
// NewParser creates a new serviceUpstream annotation parser
|
||||
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
|
||||
return serviceUpstream{r}
|
||||
return serviceUpstream{
|
||||
r: r,
|
||||
annotationConfig: serviceUpstreamAnnotations,
|
||||
}
|
||||
}
|
||||
|
||||
func (s serviceUpstream) Parse(ing *networking.Ingress) (interface{}, error) {
|
||||
defBackend := s.r.GetDefaultBackend()
|
||||
|
||||
val, err := parser.GetBoolAnnotation("service-upstream", ing)
|
||||
val, err := parser.GetBoolAnnotation(serviceUpstreamAnnotation, ing, s.annotationConfig.Annotations)
|
||||
// A missing annotation is not a problem, just use the default
|
||||
if err == errors.ErrMissingAnnotations {
|
||||
return defBackend.ServiceUpstream, nil
|
||||
|
@ -44,3 +64,7 @@ func (s serviceUpstream) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
|
||||
return val, nil
|
||||
}
|
||||
|
||||
func (s serviceUpstream) GetDocumentation() parser.AnnotationFields {
|
||||
return s.annotationConfig.Annotations
|
||||
}
|
||||
|
|
|
@ -74,7 +74,7 @@ func TestIngressAnnotationServiceUpstreamEnabled(t *testing.T) {
|
|||
ing := buildIngress()
|
||||
|
||||
data := map[string]string{}
|
||||
data[parser.GetAnnotationWithPrefix("service-upstream")] = "true"
|
||||
data[parser.GetAnnotationWithPrefix(serviceUpstreamAnnotation)] = "true"
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
val, _ := NewParser(&resolver.Mock{}).Parse(ing)
|
||||
|
@ -93,7 +93,7 @@ func TestIngressAnnotationServiceUpstreamSetFalse(t *testing.T) {
|
|||
|
||||
// Test with explicitly set to false
|
||||
data := map[string]string{}
|
||||
data[parser.GetAnnotationWithPrefix("service-upstream")] = "false"
|
||||
data[parser.GetAnnotationWithPrefix(serviceUpstreamAnnotation)] = "false"
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
val, _ := NewParser(&resolver.Mock{}).Parse(ing)
|
||||
|
@ -155,7 +155,7 @@ func TestParseAnnotationsOverridesDefaultConfig(t *testing.T) {
|
|||
ing := buildIngress()
|
||||
|
||||
data := map[string]string{}
|
||||
data[parser.GetAnnotationWithPrefix("service-upstream")] = "false"
|
||||
data[parser.GetAnnotationWithPrefix(serviceUpstreamAnnotation)] = "false"
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
val, _ := NewParser(mockBackend{}).Parse(ing)
|
||||
|
|
|
@ -65,6 +65,90 @@ const (
|
|||
annotationAffinityCookieChangeOnFailure = "session-cookie-change-on-failure"
|
||||
)
|
||||
|
||||
var sessionAffinityAnnotations = parser.Annotation{
|
||||
Group: "affinity",
|
||||
Annotations: parser.AnnotationFields{
|
||||
annotationAffinityType: {
|
||||
Validator: parser.ValidateOptions([]string{"cookie"}, true, true),
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation enables and sets the affinity type in all Upstreams of an Ingress. This way, a request will always be directed to the same upstream server. The only affinity type available for NGINX is cookie`,
|
||||
},
|
||||
annotationAffinityMode: {
|
||||
Validator: parser.ValidateOptions([]string{"balanced", "persistent"}, true, true),
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskMedium,
|
||||
Documentation: `This annotation defines the stickiness of a session.
|
||||
Setting this to balanced (default) will redistribute some sessions if a deployment gets scaled up, therefore rebalancing the load on the servers.
|
||||
Setting this to persistent will not rebalance sessions to new servers, therefore providing maximum stickiness.`,
|
||||
},
|
||||
annotationAffinityCanaryBehavior: {
|
||||
Validator: parser.ValidateOptions([]string{"sticky", "legacy"}, true, true),
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation defines the behavior of canaries when session affinity is enabled.
|
||||
Setting this to sticky (default) will ensure that users that were served by canaries, will continue to be served by canaries.
|
||||
Setting this to legacy will restore original canary behavior, when session affinity was ignored.`,
|
||||
},
|
||||
annotationAffinityCookieName: {
|
||||
Validator: parser.ValidateRegex(*parser.BasicCharsRegex, true),
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskMedium,
|
||||
Documentation: `This annotation allows to specify the name of the cookie that will be used to route the requests`,
|
||||
},
|
||||
annotationAffinityCookieSecure: {
|
||||
Validator: parser.ValidateBool,
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation set the cookie as secure regardless the protocol of the incoming request`,
|
||||
},
|
||||
annotationAffinityCookieExpires: {
|
||||
Validator: parser.ValidateRegex(*affinityCookieExpiresRegex, true),
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskMedium,
|
||||
Documentation: `This annotation is a legacy version of "session-cookie-max-age" for compatibility with older browsers, generates an "Expires" cookie directive by adding the seconds to the current date`,
|
||||
},
|
||||
annotationAffinityCookieMaxAge: {
|
||||
Validator: parser.ValidateRegex(*affinityCookieExpiresRegex, false),
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskMedium,
|
||||
Documentation: `This annotation sets the time until the cookie expires`,
|
||||
},
|
||||
annotationAffinityCookiePath: {
|
||||
Validator: parser.ValidateRegex(*parser.URLIsValidRegex, true),
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskMedium,
|
||||
Documentation: `This annotation defines the Path that will be set on the cookie (required if your Ingress paths use regular expressions)`,
|
||||
},
|
||||
annotationAffinityCookieDomain: {
|
||||
Validator: parser.ValidateRegex(*parser.BasicCharsRegex, true),
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskMedium,
|
||||
Documentation: `This annotation defines the Domain attribute of the sticky cookie.`,
|
||||
},
|
||||
annotationAffinityCookieSameSite: {
|
||||
Validator: parser.ValidateOptions([]string{"None", "Lax", "Strict"}, false, true),
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation is used to apply a SameSite attribute to the sticky cookie.
|
||||
Browser accepted values are None, Lax, and Strict`,
|
||||
},
|
||||
annotationAffinityCookieConditionalSameSiteNone: {
|
||||
Validator: parser.ValidateBool,
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation is used to omit SameSite=None from browsers with SameSite attribute incompatibilities`,
|
||||
},
|
||||
annotationAffinityCookieChangeOnFailure: {
|
||||
Validator: parser.ValidateBool,
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation, when set to false will send request to upstream pointed by sticky cookie even if previous attempt failed.
|
||||
When set to true and previous attempt failed, sticky cookie will be changed to point to another upstream.`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var (
|
||||
affinityCookieExpiresRegex = regexp.MustCompile(`(^0|-?[1-9]\d*$)`)
|
||||
)
|
||||
|
@ -109,50 +193,50 @@ func (a affinity) cookieAffinityParse(ing *networking.Ingress) *Cookie {
|
|||
|
||||
cookie := &Cookie{}
|
||||
|
||||
cookie.Name, err = parser.GetStringAnnotation(annotationAffinityCookieName, ing)
|
||||
cookie.Name, err = parser.GetStringAnnotation(annotationAffinityCookieName, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
klog.V(3).InfoS("Invalid or no annotation value found. Ignoring", "ingress", klog.KObj(ing), "annotation", annotationAffinityCookieName, "default", defaultAffinityCookieName)
|
||||
cookie.Name = defaultAffinityCookieName
|
||||
}
|
||||
|
||||
cookie.Expires, err = parser.GetStringAnnotation(annotationAffinityCookieExpires, ing)
|
||||
cookie.Expires, err = parser.GetStringAnnotation(annotationAffinityCookieExpires, ing, a.annotationConfig.Annotations)
|
||||
if err != nil || !affinityCookieExpiresRegex.MatchString(cookie.Expires) {
|
||||
klog.V(3).InfoS("Invalid or no annotation value found. Ignoring", "ingress", klog.KObj(ing), "annotation", annotationAffinityCookieExpires)
|
||||
cookie.Expires = ""
|
||||
}
|
||||
|
||||
cookie.MaxAge, err = parser.GetStringAnnotation(annotationAffinityCookieMaxAge, ing)
|
||||
cookie.MaxAge, err = parser.GetStringAnnotation(annotationAffinityCookieMaxAge, ing, a.annotationConfig.Annotations)
|
||||
if err != nil || !affinityCookieExpiresRegex.MatchString(cookie.MaxAge) {
|
||||
klog.V(3).InfoS("Invalid or no annotation value found. Ignoring", "ingress", klog.KObj(ing), "annotation", annotationAffinityCookieMaxAge)
|
||||
cookie.MaxAge = ""
|
||||
}
|
||||
|
||||
cookie.Path, err = parser.GetStringAnnotation(annotationAffinityCookiePath, ing)
|
||||
cookie.Path, err = parser.GetStringAnnotation(annotationAffinityCookiePath, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
klog.V(3).InfoS("Invalid or no annotation value found. Ignoring", "ingress", klog.KObj(ing), "annotation", annotationAffinityCookiePath)
|
||||
}
|
||||
|
||||
cookie.Domain, err = parser.GetStringAnnotation(annotationAffinityCookieDomain, ing)
|
||||
cookie.Domain, err = parser.GetStringAnnotation(annotationAffinityCookieDomain, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
klog.V(3).InfoS("Invalid or no annotation value found. Ignoring", "ingress", klog.KObj(ing), "annotation", annotationAffinityCookieDomain)
|
||||
}
|
||||
|
||||
cookie.SameSite, err = parser.GetStringAnnotation(annotationAffinityCookieSameSite, ing)
|
||||
cookie.SameSite, err = parser.GetStringAnnotation(annotationAffinityCookieSameSite, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
klog.V(3).InfoS("Invalid or no annotation value found. Ignoring", "ingress", klog.KObj(ing), "annotation", annotationAffinityCookieSameSite)
|
||||
}
|
||||
|
||||
cookie.Secure, err = parser.GetBoolAnnotation(annotationAffinityCookieSecure, ing)
|
||||
cookie.Secure, err = parser.GetBoolAnnotation(annotationAffinityCookieSecure, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
klog.V(3).InfoS("Invalid or no annotation value found. Ignoring", "ingress", klog.KObj(ing), "annotation", annotationAffinityCookieSecure)
|
||||
}
|
||||
|
||||
cookie.ConditionalSameSiteNone, err = parser.GetBoolAnnotation(annotationAffinityCookieConditionalSameSiteNone, ing)
|
||||
cookie.ConditionalSameSiteNone, err = parser.GetBoolAnnotation(annotationAffinityCookieConditionalSameSiteNone, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
klog.V(3).InfoS("Invalid or no annotation value found. Ignoring", "ingress", klog.KObj(ing), "annotation", annotationAffinityCookieConditionalSameSiteNone)
|
||||
}
|
||||
|
||||
cookie.ChangeOnFailure, err = parser.GetBoolAnnotation(annotationAffinityCookieChangeOnFailure, ing)
|
||||
cookie.ChangeOnFailure, err = parser.GetBoolAnnotation(annotationAffinityCookieChangeOnFailure, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
klog.V(3).InfoS("Invalid or no annotation value found. Ignoring", "ingress", klog.KObj(ing), "annotation", annotationAffinityCookieChangeOnFailure)
|
||||
}
|
||||
|
@ -162,11 +246,15 @@ func (a affinity) cookieAffinityParse(ing *networking.Ingress) *Cookie {
|
|||
|
||||
// NewParser creates a new Affinity annotation parser
|
||||
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
|
||||
return affinity{r}
|
||||
return affinity{
|
||||
r: r,
|
||||
annotationConfig: sessionAffinityAnnotations,
|
||||
}
|
||||
}
|
||||
|
||||
type affinity struct {
|
||||
r resolver.Resolver
|
||||
r resolver.Resolver
|
||||
annotationConfig parser.Annotation
|
||||
}
|
||||
|
||||
// ParseAnnotations parses the annotations contained in the ingress
|
||||
|
@ -174,18 +262,18 @@ type affinity struct {
|
|||
func (a affinity) Parse(ing *networking.Ingress) (interface{}, error) {
|
||||
cookie := &Cookie{}
|
||||
// Check the type of affinity that will be used
|
||||
at, err := parser.GetStringAnnotation(annotationAffinityType, ing)
|
||||
at, err := parser.GetStringAnnotation(annotationAffinityType, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
at = ""
|
||||
}
|
||||
|
||||
// Check the affinity mode that will be used
|
||||
am, err := parser.GetStringAnnotation(annotationAffinityMode, ing)
|
||||
am, err := parser.GetStringAnnotation(annotationAffinityMode, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
am = ""
|
||||
}
|
||||
|
||||
cb, err := parser.GetStringAnnotation(annotationAffinityCanaryBehavior, ing)
|
||||
cb, err := parser.GetStringAnnotation(annotationAffinityCanaryBehavior, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
cb = ""
|
||||
}
|
||||
|
@ -205,3 +293,7 @@ func (a affinity) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
Cookie: *cookie,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a affinity) GetDocumentation() parser.AnnotationFields {
|
||||
return a.annotationConfig.Annotations
|
||||
}
|
||||
|
|
|
@ -23,18 +23,42 @@ import (
|
|||
"k8s.io/ingress-nginx/internal/ingress/resolver"
|
||||
)
|
||||
|
||||
const (
|
||||
configurationSnippetAnnotation = "configuration-snippet"
|
||||
)
|
||||
|
||||
var configurationSnippetAnnotations = parser.Annotation{
|
||||
Group: "snippets",
|
||||
Annotations: parser.AnnotationFields{
|
||||
configurationSnippetAnnotation: {
|
||||
Validator: parser.ValidateNull,
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskCritical, // Critical, this annotation is not validated at all and allows arbitrary configutations
|
||||
Documentation: `This annotation allows setting a custom NGINX configuration on a location block. This annotation does not contain any validation and it's usage is not recommended!`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
type snippet struct {
|
||||
r resolver.Resolver
|
||||
r resolver.Resolver
|
||||
annotationConfig parser.Annotation
|
||||
}
|
||||
|
||||
// NewParser creates a new CORS annotation parser
|
||||
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
|
||||
return snippet{r}
|
||||
return snippet{
|
||||
r: r,
|
||||
annotationConfig: configurationSnippetAnnotations,
|
||||
}
|
||||
}
|
||||
|
||||
// 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 *networking.Ingress) (interface{}, error) {
|
||||
return parser.GetStringAnnotation("configuration-snippet", ing)
|
||||
return parser.GetStringAnnotation(configurationSnippetAnnotation, ing, a.annotationConfig.Annotations)
|
||||
}
|
||||
|
||||
func (a snippet) GetDocumentation() parser.AnnotationFields {
|
||||
return a.annotationConfig.Annotations
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@ import (
|
|||
)
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
annotation := parser.GetAnnotationWithPrefix("configuration-snippet")
|
||||
annotation := parser.GetAnnotationWithPrefix(configurationSnippetAnnotation)
|
||||
|
||||
ap := NewParser(&resolver.Mock{})
|
||||
if ap == nil {
|
||||
|
|
|
@ -17,14 +17,47 @@ limitations under the License.
|
|||
package sslcipher
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
networking "k8s.io/api/networking/v1"
|
||||
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/parser"
|
||||
"k8s.io/ingress-nginx/internal/ingress/errors"
|
||||
"k8s.io/ingress-nginx/internal/ingress/resolver"
|
||||
)
|
||||
|
||||
const (
|
||||
sslPreferServerCipherAnnotation = "ssl-prefer-server-ciphers"
|
||||
sslCipherAnnotation = "ssl-ciphers"
|
||||
)
|
||||
|
||||
var (
|
||||
// Should cover something like "ALL:!aNULL:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP"
|
||||
regexValidSSLCipher = regexp.MustCompile(`^[A-Za-z0-9!:+\-]*$`)
|
||||
)
|
||||
|
||||
var sslCipherAnnotations = parser.Annotation{
|
||||
Group: "backend",
|
||||
Annotations: parser.AnnotationFields{
|
||||
sslPreferServerCipherAnnotation: {
|
||||
Validator: parser.ValidateBool,
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `The following annotation will set the ssl_prefer_server_ciphers directive at the server level.
|
||||
This configuration specifies that server ciphers should be preferred over client ciphers when using the SSLv3 and TLS protocols.`,
|
||||
},
|
||||
sslCipherAnnotation: {
|
||||
Validator: parser.ValidateRegex(*regexValidSSLCipher, true),
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `Using this annotation will set the ssl_ciphers directive at the server level. This configuration is active for all the paths in the host.`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
type sslCipher struct {
|
||||
r resolver.Resolver
|
||||
r resolver.Resolver
|
||||
annotationConfig parser.Annotation
|
||||
}
|
||||
|
||||
// Config contains the ssl-ciphers & ssl-prefer-server-ciphers configuration
|
||||
|
@ -35,7 +68,10 @@ type Config struct {
|
|||
|
||||
// NewParser creates a new sslCipher annotation parser
|
||||
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
|
||||
return sslCipher{r}
|
||||
return sslCipher{
|
||||
r: r,
|
||||
annotationConfig: sslCipherAnnotations,
|
||||
}
|
||||
}
|
||||
|
||||
// Parse parses the annotations contained in the ingress rule
|
||||
|
@ -45,7 +81,7 @@ func (sc sslCipher) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
var err error
|
||||
var sslPreferServerCiphers bool
|
||||
|
||||
sslPreferServerCiphers, err = parser.GetBoolAnnotation("ssl-prefer-server-ciphers", ing)
|
||||
sslPreferServerCiphers, err = parser.GetBoolAnnotation(sslPreferServerCipherAnnotation, ing, sc.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
config.SSLPreferServerCiphers = ""
|
||||
} else {
|
||||
|
@ -56,7 +92,14 @@ func (sc sslCipher) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
}
|
||||
}
|
||||
|
||||
config.SSLCiphers, _ = parser.GetStringAnnotation("ssl-ciphers", ing)
|
||||
config.SSLCiphers, err = parser.GetStringAnnotation(sslCipherAnnotation, ing, sc.annotationConfig.Annotations)
|
||||
if err != nil && !errors.IsInvalidContent(err) && !errors.IsMissingAnnotations(err) {
|
||||
return config, err
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func (sc sslCipher) GetDocumentation() parser.AnnotationFields {
|
||||
return sc.annotationConfig.Annotations
|
||||
}
|
||||
|
|
|
@ -33,22 +33,24 @@ func TestParse(t *testing.T) {
|
|||
t.Fatalf("expected a parser.IngressAnnotation but returned nil")
|
||||
}
|
||||
|
||||
annotationSSLCiphers := parser.GetAnnotationWithPrefix("ssl-ciphers")
|
||||
annotationSSLPreferServerCiphers := parser.GetAnnotationWithPrefix("ssl-prefer-server-ciphers")
|
||||
annotationSSLCiphers := parser.GetAnnotationWithPrefix(sslCipherAnnotation)
|
||||
annotationSSLPreferServerCiphers := parser.GetAnnotationWithPrefix(sslPreferServerCipherAnnotation)
|
||||
|
||||
testCases := []struct {
|
||||
annotations map[string]string
|
||||
expected Config
|
||||
expectErr bool
|
||||
}{
|
||||
{map[string]string{annotationSSLCiphers: "ALL:!aNULL:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP"}, Config{"ALL:!aNULL:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP", ""}},
|
||||
{map[string]string{annotationSSLCiphers: "ALL:!aNULL:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP"}, Config{"ALL:!aNULL:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP", ""}, false},
|
||||
{map[string]string{annotationSSLCiphers: "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256"},
|
||||
Config{"ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256", ""}},
|
||||
{map[string]string{annotationSSLCiphers: ""}, Config{"", ""}},
|
||||
{map[string]string{annotationSSLPreferServerCiphers: "true"}, Config{"", "on"}},
|
||||
{map[string]string{annotationSSLPreferServerCiphers: "false"}, Config{"", "off"}},
|
||||
{map[string]string{annotationSSLCiphers: "ALL:!aNULL:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP", annotationSSLPreferServerCiphers: "true"}, Config{"ALL:!aNULL:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP", "on"}},
|
||||
{map[string]string{}, Config{"", ""}},
|
||||
{nil, Config{"", ""}},
|
||||
Config{"ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256", ""}, false},
|
||||
{map[string]string{annotationSSLCiphers: ""}, Config{"", ""}, false},
|
||||
{map[string]string{annotationSSLPreferServerCiphers: "true"}, Config{"", "on"}, false},
|
||||
{map[string]string{annotationSSLPreferServerCiphers: "false"}, Config{"", "off"}, false},
|
||||
{map[string]string{annotationSSLCiphers: "ALL:!aNULL:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP", annotationSSLPreferServerCiphers: "true"}, Config{"ALL:!aNULL:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP", "on"}, false},
|
||||
{map[string]string{annotationSSLCiphers: "ALL:SOMETHING:;locationXPTO"}, Config{"", ""}, true},
|
||||
{map[string]string{}, Config{"", ""}, false},
|
||||
{nil, Config{"", ""}, false},
|
||||
}
|
||||
|
||||
ing := &networking.Ingress{
|
||||
|
@ -61,7 +63,10 @@ func TestParse(t *testing.T) {
|
|||
|
||||
for _, testCase := range testCases {
|
||||
ing.SetAnnotations(testCase.annotations)
|
||||
result, _ := ap.Parse(ing)
|
||||
result, err := ap.Parse(ing)
|
||||
if (err != nil) != testCase.expectErr {
|
||||
t.Fatalf("expected error: %t got error: %t err value: %s. %+v", testCase.expectErr, err != nil, err, testCase.annotations)
|
||||
}
|
||||
if !reflect.DeepEqual(result, &testCase.expected) {
|
||||
t.Errorf("expected %v but returned %v, annotations: %s", testCase.expected, result, testCase.annotations)
|
||||
}
|
||||
|
|
|
@ -24,13 +24,32 @@ import (
|
|||
"k8s.io/ingress-nginx/internal/ingress/resolver"
|
||||
)
|
||||
|
||||
const (
|
||||
sslPassthroughAnnotation = "ssl-passthrough"
|
||||
)
|
||||
|
||||
var sslPassthroughAnnotations = parser.Annotation{
|
||||
Group: "", // TBD
|
||||
Annotations: parser.AnnotationFields{
|
||||
sslPassthroughAnnotation: {
|
||||
Validator: parser.ValidateBool,
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskLow, // Low, as it allows regexes but on a very limited set
|
||||
Documentation: `This annotation instructs the controller to send TLS connections directly to the backend instead of letting NGINX decrypt the communication.`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
type sslpt struct {
|
||||
r resolver.Resolver
|
||||
r resolver.Resolver
|
||||
annotationConfig parser.Annotation
|
||||
}
|
||||
|
||||
// NewParser creates a new SSL passthrough annotation parser
|
||||
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
|
||||
return sslpt{r}
|
||||
return sslpt{r: r,
|
||||
annotationConfig: sslPassthroughAnnotations,
|
||||
}
|
||||
}
|
||||
|
||||
// ParseAnnotations parses the annotations contained in the ingress
|
||||
|
@ -40,5 +59,9 @@ func (a sslpt) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
return false, ing_errors.ErrMissingAnnotations
|
||||
}
|
||||
|
||||
return parser.GetBoolAnnotation("ssl-passthrough", ing)
|
||||
return parser.GetBoolAnnotation(sslPassthroughAnnotation, ing, a.annotationConfig.Annotations)
|
||||
}
|
||||
|
||||
func (a sslpt) GetDocumentation() parser.AnnotationFields {
|
||||
return a.annotationConfig.Annotations
|
||||
}
|
||||
|
|
|
@ -54,7 +54,7 @@ func TestParseAnnotations(t *testing.T) {
|
|||
}
|
||||
|
||||
data := map[string]string{}
|
||||
data[parser.GetAnnotationWithPrefix("ssl-passthrough")] = "true"
|
||||
data[parser.GetAnnotationWithPrefix(sslPassthroughAnnotation)] = "true"
|
||||
ing.SetAnnotations(data)
|
||||
// test ingress using the annotation without a TLS section
|
||||
_, err = NewParser(&resolver.Mock{}).Parse(ing)
|
||||
|
|
|
@ -23,18 +23,42 @@ import (
|
|||
"k8s.io/ingress-nginx/internal/ingress/resolver"
|
||||
)
|
||||
|
||||
const (
|
||||
streamSnippetAnnotation = "stream-snippet"
|
||||
)
|
||||
|
||||
var streamSnippetAnnotations = parser.Annotation{
|
||||
Group: "snippets",
|
||||
Annotations: parser.AnnotationFields{
|
||||
streamSnippetAnnotation: {
|
||||
Validator: parser.ValidateNull,
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskCritical, // Critical, this annotation is not validated at all and allows arbitrary configutations
|
||||
Documentation: `This annotation allows setting a custom NGINX configuration on a stream block. This annotation does not contain any validation and it's usage is not recommended!`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
type streamSnippet struct {
|
||||
r resolver.Resolver
|
||||
r resolver.Resolver
|
||||
annotationConfig parser.Annotation
|
||||
}
|
||||
|
||||
// NewParser creates a new server snippet annotation parser
|
||||
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
|
||||
return streamSnippet{r}
|
||||
return streamSnippet{
|
||||
r: r,
|
||||
annotationConfig: streamSnippetAnnotations,
|
||||
}
|
||||
}
|
||||
|
||||
// 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 streamSnippet) Parse(ing *networking.Ingress) (interface{}, error) {
|
||||
return parser.GetStringAnnotation("stream-snippet", ing)
|
||||
return parser.GetStringAnnotation("stream-snippet", ing, a.annotationConfig.Annotations)
|
||||
}
|
||||
|
||||
func (a streamSnippet) GetDocumentation() parser.AnnotationFields {
|
||||
return a.annotationConfig.Annotations
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@ import (
|
|||
)
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
annotation := parser.GetAnnotationWithPrefix("stream-snippet")
|
||||
annotation := parser.GetAnnotationWithPrefix(streamSnippetAnnotation)
|
||||
|
||||
ap := NewParser(&resolver.Mock{})
|
||||
if ap == nil {
|
||||
|
|
|
@ -17,14 +17,54 @@ limitations under the License.
|
|||
package upstreamhashby
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
networking "k8s.io/api/networking/v1"
|
||||
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/parser"
|
||||
"k8s.io/ingress-nginx/internal/ingress/errors"
|
||||
"k8s.io/ingress-nginx/internal/ingress/resolver"
|
||||
)
|
||||
|
||||
const (
|
||||
upstreamHashByAnnotation = "upstream-hash-by"
|
||||
upstreamHashBySubsetAnnotation = "upstream-hash-by-subset"
|
||||
upstreamHashBySubsetSize = "upstream-hash-by-subset-size"
|
||||
)
|
||||
|
||||
var (
|
||||
specialChars = regexp.QuoteMeta("_${}")
|
||||
hashByRegex = regexp.MustCompilePOSIX(`^[A-Za-z0-9\-` + specialChars + `]*$`)
|
||||
)
|
||||
|
||||
var upstreamHashByAnnotations = parser.Annotation{
|
||||
Group: "backend",
|
||||
Annotations: parser.AnnotationFields{
|
||||
upstreamHashByAnnotation: {
|
||||
Validator: parser.ValidateRegex(*hashByRegex, true),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskHigh, // High, this annotation allows accessing NGINX variables
|
||||
Documentation: `This annotation defines the nginx variable, text value or any combination thereof to use for consistent hashing.
|
||||
For example: nginx.ingress.kubernetes.io/upstream-hash-by: "$request_uri" or nginx.ingress.kubernetes.io/upstream-hash-by: "$request_uri$host" or nginx.ingress.kubernetes.io/upstream-hash-by: "${request_uri}-text-value" to consistently hash upstream requests by the current request URI.`,
|
||||
},
|
||||
upstreamHashBySubsetAnnotation: {
|
||||
Validator: parser.ValidateBool,
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation maps requests to subset of nodes instead of a single one.`,
|
||||
},
|
||||
upstreamHashBySubsetSize: {
|
||||
Validator: parser.ValidateInt,
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation determines the size of each subset (default 3)`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
type upstreamhashby struct {
|
||||
r resolver.Resolver
|
||||
r resolver.Resolver
|
||||
annotationConfig parser.Annotation
|
||||
}
|
||||
|
||||
// Config contains the Consistent hash configuration to be used in the Ingress
|
||||
|
@ -36,14 +76,26 @@ type Config struct {
|
|||
|
||||
// NewParser creates a new UpstreamHashBy annotation parser
|
||||
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
|
||||
return upstreamhashby{r}
|
||||
return upstreamhashby{
|
||||
r: r,
|
||||
annotationConfig: upstreamHashByAnnotations,
|
||||
}
|
||||
}
|
||||
|
||||
// Parse parses the annotations contained in the ingress rule
|
||||
func (a upstreamhashby) Parse(ing *networking.Ingress) (interface{}, error) {
|
||||
upstreamHashBy, _ := parser.GetStringAnnotation("upstream-hash-by", ing)
|
||||
upstreamHashBySubset, _ := parser.GetBoolAnnotation("upstream-hash-by-subset", ing)
|
||||
upstreamHashbySubsetSize, _ := parser.GetIntAnnotation("upstream-hash-by-subset-size", ing)
|
||||
upstreamHashBy, err := parser.GetStringAnnotation(upstreamHashByAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil && !errors.IsMissingAnnotations(err) {
|
||||
return nil, err
|
||||
}
|
||||
upstreamHashBySubset, err := parser.GetBoolAnnotation(upstreamHashBySubsetAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil && !errors.IsMissingAnnotations(err) {
|
||||
return nil, err
|
||||
}
|
||||
upstreamHashbySubsetSize, err := parser.GetIntAnnotation(upstreamHashBySubsetSize, ing, a.annotationConfig.Annotations)
|
||||
if err != nil && !errors.IsMissingAnnotations(err) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if upstreamHashbySubsetSize == 0 {
|
||||
upstreamHashbySubsetSize = 3
|
||||
|
@ -51,3 +103,7 @@ func (a upstreamhashby) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
|
||||
return &Config{upstreamHashBy, upstreamHashBySubset, upstreamHashbySubsetSize}, nil
|
||||
}
|
||||
|
||||
func (a upstreamhashby) GetDocumentation() parser.AnnotationFields {
|
||||
return a.annotationConfig.Annotations
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@ import (
|
|||
)
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
annotation := parser.GetAnnotationWithPrefix("upstream-hash-by")
|
||||
annotation := parser.GetAnnotationWithPrefix(upstreamHashByAnnotation)
|
||||
|
||||
ap := NewParser(&resolver.Mock{})
|
||||
if ap == nil {
|
||||
|
@ -37,12 +37,15 @@ func TestParse(t *testing.T) {
|
|||
testCases := []struct {
|
||||
annotations map[string]string
|
||||
expected string
|
||||
expectErr bool
|
||||
}{
|
||||
{map[string]string{annotation: "$request_uri"}, "$request_uri"},
|
||||
{map[string]string{annotation: "$request_uri$scheme"}, "$request_uri$scheme"},
|
||||
{map[string]string{annotation: "false"}, "false"},
|
||||
{map[string]string{}, ""},
|
||||
{nil, ""},
|
||||
{map[string]string{annotation: "$request_URI"}, "$request_URI", false},
|
||||
{map[string]string{annotation: "$request_uri$scheme"}, "$request_uri$scheme", false},
|
||||
{map[string]string{annotation: "xpto;[]"}, "", true},
|
||||
{map[string]string{annotation: "lalal${scheme_test}"}, "lalal${scheme_test}", false},
|
||||
{map[string]string{annotation: "false"}, "false", false},
|
||||
{map[string]string{}, "", false},
|
||||
{nil, "", false},
|
||||
}
|
||||
|
||||
ing := &networking.Ingress{
|
||||
|
@ -55,14 +58,19 @@ func TestParse(t *testing.T) {
|
|||
|
||||
for _, testCase := range testCases {
|
||||
ing.SetAnnotations(testCase.annotations)
|
||||
result, _ := ap.Parse(ing)
|
||||
uc, ok := result.(*Config)
|
||||
if !ok {
|
||||
t.Fatalf("expected a Config type")
|
||||
result, err := ap.Parse(ing)
|
||||
if (err != nil) != testCase.expectErr {
|
||||
t.Fatalf("expected error: %t got error: %t err value: %s. %+v", testCase.expectErr, err != nil, err, testCase.annotations)
|
||||
}
|
||||
if !testCase.expectErr {
|
||||
uc, ok := result.(*Config)
|
||||
if !ok {
|
||||
t.Fatalf("expected a Config type")
|
||||
}
|
||||
|
||||
if uc.UpstreamHashBy != testCase.expected {
|
||||
t.Errorf("expected %v but returned %v, annotations: %s", testCase.expected, result, testCase.annotations)
|
||||
if uc.UpstreamHashBy != testCase.expected {
|
||||
t.Errorf("expected %v but returned %v, annotations: %s", testCase.expected, result, testCase.annotations)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,18 +23,43 @@ import (
|
|||
"k8s.io/ingress-nginx/internal/ingress/resolver"
|
||||
)
|
||||
|
||||
const (
|
||||
upstreamVhostAnnotation = "upstream-vhost"
|
||||
)
|
||||
|
||||
var upstreamVhostAnnotations = parser.Annotation{
|
||||
Group: "backend",
|
||||
Annotations: parser.AnnotationFields{
|
||||
upstreamVhostAnnotation: {
|
||||
Validator: parser.ValidateServerName,
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow, // Low, as it allows regexes but on a very limited set
|
||||
Documentation: `This configuration setting allows you to control the value for host in the following statement: proxy_set_header Host $host, which forms part of the location block.
|
||||
This is useful if you need to call the upstream server by something other than $host`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
type upstreamVhost struct {
|
||||
r resolver.Resolver
|
||||
r resolver.Resolver
|
||||
annotationConfig parser.Annotation
|
||||
}
|
||||
|
||||
// NewParser creates a new upstream VHost annotation parser
|
||||
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
|
||||
return upstreamVhost{r}
|
||||
return upstreamVhost{
|
||||
r: r,
|
||||
annotationConfig: upstreamVhostAnnotations,
|
||||
}
|
||||
}
|
||||
|
||||
// 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 *networking.Ingress) (interface{}, error) {
|
||||
return parser.GetStringAnnotation("upstream-vhost", ing)
|
||||
return parser.GetStringAnnotation(upstreamVhostAnnotation, ing, a.annotationConfig.Annotations)
|
||||
}
|
||||
|
||||
func (a upstreamVhost) GetDocumentation() parser.AnnotationFields {
|
||||
return a.annotationConfig.Annotations
|
||||
}
|
||||
|
|
|
@ -36,7 +36,7 @@ func TestParse(t *testing.T) {
|
|||
}
|
||||
|
||||
data := map[string]string{}
|
||||
data[parser.GetAnnotationWithPrefix("upstream-vhost")] = "ok.com"
|
||||
data[parser.GetAnnotationWithPrefix(upstreamVhostAnnotation)] = "ok.com"
|
||||
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
|
|
|
@ -23,17 +23,41 @@ import (
|
|||
"k8s.io/ingress-nginx/internal/ingress/resolver"
|
||||
)
|
||||
|
||||
const (
|
||||
xForwardedForPrefixAnnotation = "x-forwarded-prefix"
|
||||
)
|
||||
|
||||
var xForwardedForAnnotations = parser.Annotation{
|
||||
Group: "backend",
|
||||
Annotations: parser.AnnotationFields{
|
||||
xForwardedForPrefixAnnotation: {
|
||||
Validator: parser.ValidateRegex(*parser.BasicCharsRegex, true),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow, // Low, as it allows regexes but on a very limited set
|
||||
Documentation: `This annotation can be used to add the non-standard X-Forwarded-Prefix header to the upstream request with a string value`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
type xforwardedprefix struct {
|
||||
r resolver.Resolver
|
||||
r resolver.Resolver
|
||||
annotationConfig parser.Annotation
|
||||
}
|
||||
|
||||
// NewParser creates a new xforwardedprefix annotation parser
|
||||
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
|
||||
return xforwardedprefix{r}
|
||||
return xforwardedprefix{
|
||||
r: r,
|
||||
annotationConfig: xForwardedForAnnotations,
|
||||
}
|
||||
}
|
||||
|
||||
// Parse parses the annotations contained in the ingress rule
|
||||
// used to add an x-forwarded-prefix header to the request
|
||||
func (cbbs xforwardedprefix) Parse(ing *networking.Ingress) (interface{}, error) {
|
||||
return parser.GetStringAnnotation("x-forwarded-prefix", ing)
|
||||
return parser.GetStringAnnotation(xForwardedForPrefixAnnotation, ing, cbbs.annotationConfig.Annotations)
|
||||
}
|
||||
|
||||
func (cbbs xforwardedprefix) GetDocumentation() parser.AnnotationFields {
|
||||
return cbbs.annotationConfig.Annotations
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@ import (
|
|||
)
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
annotation := parser.GetAnnotationWithPrefix("x-forwarded-prefix")
|
||||
annotation := parser.GetAnnotationWithPrefix(xForwardedForPrefixAnnotation)
|
||||
ap := NewParser(&resolver.Mock{})
|
||||
if ap == nil {
|
||||
t.Fatalf("expected a parser.IngressAnnotation but returned nil")
|
||||
|
|
|
@ -389,14 +389,19 @@ func (n *NGINXController) CheckIngress(ing *networking.Ingress) error {
|
|||
toCheck.ObjectMeta.Name == ing.ObjectMeta.Name
|
||||
}
|
||||
ings := store.FilterIngresses(allIngresses, filter)
|
||||
parsed, err := annotations.NewAnnotationExtractor(n.store).Extract(ing)
|
||||
if err != nil {
|
||||
n.metricCollector.IncCheckErrorCount(ing.ObjectMeta.Namespace, ing.Name)
|
||||
return err
|
||||
}
|
||||
ings = append(ings, &ingress.Ingress{
|
||||
Ingress: *ing,
|
||||
ParsedAnnotations: annotations.NewAnnotationExtractor(n.store).Extract(ing),
|
||||
ParsedAnnotations: parsed,
|
||||
})
|
||||
startTest := time.Now().UnixNano() / 1000000
|
||||
_, servers, pcfg := n.getConfiguration(ings)
|
||||
|
||||
err := checkOverlap(ing, allIngresses, servers)
|
||||
err = checkOverlap(ing, allIngresses, servers)
|
||||
if err != nil {
|
||||
n.metricCollector.IncCheckErrorCount(ing.ObjectMeta.Namespace, ing.Name)
|
||||
return err
|
||||
|
@ -1509,7 +1514,7 @@ func locationApplyAnnotations(loc *ingress.Location, anns *annotations.Ingress)
|
|||
loc.Rewrite = anns.Rewrite
|
||||
loc.UpstreamVhost = anns.UpstreamVhost
|
||||
loc.Denylist = anns.Denylist
|
||||
loc.Whitelist = anns.Whitelist
|
||||
loc.Allowlist = anns.Allowlist
|
||||
loc.Denied = anns.Denied
|
||||
loc.XForwardedPrefix = anns.XForwardedPrefix
|
||||
loc.UsePortInRedirects = anns.UsePortInRedirects
|
||||
|
@ -1808,9 +1813,9 @@ func checkOverlap(ing *networking.Ingress, ingresses []*ingress.Ingress, servers
|
|||
}
|
||||
|
||||
// path overlap. Check if one of the ingresses has a canary annotation
|
||||
isCanaryEnabled, annotationErr := parser.GetBoolAnnotation("canary", ing)
|
||||
isCanaryEnabled, annotationErr := parser.GetBoolAnnotation("canary", ing, canary.CanaryAnnotations.Annotations)
|
||||
for _, existing := range existingIngresses {
|
||||
isExistingCanaryEnabled, existingAnnotationErr := parser.GetBoolAnnotation("canary", existing)
|
||||
isExistingCanaryEnabled, existingAnnotationErr := parser.GetBoolAnnotation("canary", existing, canary.CanaryAnnotations.Annotations)
|
||||
|
||||
if isCanaryEnabled && isExistingCanaryEnabled {
|
||||
return fmt.Errorf(`host "%s" and path "%s" is already defined in ingress %s/%s`, rule.Host, path.Path, existing.Namespace, existing.Name)
|
||||
|
|
|
@ -44,7 +44,7 @@ import (
|
|||
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations"
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/canary"
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/ipwhitelist"
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/ipallowlist"
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/parser"
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/proxyssl"
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/sessionaffinity"
|
||||
|
@ -2418,7 +2418,7 @@ func TestGetBackendServers(t *testing.T) {
|
|||
},
|
||||
},
|
||||
ParsedAnnotations: &annotations.Ingress{
|
||||
Whitelist: ipwhitelist.SourceRange{CIDR: []string{"10.0.0.0/24"}},
|
||||
Allowlist: ipallowlist.SourceRange{CIDR: []string{"10.0.0.0/24"}},
|
||||
ServerSnippet: "bla",
|
||||
ConfigurationSnippet: "blo",
|
||||
},
|
||||
|
@ -2439,7 +2439,7 @@ func TestGetBackendServers(t *testing.T) {
|
|||
t.Errorf("config snippet should be empty, got '%s'", s.Locations[0].ConfigurationSnippet)
|
||||
}
|
||||
|
||||
if len(s.Locations[0].Whitelist.CIDR) != 1 || s.Locations[0].Whitelist.CIDR[0] != "10.0.0.0/24" {
|
||||
if len(s.Locations[0].Allowlist.CIDR) != 1 || s.Locations[0].Allowlist.CIDR[0] != "10.0.0.0/24" {
|
||||
t.Errorf("allow list was incorrectly dropped, len should be 1 and contain 10.0.0.0/24")
|
||||
}
|
||||
|
||||
|
|
|
@ -882,9 +882,14 @@ func (s *k8sStore) syncIngress(ing *networkingv1.Ingress) {
|
|||
|
||||
k8s.SetDefaultNGINXPathType(copyIng)
|
||||
|
||||
err := s.listers.IngressWithAnnotation.Update(&ingress.Ingress{
|
||||
parsed, err := s.annotations.Extract(ing)
|
||||
if err != nil {
|
||||
klog.Error(err)
|
||||
return
|
||||
}
|
||||
err = s.listers.IngressWithAnnotation.Update(&ingress.Ingress{
|
||||
Ingress: *copyIng,
|
||||
ParsedAnnotations: s.annotations.Extract(ing),
|
||||
ParsedAnnotations: parsed,
|
||||
})
|
||||
if err != nil {
|
||||
klog.Error(err)
|
||||
|
@ -920,6 +925,7 @@ func (s *k8sStore) updateSecretIngressMap(ing *networkingv1.Ingress) {
|
|||
"proxy-ssl-secret",
|
||||
"secure-verify-ca-secret",
|
||||
}
|
||||
|
||||
for _, ann := range secretAnnotations {
|
||||
secrKey, err := objectRefAnnotationNsKey(ann, ing)
|
||||
if err != nil && !errors.IsMissingAnnotations(err) {
|
||||
|
@ -938,7 +944,8 @@ func (s *k8sStore) updateSecretIngressMap(ing *networkingv1.Ingress) {
|
|||
// objectRefAnnotationNsKey returns an object reference formatted as a
|
||||
// 'namespace/name' key from the given annotation name.
|
||||
func objectRefAnnotationNsKey(ann string, ing *networkingv1.Ingress) (string, error) {
|
||||
annValue, err := parser.GetStringAnnotation(ann, ing)
|
||||
// We pass nil fields, as this is an internal process and we don't need to validate it.
|
||||
annValue, err := parser.GetStringAnnotation(ann, ing, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
@ -951,6 +958,9 @@ func objectRefAnnotationNsKey(ann string, ing *networkingv1.Ingress) (string, er
|
|||
if secrNs == "" {
|
||||
return fmt.Sprintf("%v/%v", ing.Namespace, secrName), nil
|
||||
}
|
||||
if secrNs != ing.Namespace {
|
||||
return "", fmt.Errorf("cross namespace secret is not supported")
|
||||
}
|
||||
return annValue, nil
|
||||
}
|
||||
|
||||
|
|
|
@ -1414,18 +1414,31 @@ func TestUpdateSecretIngressMap(t *testing.T) {
|
|||
t.Run("with annotation in namespace/name format", func(t *testing.T) {
|
||||
ing := ingTpl.DeepCopy()
|
||||
ing.ObjectMeta.SetAnnotations(map[string]string{
|
||||
parser.GetAnnotationWithPrefix("auth-secret"): "otherns/auth",
|
||||
parser.GetAnnotationWithPrefix("auth-secret"): "testns/auth",
|
||||
})
|
||||
if err := s.listers.Ingress.Update(ing); err != nil {
|
||||
t.Errorf("error updating the Ingress: %v", err)
|
||||
}
|
||||
s.updateSecretIngressMap(ing)
|
||||
|
||||
if l := s.secretIngressMap.Len(); !(l == 1 && s.secretIngressMap.Has("otherns/auth")) {
|
||||
if l := s.secretIngressMap.Len(); !(l == 1 && s.secretIngressMap.Has("testns/auth")) {
|
||||
t.Errorf("Expected \"otherns/auth\" to be the only referenced Secret (got %d)", l)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("with annotation in namespace/name format should not be supported", func(t *testing.T) {
|
||||
ing := ingTpl.DeepCopy()
|
||||
ing.ObjectMeta.SetAnnotations(map[string]string{
|
||||
parser.GetAnnotationWithPrefix("auth-secret"): "anotherns/auth",
|
||||
})
|
||||
s.listers.Ingress.Update(ing)
|
||||
s.updateSecretIngressMap(ing)
|
||||
|
||||
if l := s.secretIngressMap.Len(); l != 0 {
|
||||
t.Errorf("Expected \"otherns/auth\" to be denied as it contains a different namespace (got %d)", l)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("with annotation in invalid format", func(t *testing.T) {
|
||||
ing := ingTpl.DeepCopy()
|
||||
ing.ObjectMeta.SetAnnotations(map[string]string{
|
||||
|
|
|
@ -110,3 +110,25 @@ func New(m string) error {
|
|||
func Errorf(format string, args ...interface{}) error {
|
||||
return fmt.Errorf(format, args...)
|
||||
}
|
||||
|
||||
type ValidationError struct {
|
||||
Reason error
|
||||
}
|
||||
|
||||
func (e ValidationError) Error() string {
|
||||
return e.Reason.Error()
|
||||
}
|
||||
|
||||
// NewValidationError returns a new LocationDenied error
|
||||
func NewValidationError(annotation string) error {
|
||||
return ValidationError{
|
||||
Reason: fmt.Errorf("annotation %s contains invalid value", annotation),
|
||||
}
|
||||
}
|
||||
|
||||
// IsValidationError checks if the err is an error which
|
||||
// indicates that some annotation value is invalid
|
||||
func IsValidationError(e error) bool {
|
||||
_, ok := e.(ValidationError)
|
||||
return ok
|
||||
}
|
||||
|
|
|
@ -29,8 +29,8 @@ import (
|
|||
"k8s.io/ingress-nginx/internal/ingress/annotations/cors"
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/fastcgi"
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/globalratelimit"
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/ipallowlist"
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/ipdenylist"
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/ipwhitelist"
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/log"
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/mirror"
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/modsecurity"
|
||||
|
@ -224,7 +224,7 @@ type Server struct {
|
|||
// is required.
|
||||
// The chain in the execution order of annotations should be:
|
||||
// - Denylist
|
||||
// - Whitelist
|
||||
// - Allowlist
|
||||
// - RateLimit
|
||||
// - BasicDigestAuth
|
||||
// - ExternalAuth
|
||||
|
@ -298,10 +298,10 @@ type Location struct {
|
|||
// addresses or networks are allowed.
|
||||
// +optional
|
||||
Denylist ipdenylist.SourceRange `json:"denylist,omitempty"`
|
||||
// Whitelist indicates only connections from certain client
|
||||
// Allowlist indicates only connections from certain client
|
||||
// addresses or networks are allowed.
|
||||
// +optional
|
||||
Whitelist ipwhitelist.SourceRange `json:"whitelist,omitempty"`
|
||||
Allowlist ipallowlist.SourceRange `json:"allowlist,omitempty"`
|
||||
// Proxy contains information about timeouts and buffer sizes
|
||||
// to be used in connections against endpoints
|
||||
// +optional
|
||||
|
|
|
@ -400,7 +400,7 @@ func (l1 *Location) Equal(l2 *Location) bool {
|
|||
if !(&l1.Denylist).Equal(&l2.Denylist) {
|
||||
return false
|
||||
}
|
||||
if !(&l1.Whitelist).Equal(&l2.Whitelist) {
|
||||
if !(&l1.Allowlist).Equal(&l2.Allowlist) {
|
||||
return false
|
||||
}
|
||||
if !(&l1.Proxy).Equal(&l2.Proxy) {
|
||||
|
|
|
@ -548,14 +548,14 @@ http {
|
|||
|
||||
{{ range $rl := (filterRateLimits $servers ) }}
|
||||
# Ratelimit {{ $rl.Name }}
|
||||
geo $remote_addr $whitelist_{{ $rl.ID }} {
|
||||
geo $remote_addr $allowlist_{{ $rl.ID }} {
|
||||
default 0;
|
||||
{{ range $ip := $rl.Whitelist }}
|
||||
{{ range $ip := $rl.Allowlist }}
|
||||
{{ $ip }} 1;{{ end }}
|
||||
}
|
||||
|
||||
# Ratelimit {{ $rl.Name }}
|
||||
map $whitelist_{{ $rl.ID }} $limit_{{ $rl.ID }} {
|
||||
map $allowlist_{{ $rl.ID }} $limit_{{ $rl.ID }} {
|
||||
0 {{ $cfg.LimitConnZoneVariable }};
|
||||
1 "";
|
||||
}
|
||||
|
@ -1312,8 +1312,8 @@ stream {
|
|||
{{ range $ip := $location.Denylist.CIDR }}
|
||||
deny {{ $ip }};{{ end }}
|
||||
{{ end }}
|
||||
{{ if gt (len $location.Whitelist.CIDR) 0 }}
|
||||
{{ range $ip := $location.Whitelist.CIDR }}
|
||||
{{ if gt (len $location.Allowlist.CIDR) 0 }}
|
||||
{{ range $ip := $location.Allowlist.CIDR }}
|
||||
allow {{ $ip }};{{ end }}
|
||||
deny all;
|
||||
{{ end }}
|
||||
|
|
|
@ -24,7 +24,7 @@ E2E_CHECK_LEAKS=${E2E_CHECK_LEAKS:-""}
|
|||
|
||||
reportFile="report-e2e-test-suite.xml"
|
||||
ginkgo_args=(
|
||||
"--fail-fast"
|
||||
# "--fail-fast"
|
||||
"--flake-attempts=2"
|
||||
"--junit-report=${reportFile}"
|
||||
"--nodes=${E2E_NODES}"
|
||||
|
|
|
@ -24,19 +24,19 @@ import (
|
|||
"k8s.io/ingress-nginx/test/e2e/framework"
|
||||
)
|
||||
|
||||
var _ = framework.DescribeAnnotation("whitelist-source-range", func() {
|
||||
f := framework.NewDefaultFramework("ipwhitelist")
|
||||
var _ = framework.DescribeAnnotation("allowlist-source-range", func() {
|
||||
f := framework.NewDefaultFramework("ipallowlist")
|
||||
|
||||
ginkgo.BeforeEach(func() {
|
||||
f.NewEchoDeployment()
|
||||
})
|
||||
|
||||
ginkgo.It("should set valid ip whitelist range", func() {
|
||||
host := "ipwhitelist.foo.com"
|
||||
ginkgo.It("should set valid ip allowlist range", func() {
|
||||
host := "ipallowlist.foo.com"
|
||||
nameSpace := f.Namespace
|
||||
|
||||
annotations := map[string]string{
|
||||
"nginx.ingress.kubernetes.io/whitelist-source-range": "18.0.0.0/8, 56.0.0.0/8",
|
||||
"nginx.ingress.kubernetes.io/allowlist-source-range": "18.0.0.0/8, 56.0.0.0/8",
|
||||
}
|
||||
|
||||
ing := framework.NewSingleIngress(host, "/", host, nameSpace, framework.EchoService, 80, annotations)
|
Loading…
Reference in a new issue