diff --git a/domain/kube-score.go b/domain/kube-score.go index 6e54d1b0..ebe8730a 100644 --- a/domain/kube-score.go +++ b/domain/kube-score.go @@ -3,6 +3,7 @@ package domain import ( "io" + routev1 "github.com/openshift/api/route/v1" appsv1 "k8s.io/api/apps/v1" autoscalingv1 "k8s.io/api/autoscaling/v1" corev1 "k8s.io/api/core/v1" @@ -60,6 +61,15 @@ type Ingress interface { FileLocationer } +type Route interface { + Route() routev1.Route + FileLocationer +} + +type Routes interface { + Routes() []Route +} + type Metas interface { Metas() []BothMeta } @@ -158,4 +168,5 @@ type AllTypes interface { CronJobs PodDisruptionBudgets HorizontalPodAutoscalers + Routes } diff --git a/go.mod b/go.mod index 68dae4ed..5b98f8ed 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ require ( github.com/fatih/color v1.16.0 github.com/google/go-cmp v0.6.0 github.com/mattn/go-isatty v0.0.20 + github.com/openshift/api v0.0.0-20240104110125-c7a2d3b41e1f github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.8.4 golang.org/x/term v0.15.0 diff --git a/go.sum b/go.sum index 2b4ab9dc..087489b7 100644 --- a/go.sum +++ b/go.sum @@ -33,6 +33,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/openshift/api v0.0.0-20240104110125-c7a2d3b41e1f h1:3BMVfQpz1xe8MmJprp1+NL8hrpl9I04JVP9EczdCOqE= +github.com/openshift/api v0.0.0-20240104110125-c7a2d3b41e1f/go.mod h1:CxgbWAlvu2iQB0UmKTtRu1YfepRg1/vJ64n2DlIEVz4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= diff --git a/parser/internal/route.go b/parser/internal/route.go new file mode 100644 index 00000000..b15bbf57 --- /dev/null +++ b/parser/internal/route.go @@ -0,0 +1,21 @@ +package internal + +import ( + routev1 "github.com/openshift/api/route/v1" + ks "github.com/zegl/kube-score/domain" +) + +var _ ks.Route = (*RouteV1)(nil) + +type RouteV1 struct { + Obj routev1.Route + Location ks.FileLocation +} + +func (r RouteV1) FileLocation() ks.FileLocation { + return r.Location +} + +func (r RouteV1) Route() routev1.Route { + return r.Obj +} diff --git a/parser/parse.go b/parser/parse.go index 601ae82f..79b3aa87 100644 --- a/parser/parse.go +++ b/parser/parse.go @@ -27,6 +27,7 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/serializer" + routev1 "github.com/openshift/api/route/v1" "github.com/zegl/kube-score/config" ks "github.com/zegl/kube-score/domain" "github.com/zegl/kube-score/parser/internal" @@ -68,6 +69,7 @@ func (p *Parser) addToScheme() error { batchv1beta1.AddToScheme, policyv1beta1.AddToScheme, policyv1.AddToScheme, + routev1.AddToScheme, } for _, adder := range adders { @@ -96,6 +98,7 @@ type parsedObjects struct { ingresses []ks.Ingress // supports multiple versions of ingress cronjobs []ks.CronJob hpaTargeters []ks.HpaTargeter // all versions of HPAs + routes []ks.Route } func (p *parsedObjects) Services() []ks.Service { @@ -142,6 +145,10 @@ func (p *parsedObjects) HorizontalPodAutoscalers() []ks.HpaTargeter { return p.hpaTargeters } +func (p *parsedObjects) Routes() []ks.Route { + return p.routes +} + func Empty() ks.AllTypes { return &parsedObjects{} } @@ -385,6 +392,13 @@ func (p *Parser) decodeItem(cnf config.Configuration, s *parsedObjects, detected s.ingresses = append(s.ingresses, ing) s.bothMetas = append(s.bothMetas, ks.BothMeta{TypeMeta: ingress.TypeMeta, ObjectMeta: ingress.ObjectMeta, FileLocationer: ing}) + case routev1.SchemeGroupVersion.WithKind("Route"): + var route routev1.Route + errs.AddIfErr(p.decode(fileContents, &route)) + rt := internal.RouteV1{Obj: route, Location: fileLocation} + s.routes = append(s.routes, rt) + s.bothMetas = append(s.bothMetas, ks.BothMeta{TypeMeta: route.TypeMeta, ObjectMeta: route.ObjectMeta, FileLocationer: rt}) + case autoscalingv1.SchemeGroupVersion.WithKind("HorizontalPodAutoscaler"): var hpa autoscalingv1.HorizontalPodAutoscaler errs.AddIfErr(p.decode(fileContents, &hpa)) diff --git a/score/checks/checks.go b/score/checks/checks.go index 77101052..1f0b97c8 100644 --- a/score/checks/checks.go +++ b/score/checks/checks.go @@ -3,6 +3,7 @@ package checks import ( "strings" + routev1 "github.com/openshift/api/route/v1" "github.com/zegl/kube-score/config" ks "github.com/zegl/kube-score/domain" "github.com/zegl/kube-score/scorecard" @@ -26,6 +27,7 @@ func New(cnf config.Configuration) *Checks { cronjobs: make(map[string]GenCheck[ks.CronJob]), horizontalPodAutoscalers: make(map[string]GenCheck[ks.HpaTargeter]), poddisruptionbudgets: make(map[string]GenCheck[ks.PodDisruptionBudget]), + routes: make(map[string]GenCheck[routev1.Route]), } } @@ -70,6 +72,7 @@ type Checks struct { cronjobs map[string]GenCheck[ks.CronJob] horizontalPodAutoscalers map[string]GenCheck[ks.HpaTargeter] poddisruptionbudgets map[string]GenCheck[ks.PodDisruptionBudget] + routes map[string]GenCheck[routev1.Route] cnf config.Configuration } @@ -205,6 +208,18 @@ func (c *Checks) Services() map[string]GenCheck[corev1.Service] { return c.services } +func (c *Checks) RegisterRouteCheck(name, comment string, fn CheckFunc[routev1.Route]) { + reg(c, "Route", name, comment, false, fn, c.routes) +} + +func (c *Checks) RegisterOptionalRouteCheck(name, comment string, fn CheckFunc[routev1.Route]) { + reg(c, "Route", name, comment, true, fn, c.routes) +} + +func (c *Checks) Routes() map[string]GenCheck[routev1.Route] { + return c.routes +} + func (c *Checks) All() []ks.Check { return c.all } diff --git a/score/route/route.go b/score/route/route.go new file mode 100644 index 00000000..9e4a9f91 --- /dev/null +++ b/score/route/route.go @@ -0,0 +1,60 @@ +package route + +import ( + routev1 "github.com/openshift/api/route/v1" + ks "github.com/zegl/kube-score/domain" + "github.com/zegl/kube-score/score/checks" + "github.com/zegl/kube-score/scorecard" +) + +func Register(allChecks *checks.Checks, services ks.Services) { + allChecks.RegisterRouteCheck("Route targets Service", `Makes sure that the Route targets a Service`, routeTargetsService(services.Services())) +} + +// routeTargetsService checks if a Service targets a pod and issues a critical warning if no matching pod +func routeTargetsService(svcs []ks.Service) func(routev1.Route) (scorecard.TestScore, error) { + return func(route routev1.Route) (score scorecard.TestScore, err error) { + hasMatchService := false + hasMatchPort := false + + if route.Spec.Port != nil && route.Spec.Port.TargetPort.IntValue() != 0 { + // We consider this as a match as this now matches the pod port which is very difficult to determine + hasMatchPort = true + hasMatchService = true + } else { + for _, s := range svcs { + svc := s.Service() + if svc.Namespace != route.Namespace { + break + } + if route.Spec.To.Name == svc.Name { + hasMatchService = true + if route.Spec.Port == nil && len(svc.Spec.Ports) == 1 { + hasMatchPort = true + break + } + if route.Spec.Port != nil && route.Spec.Port.TargetPort.String() != "" { + for _, p := range svc.Spec.Ports { + if route.Spec.Port.TargetPort.String() == p.Name { + hasMatchPort = true + break + } + } + } + } + } + } + + if hasMatchService && hasMatchPort { + score.Grade = scorecard.GradeAllOK + } else if hasMatchService && !hasMatchPort { + score.Grade = scorecard.GradeAlmostOK + score.AddComment("", "The route does not match any port on the service", "") + } else { + score.Grade = scorecard.GradeCritical + score.AddComment("", "The route does not reference a service", "") + } + + return + } +} diff --git a/score/route_test.go b/score/route_test.go new file mode 100644 index 00000000..e21cc5b4 --- /dev/null +++ b/score/route_test.go @@ -0,0 +1,32 @@ +package score + +import ( + "testing" + + "github.com/zegl/kube-score/scorecard" +) + +func TestRouteTargetsService(t *testing.T) { + t.Parallel() + testExpectedScore(t, "route-targets-service.yaml", "Route targets Service", scorecard.GradeAllOK) +} + +func TestRouteTargetsServiceNoMatch(t *testing.T) { + t.Parallel() + testExpectedScore(t, "route-targets-service-no-match.yaml", "Route targets Service", scorecard.GradeCritical) +} + +func TestRouteTargetsServiceNumberedPort(t *testing.T) { + t.Parallel() + testExpectedScore(t, "route-targets-service-numbered-port.yaml", "Route targets Service", scorecard.GradeAllOK) +} + +func TestRouteTargetsServiceNoPortOk(t *testing.T) { + t.Parallel() + testExpectedScore(t, "route-targets-service-no-port-ok.yaml", "Route targets Service", scorecard.GradeAllOK) +} + +func TestRouteTargetsServiceNoPortNok(t *testing.T) { + t.Parallel() + testExpectedScore(t, "route-targets-service-no-port-nok.yaml", "Route targets Service", scorecard.GradeAlmostOK) +} diff --git a/score/score.go b/score/score.go index 7ddc8b8a..970b346f 100644 --- a/score/score.go +++ b/score/score.go @@ -15,6 +15,7 @@ import ( "github.com/zegl/kube-score/score/networkpolicy" "github.com/zegl/kube-score/score/podtopologyspreadconstraints" "github.com/zegl/kube-score/score/probes" + "github.com/zegl/kube-score/score/route" "github.com/zegl/kube-score/score/security" "github.com/zegl/kube-score/score/service" "github.com/zegl/kube-score/score/stable" @@ -28,6 +29,7 @@ func RegisterAllChecks(allObjects ks.AllTypes, cnf config.Configuration) *checks deployment.Register(allChecks, allObjects) ingress.Register(allChecks, allObjects) + route.Register(allChecks, allObjects) cronjob.Register(allChecks) container.Register(allChecks, cnf) disruptionbudget.Register(allChecks, allObjects) @@ -204,5 +206,16 @@ func Score(allObjects ks.AllTypes, cnf config.Configuration) (*scorecard.Scoreca } } + for _, route := range allObjects.Routes() { + o := newObject(route.Route().TypeMeta, route.Route().ObjectMeta) + for _, test := range allChecks.Routes() { + fn, err := test.Fn(route.Route()) + if err != nil { + return nil, err + } + o.Add(fn, test.Check, route, route.Route().Annotations) + } + } + return &scoreCard, nil } diff --git a/score/testdata/route-targets-service-no-match.yaml b/score/testdata/route-targets-service-no-match.yaml new file mode 100644 index 00000000..15d3ddfa --- /dev/null +++ b/score/testdata/route-targets-service-no-match.yaml @@ -0,0 +1,24 @@ +kind: Route +apiVersion: route.openshift.io/v1 +metadata: + name: console +spec: + host: console-openshift-console.apps.xyz01.example.com + to: + kind: Service + name: console + weight: 100 + port: + targetPort: https + wildcardPolicy: None +--- +kind: Service +apiVersion: v1 +metadata: + name: no-console +spec: + ports: + - name: https + protocol: TCP + port: 443 + targetPort: 8443 diff --git a/score/testdata/route-targets-service-no-port-nok.yaml b/score/testdata/route-targets-service-no-port-nok.yaml new file mode 100644 index 00000000..d5cfb048 --- /dev/null +++ b/score/testdata/route-targets-service-no-port-nok.yaml @@ -0,0 +1,26 @@ +kind: Route +apiVersion: route.openshift.io/v1 +metadata: + name: console +spec: + host: console-openshift-console.apps.xyz01.example.com + to: + kind: Service + name: console + weight: 100 + wildcardPolicy: None +--- +kind: Service +apiVersion: v1 +metadata: + name: console +spec: + ports: + - name: https + protocol: TCP + port: 443 + targetPort: 8443 + - name: http + protocol: TCP + port: 80 + targetPort: 8080 diff --git a/score/testdata/route-targets-service-no-port-ok.yaml b/score/testdata/route-targets-service-no-port-ok.yaml new file mode 100644 index 00000000..02c35f47 --- /dev/null +++ b/score/testdata/route-targets-service-no-port-ok.yaml @@ -0,0 +1,22 @@ +kind: Route +apiVersion: route.openshift.io/v1 +metadata: + name: console +spec: + host: console-openshift-console.apps.xyz01.example.com + to: + kind: Service + name: console + weight: 100 + wildcardPolicy: None +--- +kind: Service +apiVersion: v1 +metadata: + name: console +spec: + ports: + - name: https + protocol: TCP + port: 443 + targetPort: 8443 diff --git a/score/testdata/route-targets-service-numbered-port.yaml b/score/testdata/route-targets-service-numbered-port.yaml new file mode 100644 index 00000000..7ec2b61d --- /dev/null +++ b/score/testdata/route-targets-service-numbered-port.yaml @@ -0,0 +1,13 @@ +kind: Route +apiVersion: route.openshift.io/v1 +metadata: + name: console +spec: + host: console-openshift-console.apps.xyz01.example.com + to: + kind: Service + name: console + weight: 100 + port: + targetPort: 8443 + wildcardPolicy: None \ No newline at end of file diff --git a/score/testdata/route-targets-service.yaml b/score/testdata/route-targets-service.yaml new file mode 100644 index 00000000..ae622ede --- /dev/null +++ b/score/testdata/route-targets-service.yaml @@ -0,0 +1,24 @@ +kind: Route +apiVersion: route.openshift.io/v1 +metadata: + name: console +spec: + host: console-openshift-console.apps.xyz01.example.com + to: + kind: Service + name: console + weight: 100 + port: + targetPort: https + wildcardPolicy: None +--- +kind: Service +apiVersion: v1 +metadata: + name: console +spec: + ports: + - name: https + protocol: TCP + port: 443 + targetPort: 8443