Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support nginx vars in ApisixRoute v2alpha1 #304

Merged
merged 4 commits into from
Mar 17, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
27 changes: 27 additions & 0 deletions docs/en/latest/concepts/apisix_route.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,33 @@ spec:
servicePort: 80
```

The `nginxVars` allows user to configure match conditions with arbitrary predicates in HTTP, such as queries, HTTP headers and etc.
It's composed by several expressions, which in turn composed by subject, operator and value/set.

```yaml
apiVersion: apisix.apache.org/v2alpha1
kind: ApisixRoute
metadata:
name: method-route
spec:
http:
- name: method
match:
paths:
- /
nginxVars:
- subject: arg_id
op: Equal
value: 2143
backend:
serviceName: foo
servicePort: 80
```

The above configuration configures an extra route match condition, which asks the
query `id` must be equal to `2143`. Note the subject is in [Nginx Variables](http://nginx.org/en/docs/varindex.html) style
(but without the leading `$` symbol).

Service Resolution Granularity
------------------------------

Expand Down
52 changes: 52 additions & 0 deletions docs/en/latest/references/apisix_route_v2alpha1.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,55 @@ title: ApisixRoute/v2alpha1 Reference
# limitations under the License.
#
-->

## Spec

Meaning of each field in the spec of ApisixRoute are followed, the top level fields (`apiVersion`, `kind` and `metadata`) are same as other Kubernetes Resources.

| Field | Type | Description |
|---------------|----------|----------------------------------------------------|
| http | array | ApisixRoute's HTTP route rules. |
| http[].name | string (required) | The route rule name. |
| http[].priority | integer | The route priority, it's used to determine which route will be hitted when multile routes contains the same URI. Large number means higher priority. |
| http[].match | object | Route match conditions. |
| http[].match.paths | array | A series of URI that should be matched (oneof) to use this route rule. |
| http[].match.hosts | array | A series of hosts that should be matched (oneof) to use this route rule.
| http[].match.methods | array | A series of HTTP methods(`GET`, `POST`, `PUT`, `DELETE`, `PATCH`, `HEAD`, `OPTIONS`, `CONNECT`, `TRACE`) that should be matched (oneof) to use this route rule.
| http[].match.remote_addrs | array | A series of IP address (CIDR format) that should be matched (oneof) to use this route rule.
| http[].match.nginxVars | array | A series expressions that the results should be matched (oneof) to use this route rule.
| http[].match.nginxVars[].subject | string | Expression subject, which is in [Nginx Variables](http://nginx.org/en/docs/varindex.html) style.
| http[].match.nginxVars[].op | string | Expression operator, see [Expression Operators](#expression-operators) for the detail of enumerations.
| http[].match.nginxVars[].value | string | Expected expression result, it's exclusive with `http[].match.nginxVars[].set`.
| http[].match.nginxVars[].set | array | Expected expression result set, only used when the operator is `In` or `NotIn`, it's exclusive with `http[].match.nginxVars[].value`.
| http[].backend | object | The backend service
| http[].backend.serviceName | string | The backend service name, note the service and ApisixRoute should be created in the same namespace. Cross namespace referencing is not allowed.
| http[].backend.servicePort | integer or string | The backend service port, can be the port number of the name defined in the service object.
| http[].backend.resolveGranualrity | string | See [Service Resolve Granularity](#service-resolve-granularity) for the details.
| http[].plugins | array | A series of APISIX plugins that will be executed once this route rule is matched |
| http[].plugins[].name | string | The plugin name, see [docs](http://apisix.apache.org/docs/apisix/getting-started) for learning the available plugins.
| http[].plugins[].enable | boolean | Whether the plugin is in use |
| http[].plugins[].config | object | The plugin configuration, fields should be same as in APISIX. |

## Expression Operators

| Operator | Meaning |
|----------|---------|
| Equal| The result of `subject` should be equal to the `value` |
| NotEqual | The result of `subject` should not be equal to `value` |
| GreaterThan | The result of `subject` should be a number and it must larger then `value`. |
| LessThan | The result of `subject` should be a number and it must less than `value`. |
| In | The result of `subject` should be inside the `set`. |
| NotIn | The result of `subject` should not be inside the `set`. |
| RegexMatch | The result of `subject` should be matched by the `value` (a PCRE regex pattern). |
| RegexNotMatch | The result of `subject` should not be matched by the `value` (a PCRE regex pattern). |
| RegexMatchCaseInsensitive | Similar with `RegexMatch` but the match process is case insensitive |
| RegexNotMatchCaseInsensitive | Similar with `RegexNotMatchCaseInsensitive` but the match process is case insensitive. |

## Service Resolve Granularity

The service resolve granularity determines whether the [Serivce ClusterIP](https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types) or its endpoints should be filled in the target upstream of APISIX.

| Granularity | Meaning |
| ----------- | ------- |
| endpoint | Filled upstream nodes by Pods' IP.
| service | Filled upstream nodes by Service ClusterIP, in such a case, loadbalacing are implemented by [kube-proxy](https://kubernetes.io/docs/concepts/overview/components/#kube-proxy).|
2 changes: 2 additions & 0 deletions pkg/apisix/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ type routeItem struct {
ServiceId string `json:"service_id"`
Host string `json:"host"`
URI string `json:"uri"`
Vars [][]v1.StringOrSlice `json:"vars"`
Uris []string `json:"uris"`
Desc string `json:"desc"`
Methods []string `json:"methods"`
Expand Down Expand Up @@ -108,6 +109,7 @@ func (i *item) route(clusterName string) (*v1.Route, error) {
Host: route.Host,
Path: route.URI,
Uris: route.Uris,
Vars: route.Vars,
Methods: route.Methods,
UpstreamId: route.UpstreamId,
ServiceId: route.ServiceId,
Expand Down
17 changes: 10 additions & 7 deletions pkg/apisix/route.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,14 @@ import (
)

type routeReqBody struct {
Desc string `json:"desc,omitempty"`
URI string `json:"uri,omitempty"`
Uris []string `json:"uris,omitempty"`
Host string `json:"host,omitempty"`
ServiceId string `json:"service_id,omitempty"`
UpstreamId string `json:"upstream_id,omitempty"`
Plugins v1.Plugins `json:"plugins,omitempty"`
Desc string `json:"desc,omitempty"`
URI string `json:"uri,omitempty"`
Uris []string `json:"uris,omitempty"`
Vars [][]v1.StringOrSlice `json:"vars,omitempty"`
Host string `json:"host,omitempty"`
ServiceId string `json:"service_id,omitempty"`
UpstreamId string `json:"upstream_id,omitempty"`
Plugins v1.Plugins `json:"plugins,omitempty"`
}

type routeClient struct {
Expand Down Expand Up @@ -165,6 +166,7 @@ func (r *routeClient) Create(ctx context.Context, obj *v1.Route) (*v1.Route, err
UpstreamId: obj.UpstreamId,
Uris: obj.Uris,
Plugins: obj.Plugins,
Vars: obj.Vars,
})
if err != nil {
return nil, err
Expand Down Expand Up @@ -230,6 +232,7 @@ func (r *routeClient) Update(ctx context.Context, obj *v1.Route) (*v1.Route, err
URI: obj.Path,
ServiceId: obj.ServiceId,
Plugins: obj.Plugins,
Vars: obj.Vars,
})
if err != nil {
return nil, err
Expand Down
49 changes: 31 additions & 18 deletions pkg/kube/apisix/apis/config/v2alpha1/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,33 @@ import (
// +genclient
// +genclient:noStatus

const (
// OpEqual means the equal ("==") operator in nginxVars.
OpEqual = "Equal"
// OpNotEqual means the not equal ("~=") operator in nginxVars.
OpNotEqual = "NotEqual"
// OpGreaterThan means the greater than (">") operator in nginxVars.
OpGreaterThan = "GreaterThan"
// OpGreaterThanEqual means the greater than (">=") operator in nginxVars.
OpGreaterThanEqual = "GreaterThanEqual"
// OpLessThan means the less than ("<") operator in nginxVars.
OpLessThan = "LessThan"
// OpLessThanEqual means the less than equal ("<=") operator in nginxVars.
OpLessThanEqual = "LessThanEqual"
// OpRegexMatch means the regex match ("~~") operator in nginxVars.
OpRegexMatch = "RegexMatch"
// OpRegexNotMatch means the regex not match ("!~~") operator in nginxVars.
OpRegexNotMatch = "RegexNotMatch"
// OpRegexMatchCaseInsensitive means the regex match "~*" (case insensitive mode) operator in nginxVars.
OpRegexMatchCaseInsensitive = "RegexMatchCaseInsensitive"
// OpRegexNotMatchCaseInsensitive means the regex not match "!~*" (case insensitive mode) operator in nginxVars.
OpRegexNotMatchCaseInsensitive = "RegexNotMatchCaseInsensitive"
// OpIn means the in operator ("in") in nginxVars.
OpIn = "In"
// OpNotIn means the not in operator ("not_in") in nginxVars.
OpNotIn = "NotIn"
)

// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// ApisixRoute is used to define the route rules and upstreams for Apache APISIX.
type ApisixRoute struct {
Expand Down Expand Up @@ -86,28 +113,14 @@ type ApisixRouteHTTPMatchNginxVar struct {
// be any string composed by literals and nginx
// vars.
Subject string `json:"subject"`
// Op is the operator, can be:
// - "==": equal
// - "~=": not equal
// - ">": greater than
// - ">=": greater than or equal to
// - "<": less then
// - "<=": less then or equal to
// - "~~": regex match
// - "!~~": regex not match
// - "~*": case insensitive regex match
// - "!~*": case insensitive regex not match
// - "in": set match, Value should be a string array.
// - "not_in": set match, Value should be a string array.
// - "contain": subject contains the Value (inclusion relation).
// - "not_contain": subject doesn't contain the Value (inclusion relation).
// Op is the operator.
Op string `json:"op"`
// Array is an array type object of the expression.
// Set is an array type object of the expression.
// It should be used when the Op is "in" or "not_in";
Array []string `json:"array"`
Set []string `json:"set"`
// Value is the normal type object for the expression,
// it should be used when the Op is not "in" and "not_in".
// Array and Value are exclusive so only of them can be set
// Set and Value are exclusive so only of them can be set
// in the same time.
Value *string `json:"value"`
}
Expand Down
4 changes: 2 additions & 2 deletions pkg/kube/apisix/apis/config/v2alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

96 changes: 95 additions & 1 deletion pkg/kube/translation/apisix_route.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ func (t *translator) TranslateRouteV1(ar *configv1.ApisixRoute) ([]*apisixv1.Rou

func (t *translator) TranslateRouteV2alpha1(ar *configv2alpha1.ApisixRoute) ([]*apisixv1.Route, []*apisixv1.Upstream, error) {
var (
vars [][]apisixv1.StringOrSlice
routes []*apisixv1.Route
upstreams []*apisixv1.Upstream
)
Expand Down Expand Up @@ -136,7 +137,7 @@ func (t *translator) TranslateRouteV2alpha1(ar *configv2alpha1.ApisixRoute) ([]*
}

if part.Backend.ResolveGranularity == "service" && svc.Spec.ClusterIP == "" {
log.Errorw("ApisixRoute refers to a headless service but want to use the service level resolve granualrity",
log.Errorw("ApisixRoute refers to a headless service but want to use the service level resolve granularity",
zap.Any("ApisixRoute", ar),
zap.Any("service", svc),
)
Expand All @@ -155,6 +156,16 @@ func (t *translator) TranslateRouteV2alpha1(ar *configv2alpha1.ApisixRoute) ([]*
pluginMap[plugin.Name] = make(map[string]interface{})
}
}
if part.Match.NginxVars != nil {
vars, err = t.translateNginxVars(part.Match.NginxVars)
if err != nil {
log.Errorw("ApisixRoute with bad nginxVars",
zap.Error(err),
zap.Any("ApisixRoute", ar),
)
return nil, nil, err
}
}

routeName := apisixv1.ComposeRouteName(ar.Namespace, ar.Name, part.Name)
upstreamName := apisixv1.ComposeUpstreamName(ar.Namespace, part.Backend.ServiceName, svcPort)
Expand All @@ -165,6 +176,7 @@ func (t *translator) TranslateRouteV2alpha1(ar *configv2alpha1.ApisixRoute) ([]*
ID: id.GenID(routeName),
ResourceVersion: ar.ResourceVersion,
},
Vars: vars,
Hosts: part.Match.Hosts,
Uris: part.Match.Paths,
Methods: part.Match.Methods,
Expand Down Expand Up @@ -201,3 +213,85 @@ func (t *translator) TranslateRouteV2alpha1(ar *configv2alpha1.ApisixRoute) ([]*
}
return routes, upstreams, nil
}

func (t *translator) translateNginxVars(nginxVars []configv2alpha1.ApisixRouteHTTPMatchNginxVar) ([][]apisixv1.StringOrSlice, error) {
var (
vars [][]apisixv1.StringOrSlice
op string
)
for _, expr := range nginxVars {
var (
invert bool
this []apisixv1.StringOrSlice
)
if expr.Subject == "" {
return nil, errors.New("empty nginxVar subject")
}
this = append(this, apisixv1.StringOrSlice{
StrVal: expr.Subject,
})

switch expr.Op {
case configv2alpha1.OpEqual:
op = "=="
case configv2alpha1.OpGreaterThan:
op = ">"
// TODO Implement "<=", ">=" operators after the
// lua-resty-expr supports it. See
// https://github.com/api7/lua-resty-expr/issues/28
// for details.
//case configv2alpha1.OpGreaterThanEqual:
// invert = true
// op = "<"
case configv2alpha1.OpIn:
op = "in"
case configv2alpha1.OpLessThan:
op = "<"
//case configv2alpha1.OpLessThanEqual:
// invert = true
// op = ">"
case configv2alpha1.OpNotEqual:
op = "~="
case configv2alpha1.OpNotIn:
invert = true
op = "in"
case configv2alpha1.OpRegexMatch:
op = "~~"
case configv2alpha1.OpRegexMatchCaseInsensitive:
op = "~*"
case configv2alpha1.OpRegexNotMatch:
invert = true
op = "~~"
case configv2alpha1.OpRegexNotMatchCaseInsensitive:
invert = true
op = "~*"
default:
return nil, errors.New("unknown operator")
}
if invert {
this = append(this, apisixv1.StringOrSlice{
StrVal: "!",
})
}
this = append(this, apisixv1.StringOrSlice{
StrVal: op,
})
if expr.Op == configv2alpha1.OpIn || expr.Op == configv2alpha1.OpNotIn {
if expr.Set == nil {
return nil, errors.New("empty set value")
}
this = append(this, apisixv1.StringOrSlice{
SliceVal: expr.Set,
})
} else if expr.Value != nil {
this = append(this, apisixv1.StringOrSlice{
StrVal: *expr.Value,
})
} else {
return nil, errors.New("neither set nor value is provided")
}
vars = append(vars, this)
}

return vars, nil
}