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

[NOREF] add support for queryParams and scheme #354

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 3 additions & 19 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,27 +21,11 @@ jobs:
uname -a
go version
go env
- name: Vet
if: matrix.platform == 'ubuntu-latest'
run: go vet -v ./...
- name: Lint
if: matrix.platform == 'ubuntu-latest'
run: |
export PATH=$PATH:$(go env GOPATH)/bin
go install golang.org/x/lint/golint@latest
golint -set_exit_status ./...
- name: staticcheck.io
if: matrix.platform == 'ubuntu-latest'
uses: dominikh/[email protected]
uses: golangci/golangci-lint-action@v3
with:
install-go: false
- name: gofumpt formatting
if: matrix.platform == 'ubuntu-latest'
run: |
export PATH=$PATH:$(go env GOPATH)/bin
go install mvdan.cc/gofumpt@latest
gofumpt -d .
[ -z "$(gofumpt -l .)" ]
skip-build-cache: true
skip-pkg-cache: true
- name: Test
run: go test -count=1 ./...
- name: Test with -race
Expand Down
14 changes: 14 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
run:
timeout: 10m
linters:
disable-all: true
enable:
- errcheck
- gofumpt
- gosimple
- govet
- ineffassign
- staticcheck
- stylecheck
- typecheck
- unused
22 changes: 22 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,10 +1,32 @@
CURRENTPATH = $(shell echo $(PWD))
WORKDIR = /src/github.com/getyourguide.com/istio-config-validator
GOLINT_CMD=golangci-lint
GO_FILES=$(shell find . -type f -regex ".*go")

.PHONY: run lint fix

run:
docker run -it --rm --name istio_config_validator \
-v ${CURRENTPATH}:${WORKDIR} \
-w ${WORKDIR} \
golang:1.21 \
go run cmd/istio-config-validator/main.go -t examples/ examples/

define check_linter
@if ! command -v $(GOLINT_CMD) > /dev/null; then\
echo "$(GOLINT_CMD) must be installed to run linting: try brew install $(GOLINT_CMD)";\
exit 1;\
fi
endef

lint: .golangci.yml ## Lint the code

fix: ## Fix any issues found during linting
@$(call check_linter)
$(GOLINT_CMD) run --fix

.golangci.yml: $(GO_FILES)
@$(call check_linter)
$(GOLINT_CMD) version
$(GOLINT_CMD) run
touch .golangci.yml
7 changes: 5 additions & 2 deletions cmd/istio-config-validator/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,15 +61,18 @@ func main() {
func getFiles(names []string) []string {
var files []string
for _, name := range names {
filepath.Walk(name, func(path string, info os.FileInfo, err error) error {
err := filepath.Walk(name, func(path string, info os.FileInfo, err error) error {
if err != nil {
log.Fatal(err.Error())
return err
}
if !info.IsDir() && isYaml(info) {
files = append(files, path)
}
return nil
})
if err != nil {
log.Fatal(err.Error())
}
}
return files
}
Expand Down
2 changes: 1 addition & 1 deletion examples/destination.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# This file is to test skiping non virtual services files
# This file is to test skipping non virtual services files
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
Expand Down
4 changes: 2 additions & 2 deletions internal/pkg/parser/parser.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
// Package parser is the package responsible for parsing test cases and istio configuration
// to be use on test assertionpackage parser
// to be used on test assertionpackage parser
package parser

import (
"fmt"

v1alpha3 "istio.io/client-go/pkg/apis/networking/v1alpha3"
"istio.io/client-go/pkg/apis/networking/v1alpha3"
)

// Parser contains the parsed files needed to run tests
Expand Down
36 changes: 24 additions & 12 deletions internal/pkg/parser/testcase.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,18 +43,23 @@ type TestCase struct {

// Request define the crafted http request present in the test case file.
type Request struct {
Authority []string `yaml:"authority"`
Method []string `yaml:"method"`
URI []string `yaml:"uri"`
Headers map[string]string `yaml:"headers"`
Authority []string `yaml:"authority"`
Method []string `yaml:"method"`
URI []string `yaml:"uri"`
// optional fields
Headers map[string]string `yaml:"headers"`
QueryParams map[string]string `yaml:"queryParams"`
Scheme string `yaml:"scheme"`
}

// Input contains the data structure which will be used to assert
type Input struct {
Authority string
Method string
URI string
Headers map[string]string
Authority string
Method string
URI string
Headers map[string]string
Scheme string
QueryParams map[string]string
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need a separate field for the parameters or can we use the uri and parse from it?

Also, the same parameter can be set multiple times and interpreted differently by web frameworks as there is no apparent standard, this would be another reason not to use map[string]string

https://go.dev/play/p/dcExJ0RlFYT

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed will keep as is for now - the envoy behaviour is to check the first query param

}

// Destination define the destination we should assert
Expand All @@ -78,7 +83,7 @@ type Port struct {
// {Authority:"example.com", Method: "OPTIONS"},
// }
func (r *Request) Unfold() ([]Input, error) {
out := []Input{}
var out []Input

if len(r.Authority) == 0 {
return out, ErrEmptyAuthorityList
Expand All @@ -98,7 +103,14 @@ func (r *Request) Unfold() ([]Input, error) {

for _, auth := range r.Authority {
for _, method := range r.Method {
out = append(out, Input{Authority: auth, Method: method, URI: u.Path, Headers: r.Headers})
out = append(out, Input{
Authority: auth,
Method: method,
URI: u.Path,
Headers: r.Headers,
QueryParams: r.QueryParams,
Scheme: r.Scheme,
})
}
}
}
Expand All @@ -107,7 +119,7 @@ func (r *Request) Unfold() ([]Input, error) {
}

func parseTestCases(files []string) ([]*TestCase, error) {
out := []*TestCase{}
var out []*TestCase

for _, file := range files {
fileContent, err := os.ReadFile(file)
Expand Down Expand Up @@ -137,7 +149,7 @@ func parseTestCases(files []string) ([]*TestCase, error) {
yamlFile := &TestCaseYAML{}
err = json.Unmarshal(jsonBytes, yamlFile)
if err != nil {
slog.Debug("unmarshaling failed for file '%s': %w", file, err)
slog.Debug("unmarshalling failed for file '%s': %w", file, err)
return []*TestCase{}, err

}
Expand Down
2 changes: 1 addition & 1 deletion internal/pkg/parser/virtualservice.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import (
)

func parseVirtualServices(files []string) ([]*v1alpha3.VirtualService, error) {
out := []*v1alpha3.VirtualService{}
var out []*v1alpha3.VirtualService

for _, file := range files {
fileContent, err := os.ReadFile(file)
Expand Down
88 changes: 60 additions & 28 deletions internal/pkg/unit/match_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,37 @@ import (
"istio.io/api/networking/v1alpha3"
)

// MapMatcher checks map parameters, which requires executing multiple ExtendedStringMatch checks
type MapMatcher struct {
matchers map[string]*v1alpha3.StringMatch
}

func (mm MapMatcher) Match(input map[string]string) (bool, error) {
for param, matcher := range mm.matchers {
value, exists := input[param]
if !exists {
return false, nil
}

if matcher.GetMatchType() == nil {
// match type exact: "", and {} checks for presence in query and headers respectively
continue
}

extendedMatcher := &ExtendedStringMatch{StringMatch: matcher}
matched, err := extendedMatcher.Match(value)
if err != nil {
return false, err
}

if !matched {
return false, nil
}
}

return true, nil
}

// ExtendedStringMatch copies istio ExtendedStringMatch definition and extends it to add helper methods.
type ExtendedStringMatch struct {
*v1alpha3.StringMatch
Expand All @@ -26,57 +57,58 @@ func (sm *ExtendedStringMatch) Match(s string) (bool, error) {
return true, nil
}

switch {
case sm.GetExact() != "":
return sm.GetExact() == s, nil
case sm.GetPrefix() != "":
switch m := sm.GetMatchType().(type) {
case *v1alpha3.StringMatch_Exact:
return m.Exact == s, nil
case *v1alpha3.StringMatch_Prefix:
return strings.HasPrefix(s, sm.GetPrefix()), nil
case sm.GetRegex() != "":
case *v1alpha3.StringMatch_Regex:
// The rule will not match if only a subsequence of the string matches the regex.
// https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/route/v3/route_components.proto#envoy-v3-api-field-config-route-v3-routematch-safe-regex
r, err := regexp.Compile("^" + sm.GetRegex() + "$")
if err != nil {
return false, fmt.Errorf("could not compile regex %s: %v", sm.GetRegex(), err)
}
return r.MatchString(s), nil
default:
return false, fmt.Errorf("unknown matcher type %T", sm.GetMatchType())
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Returning an error here is a behaviour change - could also be a log

}
return false, nil
}

// matchRequest takes an Input and evaluates against a HTTPMatchRequest block. It replicates
// Istio VirtualService semantic returning true when ALL conditions within the block are true.
// TODO: Add support for all fields within a match block. The ones supported today are:
// Authority, Uri, Method and Headers.
// Authority, Uri, Method, Headers, Scheme, and QueryParams.
func matchRequest(input parser.Input, httpMatchRequest *v1alpha3.HTTPMatchRequest) (bool, error) {
authority := &ExtendedStringMatch{httpMatchRequest.Authority}
uri := &ExtendedStringMatch{httpMatchRequest.Uri}
method := &ExtendedStringMatch{httpMatchRequest.Method}
if matched, err := uri.Match(input.URI); !matched || err != nil {
return false, err
}

for headerName, sm := range httpMatchRequest.Headers {
if _, ok := input.Headers[headerName]; !ok {
return false, nil
}
header := &ExtendedStringMatch{sm}
match, err := header.Match(input.Headers[headerName])
if err != nil {
return false, err
}
if !match {
return false, nil
}
authority := &ExtendedStringMatch{httpMatchRequest.Authority}
if matched, err := authority.Match(input.Authority); !matched || err != nil {
return false, err
}

uriMatch, err := uri.Match(input.URI)
if err != nil {
method := &ExtendedStringMatch{httpMatchRequest.Method}
if matched, err := method.Match(input.Method); !matched || err != nil {
return false, err
}
authorityMatch, err := authority.Match(input.Authority)
if err != nil {

scheme := &ExtendedStringMatch{httpMatchRequest.Scheme}
if matched, err := scheme.Match(input.Scheme); !matched || err != nil {
return false, err
}
methodMatch, err := method.Match(input.Method)
if err != nil {

headers := &MapMatcher{matchers: httpMatchRequest.Headers}
if matched, err := headers.Match(input.Headers); !matched || err != nil {
return false, err
}
return authorityMatch && uriMatch && methodMatch, nil

query := &MapMatcher{matchers: httpMatchRequest.QueryParams}
if matched, err := query.Match(input.QueryParams); !matched || err != nil {
return false, err
}

return true, nil
}
31 changes: 31 additions & 0 deletions internal/pkg/unit/testcases/header/service.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: example
namespace: example
spec:
gateways:
- mesh
hosts:
- www.example.com
- example.com
http:
- match:
- headers:
x-custom-header:
exact: ok
x-prefix-header:
prefix: start
route:
- destination:
host: has-header
- match:
- headers:
x-custom-header: {}
route:
- destination:
host: has-present-header
- match:
route:
- destination:
host: default
33 changes: 33 additions & 0 deletions internal/pkg/unit/testcases/header/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
TestCases:
- description: match headers with a variety of values
wantMatch: true
request:
authority: ["www.example.com"]
method: ["GET"]
uri: ["/headers/v1"]
headers:
x-custom-header: ok
x-prefix-header: start-with-prefix
route:
- destination:
host: has-header
- description: match a header by presence
wantMatch: true
request:
authority: ["www.example.com"]
method: ["GET"]
uri: ["/headers/v1"]
headers:
x-custom-header: ok
route:
- destination:
host: has-present-header
- description: missing headers goes to default
wantMatch: true
request:
authority: ["www.example.com"]
method: ["GET"]
uri: ["/headers/v1"]
route:
- destination:
host: default
Loading