Commit b113c219 authored by Geoff Simmons's avatar Geoff Simmons

Add the req-disposition field to VarnishConfig.

Ref #24
parent ddcfac81
......@@ -278,6 +278,94 @@ spec:
type: boolean
word-boundary:
type: boolean
req-disposition:
type: array
minItems: 1
items:
type: object
required:
- conditions
- disposition
properties:
conditions:
type: array
minItems: 1
items:
type: object
required:
- comparand
properties:
comparand:
type: string
pattern: "^req\\.(url|method|proto|esi_level|restarts|http\\.[a-zA-Z0-9!#$%&'*+.^_`|~-]+)$"
compare:
enum:
- equal
- not-equal
- match
- not-match
- prefix
- not-prefix
- exists
- not-exists
- greater
- greater-equal
- less
- less-equal
type: string
values:
type: array
minItems: 1
items:
type: string
count:
type: integer
minimum: 0
match-flags:
type: object
properties:
max-mem:
type: integer
min: 0
anchor:
type: string
enum:
- none
- start
- both
utf8:
type: boolean
posix-syntax:
type: boolean
longest-match:
type: boolean
literal:
type: boolean
never-capture:
type: boolean
case-sensitive:
type: boolean
perl-classes:
type: boolean
word-boundary:
type: boolean
disposition:
type: object
required:
- action
properties:
action:
enum:
- hash
- pass
- pipe
- purge
- synth
type: string
status:
type: integer
minimum: 200
maximum: 599
status:
acceptedNames:
kind: VarnishConfig
......
# Configuration for disposition of client requests that permits cache
# lookups for requests with Cookie or Authorization headers, and
# handles some requests differently from vcl_recv in builtin.vcl.
apiVersion: "ingress.varnish-cache.org/v1alpha1"
kind: VarnishConfig
metadata:
name: alt-recv-cfg
spec:
# The services array is required and must have at least one element.
# Lists the Service names of Varnish services in the same namespace
# to which this config is to be applied.
services:
- varnish-ingress
req-disposition:
- conditions:
- comparand: req.http.Host
compare: not-exists
- comparand: req.esi_level
count: 0
- comparand: req.proto
compare: prefix
values:
- HTTP/1.1
match-flags:
case-insensitive: true
disposition:
action: synth
status: 400
- conditions:
- comparand: req.method
compare: equal
values:
- CONNECT
disposition:
action: pipe
- conditions:
- comparand: req.method
compare: not-equal
values:
- GET
- HEAD
- PUT
- POST
- TRACE
- OPTIONS
- DELETE
- PATCH
disposition:
action: synth
status: 405
- conditions:
- comparand: req.method
compare: not-equal
values:
- GET
- HEAD
disposition:
action: pass
# Configuration for disposition of client requests that re-implements
# vcl_recv in builtin.vcl.
apiVersion: "ingress.varnish-cache.org/v1alpha1"
kind: VarnishConfig
metadata:
name: builtin-recv-cfg
spec:
# The services array is required and must have at least one element.
# Lists the Service names of Varnish services in the same namespace
# to which this config is to be applied.
services:
- varnish-ingress
req-disposition:
- conditions:
- comparand: req.method
compare: equal
values:
- PRI
disposition:
action: synth
status: 405
- conditions:
- comparand: req.http.Host
compare: not-exists
- comparand: req.esi_level
count: 0
- comparand: req.proto
compare: prefix
values:
- HTTP/1.1
match-flags:
case-sensitive: false
disposition:
action: synth
status: 400
- conditions:
- comparand: req.method
compare: not-equal
values:
- GET
- HEAD
- PUT
- POST
- TRACE
- OPTIONS
- DELETE
- PATCH
disposition:
action: pipe
- conditions:
- comparand: req.method
compare: not-equal
values:
- GET
- HEAD
disposition:
action: pass
- conditions:
- comparand: req.http.Cookie
compare: exists
disposition:
action: pass
- conditions:
- comparand: req.http.Authorization
compare: exists
disposition:
action: pass
# Configuration for disposition of client requests that permits cache
# lookups for requests with Cookie or Authorization headers, and
# defines URL path patterns for which cache lookups are invoked or
# bypassed.
apiVersion: "ingress.varnish-cache.org/v1alpha1"
kind: VarnishConfig
metadata:
name: cacheability-cfg
spec:
# The services array is required and must have at least one element.
# Lists the Service names of Varnish services in the same namespace
# to which this config is to be applied.
services:
- varnish-ingress
req-disposition:
- conditions:
- comparand: req.http.Host
compare: not-exists
- comparand: req.esi_level
count: 0
- comparand: req.proto
compare: prefix
values:
- HTTP/1.1
match-flags:
case-insensitive: true
disposition:
action: synth
status: 400
- conditions:
- comparand: req.method
compare: not-equal
values:
- GET
- HEAD
- PUT
- POST
- TRACE
- OPTIONS
- DELETE
- PATCH
disposition:
action: synth
status: 405
- conditions:
- comparand: req.method
compare: not-equal
values:
- GET
- HEAD
disposition:
action: pass
- conditions:
- comparand: req.url
compare: match
values:
- \.png$
- \.jpe?g$
- \.css$
- \.js$
disposition:
action: hash
- conditions:
- comparand: req.url
compare: prefix
values:
- /interactive/
- /basket/
- /personal/
- /dynamic/
disposition:
action: pass
# Configuration for disposition of client requests that permits cache
# lookups for requests with Cookie or Authorization headers, and
# allows use of the PURGE request method to purge cache objects.
apiVersion: "ingress.varnish-cache.org/v1alpha1"
kind: VarnishConfig
metadata:
name: purge-cfg
spec:
# The services array is required and must have at least one element.
# Lists the Service names of Varnish services in the same namespace
# to which this config is to be applied.
services:
- varnish-ingress
req-disposition:
- conditions:
- comparand: req.http.Host
compare: not-exists
- comparand: req.esi_level
count: 0
- comparand: req.proto
compare: prefix
values:
- HTTP/1.1
match-flags:
case-insensitive: true
disposition:
action: synth
status: 400
- conditions:
- comparand: req.method
compare: equal
values:
- PURGE
disposition:
action: purge
- conditions:
- comparand: req.method
compare: not-equal
values:
- GET
- HEAD
- PUT
- POST
- TRACE
- OPTIONS
- DELETE
- PATCH
disposition:
action: synth
status: 405
- conditions:
- comparand: req.method
compare: not-equal
values:
- GET
- HEAD
disposition:
action: pass
# Configuration for disposition of client requests that permits cache
# lookups for requests with Cookie or Authorization headers, and
# defines a whitelist for requests based on URL path prefixes.
apiVersion: "ingress.varnish-cache.org/v1alpha1"
kind: VarnishConfig
metadata:
name: url-whitelist-cfg
spec:
# The services array is required and must have at least one element.
# Lists the Service names of Varnish services in the same namespace
# to which this config is to be applied.
services:
- varnish-ingress
req-disposition:
- conditions:
- comparand: req.http.Host
compare: not-exists
- comparand: req.esi_level
count: 0
- comparand: req.proto
compare: prefix
values:
- HTTP/1.1
match-flags:
case-insensitive: true
disposition:
action: synth
status: 400
- conditions:
- comparand: req.method
compare: not-equal
values:
- GET
- HEAD
- PUT
- POST
- TRACE
- OPTIONS
- DELETE
- PATCH
disposition:
action: synth
status: 405
- conditions:
- comparand: req.method
compare: not-equal
values:
- GET
- HEAD
disposition:
action: pass
- conditions:
- comparand: req.url
compare: not-prefix
values:
- /tea/
- /coffee/
disposition:
action: synth
status: 403
......@@ -9,6 +9,7 @@ require (
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b
github.com/golang/groupcache v0.0.0-20181024230925-c65c006176ff // indirect
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c // indirect
github.com/google/go-cmp v0.2.0
github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf // indirect
github.com/googleapis/gnostic v0.2.0 // indirect
github.com/gregjones/httpcache v0.0.0-20181110185634-c63ab54fda8f // indirect
......
......@@ -32,6 +32,8 @@ github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c h1:964Od4U6p2jUkFxvCydnIczKteheJEzHRToSGK3Bnlw=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf h1:+RRA9JqSOZFfKrOeqr2z77+8R2RKyh8PG66dcu1V0ck=
github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI=
github.com/googleapis/gnostic v0.2.0 h1:l6N3VoaVzTncYYW+9yOz2LJJammFZGBO13sqgEhpy9g=
......
......@@ -49,12 +49,13 @@ type VarnishConfig struct {
// VarnishConfigSpec corresponds to the spec section of a
// VarnishConfig Custom Resource.
type VarnishConfigSpec struct {
Services []string `json:"services,omitempty"`
SelfSharding *SelfShardSpec `json:"self-sharding,omitempty"`
VCL string `json:"vcl,omitempty"`
Auth []AuthSpec `json:"auth,omitempty"`
ACLs []ACLSpec `json:"acl,omitempty"`
Rewrites []RewriteSpec `json:"rewrites,omitempty"`
Services []string `json:"services,omitempty"`
SelfSharding *SelfShardSpec `json:"self-sharding,omitempty"`
VCL string `json:"vcl,omitempty"`
Auth []AuthSpec `json:"auth,omitempty"`
ACLs []ACLSpec `json:"acl,omitempty"`
Rewrites []RewriteSpec `json:"rewrites,omitempty"`
ReqDispositions []RequestDispSpec `json:"req-disposition,omitempty"`
}
// SelfShardSpec specifies self-sharding in a Varnish cluster.
......@@ -333,6 +334,91 @@ type RewriteSpec struct {
Select SelectType `json:"select,omitempty"`
}
// ReqCompareType classifies comparison operations performed for the
// Conditions in a request disposition specification.
type ReqCompareType string
const (
// ReqEqual specifies equality -- string equality, numeric
// equality, or membership in a set of fixed strings.
ReqEqual ReqCompareType = "equal"
// ReqNotEqual specifies non-equality.
ReqNotEqual = "not-equal"
// ReqMatch specifies a regular expression match.
ReqMatch = "match"
// ReqNotMatch specifies a regular expression non-match.
ReqNotMatch = "not-match"
// ReqPrefix specifies that a string has a prefix in a set of
// fixed strings.
ReqPrefix = "prefix"
// ReqNotPrefix specifies that a string does not have a prefix
// in a set of fixed strings.
ReqNotPrefix = "not-prefix"
// Exists specifies that a request header exists.
Exists = "exists"
// NotExists specifies that a request header does not exist.
NotExists = "not-exists"
// Greater specifies the > relation between a VCL variable
// with a numeric value and a constant.
Greater = "greater"
// GreaterEqual specifies the >= relation for a numeric VCL
// variable and a constant.
GreaterEqual = "greater-equal"
// Less specifies the < relation for a numeric VCL variable
// and a constant.
Less = "less"
// LessEqual specifies the <= relation for a numeric VCL
// variable and a constant.
LessEqual = "less-equal"
)
// ReqCondition specifies (one of) the conditions that must be true if
// a client request disposition is to be executed.
type ReqCondition struct {
Values []string `json:"values,omitempty"`
MatchFlags *MatchFlagsType `json:"match-flags,omitempty"`
Count *int64 `json:"count,omitempty"`
Comparand string `json:"comparand"`
Compare ReqCompareType `json:"compare,omitempty"`
}
// RecvReturn is a name for the disposition of a client request.
// See: https://varnish-cache.org/docs/6.1/reference/states.html
type RecvReturn string
const (
// RecvHash to invoke cache lookup.
RecvHash RecvReturn = "hash"
// RecvPass to bypass cache lookup.
RecvPass = "pass"
// RecvPipe for pipe mode -- Varnish passes data between
// client and backend with no further intervention.
RecvPipe = "pipe"
// RecvPurge to purge a cache object.
// See: https://varnish-cache.org/docs/6.1/users-guide/purging.html?highlight=purge#http-purging
RecvPurge = "purge"
// RecvSynth to generate a synthetic response with a given
// HTTP response status.
RecvSynth = "synth"
)
// DispositionSpec specifies the disposition of a client request when
// associated Conditions are met.
//
// Status is the HTTP response status to set when Action is RecvSynth;
// ignored for other values of Action.
type DispositionSpec struct {
Action RecvReturn `json:"action"`
Status *int64 `json:"status,omitempty"`
}
// RequestDispSpec specifies the disposition of a client request when
// all of the Conditions are met.
type RequestDispSpec struct {
Conditions []ReqCondition `json:"conditions"`
Disposition DispositionSpec `json:"disposition"`
}
// VarnishConfigStatus is the status for a VarnishConfig resource
// type VarnishConfigStatus struct {
// AvailableReplicas int32 `json:"availableReplicas"`
......
......@@ -594,6 +594,37 @@ func (worker *NamespaceWorker) configACL(spec *vcl.Spec,
return nil
}
func configMatchFlags(flags vcr_v1alpha1.MatchFlagsType) vcl.MatchFlagsType {
vclFlags := vcl.MatchFlagsType{
UTF8: flags.UTF8,
PosixSyntax: flags.PosixSyntax,
LongestMatch: flags.LongestMatch,
Literal: flags.Literal,
NeverCapture: flags.NeverCapture,
PerlClasses: flags.PerlClasses,
WordBoundary: flags.WordBoundary,
}
if flags.MaxMem != nil && *flags.MaxMem != 0 {
vclFlags.MaxMem = *flags.MaxMem
}
if flags.CaseSensitive == nil {
vclFlags.CaseSensitive = true
} else {
vclFlags.CaseSensitive = *flags.CaseSensitive
}
switch flags.Anchor {
case vcr_v1alpha1.None:
vclFlags.Anchor = vcl.None
case vcr_v1alpha1.Start:
vclFlags.Anchor = vcl.Start
case vcr_v1alpha1.Both:
vclFlags.Anchor = vcl.Both
default:
vclFlags.Anchor = vcl.None
}
return vclFlags
}
func (worker *NamespaceWorker) configRewrites(spec *vcl.Spec,
vcfg *vcr_v1alpha1.VarnishConfig) error {
......@@ -715,40 +746,95 @@ func (worker *NamespaceWorker) configRewrites(spec *vcl.Spec,
}
if rw.MatchFlags != nil {
vclRw.MatchFlags = vcl.MatchFlagsType{
UTF8: rw.MatchFlags.UTF8,
PosixSyntax: rw.MatchFlags.PosixSyntax,
LongestMatch: rw.MatchFlags.LongestMatch,
Literal: rw.MatchFlags.Literal,
NeverCapture: rw.MatchFlags.NeverCapture,
PerlClasses: rw.MatchFlags.PerlClasses,
WordBoundary: rw.MatchFlags.WordBoundary,
}
if rw.MatchFlags.MaxMem != nil &&
*rw.MatchFlags.MaxMem != 0 {
vclRw.MatchFlags = configMatchFlags(*rw.MatchFlags)
} else {
vclRw.MatchFlags.CaseSensitive = true
}
spec.Rewrites[i] = vclRw
}
return nil
}
vclRw.MatchFlags.MaxMem = *rw.MatchFlags.MaxMem
func (worker *NamespaceWorker) configReqDisps(spec *vcl.Spec,
reqDisps []vcr_v1alpha1.RequestDispSpec, kind, namespace, name string) {
if len(reqDisps) == 0 {
worker.log.Infof("No request disposition specs found for %s "+
"%s/%s", kind, namespace, name)
return
}
worker.log.Infof("Configuring request dispositions for %s %s/%s",
kind, namespace, name)
spec.Dispositions = make([]vcl.DispositionSpec, len(reqDisps))
for i, disp := range reqDisps {
worker.log.Tracef("ReqDisposition: %+v", disp)
vclDisp := vcl.DispositionSpec{
Conditions: make([]vcl.Condition, len(disp.Conditions)),
}
for j, cond := range disp.Conditions {
vclCond := vcl.Condition{
Comparand: cond.Comparand,
}
if rw.MatchFlags.CaseSensitive == nil {
vclRw.MatchFlags.CaseSensitive = true
} else {
vclRw.MatchFlags.CaseSensitive =
*rw.MatchFlags.CaseSensitive
if len(cond.Values) > 0 {
vclCond.Values = make([]string, len(cond.Values))
copy(vclCond.Values, cond.Values)
}
if cond.Count != nil {
count := uint(*cond.Count)
vclCond.Count = &count
}
switch rw.MatchFlags.Anchor {
case vcr_v1alpha1.None:
vclRw.MatchFlags.Anchor = vcl.None
case vcr_v1alpha1.Start:
vclRw.MatchFlags.Anchor = vcl.Start
case vcr_v1alpha1.Both:
vclRw.MatchFlags.Anchor = vcl.Both
switch cond.Compare {
case vcr_v1alpha1.ReqEqual:
vclCond.Compare = vcl.ReqEqual
vclCond.Negate = false
case vcr_v1alpha1.ReqNotEqual:
vclCond.Compare = vcl.ReqEqual
vclCond.Negate = true
case vcr_v1alpha1.ReqMatch:
vclCond.Compare = vcl.ReqMatch
vclCond.Negate = false
case vcr_v1alpha1.ReqNotMatch:
vclCond.Compare = vcl.ReqMatch
vclCond.Negate = true
case vcr_v1alpha1.ReqPrefix:
vclCond.Compare = vcl.ReqPrefix
vclCond.Negate = false
case vcr_v1alpha1.ReqNotPrefix:
vclCond.Compare = vcl.ReqPrefix
vclCond.Negate = true
case vcr_v1alpha1.Exists:
vclCond.Compare = vcl.Exists
vclCond.Negate = false
case vcr_v1alpha1.NotExists:
vclCond.Compare = vcl.Exists
vclCond.Negate = true
case vcr_v1alpha1.Greater:
vclCond.Compare = vcl.Greater
case vcr_v1alpha1.GreaterEqual:
vclCond.Compare = vcl.GreaterEqual
case vcr_v1alpha1.Less:
vclCond.Compare = vcl.Less
case vcr_v1alpha1.LessEqual:
vclCond.Compare = vcl.LessEqual
default:
vclRw.MatchFlags.Anchor = vcl.None
vclCond.Compare = vcl.ReqEqual
}
if cond.MatchFlags != nil {
vclCond.MatchFlags = configMatchFlags(
*cond.MatchFlags)
} else {
vclCond.MatchFlags.CaseSensitive = true
}
vclDisp.Conditions[j] = vclCond
}
spec.Rewrites[i] = vclRw
vclDisp.Disposition.Action = vcl.RecvReturn(
disp.Disposition.Action)
if disp.Disposition.Action == vcr_v1alpha1.RecvSynth {
vclDisp.Disposition.Status = uint16(
*disp.Disposition.Status)
}
spec.Dispositions[i] = vclDisp
}
return nil
}
func (worker *NamespaceWorker) addOrUpdateIng(ing *extensions.Ingress) error {
......@@ -823,6 +909,8 @@ func (worker *NamespaceWorker) addOrUpdateIng(ing *extensions.Ingress) error {
if err = worker.configRewrites(&vclSpec, vcfg); err != nil {
return err
}
worker.configReqDisps(&vclSpec, vcfg.Spec.ReqDispositions,
vcfg.Kind, vcfg.Namespace, vcfg.Name)
vclSpec.VCL = vcfg.Spec.VCL
} else {
worker.log.Infof("Found no VarnishConfigs for Varnish Service "+
......
This diff is collapsed.
......@@ -161,6 +161,77 @@ func validateRewrites(rewrites []vcr_v1alpha1.RewriteSpec) error {
return nil
}
// XXX validating webhook should do this
func validateReqDisps(reqDisps []vcr_v1alpha1.RequestDispSpec) error {
for _, disp := range reqDisps {
if disp.Disposition.Action == vcr_v1alpha1.RecvSynth &&
disp.Disposition.Status == nil {
return fmt.Errorf("status not set for request " +
"disposition synth")
}
for _, cond := range disp.Conditions {
if len(cond.Values) == 0 && cond.Count == nil &&
cond.Compare != vcr_v1alpha1.Exists &&
cond.Compare != vcr_v1alpha1.NotExists {
return fmt.Errorf("no values or count set for "+
"request disposition condition "+
"(comparand %s)", cond.Comparand)
}
if len(cond.Values) != 0 && cond.Count != nil {
return fmt.Errorf("both values and count set "+
"for request disposition condition "+
"(comparand %s)", cond.Comparand)
}
if len(cond.Values) > 0 {
switch cond.Compare {
case vcr_v1alpha1.Greater,
vcr_v1alpha1.GreaterEqual,
vcr_v1alpha1.Less,
vcr_v1alpha1.LessEqual:
return fmt.Errorf("illegal compare "+
"(comparand %s, compare %s)",
cond.Comparand, cond.Compare)
}
switch cond.Comparand {
case "req.esi_level", "req.restarts":
return fmt.Errorf("illegal comparison "+
"(comparand %s, values %v)",
cond.Comparand, cond.Values)
}
}
if cond.Count != nil {
switch cond.Compare {
case vcr_v1alpha1.ReqMatch,
vcr_v1alpha1.ReqNotMatch,
vcr_v1alpha1.ReqPrefix,
vcr_v1alpha1.ReqNotPrefix,
vcr_v1alpha1.Exists,
vcr_v1alpha1.NotExists:
return fmt.Errorf("illegal compare "+
"(compare %s, count %d)",
cond.Compare, *cond.Count)
}
err := false
switch cond.Comparand {
case "req.url", "req.method",
"req.proto":
err = true
}
if strings.HasPrefix(cond.Comparand, "req.http") {
err = true
}
if err {
return fmt.Errorf("illegal comparison "+
"(comparand %s, count %d)",
cond.Comparand, *cond.Count)
}
}
}
}
return nil
}
func (worker *NamespaceWorker) syncVcfg(key string) error {
worker.log.Infof("Syncing VarnishConfig: %s/%s", worker.namespace, key)
vcfg, err := worker.vcfg.Get(key)
......@@ -189,6 +260,9 @@ func (worker *NamespaceWorker) syncVcfg(key string) error {
if err = validateRewrites(vcfg.Spec.Rewrites); err != nil {
return err
}
if err = validateReqDisps(vcfg.Spec.ReqDispositions); err != nil {
return err
}
return worker.enqueueIngsForVcfg(vcfg)
}
......
/*
* Copyright (c) 2019 UPLEX Nils Goroll Systemoptimierung
* All rights reserved
*
* Author: Geoffrey Simmons <geoffrey.simmons@uplex.de>
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL AUTHOR OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*/
package controller
import (
"testing"
vcr_v1alpha1 "code.uplex.de/uplex-varnish/k8s-ingress/pkg/apis/varnishingress/v1alpha1"
)
func TestValidateReqDisps(t *testing.T) {
zero := int64(0)
badDispSlice := [][]vcr_v1alpha1.RequestDispSpec{
{{
Disposition: vcr_v1alpha1.DispositionSpec{
Action: vcr_v1alpha1.RecvSynth,
},
}},
{{
Disposition: vcr_v1alpha1.DispositionSpec{
Action: vcr_v1alpha1.RecvHash,
},
Conditions: []vcr_v1alpha1.ReqCondition{{
Comparand: "req.url",
Compare: vcr_v1alpha1.ReqEqual,
}},
}},
{{
Disposition: vcr_v1alpha1.DispositionSpec{
Action: vcr_v1alpha1.RecvHash,
},
Conditions: []vcr_v1alpha1.ReqCondition{{
Comparand: "req.url",
Compare: vcr_v1alpha1.ReqEqual,
Count: &zero,
Values: []string{"/foo"},
}},
}},
{{
Disposition: vcr_v1alpha1.DispositionSpec{
Action: vcr_v1alpha1.RecvHash,
},
Conditions: []vcr_v1alpha1.ReqCondition{{
Comparand: "req.url",
Compare: vcr_v1alpha1.NotExists,
Count: &zero,
}},
}},
{{
Disposition: vcr_v1alpha1.DispositionSpec{
Action: vcr_v1alpha1.RecvHash,
},
Conditions: []vcr_v1alpha1.ReqCondition{{
Comparand: "req.url",
Compare: vcr_v1alpha1.LessEqual,
Values: []string{"/foo"},
}},
}},
{{
Disposition: vcr_v1alpha1.DispositionSpec{
Action: vcr_v1alpha1.RecvHash,
},
Conditions: []vcr_v1alpha1.ReqCondition{{
Comparand: "req.url",
Compare: vcr_v1alpha1.ReqEqual,
Count: &zero,
}},
}},
{{
Disposition: vcr_v1alpha1.DispositionSpec{
Action: vcr_v1alpha1.RecvHash,
},
Conditions: []vcr_v1alpha1.ReqCondition{{
Comparand: "req.http.Host",
Compare: vcr_v1alpha1.ReqEqual,
Count: &zero,
}},
}},
{{
Disposition: vcr_v1alpha1.DispositionSpec{
Action: vcr_v1alpha1.RecvHash,
},
Conditions: []vcr_v1alpha1.ReqCondition{{
Comparand: "req.restarts",
Compare: vcr_v1alpha1.ReqEqual,
Values: []string{"/foo"},
}},
}},
}
goodDispSlice := [][]vcr_v1alpha1.RequestDispSpec{
{{
Disposition: vcr_v1alpha1.DispositionSpec{
Action: vcr_v1alpha1.RecvPass,
},
Conditions: []vcr_v1alpha1.ReqCondition{{
Comparand: "req.http.Host",
Compare: vcr_v1alpha1.NotExists,
}},
}},
{{
Disposition: vcr_v1alpha1.DispositionSpec{
Action: vcr_v1alpha1.RecvPass,
},
Conditions: []vcr_v1alpha1.ReqCondition{{
Comparand: "req.http.Cookie",
Compare: vcr_v1alpha1.Exists,
}},
}},
}
for _, disps := range badDispSlice {
if err := validateReqDisps(disps); err == nil {
t.Errorf("validateReqDisps(%+v) expected error got=nil",
disps)
} else if testing.Verbose() {
t.Logf("validateReqDisps(%+v) returned as expected: %v",
disps, err)
}
}
for _, disps := range goodDispSlice {
if err := validateReqDisps(disps); err != nil {
t.Errorf("validateReqDisps(%+v) expected no error "+
"got='%+v'", disps, err)
}
}
}
import re2;
import selector;
{{range $didx, $d := .Dispositions -}}
{{range $cidx, $c := .Conditions -}}
{{if reqNeedsMatcher $c -}}
sub vcl_init {
new {{reqObj $didx $cidx}} = {{reqVMOD $c}}.set({{reqFlags $c}});
{{- range $val := $c.Values}}
{{reqObj $didx $cidx}}.add("{{$val}}");
{{- end}}
{{- if reqNeedsCompile $c}}
{{reqObj $didx $cidx}}.compile();
{{- end}}
}
{{end -}}
{{- end}}
{{- end}}
sub vcl_recv {
{{- range $didx, $d := .Dispositions}}
if (
{{- range $cidx, $cond := .Conditions}}
{{- if ne $cidx 0}} &&
{{end}}
{{- if $cond.Negate}}! {{end}}
{{- if reqNeedsMatcher $cond}}
{{- reqObj $didx $cidx}}.{{reqMatch $cond}}({{$cond.Comparand}})
{{- else if exists $cond.Compare}}
{{- $cond.Comparand}}
{{- else}}
{{- $cond.Comparand}} {{reqCmpRelation $cond}} {{value $cond}}
{{- end}}
{{- end -}}
) {
return (
{{- with .Disposition}}
{{- if eq .Action "synth"}}synth({{.Status}})
{{- else}}{{.Action}}
{{- end}}
{{- end -}}
);
}
{{- end}}
return (hash);
}
/*
* Copyright (c) 2019 UPLEX Nils Goroll Systemoptimierung
* All rights reserved
*
* Author: Geoffrey Simmons <geoffrey.simmons@uplex.de>
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL AUTHOR OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*/
package vcl
import "testing"
var zero uint
var builtinRecvSpec = Spec{
Dispositions: []DispositionSpec{
{
Conditions: []Condition{{
Comparand: "req.method",
Compare: ReqEqual,
Values: []string{"PRI"},
}},
Disposition: DispositionType{
Action: RecvSynth,
Status: 405,
},
},
{
Conditions: []Condition{
{
Comparand: "req.http.Host",
Compare: Exists,
Negate: true,
},
{
Comparand: "req.esi_level",
Compare: ReqEqual,
Count: &zero,
},
{
Comparand: "req.proto",
Compare: ReqPrefix,
Values: []string{"HTTP/1.1"},
MatchFlags: MatchFlagsType{
CaseSensitive: false,
},
},
},
Disposition: DispositionType{
Action: RecvSynth,
Status: 400,
},
},
{
Conditions: []Condition{{
Comparand: "req.method",
Compare: ReqEqual,
Negate: true,
Values: []string{
"GET",
"HEAD",
"PUT",
"POST",
"TRACE",
"OPTIONS",
"DELETE",
},
MatchFlags: MatchFlagsType{
CaseSensitive: true,
},
}},
Disposition: DispositionType{
Action: RecvPipe,
},
},
{
Conditions: []Condition{{
Comparand: "req.method",
Compare: ReqEqual,
Negate: true,
Values: []string{
"GET",
"HEAD",
},
MatchFlags: MatchFlagsType{
CaseSensitive: true,
},
}},
Disposition: DispositionType{
Action: RecvPass,
},
},
{
Conditions: []Condition{{
Comparand: "req.http.Cookie",
Compare: Exists,
}},
Disposition: DispositionType{
Action: RecvPass,
},
},
{
Conditions: []Condition{{
Comparand: "req.http.Authorization",
Compare: Exists,
}},
Disposition: DispositionType{
Action: RecvPass,
},
},
},
}
func TestReqDispBuiltinRecv(t *testing.T) {
gold := "recv_disp_builtin.golden"
testTemplate(t, reqDispTmpl, builtinRecvSpec, gold)
}
var pipeOnConnectSpec = Spec{
Dispositions: []DispositionSpec{{
Conditions: []Condition{{
Comparand: "req.method",
Compare: ReqEqual,
Values: []string{"CONNECT"},
}},
Disposition: DispositionType{
Action: RecvPipe,
},
}},
}
func TestReqDispPipeOnConnect(t *testing.T) {
gold := "recv_disp_pipe_on_connect.golden"
testTemplate(t, reqDispTmpl, pipeOnConnectSpec, gold)
}
var methodNotAllowedSpec = Spec{
Dispositions: []DispositionSpec{{
Conditions: []Condition{{
Comparand: "req.method",
Compare: ReqEqual,
Negate: true,
Values: []string{
"GET",
"HEAD",
"PUT",
"POST",
"TRACE",
"OPTIONS",
"DELETE",
},
MatchFlags: MatchFlagsType{
CaseSensitive: true,
},
}},
Disposition: DispositionType{
Action: RecvSynth,
Status: 405,
},
}},
}
func TestReqDispMethodNotAllowed(t *testing.T) {
gold := "recv_disp_method_not_allowed.golden"
testTemplate(t, reqDispTmpl, methodNotAllowedSpec, gold)
}
var urlWhitelistSpec = Spec{
Dispositions: []DispositionSpec{{
Conditions: []Condition{{
Comparand: "req.url",
Compare: ReqPrefix,
Negate: true,
Values: []string{
"/foo",
"/bar",
"/baz",
"/quux",
},
MatchFlags: MatchFlagsType{
CaseSensitive: true,
},
}},
Disposition: DispositionType{
Action: RecvSynth,
Status: 403,
},
}},
}
func TestReqDispURLWhitelist(t *testing.T) {
gold := "recv_disp_url_whitelist.golden"
testTemplate(t, reqDispTmpl, urlWhitelistSpec, gold)
}
var purgeMethodSpec = Spec{
Dispositions: []DispositionSpec{{
Conditions: []Condition{{
Comparand: "req.method",
Compare: ReqEqual,
Values: []string{"PURGE"},
}},
Disposition: DispositionType{
Action: RecvPurge,
},
}},
}
func TestReqDispPurgeMethod(t *testing.T) {
gold := "recv_disp_purge_method.golden"
testTemplate(t, reqDispTmpl, purgeMethodSpec, gold)
}
var cacheableSpec = Spec{
Dispositions: []DispositionSpec{{
Conditions: []Condition{{
Comparand: "req.url",
Compare: ReqMatch,
Values: []string{
`\.png$`,
`\.jpe?g$`,
`\.css$`,
`\.js$`,
},
MatchFlags: MatchFlagsType{
CaseSensitive: true,
},
}},
Disposition: DispositionType{
Action: RecvHash,
},
}},
}
func TestReqDispCacheable(t *testing.T) {
gold := "recv_disp_cacheable.golden"
testTemplate(t, reqDispTmpl, cacheableSpec, gold)
}
var nonCacheableSpec = Spec{
Dispositions: []DispositionSpec{{
Conditions: []Condition{{
Comparand: "req.url",
Compare: ReqPrefix,
Values: []string{
"/interactive/",
"/basket/",
"/personal",
"/dynamic/",
},
MatchFlags: MatchFlagsType{
CaseSensitive: true,
},
}},
Disposition: DispositionType{
Action: RecvPass,
},
}},
}
func TestReqDispNonCacheable(t *testing.T) {
gold := "recv_disp_non_cacheable.golden"
testTemplate(t, reqDispTmpl, nonCacheableSpec, gold)
}
// Code boilerplate for writing the golden file.
// import ioutils
// func TestRewriteXXX(t *testing.T) {
// gold := "rewrite_XXX.golden"
// var buf bytes.Buffer
// if err := rewriteTmpl.Execute(&buf, spec); err != nil {
// t.Fatal("Execute():", err)
// }
// if err := ioutil.WriteFile("testdata/"+gold, buf.Bytes(), 0644); err != nil {
// t.Fatal("WriteFile():", err)
// }
// if testing.Verbose() {
// t.Logf("Generated: %s", buf.String())
// }
// }
......@@ -32,12 +32,13 @@ import (
"bytes"
"io/ioutil"
"testing"
"text/template"
)
func testTemplate(t *testing.T, spec Spec, gold string) {
func testTemplate(t *testing.T, tmpl *template.Template, spec Spec, gold string) {
var buf bytes.Buffer
if err := rewriteTmpl.Execute(&buf, spec); err != nil {
if err := tmpl.Execute(&buf, spec); err != nil {
t.Fatal("Execute():", err)
}
......@@ -68,7 +69,7 @@ var replaceFromStringTest = Spec{
func TestReplaceFromString(t *testing.T) {
gold := "rewrite_replace_from_string.golden"
testTemplate(t, replaceFromStringTest, gold)
testTemplate(t, rewriteTmpl, replaceFromStringTest, gold)
}
var replaceFromSourceTest = Spec{
......@@ -84,7 +85,7 @@ var replaceFromSourceTest = Spec{
func TestReplaceFromSource(t *testing.T) {
gold := "rewrite_replace_from_source.golden"
testTemplate(t, replaceFromSourceTest, gold)
testTemplate(t, rewriteTmpl, replaceFromSourceTest, gold)
}
var replaceFromRewriteTest = Spec{
......@@ -111,7 +112,7 @@ var replaceFromRewriteTest = Spec{
func TestReplaceFromRewrite(t *testing.T) {
gold := "rewrite_replace_from_rewrite.golden"
testTemplate(t, replaceFromRewriteTest, gold)
testTemplate(t, rewriteTmpl, replaceFromRewriteTest, gold)
}
var rewriteSubTest = Spec{
......@@ -139,7 +140,7 @@ var rewriteSubTest = Spec{
func TestRewriteSub(t *testing.T) {
gold := "rewrite_sub.golden"
testTemplate(t, rewriteSubTest, gold)
testTemplate(t, rewriteTmpl, rewriteSubTest, gold)
}
var rewriteAppendTest = Spec{
......@@ -158,7 +159,7 @@ var rewriteAppendTest = Spec{
func TestRewriteAppend(t *testing.T) {
gold := "rewrite_append.golden"
testTemplate(t, rewriteAppendTest, gold)
testTemplate(t, rewriteTmpl, rewriteAppendTest, gold)
}
var rewritePrependTest = Spec{
......@@ -177,7 +178,7 @@ var rewritePrependTest = Spec{
func TestRewritePrepend(t *testing.T) {
gold := "rewrite_prepend.golden"
testTemplate(t, rewritePrependTest, gold)
testTemplate(t, rewriteTmpl, rewritePrependTest, gold)
}
var rewriteDeleteTest = Spec{
......@@ -193,7 +194,7 @@ var rewriteDeleteTest = Spec{
func TestRewriteDelete(t *testing.T) {
gold := "rewrite_delete.golden"
testTemplate(t, rewriteDeleteTest, gold)
testTemplate(t, rewriteTmpl, rewriteDeleteTest, gold)
}
var conditionalDeleteTest = Spec{
......@@ -216,7 +217,7 @@ var conditionalDeleteTest = Spec{
func TestConditionalDelete(t *testing.T) {
gold := "rewrite_conditional_delete.golden"
testTemplate(t, conditionalDeleteTest, gold)
testTemplate(t, rewriteTmpl, conditionalDeleteTest, gold)
}
var rewriteExtractTest = Spec{
......@@ -237,7 +238,7 @@ var rewriteExtractTest = Spec{
func TestRewriteExtract(t *testing.T) {
gold := "rewrite_extract.golden"
testTemplate(t, rewriteExtractTest, gold)
testTemplate(t, rewriteTmpl, rewriteExtractTest, gold)
}
var rewriteFixedPrefixTest = Spec{
......@@ -264,7 +265,7 @@ var rewriteFixedPrefixTest = Spec{
func TestRewriteFixedPrefix(t *testing.T) {
gold := "rewrite_fixed_prefix.golden"
testTemplate(t, rewriteFixedPrefixTest, gold)
testTemplate(t, rewriteTmpl, rewriteFixedPrefixTest, gold)
}
var rewriteFixedEqualTest = Spec{
......@@ -291,7 +292,7 @@ var rewriteFixedEqualTest = Spec{
func TestRewriteFixedEqual(t *testing.T) {
gold := "rewrite_fixed_equal.golden"
testTemplate(t, rewriteFixedEqualTest, gold)
testTemplate(t, rewriteTmpl, rewriteFixedEqualTest, gold)
}
var rewriteFixedSuballTest = Spec{
......@@ -318,7 +319,7 @@ var rewriteFixedSuballTest = Spec{
func TestRewriteFixedSuball(t *testing.T) {
gold := "rewrite_fixed_suball.golden"
testTemplate(t, rewriteFixedSuballTest, gold)
testTemplate(t, rewriteTmpl, rewriteFixedSuballTest, gold)
}
var rewritePrefixRegex = Spec{
......@@ -347,7 +348,7 @@ var rewritePrefixRegex = Spec{
func TestRewritePrefixRegex(t *testing.T) {
gold := "rewrite_prefix_regex.golden"
testTemplate(t, rewritePrefixRegex, gold)
testTemplate(t, rewriteTmpl, rewritePrefixRegex, gold)
}
var rewritePrependIfExists = Spec{
......@@ -370,7 +371,7 @@ var rewritePrependIfExists = Spec{
func TestRewritePrependIfExists(t *testing.T) {
gold := "rewrite_prepend_if_exists.golden"
testTemplate(t, rewritePrependIfExists, gold)
testTemplate(t, rewriteTmpl, rewritePrependIfExists, gold)
}
var rewriteExtractCookie = Spec{
......@@ -391,7 +392,7 @@ var rewriteExtractCookie = Spec{
func TestExtractCookie(t *testing.T) {
gold := "rewrite_extract_cookie.golden"
testTemplate(t, rewriteExtractCookie, gold)
testTemplate(t, rewriteTmpl, rewriteExtractCookie, gold)
}
var rewriteXCacheHdr = Spec{
......@@ -442,7 +443,7 @@ var rewriteXCacheHdr = Spec{
func TestXCacheHdr(t *testing.T) {
gold := "rewrite_x_cache_hdr.golden"
testTemplate(t, rewriteXCacheHdr, gold)
testTemplate(t, rewriteTmpl, rewriteXCacheHdr, gold)
}
var rewriteAppendFromSrcTest = Spec{
......@@ -455,7 +456,7 @@ var rewriteAppendFromSrcTest = Spec{
func TestRewriteAppendFromSrcTest(t *testing.T) {
gold := "rewrite_append_from_src.golden"
testTemplate(t, rewriteAppendFromSrcTest, gold)
testTemplate(t, rewriteTmpl, rewriteAppendFromSrcTest, gold)
}
var rewriteAppendRule = Spec{
......@@ -475,7 +476,7 @@ var rewriteAppendRule = Spec{
func TestRewriteAppendRule(t *testing.T) {
gold := "rewrite_append_rule.golden"
testTemplate(t, rewriteAppendRule, gold)
testTemplate(t, rewriteTmpl, rewriteAppendRule, gold)
}
var rewritePrependHdr = Spec{
......@@ -491,7 +492,7 @@ var rewritePrependHdr = Spec{
func TestRewritePrependHdr(t *testing.T) {
gold := "rewrite_prepend_hdr.golden"
testTemplate(t, rewritePrependHdr, gold)
testTemplate(t, rewriteTmpl, rewritePrependHdr, gold)
}
var rewriteSelectFirst = Spec{
......@@ -526,7 +527,7 @@ var rewriteSelectFirst = Spec{
func TestRewriteSelectFirst(t *testing.T) {
gold := "rewrite_select_first.golden"
testTemplate(t, rewriteSelectFirst, gold)
testTemplate(t, rewriteTmpl, rewriteSelectFirst, gold)
}
var rewriteSelectPermutations = Spec{
......@@ -614,7 +615,7 @@ var rewriteSelectPermutations = Spec{
func TestRewriteSelectPermutations(t *testing.T) {
gold := "rewrite_select_permute.golden"
testTemplate(t, rewriteSelectPermutations, gold)
testTemplate(t, rewriteTmpl, rewriteSelectPermutations, gold)
}
var rewriteSelectOperations = Spec{
......@@ -691,7 +692,7 @@ var rewriteSelectOperations = Spec{
func TestRewriteSelectOperations(t *testing.T) {
gold := "rewrite_select_ops.golden"
testTemplate(t, rewriteSelectOperations, gold)
testTemplate(t, rewriteTmpl, rewriteSelectOperations, gold)
}
// Test the use case that Auth should be executed, but the
......
......@@ -651,6 +651,113 @@ func (a byVCLSub) Len() int { return len(a) }
func (a byVCLSub) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a byVCLSub) Less(i, j int) bool { return a[i].VCLSub < a[j].VCLSub }
// ReqCompareType classifies comparison operations performed for the
// Conditions in a request disposition specification. The relation may
// be negated, if the Negate field in Condition is true.
type ReqCompareType uint8
const (
// ReqEqual specifies equality -- string equality, numeric
// equality, or membership in a set of fixed strings.
ReqEqual ReqCompareType = iota
// ReqMatch specifies a regular expression match.
ReqMatch
// ReqPrefix specifies that a string has a prefix in a set of
// fixed strings.
ReqPrefix
// Exists specifies that a request header exists.
Exists
// Greater specifies the > relation between a VCL variable
// with a numeric value and a constant.
Greater
// GreaterEqual specifies the >= relation for a numeric VCL
// variable and a constant.
GreaterEqual
// Less specifies the < relation for a numeric VCL variable
// and a constant.
Less
// LessEqual specifies the <= relation for a numeric VCL
// variable and a constant.
LessEqual
)
// Condition specifies (one of) the conditions that must be true if a
// client request disposition is to be executed.
type Condition struct {
Values []string
MatchFlags MatchFlagsType
Count *uint
Comparand string
Compare ReqCompareType
Negate bool
}
// RecvReturn is a name for the disposition of a client request.
// See: https://varnish-cache.org/docs/6.1/reference/states.html
type RecvReturn string
const (
// RecvHash to invoke cache lookup.
RecvHash RecvReturn = "hash"
// RecvPass to bypass cache lookup.
RecvPass = "pass"
// RecvPipe for pipe mode -- Varnish passes data between
// client and backend with no further intervention.
RecvPipe = "pipe"
// RecvPurge to purge a cache object.
// See: https://varnish-cache.org/docs/6.1/users-guide/purging.html?highlight=purge#http-purging
RecvPurge = "purge"
// RecvSynth to generate a synthetic response with a given
// HTTP response status.
RecvSynth = "synth"
)
// DispositionType specifies the disposition of a client request when
// associated Conditions are met.
//
// Status is the HTTP response status to set when Action is RecvSynth;
// ignored for other values of Action.
type DispositionType struct {
Action RecvReturn
Status uint16
}
// DispositionSpec specifies the disposition of a client request when
// all of the Conditions are met.
type DispositionSpec struct {
Conditions []Condition
Disposition DispositionType
}
func (reqDisp DispositionSpec) hash(hash hash.Hash) {
for _, cond := range reqDisp.Conditions {
for _, val := range cond.Values {
hash.Write([]byte(val))
}
cond.MatchFlags.hash(hash)
if cond.Count != nil {
countBytes := make([]byte, 8)
binary.BigEndian.PutUint64(countBytes,
uint64(*cond.Count))
hash.Write(countBytes)
}
hash.Write([]byte(cond.Comparand))
hash.Write([]byte{byte(cond.Compare)})
if cond.Negate {
hash.Write([]byte{1})
} else {
hash.Write([]byte{0})
}
}
hash.Write([]byte(reqDisp.Disposition.Action))
if reqDisp.Disposition.Action == RecvSynth {
statusBytes := make([]byte, 2)
binary.BigEndian.PutUint16(statusBytes,
uint16(reqDisp.Disposition.Status))
hash.Write(statusBytes)
}
}
// Spec is the specification for a VCL configuration derived from
// Ingresses and VarnishConfig Custom Resources. This abstracts the
// VCL to be loaded by all instances of a Varnish Service.
......@@ -677,8 +784,9 @@ type Spec struct {
// ACLs is a list of specifications for whitelisting or
// blacklisting IPs with access control lists, derived from
// VarnishConfig.Spec.ACLs.
ACLs []ACL
Rewrites []Rewrite
ACLs []ACL
Rewrites []Rewrite
Dispositions []DispositionSpec
}
// DeepHash computes a alphanumerically encoded hash value from a Spec
......@@ -712,6 +820,9 @@ func (spec Spec) DeepHash() string {
for _, rw := range spec.Rewrites {
rw.hash(hash)
}
for _, reqDisp := range spec.Dispositions {
reqDisp.hash(hash)
}
h := new(big.Int)
h.SetBytes(hash.Sum(nil))
return h.Text(62)
......@@ -731,6 +842,7 @@ func (spec Spec) Canonical() Spec {
Auths: make([]Auth, len(spec.Auths)),
ACLs: make([]ACL, len(spec.ACLs)),
Rewrites: make([]Rewrite, len(spec.Rewrites)),
Dispositions: make([]DispositionSpec, len(spec.Dispositions)),
}
copy(canon.DefaultService.Addresses, spec.DefaultService.Addresses)
sort.Stable(byIPPort(canon.DefaultService.Addresses))
......@@ -762,5 +874,11 @@ func (spec Spec) Canonical() Spec {
}
copy(canon.Rewrites, spec.Rewrites)
sort.Stable(byVCLSub(canon.Rewrites))
copy(canon.Dispositions, spec.Dispositions)
for _, disp := range canon.Dispositions {
for _, cond := range disp.Conditions {
sort.Strings(cond.Values)
}
}
return canon
}
......@@ -55,6 +55,11 @@ var fMap = template.FuncMap{
"rewrMatch": func(rewr Rewrite) string { return rewrMatch(rewr) },
"rewrOp": func(rewr Rewrite) string { return rewrOp(rewr) },
"rewrSelect": func(rewr Rewrite) string { return rewrSelect(rewr) },
"reqVMOD": func(cond Condition) string { return reqVMOD(cond) },
"reqMatch": func(cond Condition) string { return reqMatch(cond) },
"value": func(cond Condition) string { return reqValue(cond) },
"reqFlags": func(cond Condition) string { return reqFlags(cond) },
"exists": func(cmp ReqCompareType) bool { return cmp == Exists },
"aclCmp": func(comparand string) string {
return aclCmp(comparand)
},
......@@ -131,6 +136,18 @@ var fMap = template.FuncMap{
"needsSelectEnum": func(rewr Rewrite) bool {
return rewr.Select != Unique
},
"reqObj": func(didx, cidx int) string {
return fmt.Sprintf("vk8s_reqdisp_%d_%d", didx, cidx)
},
"reqNeedsMatcher": func(cond Condition) bool {
return reqNeedsMatcher(cond)
},
"reqNeedsCompile": func(cond Condition) bool {
return reqNeedsCompile(cond)
},
"reqCmpRelation": func(cond Condition) string {
return reqCmpRelation(cond)
},
}
const (
......@@ -139,6 +156,7 @@ const (
authTmplSrc = "auth.tmpl"
aclTmplSrc = "acl.tmpl"
rewriteTmplSrc = "rewrite.tmpl"
reqDispTmplSrc = "recv_disposition.tmpl"
// maxSymLen is a workaround for Varnish issue #2880
// https://github.com/varnishcache/varnish-cache/issues/2880
......@@ -152,6 +170,7 @@ var (
authTmpl *template.Template
aclTmpl *template.Template
rewriteTmpl *template.Template
reqDispTmpl *template.Template
vclIllegal = regexp.MustCompile("[^[:word:]-]+")
)
......@@ -163,6 +182,7 @@ func InitTemplates(tmplDir string) error {
authTmplPath := path.Join(tmplDir, authTmplSrc)
aclTmplPath := path.Join(tmplDir, aclTmplSrc)
rewriteTmplPath := path.Join(tmplDir, rewriteTmplSrc)
reqDispTmplPath := path.Join(tmplDir, reqDispTmplSrc)
ingressTmpl, err = template.New(ingTmplSrc).
Funcs(fMap).ParseFiles(ingTmplPath)
......@@ -189,6 +209,11 @@ func InitTemplates(tmplDir string) error {
if err != nil {
return err
}
reqDispTmpl, err = template.New(reqDispTmplSrc).
Funcs(fMap).ParseFiles(reqDispTmplPath)
if err != nil {
return err
}
return nil
}
......@@ -227,6 +252,11 @@ func (spec Spec) GetSrc() (string, error) {
return "", err
}
}
if len(spec.Dispositions) > 0 {
if err := reqDispTmpl.Execute(&buf, spec); err != nil {
return "", err
}
}
if spec.VCL != "" {
buf.WriteString(spec.VCL)
}
......@@ -318,6 +348,26 @@ func cmpRelation(cmp CompareType) string {
}
}
func reqCmpRelation(cond Condition) string {
switch cond.Compare {
case ReqEqual:
if cond.Negate {
return "!="
}
return "=="
case Greater:
return ">"
case GreaterEqual:
return ">="
case Less:
return "<"
case LessEqual:
return "<="
default:
return "__INVALID_COMPARISON_TYPE__"
}
}
func dirType(svc Service) string {
if svc.Director == nil {
return RoundRobin.String()
......@@ -338,6 +388,19 @@ func needsMatcher(rewr Rewrite) bool {
}
}
func reqNeedsMatcher(cond Condition) bool {
switch cond.Compare {
case ReqMatch, ReqPrefix:
return true
case Exists, Greater, GreaterEqual, Less, LessEqual:
return false
}
if cond.Count != nil || len(cond.Values) == 1 {
return false
}
return true
}
func rewrName(i int) string {
return fmt.Sprintf("vk8s_rewrite_%d", i)
}
......@@ -349,51 +412,59 @@ func rewrVMOD(rewr Rewrite) string {
return "selector"
}
func rewrFlags(rewr Rewrite) string {
if rewr.Compare != RewriteMatch {
if !rewr.MatchFlags.CaseSensitive {
func matcherFlags(isSelector bool, flagSpec MatchFlagsType) string {
if isSelector {
if !flagSpec.CaseSensitive {
return "case_sensitive=false"
}
return ""
}
var flags []string
if rewr.MatchFlags.MaxMem != 0 && rewr.MatchFlags.MaxMem != 8388608 {
maxMem := fmt.Sprintf("max_mem=%d", rewr.MatchFlags.MaxMem)
if flagSpec.MaxMem != 0 && flagSpec.MaxMem != 8388608 {
maxMem := fmt.Sprintf("max_mem=%d", flagSpec.MaxMem)
flags = append(flags, maxMem)
}
if rewr.MatchFlags.Anchor != None {
switch rewr.MatchFlags.Anchor {
if flagSpec.Anchor != None {
switch flagSpec.Anchor {
case Start:
flags = append(flags, "anchor=start")
case Both:
flags = append(flags, "anchor=both")
}
}
if rewr.MatchFlags.UTF8 {
if flagSpec.UTF8 {
flags = append(flags, "utf8=true")
}
if rewr.MatchFlags.PosixSyntax {
if flagSpec.PosixSyntax {
flags = append(flags, "posix_syntax=true")
}
if rewr.MatchFlags.LongestMatch {
if flagSpec.LongestMatch {
flags = append(flags, "longest_match=true")
}
if rewr.MatchFlags.Literal {
if flagSpec.Literal {
flags = append(flags, "literal=true")
}
if !rewr.MatchFlags.CaseSensitive {
if !flagSpec.CaseSensitive {
flags = append(flags, "case_sensitive=false")
}
if rewr.MatchFlags.PerlClasses {
if flagSpec.PerlClasses {
flags = append(flags, "perl_classes=true")
}
if rewr.MatchFlags.WordBoundary {
if flagSpec.WordBoundary {
flags = append(flags, "word_boundary=true")
}
return strings.Join(flags, ",")
}
func rewrFlags(rewr Rewrite) string {
return matcherFlags(rewr.Compare != RewriteMatch, rewr.MatchFlags)
}
func reqFlags(cond Condition) string {
return matcherFlags(cond.Compare != ReqMatch, cond.MatchFlags)
}
func needsSave(rewr Rewrite) bool {
if rewr.Compare != RewriteMatch {
return false
......@@ -410,6 +481,10 @@ func needsCompile(rewr Rewrite) bool {
return rewr.Compare == RewriteMatch
}
func reqNeedsCompile(cond Condition) bool {
return cond.Compare == ReqMatch
}
func rewrSub(rewr Rewrite) string {
if rewr.VCLSub == Unspecified {
if strings.HasPrefix(rewr.Target, "resp") ||
......@@ -478,6 +553,7 @@ func rewrOperand2(rewr Rewrite, i int) string {
return rewr.Source
}
// XXX DRY
func rewrMatch(rewr Rewrite) string {
switch rewr.Compare {
case RewriteMatch, RewriteEqual:
......@@ -489,6 +565,17 @@ func rewrMatch(rewr Rewrite) string {
}
}
func reqMatch(cond Condition) string {
switch cond.Compare {
case ReqMatch, ReqEqual:
return "match"
case ReqPrefix:
return "hasprefix"
default:
return "__INVALID_MATCH_OPERATION__"
}
}
func rewrOp(rewr Rewrite) string {
switch rewr.Method {
case Sub:
......@@ -504,3 +591,21 @@ func rewrOp(rewr Rewrite) string {
return "__INVALID_REWRITE_OPERAION__"
}
}
func reqVMOD(cond Condition) string {
switch cond.Compare {
case ReqEqual, ReqPrefix:
return "selector"
case ReqMatch:
return "re2"
default:
return "__INVALID_COMPARISON_FOR_VMOD__"
}
}
func reqValue(cond Condition) string {
if cond.Count != nil {
return fmt.Sprintf("%d", *cond.Count)
}
return `"` + cond.Values[0] + `"`
}
import re2;
import selector;
sub vcl_init {
new vk8s_reqdisp_1_2 = selector.set(case_sensitive=false);
vk8s_reqdisp_1_2.add("HTTP/1.1");
}
sub vcl_init {
new vk8s_reqdisp_2_0 = selector.set();
vk8s_reqdisp_2_0.add("GET");
vk8s_reqdisp_2_0.add("HEAD");
vk8s_reqdisp_2_0.add("PUT");
vk8s_reqdisp_2_0.add("POST");
vk8s_reqdisp_2_0.add("TRACE");
vk8s_reqdisp_2_0.add("OPTIONS");
vk8s_reqdisp_2_0.add("DELETE");
}
sub vcl_init {
new vk8s_reqdisp_3_0 = selector.set();
vk8s_reqdisp_3_0.add("GET");
vk8s_reqdisp_3_0.add("HEAD");
}
sub vcl_recv {
if (req.method == "PRI") {
return (synth(405));
}
if (! req.http.Host &&
req.esi_level == 0 &&
vk8s_reqdisp_1_2.hasprefix(req.proto)) {
return (synth(400));
}
if (! vk8s_reqdisp_2_0.match(req.method)) {
return (pipe);
}
if (! vk8s_reqdisp_3_0.match(req.method)) {
return (pass);
}
if (req.http.Cookie) {
return (pass);
}
if (req.http.Authorization) {
return (pass);
}
return (hash);
}
import re2;
import selector;
sub vcl_init {
new vk8s_reqdisp_0_0 = re2.set();
vk8s_reqdisp_0_0.add("\.png$");
vk8s_reqdisp_0_0.add("\.jpe?g$");
vk8s_reqdisp_0_0.add("\.css$");
vk8s_reqdisp_0_0.add("\.js$");
vk8s_reqdisp_0_0.compile();
}
sub vcl_recv {
if (vk8s_reqdisp_0_0.match(req.url)) {
return (hash);
}
return (hash);
}
import re2;
import selector;
sub vcl_recv {
if (req.method == "CONNECT") {
return (pipe);
}
return (hash);
}
import re2;
import selector;
sub vcl_init {
new vk8s_reqdisp_0_0 = selector.set();
vk8s_reqdisp_0_0.add("GET");
vk8s_reqdisp_0_0.add("HEAD");
vk8s_reqdisp_0_0.add("PUT");
vk8s_reqdisp_0_0.add("POST");
vk8s_reqdisp_0_0.add("TRACE");
vk8s_reqdisp_0_0.add("OPTIONS");
vk8s_reqdisp_0_0.add("DELETE");
}
sub vcl_recv {
if (! vk8s_reqdisp_0_0.match(req.method)) {
return (synth(405));
}
return (hash);
}
import re2;
import selector;
sub vcl_init {
new vk8s_reqdisp_0_0 = selector.set();
vk8s_reqdisp_0_0.add("/interactive/");
vk8s_reqdisp_0_0.add("/basket/");
vk8s_reqdisp_0_0.add("/personal");
vk8s_reqdisp_0_0.add("/dynamic/");
}
sub vcl_recv {
if (vk8s_reqdisp_0_0.hasprefix(req.url)) {
return (pass);
}
return (hash);
}
import re2;
import selector;
sub vcl_recv {
if (req.method == "CONNECT") {
return (pipe);
}
return (hash);
}
import re2;
import selector;
sub vcl_recv {
if (req.method == "PURGE") {
return (purge);
}
return (hash);
}
import re2;
import selector;
sub vcl_init {
new vk8s_reqdisp_0_0 = selector.set();
vk8s_reqdisp_0_0.add("/foo");
vk8s_reqdisp_0_0.add("/bar");
vk8s_reqdisp_0_0.add("/baz");
vk8s_reqdisp_0_0.add("/quux");
}
sub vcl_recv {
if (! vk8s_reqdisp_0_0.hasprefix(req.url)) {
return (synth(403));
}
return (hash);
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment