From ec08cac21496b34b123b75b06d9283eb6539e890 Mon Sep 17 00:00:00 2001 From: Aris Boutselis Date: Fri, 24 Nov 2023 10:09:54 +0000 Subject: [PATCH] feat: add Gateway analysers (#764) * feat: add GatewayClass analyser Signed-off-by: Aris Boutselis * chore: add a valid GW class object Signed-off-by: Aris Boutselis * feat: add gw analyzer and switch to controller-runtime client Signed-off-by: Aris Boutselis * chore: add unit tests for gw analyser Signed-off-by: Aris Boutselis * chore: replace constants with condition status Signed-off-by: Aris Boutselis * feat: add httproute analyzer Signed-off-by: Aris Boutselis * feat: add HTTPRoute individual tests. Signed-off-by: Aris Boutselis * docs: add analyzers Signed-off-by: Aris Boutselis --------- Signed-off-by: Aris Boutselis Signed-off-by: Aris Boutselis Co-authored-by: Aris Boutselis --- README.md | 3 + go.mod | 31 +-- go.sum | 61 +++-- pkg/analyzer/analyzer.go | 3 + pkg/analyzer/gateway.go | 108 +++++++++ pkg/analyzer/gateway_test.go | 161 +++++++++++++ pkg/analyzer/gatewayclass.go | 84 +++++++ pkg/analyzer/gatewayclass_test.go | 51 ++++ pkg/analyzer/httroute.go | 228 ++++++++++++++++++ pkg/analyzer/httroute_test.go | 374 ++++++++++++++++++++++++++++++ pkg/common/types.go | 4 + pkg/kubernetes/kubernetes.go | 11 + pkg/kubernetes/types.go | 2 + pkg/util/util.go | 11 + 14 files changed, 1093 insertions(+), 39 deletions(-) create mode 100644 pkg/analyzer/gateway.go create mode 100644 pkg/analyzer/gateway_test.go create mode 100644 pkg/analyzer/gatewayclass.go create mode 100644 pkg/analyzer/gatewayclass_test.go create mode 100644 pkg/analyzer/httroute.go create mode 100644 pkg/analyzer/httroute_test.go diff --git a/README.md b/README.md index 7b17e01dd1..52852f30ed 100644 --- a/README.md +++ b/README.md @@ -159,6 +159,9 @@ you will be able to write your own analyzers. - [x] hpaAnalyzer - [x] pdbAnalyzer - [x] networkPolicyAnalyzer +- [x] gatewayClass +- [x] gateway +- [x] httproute ## Examples diff --git a/go.mod b/go.mod index bec3f82608..26035a4848 100644 --- a/go.mod +++ b/go.mod @@ -32,6 +32,8 @@ require ( github.com/aws/aws-sdk-go v1.48.3 github.com/cohere-ai/cohere-go v0.2.0 github.com/olekukonko/tablewriter v0.0.5 + sigs.k8s.io/controller-runtime v0.16.3 + sigs.k8s.io/gateway-api v1.0.0 google.golang.org/api v0.151.0 ) @@ -47,6 +49,7 @@ require ( github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092 // indirect github.com/cohere-ai/tokenizer v1.1.1 // indirect github.com/dlclark/regexp2 v1.4.0 // indirect + github.com/evanphx/json-patch/v5 v5.7.0 // indirect github.com/golang-jwt/jwt/v5 v5.0.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49 // indirect @@ -55,6 +58,7 @@ require ( github.com/googleapis/gax-go/v2 v2.12.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/kylelemons/godebug v1.1.0 // indirect + github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect github.com/sagikazarmark/locafero v0.3.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect @@ -95,16 +99,16 @@ require ( github.com/docker/go-metrics v0.0.1 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect - github.com/evanphx/json-patch v5.6.0+incompatible // indirect + github.com/evanphx/json-patch v5.7.0+incompatible // indirect github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect - github.com/fsnotify/fsnotify v1.6.0 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/go-errors/errors v1.4.2 // indirect github.com/go-gorp/gorp/v3 v3.1.0 // indirect github.com/go-logr/logr v1.2.4 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-openapi/jsonpointer v0.19.6 // indirect + github.com/go-openapi/jsonpointer v0.20.0 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect - github.com/go-openapi/swag v0.22.3 // indirect + github.com/go-openapi/swag v0.22.4 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.3 // indirect @@ -122,7 +126,7 @@ require ( github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/huandu/xstrings v1.4.0 // indirect - github.com/imdario/mergo v0.3.15 // indirect + github.com/imdario/mergo v0.3.16 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jmoiron/sqlx v1.3.5 // indirect github.com/josharian/intern v1.0.0 // indirect @@ -137,7 +141,6 @@ require ( github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.14 // indirect - github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect @@ -158,9 +161,9 @@ require ( github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.17.0 - github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 // indirect - github.com/prometheus/common v0.44.0 // indirect - github.com/prometheus/procfs v0.11.1 // indirect + github.com/prometheus/client_model v0.5.0 // indirect + github.com/prometheus/common v0.45.0 // indirect + github.com/prometheus/procfs v0.12.0 // indirect github.com/rivo/uniseg v0.4.4 // indirect github.com/robfig/cron/v3 v3.0.1 github.com/rubenv/sql-migrate v1.5.2 // indirect @@ -191,7 +194,7 @@ require ( golang.org/x/text v0.13.0 // indirect golang.org/x/time v0.3.0 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect - google.golang.org/appengine v1.6.7 // indirect + google.golang.org/appengine v1.6.8 // indirect google.golang.org/grpc v1.59.0 google.golang.org/protobuf v1.31.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect @@ -203,14 +206,14 @@ require ( k8s.io/cli-runtime v0.28.4 // indirect k8s.io/component-base v0.28.4 // indirect k8s.io/klog/v2 v2.100.1 // indirect - k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 // indirect - k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 // indirect + k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 // indirect + k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect oras.land/oras-go v1.2.4 // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/kustomize/api v0.13.5-0.20230601165947-6ce0bf390ce3 // indirect sigs.k8s.io/kustomize/kyaml v0.14.3-0.20230601165947-6ce0bf390ce3 // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect - sigs.k8s.io/yaml v1.3.0 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.3.0 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect ) // v1.2.0 is taken from github.com/open-policy-agent/opa v0.42.0 diff --git a/go.sum b/go.sum index 186bccc619..1e9db93415 100644 --- a/go.sum +++ b/go.sum @@ -776,8 +776,10 @@ github.com/envoyproxy/go-control-plane v0.10.3/go.mod h1:fJJn/j26vwOu972OllsvAgJ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v0.6.7/go.mod h1:dyJXwwfPK2VSqiB9Klm1J6romD608Ba7Hij42vrOBCo= github.com/envoyproxy/protoc-gen-validate v0.9.1/go.mod h1:OKNgG7TCp5pF4d6XftA0++PMirau2/yoOwVac3AbF2w= -github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U= -github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch v5.7.0+incompatible h1:vgGkfT/9f8zE6tvSCe74nfpAVDQ2tG6yudJd8LBksgI= +github.com/evanphx/json-patch v5.7.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch/v5 v5.7.0 h1:nJqP7uwL84RJInrohHfW0Fx3awjbm8qZeFv0nW9SYGc= +github.com/evanphx/json-patch/v5 v5.7.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f h1:Wl78ApPPB2Wvf/TIe2xdyJxTlb6obmF18d8QdkxNDu4= github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f/go.mod h1:OSYXu++VVOHnXeitef/D8n/6y4QV8uLHSFXX4NeXMGc= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= @@ -788,8 +790,8 @@ github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/ github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/foxcpp/go-mockdns v1.0.0 h1:7jBqxd3WDWwi/6WhDvacvH1XsN3rOLXyHM1uhvIx6FI= github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= -github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= -github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= @@ -817,12 +819,15 @@ github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= +github.com/go-logr/zapr v1.2.4 h1:QHVo+6stLbfJmYGkQ7uGHUCu5hnAFAj6mDe6Ea0SeOo= github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonpointer v0.20.0 h1:ESKJdU9ASRfaPNOPRx12IUyA1vn3R9GiE3KYD14BXdQ= +github.com/go-openapi/jsonpointer v0.20.0/go.mod h1:6PGzBjjIIumbLYysB73Klnms1mwnU4G3YHOECG3CedA= github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= -github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU= +github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-pdf/fpdf v0.5.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M= github.com/go-pdf/fpdf v0.6.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M= github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= @@ -994,8 +999,8 @@ github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47 github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= -github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM= -github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= +github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= +github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= @@ -1076,8 +1081,8 @@ github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A github.com/mattn/go-sqlite3 v1.14.14/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= -github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg= +github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k= github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA= github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY= github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3/go.mod h1:RagcQ7I8IeTMnF8JTXieKnO4Z6JCsikNEzj0DwauVzE= @@ -1157,17 +1162,17 @@ github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1: github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 h1:v7DLqVdK4VrYkVD5diGdl4sxJurKJEMnODWRJlxV9oM= -github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= +github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= +github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= -github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY= -github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY= +github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM= +github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= -github.com/prometheus/procfs v0.11.1 h1:xRC8Iq1yyca5ypa9n1EZnWZkt7dwcoRPQwX/5gwaUuI= -github.com/prometheus/procfs v0.11.1/go.mod h1:eesXgaPo1q7lBpVMoMy0ZOFTth9hBn4W/y0/p/ScXhY= +github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= +github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= @@ -1548,7 +1553,6 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1660,6 +1664,7 @@ golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNq golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= gonum.org/v1/gonum v0.9.3/go.mod h1:TZumC3NeyVQskjXqmyWt4S3bINhy7B4eYwW69EbyX+0= @@ -1733,8 +1738,9 @@ google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7 google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= +google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= @@ -1988,12 +1994,12 @@ k8s.io/component-base v0.28.4 h1:c/iQLWPdUgI90O+T9TeECg8o7N3YJTiuz2sKxILYcYo= k8s.io/component-base v0.28.4/go.mod h1:m9hR0uvqXDybiGL2nf/3Lf0MerAfQXzkfWhUY58JUbU= k8s.io/klog/v2 v2.100.1 h1:7WCHKK6K8fNhTqfBhISHQ97KrnJNFZMcQvKp7gP/tmg= k8s.io/klog/v2 v2.100.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= -k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 h1:LyMgNKD2P8Wn1iAwQU5OhxCKlKJy0sHc+PcDwFB24dQ= -k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9/go.mod h1:wZK2AVp1uHCp4VamDVgBP2COHZjqD1T68Rf0CM3YjSM= +k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 h1:aVUu9fTY98ivBPKR9Y5w/AuzbMm96cd3YHRTU83I780= +k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00/go.mod h1:AsvuZPBlUDVuCdzJ87iajxtXuR9oktsTctW/R9wwouA= k8s.io/kubectl v0.28.4 h1:gWpUXW/T7aFne+rchYeHkyB8eVDl5UZce8G4X//kjUQ= k8s.io/kubectl v0.28.4/go.mod h1:CKOccVx3l+3MmDbkXtIUtibq93nN2hkDR99XDCn7c/c= -k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 h1:qY1Ad8PODbnymg2pRbkyMT/ylpTrCM8P2RJ0yroCyIk= -k8s.io/utils v0.0.0-20230406110748-d93618cff8a2/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI= +k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= modernc.org/cc/v3 v3.36.0/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= @@ -2034,13 +2040,18 @@ rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8 rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +sigs.k8s.io/controller-runtime v0.16.3 h1:2TuvuokmfXvDUamSx1SuAOO3eTyye+47mJCigwG62c4= +sigs.k8s.io/controller-runtime v0.16.3/go.mod h1:j7bialYoSn142nv9sCOJmQgDXQXxnroFU4VnX/brVJ0= +sigs.k8s.io/gateway-api v1.0.0 h1:iPTStSv41+d9p0xFydll6d7f7MOBGuqXM6p2/zVYMAs= +sigs.k8s.io/gateway-api v1.0.0/go.mod h1:4cUgr0Lnp5FZ0Cdq8FdRwCvpiWws7LVhLHGIudLlf4c= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= sigs.k8s.io/kustomize/api v0.13.5-0.20230601165947-6ce0bf390ce3 h1:XX3Ajgzov2RKUdc5jW3t5jwY7Bo7dcRm+tFxT+NfgY0= sigs.k8s.io/kustomize/api v0.13.5-0.20230601165947-6ce0bf390ce3/go.mod h1:9n16EZKMhXBNSiUC5kSdFQJkdH3zbxS/JoO619G1VAY= sigs.k8s.io/kustomize/kyaml v0.14.3-0.20230601165947-6ce0bf390ce3 h1:W6cLQc5pnqM7vh3b7HvGNfXrJ/xL6BDMS0v1V/HHg5U= sigs.k8s.io/kustomize/kyaml v0.14.3-0.20230601165947-6ce0bf390ce3/go.mod h1:JWP1Fj0VWGHyw3YUPjXSQnRnrwezrZSrApfX5S0nIag= -sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE= -sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E= -sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= +sigs.k8s.io/structured-merge-diff/v4 v4.3.0 h1:UZbZAZfX0wV2zr7YZorDz6GXROfDFj6LvqCRm4VUVKk= +sigs.k8s.io/structured-merge-diff/v4 v4.3.0/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/pkg/analyzer/analyzer.go b/pkg/analyzer/analyzer.go index 0c6ac9f2fa..d63093c7e9 100644 --- a/pkg/analyzer/analyzer.go +++ b/pkg/analyzer/analyzer.go @@ -50,6 +50,9 @@ var additionalAnalyzerMap = map[string]common.IAnalyzer{ "PodDisruptionBudget": PdbAnalyzer{}, "NetworkPolicy": NetworkPolicyAnalyzer{}, "Log": LogAnalyzer{}, + "GatewayClass": GatewayClassAnalyzer{}, + "Gateway": GatewayAnalyzer{}, + "HTTPRoute": HTTPRouteAnalyzer{}, } func ListFilters() ([]string, []string, []string) { diff --git a/pkg/analyzer/gateway.go b/pkg/analyzer/gateway.go new file mode 100644 index 0000000000..a67cc4e6c6 --- /dev/null +++ b/pkg/analyzer/gateway.go @@ -0,0 +1,108 @@ +/* +Copyright 2023 The K8sGPT Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package analyzer + +import ( + "fmt" + + "github.com/k8sgpt-ai/k8sgpt/pkg/common" + "github.com/k8sgpt-ai/k8sgpt/pkg/util" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + ctrl "sigs.k8s.io/controller-runtime/pkg/client" + gtwapi "sigs.k8s.io/gateway-api/apis/v1" +) + +type GatewayAnalyzer struct{} + +// Gateway analyser will analyse all different Kinds and search for missing object dependencies +func (GatewayAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) { + + kind := "Gateway" + AnalyzerErrorsMetric.DeletePartialMatch(map[string]string{ + "analyzer_name": kind, + }) + + gtwList := >wapi.GatewayList{} + gc := >wapi.GatewayClass{} + client := a.Client.CtrlClient + gtwapi.AddToScheme(client.Scheme()) + if err := client.List(a.Context, gtwList, &ctrl.ListOptions{}); err != nil { + return nil, err + } + + var preAnalysis = map[string]common.PreAnalysis{} + // Find all unhealthy gateway Classes + + for _, gtw := range gtwList.Items { + var failures []common.Failure + + gtwName := gtw.GetName() + gtwNamespace := gtw.GetNamespace() + // Check if gatewayclass exists + err := client.Get(a.Context, ctrl.ObjectKey{Namespace: gtwNamespace, Name: string(gtw.Spec.GatewayClassName)}, gc, &ctrl.GetOptions{}) + if errors.IsNotFound(err) { + failures = append(failures, common.Failure{ + Text: fmt.Sprintf( + "Gateway uses the GatewayClass %s which does not exist.", + gtw.Spec.GatewayClassName, + ), + Sensitive: []common.Sensitive{ + { + Unmasked: string(gtw.Spec.GatewayClassName), + Masked: util.MaskString(string(gtw.Spec.GatewayClassName)), + }, + }, + }) + } + + // Check only the current conditions + // TODO: maybe check other statuses Listeners, addresses? + if gtw.Status.Conditions[0].Status != metav1.ConditionTrue { + failures = append(failures, common.Failure{ + Text: fmt.Sprintf("Gateway '%s/%s' is not accepted. Message: '%s'.", + gtwNamespace, + gtwName, + gtw.Status.Conditions[0].Message, + ), + Sensitive: []common.Sensitive{ + { + Unmasked: gtwNamespace, + Masked: util.MaskString(gtwNamespace), + }, + { + Unmasked: gtwName, + Masked: util.MaskString(gtwName), + }, + }, + }) + } + if len(failures) > 0 { + preAnalysis[fmt.Sprintf("%s/%s", gtwNamespace, gtwName)] = common.PreAnalysis{ + Gateway: gtw, + FailureDetails: failures, + } + AnalyzerErrorsMetric.WithLabelValues(kind, gtwName, gtwNamespace).Set(float64(len(failures))) + } + } + for key, value := range preAnalysis { + var currentAnalysis = common.Result{ + Kind: kind, + Name: key, + Error: value.FailureDetails, + } + a.Results = append(a.Results, currentAnalysis) + } + return a.Results, nil +} diff --git a/pkg/analyzer/gateway_test.go b/pkg/analyzer/gateway_test.go new file mode 100644 index 0000000000..44d6893c80 --- /dev/null +++ b/pkg/analyzer/gateway_test.go @@ -0,0 +1,161 @@ +package analyzer + +import ( + "context" + "testing" + + "github.com/k8sgpt-ai/k8sgpt/pkg/common" + "github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes" + "github.com/magiconair/properties/assert" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/kubernetes/scheme" + fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake" + gtwapi "sigs.k8s.io/gateway-api/apis/v1" +) + +func BuildGatewayClass(name string) gtwapi.GatewayClass { + GatewayClass := gtwapi.GatewayClass{} + GatewayClass.Name = name + // Namespace is not needed outside of this test, GatewayClass is cluster-scoped + GatewayClass.Namespace = "default" + GatewayClass.Spec.ControllerName = "gateway.fooproxy.io/gatewayclass-controller" + + return GatewayClass +} + +func BuildGateway(className gtwapi.ObjectName, status metav1.ConditionStatus) gtwapi.Gateway { + Gateway := gtwapi.Gateway{} + Gateway.Name = "foobar" + Gateway.Namespace = "default" + Gateway.Spec.GatewayClassName = className + Gateway.Spec.Listeners = []gtwapi.Listener{ + { + Name: "proxy", + Port: 80, + Protocol: gtwapi.HTTPProtocolType, + }, + } + Condition := metav1.Condition{ + Type: "Accepted", + Status: status, + Message: "An expected message", + Reason: "Test", + } + Gateway.Status.Conditions = []metav1.Condition{Condition} + + return Gateway +} + +func TestGatewayAnalyzer(t *testing.T) { + ClassName := gtwapi.ObjectName("exists") + AcceptedStatus := metav1.ConditionTrue + GatewayClass := BuildGatewayClass(string(ClassName)) + + Gateway := BuildGateway(ClassName, AcceptedStatus) + // Create a Gateway Analyzer instance with the fake client + scheme := scheme.Scheme + gtwapi.Install(scheme) + apiextensionsv1.AddToScheme(scheme) + objects := []runtime.Object{ + &Gateway, + &GatewayClass, + } + + fakeClient := fakeclient.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(objects...).Build() + + analyzerInstance := GatewayAnalyzer{} + config := common.Analyzer{ + Client: &kubernetes.Client{ + CtrlClient: fakeClient, + }, + Context: context.Background(), + Namespace: "default", + } + analysisResults, err := analyzerInstance.Analyze(config) + if err != nil { + t.Error(err) + } + assert.Equal(t, len(analysisResults), 0) + +} + +func TestMissingClassGatewayAnalyzer(t *testing.T) { + ClassName := gtwapi.ObjectName("non-existed") + AcceptedStatus := metav1.ConditionTrue + Gateway := BuildGateway(ClassName, AcceptedStatus) + + // Create a Gateway Analyzer instance with the fake client + scheme := scheme.Scheme + gtwapi.Install(scheme) + apiextensionsv1.AddToScheme(scheme) + objects := []runtime.Object{ + &Gateway, + } + + fakeClient := fakeclient.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(objects...).Build() + + analyzerInstance := GatewayAnalyzer{} + config := common.Analyzer{ + Client: &kubernetes.Client{ + CtrlClient: fakeClient, + }, + Context: context.Background(), + Namespace: "default", + } + analysisResults, err := analyzerInstance.Analyze(config) + if err != nil { + t.Error(err) + } + assert.Equal(t, len(analysisResults), 1) + +} + +func TestStatusGatewayAnalyzer(t *testing.T) { + ClassName := gtwapi.ObjectName("exists") + AcceptedStatus := metav1.ConditionUnknown + GatewayClass := BuildGatewayClass(string(ClassName)) + + Gateway := BuildGateway(ClassName, AcceptedStatus) + + // Create a Gateway Analyzer instance with the fake client + scheme := scheme.Scheme + gtwapi.Install(scheme) + apiextensionsv1.AddToScheme(scheme) + objects := []runtime.Object{ + &Gateway, + &GatewayClass, + } + + fakeClient := fakeclient.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(objects...).Build() + + analyzerInstance := GatewayAnalyzer{} + config := common.Analyzer{ + Client: &kubernetes.Client{ + CtrlClient: fakeClient, + }, + Context: context.Background(), + Namespace: "default", + } + analysisResults, err := analyzerInstance.Analyze(config) + if err != nil { + t.Error(err) + } + var errorFound bool + want := "Gateway 'default/foobar' is not accepted. Message: 'An expected message'." + for _, analysis := range analysisResults { + for _, got := range analysis.Error { + if want == got.Text { + errorFound = true + } + } + if errorFound { + break + } + } + + if !errorFound { + t.Errorf("Expected message, <%v> , not found in Gateway's analysis results", want) + } +} diff --git a/pkg/analyzer/gatewayclass.go b/pkg/analyzer/gatewayclass.go new file mode 100644 index 0000000000..50a58eff59 --- /dev/null +++ b/pkg/analyzer/gatewayclass.go @@ -0,0 +1,84 @@ +/* +Copyright 2023 The K8sGPT Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package analyzer + +import ( + "fmt" + + "github.com/k8sgpt-ai/k8sgpt/pkg/common" + "github.com/k8sgpt-ai/k8sgpt/pkg/util" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + ctrl "sigs.k8s.io/controller-runtime/pkg/client" + gtwapi "sigs.k8s.io/gateway-api/apis/v1" +) + +type GatewayClassAnalyzer struct{} + +// Gateway analyser will analyse all different Kinds and search for missing object dependencies +func (GatewayClassAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) { + + kind := "GatewayClass" + AnalyzerErrorsMetric.DeletePartialMatch(map[string]string{ + "analyzer_name": kind, + }) + + gcList := >wapi.GatewayClassList{} + client := a.Client.CtrlClient + gtwapi.AddToScheme(client.Scheme()) + if err := client.List(a.Context, gcList, &ctrl.ListOptions{}); err != nil { + return nil, err + } + var preAnalysis = map[string]common.PreAnalysis{} + + // Find all unhealthy gateway Classes + + for _, gc := range gcList.Items { + var failures []common.Failure + + gcName := gc.GetName() + // Check only the current condition + if gc.Status.Conditions[0].Status != metav1.ConditionTrue { + failures = append(failures, common.Failure{ + Text: fmt.Sprintf( + "GatewayClass '%s' with a controller name '%s' is not accepted. Message: '%s'.", + gcName, + gc.Spec.ControllerName, + gc.Status.Conditions[0].Message, + ), + Sensitive: []common.Sensitive{ + { + Unmasked: gcName, + Masked: util.MaskString(gcName), + }, + }, + }) + } + if len(failures) > 0 { + preAnalysis[gcName] = common.PreAnalysis{ + GatewayClass: gc, + FailureDetails: failures, + } + AnalyzerErrorsMetric.WithLabelValues(kind, gcName, "").Set(float64(len(failures))) + } + } + for key, value := range preAnalysis { + var currentAnalysis = common.Result{ + Kind: kind, + Name: key, + Error: value.FailureDetails, + } + a.Results = append(a.Results, currentAnalysis) + } + return a.Results, nil +} diff --git a/pkg/analyzer/gatewayclass_test.go b/pkg/analyzer/gatewayclass_test.go new file mode 100644 index 0000000000..1f1604a98c --- /dev/null +++ b/pkg/analyzer/gatewayclass_test.go @@ -0,0 +1,51 @@ +package analyzer + +import ( + "context" + "testing" + + "github.com/k8sgpt-ai/k8sgpt/pkg/common" + "github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes" + "github.com/stretchr/testify/assert" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/scheme" + fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake" + gtwapi "sigs.k8s.io/gateway-api/apis/v1" +) + +// Testing with the fake dynamic client if GatewayClasses have an accepted status +func TestGatewayClassAnalyzer(t *testing.T) { + GatewayClass := >wapi.GatewayClass{} + GatewayClass.Name = "foobar" + GatewayClass.Spec.ControllerName = "gateway.fooproxy.io/gatewayclass-controller" + // Initialize Conditions slice before setting properties + BadCondition := metav1.Condition{ + Type: "Accepted", + Status: "Uknown", + Message: "Waiting for controller", + Reason: "Pending", + } + GatewayClass.Status.Conditions = []metav1.Condition{BadCondition} + // Create a GatewayClassAnalyzer instance with the fake client + scheme := scheme.Scheme + gtwapi.Install(scheme) + apiextensionsv1.AddToScheme(scheme) + + fakeClient := fakeclient.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(GatewayClass).Build() + + analyzerInstance := GatewayClassAnalyzer{} + config := common.Analyzer{ + Client: &kubernetes.Client{ + CtrlClient: fakeClient, + }, + Context: context.Background(), + Namespace: "default", + } + analysisResults, err := analyzerInstance.Analyze(config) + if err != nil { + t.Error(err) + } + assert.Equal(t, len(analysisResults), 1) + +} diff --git a/pkg/analyzer/httroute.go b/pkg/analyzer/httroute.go new file mode 100644 index 0000000000..d85a20678b --- /dev/null +++ b/pkg/analyzer/httroute.go @@ -0,0 +1,228 @@ +/* +Copyright 2023 The K8sGPT Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package analyzer + +import ( + "fmt" + + "github.com/k8sgpt-ai/k8sgpt/pkg/common" + "github.com/k8sgpt-ai/k8sgpt/pkg/util" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + ctrl "sigs.k8s.io/controller-runtime/pkg/client" + gtwapi "sigs.k8s.io/gateway-api/apis/v1" +) + +type HTTPRouteAnalyzer struct{} + +// Gateway analyser will analyse all different Kinds and search for missing object dependencies +func (HTTPRouteAnalyzer) Analyze(a common.Analyzer) ([]common.Result, error) { + + kind := "HTTPRoute" + AnalyzerErrorsMetric.DeletePartialMatch(map[string]string{ + "analyzer_name": kind, + }) + + routeList := >wapi.HTTPRouteList{} + gtw := >wapi.Gateway{} + service := &corev1.Service{} + client := a.Client.CtrlClient + gtwapi.AddToScheme(client.Scheme()) + if err := client.List(a.Context, routeList, &ctrl.ListOptions{}); err != nil { + return nil, err + } + var preAnalysis = map[string]common.PreAnalysis{} + + // Find all unhealthy gateway Classes + + for _, route := range routeList.Items { + var failures []common.Failure + + // Check if Gateways exists in the same or designated namespace + // TODO: when meshes and ClusterIp options are adopted we can add more checks + // e.g Service Port matching + for _, gtwref := range route.Spec.ParentRefs { + namespace := route.Namespace + if gtwref.Namespace != nil { + namespace = string(*gtwref.Namespace) + } + err := client.Get(a.Context, ctrl.ObjectKey{Namespace: namespace, Name: string(gtwref.Name)}, gtw, &ctrl.GetOptions{}) + if errors.IsNotFound(err) { + failures = append(failures, common.Failure{ + Text: fmt.Sprintf( + "HTTPRoute uses the Gateway '%s/%s' which does not exist in the same namespace.", + namespace, + gtwref.Name, + ), + Sensitive: []common.Sensitive{ + { + Unmasked: gtw.Namespace, + Masked: util.MaskString(gtw.Namespace), + }, + { + Unmasked: gtw.Name, + Masked: util.MaskString(gtw.Name), + }, + }, + }) + } else { + // Check if the aforementioned Gateway allows the HTTPRoutes from the route's namespace + for _, listener := range gtw.Spec.Listeners { + if listener.AllowedRoutes.Namespaces != nil { + switch allow := listener.AllowedRoutes.Namespaces.From; { + case *allow == gtwapi.NamespacesFromSame: + // check if Gateway is in the same namespace + if route.Namespace != gtw.Namespace { + failures = append(failures, common.Failure{ + Text: fmt.Sprintf("HTTPRoute '%s/%s' is deployed in a different namespace from Gateway '%s/%s' which only allows HTTPRoutes from its namespace.", + route.Namespace, + route.Name, + gtw.Namespace, + gtw.Name, + ), + Sensitive: []common.Sensitive{ + { + Unmasked: route.Namespace, + Masked: util.MaskString(route.Namespace), + }, + { + Unmasked: route.Name, + Masked: util.MaskString(route.Name), + }, + { + Unmasked: gtw.Namespace, + Masked: util.MaskString(gtw.Namespace), + }, + { + Unmasked: gtw.Name, + Masked: util.MaskString(gtw.Name), + }, + }, + }) + } + case *allow == gtwapi.NamespacesFromSelector: + // check if our route include the same selector Label + if !util.LabelsIncludeAny(listener.AllowedRoutes.Namespaces.Selector.MatchLabels, route.Labels) { + failures = append(failures, common.Failure{ + Text: fmt.Sprintf( + "HTTPRoute '%s/%s' can't be attached on Gateway '%s/%s', selector labels do not match HTTProute's labels.", + route.Namespace, + route.Name, + gtw.Namespace, + gtw.Name, + ), + Sensitive: []common.Sensitive{ + { + Unmasked: route.Namespace, + Masked: util.MaskString(route.Namespace), + }, + { + Unmasked: route.Name, + Masked: util.MaskString(route.Name), + }, + { + Unmasked: gtw.Namespace, + Masked: util.MaskString(gtw.Namespace), + }, + { + Unmasked: gtw.Name, + Masked: util.MaskString(gtw.Name), + }, + }, + }) + + } + + } + } + } + } + + } + // Check if the Backends are valid services and ports are matching with services Ports + for _, rule := range route.Spec.Rules { + for _, backend := range rule.BackendRefs { + err := client.Get(a.Context, ctrl.ObjectKey{Namespace: route.Namespace, Name: string(backend.Name)}, service, &ctrl.GetOptions{}) + if errors.IsNotFound(err) { + failures = append(failures, common.Failure{ + Text: fmt.Sprintf( + "HTTPRoute uses the Service '%s/%s' which does not exist.", + route.Namespace, + backend.Name, + ), + Sensitive: []common.Sensitive{ + { + Unmasked: service.Namespace, + Masked: util.MaskString(service.Namespace), + }, + { + Unmasked: service.Name, + Masked: util.MaskString(service.Name), + }, + }, + }) + } else { + portMatch := false + for _, svcPort := range service.Spec.Ports { + if int32(*backend.Port) == svcPort.Port { + portMatch = true + } + } + if !portMatch { + failures = append(failures, common.Failure{ + Text: fmt.Sprintf( + "HTTPRoute's backend service '%s' is using port '%d' but the corresponding K8s service '%s/%s' isn't configured with the same port.", + backend.Name, + int32(*backend.Port), + service.Namespace, + service.Name, + ), + Sensitive: []common.Sensitive{ + { + Unmasked: string(backend.Name), + Masked: util.MaskString(string(backend.Name)), + }, + { + Unmasked: service.Name, + Masked: util.MaskString(service.Name), + }, + { + Unmasked: service.Namespace, + Masked: service.Namespace, + }, + }, + }) + } + } + } + } + if len(failures) > 0 { + preAnalysis[fmt.Sprintf("%s/%s", route.Namespace, route.Name)] = common.PreAnalysis{ + HTTPRoute: route, + FailureDetails: failures, + } + AnalyzerErrorsMetric.WithLabelValues(kind, route.Name, route.Namespace).Set(float64(len(failures))) + } + } + for key, value := range preAnalysis { + var currentAnalysis = common.Result{ + Kind: kind, + Name: key, + Error: value.FailureDetails, + } + a.Results = append(a.Results, currentAnalysis) + } + return a.Results, nil + +} diff --git a/pkg/analyzer/httroute_test.go b/pkg/analyzer/httroute_test.go new file mode 100644 index 0000000000..b2f914110b --- /dev/null +++ b/pkg/analyzer/httroute_test.go @@ -0,0 +1,374 @@ +package analyzer + +import ( + "context" + "testing" + + "github.com/k8sgpt-ai/k8sgpt/pkg/common" + "github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes" + corev1 "k8s.io/api/core/v1" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/client-go/kubernetes/scheme" + fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake" + gtwapi "sigs.k8s.io/gateway-api/apis/v1" +) + +func BuildRouteGateway(namespace, name, fromNamespaceref string) gtwapi.Gateway { + routeNamespace := >wapi.RouteNamespaces{} + switch fromNamespaceref { + case "Same": + fromSame := gtwapi.NamespacesFromSame + routeNamespace.From = &fromSame + case "Selector": + fromSelector := gtwapi.NamespacesFromSelector + routeNamespace.From = &fromSelector + routeNamespace.Selector = &metav1.LabelSelector{} + routeNamespace.Selector.MatchLabels = map[string]string{"foo": "bar"} + + default: + fromAll := gtwapi.NamespacesFromAll + routeNamespace.From = &fromAll + } + Gateway := gtwapi.Gateway{} + Gateway.Name = name + Gateway.Namespace = namespace + Gateway.Spec.GatewayClassName = "fooclassName" + Gateway.Spec.Listeners = []gtwapi.Listener{ + { + Name: "proxy", + Port: 80, + Protocol: gtwapi.HTTPProtocolType, + AllowedRoutes: >wapi.AllowedRoutes{ + Namespaces: routeNamespace, + }, + }, + } + Condition := metav1.Condition{ + Type: "Accepted", + Status: "True", + Message: "An expected message", + Reason: "Test", + } + Gateway.Status.Conditions = []metav1.Condition{Condition} + + return Gateway +} + +func BuildHTTPRoute(backendName, gtwName gtwapi.ObjectName, gtwNamespace gtwapi.Namespace, svcPort *gtwapi.PortNumber, namespace string) gtwapi.HTTPRoute { + HTTPRoute := gtwapi.HTTPRoute{} + HTTPRoute.Name = "foohttproute" + HTTPRoute.Namespace = namespace + HTTPRoute.Spec.ParentRefs = []gtwapi.ParentReference{ + { + Name: gtwName, + Namespace: >wNamespace, + }, + } + HTTPRoute.Spec.Rules = []gtwapi.HTTPRouteRule{ + { + BackendRefs: []gtwapi.HTTPBackendRef{ + { + BackendRef: gtwapi.BackendRef{ + BackendObjectReference: gtwapi.BackendObjectReference{ + Name: backendName, + Port: svcPort, + }, + }, + }, + }, + }, + } + return HTTPRoute +} + +/* + Testing different cases + +1. Gateway doesn't exist or at least doesn't exist in the same namespace +2. Gateway exists in different namespace, is configured in httproute's spec +and Gateway's configuration is allowing only from its same namespace +3. Gateway exists in the same namespace but has selectors different from route's labels +4. BackendRef is pointing to a non existent Service +5. BackendRef's port and Service Port are different +*/ +func TestGWMissiningHTTRouteAnalyzer(t *testing.T) { + backendName := gtwapi.ObjectName("foobackend") + gtwName := gtwapi.ObjectName("non-existent") + gtwNamespace := gtwapi.Namespace("non-existent") + svcPort := gtwapi.PortNumber(1027) + httpRouteNamespace := "default" + + HTTPRoute := BuildHTTPRoute(backendName, gtwName, gtwNamespace, &svcPort, httpRouteNamespace) + // Create a Gateway Analyzer instance with the fake client + scheme := scheme.Scheme + gtwapi.Install(scheme) + apiextensionsv1.AddToScheme(scheme) + objects := []runtime.Object{ + &HTTPRoute, + } + + fakeClient := fakeclient.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(objects...).Build() + + analyzerInstance := HTTPRouteAnalyzer{} + config := common.Analyzer{ + Client: &kubernetes.Client{ + CtrlClient: fakeClient, + }, + Context: context.Background(), + Namespace: "default", + } + analysisResults, err := analyzerInstance.Analyze(config) + if err != nil { + t.Error(err) + } + + var errorFound bool + want := "HTTPRoute uses the Gateway 'non-existent/non-existent' which does not exist in the same namespace." + for _, analysis := range analysisResults { + for _, got := range analysis.Error { + if want == got.Text { + errorFound = true + } + } + if errorFound { + break + } + } + + if !errorFound { + t.Errorf("Expected message, <%s> , not found in HTTPRoute's analysis results", want) + } + +} + +func TestGWConfigSameHTTRouteAnalyzer(t *testing.T) { + backendName := gtwapi.ObjectName("foobackend") + gtwName := gtwapi.ObjectName("gatewayname") + gtwNamespace := gtwapi.Namespace("differentnamespace") + svcPort := gtwapi.PortNumber(1027) + httpRouteNamespace := "default" + + HTTPRoute := BuildHTTPRoute(backendName, gtwName, gtwNamespace, &svcPort, httpRouteNamespace) + + Gateway := BuildRouteGateway("differentnamespace", "gatewayname", "Same") + // Create a Gateway Analyzer instance with the fake client + scheme := scheme.Scheme + gtwapi.Install(scheme) + apiextensionsv1.AddToScheme(scheme) + objects := []runtime.Object{ + &HTTPRoute, + &Gateway, + } + + fakeClient := fakeclient.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(objects...).Build() + + analyzerInstance := HTTPRouteAnalyzer{} + config := common.Analyzer{ + Client: &kubernetes.Client{ + CtrlClient: fakeClient, + }, + Context: context.Background(), + Namespace: "default", + } + analysisResults, err := analyzerInstance.Analyze(config) + if err != nil { + t.Error(err) + } + + var errorFound bool + want := "HTTPRoute 'default/foohttproute' is deployed in a different namespace from Gateway 'differentnamespace/gatewayname' which only allows HTTPRoutes from its namespace." + for _, analysis := range analysisResults { + for _, got := range analysis.Error { + if want == got.Text { + errorFound = true + } + } + if errorFound { + break + } + } + + if !errorFound { + t.Errorf("Expected message, <%s> , not found in HTTPRoute's analysis results", want) + } +} +func TestGWConfigSelectorHTTRouteAnalyzer(t *testing.T) { + backendName := gtwapi.ObjectName("foobackend") + gtwName := gtwapi.ObjectName("gatewayname") + gtwNamespace := gtwapi.Namespace("default") + svcPort := gtwapi.PortNumber(1027) + httpRouteNamespace := "default" + + HTTPRoute := BuildHTTPRoute(backendName, gtwName, gtwNamespace, &svcPort, httpRouteNamespace) + + Gateway := BuildRouteGateway("default", "gatewayname", "Selector") + // Create a Gateway Analyzer instance with the fake client + scheme := scheme.Scheme + gtwapi.Install(scheme) + apiextensionsv1.AddToScheme(scheme) + objects := []runtime.Object{ + &HTTPRoute, + &Gateway, + } + + fakeClient := fakeclient.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(objects...).Build() + + analyzerInstance := HTTPRouteAnalyzer{} + config := common.Analyzer{ + Client: &kubernetes.Client{ + CtrlClient: fakeClient, + }, + Context: context.Background(), + Namespace: "default", + } + analysisResults, err := analyzerInstance.Analyze(config) + if err != nil { + t.Error(err) + } + + var errorFound bool + want := "HTTPRoute 'default/foohttproute' can't be attached on Gateway 'default/gatewayname', selector labels do not match HTTProute's labels." + for _, analysis := range analysisResults { + for _, got := range analysis.Error { + if want == got.Text { + errorFound = true + } + } + if errorFound { + break + } + } + + if !errorFound { + t.Errorf("Expected message, <%s> , not found in HTTPRoute's analysis results", want) + } +} + +func TestSvcMissingHTTRouteAnalyzer(t *testing.T) { + backendName := gtwapi.ObjectName("foobackend") + gtwName := gtwapi.ObjectName("gatewayname") + gtwNamespace := gtwapi.Namespace("default") + svcPort := gtwapi.PortNumber(1027) + httpRouteNamespace := "default" + + HTTPRoute := BuildHTTPRoute(backendName, gtwName, gtwNamespace, &svcPort, httpRouteNamespace) + + Gateway := BuildRouteGateway("default", "gatewayname", "Same") + // Create a Gateway Analyzer instance with the fake client + scheme := scheme.Scheme + gtwapi.Install(scheme) + apiextensionsv1.AddToScheme(scheme) + objects := []runtime.Object{ + &HTTPRoute, + &Gateway, + } + + fakeClient := fakeclient.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(objects...).Build() + + analyzerInstance := HTTPRouteAnalyzer{} + config := common.Analyzer{ + Client: &kubernetes.Client{ + CtrlClient: fakeClient, + }, + Context: context.Background(), + Namespace: "default", + } + analysisResults, err := analyzerInstance.Analyze(config) + if err != nil { + t.Error(err) + } + + var errorFound bool + want := "HTTPRoute uses the Service 'default/foobackend' which does not exist." + for _, analysis := range analysisResults { + for _, got := range analysis.Error { + if want == got.Text { + errorFound = true + } + } + if errorFound { + break + } + } + + if !errorFound { + t.Errorf("Expected message, <%s> , not found in HTTPRoute's analysis results", want) + } +} +func TestSvcDifferentPortHTTRouteAnalyzer(t *testing.T) { + //Add a Service Object + Service := corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foobackend", + Namespace: "default", + }, + Spec: corev1.ServiceSpec{ + Selector: map[string]string{ + "app": "example-app", + }, + Ports: []corev1.ServicePort{ + { + Name: "http", + Protocol: "TCP", + Port: 80, + TargetPort: intstr.FromInt(8080), + }, + }, + Type: corev1.ServiceTypeClusterIP, + }, + } + backendName := gtwapi.ObjectName("foobackend") + gtwName := gtwapi.ObjectName("gatewayname") + gtwNamespace := gtwapi.Namespace("default") + // different port + svcPort := gtwapi.PortNumber(1027) + httpRouteNamespace := "default" + + HTTPRoute := BuildHTTPRoute(backendName, gtwName, gtwNamespace, &svcPort, httpRouteNamespace) + + Gateway := BuildRouteGateway("default", "gatewayname", "Same") + // Create a Gateway Analyzer instance with the fake client + scheme := scheme.Scheme + gtwapi.Install(scheme) + apiextensionsv1.AddToScheme(scheme) + objects := []runtime.Object{ + &HTTPRoute, + &Gateway, + &Service, + } + + fakeClient := fakeclient.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(objects...).Build() + + analyzerInstance := HTTPRouteAnalyzer{} + config := common.Analyzer{ + Client: &kubernetes.Client{ + CtrlClient: fakeClient, + }, + Context: context.Background(), + Namespace: "default", + } + analysisResults, err := analyzerInstance.Analyze(config) + if err != nil { + t.Error(err) + } + + var errorFound bool + want := "HTTPRoute's backend service 'foobackend' is using port '1027' but the corresponding K8s service 'default/foobackend' isn't configured with the same port." + for _, analysis := range analysisResults { + for _, got := range analysis.Error { + if want == got.Text { + errorFound = true + } + } + if errorFound { + break + } + } + + if !errorFound { + t.Errorf("Expected message, <%s> , not found in HTTPRoute's analysis results", want) + } +} diff --git a/pkg/common/types.go b/pkg/common/types.go index 6d781ceb56..53cc952278 100644 --- a/pkg/common/types.go +++ b/pkg/common/types.go @@ -26,6 +26,7 @@ import ( v1 "k8s.io/api/core/v1" networkv1 "k8s.io/api/networking/v1" policyv1 "k8s.io/api/policy/v1" + gtwapi "sigs.k8s.io/gateway-api/apis/v1" ) type IAnalyzer interface { @@ -57,6 +58,9 @@ type PreAnalysis struct { Node v1.Node ValidatingWebhook regv1.ValidatingWebhookConfiguration MutatingWebhook regv1.MutatingWebhookConfiguration + GatewayClass gtwapi.GatewayClass + Gateway gtwapi.Gateway + HTTPRoute gtwapi.HTTPRoute // Integrations TrivyVulnerabilityReport trivy.VulnerabilityReport TrivyConfigAuditReport trivy.ConfigAuditReport diff --git a/pkg/kubernetes/kubernetes.go b/pkg/kubernetes/kubernetes.go index 942aa054e5..7c96530bea 100644 --- a/pkg/kubernetes/kubernetes.go +++ b/pkg/kubernetes/kubernetes.go @@ -20,6 +20,7 @@ import ( "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" "k8s.io/kubectl/pkg/scheme" + ctrl "sigs.k8s.io/controller-runtime/pkg/client" ) func (c *Client) GetConfig() *rest.Config { @@ -34,6 +35,10 @@ func (c *Client) GetRestClient() rest.Interface { return c.RestClient } +func (c *Client) GetCtrlClient() ctrl.Client { + return c.CtrlClient +} + func NewClient(kubecontext string, kubeconfig string) (*Client, error) { var config *rest.Config config, err := rest.InClusterConfig() @@ -68,6 +73,11 @@ func NewClient(kubecontext string, kubeconfig string) (*Client, error) { return nil, err } + ctrlClient, err := ctrl.New(config, ctrl.Options{}) + if err != nil { + return nil, err + } + serverVersion, err := clientSet.ServerVersion() if err != nil { return nil, err @@ -76,6 +86,7 @@ func NewClient(kubecontext string, kubeconfig string) (*Client, error) { return &Client{ Client: clientSet, RestClient: restClient, + CtrlClient: ctrlClient, Config: config, ServerVersion: serverVersion, }, nil diff --git a/pkg/kubernetes/types.go b/pkg/kubernetes/types.go index b97745a5b7..0e013e0dcf 100644 --- a/pkg/kubernetes/types.go +++ b/pkg/kubernetes/types.go @@ -6,11 +6,13 @@ import ( "k8s.io/apimachinery/pkg/version" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" + ctrl "sigs.k8s.io/controller-runtime/pkg/client" ) type Client struct { Client kubernetes.Interface RestClient rest.Interface + CtrlClient ctrl.Client Config *rest.Config ServerVersion *version.Info } diff --git a/pkg/util/util.go b/pkg/util/util.go index c1a8168903..9ad38ac4bf 100644 --- a/pkg/util/util.go +++ b/pkg/util/util.go @@ -219,3 +219,14 @@ func MapToString(m map[string]string) string { } return result[:len(result)-1] } + +func LabelsIncludeAny(predefinedSelector, Labels map[string]string) bool { + // Check if any label in the predefinedSelector exists in Labels + for key := range predefinedSelector { + if _, exists := Labels[key]; exists { + return true + } + } + + return false +}