Merge pull request #4344 from Nuglif/fastcgi-backend-support

Add FastCGI backend support (#2982)
This commit is contained in:
Kubernetes Prow Robot 2019-07-31 11:20:14 -07:00 committed by GitHub
commit c8a3710fb8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 938 additions and 6 deletions

View file

@ -38,6 +38,7 @@ import (
"k8s.io/ingress-nginx/internal/ingress/annotations/cors"
"k8s.io/ingress-nginx/internal/ingress/annotations/customhttperrors"
"k8s.io/ingress-nginx/internal/ingress/annotations/defaultbackend"
"k8s.io/ingress-nginx/internal/ingress/annotations/fastcgi"
"k8s.io/ingress-nginx/internal/ingress/annotations/http2pushpreload"
"k8s.io/ingress-nginx/internal/ingress/annotations/influxdb"
"k8s.io/ingress-nginx/internal/ingress/annotations/ipwhitelist"
@ -82,6 +83,7 @@ type Ingress struct {
CustomHTTPErrors []int
DefaultBackend *apiv1.Service
//TODO: Change this back into an error when https://github.com/imdario/mergo/issues/100 is resolved
FastCGI fastcgi.Config
Denied *string
ExternalAuth authreq.Config
EnableGlobalAuth bool
@ -128,6 +130,7 @@ func NewAnnotationExtractor(cfg resolver.Resolver) Extractor {
"CorsConfig": cors.NewParser(cfg),
"CustomHTTPErrors": customhttperrors.NewParser(cfg),
"DefaultBackend": defaultbackend.NewParser(cfg),
"FastCGI": fastcgi.NewParser(cfg),
"ExternalAuth": authreq.NewParser(cfg),
"EnableGlobalAuth": authreqglobal.NewParser(cfg),
"HTTP2PushPreload": http2pushpreload.NewParser(cfg),

View file

@ -31,7 +31,7 @@ import (
const HTTP = "HTTP"
var (
validProtocols = regexp.MustCompile(`^(HTTP|HTTPS|AJP|GRPC|GRPCS)$`)
validProtocols = regexp.MustCompile(`^(HTTP|HTTPS|AJP|GRPC|GRPCS|FCGI)$`)
)
type backendProtocol struct {

View file

@ -0,0 +1,107 @@
/*
Copyright 2018 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 fastcgi
import (
"fmt"
"reflect"
"github.com/pkg/errors"
networking "k8s.io/api/networking/v1beta1"
"k8s.io/client-go/tools/cache"
"k8s.io/ingress-nginx/internal/ingress/annotations/parser"
ing_errors "k8s.io/ingress-nginx/internal/ingress/errors"
"k8s.io/ingress-nginx/internal/ingress/resolver"
)
type fastcgi struct {
r resolver.Resolver
}
// Config describes the per location fastcgi config
type Config struct {
Index string `json:"index"`
Params map[string]string `json:"params"`
}
// Equal tests for equality between two Configuration types
func (l1 *Config) Equal(l2 *Config) bool {
if l1 == l2 {
return true
}
if l1 == nil || l2 == nil {
return false
}
if l1.Index != l2.Index {
return false
}
return reflect.DeepEqual(l1.Params, l2.Params)
}
// NewParser creates a new fastcgiConfig protocol annotation parser
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
return fastcgi{r}
}
// ParseAnnotations parses the annotations contained in the ingress
// rule used to indicate the fastcgiConfig.
func (a fastcgi) Parse(ing *networking.Ingress) (interface{}, error) {
fcgiConfig := Config{}
if ing.GetAnnotations() == nil {
return fcgiConfig, nil
}
index, err := parser.GetStringAnnotation("fastcgi-index", ing)
if err != nil {
index = ""
}
fcgiConfig.Index = index
cm, err := parser.GetStringAnnotation("fastcgi-params-configmap", ing)
if err != nil {
return fcgiConfig, nil
}
cmns, cmn, err := cache.SplitMetaNamespaceKey(cm)
if err != nil {
return fcgiConfig, ing_errors.LocationDenied{
Reason: errors.Wrap(err, "error reading configmap name from annotation"),
}
}
if cmns == "" {
cmns = ing.Namespace
}
cm = fmt.Sprintf("%v/%v", cmns, cmn)
cmap, err := a.r.GetConfigMap(cm)
if err != nil {
return fcgiConfig, ing_errors.LocationDenied{
Reason: errors.Wrapf(err, "unexpected error reading configmap %v", cm),
}
}
fcgiConfig.Params = cmap.Data
return fcgiConfig, nil
}

View file

@ -0,0 +1,263 @@
/*
Copyright 2018 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 fastcgi
import (
"testing"
api "k8s.io/api/core/v1"
networking "k8s.io/api/networking/v1beta1"
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"
"k8s.io/apimachinery/pkg/util/intstr"
)
func buildIngress() *networking.Ingress {
return &networking.Ingress{
ObjectMeta: meta_v1.ObjectMeta{
Name: "foo",
Namespace: api.NamespaceDefault,
},
Spec: networking.IngressSpec{
Backend: &networking.IngressBackend{
ServiceName: "fastcgi",
ServicePort: intstr.FromInt(80),
},
},
}
}
type mockConfigMap struct {
resolver.Mock
}
func (m mockConfigMap) GetConfigMap(name string) (*api.ConfigMap, error) {
if name != "default/demo-configmap" {
return nil, errors.Errorf("there is no configmap with name %v", name)
}
return &api.ConfigMap{
ObjectMeta: meta_v1.ObjectMeta{
Namespace: api.NamespaceDefault,
Name: "demo-secret",
},
Data: map[string]string{"REDIRECT_STATUS": "200", "SERVER_NAME": "$server_name"},
}, nil
}
func TestParseEmptyFastCGIAnnotations(t *testing.T) {
ing := buildIngress()
i, err := NewParser(&mockConfigMap{}).Parse(ing)
if err != nil {
t.Errorf("unexpected error parsing ingress without fastcgi")
}
config, ok := i.(Config)
if !ok {
t.Errorf("Parse do not return a Config object")
}
if config.Index != "" {
t.Errorf("Index should be an empty string")
}
if 0 != len(config.Params) {
t.Errorf("Params should be an empty slice")
}
}
func TestParseFastCGIIndexAnnotation(t *testing.T) {
ing := buildIngress()
const expectedAnnotation = "index.php"
data := map[string]string{}
data[parser.GetAnnotationWithPrefix("fastcgi-index")] = expectedAnnotation
ing.SetAnnotations(data)
i, err := NewParser(&mockConfigMap{}).Parse(ing)
if err != nil {
t.Errorf("unexpected error parsing ingress without fastcgi")
}
config, ok := i.(Config)
if !ok {
t.Errorf("Parse do not return a Config object")
}
if config.Index != "index.php" {
t.Errorf("expected %s but %v returned", expectedAnnotation, config.Index)
}
}
func TestParseEmptyFastCGIParamsConfigMapAnnotation(t *testing.T) {
ing := buildIngress()
data := map[string]string{}
data[parser.GetAnnotationWithPrefix("fastcgi-params-configmap")] = ""
ing.SetAnnotations(data)
i, err := NewParser(&mockConfigMap{}).Parse(ing)
if err != nil {
t.Errorf("unexpected error parsing ingress without fastcgi")
}
config, ok := i.(Config)
if !ok {
t.Errorf("Parse do not return a Config object")
}
if 0 != len(config.Params) {
t.Errorf("Params should be an empty slice")
}
}
func TestParseFastCGIInvalidParamsConfigMapAnnotation(t *testing.T) {
ing := buildIngress()
invalidConfigMapList := []string{"unknown/configMap", "unknown/config/map"}
for _, configmap := range invalidConfigMapList {
data := map[string]string{}
data[parser.GetAnnotationWithPrefix("fastcgi-params-configmap")] = configmap
ing.SetAnnotations(data)
i, err := NewParser(&mockConfigMap{}).Parse(ing)
if err == nil {
t.Errorf("Reading an unexisting configmap should return an error")
}
config, ok := i.(Config)
if !ok {
t.Errorf("Parse do not return a Config object")
}
if 0 != len(config.Params) {
t.Errorf("Params should be an empty slice")
}
}
}
func TestParseFastCGIParamsConfigMapAnnotationWithoutNS(t *testing.T) {
ing := buildIngress()
data := map[string]string{}
data[parser.GetAnnotationWithPrefix("fastcgi-params-configmap")] = "demo-configmap"
ing.SetAnnotations(data)
i, err := NewParser(&mockConfigMap{}).Parse(ing)
if err != nil {
t.Errorf("unexpected error parsing ingress without fastcgi")
}
config, ok := i.(Config)
if !ok {
t.Errorf("Parse do not return a Config object")
}
if 2 != len(config.Params) {
t.Errorf("Params should have a length of 2")
}
if "200" != config.Params["REDIRECT_STATUS"] || "$server_name" != config.Params["SERVER_NAME"] {
t.Errorf("Params value is not the one expected")
}
}
func TestParseFastCGIParamsConfigMapAnnotationWithNS(t *testing.T) {
ing := buildIngress()
data := map[string]string{}
data[parser.GetAnnotationWithPrefix("fastcgi-params-configmap")] = "default/demo-configmap"
ing.SetAnnotations(data)
i, err := NewParser(&mockConfigMap{}).Parse(ing)
if err != nil {
t.Errorf("unexpected error parsing ingress without fastcgi")
}
config, ok := i.(Config)
if !ok {
t.Errorf("Parse do not return a Config object")
}
if 2 != len(config.Params) {
t.Errorf("Params should have a length of 2")
}
if "200" != config.Params["REDIRECT_STATUS"] || "$server_name" != config.Params["SERVER_NAME"] {
t.Errorf("Params value is not the one expected")
}
}
func TestConfigEquality(t *testing.T) {
var nilConfig *Config
config := Config{
Index: "index.php",
Params: map[string]string{"REDIRECT_STATUS": "200", "SERVER_NAME": "$server_name"},
}
configCopy := Config{
Index: "index.php",
Params: map[string]string{"REDIRECT_STATUS": "200", "SERVER_NAME": "$server_name"},
}
config2 := Config{
Index: "index.php",
Params: map[string]string{"REDIRECT_STATUS": "200"},
}
config3 := Config{
Index: "index.py",
Params: map[string]string{"SERVER_NAME": "$server_name", "REDIRECT_STATUS": "200"},
}
config4 := Config{
Index: "index.php",
Params: map[string]string{"SERVER_NAME": "$server_name", "REDIRECT_STATUS": "200"},
}
if !config.Equal(&config) {
t.Errorf("config should be equal to itself")
}
if nilConfig.Equal(&config) {
t.Errorf("Foo")
}
if !config.Equal(&configCopy) {
t.Errorf("config should be equal to configCopy")
}
if config.Equal(&config2) {
t.Errorf("config2 should not be equal to config")
}
if config.Equal(&config3) {
t.Errorf("config3 should not be equal to config")
}
if !config.Equal(&config4) {
t.Errorf("config4 should be equal to config")
}
}

View file

@ -1165,6 +1165,7 @@ func locationApplyAnnotations(loc *ingress.Location, anns *annotations.Ingress)
loc.InfluxDB = anns.InfluxDB
loc.DefaultBackend = anns.DefaultBackend
loc.BackendProtocol = anns.BackendProtocol
loc.FastCGI = anns.FastCGI
loc.CustomHTTPErrors = anns.CustomHTTPErrors
loc.ModSecurity = anns.ModSecurity
loc.Satisfy = anns.Satisfy

View file

@ -497,6 +497,9 @@ func buildProxyPass(host string, b interface{}, loc interface{}) string {
case "AJP":
proto = ""
proxyPass = "ajp_pass"
case "FCGI":
proto = ""
proxyPass = "fastcgi_pass"
}
upstreamName := "upstream_balancer"

View file

@ -874,6 +874,7 @@ func TestOpentracingPropagateContext(t *testing.T) {
&ingress.Location{BackendProtocol: "GRPC"}: "opentracing_grpc_propagate_context",
&ingress.Location{BackendProtocol: "GRPCS"}: "opentracing_grpc_propagate_context",
&ingress.Location{BackendProtocol: "AJP"}: "opentracing_propagate_context",
&ingress.Location{BackendProtocol: "FCGI"}: "opentracing_propagate_context",
"not a location": "opentracing_propagate_context",
}

View file

@ -26,6 +26,9 @@ type Resolver interface {
// GetDefaultBackend returns the backend that must be used as default
GetDefaultBackend() defaults.Backend
// GetConfigMap searches for configmap containing the namespace and name usting the character /
GetConfigMap(string) (*apiv1.ConfigMap, error)
// GetSecret searches for secrets containing the namespace and name using a the character /
GetSecret(string) (*apiv1.Secret, error)

View file

@ -31,6 +31,11 @@ func (m Mock) GetDefaultBackend() defaults.Backend {
return defaults.Backend{}
}
// GetConfigMap searches for configmap containing the namespace and name usting the character /
func (m Mock) GetConfigMap(string) (*apiv1.ConfigMap, error) {
return nil, nil
}
// GetSecret searches for secrets contenating the namespace and name using a the character /
func (m Mock) GetSecret(string) (*apiv1.Secret, error) {
return nil, nil

View file

@ -27,6 +27,7 @@ import (
"k8s.io/ingress-nginx/internal/ingress/annotations/authtls"
"k8s.io/ingress-nginx/internal/ingress/annotations/connection"
"k8s.io/ingress-nginx/internal/ingress/annotations/cors"
"k8s.io/ingress-nginx/internal/ingress/annotations/fastcgi"
"k8s.io/ingress-nginx/internal/ingress/annotations/influxdb"
"k8s.io/ingress-nginx/internal/ingress/annotations/ipwhitelist"
"k8s.io/ingress-nginx/internal/ingress/annotations/log"
@ -308,6 +309,9 @@ type Location struct {
// BackendProtocol indicates which protocol should be used to communicate with the service
// By default this is HTTP
BackendProtocol string `json:"backend-protocol"`
// FastCGI allows the ingress to act as a FastCGI client for a given location.
// +optional
FastCGI fastcgi.Config `json:"fastcgi,omitempty"`
// CustomHTTPErrors specifies the error codes that should be intercepted.
// +optional
CustomHTTPErrors []int `json:"custom-http-errors"`

View file

@ -401,6 +401,10 @@ func (l1 *Location) Equal(l2 *Location) bool {
return false
}
if !(&l1.FastCGI).Equal(&l2.FastCGI) {
return false
}
match := compareInts(l1.CustomHTTPErrors, l2.CustomHTTPErrors)
if !match {
return false