mirror of
https://github.com/fluxcd/flagger.git
synced 2026-04-15 06:57:34 +00:00
Merge pull request #82 from stefanprodan/headers-ops
Add support for HTTP request header manipulation rules
This commit is contained in:
10
README.md
10
README.md
@@ -107,10 +107,12 @@ spec:
|
||||
rewrite:
|
||||
uri: /
|
||||
# Envoy timeout and retry policy (optional)
|
||||
appendHeaders:
|
||||
x-envoy-upstream-rq-timeout-ms: "15000"
|
||||
x-envoy-max-retries: "10"
|
||||
x-envoy-retry-on: "gateway-error,connect-failure,refused-stream"
|
||||
headers:
|
||||
request:
|
||||
add:
|
||||
x-envoy-upstream-rq-timeout-ms: "15000"
|
||||
x-envoy-max-retries: "10"
|
||||
x-envoy-retry-on: "gateway-error,connect-failure,refused-stream"
|
||||
# promote the canary without analysing it (default false)
|
||||
skipAnalysis: false
|
||||
# define the canary analysis timing and KPIs
|
||||
|
||||
@@ -34,10 +34,12 @@ spec:
|
||||
rewrite:
|
||||
uri: /
|
||||
# Envoy timeout and retry policy (optional)
|
||||
appendHeaders:
|
||||
x-envoy-upstream-rq-timeout-ms: "15000"
|
||||
x-envoy-max-retries: "10"
|
||||
x-envoy-retry-on: "gateway-error,connect-failure,refused-stream"
|
||||
headers:
|
||||
request:
|
||||
add:
|
||||
x-envoy-upstream-rq-timeout-ms: "15000"
|
||||
x-envoy-max-retries: "10"
|
||||
x-envoy-retry-on: "gateway-error,connect-failure,refused-stream"
|
||||
# promote the canary without analysing it (default false)
|
||||
skipAnalysis: false
|
||||
canaryAnalysis:
|
||||
|
||||
@@ -39,18 +39,6 @@ spec:
|
||||
# Istio virtual service host names (optional)
|
||||
hosts:
|
||||
- podinfo.example.com
|
||||
# HTTP match conditions (optional)
|
||||
match:
|
||||
- uri:
|
||||
prefix: /
|
||||
# HTTP rewrite (optional)
|
||||
rewrite:
|
||||
uri: /
|
||||
# Envoy timeout and retry policy (optional)
|
||||
appendHeaders:
|
||||
x-envoy-upstream-rq-timeout-ms: "15000"
|
||||
x-envoy-max-retries: "10"
|
||||
x-envoy-retry-on: "gateway-error,connect-failure,refused-stream"
|
||||
# promote the canary without analysing it (default false)
|
||||
skipAnalysis: false
|
||||
# define the canary analysis timing and KPIs
|
||||
@@ -138,10 +126,12 @@ metadata:
|
||||
rewrite:
|
||||
uri: /
|
||||
# Envoy timeout and retry policy (optional)
|
||||
appendHeaders:
|
||||
x-envoy-upstream-rq-timeout-ms: "15000"
|
||||
x-envoy-max-retries: "10"
|
||||
x-envoy-retry-on: "gateway-error,connect-failure,refused-stream"
|
||||
headers:
|
||||
request:
|
||||
add:
|
||||
x-envoy-upstream-rq-timeout-ms: "15000"
|
||||
x-envoy-max-retries: "10"
|
||||
x-envoy-retry-on: "gateway-error,connect-failure,refused-stream"
|
||||
# retry policy when a HTTP request fails (optional)
|
||||
retries:
|
||||
attempts: 3
|
||||
@@ -171,26 +161,26 @@ spec:
|
||||
- frontend.example.com
|
||||
- frontend
|
||||
http:
|
||||
- match:
|
||||
- uri:
|
||||
prefix: /
|
||||
rewrite:
|
||||
uri: /
|
||||
appendHeaders:
|
||||
x-envoy-upstream-rq-timeout-ms: "15000"
|
||||
x-envoy-max-retries: "10"
|
||||
x-envoy-retry-on: "gateway-error,connect-failure,refused-stream"
|
||||
route:
|
||||
- destination:
|
||||
host: frontend-primary
|
||||
port:
|
||||
number: 9898
|
||||
weight: 100
|
||||
- destination:
|
||||
host: frontend-canary
|
||||
port:
|
||||
number: 9898
|
||||
weight: 0
|
||||
- appendHeaders:
|
||||
x-envoy-max-retries: "10"
|
||||
x-envoy-retry-on: gateway-error,connect-failure,refused-stream
|
||||
x-envoy-upstream-rq-timeout-ms: "15000"
|
||||
match:
|
||||
- uri:
|
||||
prefix: /
|
||||
rewrite:
|
||||
uri: /
|
||||
route:
|
||||
- destination:
|
||||
host: podinfo-primary
|
||||
port:
|
||||
number: 9898
|
||||
weight: 100
|
||||
- destination:
|
||||
host: podinfo-canary
|
||||
port:
|
||||
number: 9898
|
||||
weight: 0
|
||||
```
|
||||
|
||||
Flagger keeps in sync the virtual service with the canary service spec. Any direct modification of the virtual
|
||||
|
||||
@@ -109,14 +109,14 @@ type CanaryStatus struct {
|
||||
// CanaryService is used to create ClusterIP services
|
||||
// and Istio Virtual Service
|
||||
type CanaryService struct {
|
||||
Port int32 `json:"port"`
|
||||
Gateways []string `json:"gateways"`
|
||||
Hosts []string `json:"hosts"`
|
||||
Match []istiov1alpha3.HTTPMatchRequest `json:"match,omitempty"`
|
||||
Rewrite *istiov1alpha3.HTTPRewrite `json:"rewrite,omitempty"`
|
||||
Timeout string `json:"timeout,omitempty"`
|
||||
Retries *istiov1alpha3.HTTPRetry `json:"retries,omitempty"`
|
||||
AppendHeaders map[string]string `json:"appendHeaders,omitempty"`
|
||||
Port int32 `json:"port"`
|
||||
Gateways []string `json:"gateways"`
|
||||
Hosts []string `json:"hosts"`
|
||||
Match []istiov1alpha3.HTTPMatchRequest `json:"match,omitempty"`
|
||||
Rewrite *istiov1alpha3.HTTPRewrite `json:"rewrite,omitempty"`
|
||||
Timeout string `json:"timeout,omitempty"`
|
||||
Retries *istiov1alpha3.HTTPRetry `json:"retries,omitempty"`
|
||||
Headers *istiov1alpha3.Headers `json:"headers,omitempty"`
|
||||
}
|
||||
|
||||
// CanaryAnalysis is used to describe how the analysis should be done
|
||||
|
||||
@@ -161,12 +161,10 @@ func (in *CanaryService) DeepCopyInto(out *CanaryService) {
|
||||
*out = new(istiov1alpha3.HTTPRetry)
|
||||
**out = **in
|
||||
}
|
||||
if in.AppendHeaders != nil {
|
||||
in, out := &in.AppendHeaders, &out.AppendHeaders
|
||||
*out = make(map[string]string, len(*in))
|
||||
for key, val := range *in {
|
||||
(*out)[key] = val
|
||||
}
|
||||
if in.Headers != nil {
|
||||
in, out := &in.Headers, &out.Headers
|
||||
*out = new(istiov1alpha3.Headers)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -325,6 +325,9 @@ type HTTPRoute struct {
|
||||
// destination.
|
||||
Mirror *Destination `json:"mirror,omitempty"`
|
||||
|
||||
// Cross-Origin Resource Sharing policy (CORS). Refer to
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
|
||||
// for further details about cross origin resource sharing.
|
||||
CorsPolicy *CorsPolicy `json:"CorsPolicy,omitempty"`
|
||||
|
||||
// Additional HTTP headers to add before forwarding a request to the
|
||||
@@ -333,6 +336,33 @@ type HTTPRoute struct {
|
||||
|
||||
// Http headers to remove before returning the response to the caller
|
||||
RemoveResponseHeaders map[string]string `json:"removeResponseHeaders,omitempty"`
|
||||
|
||||
// Header manipulation rules
|
||||
Headers *Headers `json:"headers,omitempty"`
|
||||
}
|
||||
|
||||
// Header manipulation rules
|
||||
type Headers struct {
|
||||
// Header manipulation rules to apply before forwarding a request
|
||||
// to the destination service
|
||||
Request *HeaderOperations `json:"request,omitempty"`
|
||||
|
||||
// Header manipulation rules to apply before returning a response
|
||||
// to the caller
|
||||
Response *HeaderOperations `json:"response,omitempty"`
|
||||
}
|
||||
|
||||
// HeaderOperations Describes the header manipulations to apply
|
||||
type HeaderOperations struct {
|
||||
// Overwrite the headers specified by key with the given values
|
||||
Set map[string]string `json:"set"`
|
||||
|
||||
// Append the given values to the headers specified by keys
|
||||
// (will create a comma-separated list of values)
|
||||
Add map[string]string `json:"add"`
|
||||
|
||||
// Remove the specified headers
|
||||
Remove []string `json:"remove"`
|
||||
}
|
||||
|
||||
// HttpMatchRequest specifies a set of criterion to be met in order for the
|
||||
|
||||
@@ -283,6 +283,11 @@ func (in *HTTPRoute) DeepCopyInto(out *HTTPRoute) {
|
||||
(*out)[key] = val
|
||||
}
|
||||
}
|
||||
if in.Headers != nil {
|
||||
in, out := &in.Headers, &out.Headers
|
||||
*out = new(Headers)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -296,6 +301,67 @@ func (in *HTTPRoute) DeepCopy() *HTTPRoute {
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *HeaderOperations) DeepCopyInto(out *HeaderOperations) {
|
||||
*out = *in
|
||||
if in.Set != nil {
|
||||
in, out := &in.Set, &out.Set
|
||||
*out = make(map[string]string, len(*in))
|
||||
for key, val := range *in {
|
||||
(*out)[key] = val
|
||||
}
|
||||
}
|
||||
if in.Add != nil {
|
||||
in, out := &in.Add, &out.Add
|
||||
*out = make(map[string]string, len(*in))
|
||||
for key, val := range *in {
|
||||
(*out)[key] = val
|
||||
}
|
||||
}
|
||||
if in.Remove != nil {
|
||||
in, out := &in.Remove, &out.Remove
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HeaderOperations.
|
||||
func (in *HeaderOperations) DeepCopy() *HeaderOperations {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(HeaderOperations)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Headers) DeepCopyInto(out *Headers) {
|
||||
*out = *in
|
||||
if in.Request != nil {
|
||||
in, out := &in.Request, &out.Request
|
||||
*out = new(HeaderOperations)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.Response != nil {
|
||||
in, out := &in.Response, &out.Response
|
||||
*out = new(HeaderOperations)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Headers.
|
||||
func (in *Headers) DeepCopy() *Headers {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(Headers)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *InjectAbort) DeepCopyInto(out *InjectAbort) {
|
||||
*out = *in
|
||||
|
||||
@@ -24,12 +24,12 @@ type IstioRouter struct {
|
||||
}
|
||||
|
||||
// Sync creates or updates the Istio virtual service
|
||||
func (ir *IstioRouter) Sync(cd *flaggerv1.Canary) error {
|
||||
targetName := cd.Spec.TargetRef.Name
|
||||
func (ir *IstioRouter) Sync(canary *flaggerv1.Canary) error {
|
||||
targetName := canary.Spec.TargetRef.Name
|
||||
primaryName := fmt.Sprintf("%s-primary", targetName)
|
||||
|
||||
// set hosts and add the ClusterIP service host if it doesn't exists
|
||||
hosts := cd.Spec.Service.Hosts
|
||||
hosts := canary.Spec.Service.Hosts
|
||||
var hasServiceHost bool
|
||||
for _, h := range hosts {
|
||||
if h == targetName {
|
||||
@@ -42,7 +42,7 @@ func (ir *IstioRouter) Sync(cd *flaggerv1.Canary) error {
|
||||
}
|
||||
|
||||
// set gateways and add the mesh gateway if it doesn't exists
|
||||
gateways := cd.Spec.Service.Gateways
|
||||
gateways := canary.Spec.Service.Gateways
|
||||
var hasMeshGateway bool
|
||||
for _, g := range gateways {
|
||||
if g == "mesh" {
|
||||
@@ -60,7 +60,7 @@ func (ir *IstioRouter) Sync(cd *flaggerv1.Canary) error {
|
||||
Destination: istiov1alpha3.Destination{
|
||||
Host: primaryName,
|
||||
Port: istiov1alpha3.PortSelector{
|
||||
Number: uint32(cd.Spec.Service.Port),
|
||||
Number: uint32(canary.Spec.Service.Port),
|
||||
},
|
||||
},
|
||||
Weight: 100,
|
||||
@@ -69,36 +69,37 @@ func (ir *IstioRouter) Sync(cd *flaggerv1.Canary) error {
|
||||
Destination: istiov1alpha3.Destination{
|
||||
Host: fmt.Sprintf("%s-canary", targetName),
|
||||
Port: istiov1alpha3.PortSelector{
|
||||
Number: uint32(cd.Spec.Service.Port),
|
||||
Number: uint32(canary.Spec.Service.Port),
|
||||
},
|
||||
},
|
||||
Weight: 0,
|
||||
},
|
||||
}
|
||||
|
||||
newSpec := istiov1alpha3.VirtualServiceSpec{
|
||||
Hosts: hosts,
|
||||
Gateways: gateways,
|
||||
Http: []istiov1alpha3.HTTPRoute{
|
||||
{
|
||||
Match: cd.Spec.Service.Match,
|
||||
Rewrite: cd.Spec.Service.Rewrite,
|
||||
Timeout: cd.Spec.Service.Timeout,
|
||||
Retries: cd.Spec.Service.Retries,
|
||||
AppendHeaders: cd.Spec.Service.AppendHeaders,
|
||||
Match: canary.Spec.Service.Match,
|
||||
Rewrite: canary.Spec.Service.Rewrite,
|
||||
Timeout: canary.Spec.Service.Timeout,
|
||||
Retries: canary.Spec.Service.Retries,
|
||||
AppendHeaders: addHeaders(canary),
|
||||
Route: route,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
virtualService, err := ir.istioClient.NetworkingV1alpha3().VirtualServices(cd.Namespace).Get(targetName, metav1.GetOptions{})
|
||||
virtualService, err := ir.istioClient.NetworkingV1alpha3().VirtualServices(canary.Namespace).Get(targetName, metav1.GetOptions{})
|
||||
// insert
|
||||
if errors.IsNotFound(err) {
|
||||
virtualService = &istiov1alpha3.VirtualService{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: targetName,
|
||||
Namespace: cd.Namespace,
|
||||
Namespace: canary.Namespace,
|
||||
OwnerReferences: []metav1.OwnerReference{
|
||||
*metav1.NewControllerRef(cd, schema.GroupVersionKind{
|
||||
*metav1.NewControllerRef(canary, schema.GroupVersionKind{
|
||||
Group: flaggerv1.SchemeGroupVersion.Group,
|
||||
Version: flaggerv1.SchemeGroupVersion.Version,
|
||||
Kind: flaggerv1.CanaryKind,
|
||||
@@ -107,17 +108,17 @@ func (ir *IstioRouter) Sync(cd *flaggerv1.Canary) error {
|
||||
},
|
||||
Spec: newSpec,
|
||||
}
|
||||
_, err = ir.istioClient.NetworkingV1alpha3().VirtualServices(cd.Namespace).Create(virtualService)
|
||||
_, err = ir.istioClient.NetworkingV1alpha3().VirtualServices(canary.Namespace).Create(virtualService)
|
||||
if err != nil {
|
||||
return fmt.Errorf("VirtualService %s.%s create error %v", targetName, cd.Namespace, err)
|
||||
return fmt.Errorf("VirtualService %s.%s create error %v", targetName, canary.Namespace, err)
|
||||
}
|
||||
ir.logger.With("canary", fmt.Sprintf("%s.%s", cd.Name, cd.Namespace)).
|
||||
Infof("VirtualService %s.%s created", virtualService.GetName(), cd.Namespace)
|
||||
ir.logger.With("canary", fmt.Sprintf("%s.%s", canary.Name, canary.Namespace)).
|
||||
Infof("VirtualService %s.%s created", virtualService.GetName(), canary.Namespace)
|
||||
return nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("VirtualService %s.%s query error %v", targetName, cd.Namespace, err)
|
||||
return fmt.Errorf("VirtualService %s.%s query error %v", targetName, canary.Namespace, err)
|
||||
}
|
||||
|
||||
// update service but keep the original destination weights
|
||||
@@ -128,12 +129,12 @@ func (ir *IstioRouter) Sync(cd *flaggerv1.Canary) error {
|
||||
if len(virtualService.Spec.Http) > 0 {
|
||||
vtClone.Spec.Http[0].Route = virtualService.Spec.Http[0].Route
|
||||
}
|
||||
_, err = ir.istioClient.NetworkingV1alpha3().VirtualServices(cd.Namespace).Update(vtClone)
|
||||
_, err = ir.istioClient.NetworkingV1alpha3().VirtualServices(canary.Namespace).Update(vtClone)
|
||||
if err != nil {
|
||||
return fmt.Errorf("VirtualService %s.%s update error %v", targetName, cd.Namespace, err)
|
||||
return fmt.Errorf("VirtualService %s.%s update error %v", targetName, canary.Namespace, err)
|
||||
}
|
||||
ir.logger.With("canary", fmt.Sprintf("%s.%s", cd.Name, cd.Namespace)).
|
||||
Infof("VirtualService %s.%s updated", virtualService.GetName(), cd.Namespace)
|
||||
ir.logger.With("canary", fmt.Sprintf("%s.%s", canary.Name, canary.Namespace)).
|
||||
Infof("VirtualService %s.%s updated", virtualService.GetName(), canary.Namespace)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,7 +201,7 @@ func (ir *IstioRouter) SetRoutes(
|
||||
Rewrite: canary.Spec.Service.Rewrite,
|
||||
Timeout: canary.Spec.Service.Timeout,
|
||||
Retries: canary.Spec.Service.Retries,
|
||||
AppendHeaders: canary.Spec.Service.AppendHeaders,
|
||||
AppendHeaders: addHeaders(canary),
|
||||
Route: []istiov1alpha3.DestinationWeight{
|
||||
{
|
||||
Destination: istiov1alpha3.Destination{
|
||||
@@ -231,3 +232,15 @@ func (ir *IstioRouter) SetRoutes(
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// addHeaders applies headers before forwarding a request to the destination service
|
||||
// compatible with Istio 1.0.x and 1.1.0
|
||||
func addHeaders(canary *flaggerv1.Canary) (headers map[string]string) {
|
||||
if canary.Spec.Service.Headers != nil &&
|
||||
canary.Spec.Service.Headers.Request != nil &&
|
||||
len(canary.Spec.Service.Headers.Request.Add) > 0 {
|
||||
headers = canary.Spec.Service.Headers.Request.Add
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -177,3 +177,33 @@ func TestIstioRouter_GetRoutes(t *testing.T) {
|
||||
t.Errorf("Got canary weight %v wanted %v", c, 0)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIstioRouter_HTTPRequestHeaders(t *testing.T) {
|
||||
mocks := setupfakeClients()
|
||||
router := &IstioRouter{
|
||||
logger: mocks.logger,
|
||||
flaggerClient: mocks.flaggerClient,
|
||||
istioClient: mocks.istioClient,
|
||||
kubeClient: mocks.kubeClient,
|
||||
}
|
||||
|
||||
err := router.Sync(mocks.canary)
|
||||
if err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
|
||||
// test insert
|
||||
vs, err := mocks.istioClient.NetworkingV1alpha3().VirtualServices("default").Get("podinfo", metav1.GetOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err.Error())
|
||||
}
|
||||
|
||||
if len(vs.Spec.Http) != 1 {
|
||||
t.Fatalf("Got HTTPRoute %v wanted %v", len(vs.Spec.Http), 1)
|
||||
}
|
||||
|
||||
timeout := vs.Spec.Http[0].AppendHeaders["x-envoy-upstream-rq-timeout-ms"]
|
||||
if timeout != "15000" {
|
||||
t.Errorf("Got timeout %v wanted %v", timeout, "15000")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package router
|
||||
|
||||
import (
|
||||
"github.com/stefanprodan/flagger/pkg/apis/flagger/v1alpha3"
|
||||
istiov1alpha3 "github.com/stefanprodan/flagger/pkg/apis/istio/v1alpha3"
|
||||
clientset "github.com/stefanprodan/flagger/pkg/client/clientset/versioned"
|
||||
fakeFlagger "github.com/stefanprodan/flagger/pkg/client/clientset/versioned/fake"
|
||||
"github.com/stefanprodan/flagger/pkg/logging"
|
||||
@@ -57,6 +58,13 @@ func newMockCanary() *v1alpha3.Canary {
|
||||
},
|
||||
Service: v1alpha3.CanaryService{
|
||||
Port: 9898,
|
||||
Headers: &istiov1alpha3.Headers{
|
||||
Request: &istiov1alpha3.HeaderOperations{
|
||||
Add: map[string]string{
|
||||
"x-envoy-upstream-rq-timeout-ms": "15000",
|
||||
},
|
||||
},
|
||||
},
|
||||
}, CanaryAnalysis: v1alpha3.CanaryAnalysis{
|
||||
Threshold: 10,
|
||||
StepWeight: 10,
|
||||
|
||||
Reference in New Issue
Block a user