Add Ingress host, path and annotations support (#1325)

For each ingress controller, the format of ingress host,
path and annotations are different. To support different
kinds of controller, this PR adds new ingress config field
to http trigger spec. A user can set annotations, host and
path based on the type of underlying ingress controller
with CLI.

Command example:

fission route create --name foo \
    --url /foo/{bar} --function foofn --createingress \
    --ingressannotation "nginx.ingress.kubernetes.io/ssl-redirect=false" \
    --ingressannotation "nginx.ingress.kubernetes.io/use-regex=true" \
    --ingressrule "*=/foo/*"
This commit is contained in:
Ta-Ching Chen
2019-09-26 16:54:16 +08:00
committed by GitHub
parent 82a7acf408
commit 49d60b19f3
14 changed files with 1421 additions and 138 deletions
+43 -70
View File
@@ -18,16 +18,15 @@ package router
import (
"os"
"reflect"
"go.uber.org/zap"
"k8s.io/api/extensions/v1beta1"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/client-go/kubernetes"
fv1 "github.com/fission/fission/pkg/apis/fission.io/v1"
"github.com/fission/fission/pkg/router/util"
)
var podNamespace string
@@ -43,61 +42,14 @@ func createIngress(logger *zap.Logger, trigger *fv1.HTTPTrigger, kubeClient *kub
if !trigger.Spec.CreateIngress {
return
}
_, err := kubeClient.ExtensionsV1beta1().Ingresses(podNamespace).Get(trigger.Metadata.Name, v1.GetOptions{})
if err == nil {
return
}
ing := &v1beta1.Ingress{
ObjectMeta: metav1.ObjectMeta{
Labels: getDeployLabels(trigger),
Name: trigger.Metadata.Name,
// The Ingress NS MUST be same as Router NS, check long discussion:
// https://github.com/kubernetes/kubernetes/issues/17088
// We need to revisit this in future, once Kubernetes supports cross namespace ingress
Namespace: podNamespace,
},
Spec: v1beta1.IngressSpec{
Rules: []v1beta1.IngressRule{
{
Host: trigger.Spec.Host,
IngressRuleValue: v1beta1.IngressRuleValue{
HTTP: &v1beta1.HTTPIngressRuleValue{
Paths: []v1beta1.HTTPIngressPath{
{
Backend: v1beta1.IngressBackend{
ServiceName: "router",
ServicePort: intstr.IntOrString{
Type: intstr.Int,
IntVal: 80,
},
},
Path: trigger.Spec.RelativeURL,
},
},
},
},
},
},
},
}
_, err = kubeClient.ExtensionsV1beta1().Ingresses(podNamespace).Create(ing)
if err != nil {
_, err := kubeClient.ExtensionsV1beta1().Ingresses(podNamespace).Create(util.GetIngressSpec(podNamespace, trigger))
if err != nil && !k8serrors.IsAlreadyExists(err) {
logger.Error("failed to create ingress", zap.Error(err))
return
}
logger.Debug("created ingress successfully for trigger", zap.String("trigger", trigger.Metadata.Name))
}
func getDeployLabels(trigger *fv1.HTTPTrigger) map[string]string {
return map[string]string{
"triggerName": trigger.Metadata.Name,
"functionName": trigger.Spec.FunctionReference.Name,
"triggerNamespace": trigger.Metadata.Namespace,
}
}
func deleteIngress(logger *zap.Logger, trigger *fv1.HTTPTrigger, kubeClient *kubernetes.Clientset) {
if !trigger.Spec.CreateIngress {
return
@@ -130,25 +82,46 @@ func updateIngress(logger *zap.Logger, oldT *fv1.HTTPTrigger, newT *fv1.HTTPTrig
return
}
if newT.Spec.Host != oldT.Spec.Host || newT.Spec.RelativeURL != oldT.Spec.RelativeURL {
ingress, err := kubeClient.ExtensionsV1beta1().Ingresses(podNamespace).Get(oldT.Metadata.Name, v1.GetOptions{})
oldIngress, err := kubeClient.ExtensionsV1beta1().Ingresses(podNamespace).Get(oldT.Metadata.Name, v1.GetOptions{})
if err != nil {
if k8serrors.IsNotFound(err) {
createIngress(logger, newT, kubeClient)
}
logger.Error("failed to get ingress when updating trigger",
zap.Error(err),
zap.String("trigger", oldT.Metadata.Name))
return
}
newIngress := util.GetIngressSpec(podNamespace, newT)
changes := false
if !reflect.DeepEqual(oldIngress.Annotations, newIngress.Annotations) {
logger.Debug("ingress annotation",
zap.Any("old_trigger", oldIngress.Annotations), zap.Any("new_trigger", newIngress.Annotations))
if oldIngress.Annotations == nil || newIngress.Annotations == nil {
oldIngress.Annotations = newIngress.Annotations
} else {
for k, v := range newIngress.Annotations {
oldIngress.Annotations[k] = v
}
}
changes = true
}
if !reflect.DeepEqual(oldIngress.Spec, newIngress.Spec) {
logger.Debug("ingress spec",
zap.Any("old_trigger", oldIngress.Spec), zap.Any("new_trigger", newIngress.Spec))
oldIngress.Spec = newIngress.Spec
changes = true
}
if changes {
_, err = kubeClient.ExtensionsV1beta1().Ingresses(podNamespace).Update(oldIngress)
if err != nil {
logger.Error("failed to get ingress when updating trigger",
zap.Error(err),
zap.String("trigger", oldT.Metadata.Name))
}
if newT.Spec.Host != oldT.Spec.Host {
ingress.Spec.Rules[0].Host = newT.Spec.Host
}
if newT.Spec.RelativeURL != oldT.Spec.RelativeURL {
ingress.Spec.Rules[0].HTTP.Paths[0].Path = newT.Spec.RelativeURL
}
_, err = kubeClient.ExtensionsV1beta1().Ingresses(podNamespace).Update(ingress)
if err != nil {
logger.Error("failed to update ingress for trigger", zap.String("trigger", oldT.Metadata.Name))
logger.Error("failed to update ingress for trigger", zap.Error(err), zap.String("trigger", oldT.Metadata.Name))
return
}
+85
View File
@@ -0,0 +1,85 @@
/*
Copyright 2019 The Fission 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 util
import (
"k8s.io/api/extensions/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
fv1 "github.com/fission/fission/pkg/apis/fission.io/v1"
)
func GetIngressSpec(namespace string, trigger *fv1.HTTPTrigger) *v1beta1.Ingress {
// TODO: remove backward compatibility
host, path := trigger.Spec.Host, trigger.Spec.RelativeURL
if len(trigger.Spec.IngressConfig.Host) > 0 && len(trigger.Spec.IngressConfig.Path) > 0 {
host, path = trigger.Spec.IngressConfig.Host, trigger.Spec.IngressConfig.Path
}
// In Ingress, to accept requests from all host, the host field will
// be an empty string instead of "*" shown in kubectl. So replace it
// with empty string
if host == "*" {
host = "" // wildcard Ingress host
}
ing := &v1beta1.Ingress{
ObjectMeta: metav1.ObjectMeta{
Labels: GetDeployLabels(trigger),
Name: trigger.Metadata.Name,
// The Ingress NS MUST be same as Router NS, check long discussion:
// https://github.com/kubernetes/kubernetes/issues/17088
// We need to revisit this in future, once Kubernetes supports cross namespace ingress
Namespace: namespace,
Annotations: trigger.Spec.IngressConfig.Annotations,
},
Spec: v1beta1.IngressSpec{
Rules: []v1beta1.IngressRule{
{
Host: host,
IngressRuleValue: v1beta1.IngressRuleValue{
HTTP: &v1beta1.HTTPIngressRuleValue{
Paths: []v1beta1.HTTPIngressPath{
{
Backend: v1beta1.IngressBackend{
ServiceName: "router",
ServicePort: intstr.IntOrString{
Type: intstr.Int,
IntVal: 80,
},
},
Path: path,
},
},
},
},
},
},
},
}
return ing
}
func GetDeployLabels(trigger *fv1.HTTPTrigger) map[string]string {
// TODO: support function weight
return map[string]string{
"triggerName": trigger.Metadata.Name,
"functionName": trigger.Spec.FunctionReference.Name,
"triggerNamespace": trigger.Metadata.Namespace,
}
}
+499
View File
@@ -0,0 +1,499 @@
/*
Copyright 2019 The Fission 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 util
import (
"reflect"
"testing"
"k8s.io/api/extensions/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
fv1 "github.com/fission/fission/pkg/apis/fission.io/v1"
)
func TestGetIngressSpec(t *testing.T) {
type args struct {
ingressNS string
trigger *fv1.HTTPTrigger
}
tests := []struct {
name string
args args
want *v1beta1.Ingress
}{
{
name: "host-backward-compatibility",
args: args{
ingressNS: "foobarNS",
trigger: &fv1.HTTPTrigger{
Metadata: metav1.ObjectMeta{
Name: "foo",
Namespace: "bar",
},
Spec: fv1.HTTPTriggerSpec{
Host: "test.com",
RelativeURL: "/foo/bar",
FunctionReference: fv1.FunctionReference{
Name: "foofunc",
},
IngressConfig: fv1.IngressConfig{
Annotations: nil,
},
},
},
},
want: &v1beta1.Ingress{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
"triggerName": "foo",
"functionName": "foofunc",
"triggerNamespace": "bar",
},
Name: "foo",
Namespace: "foobarNS",
Annotations: nil,
},
Spec: v1beta1.IngressSpec{
Rules: []v1beta1.IngressRule{
{
Host: "test.com",
IngressRuleValue: v1beta1.IngressRuleValue{
HTTP: &v1beta1.HTTPIngressRuleValue{
Paths: []v1beta1.HTTPIngressPath{
{
Backend: v1beta1.IngressBackend{
ServiceName: "router",
ServicePort: intstr.IntOrString{
Type: intstr.Int,
IntVal: 80,
},
},
Path: "/foo/bar",
},
},
},
},
},
},
},
},
},
{
name: "create-ingress-with-only-annotations",
args: args{
ingressNS: "foobarNS",
trigger: &fv1.HTTPTrigger{
Metadata: metav1.ObjectMeta{
Name: "foo",
Namespace: "bar",
},
Spec: fv1.HTTPTriggerSpec{
RelativeURL: "/foo/bar",
FunctionReference: fv1.FunctionReference{
Name: "foofunc",
},
IngressConfig: fv1.IngressConfig{
Annotations: map[string]string{
"key": "value",
},
},
},
},
},
want: &v1beta1.Ingress{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
"triggerName": "foo",
"functionName": "foofunc",
"triggerNamespace": "bar",
},
Name: "foo",
Namespace: "foobarNS",
Annotations: map[string]string{
"key": "value",
},
},
Spec: v1beta1.IngressSpec{
Rules: []v1beta1.IngressRule{
{
Host: "",
IngressRuleValue: v1beta1.IngressRuleValue{
HTTP: &v1beta1.HTTPIngressRuleValue{
Paths: []v1beta1.HTTPIngressPath{
{
Backend: v1beta1.IngressBackend{
ServiceName: "router",
ServicePort: intstr.IntOrString{
Type: intstr.Int,
IntVal: 80,
},
},
Path: "/foo/bar",
},
},
},
},
},
},
},
},
},
{
name: "create-ingress-with-only-rule",
args: args{
ingressNS: "foobarNS",
trigger: &fv1.HTTPTrigger{
Metadata: metav1.ObjectMeta{
Name: "foo",
Namespace: "bar",
},
Spec: fv1.HTTPTriggerSpec{
RelativeURL: "/foo/{bar}",
FunctionReference: fv1.FunctionReference{
Name: "foofunc",
},
IngressConfig: fv1.IngressConfig{
Annotations: nil,
Path: "/foo/bar",
Host: "test.com",
},
},
},
},
want: &v1beta1.Ingress{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
"triggerName": "foo",
"functionName": "foofunc",
"triggerNamespace": "bar",
},
Name: "foo",
Namespace: "foobarNS",
Annotations: nil,
},
Spec: v1beta1.IngressSpec{
Rules: []v1beta1.IngressRule{
{
Host: "test.com",
IngressRuleValue: v1beta1.IngressRuleValue{
HTTP: &v1beta1.HTTPIngressRuleValue{
Paths: []v1beta1.HTTPIngressPath{
{
Backend: v1beta1.IngressBackend{
ServiceName: "router",
ServicePort: intstr.IntOrString{
Type: intstr.Int,
IntVal: 80,
},
},
Path: "/foo/bar",
},
},
},
},
},
},
},
},
},
{
name: "create-ingress-with-empty-rule-host",
args: args{
ingressNS: "foobarNS",
trigger: &fv1.HTTPTrigger{
Metadata: metav1.ObjectMeta{
Name: "foo",
Namespace: "bar",
},
Spec: fv1.HTTPTriggerSpec{
RelativeURL: "/foo/{bar}",
FunctionReference: fv1.FunctionReference{
Name: "foofunc",
},
IngressConfig: fv1.IngressConfig{
Annotations: nil,
Path: "/foo/bar",
Host: "",
},
},
},
},
want: &v1beta1.Ingress{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
"triggerName": "foo",
"functionName": "foofunc",
"triggerNamespace": "bar",
},
Name: "foo",
Namespace: "foobarNS",
Annotations: nil,
},
Spec: v1beta1.IngressSpec{
Rules: []v1beta1.IngressRule{
{
Host: "",
IngressRuleValue: v1beta1.IngressRuleValue{
HTTP: &v1beta1.HTTPIngressRuleValue{
Paths: []v1beta1.HTTPIngressPath{
{
Backend: v1beta1.IngressBackend{
ServiceName: "router",
ServicePort: intstr.IntOrString{
Type: intstr.Int,
IntVal: 80,
},
},
Path: "/foo/{bar}",
},
},
},
},
},
},
},
},
},
{
name: "create-ingress-with-empty-rule-path",
args: args{
ingressNS: "foobarNS",
trigger: &fv1.HTTPTrigger{
Metadata: metav1.ObjectMeta{
Name: "foo",
Namespace: "bar",
},
Spec: fv1.HTTPTriggerSpec{
RelativeURL: "/foo/{bar}",
FunctionReference: fv1.FunctionReference{
Name: "foofunc",
},
IngressConfig: fv1.IngressConfig{
Annotations: nil,
Path: "",
Host: "test.com",
},
},
},
},
want: &v1beta1.Ingress{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
"triggerName": "foo",
"functionName": "foofunc",
"triggerNamespace": "bar",
},
Name: "foo",
Namespace: "foobarNS",
Annotations: nil,
},
Spec: v1beta1.IngressSpec{
Rules: []v1beta1.IngressRule{
{
Host: "",
IngressRuleValue: v1beta1.IngressRuleValue{
HTTP: &v1beta1.HTTPIngressRuleValue{
Paths: []v1beta1.HTTPIngressPath{
{
Backend: v1beta1.IngressBackend{
ServiceName: "router",
ServicePort: intstr.IntOrString{
Type: intstr.Int,
IntVal: 80,
},
},
Path: "/foo/{bar}",
},
},
},
},
},
},
},
},
},
{
name: "create-ingress-with-host-and-rule",
args: args{
ingressNS: "foobarNS",
trigger: &fv1.HTTPTrigger{
Metadata: metav1.ObjectMeta{
Name: "foo",
Namespace: "bar",
},
Spec: fv1.HTTPTriggerSpec{
Host: "example.com",
RelativeURL: "/foo/{bar}",
FunctionReference: fv1.FunctionReference{
Name: "foofunc",
},
IngressConfig: fv1.IngressConfig{
Annotations: nil,
Path: "/foo/bar",
Host: "test.com",
},
},
},
},
want: &v1beta1.Ingress{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
"triggerName": "foo",
"functionName": "foofunc",
"triggerNamespace": "bar",
},
Name: "foo",
Namespace: "foobarNS",
Annotations: nil,
},
Spec: v1beta1.IngressSpec{
Rules: []v1beta1.IngressRule{
{
Host: "test.com",
IngressRuleValue: v1beta1.IngressRuleValue{
HTTP: &v1beta1.HTTPIngressRuleValue{
Paths: []v1beta1.HTTPIngressPath{
{
Backend: v1beta1.IngressBackend{
ServiceName: "router",
ServicePort: intstr.IntOrString{
Type: intstr.Int,
IntVal: 80,
},
},
Path: "/foo/bar",
},
},
},
},
},
},
},
},
},
{
name: "create-ingress-with-wildecard-rule-host",
args: args{
ingressNS: "foobarNS",
trigger: &fv1.HTTPTrigger{
Metadata: metav1.ObjectMeta{
Name: "foo",
Namespace: "bar",
},
Spec: fv1.HTTPTriggerSpec{
RelativeURL: "/foo/{bar}",
FunctionReference: fv1.FunctionReference{
Name: "foofunc",
},
IngressConfig: fv1.IngressConfig{
Annotations: nil,
Path: "/foo/bar",
Host: "*",
},
},
},
},
want: &v1beta1.Ingress{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
"triggerName": "foo",
"functionName": "foofunc",
"triggerNamespace": "bar",
},
Name: "foo",
Namespace: "foobarNS",
Annotations: nil,
},
Spec: v1beta1.IngressSpec{
Rules: []v1beta1.IngressRule{
{
Host: "",
IngressRuleValue: v1beta1.IngressRuleValue{
HTTP: &v1beta1.HTTPIngressRuleValue{
Paths: []v1beta1.HTTPIngressPath{
{
Backend: v1beta1.IngressBackend{
ServiceName: "router",
ServicePort: intstr.IntOrString{
Type: intstr.Int,
IntVal: 80,
},
},
Path: "/foo/bar",
},
},
},
},
},
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := GetIngressSpec(tt.args.ingressNS, tt.args.trigger); !reflect.DeepEqual(got, tt.want) {
t.Errorf("GetIngressSpec() = %v, want %v", got, tt.want)
}
})
}
}
func TestGetDeployLabels(t *testing.T) {
type args struct {
trigger *fv1.HTTPTrigger
}
// TODO: support function weight
tests := []struct {
name string
args args
want map[string]string
}{
{
name: "getdeploylabels",
args: args{
trigger: &fv1.HTTPTrigger{
Metadata: metav1.ObjectMeta{
Name: "foo",
Namespace: "bar",
},
Spec: fv1.HTTPTriggerSpec{
FunctionReference: fv1.FunctionReference{
Type: "name",
Name: "foobar",
FunctionWeights: nil,
},
},
},
},
want: map[string]string{
"triggerName": "foo",
"functionName": "foobar",
"triggerNamespace": "bar",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := GetDeployLabels(tt.args.trigger); !reflect.DeepEqual(got, tt.want) {
t.Errorf("GetDeployLabels() = %v, want %v", got, tt.want)
}
})
}
}