diff --git a/.gitignore b/.gitignore index a2c5f392..85bc3194 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ popeye .idea spinach.yml /kind +/spinach-me diff --git a/.gitpod.yml b/.gitpod.yml deleted file mode 100644 index 8e02ea30..00000000 --- a/.gitpod.yml +++ /dev/null @@ -1,11 +0,0 @@ -# This configuration file was automatically generated by Gitpod. -# Please adjust to your needs (see https://www.gitpod.io/docs/config-gitpod-file) -# and commit this file to your remote git repository to share the goodness with others. - -tasks: - - init: go get && go build ./... && go test ./... && make - command: go run -vscode: - extensions: - - golang.go - diff --git a/Makefile b/Makefile index a4f28fc9..bd7f1941 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ NAME := popeye PACKAGE := github.com/derailed/$(NAME) -VERSION := v0.11.3 +VERSION := v0.20.0 GIT := $(shell git rev-parse --short HEAD) DATE := $(shell date +%FT%T%Z) IMG_NAME := derailed/popeye @@ -9,6 +9,7 @@ IMAGE := ${IMG_NAME}:${VERSION} default: help test: ## Run all tests + @go clean --testcache @go test ./... cover: ## Run test coverage suite diff --git a/README.md b/README.md index 618bc407..8fbb0f84 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,13 @@ -# Popeye - A Kubernetes Cluster Sanitizer +# Popeye - A Kubernetes Live Cluster Resource Linter -Popeye is a utility that scans live Kubernetes cluster and reports potential issues with deployed resources and configurations. It sanitizes your cluster based on what's deployed and not what's sitting on disk. By scanning your cluster, it detects misconfigurations and helps you to ensure that best practices are in place, thus preventing future headaches. It aims at reducing the cognitive *over*load one faces when operating a Kubernetes cluster in the wild. Furthermore, if your cluster employs a metric-server, it reports potential resources over/under allocations and attempts to warn you should your cluster run out of capacity. +Popeye is a utility that scans live Kubernetes clusters and reports potential issues with deployed resources and configurations. +As Kubernetes landscapes grows, it is becoming a challenge for a human to track the slew of manifests and policies that orchestrate a cluster. +Popeye scans your cluster based on what's deployed and not what's sitting on disk. By linting your cluster, it detects misconfigurations, +stale resources and assists you to ensure that best practices are in place, thus preventing future headaches. +It aims at reducing the cognitive *over*load one faces when operating a Kubernetes cluster in the wild. +Furthermore, if your cluster employs a metric-server, it reports potential resources over/under allocations and attempts to warn you should your cluster run out of capacity. Popeye is a readonly tool, it does not alter any of your Kubernetes resources in any way! @@ -22,7 +27,30 @@ Popeye is a readonly tool, it does not alter any of your Kubernetes resources in --- -[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/derailed/popeye) +## Screenshots + +### Console + + + +### JSON + + + +### HTML + +You can dump the scan report to HTML. + + + +### Grafana Dashboard + +Popeye publishes [Prometheus](https://prometheus.io) metrics. +We provided a sample Popeye dashboard to get you started in this repo. + + + +--- ## Installation @@ -44,7 +72,7 @@ Popeye is available on Linux, OSX and Windows platforms. ``` * Building from source - Popeye was built using go 1.12+. In order to build Popeye from source you must: + Popeye was built using go 1.21+. In order to build Popeye from source you must: 1. Clone the repo 2. Add the following command in your go.mod file @@ -67,7 +95,7 @@ Popeye is available on Linux, OSX and Windows platforms. git clone https://github.com/derailed/popeye cd popeye # Build and install - go install + make build # Run popeye ``` @@ -80,24 +108,25 @@ Popeye is available on Linux, OSX and Windows platforms. export TERM=xterm-256color ``` -## Sanitizers +--- + +## Linters Popeye scans your cluster for best practices and potential issues. -Currently, Popeye only looks at nodes, namespaces, pods and services. -More will come soon! We are hoping Kubernetes friends will pitch'in -to make Popeye even better. +Currently, Popeye only looks for a given set of curated Kubernetes resources. +More will come soon! +We are hoping Kubernetes friends will pitch'in to make Popeye even better. -The aim of the sanitizers is to pick up on misconfigurations, i.e. things +The aim of the linters is to pick up on misconfigurations, i.e. things like port mismatches, dead or unused resources, metrics utilization, probes, container images, RBAC rules, naked resources, etc... Popeye is not another static analysis tool. It runs and inspect Kubernetes resources on -live clusters and sanitize resources as they are in the wild! - -Here is a list of some of the available sanitizers: +live clusters and lint resources as they are in the wild! +Here is a list of some of the available linters: -| | Resource | Sanitizers | Aliases | +| | Resource | Linters | Aliases | |----|-------------------------|-------------------------------------------------------------------------|------------| | 🛀 | Node | | no | | | | Conditions ie not ready, out of mem/disk, network, pids, etc | | @@ -151,41 +180,54 @@ Here is a list of some of the available sanitizers: | 🛀 | Ingress | | | | | | Valid | ing | | 🛀 | NetworkPolicy | | | -| | | Valid | np | +| | | Valid, Stale, Guarded | np | | 🛀 | PodSecurityPolicy | | | | | | Valid | psp | +| 🛀 | Cronjob | | | +| | | Valid, Suspended, Runs | cj | +| 🛀 | Job | | | +| | | Pod checks | job | +| 🛀 | GatewayClass | | | +| | | Valid, Unused | gwc | +| 🛀 | Gateway | | | +| | | Valid, Unused | gw | +| 🛀 | HTTPRoute | | | +| | | Valid, Unused | gwr | You can also see the [full list of codes](docs/codes.md) -### Save the report +--- + +## Saving Scans To save the Popeye report to a file pass the `--save` flag to the command. -By default it will create a temp directory and will store the report there, -the path of the temp directory will be printed out on STDOUT. +By default it will create a tmp directory and will store your scan report there. +The path of the tmp directory will be printed out on STDOUT. If you have the need to specify the output directory for the report, -you can use the environment variable `POPEYE_REPORT_DIR`. -By default, the name of the output file follow the following format : `sanitizer__.` (e.g. : "sanitizer-mycluster-1594019782530851873.html"). -If you have the need to specify the output file name for the report, -you can pass the `--output-file` flag with the filename you want as parameter. +you can use this environment variable `POPEYE_REPORT_DIR`. +By default, the name of the output file follow the following format : `lint__.` (e.g. : "lint-mycluster-1594019782530851873.html"). +If you want to also specify the output file name for the report, you can pass the `--output-file` flag with the filename you want as parameter. Example to save report in working directory: ```shell - $ POPEYE_REPORT_DIR=$(pwd) popeye --save +POPEYE_REPORT_DIR=$(pwd) popeye --save ``` Example to save report in working directory in HTML format under the name "report.html" : ```shell - $ POPEYE_REPORT_DIR=$(pwd) popeye --save --out html --output-file report.html +POPEYE_REPORT_DIR=$(pwd) popeye --save --out html --output-file report.html ``` -### Save the report to S3 +### Save To S3 -You can also save the generated report to an AWS S3 bucket (or another S3 compatible Object Storage) with providing the flag `--s3-bucket`. As parameter you need to provide the name of the S3 bucket where you want to store the report. +Alternatively, you can push the generated reports to an AWS S3 bucket (or other S3 compatible Object Storage) by providing the flag `--s3-bucket`. +For parameters you need to provide the name of the S3 bucket where you want to store the report. To save the report in a bucket subdirectory provide the bucket parameter as `bucket/path/to/report`. -Underlying the AWS Go lib is used which is handling the credential loading. For more information check out the official [documentation](https://docs.aws.amazon.com/sdk-for-go/api/aws/session/). +The AWS Go lib is used which handles your credentials. +For more information check out the official [documentation](https://docs.aws.amazon.com/sdk-for-go/api/aws/session/). Example to save report to S3: @@ -193,66 +235,68 @@ Example to save report to S3: popeye --s3-bucket=NAME-OF-YOUR-S3-BUCKET/OPTIONAL/SUBDIRECTORY --out=json ``` -If AWS sS3 is not your bag, you can further define an S3 compatible storage (OVHcloud Object Storage, Minio, Google cloud storage, etc...) using s3-endpoint and s3-region as so: +If AWS S3 is not your bag, you can further define an S3 compatible storage (OVHcloud Object Storage, Minio, Google cloud storage, etc...) using s3-endpoint and s3-region as so: ```shell popeye --s3-bucket=NAME-OF-YOUR-S3-BUCKET/OPTIONAL/SUBDIRECTORY --s3-region YOUR-REGION --s3-endpoint URL-OF-THE-ENDPOINT ``` -### Run public Docker image locally +--- + +## Docker Support -You don't have to build and/or install the binary to run popeye: you can just -run it directly from the official docker repo on DockerHub. The default command -when you run the docker container is `popeye`, so you just need to pass -whatever cli args are normally passed to popeye. To access your clusters, map -your local kube config directory into the container with `-v` : +You can also run Popeye in a container by running it directly from the official docker repo on DockerHub. +The default command when you run the docker container is `popeye`, so you customize the scan by using the supported cli flags. +To access your clusters, map your local kubeconfig directory into the container with `-v` : ```shell - docker run --rm -it \ - -v $HOME/.kube:/root/.kube \ - derailed/popeye --context foo -n bar +docker run --rm -it -v $HOME/.kube:/root/.kube derailed/popeye --context foo -n bar ``` -Running the above docker command with `--rm` means that the container gets -deleted when popeye exits. When you use `--save`, it will write it to /tmp in -the container and then delete the container when popeye exits, which means you -lose the output. To get around this, map /tmp to the container's /tmp. -NOTE: You can override the default output directory location by setting `POPEYE_REPORT_DIR` env variable. +Running the above docker command with `--rm` means that the container gets deleted when Popeye exits. +When you use `--save`, it will write it to /tmp in the container and then delete the container when popeye exits, which means you lose the output ;( +To get around this, map /tmp to the container's /tmp. + +> NOTE: You can override the default output directory location by setting `POPEYE_REPORT_DIR` env variable. ```shell - docker run --rm -it \ - -v $HOME/.kube:/root/.kube \ - -e POPEYE_REPORT_DIR=/tmp/popeye \ - -v /tmp:/tmp \ - derailed/popeye --context foo -n bar --save --output-file my_report.txt - - # Docker has exited, and the container has been deleted, but the file - # is in your /tmp directory because you mapped it into the container - $ cat /tmp/popeye/my_report.txt - +docker run --rm -it \ + -v $HOME/.kube:/root/.kube \ + -e POPEYE_REPORT_DIR=/tmp/popeye \ + -v /tmp:/tmp \ + derailed/popeye --context foo -n bar --save --output-file my_report.txt + +# Docker has exited, and the container has been deleted, but the file +# is in your /tmp directory because you mapped it into the container +cat /tmp/popeye/my_report.txt + ``` +--- + ## The Command Line -You can use Popeye standalone or using a spinach yaml config to -tune the sanitizer. Details about the Popeye configuration file are below. +You can use Popeye wide open or using a spinach yaml config to +tune your linters. Details about the Popeye configuration file are below. ```shell -# Dump version info +# Dump version info and logs location popeye version # Popeye a cluster using your current kubeconfig environment. popeye # Popeye uses a spinach config file of course! aka spinachyaml! -popeye -f spinach.yml +popeye -f spinach.yaml # Popeye a cluster using a kubeconfig context. popeye --context olive # Stuck? popeye help ``` +--- + ## Output Formats -Popeye can generate sanitizer reports in a variety of formats. You can use the -o cli option and pick your poison from there. +Popeye can generate linter reports in a variety of formats. You can use the -o cli option and pick your poison from there. | Format | Description | Default | Credits | |------------|--------------------------------------------------------|---------|----------------------------------------------| @@ -262,31 +306,85 @@ Popeye can generate sanitizer reports in a variety of formats. You can use the - | html | As HTML | | | | json | As JSON | | | | junit | For the Java melancholic | | | -| prometheus | Dumps report a prometheus scrapable metrics | | [dardanel](https://github.com/eminugurkenar) | -| score | Returns a single cluster sanitizer score value (0-100) | | [kabute](https://github.com/kabute) | +| prometheus | Dumps report a prometheus metrics | | [dardanel](https://github.com/eminugurkenar) | +| score | Returns a single cluster linter score value (0-100) | | [kabute](https://github.com/kabute) | + +--- + +## The Prom Queen! + +Popeye can publish Prometheus metrics directly from a scan. You will need to have access to a prometheus pushgateway and credentials. + +> NOTE! These are subject to change based on users feedback and usage!! + +In order to publish metrics, additional cli args must be present. + +```shell +# Run popeye using console output and push prom metrics. +popeye --push-gtwy-url http://localhost:9091 + +# Run popeye using a saved html output and push prom metrics. +# NOTE! When scan are dump to disk, popeye_cluster_score metric below includes +# an additional label to track the persisted artifact so you can aggregate with the scan +# Don't think it's the correct approach as this changes the metric cardinality on every push. +# Hence open for suggestions here?? +popeye -o html --save --push-gtwy-url http://localhost:9091 +``` + +### PopProm metrics + +The following Popeye prometheus metrics are published: -## The SpinachYAML Configuration +* `popeye_severity_total` [gauge] tracks various counts based on severity. +* `popeye_code_total` [gauge] tracks counts by Popeye's linter codes. +* `popeye_linter_tally_total` [gauge] tracks counts per linters. +* `popeye_report_errors_total` [gauge] tracks scan errors totals. +* `popeye_cluster_score` [gauge] tracks scan report scores. -A spinach.yml configuration file can be specified via the `-f` option to further configure the sanitizers. This file may specify -the container utilization threshold and specific sanitizer configurations as well as resources that will be excluded from the sanitization. -NOTE: This file will change as Popeye matures! +### PopGraf -Under the `excludes` key you can configure to skip certain resources, or certain checks by code. Here, resource types are indicated in a group/version/resource notation. Example: to exclude PodDisruptionBugdets, use the notation `policy/v1/poddisruptionbudgets`. Note that the resource name is written in the plural form and everything is spelled in lowercase. For resources without an API group, the group part is omitted (Examples: `v1/pods`, `v1/services`, `v1/configmaps`). +A sample [Grafana](https://grafana.com) dashboard can be found in this repo to get you started. -A resource is identified by a resource kind and a fully qualified resource name, i.e. `namespace/resource_name`. +> NOTE! Work in progress, please feel free to contribute if you have UX/grafana/promql chops. -For example, the FQN of a pod named `fred-1234` in the namespace `blee` will be `blee/fred-1234`. This provides for differentiating `fred/p1` and `blee/p1`. For cluster wide resources, the FQN is equivalent to the name. Exclude rules can have either a straight string match or a regular expression. In the latter case the regular expression must be indicated using the `rx:` prefix. -NOTE! Please be careful with your regex as more resources than expected may get excluded from the report with a *loose* regex rule. When your cluster resources change, this could lead to a sub-optimal sanitization. Once in a while it might be a good idea to run Popeye „configless“ to make sure you will recognize any new issues that may have arisen in your clusters… +--- + +## SpinachYAML + +A spinach YAML configuration file can be specified via the `-f` option to further configure the linters. This file may specify +the container utilization threshold and specific linter configurations as well as resources and codes that will be excluded from the linter. + +> NOTE! This file will change as Popeye matures! + +Under the `excludes` key you can configure to skip certain resources, or linter codes. +Popeye's linters are named after the k8s resource names. +For example the PodDisruptionBudget linter is named `poddisruptionbudgets` and scans `policy/v1/poddisruptionbudgets` + +> NOTE! The linter uses the plural resource `kind` form and everything is spelled in lowercase. + +A resource fully qualified name aka `FQN` is used in the spinach file to identity a resource name i.e. `namespace/resource_name`. + +For example, the FQN of a pod named `fred-1234` in the namespace `blee` will be `blee/fred-1234`. This provides for differentiating `fred/p1` and `blee/p1`. +For cluster wide resources, the FQN is equivalent to the name. +Exclude rules can be either a straight string match or a regular expression. In the latter case the regular expression must be specified via the `rx:` prefix. + +> NOTE! Please be careful with your regex as more resources than expected may get excluded from the report with a *loose* regex rule. +> When your cluster resources change, this could lead to a sub-optimal scans. +> Thus we recommend running Popeye `wide open` once in a while to make sure you will pick up on any new issues that may have arisen in your clusters… -Here is an example spinach file as it stands in this release. There is a fuller eks and aks based spinach file in this repo under `spinach`. (BTW: for new comers into the project, might be a great way to contribute by adding cluster specific spinach file PRs...) +Here is an example spinach file as it stands in this release. +There is a fuller eks and aks based spinach file in this repo under `spinach`. +(BTW: for new comers into the project, might be a great way to contribute by adding cluster specific spinach file PRs...) ```yaml +# spinach.yaml + # A Popeye sample configuration file popeye: # Checks resources against reported metrics usage. - # If over/under these thresholds a sanitization warning will be issued. + # If over/under these thresholds a linter warning will be issued. # Your cluster must run a metrics-server for these to take place! allocations: cpu: @@ -298,73 +396,92 @@ popeye: # Excludes excludes certain resources from Popeye scans excludes: - v1/pods: - # In the monitoring namespace excludes all probes check on pod's containers. - - name: rx:monitoring - codes: - - 102 - # Excludes all istio-proxy container scans for pods in the icx namespace. - - name: rx:icx/.* - containers: - # Excludes istio init/sidecar container from scan! - - istio-proxy - - istio-init - # ConfigMap sanitizer exclusions... - v1/configmaps: - # Excludes key must match the singular form of the resource. - # For instance this rule will exclude all configmaps named fred.v2.3 and fred.v2.4 - - name: rx:fred.+\.v\d+ - # Namespace sanitizer exclusions... - v1/namespaces: - # Exclude all fred* namespaces if the namespaces are not found (404), other error codes will be reported! - - name: rx:fred - codes: - - 404 - # Exclude all istio* namespaces from being scanned. - - name: rx:istio - # Completely exclude horizontal pod autoscalers. - autoscaling/v1/horizontalpodautoscalers: - - name: rx:.* - - # Configure node resources. - node: - # Limits set a cpu/mem threshold in % ie if cpu|mem > limit a lint warning is triggered. - limits: - # CPU checks if current CPU utilization on a node is greater than 90%. - cpu: 90 - # Memory checks if current Memory utilization on a node is greater than 80%. - memory: 80 - - # Configure pod resources - pod: - # Restarts check the restarts count and triggers a lint warning if above threshold. - restarts: - 3 - # Check container resource utilization in percent. - # Issues a lint warning if about these threshold. - limits: - cpu: 80 - memory: 75 - - # Configure a list of allowed registries to pull images from + # [NEW!] Global exclude resources and codes globally of any linters. + global: + fqns: [rx:^kube-] # => excludes all resources in kube-system, kube-public, etc.. + # [NEW!] Exclude resources for all linters matching these labels + labels: + app: [bozo, bono] #=> exclude any resources with labels matching either app=bozo or app=bono + # [NEW!] Exclude resources for all linters matching these annotations + annotations: + fred: [blee, duh] # => exclude any resources with annotations matching either fred=blee or fred=duh + # [NEW!] Exclude scan codes globally via straight codes or regex! + codes: ["300", "206", "rx:^41"] # => exclude issue codes 300, 206, 410, 415 (Note: regex match!) + + # [NEW!] Configure individual resource linters + linters: + # Configure the namespaces linter for v1/namespaces + namespaces: + # [NEW!] Exclude these codes for all namespace resources straight up or via regex. + codes: ["100", "rx:^22"] # => exclude codes 100, 220, 225, ... + # [NEW!] Excludes specific namespaces from the scan + instances: + - fqns: [kube-public, kube-system] # => skip ns kube-pulbic and kube-system + - fqns: [blee-ns] + codes: [106] # => skip code 106 for namespace blee-ns + + # Configure the pods linter for v1/pods. + pods: + instances: + # [NEW!] exclude all pods matching these labels. + - labels: + app: [fred,blee] # Exclude codes 102, 105 for any pods with labels app=fred or app=blee + codes: [102, 105] + + resources: + # Configure node resources. + node: + # Limits set a cpu/mem threshold in % ie if cpu|mem > limit a lint warning is triggered. + limits: + # CPU checks if current CPU utilization on a node is greater than 90%. + cpu: 90 + # Memory checks if current Memory utilization on a node is greater than 80%. + memory: 80 + + # Configure pod resources + pod: + # Restarts check the restarts count and triggers a lint warning if above threshold. + restarts: 3 + # Check container resource utilization in percent. + # Issues a lint warning if about these threshold. + limits: + cpu: 80 + memory: 75 + + + # [New!] overrides code severity + overrides: + # Code specifies a custom severity level ie critical=3, warn=2, info=1 + - code: 206 + severity: 1 + + # Configure a list of allowed registries to pull images from. + # Any resources not using the following registries will be flagged! registries: - quay.io - docker.io ``` -## Popeye In Your Clusters! +--- + +## In Cluster -Alternatively, Popeye is containerized and can be run directly in your Kubernetes clusters as a one-off or CronJob. +Popeye is containerized and can be run directly in your Kubernetes clusters as a one-off or CronJob. Here is a sample setup, please modify per your needs/wants. The manifests for this are in the k8s directory in this repo. ```shell -kubectl apply -f k8s/popeye/ns.yml && kubectl apply -f k8s/popeye +kubectl apply -f k8s/popeye ``` ```yaml --- +apiVersion: v1 +kind: Namespace +metadata: + name: popeye +--- apiVersion: batch/v1 kind: CronJob metadata: @@ -381,7 +498,7 @@ spec: restartPolicy: Never containers: - name: popeye - image: derailed/popeye + image: derailed/popeye:vX.Y.Z imagePullPolicy: IfNotPresent args: - -o @@ -395,16 +512,16 @@ spec: The `--force-exit-zero` should be set. Otherwise, the pods will end up in an error state. -> Note: Popeye exits with a non-zero error code if the report has any errors. +> NOTE! Popeye exits with a non-zero error code if any lint errors are detected. +### Popeye Got Your RBAC! -## Popeye got your RBAC! - -In order for Popeye to do his work, the signed-in user must have enough RBAC oomph to -get/list the resources mentioned above. +In order for Popeye to do his work, the signed-in user must have enough RBAC oomph to get/list the resources mentioned above. Sample Popeye RBAC Rules (please note that those are **subject to change**.) +> NOTE! Please review and tune per your cluster policies. + ```yaml --- # Popeye ServiceAccount. @@ -425,7 +542,6 @@ rules: resources: - configmaps - endpoints - - limitranges - namespaces - nodes - persistentvolumes @@ -447,6 +563,17 @@ rules: - ingresses - networkpolicies verbs: ["get", "list"] +- apiGroups: ["batch.k8s.io"] + resources: + - cronjobs + - jobs + verbs: ["get", "list"] +- apiGroups: ["gateway.networking.k8s.io"] + resources: + - gateway-classes + - gateways + - httproutes + verbs: ["get", "list"] - apiGroups: ["autoscaling"] resources: - horizontalpodautoscalers @@ -485,24 +612,16 @@ roleRef: apiGroup: rbac.authorization.k8s.io ``` -## Screenshots - -### Cluster D Score - - - -### Cluster A Score - - +--- ## Report Morphology -The sanitizer report outputs each resource group scanned and their potential issues. -The report is color/emoji coded in term of Sanitizer severity levels: +The lint report outputs each resource group scanned and their potential issues. +The report is color/emoji coded in term of linter severity levels: | Level | Icon | Jurassic | Color | Description | |-------|------|----------|-----------|-----------------| -| Ok | ✅ | OK | Green | Happy! | +| Ok | ✅ | OK | Green | Happy! | | Info | 🔊 | I | BlueGreen | FYI | | Warn | 😱 | W | Yellow | Potential Issue | | Error | 💥 | E | Red | Action required | @@ -510,20 +629,26 @@ The report is color/emoji coded in term of Sanitizer severity levels: The heading section for each scanned Kubernetes resource provides a summary count for each of the categories above. -The Summary section provides a **Popeye Score** based on the sanitization pass on the given cluster. +The Summary section provides a **Popeye Score** based on the linter pass on the given cluster. + +--- ## Known Issues This initial drop is brittle. Popeye will most likely blow up when… -* You're running older versions of Kubernetes. Popeye works best with Kubernetes 1.13+. +* You're running older versions of Kubernetes. Popeye works best with Kubernetes 1.25.X. * You don't have enough RBAC oomph to manage your cluster (see RBAC section) +--- + ## Disclaimer This is work in progress! If there is enough interest in the Kubernetes -community, we will enhance per your recommendations/contributions. Also if you -dig this effort, please let us know that too! +community, we will enhance per your recommendations/contributions. +Also if you dig this effort, please let us know that too! + +--- ## ATTA Girls/Boys! @@ -531,12 +656,12 @@ Popeye sits on top of many of open source projects and libraries. Our *sincere* appreciations to all the OSS contributors that work nights and weekends to make this project a reality! -## Contact Info +### Contact Info 1. **Email**: fernand@imhotep.io 2. **Twitter**: [@kitesurfer](https://twitter.com/kitesurfer?lang=en) --- -  © 2020 Imhotep Software LLC. +  © 2024 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0) diff --git a/assets/html_report.png b/assets/html_report.png deleted file mode 100644 index 990f8bc2..00000000 Binary files a/assets/html_report.png and /dev/null differ diff --git a/assets/nikita.jpg b/assets/nikita.jpg deleted file mode 100644 index d16e8c35..00000000 Binary files a/assets/nikita.jpg and /dev/null differ diff --git a/assets/screens/console.png b/assets/screens/console.png new file mode 100644 index 00000000..54e0deab Binary files /dev/null and b/assets/screens/console.png differ diff --git a/assets/screens/html.png b/assets/screens/html.png new file mode 100644 index 00000000..e46ef902 Binary files /dev/null and b/assets/screens/html.png differ diff --git a/assets/screens/json.png b/assets/screens/json.png new file mode 100644 index 00000000..1ebc0d7b Binary files /dev/null and b/assets/screens/json.png differ diff --git a/assets/screens/pop-dash.png b/assets/screens/pop-dash.png new file mode 100644 index 00000000..c5855d05 Binary files /dev/null and b/assets/screens/pop-dash.png differ diff --git a/assets/a_score.png b/assets/screens/score-a.png similarity index 100% rename from assets/a_score.png rename to assets/screens/score-a.png diff --git a/assets/d_score.png b/assets/screens/score-d.png similarity index 100% rename from assets/d_score.png rename to assets/screens/score-d.png diff --git a/change_logs/release_v0.12.0.md b/change_logs/release_v0.12.0.md new file mode 100644 index 00000000..510096a4 --- /dev/null +++ b/change_logs/release_v0.12.0.md @@ -0,0 +1,26 @@ + + +# Release v0.12.0 + +## Notes + +Thank you to all that contributed with flushing out issues and enhancements for Popeye! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make Popeye better is as ever very much noticed and appreciated! + +This project offers a GitHub Sponsor button (over here 👆). As you well know this is not pimped out by big corps with deep pockets. If you feel `Popeye` is saving you cycles diagnosing potential cluster issues please consider sponsoring this project!! It does go a long way in keeping our servers lights on and beers in our fridge. + +Also if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer) + +--- + +## Feature Release! + +--- + +## Resolved Issues + +* [#259](https://github.com/derailed/popeye/issues/259) Checking Kubernetes clusters fails because v1/PodSecurityPolicy is checked +* [#229](https://github.com/derailed/popeye/issues/229) Timestamp on the report + +--- + +  © 2024 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0) diff --git a/change_logs/release_v0.20.0.md b/change_logs/release_v0.20.0.md new file mode 100644 index 00000000..288ac40e --- /dev/null +++ b/change_logs/release_v0.20.0.md @@ -0,0 +1,149 @@ + + +# Release v0.20.0 + +## Notes + +Thank you to all that contributed with flushing out issues and enhancements for Popeye! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make Popeye better is as ever very much noticed and appreciated! + +This project offers a GitHub Sponsor button (over here 👆). As you well know this is not pimped out by big corps with deep pockets. If you feel `Popeye` is saving you cycles diagnosing potential cluster issues please consider sponsoring this project!! It does go a long way in keeping our servers lights on and beers in our fridge. + +Also if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer) + +--- + +## ♫ Sounds Behind The Release ♭ + +🏹💕 Happy belated Valentines 💕🏹 + +* [Glory Box - Portishead](https://www.youtube.com/watch?v=NVuRbwnav_Y) +* [Funny Valentine - Elvis Costello](https://www.youtube.com/watch?v=ni3DjM8wcds) +* [Cause We've Ended As Lovers - Jeff Beck](https://www.youtube.com/watch?v=VC02wGj5gPw) + +--- + +## 🎉 Feature Release 🥳 + +Popeye just got a new spinach formula and pipe! + +😳 This is a big one! 😳 + +> NOTE! 🫣 Paint is still fresh on this deal and I might have broken stuff in the process ;( +> Please help us vet this drop to help us solidify and make Popeye better for all of us. +> Thank you!! + +Splendid! So what changed? + +### Biffs'em If You Got'em! + +As of this drop, Popeye linters family got extended. The following linters were added/extended: + +* Cronjobs +* Jobs +* Gateway-Classes +* Gateways +* HTTPRoutes +* NetworkPolicies (Beefed up!) + +### New Spinach Formula! + +The SpinachYAML configuration changed and won't be compatible with previous versions. +The new format provides for global exclusions and linters specific ones. +Please see the docs for the gory details but in short this is what a spinach file now looks like: + +```yaml +popeye: + allocations: + cpu: + underPercUtilization: 200 + overPercUtilization: 50 + memory: + underPercUtilization: 200 + overPercUtilization: 50 + + # [!!NEW!!] Specify global exclusions for fqn, codes, labels, annotations + excludes: + global: + # Exclude kube-system ns for all linters. + fqns: [rx:^kube-system] + # Exclude these workload labels for all linters. + labels: + app: [blee, bozo] + + # [!!NEW!!] Linters exclude section + linters: + # [!!NEW!!] use the R from GVR resource specification to name the linter + statefulsets: + # [!!NEW!!] Exclude codes via regexp ie skip 101, 1000,... + codes: ["rx:^10"] + instances: + # Skip scan for a particular FQN aka namespace/res-name + - fqns: [default/prom-alertmanager] + codes: [106] + + pods: + codes: ["306", "rx:^11"] + instances: + - fqns: [rx:^default/prom] + - fqns: [rx:^default/graf] + # [!!NEW!!] Skip using either labels or annotations and/or specific codes + - labels: + app: [blee, blah, zorg] + codes: [300] + - fqns: [rx:^default/pappi] + codes: [300, 102, 306] + containers: [c1] + + resources: + node: + limits: + cpu: 90 + memory: 80 + pod: + limits: + cpu: 80 + memory: 75 + restarts: 3 + + overrides: + - code: 1502 + severity: 3 + + registries: + - quay2.io + - docker1.io +``` + +### Popeye The Prom Queen? + +Additionally, we've updated Popeye's prometheus metrics to provide more scan insights and signals. Please see the docs for details. + +. `popeye_severity_total` [gauge] tracks various counts based on severity. +. `popeye_code_total` [gauge] tracks counts by Popeye's linter codes. +. `popeye_linter_tally_total` [gauge] tracks counts per linters. +. `popeye_report_errors_total` [gauge] tracks scan errors totals. +. `popeye_cluster_score` [gauge] tracks scan report scores. + +--- + +## Resolved Issues + +. [#265](https://github.com/derailed/popeye/issues/265) additional/fine grained prometheus metrics +. [#237](https://github.com/derailed/popeye/issues/237) Support multiple outputs at once +. [#235](https://github.com/derailed/popeye/issues/235) --lint level does not affect html output +. [#232](https://github.com/derailed/popeye/issues/232) Metrics get overridden when using the same Pushgateway for multiple k8s clusters +. [#231](https://github.com/derailed/popeye/issues/231) wrong warning: [POP-107] No resource limits defined +. [#230](https://github.com/derailed/popeye/issues/230) APIs: metrics.k8s.io/v1beta1: the server is currently unable to handle the request +. [#214](https://github.com/derailed/popeye/issues/214) [POP-1100] No pods match service selector - should not be detected for ExternalName service type +. [#213](https://github.com/derailed/popeye/issues/213) Ingress extensions/v1beta1 deprecated (and deleted in k8s v1.22) is not detected ONLY in kube-metriques namespace +. [#212](https://github.com/derailed/popeye/issues/212) Ingress networking.k8s.io/v1beta1 deprecated since k8s v1.19 and deleted in k8s v1.22, is not detected ONLY in specific namespace name as kube-metriques +. [#209](https://github.com/derailed/popeye/issues/209) POP-403 - PodSecurityPolicy (PSP) k8s v1.21 deprecation - k8s v1.25 deletion - not detected +. [#202](https://github.com/derailed/popeye/issues/202) False positive on NetworkPolicy using a catch all namespaceSelector +. [#163](https://github.com/derailed/popeye/issues/163) popeye 0.9.0 with K8S 1.21.0 bug on PodDisruptionBudget - Wrong default API +. [#125](https://github.com/derailed/popeye/issues/125) info/error/warning messages to the metrics sent to prometheus +. [#97](https://github.com/derailed/popeye/issues/97) Add support for explicitly sanitizing jobs to popeye +. [#59](https://github.com/derailed/popeye/issues/59) StatefulSet incorrectly determines apiVersio + +--- + +  © 2024 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0) diff --git a/cmd/root.go b/cmd/root.go index 1180b0e1..373af86c 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -4,7 +4,6 @@ package cmd import ( - "errors" "fmt" "os" "path/filepath" @@ -26,7 +25,7 @@ var ( flags = config.NewFlags() rootCmd = &cobra.Command{ Use: execName(), - Short: "A Kubernetes Cluster sanitizer and linter", + Short: "A Kubernetes Cluster resource linter", Long: `Popeye scans your Kubernetes clusters and reports potential resource issues.`, Run: doIt, } @@ -48,14 +47,12 @@ func init() { // Execute root command func Execute() { if err := rootCmd.Execute(); err != nil { - bomb(fmt.Sprintf("Exec failed %s", err)) + return } } // Doit runs the scans and lints pass over the specified cluster. func doIt(cmd *cobra.Command, args []string) { - zerolog.SetGlobalLevel(zerolog.DebugLevel) - defer func() { if err := recover(); err != nil { printMsgLogo("DOH", "X", report.ColorOrangish, report.ColorRed) @@ -66,34 +63,33 @@ func doIt(cmd *cobra.Command, args []string) { } }() + zerolog.SetGlobalLevel(zerolog.DebugLevel) clearScreen() - if err := checkFlags(); err != nil { - bomb(fmt.Sprintf("%v", err)) - } + bomb(flags.Validate()) flags.StandAlone = true popeye, err := pkg.NewPopeye(flags, &log.Logger) if err != nil { - bomb(fmt.Sprintf("Popeye configuration load failed %v", err)) - } - if e := popeye.Init(); e != nil { - bomb(e.Error()) + bomb(fmt.Errorf("popeye configuration load failed %w", err)) } - errCount, score, err := popeye.Sanitize() + bomb(popeye.Init()) + + errCount, score, err := popeye.Lint() if err != nil { - bomb(err.Error()) + bomb(err) } - if flags.ForceExitZero != nil && *flags.ForceExitZero { os.Exit(0) } - if errCount > 0 || (flags.MinScore != nil && score < *flags.MinScore) { os.Exit(1) } } -func bomb(msg string) { - panic(fmt.Sprintf("💥 %s\n", report.Colorize(msg, report.ColorRed))) +func bomb(err error) { + if err == nil { + return + } + panic(fmt.Sprintf("💥 %s\n", report.Colorize(err.Error(), report.ColorRed))) } func initPopeyeFlags() { @@ -115,7 +111,7 @@ func initPopeyeFlags() { rootCmd.Flags().StringVarP(flags.Output, "out", "o", "standard", - "Specify the output type (standard, jurassic, yaml, json, html, junit, prometheus, score)", + "Specify the output type (standard, jurassic, yaml, json, html, junit, score)", ) rootCmd.Flags().BoolVarP(flags.Save, "save", "", @@ -125,25 +121,25 @@ func initPopeyeFlags() { rootCmd.Flags().StringVarP(flags.OutputFile, "output-file", "", "", - "Specify the name of the saved output file", + "Specify the file name to persist report to disk", ) - rootCmd.Flags().StringVarP(flags.S3Bucket, "s3-bucket", "", + rootCmd.Flags().StringVarP(flags.S3.Bucket, "s3-bucket", "", "", "Specify to which S3 bucket you want to save the output file", ) - rootCmd.Flags().StringVarP(flags.S3Region, "s3-region", "", + rootCmd.Flags().StringVarP(flags.S3.Region, "s3-region", "", "", "Specify an s3 compatible region when the s3-bucket option is enabled", ) - rootCmd.Flags().StringVarP(flags.S3Endpoint, "s3-endpoint", "", + rootCmd.Flags().StringVarP(flags.S3.Endpoint, "s3-endpoint", "", "", "Specify an s3 compatible endpoint when the s3-bucket option is enabled", ) rootCmd.Flags().StringVarP(flags.InClusterName, "cluster-name", "", "", - "Specificy a cluster name when running popeye in cluster", + "Specify a cluster name when running popeye in cluster", ) rootCmd.Flags().StringVarP(flags.LintLevel, "lint", "l", @@ -163,7 +159,7 @@ func initPopeyeFlags() { rootCmd.Flags().BoolVarP(flags.AllNamespaces, "all-namespaces", "A", false, - "Sanitize all namespaces", + "When present, runs linters for all namespaces", ) rootCmd.Flags().StringVarP(flags.Spinach, "file", "f", @@ -173,7 +169,7 @@ func initPopeyeFlags() { rootCmd.Flags().StringSliceVarP(flags.Sections, "sections", "s", []string{}, - "Specifies which resources to include in the scan ie -s po,svc", + "Specify which resources to include in the scan ie -s po,svc", ) } @@ -276,35 +272,25 @@ func initFlags() { ) rootCmd.Flags().StringVar( - flags.PushGateway.Address, - "pushgateway-address", + flags.PushGateway.URL, + "push-gtwy-url", "", - "Address of pushgateway e.g. http://localhost:9091", + "Prometheus pushgateway address e.g. http://localhost:9091", ) rootCmd.Flags().StringVar( flags.PushGateway.BasicAuth.User, - "pushgateway-user", + "push-gtwy-user", "", - "BasicAuth username for pushgateway", + "Prometheus pushgateway auth username", ) rootCmd.Flags().StringVar( flags.PushGateway.BasicAuth.Password, - "pushgateway-password", + "push-gtwy-password", "", - "BasicAuth password for pushgateway", + "Prometheus pushgateway auth password", ) } -func checkFlags() error { - if flags.OutputFormat() == report.PrometheusFormat && *flags.PushGateway.Address == "" { - return errors.New("Please set pushgateway-address and auth if necessary") - } - if !*flags.Save && *flags.OutputFile != "" { - return errors.New("Please set '--save' flag to use 'output-file'.") - } - return nil -} - // ---------------------------------------------------------------------------- // Helpers... diff --git a/docs/codes.md b/docs/codes.md index 7e438f11..113c5f41 100644 --- a/docs/codes.md +++ b/docs/codes.md @@ -163,3 +163,22 @@ | Error Code | Message | Severity | Info / Reference | | ---------- | ----------------------------------------- | -------- | ---------------- | | 1300 | References a %s (%s) which does not exist | 2 | | + +## Ingress + +| Error Code | Message | Severity | Info / Reference | +| ---------- | ------------------------------------------------------------- | -------- | ---------------- | +| 1400 | Ingress LoadBalancer port reported an error: %s | 3 | | +| 1401 | Ingress references a service backend which does not exist: %s | 3 | | +| 1402 | Ingress references a service port which is not defined: %s | 3 | | +| 1403 | Ingress backend uses a port#, prefer a named port: %d | 1 | | +| 1404 | Invalid Ingress backend spec. Must use port name or number | 3 | | + + +## CronJob + +| Error Code | Message | Severity | Info / Reference | +| ---------- | ----------------------------------------- | -------- | ---------------- | +| 1500 | %s is suspended | 2 | | +| 1501 | No active jobs detected | 1 | | +| 1502 | CronJob has not run yet or is failing | 2 | | \ No newline at end of file diff --git a/go.mod b/go.mod index 217aba56..a12e3c93 100644 --- a/go.mod +++ b/go.mod @@ -3,35 +3,40 @@ module github.com/derailed/popeye go 1.21.1 require ( + github.com/Masterminds/semver v1.5.0 github.com/aws/aws-sdk-go v1.35.21 github.com/fvbommel/sortorder v1.0.1 - github.com/magiconair/properties v1.8.5 - github.com/prometheus/client_golang v1.12.1 - github.com/prometheus/common v0.34.0 + github.com/hashicorp/go-memdb v1.3.4 + github.com/prometheus/client_golang v1.17.0 + github.com/prometheus/common v0.45.0 github.com/rs/zerolog v1.18.0 github.com/spf13/cobra v1.7.0 github.com/stretchr/testify v1.8.4 + github.com/xeipuuv/gojsonschema v1.2.0 golang.org/x/net v0.17.0 gopkg.in/yaml.v2 v2.4.0 + gopkg.in/yaml.v3 v3.0.1 k8s.io/api v0.29.0 k8s.io/apimachinery v0.29.0 k8s.io/cli-runtime v0.29.0 k8s.io/client-go v0.29.0 k8s.io/metrics v0.29.0 + sigs.k8s.io/gateway-api v1.0.0 + sigs.k8s.io/yaml v1.4.0 ) require ( github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect github.com/beorn7/perks v1.0.1 // indirect - github.com/cespare/xxhash/v2 v2.1.2 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect - github.com/evanphx/json-patch v4.12.0+incompatible // indirect + github.com/evanphx/json-patch v5.7.0+incompatible // indirect github.com/go-errors/errors v1.4.2 // indirect github.com/go-logr/logr v1.3.0 // 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/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/google/btree v1.0.1 // indirect @@ -39,16 +44,18 @@ require ( github.com/google/go-cmp v0.6.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect - github.com/google/uuid v1.3.0 // indirect + github.com/google/uuid v1.3.1 // indirect github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect - github.com/imdario/mergo v0.3.6 // indirect + github.com/hashicorp/go-immutable-radix v1.3.0 // indirect + github.com/hashicorp/golang-lru v0.5.4 // indirect + github.com/imdario/mergo v0.3.16 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect github.com/mailru/easyjson v0.7.7 // indirect - github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect + github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect github.com/moby/term v0.0.0-20221205130635-1aeaba878587 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect @@ -57,21 +64,22 @@ require ( github.com/peterbourgon/diskv v2.0.1+incompatible // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/prometheus/client_model v0.2.0 // indirect - github.com/prometheus/procfs v0.7.3 // indirect + github.com/prometheus/client_model v0.5.0 // indirect + github.com/prometheus/procfs v0.12.0 // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect + github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xlab/treeprint v1.2.0 // indirect go.starlark.net v0.0.0-20230525235612-a134d8f9ddca // indirect - golang.org/x/oauth2 v0.10.0 // indirect + golang.org/x/oauth2 v0.13.0 // indirect golang.org/x/sync v0.3.0 // indirect golang.org/x/sys v0.13.0 // indirect golang.org/x/term v0.13.0 // indirect golang.org/x/text v0.13.0 // indirect golang.org/x/time v0.3.0 // indirect - google.golang.org/appengine v1.6.7 // indirect + google.golang.org/appengine v1.6.8 // indirect google.golang.org/protobuf v1.31.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/klog/v2 v2.110.1 // indirect k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 // indirect k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect @@ -79,5 +87,4 @@ require ( 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.4.1 // indirect - sigs.k8s.io/yaml v1.3.0 // indirect ) diff --git a/go.sum b/go.sum index 80ec86dd..84a64373 100644 --- a/go.sum +++ b/go.sum @@ -1,60 +1,20 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= -cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= -cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= -cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= -cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= -cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= -cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= -cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= -cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= -cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= -cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= -cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= -cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= -cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= -cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= -cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= -cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= -cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= +github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/aws/aws-sdk-go v1.35.21 h1:6cMeHzcca+0uweOpUonDYv4DsPp9Qa9PTMYxH+VqDkY= github.com/aws/aws-sdk-go v1.35.21/go.mod h1:tlPOdRjfxPBpNIwqDj61rmsnA85v9jc0Ps9+muhnW+k= -github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= -github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= @@ -65,72 +25,42 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= -github.com/evanphx/json-patch v4.12.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/fvbommel/sortorder v1.0.1 h1:dSnXLt4mJYH25uDDGa3biZNQsozaUWDSWeKJ0qqFfzE= github.com/fvbommel/sortorder v1.0.1/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0= github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= -github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= -github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= -github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= -github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= -github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= 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-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +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-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= -github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= @@ -139,10 +69,8 @@ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5a github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= @@ -150,31 +78,25 @@ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= +github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 h1:pdN6V1QBWetyv/0+wjACpqVH+eVULgEjkurDLq3goeM= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/hashicorp/go-immutable-radix v1.3.0 h1:8exGP7ego3OmkfksihtSouGMZ+hQrhxx+FVELeXpVPE= +github.com/hashicorp/go-immutable-radix v1.3.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-memdb v1.3.4 h1:XSL3NR682X/cVk2IeV0d70N4DZ9ljI885xAEU8IoK3c= +github.com/hashicorp/go-memdb v1.3.4/go.mod h1:uBTr1oQbtuMgd1SSGoR8YV27eT3sBHbYiNm53bMpgSg= +github.com/hashicorp/go-uuid v1.0.0 h1:RS8zrF7PhGwyNPOtxSClXXj9HA8feRnJzgnI1RJCSnM= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28= -github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= +github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +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/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= @@ -183,22 +105,10 @@ github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGw github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= -github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= -github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -208,63 +118,41 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= -github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls= -github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= -github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +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/moby/term v0.0.0-20221205130635-1aeaba878587 h1:HfkjXDfhgVaN5rmueG8cL8KKeFNecRCXFhaJ2qZ5SKA= github.com/moby/term v0.0.0-20221205130635-1aeaba878587/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 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/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/onsi/ginkgo/v2 v2.13.0 h1:0jY9lJquiL8fcf3M4LAXN5aMlS/b2BV86HFFPCPMgE4= github.com/onsi/ginkgo/v2 v2.13.0/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o= github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg= github.com/onsi/gomega v1.29.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= -github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= -github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= -github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= -github.com/prometheus/client_golang v1.12.1 h1:ZiaPsmm9uiBeaSMRznKsCDNtPCS0T3JVDGF+06gjBzk= -github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= -github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q= +github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= -github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= -github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= -github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= -github.com/prometheus/common v0.34.0 h1:RBmGO9d/FVjqHT0yUGQwBJhkwKV+wPCn7KGpvfab0uE= -github.com/prometheus/common v0.34.0/go.mod h1:gB3sOl7P0TvJabZpLY5uQMpUqRCPPCyRLCZYc7JZTNE= -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.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= -github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU= -github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +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.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM= +github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY= +github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= +github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= @@ -273,173 +161,80 @@ github.com/rs/zerolog v1.18.0/go.mod h1:9nvC1axdVrAHcu/s9taAVfBuIdTZLVQmKQyvrUjF github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= -github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= -github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.starlark.net v0.0.0-20230525235612-a134d8f9ddca h1:VdD38733bfYv5tUZwEIskMM93VanwNIi5bIKnDrJdEY= go.starlark.net v0.0.0-20230525235612-a134d8f9ddca/go.mod h1:jxU+3+j+71eXOW14274+SmmuW82qJzl6iZSeqEtTGds= -golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= -golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= -golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= -golang.org/x/oauth2 v0.10.0 h1:zHCpF2Khkwy4mMB4bv0U37YtJdTGW8jI0glAApi0Kh8= -golang.org/x/oauth2 v0.10.0/go.mod h1:kTpgurOux7LqtuxjuyZa4Gj2gdezIt/jQtGnNFfypQI= +golang.org/x/oauth2 v0.13.0 h1:jDDenyj+WgFtmV3zYVoi8aE2BwtXFLWOA67ZfNWftiY= +golang.org/x/oauth2 v0.13.0/go.mod h1:/JMhi4ZRXAf4HG9LiNmxvk+45+96RUlVThiH8FzNBn0= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -447,175 +242,65 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190828213141-aed303cbaa74/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.12.0 h1:YW6HUoUmYBpwSgyaGaZq1fHjrBjX1rlpZ54T6mu2kss= -golang.org/x/tools v0.12.0/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc= +golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= -google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= -google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -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= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= -google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= -google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= -google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= k8s.io/api v0.29.0 h1:NiCdQMY1QOp1H8lfRyeEf8eOwV6+0xA6XEE44ohDX2A= k8s.io/api v0.29.0/go.mod h1:sdVmXoz2Bo/cb77Pxi71IPTSErEW32xa4aXwKH7gfBA= k8s.io/apimachinery v0.29.0 h1:+ACVktwyicPz0oc6MTMLwa2Pw3ouLAfAon1wPLtG48o= @@ -632,9 +317,8 @@ k8s.io/metrics v0.29.0 h1:a6dWcNM+EEowMzMZ8trka6wZtSRIfEA/9oLjuhBksGc= k8s.io/metrics v0.29.0/go.mod h1:UCuTT4dC/x/x6ODSk87IWIZQnuAfcwxOjb1gjWJdjMA= k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI= k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -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/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= @@ -643,5 +327,5 @@ sigs.k8s.io/kustomize/kyaml v0.14.3-0.20230601165947-6ce0bf390ce3 h1:W6cLQc5pnqM sigs.k8s.io/kustomize/kyaml v0.14.3-0.20230601165947-6ce0bf390ce3/go.mod h1:JWP1Fj0VWGHyw3YUPjXSQnRnrwezrZSrApfX5S0nIag= sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= -sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= -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/grafana/PopDash.json b/grafana/PopDash.json new file mode 100644 index 00000000..75de3d4f --- /dev/null +++ b/grafana/PopDash.json @@ -0,0 +1,1149 @@ +{ + "__inputs": [ + { + "name": "DS_PROMDASHB", + "label": "PromDashB", + "description": "", + "type": "datasource", + "pluginId": "prometheus", + "pluginName": "Prometheus" + } + ], + "__elements": {}, + "__requires": [ + { + "type": "panel", + "id": "gauge", + "name": "Gauge", + "version": "" + }, + { + "type": "grafana", + "id": "grafana", + "name": "Grafana", + "version": "10.2.2" + }, + { + "type": "datasource", + "id": "prometheus", + "name": "Prometheus", + "version": "1.0.0" + }, + { + "type": "panel", + "id": "stat", + "name": "Stat", + "version": "" + }, + { + "type": "panel", + "id": "state-timeline", + "name": "State timeline", + "version": "" + }, + { + "type": "panel", + "id": "text", + "name": "Text", + "version": "" + }, + { + "type": "panel", + "id": "timeseries", + "name": "Time series", + "version": "" + } + ], + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "description": "Popeye cluster scan report dashboard", + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": null, + "links": [], + "liveNow": true, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMDASHB}" + }, + "description": "", + "gridPos": { + "h": 6, + "w": 15, + "x": 0, + "y": 0 + }, + "id": 2, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "\"popeye-logo\"/\nPopeye K8s Cluster Scan\n

Biffs`em and Buffs`em!

\n\n", + "mode": "html" + }, + "pluginVersion": "10.2.2", + "transparent": true, + "type": "text" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 6 + }, + "id": 21, + "panels": [], + "title": "Grades", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMDASHB}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "percentage", + "steps": [ + { + "color": "semi-dark-red", + "value": null + }, + { + "color": "dark-orange", + "value": 50 + }, + { + "color": "green", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 24, + "x": 0, + "y": 7 + }, + "id": 9, + "options": { + "minVizHeight": 75, + "minVizWidth": 75, + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true + }, + "pluginVersion": "10.2.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMDASHB}" + }, + "editorMode": "code", + "expr": "popeye_cluster_score{scan!=\"\", cluster=~\"$cluster\", namespace=~\"$namespace\"}", + "instant": false, + "legendFormat": "{{cluster}}-{{namespace}} ({{grade}})", + "range": true, + "refId": "A" + } + ], + "type": "gauge" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 12 + }, + "id": 14, + "panels": [], + "title": "ScanCodes", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMDASHB}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "red", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 0, + "y": 13 + }, + "id": 11, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "10.2.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMDASHB}" + }, + "editorMode": "code", + "expr": "topk($topk, popeye_code_total{severity=\"error\", cluster=~\"$cluster\", namespace=~\"$namespace\", linter=~\"$linter\"}) by (namespace, linter)", + "format": "time_series", + "instant": false, + "legendFormat": "[POP-{{code}}] {{linter}}", + "range": true, + "refId": "A" + } + ], + "title": "Errors", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMDASHB}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "red", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 8, + "y": 13 + }, + "id": 17, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "10.2.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMDASHB}" + }, + "editorMode": "code", + "expr": "topk($topk, popeye_code_total{severity=\"warn\", cluster=~\"$cluster\", namespace=~\"$namespace\", linter=~\"$linter\"}) by (namespace, linter)", + "format": "time_series", + "instant": false, + "legendFormat": "[POP-{{code}}] {{linter}} ({{namespace}})", + "range": true, + "refId": "A" + } + ], + "title": "Warnings", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMDASHB}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "red", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 16, + "y": 13 + }, + "id": 12, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "10.2.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMDASHB}" + }, + "editorMode": "code", + "expr": "topk($topk, popeye_code_total{severity=\"info\", cluster=~\"$cluster\", namespace=~\"$namespace\", linter=~\"$linter\"}) by (namespace, linter)", + "format": "time_series", + "instant": false, + "legendFormat": "[POP-{{code}}] {{linter}}", + "range": true, + "refId": "A" + } + ], + "title": "Infos", + "type": "timeseries" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 20 + }, + "id": 13, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMDASHB}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "red", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 15, + "w": 8, + "x": 0, + "y": 21 + }, + "id": 7, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "10.2.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMDASHB}" + }, + "editorMode": "code", + "expr": "popeye_severity_total{severity=\"error\", cluster=~\"$cluster\", namespace=~\"$namespace\"}", + "format": "time_series", + "instant": false, + "legendFormat": "{{cluster}} ({{namespace}})", + "range": true, + "refId": "A" + } + ], + "title": "Errors", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMDASHB}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "orange", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 15, + "w": 8, + "x": 8, + "y": 21 + }, + "id": 6, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "10.2.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMDASHB}" + }, + "editorMode": "code", + "expr": "popeye_severity_total{severity=\"warn\", cluster=~\"$cluster\", namespace=~\"$namespace\"}", + "format": "time_series", + "instant": false, + "legendFormat": "{{cluster}} ({{namespace}})", + "range": true, + "refId": "A" + } + ], + "title": "Warnings", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMDASHB}" + }, + "description": "Popeye severity total scores.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 15, + "w": 8, + "x": 16, + "y": 21 + }, + "id": 5, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "value_and_name", + "wideLayout": true + }, + "pluginVersion": "10.2.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMDASHB}" + }, + "editorMode": "code", + "expr": "topk($topk, popeye_severity_total{severity=\"info\", cluster=~\"$cluster\", namespace=~\"$namespace\"})", + "instant": false, + "legendFormat": "{{namespace}}", + "range": true, + "refId": "A" + } + ], + "title": "Infos", + "type": "stat" + } + ], + "title": "Severities", + "type": "row" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 21 + }, + "id": 16, + "panels": [], + "title": "Linters", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMDASHB}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "fillOpacity": 70, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineWidth": 1, + "spanNulls": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "transparent", + "value": null + }, + { + "color": "super-light-red", + "value": 1 + }, + { + "color": "light-red", + "value": 5 + }, + { + "color": "red", + "value": 10 + }, + { + "color": "semi-dark-red", + "value": 20 + }, + { + "color": "dark-red", + "value": 30 + }, + { + "color": "#f9051b", + "value": 50 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 12, + "w": 8, + "x": 0, + "y": 22 + }, + "id": 10, + "options": { + "alignValue": "center", + "legend": { + "displayMode": "table", + "placement": "bottom", + "showLegend": false + }, + "mergeValues": true, + "rowHeight": 0.64, + "showValue": "auto", + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "10.2.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMDASHB}" + }, + "editorMode": "code", + "expr": "topk($topk, sum by (linter) (popeye_linter_tally_total{severity=\"error\", cluster=~\"$cluster\", linter=~\"$linter\"})) by (cluster, linter)", + "format": "time_series", + "instant": false, + "legendFormat": "{{linter}}", + "range": true, + "refId": "A" + } + ], + "title": "Errors", + "type": "state-timeline" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMDASHB}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "fillOpacity": 70, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineWidth": 0, + "spanNulls": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "transparent", + "value": null + }, + { + "color": "light-yellow", + "value": 1 + }, + { + "color": "dark-yellow", + "value": 5 + }, + { + "color": "light-orange", + "value": 10 + }, + { + "color": "semi-dark-orange", + "value": 30 + }, + { + "color": "dark-orange", + "value": 50 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 12, + "w": 8, + "x": 8, + "y": 22 + }, + "id": 18, + "options": { + "alignValue": "left", + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "mergeValues": true, + "rowHeight": 0.9, + "showValue": "auto", + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "10.2.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMDASHB}" + }, + "editorMode": "code", + "expr": "topk($topk, sum by (linter) (popeye_linter_tally_total{severity=\"warn\", cluster=~\"$cluster\", linter=~\"$linter\"})) by (cluster, linter)", + "format": "time_series", + "instant": false, + "legendFormat": "{{linter}}", + "range": true, + "refId": "A" + } + ], + "title": "Warnings", + "type": "state-timeline" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMDASHB}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "fillOpacity": 70, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineWidth": 0, + "spanNulls": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "transparent", + "value": null + }, + { + "color": "super-light-blue", + "value": 1 + }, + { + "color": "light-blue", + "value": 5 + }, + { + "color": "blue", + "value": 10 + }, + { + "color": "semi-dark-blue", + "value": 20 + }, + { + "color": "dark-blue", + "value": 30 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 12, + "w": 8, + "x": 16, + "y": 22 + }, + "id": 19, + "options": { + "alignValue": "left", + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "mergeValues": true, + "rowHeight": 0.9, + "showValue": "auto", + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "10.2.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMDASHB}" + }, + "editorMode": "code", + "expr": "topk($topk, sum by (linter) (popeye_linter_tally_total{severity=\"info\", cluster=~\"$cluster\", linter=~\"$linter\"})) by (cluster, linter)", + "format": "time_series", + "instant": false, + "legendFormat": "{{linter}}", + "range": true, + "refId": "A" + } + ], + "title": "Info", + "type": "state-timeline" + } + ], + "refresh": "", + "schemaVersion": 38, + "tags": [ + "popeye" + ], + "templating": { + "list": [ + { + "current": { + "selected": false, + "text": "10", + "value": "10" + }, + "description": "Top k values", + "hide": 0, + "includeAll": false, + "label": "TopK", + "multi": false, + "name": "topk", + "options": [ + { + "selected": true, + "text": "10", + "value": "10" + }, + { + "selected": false, + "text": "20", + "value": "20" + }, + { + "selected": false, + "text": "50", + "value": "50" + } + ], + "query": "10,20,50", + "queryValue": "", + "skipUrlSync": false, + "type": "custom" + }, + { + "allValue": "", + "current": {}, + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMDASHB}" + }, + "definition": "label_values(popeye_code_total,cluster)", + "hide": 0, + "includeAll": true, + "label": "Cluster", + "multi": false, + "name": "cluster", + "options": [], + "query": { + "qryType": 1, + "query": "label_values(popeye_code_total,cluster)", + "refId": "PrometheusVariableQueryEditor-VariableQuery" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 1, + "type": "query" + }, + { + "current": {}, + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMDASHB}" + }, + "definition": "label_values(popeye_code_total,namespace)", + "hide": 0, + "includeAll": true, + "label": "Namespace", + "multi": false, + "name": "namespace", + "options": [], + "query": { + "qryType": 1, + "query": "label_values(popeye_code_total,namespace)", + "refId": "PrometheusVariableQueryEditor-VariableQuery" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "type": "query" + }, + { + "current": {}, + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMDASHB}" + }, + "definition": "label_values(popeye_code_total,linter)", + "hide": 0, + "includeAll": true, + "label": "Linter", + "multi": false, + "name": "linter", + "options": [], + "query": { + "qryType": 1, + "query": "label_values(popeye_code_total,linter)", + "refId": "PrometheusVariableQueryEditor-VariableQuery" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "type": "query" + } + ] + }, + "time": { + "from": "now-3h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "PopDash", + "uid": "ca2c5173-a010-4a3d-aab2-cb2871e6f3dd", + "version": 60, + "weekStart": "" +} \ No newline at end of file diff --git a/internal/alias.go b/internal/alias.go index f65cea1a..ee647c52 100644 --- a/internal/alias.go +++ b/internal/alias.go @@ -5,62 +5,110 @@ package internal import ( "fmt" + "slices" "strings" - "github.com/derailed/popeye/internal/client" "github.com/derailed/popeye/types" "github.com/rs/zerolog/log" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +var ( + ClusterGVR = types.NewGVR("cluster") ) // ResourceMetas represents a collection of resource metadata. -type ResourceMetas map[client.GVR]metav1.APIResource +type ResourceMetas map[types.GVR]metav1.APIResource // Aliases represents a collection of resource aliases. type Aliases struct { - aliases map[string]client.GVR + aliases map[string]types.GVR metas ResourceMetas } // NewAliases returns a new instance. func NewAliases() *Aliases { a := Aliases{ - aliases: make(map[string]client.GVR), + aliases: make(map[string]types.GVR), metas: make(ResourceMetas), } return &a } +func (a *Aliases) Dump() { + log.Debug().Msgf("\nAliases...") + kk := make([]string, 0, len(a.aliases)) + for k := range a.aliases { + kk = append(kk, k) + } + slices.Sort(kk) + for _, k := range kk { + log.Debug().Msgf("%-25s: %s", k, a.aliases[k]) + } +} + +var customShortNames = map[string][]string{ + "cluster": {"cl"}, + "secrets": {"sec"}, + "deployments": {"dp"}, + "clusterroles": {"cr"}, + "clusterrolebindings": {"crb"}, + "roles": {"ro"}, + "rolebindings": {"rb"}, + "networkpolicies": {"np"}, + "httproutes": {"gwr"}, + "gatewayclassess": {"gwc"}, + "gateways": {"gw"}, +} + // Init loads the aliases glossary. -func (a *Aliases) Init(f types.Factory, gvrs GVRs) error { - if err := a.loadPreferred(f); err != nil { +func (a *Aliases) Init(c types.Connection) error { + if err := a.loadPreferred(c); err != nil { return err } - for _, k := range gvrs { - gvr := client.NewGVR(k) - res, ok := a.metas[gvr] - if !ok { - panic(fmt.Sprintf("No resource meta found for %s", gvr)) - } + for gvr, res := range a.metas { a.aliases[res.Name] = gvr - a.aliases[res.SingularName] = gvr + if res.SingularName != "" { + a.aliases[res.SingularName] = gvr + } for _, n := range res.ShortNames { a.aliases[n] = gvr } + if kk, ok := customShortNames[res.Name]; ok { + for _, k := range kk { + a.aliases[k] = gvr + } + } + if lgvr, ok := Glossary[R(res.SingularName)]; ok { + if greaterV(gvr.V(), lgvr.V()) { + Glossary[R(res.SingularName)] = gvr + } + } else if lgvr, ok := Glossary[R(res.Name)]; ok { + if greaterV(gvr.V(), lgvr.V()) { + Glossary[R(res.Name)] = gvr + } + } } - a.aliases["cl"] = client.NewGVR("cluster") - a.aliases["sec"] = client.NewGVR("v1/secrets") - a.aliases["dp"] = client.NewGVR("apps/v1/deployments") - a.aliases["cr"] = client.NewGVR("rbac.authorization.k8s.io/v1/clusterroles") - a.aliases["crb"] = client.NewGVR("rbac.authorization.k8s.io/v1/clusterrolebindings") - a.aliases["ro"] = client.NewGVR("rbac.authorization.k8s.io/v1/roles") - a.aliases["rb"] = client.NewGVR("rbac.authorization.k8s.io/v1/rolebindings") - a.aliases["np"] = client.NewGVR("networking.k8s.io/v1/networkpolicies") return nil } +func greaterV(v1, v2 string) bool { + if v1 == "" && v2 == "" { + return true + } + if v2 == "" { + return true + } + if v1 == "v1" || v1 == "v2" { + return true + } + + return false +} + // TitleFor produces a section title from an alias. func (a *Aliases) TitleFor(s string, plural bool) string { gvr, ok := a.aliases[s] @@ -77,28 +125,31 @@ func (a *Aliases) TitleFor(s string, plural bool) string { return m.SingularName } -func (a *Aliases) loadPreferred(f types.Factory) error { - dial, err := f.Client().CachedDiscovery() +func (a *Aliases) loadPreferred(c types.Connection) error { + dial, err := c.CachedDiscovery() if err != nil { return err } - rr, err := dial.ServerPreferredResources() + ll, err := dial.ServerPreferredResources() if err != nil { return err } - for _, r := range rr { - for _, res := range r.APIResources { - gvr := client.FromGVAndR(r.GroupVersion, res.Name) - res.Group, res.Version = gvr.G(), gvr.V() - if res.SingularName == "" { - res.SingularName = strings.ToLower(res.Kind) + for _, l := range ll { + gv, err := schema.ParseGroupVersion(l.GroupVersion) + if err != nil { + continue + } + for _, r := range l.APIResources { + gvr := types.NewGVRFromAPIRes(gv, r) + r.Group, r.Version = gvr.G(), gvr.V() + if r.SingularName == "" { + r.SingularName = strings.ToLower(r.Kind) } - a.metas[gvr] = res + a.metas[gvr] = r } } - - a.metas[client.NewGVR("cluster")] = metav1.APIResource{ - Name: "cluster", + a.metas[ClusterGVR] = metav1.APIResource{ + Name: ClusterGVR.String(), } return nil @@ -118,17 +169,18 @@ func (a *Aliases) ToResources(nn []string) []string { } // Singular returns a singular resource name. -func (a *Aliases) Singular(gvr client.GVR) string { +func (a *Aliases) Singular(gvr types.GVR) string { m, ok := a.metas[gvr] if !ok { log.Error().Msgf("Missing meta for gvr %q", gvr) return gvr.R() } + return m.SingularName } // Exclude checks if section should be excluded from the report. -func (a *Aliases) Exclude(gvr client.GVR, sections []string) bool { +func (a *Aliases) Exclude(gvr types.GVR, sections []string) bool { if len(sections) == 0 { return false } diff --git a/internal/cache/cluster.go b/internal/cache/cluster.go index b21e1c11..751a92b5 100644 --- a/internal/cache/cluster.go +++ b/internal/cache/cluster.go @@ -3,20 +3,22 @@ package cache +import "github.com/Masterminds/semver" + // ClusterKey tracks Cluster resource references const ClusterKey = "cl" // Cluster represents Cluster cache. type Cluster struct { - major, minor string + rev *semver.Version } // NewCluster returns a new Cluster cache. -func NewCluster(major, minor string) *Cluster { - return &Cluster{major: major, minor: minor} +func NewCluster(v *semver.Version) *Cluster { + return &Cluster{rev: v} } // ListVersion returns cluster server version. -func (c *Cluster) ListVersion() (string, string) { - return c.major, c.minor +func (c *Cluster) ListVersion() *semver.Version { + return c.rev } diff --git a/internal/cache/cluster_test.go b/internal/cache/cluster_test.go index d5ffa4d9..459f70fc 100644 --- a/internal/cache/cluster_test.go +++ b/internal/cache/cluster_test.go @@ -6,14 +6,21 @@ package cache_test import ( "testing" + "github.com/Masterminds/semver" "github.com/derailed/popeye/internal/cache" + "github.com/rs/zerolog" "github.com/stretchr/testify/assert" ) +func init() { + zerolog.SetGlobalLevel(zerolog.FatalLevel) +} + func TestCluster(t *testing.T) { - c := cache.NewCluster("1", "9") + v, err := semver.NewVersion("1.9") + assert.NoError(t, err) + c := cache.NewCluster(v) - ma, mi := c.ListVersion() - assert.Equal(t, "1", ma) - assert.Equal(t, "9", mi) + v1 := c.ListVersion() + assert.Equal(t, v, v1) } diff --git a/internal/cache/cm.go b/internal/cache/cm.go deleted file mode 100644 index 2aa81b0e..00000000 --- a/internal/cache/cm.go +++ /dev/null @@ -1,26 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package cache - -import ( - v1 "k8s.io/api/core/v1" -) - -// ConfigMapKey tracks ConfigMap resource references -const ConfigMapKey = "cm" - -// ConfigMap represents ConfigMap cache. -type ConfigMap struct { - cms map[string]*v1.ConfigMap -} - -// NewConfigMap returns a new ConfigMap cache. -func NewConfigMap(cms map[string]*v1.ConfigMap) *ConfigMap { - return &ConfigMap{cms: cms} -} - -// ListConfigMaps returns all available ConfigMaps on the cluster. -func (c *ConfigMap) ListConfigMaps() map[string]*v1.ConfigMap { - return c.cms -} diff --git a/internal/cache/cr.go b/internal/cache/cr.go deleted file mode 100644 index 400098a6..00000000 --- a/internal/cache/cr.go +++ /dev/null @@ -1,26 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package cache - -import ( - rbacv1 "k8s.io/api/rbac/v1" -) - -// ClusterRoleKey tracks ClusterRole resource references -const ClusterRoleKey = "clusterrole" - -// ClusterRole represents ClusterRole cache. -type ClusterRole struct { - crs map[string]*rbacv1.ClusterRole -} - -// NewClusterRole returns a new ClusterRole cache. -func NewClusterRole(crs map[string]*rbacv1.ClusterRole) *ClusterRole { - return &ClusterRole{crs: crs} -} - -// ListClusterRoles returns all available ClusterRoles on the cluster. -func (c *ClusterRole) ListClusterRoles() map[string]*rbacv1.ClusterRole { - return c.crs -} diff --git a/internal/cache/crb.go b/internal/cache/crb.go index 110f8a40..a503a602 100644 --- a/internal/cache/crb.go +++ b/internal/cache/crb.go @@ -8,27 +8,28 @@ import ( "sync" "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/client" + "github.com/derailed/popeye/internal/db" rbacv1 "k8s.io/api/rbac/v1" ) // ClusterRoleBinding represents ClusterRoleBinding cache. type ClusterRoleBinding struct { - crbs map[string]*rbacv1.ClusterRoleBinding + db *db.DB } // NewClusterRoleBinding returns a new ClusterRoleBinding cache. -func NewClusterRoleBinding(crbs map[string]*rbacv1.ClusterRoleBinding) *ClusterRoleBinding { - return &ClusterRoleBinding{crbs: crbs} -} - -// ListClusterRoleBindings returns all available ClusterRoleBindings on the cluster. -func (c *ClusterRoleBinding) ListClusterRoleBindings() map[string]*rbacv1.ClusterRoleBinding { - return c.crbs +func NewClusterRoleBinding(db *db.DB) *ClusterRoleBinding { + return &ClusterRoleBinding{db: db} } // ClusterRoleRefs computes all clusterrole external references. func (c *ClusterRoleBinding) ClusterRoleRefs(refs *sync.Map) { - for fqn, crb := range c.crbs { + txn, it := c.db.MustITFor(internal.Glossary[internal.CRB]) + defer txn.Abort() + for o := it.Next(); o != nil; o = it.Next() { + crb := o.(*rbacv1.ClusterRoleBinding) + fqn := client.FQN(crb.Namespace, crb.Name) key := ResFqn(strings.ToLower(crb.RoleRef.Kind), FQN(crb.Namespace, crb.RoleRef.Name)) if c, ok := refs.Load(key); ok { c.(internal.StringSet).Add(fqn) diff --git a/internal/cache/crb_test.go b/internal/cache/crb_test.go index a3629bb9..82a06df3 100644 --- a/internal/cache/crb_test.go +++ b/internal/cache/crb_test.go @@ -9,13 +9,21 @@ import ( "github.com/derailed/popeye/internal" "github.com/derailed/popeye/internal/cache" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/test" "github.com/stretchr/testify/assert" rbacv1 "k8s.io/api/rbac/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) func TestClusterRoleRef(t *testing.T) { - cr := cache.NewClusterRoleBinding(makeCRBMap()) + dba, err := test.NewTestDB() + assert.NoError(t, err) + l := db.NewLoader(dba) + + ctx := test.MakeCtx(t) + assert.NoError(t, test.LoadDB[*rbacv1.ClusterRoleBinding](ctx, l.DB, "auth/crb/1.yaml", internal.Glossary[internal.CRB])) + + cr := cache.NewClusterRoleBinding(dba) var refs sync.Map cr.ClusterRoleRefs(&refs) @@ -24,36 +32,9 @@ func TestClusterRoleRef(t *testing.T) { _, ok = m.(internal.StringSet)["crb1"] assert.True(t, ok) - m, ok = refs.Load("role:blee/r1") - assert.True(t, ok) - _, ok = m.(internal.StringSet)["crb2"] + m, ok = refs.Load("role:r1") assert.True(t, ok) -} - -// Helpers... -func makeCRBMap() map[string]*rbacv1.ClusterRoleBinding { - return map[string]*rbacv1.ClusterRoleBinding{ - "crb1": makeCRB("", "crb1", "ClusterRole", "cr1"), - "crb2": makeCRB("blee", "crb2", "Role", "r1"), - } -} - -func makeCRB(ns, name, kind, refName string) *rbacv1.ClusterRoleBinding { - return &rbacv1.ClusterRoleBinding{ - ObjectMeta: makeObjMeta(ns, name), - RoleRef: rbacv1.RoleRef{ - Kind: kind, - Name: refName, - }, - } -} - -func makeObjMeta(ns, n string) metav1.ObjectMeta { - m := metav1.ObjectMeta{Name: n} - if ns != "" { - m.Namespace = ns - } - - return m + _, ok = m.(internal.StringSet)["crb3"] + assert.True(t, ok) } diff --git a/internal/cache/dp.go b/internal/cache/dp.go deleted file mode 100644 index f7a47e6b..00000000 --- a/internal/cache/dp.go +++ /dev/null @@ -1,26 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package cache - -import ( - appsv1 "k8s.io/api/apps/v1" -) - -// DeploymentKey tracks Deployment resource references -const DeploymentKey = "dp" - -// Deployment represents Deployment cache. -type Deployment struct { - dps map[string]*appsv1.Deployment -} - -// NewDeployment returns a new Deployment cache. -func NewDeployment(dps map[string]*appsv1.Deployment) *Deployment { - return &Deployment{dps: dps} -} - -// ListDeployments returns all available Deployments on the cluster. -func (d *Deployment) ListDeployments() map[string]*appsv1.Deployment { - return d.dps -} diff --git a/internal/cache/ds.go b/internal/cache/ds.go deleted file mode 100644 index ee8e53e9..00000000 --- a/internal/cache/ds.go +++ /dev/null @@ -1,26 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package cache - -import ( - appsv1 "k8s.io/api/apps/v1" -) - -// DaemonSetKey tracks DaemonSet resource references -const DaemonSetKey = "ds" - -// DaemonSet represents DaemonSet cache. -type DaemonSet struct { - ds map[string]*appsv1.DaemonSet -} - -// NewDaemonSet returns a new DaemonSet cache. -func NewDaemonSet(ds map[string]*appsv1.DaemonSet) *DaemonSet { - return &DaemonSet{ds: ds} -} - -// ListDaemonSets returns all available DaemonSets on the cluster. -func (d *DaemonSet) ListDaemonSets() map[string]*appsv1.DaemonSet { - return d.ds -} diff --git a/internal/cache/ep.go b/internal/cache/ep.go deleted file mode 100644 index 21b47ff5..00000000 --- a/internal/cache/ep.go +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package cache - -import ( - v1 "k8s.io/api/core/v1" -) - -// Endpoints represents Endpoints cache. -type Endpoints struct { - eps map[string]*v1.Endpoints -} - -// NewEndpoints returns a new Endpoints cache. -func NewEndpoints(eps map[string]*v1.Endpoints) *Endpoints { - return &Endpoints{eps: eps} -} - -// GetEndpoints returns all available Endpoints on the cluster. -func (e *Endpoints) GetEndpoints(fqn string) *v1.Endpoints { - return e.eps[fqn] -} diff --git a/internal/cache/helper.go b/internal/cache/helper.go index e42383dd..3c0b479d 100644 --- a/internal/cache/helper.go +++ b/internal/cache/helper.go @@ -37,7 +37,7 @@ func namespaced(fqn string) (string, string) { } // MatchLabels check if pod labels match a selector. -func matchLabels(labels, sel map[string]string) bool { +func MatchLabels(labels, sel map[string]string) bool { if len(sel) == 0 { return false } diff --git a/internal/cache/helper_test.go b/internal/cache/helper_test.go index d7d7ee4d..71e2c30f 100644 --- a/internal/cache/helper_test.go +++ b/internal/cache/helper_test.go @@ -78,7 +78,7 @@ func TestMatchLabels(t *testing.T) { for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, matchLabels(u.labels, u.selector)) + assert.Equal(t, u.e, MatchLabels(u.labels, u.selector)) }) } } diff --git a/internal/cache/hpa.go b/internal/cache/hpa.go deleted file mode 100644 index 7af3340d..00000000 --- a/internal/cache/hpa.go +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package cache - -import ( - autoscalingv1 "k8s.io/api/autoscaling/v1" -) - -// HorizontalPodAutoscaler represents a collection of HorizontalPodAutoScalers available on a cluster. -type HorizontalPodAutoscaler struct { - hpas map[string]*autoscalingv1.HorizontalPodAutoscaler -} - -// NewHorizontalPodAutoscaler returns a new HorizontalPodAutoScaler. -func NewHorizontalPodAutoscaler(svcs map[string]*autoscalingv1.HorizontalPodAutoscaler) *HorizontalPodAutoscaler { - return &HorizontalPodAutoscaler{svcs} -} - -// ListHorizontalPodAutoscalers returns all available HorizontalPodAutoScalers on the cluster. -func (h *HorizontalPodAutoscaler) ListHorizontalPodAutoscalers() map[string]*autoscalingv1.HorizontalPodAutoscaler { - return h.hpas -} diff --git a/internal/cache/ing.go b/internal/cache/ing.go index fc118d55..b67980ca 100644 --- a/internal/cache/ing.go +++ b/internal/cache/ing.go @@ -4,11 +4,12 @@ package cache import ( + "errors" "sync" - netv1 "k8s.io/api/networking/v1" - "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" + netv1 "k8s.io/api/networking/v1" ) // IngressKey tracks Ingress resource references @@ -16,26 +17,29 @@ const IngressKey = "ing" // Ingress represents Ingress cache. type Ingress struct { - ings map[string]*netv1.Ingress + db *db.DB } // NewIngress returns a new Ingress cache. -func NewIngress(ings map[string]*netv1.Ingress) *Ingress { - return &Ingress{ings: ings} -} - -// ListIngresses returns all available Ingresss on the cluster. -func (d *Ingress) ListIngresses() map[string]*netv1.Ingress { - return d.ings +func NewIngress(db *db.DB) *Ingress { + return &Ingress{db: db} } // IngressRefs computes all ingress external references. -func (d *Ingress) IngressRefs(refs *sync.Map) { - for _, ing := range d.ings { +func (d *Ingress) IngressRefs(refs *sync.Map) error { + txn, it := d.db.MustITFor(internal.Glossary[internal.ING]) + defer txn.Abort() + for o := it.Next(); o != nil; o = it.Next() { + ing, ok := o.(*netv1.Ingress) + if !ok { + return errors.New("expected ing") + } for _, tls := range ing.Spec.TLS { d.trackReference(refs, ResFqn(SecretKey, FQN(ing.Namespace, tls.SecretName))) } } + + return nil } func (d *Ingress) trackReference(refs *sync.Map, key string) { diff --git a/internal/cache/ing_test.go b/internal/cache/ing_test.go index b393c953..0a66d9d9 100644 --- a/internal/cache/ing_test.go +++ b/internal/cache/ing_test.go @@ -7,29 +7,24 @@ import ( "sync" "testing" - "github.com/magiconair/properties/assert" + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/test" + "github.com/stretchr/testify/assert" netv1 "k8s.io/api/networking/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) func TestIngressRefs(t *testing.T) { - ing := NewIngress(map[string]*netv1.Ingress{ - "default/ing1": { - ObjectMeta: metav1.ObjectMeta{ - Namespace: "default", - }, - Spec: netv1.IngressSpec{ - TLS: []netv1.IngressTLS{ - { - SecretName: "foo", - }, - }, - }, - }, - }) + dba, err := test.NewTestDB() + assert.NoError(t, err) + l := db.NewLoader(dba) + + ctx := test.MakeCtx(t) + assert.NoError(t, test.LoadDB[*netv1.Ingress](ctx, l.DB, "net/ingress/1.yaml", internal.Glossary[internal.ING])) var refs sync.Map - ing.IngressRefs(&refs) + ing := NewIngress(dba) + assert.NoError(t, ing.IngressRefs(&refs)) _, ok := refs.Load("sec:default/foo") assert.Equal(t, ok, true) diff --git a/internal/cache/limit_range.go b/internal/cache/limit_range.go deleted file mode 100644 index 68a9f41f..00000000 --- a/internal/cache/limit_range.go +++ /dev/null @@ -1,26 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package cache - -import ( - v1 "k8s.io/api/core/v1" -) - -// LimitRangeKey tracks LimitRange resource references -const LimitRangeKey = "lr" - -// LimitRange represents LimitRange cache. -type LimitRange struct { - lrs map[string]*v1.LimitRange -} - -// NewLimitRange returns a new LimitRange cache. -func NewLimitRange(lrs map[string]*v1.LimitRange) *LimitRange { - return &LimitRange{lrs: lrs} -} - -// ListLimitRanges returns all available LimitRanges on the cluster. -func (c *LimitRange) ListLimitRanges() map[string]*v1.LimitRange { - return c.lrs -} diff --git a/internal/cache/no.go b/internal/cache/no.go deleted file mode 100644 index 6b27818c..00000000 --- a/internal/cache/no.go +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package cache - -import ( - v1 "k8s.io/api/core/v1" -) - -// Node represents a collection of Nodes available on a cluster. -type Node struct { - nodes map[string]*v1.Node -} - -// NewNode returns a new Node. -func NewNode(svcs map[string]*v1.Node) *Node { - return &Node{svcs} -} - -// ListNodes returns all available Nodes on the cluster. -func (n *Node) ListNodes() map[string]*v1.Node { - return n.nodes -} diff --git a/internal/cache/no_mx.go b/internal/cache/no_mx.go index 89073422..2d83f753 100644 --- a/internal/cache/no_mx.go +++ b/internal/cache/no_mx.go @@ -4,53 +4,46 @@ package cache import ( + "github.com/derailed/popeye/internal/db" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" - mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" ) -// NodesMetrics represents a Node metrics cache. -type NodesMetrics struct { - nmx map[string]*mv1beta1.NodeMetrics -} - -// NewNodesMetrics returns new Node metrics cache. -func NewNodesMetrics(mx map[string]*mv1beta1.NodeMetrics) *NodesMetrics { - return &NodesMetrics{nmx: mx} -} - -// ListNodesMetrics returns all available NodeMetrics on the cluster. -func (n *NodesMetrics) ListNodesMetrics() map[string]*mv1beta1.NodeMetrics { - return n.nmx -} - // ListAllocatedMetrics collects total used cpu and mem on the cluster. -func (n *NodesMetrics) ListAllocatedMetrics() v1.ResourceList { +func listAllocatedMetrics(db *db.DB) (v1.ResourceList, error) { cpu, mem := new(resource.Quantity), new(resource.Quantity) - for _, mx := range n.nmx { + mm, err := db.ListNMX() + if err != nil { + return nil, err + } + for _, mx := range mm { cpu.Add(*mx.Usage.Cpu()) mem.Add(*mx.Usage.Memory()) } - return v1.ResourceList{ - v1.ResourceCPU: *cpu, - v1.ResourceMemory: *mem, - } + return v1.ResourceList{v1.ResourceCPU: *cpu, v1.ResourceMemory: *mem}, nil } // ListAvailableMetrics return the total cluster available cpu/mem. -func (n *NodesMetrics) ListAvailableMetrics(nn map[string]*v1.Node) v1.ResourceList { +func ListAvailableMetrics(db *db.DB) (v1.ResourceList, error) { cpu, mem := new(resource.Quantity), new(resource.Quantity) + nn, err := db.ListNodes() + if err != nil { + return nil, err + } for _, n := range nn { cpu.Add(*n.Status.Allocatable.Cpu()) mem.Add(*n.Status.Allocatable.Memory()) } - used := n.ListAllocatedMetrics() + used, err := listAllocatedMetrics(db) + if err != nil { + return nil, err + } cpu.Sub(*used.Cpu()) mem.Sub(*used.Memory()) return v1.ResourceList{ v1.ResourceCPU: *cpu, v1.ResourceMemory: *mem, - } + }, nil } diff --git a/internal/cache/no_mx_test.go b/internal/cache/no_mx_test.go index 19c00f30..739d904f 100644 --- a/internal/cache/no_mx_test.go +++ b/internal/cache/no_mx_test.go @@ -6,9 +6,11 @@ package cache import ( "testing" + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/test" "github.com/stretchr/testify/assert" v1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" ) @@ -24,17 +26,26 @@ func TestClusterAllocatableMetrics(t *testing.T) { "n2": makeNodeMx("n2", "300m", "200Mi"), }, e: v1.ResourceList{ - v1.ResourceCPU: toQty("400m"), - v1.ResourceMemory: toQty("300Mi"), + v1.ResourceCPU: test.ToQty("2"), + v1.ResourceMemory: test.ToQty("200Mi"), }, }, } + dba, err := test.NewTestDB() + assert.NoError(t, err) + l := db.NewLoader(dba) + + ctx := test.MakeCtx(t) + assert.NoError(t, test.LoadDB[*mv1beta1.NodeMetrics](ctx, l.DB, "mx/node/1.yaml", internal.Glossary[internal.NMX])) + assert.NoError(t, test.LoadDB[*v1.Node](ctx, l.DB, "core/node/1.yaml", internal.Glossary[internal.NO])) + for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { - n := NewNodesMetrics(map[string]*mv1beta1.NodeMetrics{}) - res := n.ListAvailableMetrics(u.nn) + res, err := ListAvailableMetrics(dba) + assert.NoError(t, err) + assert.Equal(t, u.e.Cpu().Value(), res.Cpu().Value()) assert.Equal(t, u.e.Memory().Value(), res.Memory().Value()) }) @@ -44,19 +55,13 @@ func TestClusterAllocatableMetrics(t *testing.T) { // ---------------------------------------------------------------------------- // Helpers... -func toQty(s string) resource.Quantity { - q, _ := resource.ParseQuantity(s) - - return q -} - func makeNodeMx(n, cpu, mem string) *v1.Node { return &v1.Node{ ObjectMeta: metav1.ObjectMeta{Name: n}, Status: v1.NodeStatus{ Allocatable: v1.ResourceList{ - v1.ResourceCPU: toQty(cpu), - v1.ResourceMemory: toQty(mem), + v1.ResourceCPU: test.ToQty(cpu), + v1.ResourceMemory: test.ToQty(mem), }, }, } diff --git a/internal/cache/np.go b/internal/cache/np.go deleted file mode 100644 index 85038b2b..00000000 --- a/internal/cache/np.go +++ /dev/null @@ -1,26 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package cache - -import ( - nv1 "k8s.io/api/networking/v1" -) - -// NetworkPolicyKey tracks NetworkPolicy resource references -const NetworkPolicyKey = "np" - -// NetworkPolicy represents NetworkPolicy cache. -type NetworkPolicy struct { - nps map[string]*nv1.NetworkPolicy -} - -// NewNetworkPolicy returns a new NetworkPolicy cache. -func NewNetworkPolicy(nps map[string]*nv1.NetworkPolicy) *NetworkPolicy { - return &NetworkPolicy{nps: nps} -} - -// ListNetworkPolicies returns all available NetworkPolicys on the cluster. -func (d *NetworkPolicy) ListNetworkPolicies() map[string]*nv1.NetworkPolicy { - return d.nps -} diff --git a/internal/cache/ns.go b/internal/cache/ns.go deleted file mode 100644 index 1def995e..00000000 --- a/internal/cache/ns.go +++ /dev/null @@ -1,39 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package cache - -import ( - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -// Namespace represents a collection of Namespaces available on a cluster. -type Namespace struct { - nss map[string]*v1.Namespace -} - -// NewNamespace returns a new Namespace. -func NewNamespace(nss map[string]*v1.Namespace) *Namespace { - return &Namespace{nss} -} - -// ListNamespaces returns all available Namespaces on the cluster. -func (n *Namespace) ListNamespaces() map[string]*v1.Namespace { - return n.nss -} - -// ListNamespacesBySelector list all pods matching the given selector. -func (n *Namespace) ListNamespacesBySelector(sel *metav1.LabelSelector) map[string]*v1.Namespace { - res := map[string]*v1.Namespace{} - if sel == nil { - return res - } - for fqn, ns := range n.nss { - if matchLabels(ns.ObjectMeta.Labels, sel.MatchLabels) { - res[fqn] = ns - } - } - - return res -} diff --git a/internal/cache/pdb.go b/internal/cache/pdb.go deleted file mode 100644 index 4bb0435a..00000000 --- a/internal/cache/pdb.go +++ /dev/null @@ -1,41 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package cache - -import ( - policyv1 "k8s.io/api/policy/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -// PodDisruptionBudgetKey tracks PodDisruptionBudget resource references -const PodDisruptionBudgetKey = "pdb" - -// PodDisruptionBudget represents PodDisruptionBudget cache. -type PodDisruptionBudget struct { - cms map[string]*policyv1.PodDisruptionBudget -} - -// NewPodDisruptionBudget returns a new PodDisruptionBudget cache. -func NewPodDisruptionBudget(cms map[string]*policyv1.PodDisruptionBudget) *PodDisruptionBudget { - return &PodDisruptionBudget{cms: cms} -} - -// ListPodDisruptionBudgets returns all available PodDisruptionBudgets on the cluster. -func (c *PodDisruptionBudget) ListPodDisruptionBudgets() map[string]*policyv1.PodDisruptionBudget { - return c.cms -} - -// ForLabels returns a pdb whose selector match the given labels. Returns nil if no match. -func (c *PodDisruptionBudget) ForLabels(labels map[string]string) *policyv1.PodDisruptionBudget { - for _, pdb := range c.ListPodDisruptionBudgets() { - m, err := metav1.LabelSelectorAsMap(pdb.Spec.Selector) - if err != nil { - continue - } - if matchLabels(labels, m) { - return pdb - } - } - return nil -} diff --git a/internal/cache/pod.go b/internal/cache/pod.go index 47987d2b..c77a78e4 100644 --- a/internal/cache/pod.go +++ b/internal/cache/pod.go @@ -4,62 +4,37 @@ package cache import ( + "errors" "sync" "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/client" + "github.com/derailed/popeye/internal/db" v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/labels" ) // Pod represents a Pod cache. type Pod struct { - pods map[string]*v1.Pod + db *db.DB } // NewPod returns a Pod cache. -func NewPod(pods map[string]*v1.Pod) *Pod { - return &Pod{pods: pods} -} - -// ListPods return available pods. -func (p *Pod) ListPods() map[string]*v1.Pod { - return p.pods -} - -// ListPodsBySelector list all pods matching the given selector. -func (p *Pod) ListPodsBySelector(ns string, sel *metav1.LabelSelector) map[string]*v1.Pod { - res := map[string]*v1.Pod{} - if sel == nil || sel.Size() == 0 { - return res - } - for fqn, po := range p.pods { - if po.Namespace != ns { - continue - } - if s, err := metav1.LabelSelectorAsSelector(sel); err == nil && s.Matches(labels.Set(po.Labels)) { - res[fqn] = po - } - } - - return res -} - -// GetPod returns a pod via a label query. -func (p *Pod) GetPod(ns string, sel map[string]string) *v1.Pod { - res := p.ListPodsBySelector(ns, &metav1.LabelSelector{MatchLabels: sel}) - for _, v := range res { - return v - } - - return nil +func NewPod(dba *db.DB) *Pod { + return &Pod{db: dba} } // PodRefs computes all pods external references. -func (p *Pod) PodRefs(refs *sync.Map) { - for fqn, po := range p.pods { +func (p *Pod) PodRefs(refs *sync.Map) error { + txn, it := p.db.MustITFor(internal.Glossary[internal.PO]) + defer txn.Abort() + for o := it.Next(); o != nil; o = it.Next() { + po, ok := o.(*v1.Pod) + if !ok { + return errors.New("expected a pod") + } + fqn := client.FQN(po.Namespace, po.Name) p.imagePullSecRefs(po.Namespace, po.Spec.ImagePullSecrets, refs) - p.namespaceRefs(po.Namespace, refs) + namespaceRefs(po.Namespace, refs) for _, co := range po.Spec.InitContainers { p.containerRefs(fqn, co, refs) } @@ -68,19 +43,8 @@ func (p *Pod) PodRefs(refs *sync.Map) { } p.volumeRefs(po.Namespace, po.Spec.Volumes, refs) } -} -func (p *Pod) imagePullSecRefs(ns string, sRefs []v1.LocalObjectReference, refs *sync.Map) { - for _, s := range sRefs { - key := ResFqn(SecretKey, FQN(ns, s.Name)) - refs.Store(key, internal.AllKeys) - } -} - -func (p *Pod) namespaceRefs(ns string, refs *sync.Map) { - if set, ok := refs.LoadOrStore("ns", internal.StringSet{ns: internal.Blank}); ok { - set.(internal.StringSet).Add(ns) - } + return nil } func (p *Pod) containerRefs(pfqn string, co v1.Container, refs *sync.Map) { @@ -104,6 +68,34 @@ func (p *Pod) containerRefs(pfqn string, co v1.Container, refs *sync.Map) { } } +func (*Pod) volumeRefs(ns string, vv []v1.Volume, refs *sync.Map) { + for _, v := range vv { + sv := v.VolumeSource.Secret + if sv != nil { + addKeys(SecretKey, FQN(ns, sv.SecretName), sv.Items, refs) + continue + } + + cmv := v.VolumeSource.ConfigMap + if cmv != nil { + addKeys(ConfigMapKey, FQN(ns, cmv.LocalObjectReference.Name), cmv.Items, refs) + } + } +} + +func (p *Pod) imagePullSecRefs(ns string, sRefs []v1.LocalObjectReference, refs *sync.Map) { + for _, s := range sRefs { + key := ResFqn(SecretKey, FQN(ns, s.Name)) + refs.Store(key, internal.AllKeys) + } +} + +func namespaceRefs(ns string, refs *sync.Map) { + if set, ok := refs.LoadOrStore("ns", internal.StringSet{ns: internal.Blank}); ok { + set.(internal.StringSet).Add(ns) + } +} + func (p *Pod) secretRefs(ns string, ref *v1.SecretKeySelector, refs *sync.Map) { if ref == nil { return @@ -132,21 +124,6 @@ func (p *Pod) configMapRefs(ns string, ref *v1.ConfigMapKeySelector, refs *sync. } } -func (*Pod) volumeRefs(ns string, vv []v1.Volume, refs *sync.Map) { - for _, v := range vv { - sv := v.VolumeSource.Secret - if sv != nil { - addKeys(SecretKey, FQN(ns, sv.SecretName), sv.Items, refs) - continue - } - - cmv := v.VolumeSource.ConfigMap - if cmv != nil { - addKeys(ConfigMapKey, FQN(ns, cmv.LocalObjectReference.Name), cmv.Items, refs) - } - } -} - // ---------------------------------------------------------------------------- // Helpers... diff --git a/internal/cache/pod_mx.go b/internal/cache/pod_mx.go deleted file mode 100644 index 3a26036a..00000000 --- a/internal/cache/pod_mx.go +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package cache - -import ( - mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" -) - -// PodsMetrics represents a Pod metrics cache. -type PodsMetrics struct { - mx map[string]*mv1beta1.PodMetrics -} - -// NewPodsMetrics returns new Pod metrics cache. -func NewPodsMetrics(mx map[string]*mv1beta1.PodMetrics) *PodsMetrics { - return &PodsMetrics{mx: mx} -} - -// ListPodsMetrics returns all available PodMetrics on the cluster. -func (p *PodsMetrics) ListPodsMetrics() map[string]*mv1beta1.PodMetrics { - return p.mx -} diff --git a/internal/cache/pod_test.go b/internal/cache/pod_test.go index 1221d291..f1dde35a 100644 --- a/internal/cache/pod_test.go +++ b/internal/cache/pod_test.go @@ -1,385 +1,75 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright Authors of Popeye -package cache +package cache_test import ( - "sort" "sync" "testing" "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/cache" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/test" "github.com/stretchr/testify/assert" v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -func TestGetPod(t *testing.T) { - pods := map[string]*v1.Pod{ - "default/p1": makePodLabels("p1", map[string]string{"a": "a", "b": "b", "c": "c"}), - "default/p2": makePodLabels("p2", map[string]string{"a": "a", "b": "b"}), - "default/p3": makePodLabels("p3", map[string]string{"a": "c"}), - } +func TestPodRef(t *testing.T) { + dba, err := test.NewTestDB() + assert.NoError(t, err) + l := db.NewLoader(dba) + + ctx := test.MakeCtx(t) + assert.NoError(t, test.LoadDB[*v1.Pod](ctx, l.DB, "core/pod/1.yaml", internal.Glossary[internal.PO])) + + cr := cache.NewPod(dba) + var refs sync.Map + assert.NoError(t, cr.PodRefs(&refs)) uu := map[string]struct { - sel map[string]string - e string + k string + vv []string }{ - "noSelector": { - sel: map[string]string{}, + "ns": { + k: "ns", }, - "p1": { - sel: map[string]string{"a": "a", "b": "b", "c": "c"}, - e: "default/p1", + "cm1-env": { + k: "cm:default/cm1", + vv: []string{"blee", "ns"}, }, - "p3": { - sel: map[string]string{"a": "c"}, - e: "default/p3", + "cm3-vol": { + k: "cm:default/cm3", + vv: []string{"k1", "k2", "k3", "k4"}, }, - "none": { - sel: map[string]string{"a": "x"}, + "cm4-env-from": { + k: "cm:default/cm4", }, - } - - p := NewPod(pods) - for k := range uu { - u := uu[k] - t.Run(k, func(t *testing.T) { - po := p.GetPod("default", u.sel) - if po == nil { - assert.Equal(t, u.e, "") - } else { - assert.Equal(t, u.e, MetaFQN(po.ObjectMeta)) - } - }) - } -} - -func TestListPodsBySelector(t *testing.T) { - pods := map[string]*v1.Pod{ - "default/p1": makePodLabels("p1", map[string]string{"a": "a", "b": "b"}), - "default/p2": makePodLabels("p2", map[string]string{"a": "a", "b": "b"}), - "default/p3": makePodLabels("p3", map[string]string{"a": "c"}), - } - - uu := map[string]struct { - sel *metav1.LabelSelector - e []string - }{ - "noSelector": { - nil, - []string{}, + "sec1-vol": { + k: "sec:default/sec1", + vv: []string{"k1"}, }, - "p1p2": { - &metav1.LabelSelector{MatchLabels: map[string]string{"a": "a"}}, - []string{"default/p1", "default/p2"}, + "sec2-env": { + k: "sec:default/sec2", + vv: []string{"ca.crt", "fred", "k1", "namespace"}, }, - "p3": { - &metav1.LabelSelector{MatchLabels: map[string]string{"a": "c"}}, - []string{"default/p3"}, + "sec3-img-pull": { + k: "sec:default/sec3", }, - "none": { - &metav1.LabelSelector{MatchLabels: map[string]string{"a": "x"}}, - []string{}, + "sec4-env-from": { + k: "sec:default/sec4", }, } - p := NewPod(pods) for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { - res := p.ListPodsBySelector("default", u.sel) - keys := []string{} - for k := range res { - keys = append(keys, k) + m, ok := refs.Load(u.k) + assert.True(t, ok) + for _, k := range u.vv { + _, ok = m.(internal.StringSet)[k] + assert.True(t, ok) } - sort.Strings(keys) - assert.Equal(t, u.e, keys) }) } } - -func TestPodRefsVolume(t *testing.T) { - pods := map[string]*v1.Pod{ - "default/p1": makePodVolume("p1", "cm1", "s1", false), - "default/p2": makePodVolume("p2", "cm2", "s2", true), - "default/p3": makePodVolume("p3", "cm2", "s2", false), - } - - p := NewPod(pods) - - var refs sync.Map - p.PodRefs(&refs) - - ii, ok := refs.Load("cm:default/cm1") - assert.True(t, ok) - assert.Equal(t, 2, len(ii.(internal.StringSet))) - - ii, ok = refs.Load("cm:default/cm2") - assert.True(t, ok) - assert.Equal(t, 2, len(ii.(internal.StringSet))) - - ii, ok = refs.Load("sec:default/s1") - assert.True(t, ok) - assert.Equal(t, 1, len(ii.(internal.StringSet))) - - ii, ok = refs.Load("sec:default/s2") - assert.True(t, ok) - assert.Equal(t, 1, len(ii.(internal.StringSet))) - - ii, ok = refs.Load("ns") - assert.True(t, ok) - assert.Equal(t, 1, len(ii.(internal.StringSet))) -} - -func TestPodRefsEnvFrom(t *testing.T) { - pods := map[string]*v1.Pod{ - "default/p1": makePodEnvFrom("p1", "r1", false), - "default/p2": makePodEnvFrom("p2", "r2", true), - "default/p3": makePodEnvFrom("p3", "r1", false), - } - - p := NewPod(pods) - - var refs sync.Map - p.PodRefs(&refs) - - ii, ok := refs.Load("cm:default/r1") - assert.True(t, ok) - assert.Equal(t, 1, len(ii.(internal.StringSet))) - - ii, ok = refs.Load("cm:default/r2") - assert.True(t, ok) - assert.Equal(t, 1, len(ii.(internal.StringSet))) - - ii, ok = refs.Load("sec:default/r1") - assert.True(t, ok) - assert.Equal(t, 1, len(ii.(internal.StringSet))) - - ii, ok = refs.Load("sec:default/r2") - assert.True(t, ok) - assert.Equal(t, 1, len(ii.(internal.StringSet))) -} - -func TestPodRefsEnv(t *testing.T) { - pods := map[string]*v1.Pod{ - "default/p1": makePodEnv("p1", "r1", false), - "default/p2": makePodEnv("p2", "r2", true), - } - p := NewPod(pods) - var refs sync.Map - p.PodRefs(&refs) - - ii, ok := refs.Load("cm:default/r1") - assert.True(t, ok) - assert.Equal(t, 2, len(ii.(internal.StringSet))) - - ii, ok = refs.Load("cm:default/r2") - assert.True(t, ok) - assert.Equal(t, 2, len(ii.(internal.StringSet))) - - ii, ok = refs.Load("sec:default/r1") - assert.True(t, ok) - assert.Equal(t, 1, len(ii.(internal.StringSet))) - - ii, ok = refs.Load("sec:default/r2") - assert.True(t, ok) - assert.Equal(t, 1, len(ii.(internal.StringSet))) -} - -func TestPodPullImageSecrets(t *testing.T) { - pods := map[string]*v1.Pod{ - "default/p1": makePodPull("p1", "r1", false), - "default/p2": makePodPull("p2", "r2", true), - } - - p := NewPod(pods) - var refs sync.Map - p.PodRefs(&refs) - - ii, ok := refs.Load("cm:default/r1") - assert.True(t, ok) - assert.Equal(t, 2, len(ii.(internal.StringSet))) - - ii, ok = refs.Load("cm:default/r2") - assert.True(t, ok) - assert.Equal(t, 2, len(ii.(internal.StringSet))) - - ii, ok = refs.Load("sec:default/s1") - assert.True(t, ok) - assert.Equal(t, 1, len(ii.(internal.StringSet))) - - ii, ok = refs.Load("sec:default/s2") - assert.True(t, ok) - assert.Equal(t, 1, len(ii.(internal.StringSet))) -} - -func TestNamespaced(t *testing.T) { - uu := []struct { - s, ens, en string - }{ - {"fred/blee", "fred", "blee"}, - {"blee", "", "blee"}, - } - - for _, u := range uu { - ns, n := namespaced(u.s) - assert.Equal(t, u.ens, ns) - assert.Equal(t, u.en, n) - } -} - -// ---------------------------------------------------------------------------- -// Helpers... - -func makePodVolume(n, cm, sec string, optional bool) *v1.Pod { - po := makePod(n) - po.Spec.Volumes = []v1.Volume{ - { - Name: "v1", - VolumeSource: v1.VolumeSource{ - ConfigMap: &v1.ConfigMapVolumeSource{ - LocalObjectReference: v1.LocalObjectReference{ - Name: cm, - }, - Items: []v1.KeyToPath{ - {Key: "k1"}, - {Key: "k2"}, - }, - Optional: &optional, - }, - }, - }, - { - Name: "v2", - VolumeSource: v1.VolumeSource{ - Secret: &v1.SecretVolumeSource{ - SecretName: sec, - Optional: &optional, - }, - }, - }, - } - - return po -} - -func makePodPull(n, ref string, optional bool) *v1.Pod { - po := makePodEnv(n, ref, optional) - - po.Spec.ImagePullSecrets = []v1.LocalObjectReference{ - {Name: "s1"}, - {Name: "s2"}, - } - - return po -} - -func makePodEnv(n, ref string, optional bool) *v1.Pod { - po := makePod(n) - po.Spec.Containers = []v1.Container{ - { - Name: "c1", - Env: []v1.EnvVar{ - { - Name: "e1", - ValueFrom: &v1.EnvVarSource{ - ConfigMapKeyRef: &v1.ConfigMapKeySelector{ - LocalObjectReference: v1.LocalObjectReference{ - Name: ref, - }, - Key: "k1", - Optional: &optional, - }, - }, - }, - }, - }, - { - Name: "c2", - Env: []v1.EnvVar{ - { - Name: "e2", - ValueFrom: &v1.EnvVarSource{ - ConfigMapKeyRef: &v1.ConfigMapKeySelector{ - LocalObjectReference: v1.LocalObjectReference{ - Name: ref, - }, - Key: "k2", - Optional: &optional, - }, - }, - }, - }, - }, - } - po.Spec.InitContainers = []v1.Container{ - { - Name: "ic1", - Env: []v1.EnvVar{ - { - Name: "e1", - ValueFrom: &v1.EnvVarSource{ - SecretKeyRef: &v1.SecretKeySelector{ - LocalObjectReference: v1.LocalObjectReference{Name: ref}, - Key: "k2", - Optional: &optional, - }, - }, - }, - }, - }, - } - - return po -} - -func makePodEnvFrom(n, cm string, optional bool) *v1.Pod { - po := makePod(n) - po.Spec.Containers = []v1.Container{ - { - Name: "c1", - EnvFrom: []v1.EnvFromSource{ - { - ConfigMapRef: &v1.ConfigMapEnvSource{ - LocalObjectReference: v1.LocalObjectReference{Name: cm}, - Optional: &optional, - }, - }, - }, - }, - } - po.Spec.InitContainers = []v1.Container{ - { - Name: "ic1", - EnvFrom: []v1.EnvFromSource{ - { - SecretRef: &v1.SecretEnvSource{ - LocalObjectReference: v1.LocalObjectReference{Name: cm}, - Optional: &optional, - }, - }, - }, - }, - } - - return po -} - -func makePod(n string) *v1.Pod { - po := v1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Name: n, - Namespace: "default", - }, - } - - return &po -} - -func makePodLabels(n string, labels map[string]string) *v1.Pod { - po := makePod(n) - po.ObjectMeta.Labels = labels - - return po -} diff --git a/internal/cache/pv.go b/internal/cache/pv.go deleted file mode 100644 index 24303d33..00000000 --- a/internal/cache/pv.go +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package cache - -import ( - v1 "k8s.io/api/core/v1" -) - -// PersistentVolume represents a collection of PersistentVolumes available on a cluster. -type PersistentVolume struct { - pvs map[string]*v1.PersistentVolume -} - -// NewPersistentVolume returns a new PersistentVolume. -func NewPersistentVolume(pvs map[string]*v1.PersistentVolume) *PersistentVolume { - return &PersistentVolume{pvs} -} - -// ListPersistentVolumes returns all available PersistentVolumes on the cluster. -func (p *PersistentVolume) ListPersistentVolumes() map[string]*v1.PersistentVolume { - return p.pvs -} diff --git a/internal/cache/pvc.go b/internal/cache/pvc.go deleted file mode 100644 index d64dc7f8..00000000 --- a/internal/cache/pvc.go +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package cache - -import ( - v1 "k8s.io/api/core/v1" -) - -// PersistentVolumeClaim represents a collection of PersistentVolumeClaims available on a cluster. -type PersistentVolumeClaim struct { - pvcs map[string]*v1.PersistentVolumeClaim -} - -// NewPersistentVolumeClaim returns a new PersistentVolumeClaim. -func NewPersistentVolumeClaim(pvcs map[string]*v1.PersistentVolumeClaim) *PersistentVolumeClaim { - return &PersistentVolumeClaim{pvcs} -} - -// ListPersistentVolumeClaims returns all available PersistentVolumeClaims on the cluster. -func (p *PersistentVolumeClaim) ListPersistentVolumeClaims() map[string]*v1.PersistentVolumeClaim { - return p.pvcs -} diff --git a/internal/cache/rb.go b/internal/cache/rb.go index f5f89194..b9b4b426 100644 --- a/internal/cache/rb.go +++ b/internal/cache/rb.go @@ -8,6 +8,8 @@ import ( "sync" "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/client" + "github.com/derailed/popeye/internal/db" rbacv1 "k8s.io/api/rbac/v1" ) @@ -16,23 +18,26 @@ const RoleKey = "role" // RoleBinding represents RoleBinding cache. type RoleBinding struct { - rbs map[string]*rbacv1.RoleBinding + db *db.DB } // NewRoleBinding returns a new RoleBinding cache. -func NewRoleBinding(rbs map[string]*rbacv1.RoleBinding) *RoleBinding { - return &RoleBinding{rbs: rbs} -} - -// ListRoleBindings returns all available RoleBindings on the cluster. -func (r *RoleBinding) ListRoleBindings() map[string]*rbacv1.RoleBinding { - return r.rbs +func NewRoleBinding(db *db.DB) *RoleBinding { + return &RoleBinding{db: db} } // RoleRefs computes all role external references. func (r *RoleBinding) RoleRefs(refs *sync.Map) { - for fqn, rb := range r.rbs { - key := ResFqn(strings.ToLower(rb.RoleRef.Kind), FQN(rb.Namespace, rb.RoleRef.Name)) + txn, it := r.db.MustITFor(internal.Glossary[internal.ROB]) + defer txn.Abort() + for o := it.Next(); o != nil; o = it.Next() { + rb := o.(*rbacv1.RoleBinding) + fqn := client.FQN(rb.Namespace, rb.Name) + cfqn := FQN(rb.Namespace, rb.RoleRef.Name) + if rb.RoleRef.Kind == "ClusterRole" { + cfqn = client.FQN("", rb.RoleRef.Name) + } + key := ResFqn(strings.ToLower(rb.RoleRef.Kind), cfqn) if c, ok := refs.LoadOrStore(key, internal.StringSet{fqn: internal.Blank}); ok { c.(internal.StringSet).Add(fqn) } diff --git a/internal/cache/rb_test.go b/internal/cache/rb_test.go index fbd9b433..2596f79e 100644 --- a/internal/cache/rb_test.go +++ b/internal/cache/rb_test.go @@ -9,41 +9,31 @@ import ( "github.com/derailed/popeye/internal" "github.com/derailed/popeye/internal/cache" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/test" "github.com/stretchr/testify/assert" rbacv1 "k8s.io/api/rbac/v1" ) func TestRoleRef(t *testing.T) { - cr := cache.NewRoleBinding(makeRBMap()) + dba, err := test.NewTestDB() + assert.NoError(t, err) + l := db.NewLoader(dba) + + ctx := test.MakeCtx(t) + assert.NoError(t, test.LoadDB[*rbacv1.RoleBinding](ctx, l.DB, "auth/rob/1.yaml", internal.Glossary[internal.ROB])) + + cr := cache.NewRoleBinding(dba) var refs sync.Map cr.RoleRefs(&refs) - m, ok := refs.Load("clusterrole:cr1") + m, ok := refs.Load("clusterrole:cr-bozo") assert.True(t, ok) - _, ok = m.(internal.StringSet)["rb1"] + _, ok = m.(internal.StringSet)["default/rb3"] assert.True(t, ok) - m, ok = refs.Load("role:blee/r1") + m, ok = refs.Load("role:default/r1") assert.True(t, ok) - _, ok = m.(internal.StringSet)["rb2"] + _, ok = m.(internal.StringSet)["default/rb1"] assert.True(t, ok) } - -// Helpers... - -func makeRBMap() map[string]*rbacv1.RoleBinding { - return map[string]*rbacv1.RoleBinding{ - "rb1": makeRB("", "r1", "ClusterRole", "cr1"), - "rb2": makeRB("blee", "r2", "Role", "r1"), - } -} - -func makeRB(ns, name, kind, refName string) *rbacv1.RoleBinding { - return &rbacv1.RoleBinding{ - ObjectMeta: makeObjMeta(ns, name), - RoleRef: rbacv1.RoleRef{ - Kind: kind, - Name: refName, - }, - } -} diff --git a/internal/cache/role.go b/internal/cache/role.go deleted file mode 100644 index f10f5070..00000000 --- a/internal/cache/role.go +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package cache - -import ( - rbacv1 "k8s.io/api/rbac/v1" -) - -// Role represents Role cache. -type Role struct { - ros map[string]*rbacv1.Role -} - -// NewRole returns a new Role cache. -func NewRole(ros map[string]*rbacv1.Role) *Role { - return &Role{ros: ros} -} - -// ListRoles returns all available Roles on the cluster. -func (r *Role) ListRoles() map[string]*rbacv1.Role { - return r.ros -} diff --git a/internal/cache/rs.go b/internal/cache/rs.go deleted file mode 100644 index 071ef4a5..00000000 --- a/internal/cache/rs.go +++ /dev/null @@ -1,26 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package cache - -import ( - appsv1 "k8s.io/api/apps/v1" -) - -// ReplicaSetKey tracks ReplicaSet resource references -const ReplicaSetKey = "ds" - -// ReplicaSet represents ReplicaSet cache. -type ReplicaSet struct { - rss map[string]*appsv1.ReplicaSet -} - -// NewReplicaSet returns a new ReplicaSet cache. -func NewReplicaSet(rss map[string]*appsv1.ReplicaSet) *ReplicaSet { - return &ReplicaSet{rss: rss} -} - -// ListReplicaSets returns all available ReplicaSets on the cluster. -func (d *ReplicaSet) ListReplicaSets() map[string]*appsv1.ReplicaSet { - return d.rss -} diff --git a/internal/cache/sa.go b/internal/cache/sa.go index bd5c4ccf..258e4e73 100644 --- a/internal/cache/sa.go +++ b/internal/cache/sa.go @@ -4,38 +4,44 @@ package cache import ( + "errors" "sync" "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" v1 "k8s.io/api/core/v1" ) // ServiceAccount tracks serviceaccounts. type ServiceAccount struct { - sas map[string]*v1.ServiceAccount + db *db.DB } // NewServiceAccount returns a new serviceaccount loader. -func NewServiceAccount(sas map[string]*v1.ServiceAccount) *ServiceAccount { - return &ServiceAccount{sas: sas} -} - -// ListServiceAccounts list available ServiceAccounts. -func (s *ServiceAccount) ListServiceAccounts() map[string]*v1.ServiceAccount { - return s.sas +func NewServiceAccount(db *db.DB) *ServiceAccount { + return &ServiceAccount{db: db} } // ServiceAccountRefs computes all serviceaccount external references. -func (s *ServiceAccount) ServiceAccountRefs(refs *sync.Map) { - for _, sa := range s.sas { +func (s *ServiceAccount) ServiceAccountRefs(refs *sync.Map) error { + txn, it := s.db.MustITFor(internal.Glossary[internal.SA]) + defer txn.Abort() + for o := it.Next(); o != nil; o = it.Next() { + sa, ok := o.(*v1.ServiceAccount) + if !ok { + return errors.New("expected sa") + } + namespaceRefs(sa.Namespace, refs) for _, s := range sa.Secrets { key := ResFqn(SecretKey, FQN(s.Namespace, s.Name)) refs.Store(key, internal.AllKeys) } - for _, s := range sa.ImagePullSecrets { key := ResFqn(SecretKey, FQN(sa.Namespace, s.Name)) refs.Store(key, internal.AllKeys) } + } + + return nil } diff --git a/internal/cache/sa_test.go b/internal/cache/sa_test.go index 67ae8ead..b0c613d5 100644 --- a/internal/cache/sa_test.go +++ b/internal/cache/sa_test.go @@ -8,24 +8,35 @@ import ( "testing" "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/test" "github.com/stretchr/testify/assert" v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) func TestServiceAccountRefs(t *testing.T) { + dba, err := test.NewTestDB() + assert.NoError(t, err) + l := db.NewLoader(dba) + + ctx := test.MakeCtx(t) + assert.NoError(t, test.LoadDB[*v1.ServiceAccount](ctx, l.DB, "core/sa/1.yaml", internal.Glossary[internal.SA])) + uu := []struct { keys []string }{ - {[]string{"sec:default/s1", "sec:default/is1"}}, + { + []string{ + "sec:default/s1", + "sec:default/bozo", + }, + }, } - sa := NewServiceAccount(map[string]*v1.ServiceAccount{ - "default/sa1": makeSASecrets("sa1"), - }) + var refs sync.Map + sa := NewServiceAccount(dba) + assert.NoError(t, sa.ServiceAccountRefs(&refs)) for _, u := range uu { - var refs sync.Map - sa.ServiceAccountRefs(&refs) for _, k := range u.keys { v, ok := refs.Load(k) assert.True(t, ok) @@ -33,33 +44,3 @@ func TestServiceAccountRefs(t *testing.T) { } } } - -// ---------------------------------------------------------------------------- -// Helpers... - -func makeSASecrets(n string) *v1.ServiceAccount { - sa := makeSA(n) - sa.Secrets = []v1.ObjectReference{ - { - Kind: "Secret", - Name: "s1", - Namespace: "default", - }, - } - sa.ImagePullSecrets = []v1.LocalObjectReference{ - { - Name: "is1", - }, - } - - return sa -} - -func makeSA(n string) *v1.ServiceAccount { - return &v1.ServiceAccount{ - ObjectMeta: metav1.ObjectMeta{ - Name: n, - Namespace: "default", - }, - } -} diff --git a/internal/cache/sec.go b/internal/cache/sec.go deleted file mode 100644 index 90b89dcb..00000000 --- a/internal/cache/sec.go +++ /dev/null @@ -1,26 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package cache - -import ( - v1 "k8s.io/api/core/v1" -) - -// SecretKey tracks Secret resource references -const SecretKey = "sec" - -// Secret represents a collection of Secrets available on a cluster. -type Secret struct { - secrets map[string]*v1.Secret -} - -// NewSecret returns a new Secret cache. -func NewSecret(ss map[string]*v1.Secret) *Secret { - return &Secret{ss} -} - -// ListSecrets returns all available Secrets on the cluster. -func (s *Secret) ListSecrets() map[string]*v1.Secret { - return s.secrets -} diff --git a/internal/cache/sts.go b/internal/cache/sts.go deleted file mode 100644 index 353d48cc..00000000 --- a/internal/cache/sts.go +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package cache - -import ( - appsv1 "k8s.io/api/apps/v1" -) - -// StatefulSet represents a collection of StatefulSets available on a cluster. -type StatefulSet struct { - sts map[string]*appsv1.StatefulSet -} - -// NewStatefulSet returns a new StatefulSet. -func NewStatefulSet(sts map[string]*appsv1.StatefulSet) *StatefulSet { - return &StatefulSet{sts} -} - -// ListStatefulSets returns all available StatefulSets on the cluster. -func (s *StatefulSet) ListStatefulSets() map[string]*appsv1.StatefulSet { - return s.sts -} diff --git a/internal/cache/svc.go b/internal/cache/svc.go deleted file mode 100644 index 20c762bd..00000000 --- a/internal/cache/svc.go +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package cache - -import ( - v1 "k8s.io/api/core/v1" -) - -// Service represents a collection of Services available on a cluster. -type Service struct { - svcs map[string]*v1.Service -} - -// NewService returns a new Service. -func NewService(svcs map[string]*v1.Service) *Service { - return &Service{svcs} -} - -// ListServices returns all available Services on the cluster. -func (s *Service) ListServices() map[string]*v1.Service { - return s.svcs -} diff --git a/internal/cache/testdata/auth/crb/1.yaml b/internal/cache/testdata/auth/crb/1.yaml new file mode 100644 index 00000000..0193807b --- /dev/null +++ b/internal/cache/testdata/auth/crb/1.yaml @@ -0,0 +1,43 @@ +--- +apiVersion: v1 +kind: List +items: + - apiVersion: rbac.authorization.k8s.io/v1 + kind: ClusterRoleBinding + metadata: + name: crb1 + subjects: + - kind: User + name: fred + apiGroup: rbac.authorization.k8s.io + roleRef: + kind: ClusterRole + name: cr1 + apiGroup: rbac.authorization.k8s.io + - apiVersion: rbac.authorization.k8s.io/v1 + kind: ClusterRoleBinding + metadata: + name: crb2 + subjects: + - kind: ServiceAccount + name: sa2 + namespace: default + apiGroup: rbac.authorization.k8s.io + roleRef: + kind: ClusterRole + name: cr-bozo + apiGroup: rbac.authorization.k8s.io + - apiVersion: rbac.authorization.k8s.io/v1 + kind: ClusterRoleBinding + metadata: + name: crb3 + subjects: + - kind: ServiceAccount + name: sa-bozo + namespace: default + apiGroup: rbac.authorization.k8s.io + roleRef: + kind: Role + name: r1 + namespace: blee + apiGroup: rbac.authorization.k8s.io \ No newline at end of file diff --git a/internal/cache/testdata/auth/rob/1.yaml b/internal/cache/testdata/auth/rob/1.yaml new file mode 100644 index 00000000..f338fe2c --- /dev/null +++ b/internal/cache/testdata/auth/rob/1.yaml @@ -0,0 +1,43 @@ +--- +apiVersion: v1 +kind: List +items: + - apiVersion: rbac.authorization.k8s.io/v1 + kind: RoleBinding + metadata: + name: rb1 + namespace: default + subjects: + - kind: User + name: fred + apiGroup: rbac.authorization.k8s.io + roleRef: + kind: Role + name: r1 + apiGroup: rbac.authorization.k8s.io + - apiVersion: rbac.authorization.k8s.io/v1 + kind: RoleBinding + metadata: + name: rb2 + namespace: default + subjects: + - kind: ServiceAccount + name: sa-bozo + apiGroup: rbac.authorization.k8s.io + roleRef: + kind: Role + name: r-bozo + apiGroup: rbac.authorization.k8s.io + - apiVersion: rbac.authorization.k8s.io/v1 + kind: RoleBinding + metadata: + name: rb3 + namespace: default + subjects: + - kind: ServiceAccount + name: sa-bozo + apiGroup: rbac.authorization.k8s.io + roleRef: + kind: ClusterRole + name: cr-bozo + apiGroup: rbac.authorization.k8s.io \ No newline at end of file diff --git a/internal/cache/testdata/core/node/1.yaml b/internal/cache/testdata/core/node/1.yaml new file mode 100644 index 00000000..cd0384cc --- /dev/null +++ b/internal/cache/testdata/core/node/1.yaml @@ -0,0 +1,71 @@ +apiVersion: v1 +kind: List +items: +- apiVersion: v1 + kind: Node + metadata: + labels: + node-role.kubernetes.io/control-plane: "" + node-role.kubernetes.io/master: "" + node.kubernetes.io/exclude-from-external-load-balancers: "" + name: n1 + spec: + podCIDR: 10.244.0.0/24 + podCIDRs: + - 10.244.0.0/24 + status: + addresses: + - address: 192.168.228.3 + type: InternalIP + - address: dashb-control-plane + type: Hostname + allocatable: + cpu: 4 + ephemeral-storage: 816748224Ki + memory: 400Mi + pods: "110" + capacity: + cpu: "10" + ephemeral-storage: 816748224Ki + memory: 8124744Ki + pods: "110" + conditions: + - lastHeartbeatTime: "2024-01-27T15:31:39Z" + lastTransitionTime: "2024-01-03T20:35:11Z" + message: kubelet has sufficient memory available + reason: KubeletHasSufficientMemory + status: "False" + type: MemoryPressure + - lastHeartbeatTime: "2024-01-27T15:31:39Z" + lastTransitionTime: "2024-01-03T20:35:11Z" + message: kubelet has no disk pressure + reason: KubeletHasNoDiskPressure + status: "False" + type: DiskPressure + - lastHeartbeatTime: "2024-01-27T15:31:39Z" + lastTransitionTime: "2024-01-03T20:35:11Z" + message: kubelet has sufficient PID available + reason: KubeletHasSufficientPID + status: "False" + type: PIDPressure + - lastHeartbeatTime: "2024-01-27T15:31:39Z" + lastTransitionTime: "2024-01-03T20:35:38Z" + message: kubelet is posting ready status + reason: KubeletReady + status: "True" + type: Ready + daemonEndpoints: + kubeletEndpoint: + Port: 10250 + images: + nodeInfo: + architecture: arm64 + bootID: 0836e65d-3091-4cb5-8ad4-8f65425f87e3 + containerRuntimeVersion: containerd://1.5.1 + kernelVersion: 6.5.10-orbstack-00110-gbcfe04c86d2f + kubeProxyVersion: v1.21.1 + kubeletVersion: v1.21.1 + machineID: 6bbc44bb821d48b995092021d706d8e6 + operatingSystem: linux + osImage: Ubuntu 20.10 + systemUUID: 6bbc44bb821d48b995092021d706d8e6 diff --git a/internal/cache/testdata/core/pod/1.yaml b/internal/cache/testdata/core/pod/1.yaml new file mode 100644 index 00000000..0e7f8910 --- /dev/null +++ b/internal/cache/testdata/core/pod/1.yaml @@ -0,0 +1,138 @@ +--- +apiVersion: v1 +kind: List +items: +- apiVersion: v1 + kind: Pod + metadata: + name: p1 + namespace: default + labels: + app: p1 + spec: + serviceAccountName: default + tolerations: + - key: t1 + operator: Exists + effect: NoSchedule + containers: + - name: c1 + image: alpine + resources: + requests: + cpu: 1 + memory: 1Mi + limits: + cpu: 1 + memory: 1Mi + ports: + - containerPort: 9090 + name: http + protocol: TCP + env: + - name: env1 + valueFrom: + configMapKeyRef: + name: cm1 + key: ns + - name: env2 + valueFrom: + secretKeyRef: + name: sec1 + key: k1 + volumeMounts: + - name: config + mountPath: "/config" + readOnly: true + volumes: + - name: mypd + persistentVolumeClaim: + claimName: pvc1 + - name: config + configMap: + name: cm3 + items: + - key: k1 + path: "game.properties" + - key: k2 + path: "user-interface.properties" + - name: secret + secret: + secretName: sec2 + optional: false + items: + - key: fred + path: blee +- apiVersion: v1 + kind: Pod + metadata: + name: p2 + namespace: default + labels: + app: p2 + spec: + serviceAccountName: default + imagePullSecrets: + - name: sec3 + tolerations: + - key: t1 + operator: Exists + effect: NoSchedule + initContainers: + - name: ic1 + image: fred + containers: + - name: c1 + image: alpine + resources: + requests: + cpu: 1 + memory: 1Mi + limits: + cpu: 1 + memory: 1Mi + ports: + - containerPort: 9090 + name: http + protocol: TCP + envFrom: + - configMapRef: + name: cm4 + - secretRef: + name: sec4 + env: + - name: env1 + valueFrom: + configMapKeyRef: + name: cm1 + key: blee + - name: env2 + valueFrom: + secretKeyRef: + name: sec2 + key: k1 + volumeMounts: + - name: config + mountPath: "/config" + readOnly: true + volumes: + - name: mypd + persistentVolumeClaim: + claimName: pvc1 + - name: config + configMap: + name: cm3 + items: + - key: k3 + path: blee + - key: k4 + path: zorg + - name: secret + secret: + secretName: sec2 + optional: false + items: + - key: ca.crt + path: "game.properties" + - key: namespace + path: "user-interface.properties" diff --git a/internal/cache/testdata/core/pod/2.yaml b/internal/cache/testdata/core/pod/2.yaml new file mode 100644 index 00000000..933ac556 --- /dev/null +++ b/internal/cache/testdata/core/pod/2.yaml @@ -0,0 +1,178 @@ +--- +apiVersion: v1 +kind: List +items: + - apiVersion: v1 + kind: Pod + metadata: + name: p1 + namespace: default + labels: + app: p1 + ownerReferences: + - apiVersion: apps/v1 + controller: true + kind: ReplicaSet + name: rs1 + spec: + serviceAccountName: sa1 + automountServiceAccountToken: false + status: + conditions: + phase: Running + - apiVersion: v1 + kind: Pod + metadata: + name: p2 + namespace: default + labels: + app: test2 + spec: + serviceAccountName: sa2 + - apiVersion: v1 + kind: Pod + metadata: + name: p3 + namespace: default + labels: + app: p3 + ownerReferences: + - apiVersion: apps/v1 + controller: true + kind: DaemonSet + name: rs3 + spec: + serviceAccountName: sa3 + containers: + - image: dorker.io/blee:1.0.1 + name: c1 + resources: + limits: + cpu: 1 + mem: 1Mi + livenessProbe: + httpGet: + path: /healthz + port: 8080 + initialDelaySeconds: 3 + periodSeconds: 3 + readinessProbe: + httpGet: + path: /healthz + port: 8080 + initialDelaySeconds: 3 + periodSeconds: 3 + status: + conditions: + - status: "False" + type: Initialized + - status: "False" + type: Ready + - status: "False" + type: ContainersReady + - status: "False" + type: PodScheduled + phase: Running + - apiVersion: v1 + kind: Pod + metadata: + name: p4 + namespace: default + labels: + app: test4 + ownerReferences: + - apiVersion: apps/v1 + controller: false + kind: Job + name: j4 + spec: + serviceAccountName: default + automountServiceAccountToken: true + initContainers: + - image: zorg + imagePullPolicy: IfNotPresent + name: ic1 + containers: + - image: blee + imagePullPolicy: IfNotPresent + name: c1 + resources: + limits: + cpu: 1 + mem: 1Mi + livenessProbe: + httpGet: + path: /healthz + port: 8080 + initialDelaySeconds: 3 + periodSeconds: 3 + volumeMounts: + - mountPath: /etc/config + name: config-volume + readOnly: true + - mountPath: /var/run/secrets/kubernetes.io/serviceaccount + name: kube-api-access-jgtlv + readOnly: true + - image: zorg:latest + imagePullPolicy: IfNotPresent + name: c2 + resources: + requests: + mem: 1Mi + readinessProbe: + httpGet: + path: /healthz + port: p1 + initialDelaySeconds: 3 + periodSeconds: 3 + volumeMounts: + - mountPath: /etc/config + name: config-volume + readOnly: true + - mountPath: /var/run/secrets/kubernetes.io/serviceaccount + name: kube-api-access-jgtlv + readOnly: true + status: + phase: Running + conditions: + initContainerStatuses: + - containerID: ic1 + image: blee + name: ic1 + ready: false + restartCount: 1000 + started: false + containerStatuses: + - containerID: c1 + image: blee + name: c1 + ready: false + restartCount: 1000 + started: false + - containerID: c2 + name: c2 + ready: true + restartCount: 0 + started: true + - apiVersion: v1 + kind: Pod + metadata: + name: p5 + namespace: default + labels: + app: test5 + ownerReferences: + - apiVersion: apps/v1 + controller: true + kind: ReplicaSet + name: rs5 + spec: + serviceAccountName: sa5 + automountServiceAccountToken: true + containers: + - image: blee:v1.2 + imagePullPolicy: IfNotPresent + name: c1 + status: + conditions: + phase: Running diff --git a/internal/cache/testdata/core/sa/1.yaml b/internal/cache/testdata/core/sa/1.yaml new file mode 100644 index 00000000..a6ff6fad --- /dev/null +++ b/internal/cache/testdata/core/sa/1.yaml @@ -0,0 +1,54 @@ +--- +apiVersion: v1 +kind: List +items: + - apiVersion: v1 + kind: ServiceAccount + metadata: + name: default + namespace: default + - apiVersion: v1 + kind: ServiceAccount + metadata: + name: sa1 + namespace: default + automountServiceAccountToken: false + - apiVersion: v1 + kind: ServiceAccount + metadata: + name: sa2 + namespace: default + automountServiceAccountToken: true + - apiVersion: v1 + kind: ServiceAccount + metadata: + name: sa3 + namespace: default + automountServiceAccountToken: true + - apiVersion: v1 + kind: ServiceAccount + metadata: + name: sa4 + namespace: default + automountServiceAccountToken: false + secrets: + - kind: Secret + namespace: default + name: bozo + apiVersion: v1 + imagePullSecrets: + - name: s1 + namespace: fred + - apiVersion: v1 + kind: ServiceAccount + metadata: + name: sa5 + namespace: default + automountServiceAccountToken: false + secrets: + - kind: Secret + namespace: default + name: s1 + apiVersion: v1 + imagePullSecrets: + - name: bozo diff --git a/internal/cache/testdata/mx/node/1.yaml b/internal/cache/testdata/mx/node/1.yaml new file mode 100644 index 00000000..bcf0eab5 --- /dev/null +++ b/internal/cache/testdata/mx/node/1.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: List +items: +- apiVersion: metrics.k8s.io/v1beta1 + kind: NodeMetrics + metadata: + name: n1 + usage: + cpu: 2 + memory: 200Mi diff --git a/internal/cache/testdata/net/ingress/1.yaml b/internal/cache/testdata/net/ingress/1.yaml new file mode 100644 index 00000000..48b5892f --- /dev/null +++ b/internal/cache/testdata/net/ingress/1.yaml @@ -0,0 +1,111 @@ +apiVersion: v1 +kind: List +items: +- apiVersion: networking.k8s.io/v1 + kind: Ingress + metadata: + name: ing1 + namespace: default + spec: + ingressClassName: nginx + rules: + - http: + paths: + - path: /testpath + pathType: Prefix + backend: + service: + name: svc1 + port: + name: http +- apiVersion: networking.k8s.io/v1 + kind: Ingress + metadata: + name: ing2 + namespace: default + spec: + ingressClassName: nginx + tls: + - secretName: foo + rules: + - http: + paths: + - path: /testpath + pathType: Prefix + backend: + service: + name: svc1 + port: + number: 9090 +- apiVersion: networking.k8s.io/v1 + kind: Ingress + metadata: + name: ing3 + namespace: default + spec: + ingressClassName: nginx + rules: + - http: + paths: + - path: /testpath + pathType: Prefix + backend: + service: + name: s2 + port: + number: 80 +- apiVersion: networking.k8s.io/v1 + kind: Ingress + metadata: + name: ing4 + namespace: default + spec: + ingressClassName: nginx + rules: + - http: + paths: + - path: /testpath + pathType: Prefix + backend: + service: + name: svc2 +- apiVersion: networking.k8s.io/v1 + kind: Ingress + metadata: + name: ing5 + namespace: default + annotations: + spec: + ingressClassName: nginx + rules: + - http: + paths: + - path: /testpath + pathType: Prefix + backend: + resource: + apiGroup: fred.com + kind: Zorg + name: zorg + status: + loadBalancer: + ingress: + - ports: + - error: boom +- apiVersion: networking.k8s.io/v1 + kind: Ingress + metadata: + name: ing6 + namespace: default + spec: + ingressClassName: nginx + rules: + - http: + paths: + - path: /testpath + pathType: Prefix + backend: + service: + name: svc1 + port: + number: 9091 diff --git a/internal/cache/types.go b/internal/cache/types.go new file mode 100644 index 00000000..822bfa4c --- /dev/null +++ b/internal/cache/types.go @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package cache + +const ( + // SecretKey tracks Secret resource references + SecretKey = "sec" + + // ClusterRoleKey tracks ClusterRole resource references + ClusterRoleKey = "clusterrole" + + // ConfigMapKey tracks ConfigMap resource references + ConfigMapKey = "cm" +) diff --git a/internal/client/client.go b/internal/client/client.go index 13ba0382..a8f61d7d 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -45,7 +45,7 @@ type APIClient struct { mxsClient *versioned.Clientset cachedClient *disk.CachedDiscoveryClient config types.Config - mx sync.Mutex + mx sync.RWMutex cache *cache.LRUExpireCache } @@ -71,19 +71,18 @@ func InitConnectionOrDie(config types.Config) (*APIClient, error) { return &a, nil } -func makeSAR(ns, gvr string) *authorizationv1.SelfSubjectAccessReview { +func makeSAR(ns string, gvr types.GVR) *authorizationv1.SelfSubjectAccessReview { if ns == "-" { ns = "" } - spec := NewGVR(gvr) - res := spec.GVR() + res := gvr.GVR() return &authorizationv1.SelfSubjectAccessReview{ Spec: authorizationv1.SelfSubjectAccessReviewSpec{ ResourceAttributes: &authorizationv1.ResourceAttributes{ Namespace: ns, Group: res.Group, Resource: res.Resource, - Subresource: spec.SubResource(), + Subresource: gvr.SubResource(), }, }, } @@ -100,9 +99,21 @@ func (a *APIClient) ActiveContext() string { log.Error().Msgf("Unable to located active context") return "" } + return c } +// ActiveCluster returns the current cluster name. +func (a *APIClient) ActiveCluster() string { + cl, err := a.config.CurrentClusterName() + if err != nil { + log.Error().Msgf("Unable to located active cluster") + return "" + } + + return cl +} + // IsActiveNamespace returns true if namespaces matches. func (a *APIClient) IsActiveNamespace(ns string) bool { if a.ActiveNamespace() == AllNamespaces { @@ -115,8 +126,9 @@ func (a *APIClient) IsActiveNamespace(ns string) bool { func (a *APIClient) ActiveNamespace() string { ns, err := a.CurrentNamespaceName() if err != nil { - return AllNamespaces + return DefaultNamespace } + return ns } @@ -126,12 +138,19 @@ func (a *APIClient) clearCache() { } } +// ConnectionOK checks api server connection status. +func (a *APIClient) ConnectionOK() bool { + _, err := a.Dial() + + return err == nil +} + // CanI checks if user has access to a certain resource. -func (a *APIClient) CanI(ns, gvr string, verbs []string) (auth bool, err error) { +func (a *APIClient) CanI(ns string, gvr types.GVR, verbs ...string) (auth bool, err error) { if IsClusterWide(ns) { ns = AllNamespaces } - key := makeCacheKey(ns, gvr, verbs) + key := makeCacheKey(ns, gvr.String(), verbs) if v, ok := a.cache.Get(key); ok { if auth, ok = v.(bool); ok { return auth, nil @@ -291,10 +310,15 @@ func (a *APIClient) CachedDiscovery() (*disk.CachedDiscoveryClient, error) { // DynDial returns a handle to a dynamic interface. func (a *APIClient) DynDial() (dynamic.Interface, error) { + a.mx.RLock() if a.dClient != nil { + a.mx.RUnlock() return a.dClient, nil } + a.mx.RUnlock() + a.mx.Lock() + defer a.mx.Unlock() rc, err := a.RestConfig() if err != nil { return nil, err diff --git a/internal/client/config.go b/internal/client/config.go index b849a258..85cd1f3f 100644 --- a/internal/client/config.go +++ b/internal/client/config.go @@ -157,8 +157,8 @@ func (c *Config) CurrentClusterName() (string, error) { current = *c.flags.Context } - if ctx, ok := cfg.Contexts[current]; ok { - return ctx.Cluster, nil + if ct, ok := cfg.Contexts[current]; ok { + return ct.Cluster, nil } return "", errors.New("unable to locate current cluster") @@ -240,19 +240,16 @@ func (c *Config) CurrentNamespaceName() (string, error) { cfg, err := c.RawConfig() if err != nil { - return "", err + return DefaultNamespace, err } - ctx, err := c.CurrentContextName() - if err != nil { - return "", err - } - if ctx, ok := cfg.Contexts[ctx]; ok { - if isSet(&ctx.Namespace) { - return ctx.Namespace, nil + if ct, ok := cfg.Contexts[cfg.CurrentContext]; ok { + if ct.Namespace == BlankNamespace { + return DefaultNamespace, nil } + return ct.Namespace, nil } - return "", fmt.Errorf("No active namespace specified") + return DefaultNamespace, nil } // NamespaceNames fetch all available namespaces on current cluster. diff --git a/internal/client/config_test.go b/internal/client/config_test.go index 01d1a715..d00a12bb 100644 --- a/internal/client/config_test.go +++ b/internal/client/config_test.go @@ -5,21 +5,15 @@ package client_test import ( "errors" - "fmt" "testing" "github.com/derailed/popeye/internal/client" - "github.com/rs/zerolog" "github.com/stretchr/testify/assert" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/cli-runtime/pkg/genericclioptions" ) -func init() { - zerolog.SetGlobalLevel(zerolog.FatalLevel) -} - func TestConfigCurrentContext(t *testing.T) { name, kubeConfig := "blee", "./testdata/config" uu := []struct { @@ -75,21 +69,35 @@ func TestConfigCurrentUser(t *testing.T) { } func TestConfigCurrentNamespace(t *testing.T) { - name, kubeConfig := "blee", "./testdata/config" - uu := []struct { - flags *genericclioptions.ConfigFlags - namespace string - err error + ns, kubeConfig := "ns1", "./testdata/config" + uu := map[string]struct { + flags *genericclioptions.ConfigFlags + ns string + err error }{ - {&genericclioptions.ConfigFlags{KubeConfig: &kubeConfig}, "", fmt.Errorf("No active namespace specified")}, - {&genericclioptions.ConfigFlags{KubeConfig: &kubeConfig, Namespace: &name}, "blee", nil}, - } - - for _, u := range uu { - cfg := client.NewConfig(u.flags) - ns, err := cfg.CurrentNamespaceName() - assert.Equal(t, u.err, err) - assert.Equal(t, u.namespace, ns) + "open": { + flags: &genericclioptions.ConfigFlags{ + KubeConfig: &kubeConfig, + }, + ns: client.DefaultNamespace, + }, + "manual": { + flags: &genericclioptions.ConfigFlags{ + KubeConfig: &kubeConfig, + Namespace: &ns, + }, + ns: ns, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + cfg := client.NewConfig(u.flags) + ns, err := cfg.CurrentNamespaceName() + assert.Equal(t, u.err, err) + assert.Equal(t, u.ns, ns) + }) } } diff --git a/internal/client/factory.go b/internal/client/factory.go index 5c2cfc20..99d40804 100644 --- a/internal/client/factory.go +++ b/internal/client/factory.go @@ -65,8 +65,8 @@ func (f *Factory) Terminate() { } // List returns a resource collection. -func (f *Factory) List(gvr, ns string, wait bool, labels labels.Selector) ([]runtime.Object, error) { - inf, err := f.CanForResource(ns, gvr, types.MonitorAccess) +func (f *Factory) List(gvr types.GVR, ns string, wait bool, labels labels.Selector) ([]runtime.Object, error) { + inf, err := f.CanForResource(ns, gvr, types.MonitorAccess...) if err != nil { return nil, err } @@ -84,9 +84,9 @@ func (f *Factory) List(gvr, ns string, wait bool, labels labels.Selector) ([]run } // Get retrieves a given resource. -func (f *Factory) Get(gvr, path string, wait bool, sel labels.Selector) (runtime.Object, error) { +func (f *Factory) Get(gvr types.GVR, path string, wait bool, sel labels.Selector) (runtime.Object, error) { ns, n := Namespaced(path) - inf, err := f.CanForResource(ns, gvr, []string{types.GetVerb}) + inf, err := f.CanForResource(ns, gvr, types.GetVerb) if err != nil { return nil, err } @@ -166,15 +166,15 @@ func (f *Factory) isClusterWide() bool { } // CanForResource return an informer is user has access. -func (f *Factory) CanForResource(ns, gvr string, verbs []string) (informers.GenericInformer, error) { +func (f *Factory) CanForResource(ns string, gvr types.GVR, verbs ...string) (informers.GenericInformer, error) { // If user can access resource cluster wide, prefer cluster wide factory. if !IsClusterWide(ns) { - auth, err := f.Client().CanI(AllNamespaces, gvr, verbs) + auth, err := f.Client().CanI(AllNamespaces, gvr, verbs...) if auth && err == nil { return f.ForResource(AllNamespaces, gvr) } } - auth, err := f.Client().CanI(ns, gvr, verbs) + auth, err := f.Client().CanI(ns, gvr, verbs...) if err != nil { return nil, err } @@ -186,12 +186,12 @@ func (f *Factory) CanForResource(ns, gvr string, verbs []string) (informers.Gene } // ForResource returns an informer for a given resource. -func (f *Factory) ForResource(ns, gvr string) (informers.GenericInformer, error) { +func (f *Factory) ForResource(ns string, gvr types.GVR) (informers.GenericInformer, error) { fact, err := f.ensureFactory(ns) if err != nil { return nil, err } - inf := fact.ForResource(NewGVR(gvr).GVR()) + inf := fact.ForResource(gvr.GVR()) if inf == nil { log.Error().Err(fmt.Errorf("MEOW! No informer for %q:%q", ns, gvr)) return inf, nil diff --git a/internal/client/meta.go b/internal/client/meta.go deleted file mode 100644 index fd9bf1a8..00000000 --- a/internal/client/meta.go +++ /dev/null @@ -1,46 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package client - -import ( - "github.com/derailed/popeye/types" -) - -// Schema tracks resource schema. -type Schema struct { - GVR GVR - Preferred bool -} - -// Meta tracks a collection of resources. -type Meta map[string][]Schema - -func newMeta() Meta { - return make(map[string][]Schema) -} - -// Resources tracks dictionary of resources. -var Resources = newMeta() - -// Load loads resource meta from server. -func Load(f types.Factory) error { - dial, err := f.Client().CachedDiscovery() - if err != nil { - return err - } - rr, err := dial.ServerPreferredResources() - if err != nil { - return err - } - - for _, r := range rr { - for _, res := range r.APIResources { - gvr := FromGVAndR(r.GroupVersion, res.Name) - res.Group, res.Version = gvr.G(), gvr.V() - Resources[gvr.R()] = []Schema{{GVR: gvr, Preferred: true}} - } - } - - return nil -} diff --git a/internal/client/metrics_test.go b/internal/client/metrics_test.go index 4aa6f007..695b3a61 100644 --- a/internal/client/metrics_test.go +++ b/internal/client/metrics_test.go @@ -38,7 +38,6 @@ func TestMetricsEmpty(t *testing.T) { } } -// ---------------------------------------------------------------------------- // Helpers... func toQty(s string) resource.Quantity { diff --git a/internal/client/types.go b/internal/client/types.go index c2eccbea..cec3ce8e 100644 --- a/internal/client/types.go +++ b/internal/client/types.go @@ -18,4 +18,10 @@ const ( // NotNamespaced designates a non resource namespace. NotNamespaced = "*" + + // BlankNamespace tracks an unspecified namespace. + BlankNamespace = "" + + // DefaultNamespace tracks the default namespace. + DefaultNamespace = "default" ) diff --git a/internal/context.go b/internal/context.go index dcbb62b3..4fb3a43a 100644 --- a/internal/context.go +++ b/internal/context.go @@ -6,42 +6,46 @@ package internal import ( "context" - "github.com/derailed/popeye/internal/client" + "github.com/derailed/popeye/internal/rules" + "github.com/derailed/popeye/types" ) -// RunInfo describes a sanitizer run. +// RunInfo describes a scan run. type RunInfo struct { Section string - SectionGVR client.GVR - FQN string + SectionGVR types.GVR Group string - GroupGVR client.GVR + GroupGVR types.GVR + Spec rules.Spec + Total int +} + +func NewRunInfo(gvr types.GVR) RunInfo { + return RunInfo{ + Section: gvr.R(), + SectionGVR: gvr, + } } // WithGroup adds a group to the context. -func WithGroup(ctx context.Context, gvr client.GVR, grp string) context.Context { +func WithGroup(ctx context.Context, gvr types.GVR, grp string) context.Context { r := MustExtractRunInfo(ctx) r.Group, r.GroupGVR = grp, gvr - return context.WithValue(ctx, KeyRunInfo, r) -} -// WithFQN adds a fqn to the context. -func WithFQN(ctx context.Context, fqn string) context.Context { - r := MustExtractRunInfo(ctx) - r.FQN = fqn return context.WithValue(ctx, KeyRunInfo, r) } -// MustExtractFQN extract fqn from context or die. -func MustExtractFQN(ctx context.Context) string { +func WithSpec(ctx context.Context, spec rules.Spec) context.Context { r := MustExtractRunInfo(ctx) - return r.FQN + r.Spec = spec + + return context.WithValue(ctx, KeyRunInfo, r) } // MustExtractSectionGVR extract section gvr from context or die. -func MustExtractSectionGVR(ctx context.Context) string { +func MustExtractSectionGVR(ctx context.Context) types.GVR { r := MustExtractRunInfo(ctx) - return r.SectionGVR.String() + return r.SectionGVR } // MustExtractRunInfo extracts runinfo from context or die. @@ -52,3 +56,11 @@ func MustExtractRunInfo(ctx context.Context) RunInfo { } return r } + +func MustExtractFactory(ctx context.Context) types.Factory { + f, ok := ctx.Value(KeyFactory).(types.Factory) + if !ok { + panic("Doh! No factory in context") + } + return f +} diff --git a/internal/dag/cluster.go b/internal/dag/cluster.go index 572726f1..dfc8b07b 100644 --- a/internal/dag/cluster.go +++ b/internal/dag/cluster.go @@ -3,19 +3,28 @@ package dag -import "context" +import ( + "context" + + "github.com/Masterminds/semver" +) // ListVersion return server api version. -func ListVersion(ctx context.Context) (string, string, error) { +func ListVersion(ctx context.Context) (*semver.Version, error) { f := mustExtractFactory(ctx) dial, err := f.Client().Dial() if err != nil { - return "", "", err + return nil, err + } + info, err := dial.Discovery().ServerVersion() + if err != nil { + return nil, err } - v, err := dial.Discovery().ServerVersion() + + rev, err := semver.NewVersion(info.Major + "." + info.Minor) if err != nil { - return "", "", err + return nil, err } - return v.Major, v.Minor, nil + return rev, nil } diff --git a/internal/dag/cm.go b/internal/dag/cm.go deleted file mode 100644 index 4687d0b0..00000000 --- a/internal/dag/cm.go +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package dag - -import ( - "context" - "errors" - - "github.com/derailed/popeye/internal/client" - "github.com/derailed/popeye/internal/dao" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" -) - -// ctx := context.WithValue(context.Background(), internal.KeyFactory, f) - -// ListConfigMaps list all included ConfigMaps. -func ListConfigMaps(ctx context.Context) (map[string]*v1.ConfigMap, error) { - return listAllConfigMaps(ctx) -} - -// ListAllConfigMaps fetch all ConfigMaps on the cluster. -func listAllConfigMaps(ctx context.Context) (map[string]*v1.ConfigMap, error) { - ll, err := fetchConfigMaps(ctx) - if err != nil { - return nil, err - } - cms := make(map[string]*v1.ConfigMap, len(ll.Items)) - for i := range ll.Items { - cms[metaFQN(ll.Items[i].ObjectMeta)] = &ll.Items[i] - } - - return cms, nil -} - -// FetchConfigMaps retrieves all ConfigMaps on the cluster. -func fetchConfigMaps(ctx context.Context) (*v1.ConfigMapList, error) { - f, cfg := mustExtractFactory(ctx), mustExtractConfig(ctx) - if cfg.Flags.StandAlone { - dial, err := f.Client().Dial() - if err != nil { - return nil, err - } - return dial.CoreV1().ConfigMaps(f.Client().ActiveNamespace()).List(ctx, metav1.ListOptions{}) - } - - var res dao.Resource - res.Init(f, client.NewGVR("v1/configmaps")) - oo, err := res.List(ctx) - if err != nil { - return nil, err - } - var ll v1.ConfigMapList - for _, o := range oo { - var cm v1.ConfigMap - err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &cm) - if err != nil { - return nil, errors.New("expecting configmap resource") - } - ll.Items = append(ll.Items, cm) - } - - return &ll, nil -} diff --git a/internal/dag/cr.go b/internal/dag/cr.go deleted file mode 100644 index 7c212613..00000000 --- a/internal/dag/cr.go +++ /dev/null @@ -1,65 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package dag - -import ( - "context" - "errors" - - "github.com/derailed/popeye/internal/client" - "github.com/derailed/popeye/internal/dao" - rbacv1 "k8s.io/api/rbac/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" -) - -// ListClusterRoles list included ClusterRoles. -func ListClusterRoles(ctx context.Context) (map[string]*rbacv1.ClusterRole, error) { - return listAllClusterRoles(ctx) -} - -// ListAllClusterRoles fetch all ClusterRoles on the cluster. -func listAllClusterRoles(ctx context.Context) (map[string]*rbacv1.ClusterRole, error) { - ll, err := fetchClusterRoles(ctx) - if err != nil { - return nil, err - } - crs := make(map[string]*rbacv1.ClusterRole, len(ll.Items)) - for i := range ll.Items { - crs[metaFQN(ll.Items[i].ObjectMeta)] = &ll.Items[i] - } - - return crs, nil -} - -// FetchClusterRoles retrieves all ClusterRoles on the cluster. -func fetchClusterRoles(ctx context.Context) (*rbacv1.ClusterRoleList, error) { - f, cfg := mustExtractFactory(ctx), mustExtractConfig(ctx) - if cfg.Flags.StandAlone { - dial, err := f.Client().Dial() - if err != nil { - return nil, err - } - return dial.RbacV1().ClusterRoles().List(ctx, metav1.ListOptions{}) - } - - var res dao.Resource - res.Init(f, client.NewGVR("rbac.authorization.k8s.io/v1/clusterroles")) - oo, err := res.List(ctx) - if err != nil { - return nil, err - } - var ll rbacv1.ClusterRoleList - for _, o := range oo { - var cr rbacv1.ClusterRole - err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &cr) - if err != nil { - return nil, errors.New("expecting clusterrole resource") - } - ll.Items = append(ll.Items, cr) - } - - return &ll, nil -} diff --git a/internal/dag/crb.go b/internal/dag/crb.go deleted file mode 100644 index d06155ce..00000000 --- a/internal/dag/crb.go +++ /dev/null @@ -1,66 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package dag - -import ( - "context" - "errors" - - "github.com/derailed/popeye/internal/client" - "github.com/derailed/popeye/internal/dao" - rbacv1 "k8s.io/api/rbac/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" -) - -// ListClusterRoleBindings list included ClusterRoleBindings. -func ListClusterRoleBindings(ctx context.Context) (map[string]*rbacv1.ClusterRoleBinding, error) { - return listAllClusterRoleBindings(ctx) -} - -// ListAllClusterRoleBindings fetch all ClusterRoleBindings on the cluster. -func listAllClusterRoleBindings(ctx context.Context) (map[string]*rbacv1.ClusterRoleBinding, error) { - ll, err := fetchClusterRoleBindings(ctx) - if err != nil { - return nil, err - } - - crbs := make(map[string]*rbacv1.ClusterRoleBinding, len(ll.Items)) - for i := range ll.Items { - crbs[metaFQN(ll.Items[i].ObjectMeta)] = &ll.Items[i] - } - - return crbs, nil -} - -// FetchClusterRoleBindings retrieves all ClusterRoleBindings on the cluster. -func fetchClusterRoleBindings(ctx context.Context) (*rbacv1.ClusterRoleBindingList, error) { - f, cfg := mustExtractFactory(ctx), mustExtractConfig(ctx) - if cfg.Flags.StandAlone { - dial, err := f.Client().Dial() - if err != nil { - return nil, err - } - return dial.RbacV1().ClusterRoleBindings().List(ctx, metav1.ListOptions{}) - } - - var res dao.Resource - res.Init(f, client.NewGVR("rbac.authorization.k8s.io/v1/clusterrolebindings")) - oo, err := res.List(ctx) - if err != nil { - return nil, err - } - var ll rbacv1.ClusterRoleBindingList - for _, o := range oo { - var crb rbacv1.ClusterRoleBinding - err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &crb) - if err != nil { - return nil, errors.New("expecting clusterrolebinding resource") - } - ll.Items = append(ll.Items, crb) - } - - return &ll, nil -} diff --git a/internal/dag/dp.go b/internal/dag/dp.go deleted file mode 100644 index d7972388..00000000 --- a/internal/dag/dp.go +++ /dev/null @@ -1,65 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package dag - -import ( - "context" - "errors" - - "github.com/derailed/popeye/internal/client" - "github.com/derailed/popeye/internal/dao" - appsv1 "k8s.io/api/apps/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" -) - -// ListDeployments list all included Deployments. -func ListDeployments(ctx context.Context) (map[string]*appsv1.Deployment, error) { - return listAllDeployments(ctx) -} - -// ListAllDeployments fetch all Deployments on the cluster. -func listAllDeployments(ctx context.Context) (map[string]*appsv1.Deployment, error) { - ll, err := fetchDeployments(ctx) - if err != nil { - return nil, err - } - dps := make(map[string]*appsv1.Deployment, len(ll.Items)) - for i := range ll.Items { - dps[metaFQN(ll.Items[i].ObjectMeta)] = &ll.Items[i] - } - - return dps, nil -} - -// FetchDeployments retrieves all Deployments on the cluster. -func fetchDeployments(ctx context.Context) (*appsv1.DeploymentList, error) { - f, cfg := mustExtractFactory(ctx), mustExtractConfig(ctx) - if cfg.Flags.StandAlone { - dial, err := f.Client().Dial() - if err != nil { - return nil, err - } - return dial.AppsV1().Deployments(f.Client().ActiveNamespace()).List(ctx, metav1.ListOptions{}) - } - - var res dao.Resource - res.Init(f, client.NewGVR("apps/v1/deployments")) - oo, err := res.List(ctx) - if err != nil { - return nil, err - } - var ll appsv1.DeploymentList - for _, o := range oo { - var dp appsv1.Deployment - err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &dp) - if err != nil { - return nil, errors.New("expecting deployment resource") - } - ll.Items = append(ll.Items, dp) - } - - return &ll, nil -} diff --git a/internal/dag/ds.go b/internal/dag/ds.go deleted file mode 100644 index 6cc11dd3..00000000 --- a/internal/dag/ds.go +++ /dev/null @@ -1,65 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package dag - -import ( - "context" - "errors" - - "github.com/derailed/popeye/internal/client" - "github.com/derailed/popeye/internal/dao" - appsv1 "k8s.io/api/apps/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" -) - -// ListDaemonSets list all included DaemonSets. -func ListDaemonSets(ctx context.Context) (map[string]*appsv1.DaemonSet, error) { - return listAllDaemonSets(ctx) -} - -// ListAllDaemonSets fetch all DaemonSets on the cluster. -func listAllDaemonSets(ctx context.Context) (map[string]*appsv1.DaemonSet, error) { - ll, err := fetchDaemonSets(ctx) - if err != nil { - return nil, err - } - dps := make(map[string]*appsv1.DaemonSet, len(ll.Items)) - for i := range ll.Items { - dps[metaFQN(ll.Items[i].ObjectMeta)] = &ll.Items[i] - } - - return dps, nil -} - -// FetchDaemonSets retrieves all DaemonSets on the cluster. -func fetchDaemonSets(ctx context.Context) (*appsv1.DaemonSetList, error) { - f, cfg := mustExtractFactory(ctx), mustExtractConfig(ctx) - if cfg.Flags.StandAlone { - dial, err := f.Client().Dial() - if err != nil { - return nil, err - } - return dial.AppsV1().DaemonSets(f.Client().ActiveNamespace()).List(ctx, metav1.ListOptions{}) - } - - var res dao.Resource - res.Init(f, client.NewGVR("apps/v1/daemonsets")) - oo, err := res.List(ctx) - if err != nil { - return nil, err - } - var ll appsv1.DaemonSetList - for _, o := range oo { - var ds appsv1.DaemonSet - err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &ds) - if err != nil { - return nil, errors.New("expecting daemonset resource") - } - ll.Items = append(ll.Items, ds) - } - - return &ll, nil -} diff --git a/internal/dag/ep.go b/internal/dag/ep.go deleted file mode 100644 index 0e053bac..00000000 --- a/internal/dag/ep.go +++ /dev/null @@ -1,66 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package dag - -import ( - "context" - "errors" - - "github.com/derailed/popeye/internal/client" - "github.com/derailed/popeye/internal/dao" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" -) - -// ListEndpoints list all included Endpoints. -func ListEndpoints(ctx context.Context) (map[string]*v1.Endpoints, error) { - return listAllEndpoints(ctx) -} - -// ListAllEndpoints fetch all Endpoints on the cluster. -func listAllEndpoints(ctx context.Context) (map[string]*v1.Endpoints, error) { - ll, err := fetchEndpoints(ctx) - if err != nil { - return nil, err - } - eps := make(map[string]*v1.Endpoints, len(ll.Items)) - for i := range ll.Items { - eps[metaFQN(ll.Items[i].ObjectMeta)] = &ll.Items[i] - } - - return eps, nil -} - -// FetchEndpoints retrieves all Endpoints on the cluster. -func fetchEndpoints(ctx context.Context) (*v1.EndpointsList, error) { - f, cfg := mustExtractFactory(ctx), mustExtractConfig(ctx) - if cfg.Flags.StandAlone { - dial, err := f.Client().Dial() - if err != nil { - return nil, err - } - return dial.CoreV1().Endpoints(f.Client().ActiveNamespace()).List(ctx, metav1.ListOptions{}) - } - - var res dao.Resource - res.Init(f, client.NewGVR("v1/endpoints")) - oo, err := res.List(ctx) - if err != nil { - return nil, err - } - var ll v1.EndpointsList - for _, o := range oo { - var ep v1.Endpoints - err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &ep) - if err != nil { - return nil, errors.New("expecting endpoints resource") - } - ll.Items = append(ll.Items, ep) - } - - return &ll, nil - -} diff --git a/internal/dag/helpers.go b/internal/dag/helpers.go index 7d03abf3..2a802493 100644 --- a/internal/dag/helpers.go +++ b/internal/dag/helpers.go @@ -7,7 +7,6 @@ import ( "context" "github.com/derailed/popeye/internal" - "github.com/derailed/popeye/pkg/config" "github.com/derailed/popeye/types" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -20,14 +19,6 @@ func mustExtractFactory(ctx context.Context) types.Factory { return f } -func mustExtractConfig(ctx context.Context) *config.Config { - cfg, ok := ctx.Value(internal.KeyConfig).(*config.Config) - if !ok { - panic("expecting config in context") - } - return cfg -} - // MetaFQN returns a full qualified ns/name string. func metaFQN(m metav1.ObjectMeta) string { if m.Namespace == "" { diff --git a/internal/dag/hpa.go b/internal/dag/hpa.go deleted file mode 100644 index 30fca7ad..00000000 --- a/internal/dag/hpa.go +++ /dev/null @@ -1,65 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package dag - -import ( - "context" - "errors" - - "github.com/derailed/popeye/internal/client" - "github.com/derailed/popeye/internal/dao" - autoscalingv1 "k8s.io/api/autoscaling/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" -) - -// ListHorizontalPodAutoscalers list all included HorizontalPodAutoscalers. -func ListHorizontalPodAutoscalers(ctx context.Context) (map[string]*autoscalingv1.HorizontalPodAutoscaler, error) { - return listAllHorizontalPodAutoscalers(ctx) -} - -// ListAllHorizontalPodAutoscalers fetch all HorizontalPodAutoscalers on the cluster. -func listAllHorizontalPodAutoscalers(ctx context.Context) (map[string]*autoscalingv1.HorizontalPodAutoscaler, error) { - ll, err := fetchHorizontalPodAutoscalers(ctx) - if err != nil { - return nil, err - } - hpas := make(map[string]*autoscalingv1.HorizontalPodAutoscaler, len(ll.Items)) - for i := range ll.Items { - hpas[metaFQN(ll.Items[i].ObjectMeta)] = &ll.Items[i] - } - - return hpas, nil -} - -// FetchHorizontalPodAutoscalers retrieves all HorizontalPodAutoscalers on the cluster. -func fetchHorizontalPodAutoscalers(ctx context.Context) (*autoscalingv1.HorizontalPodAutoscalerList, error) { - f, cfg := mustExtractFactory(ctx), mustExtractConfig(ctx) - if cfg.Flags.StandAlone { - dial, err := f.Client().Dial() - if err != nil { - return nil, err - } - return dial.AutoscalingV1().HorizontalPodAutoscalers(f.Client().ActiveNamespace()).List(ctx, metav1.ListOptions{}) - } - - var res dao.Resource - res.Init(f, client.NewGVR("autoscaling/v1/horizontalpodautoscalers")) - oo, err := res.List(ctx) - if err != nil { - return nil, err - } - var ll autoscalingv1.HorizontalPodAutoscalerList - for _, o := range oo { - var hpa autoscalingv1.HorizontalPodAutoscaler - err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &hpa) - if err != nil { - return nil, errors.New("expecting hpa resource") - } - ll.Items = append(ll.Items, hpa) - } - - return &ll, nil -} diff --git a/internal/dag/ing.go b/internal/dag/ing.go deleted file mode 100644 index 4f60aca8..00000000 --- a/internal/dag/ing.go +++ /dev/null @@ -1,70 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package dag - -import ( - "context" - "errors" - - netv1 "k8s.io/api/networking/v1" - - "github.com/derailed/popeye/internal/client" - "github.com/derailed/popeye/internal/dao" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" -) - -// IngressGVR tracks ingress specification -var IngressGVR = client.NewGVR("networking.k8s.io/v1/ingresses") - -// ListIngresses list all included Ingresses. -func ListIngresses(ctx context.Context) (map[string]*netv1.Ingress, error) { - return listAllIngresses(ctx) -} - -// ListAllIngresses fetch all Ingresses on the cluster. -func listAllIngresses(ctx context.Context) (map[string]*netv1.Ingress, error) { - ll, err := fetchIngresses(ctx) - if err != nil { - return nil, err - } - ings := make(map[string]*netv1.Ingress, len(ll.Items)) - for i := range ll.Items { - ings[metaFQN(ll.Items[i].ObjectMeta)] = &ll.Items[i] - } - - return ings, nil -} - -// FetchIngresses retrieves all Ingresses on the cluster. -func fetchIngresses(ctx context.Context) (*netv1.IngressList, error) { - f, cfg := mustExtractFactory(ctx), mustExtractConfig(ctx) - if cfg.Flags.StandAlone { - dial, err := f.Client().Dial() - if err != nil { - return nil, err - } - - return dial.NetworkingV1().Ingresses(f.Client().ActiveNamespace()).List(ctx, metav1.ListOptions{}) - } - - var res dao.Resource - res.Init(f, IngressGVR) - oo, err := res.List(ctx) - if err != nil { - return nil, err - } - var ll netv1.IngressList - for _, o := range oo { - var ing netv1.Ingress - err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &ing) - if err != nil { - return nil, errors.New("expecting ingress resource") - } - ll.Items = append(ll.Items, ing) - } - - return &ll, nil -} diff --git a/internal/dag/limit_range.go b/internal/dag/limit_range.go deleted file mode 100644 index 5ce9819c..00000000 --- a/internal/dag/limit_range.go +++ /dev/null @@ -1,65 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package dag - -import ( - "context" - "errors" - - "github.com/derailed/popeye/internal/client" - "github.com/derailed/popeye/internal/dao" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" -) - -// ListLimitRanges list all included LimitRanges. -func ListLimitRanges(ctx context.Context) (map[string]*v1.LimitRange, error) { - return listAllLimitRanges(ctx) -} - -// ListAllLimitRanges fetch all LimitRanges on the cluster. -func listAllLimitRanges(ctx context.Context) (map[string]*v1.LimitRange, error) { - ll, err := fetchLimitRanges(ctx) - if err != nil { - return nil, err - } - lrs := make(map[string]*v1.LimitRange, len(ll.Items)) - for i := range ll.Items { - lrs[metaFQN(ll.Items[i].ObjectMeta)] = &ll.Items[i] - } - - return lrs, nil -} - -// fetchLimitRanges retrieves all LimitRanges on the cluster. -func fetchLimitRanges(ctx context.Context) (*v1.LimitRangeList, error) { - f, cfg := mustExtractFactory(ctx), mustExtractConfig(ctx) - if cfg.Flags.StandAlone { - dial, err := f.Client().Dial() - if err != nil { - return nil, err - } - return dial.CoreV1().LimitRanges(f.Client().ActiveNamespace()).List(ctx, metav1.ListOptions{}) - } - - var res dao.Resource - res.Init(f, client.NewGVR("v1/limitranges")) - oo, err := res.List(ctx) - if err != nil { - return nil, err - } - var ll v1.LimitRangeList - for _, o := range oo { - var lr v1.LimitRange - err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &lr) - if err != nil { - return nil, errors.New("expecting limitrange resource") - } - ll.Items = append(ll.Items, lr) - } - - return &ll, nil -} diff --git a/internal/dag/no.go b/internal/dag/no.go deleted file mode 100644 index 3d4740b6..00000000 --- a/internal/dag/no.go +++ /dev/null @@ -1,65 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package dag - -import ( - "context" - "errors" - - "github.com/derailed/popeye/internal/client" - "github.com/derailed/popeye/internal/dao" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" -) - -// ListNodes list all included Nodes. -func ListNodes(ctx context.Context) (map[string]*v1.Node, error) { - return listAllNodes(ctx) -} - -// ListAllNodes fetch all Nodes on the cluster. -func listAllNodes(ctx context.Context) (map[string]*v1.Node, error) { - ll, err := fetchNodes(ctx) - if err != nil { - return nil, err - } - nos := make(map[string]*v1.Node, len(ll.Items)) - for i := range ll.Items { - nos[metaFQN(ll.Items[i].ObjectMeta)] = &ll.Items[i] - } - - return nos, nil -} - -// FetchNodes retrieves all Nodes on the cluster. -func fetchNodes(ctx context.Context) (*v1.NodeList, error) { - f, cfg := mustExtractFactory(ctx), mustExtractConfig(ctx) - if cfg.Flags.StandAlone { - dial, err := f.Client().Dial() - if err != nil { - return nil, err - } - return dial.CoreV1().Nodes().List(ctx, metav1.ListOptions{}) - } - - var res dao.Resource - res.Init(f, client.NewGVR("v1/nodes")) - oo, err := res.List(ctx) - if err != nil { - return nil, err - } - var ll v1.NodeList - for _, o := range oo { - var no v1.Node - err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &no) - if err != nil { - return nil, errors.New("expecting node resource") - } - ll.Items = append(ll.Items, no) - } - - return &ll, nil -} diff --git a/internal/dag/no_mx.go b/internal/dag/no_mx.go deleted file mode 100644 index 720344b4..00000000 --- a/internal/dag/no_mx.go +++ /dev/null @@ -1,40 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package dag - -import ( - "context" - - "github.com/derailed/popeye/internal/client" - "github.com/derailed/popeye/types" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" -) - -// ListNodesMetrics fetch all available Node metrics on the cluster. -func ListNodesMetrics(c types.Connection) (map[string]*mv1beta1.NodeMetrics, error) { - ll, err := fetchNodesMetrics(c) - if err != nil { - return map[string]*mv1beta1.NodeMetrics{}, err - } - - pmx := make(map[string]*mv1beta1.NodeMetrics, len(ll.Items)) - for i := range ll.Items { - pmx[metaFQN(ll.Items[i].ObjectMeta)] = &ll.Items[i] - } - - return pmx, nil -} - -// FetchNodesMetrics retrieves all Node metrics on the cluster. -func fetchNodesMetrics(c types.Connection) (*mv1beta1.NodeMetricsList, error) { - vc, err := c.MXDial() - if err != nil { - return nil, err - } - - ctx, cancel := context.WithTimeout(context.Background(), client.CallTimeout) - defer cancel() - return vc.MetricsV1beta1().NodeMetricses().List(ctx, metav1.ListOptions{}) -} diff --git a/internal/dag/np.go b/internal/dag/np.go deleted file mode 100644 index e2b046c1..00000000 --- a/internal/dag/np.go +++ /dev/null @@ -1,65 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package dag - -import ( - "context" - "errors" - - "github.com/derailed/popeye/internal/client" - "github.com/derailed/popeye/internal/dao" - nv1 "k8s.io/api/networking/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" -) - -// ListNetworkPolicies list all included NetworkPolicies. -func ListNetworkPolicies(ctx context.Context) (map[string]*nv1.NetworkPolicy, error) { - return listAllNetworkPolicies(ctx) -} - -// ListAllNetworkPolicies fetch all NetworkPolicies on the cluster. -func listAllNetworkPolicies(ctx context.Context) (map[string]*nv1.NetworkPolicy, error) { - ll, err := fetchNetworkPolicies(ctx) - if err != nil { - return nil, err - } - dps := make(map[string]*nv1.NetworkPolicy, len(ll.Items)) - for i := range ll.Items { - dps[metaFQN(ll.Items[i].ObjectMeta)] = &ll.Items[i] - } - - return dps, nil -} - -// FetchNetworkPolicies retrieves all NetworkPolicies on the cluster. -func fetchNetworkPolicies(ctx context.Context) (*nv1.NetworkPolicyList, error) { - f, cfg := mustExtractFactory(ctx), mustExtractConfig(ctx) - if cfg.Flags.StandAlone { - dial, err := f.Client().Dial() - if err != nil { - return nil, err - } - return dial.NetworkingV1().NetworkPolicies(f.Client().ActiveNamespace()).List(ctx, metav1.ListOptions{}) - } - - var res dao.Resource - res.Init(f, client.NewGVR("networking.k8s.io/v1/networkpolicies")) - oo, err := res.List(ctx) - if err != nil { - return nil, err - } - var ll nv1.NetworkPolicyList - for _, o := range oo { - var np nv1.NetworkPolicy - err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &np) - if err != nil { - return nil, errors.New("expecting networkpolicy resource") - } - ll.Items = append(ll.Items, np) - } - - return &ll, nil -} diff --git a/internal/dag/ns.go b/internal/dag/ns.go deleted file mode 100644 index 10b2740f..00000000 --- a/internal/dag/ns.go +++ /dev/null @@ -1,65 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package dag - -import ( - "context" - "errors" - - "github.com/derailed/popeye/internal/client" - "github.com/derailed/popeye/internal/dao" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" -) - -// ListNamespaces list all included Namespaces. -func ListNamespaces(ctx context.Context) (map[string]*v1.Namespace, error) { - return listAllNamespaces(ctx) -} - -// ListAllNamespaces fetch all Namespaces on the cluster. -func listAllNamespaces(ctx context.Context) (map[string]*v1.Namespace, error) { - ll, err := fetchNamespaces(ctx) - if err != nil { - return nil, err - } - nss := make(map[string]*v1.Namespace, len(ll.Items)) - for i := range ll.Items { - nss[metaFQN(ll.Items[i].ObjectMeta)] = &ll.Items[i] - } - - return nss, nil -} - -// FetchNamespaces retrieves all Namespaces on the cluster. -func fetchNamespaces(ctx context.Context) (*v1.NamespaceList, error) { - f, cfg := mustExtractFactory(ctx), mustExtractConfig(ctx) - if cfg.Flags.StandAlone { - dial, err := f.Client().Dial() - if err != nil { - return nil, err - } - return dial.CoreV1().Namespaces().List(ctx, metav1.ListOptions{}) - } - - var res dao.Resource - res.Init(f, client.NewGVR("v1/namespaces")) - oo, err := res.List(ctx) - if err != nil { - return nil, err - } - var ll v1.NamespaceList - for _, o := range oo { - var ns v1.Namespace - err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &ns) - if err != nil { - return nil, errors.New("expecting namespace resource") - } - ll.Items = append(ll.Items, ns) - } - - return &ll, nil -} diff --git a/internal/dag/pdb.go b/internal/dag/pdb.go deleted file mode 100644 index ea8c77e6..00000000 --- a/internal/dag/pdb.go +++ /dev/null @@ -1,65 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package dag - -import ( - "context" - "errors" - - "github.com/derailed/popeye/internal/client" - "github.com/derailed/popeye/internal/dao" - policyv1 "k8s.io/api/policy/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" -) - -// ListPodDisruptionBudgets list all included PodDisruptionBudgets. -func ListPodDisruptionBudgets(ctx context.Context) (map[string]*policyv1.PodDisruptionBudget, error) { - return listAllPodDisruptionBudgets(ctx) -} - -// ListAllPodDisruptionBudgets fetch all PodDisruptionBudgets on the cluster. -func listAllPodDisruptionBudgets(ctx context.Context) (map[string]*policyv1.PodDisruptionBudget, error) { - ll, err := fetchPodDisruptionBudgets(ctx) - if err != nil { - return nil, err - } - pdbs := make(map[string]*policyv1.PodDisruptionBudget, len(ll.Items)) - for i := range ll.Items { - pdbs[metaFQN(ll.Items[i].ObjectMeta)] = &ll.Items[i] - } - - return pdbs, nil -} - -// fetchPodDisruptionBudgets retrieves all PodDisruptionBudgets on the cluster. -func fetchPodDisruptionBudgets(ctx context.Context) (*policyv1.PodDisruptionBudgetList, error) { - f, cfg := mustExtractFactory(ctx), mustExtractConfig(ctx) - if cfg.Flags.StandAlone { - dial, err := f.Client().Dial() - if err != nil { - return nil, err - } - return dial.PolicyV1().PodDisruptionBudgets(f.Client().ActiveNamespace()).List(ctx, metav1.ListOptions{}) - } - - var res dao.Resource - res.Init(f, client.NewGVR("policy/v1/poddisruptionbudgets")) - oo, err := res.List(ctx) - if err != nil { - return nil, err - } - var ll policyv1.PodDisruptionBudgetList - for _, o := range oo { - var pdb policyv1.PodDisruptionBudget - err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &pdb) - if err != nil { - return nil, errors.New("expecting pdb resource") - } - ll.Items = append(ll.Items, pdb) - } - - return &ll, nil -} diff --git a/internal/dag/pod.go b/internal/dag/pod.go deleted file mode 100644 index 7d0592bf..00000000 --- a/internal/dag/pod.go +++ /dev/null @@ -1,65 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package dag - -import ( - "context" - "errors" - - "github.com/derailed/popeye/internal/client" - "github.com/derailed/popeye/internal/dao" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" -) - -// ListPods list all filtered pods. -func ListPods(ctx context.Context) (map[string]*v1.Pod, error) { - return listAllPods(ctx) -} - -// ListAllPods fetch all Pods on the cluster. -func listAllPods(ctx context.Context) (map[string]*v1.Pod, error) { - ll, err := fetchPods(ctx) - if err != nil { - return nil, err - } - pods := make(map[string]*v1.Pod, len(ll.Items)) - for i := range ll.Items { - pods[metaFQN(ll.Items[i].ObjectMeta)] = &ll.Items[i] - } - - return pods, nil -} - -// FetchPods retrieves all Pods on the cluster. -func fetchPods(ctx context.Context) (*v1.PodList, error) { - f, cfg := mustExtractFactory(ctx), mustExtractConfig(ctx) - if cfg.Flags.StandAlone { - dial, err := f.Client().Dial() - if err != nil { - return nil, err - } - return dial.CoreV1().Pods(f.Client().ActiveNamespace()).List(ctx, metav1.ListOptions{}) - } - - var res dao.Resource - res.Init(f, client.NewGVR("v1/pods")) - oo, err := res.List(ctx) - if err != nil { - return nil, err - } - var ll v1.PodList - for _, o := range oo { - var po v1.Pod - err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &po) - if err != nil { - return nil, errors.New("expecting pod resource") - } - ll.Items = append(ll.Items, po) - } - - return &ll, nil -} diff --git a/internal/dag/pod_mx.go b/internal/dag/pod_mx.go deleted file mode 100644 index 12996338..00000000 --- a/internal/dag/pod_mx.go +++ /dev/null @@ -1,39 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package dag - -import ( - "context" - - "github.com/derailed/popeye/internal/client" - "github.com/derailed/popeye/types" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" -) - -// ListPodsMetrics fetch all available Pod metrics on the cluster. -func ListPodsMetrics(c types.Connection) (map[string]*mv1beta1.PodMetrics, error) { - ll, err := fetchPodsMetrics(c) - if err != nil { - return map[string]*mv1beta1.PodMetrics{}, err - } - - pmx := make(map[string]*mv1beta1.PodMetrics, len(ll.Items)) - for i := range ll.Items { - pmx[metaFQN(ll.Items[i].ObjectMeta)] = &ll.Items[i] - } - - return pmx, nil -} - -// FetchPodsMetrics retrieves all Pod metrics on the cluster. -func fetchPodsMetrics(c types.Connection) (*mv1beta1.PodMetricsList, error) { - vc, err := c.MXDial() - if err != nil { - return nil, err - } - ctx, cancel := context.WithTimeout(context.Background(), client.CallTimeout) - defer cancel() - return vc.MetricsV1beta1().PodMetricses(c.ActiveNamespace()).List(ctx, metav1.ListOptions{}) -} diff --git a/internal/dag/pv.go b/internal/dag/pv.go deleted file mode 100644 index ba611ca6..00000000 --- a/internal/dag/pv.go +++ /dev/null @@ -1,65 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package dag - -import ( - "context" - "errors" - - "github.com/derailed/popeye/internal/client" - "github.com/derailed/popeye/internal/dao" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" -) - -// ListPersistentVolumes list all included PersistentVolumes. -func ListPersistentVolumes(ctx context.Context) (map[string]*v1.PersistentVolume, error) { - return listAllPersistentVolumes(ctx) -} - -// ListAllPersistentVolumes fetch all PersistentVolumes on the cluster. -func listAllPersistentVolumes(ctx context.Context) (map[string]*v1.PersistentVolume, error) { - ll, err := fetchPersistentVolumes(ctx) - if err != nil { - return nil, err - } - pvs := make(map[string]*v1.PersistentVolume, len(ll.Items)) - for i := range ll.Items { - pvs[metaFQN(ll.Items[i].ObjectMeta)] = &ll.Items[i] - } - - return pvs, nil -} - -// FetchPersistentVolumes retrieves all PersistentVolumes on the cluster. -func fetchPersistentVolumes(ctx context.Context) (*v1.PersistentVolumeList, error) { - f, cfg := mustExtractFactory(ctx), mustExtractConfig(ctx) - if cfg.Flags.StandAlone { - dial, err := f.Client().Dial() - if err != nil { - return nil, err - } - return dial.CoreV1().PersistentVolumes().List(ctx, metav1.ListOptions{}) - } - - var res dao.Resource - res.Init(f, client.NewGVR("v1/persistentvolumes")) - oo, err := res.List(ctx) - if err != nil { - return nil, err - } - var ll v1.PersistentVolumeList - for _, o := range oo { - var pv v1.PersistentVolume - err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &pv) - if err != nil { - return nil, errors.New("expecting persistentvolume resource") - } - ll.Items = append(ll.Items, pv) - } - - return &ll, nil -} diff --git a/internal/dag/pvc.go b/internal/dag/pvc.go deleted file mode 100644 index d8833d2c..00000000 --- a/internal/dag/pvc.go +++ /dev/null @@ -1,65 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package dag - -import ( - "context" - "errors" - - "github.com/derailed/popeye/internal/client" - "github.com/derailed/popeye/internal/dao" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" -) - -// ListPersistentVolumeClaims list all included PersistentVolumeClaims. -func ListPersistentVolumeClaims(ctx context.Context) (map[string]*v1.PersistentVolumeClaim, error) { - return listAllPersistentVolumeClaims(ctx) -} - -// ListAllPersistentVolumeClaims fetch all PersistentVolumeClaims on the cluster. -func listAllPersistentVolumeClaims(ctx context.Context) (map[string]*v1.PersistentVolumeClaim, error) { - ll, err := fetchPersistentVolumeClaims(ctx) - if err != nil { - return nil, err - } - pvcs := make(map[string]*v1.PersistentVolumeClaim, len(ll.Items)) - for i := range ll.Items { - pvcs[metaFQN(ll.Items[i].ObjectMeta)] = &ll.Items[i] - } - - return pvcs, nil -} - -// FetchPersistentVolumeClaims retrieves all PersistentVolumeClaims on the cluster. -func fetchPersistentVolumeClaims(ctx context.Context) (*v1.PersistentVolumeClaimList, error) { - f, cfg := mustExtractFactory(ctx), mustExtractConfig(ctx) - if cfg.Flags.StandAlone { - dial, err := f.Client().Dial() - if err != nil { - return nil, err - } - return dial.CoreV1().PersistentVolumeClaims(f.Client().ActiveNamespace()).List(ctx, metav1.ListOptions{}) - } - - var res dao.Resource - res.Init(f, client.NewGVR("v1/persistentvolumeclaims")) - oo, err := res.List(ctx) - if err != nil { - return nil, err - } - var ll v1.PersistentVolumeClaimList - for _, o := range oo { - var pvc v1.PersistentVolumeClaim - err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &pvc) - if err != nil { - return nil, errors.New("expecting persistentvolumeclaim resource") - } - ll.Items = append(ll.Items, pvc) - } - - return &ll, nil -} diff --git a/internal/dag/rb.go b/internal/dag/rb.go deleted file mode 100644 index 39b6e510..00000000 --- a/internal/dag/rb.go +++ /dev/null @@ -1,65 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package dag - -import ( - "context" - "errors" - - "github.com/derailed/popeye/internal/client" - "github.com/derailed/popeye/internal/dao" - rbacv1 "k8s.io/api/rbac/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" -) - -// ListRoleBindings list included RoleBindings. -func ListRoleBindings(ctx context.Context) (map[string]*rbacv1.RoleBinding, error) { - return listAllRoleBindings(ctx) -} - -// ListAllRoleBindings fetch all RoleBindings on the cluster. -func listAllRoleBindings(ctx context.Context) (map[string]*rbacv1.RoleBinding, error) { - ll, err := fetchRoleBindings(ctx) - if err != nil { - return nil, err - } - rbs := make(map[string]*rbacv1.RoleBinding, len(ll.Items)) - for i := range ll.Items { - rbs[metaFQN(ll.Items[i].ObjectMeta)] = &ll.Items[i] - } - - return rbs, nil -} - -// FetchRoleBindings retrieves all RoleBindings on the cluster. -func fetchRoleBindings(ctx context.Context) (*rbacv1.RoleBindingList, error) { - f, cfg := mustExtractFactory(ctx), mustExtractConfig(ctx) - if cfg.Flags.StandAlone { - dial, err := f.Client().Dial() - if err != nil { - return nil, err - } - return dial.RbacV1().RoleBindings(f.Client().ActiveNamespace()).List(ctx, metav1.ListOptions{}) - } - - var res dao.Resource - res.Init(f, client.NewGVR("rbac.authorization.k8s.io/v1/rolebindings")) - oo, err := res.List(ctx) - if err != nil { - return nil, err - } - var ll rbacv1.RoleBindingList - for _, o := range oo { - var rb rbacv1.RoleBinding - err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &rb) - if err != nil { - return nil, errors.New("expecting rolebinding resource") - } - ll.Items = append(ll.Items, rb) - } - - return &ll, nil -} diff --git a/internal/dag/role.go b/internal/dag/role.go deleted file mode 100644 index d9ca9b3e..00000000 --- a/internal/dag/role.go +++ /dev/null @@ -1,65 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package dag - -import ( - "context" - "errors" - - "github.com/derailed/popeye/internal/client" - "github.com/derailed/popeye/internal/dao" - rbacv1 "k8s.io/api/rbac/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" -) - -// ListRoles list included Roles. -func ListRoles(ctx context.Context) (map[string]*rbacv1.Role, error) { - return listAllRoles(ctx) -} - -// ListAllRoles fetch all Roles on the cluster. -func listAllRoles(ctx context.Context) (map[string]*rbacv1.Role, error) { - ll, err := fetchRoles(ctx) - if err != nil { - return nil, err - } - ros := make(map[string]*rbacv1.Role, len(ll.Items)) - for i := range ll.Items { - ros[metaFQN(ll.Items[i].ObjectMeta)] = &ll.Items[i] - } - - return ros, nil -} - -// FetchRoleBindings retrieves all RoleBindings on the cluster. -func fetchRoles(ctx context.Context) (*rbacv1.RoleList, error) { - f, cfg := mustExtractFactory(ctx), mustExtractConfig(ctx) - if cfg.Flags.StandAlone { - dial, err := f.Client().Dial() - if err != nil { - return nil, err - } - return dial.RbacV1().Roles(f.Client().ActiveNamespace()).List(ctx, metav1.ListOptions{}) - } - - var res dao.Resource - res.Init(f, client.NewGVR("rbac.authorization.k8s.io/v1/roles")) - oo, err := res.List(ctx) - if err != nil { - return nil, err - } - var ll rbacv1.RoleList - for _, o := range oo { - var ro rbacv1.Role - err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &ro) - if err != nil { - return nil, errors.New("expecting role resource") - } - ll.Items = append(ll.Items, ro) - } - - return &ll, nil -} diff --git a/internal/dag/rs.go b/internal/dag/rs.go deleted file mode 100644 index 477ff2e0..00000000 --- a/internal/dag/rs.go +++ /dev/null @@ -1,65 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package dag - -import ( - "context" - "errors" - - "github.com/derailed/popeye/internal/client" - "github.com/derailed/popeye/internal/dao" - appsv1 "k8s.io/api/apps/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" -) - -// ListReplicaSets list all included ReplicaSets. -func ListReplicaSets(ctx context.Context) (map[string]*appsv1.ReplicaSet, error) { - return listAllReplicaSets(ctx) -} - -// ListAllReplicaSets fetch all ReplicaSets on the cluster. -func listAllReplicaSets(ctx context.Context) (map[string]*appsv1.ReplicaSet, error) { - ll, err := fetchReplicaSets(ctx) - if err != nil { - return nil, err - } - rss := make(map[string]*appsv1.ReplicaSet, len(ll.Items)) - for i := range ll.Items { - rss[metaFQN(ll.Items[i].ObjectMeta)] = &ll.Items[i] - } - - return rss, nil -} - -// FetchReplicaSets retrieves all ReplicaSets on the cluster. -func fetchReplicaSets(ctx context.Context) (*appsv1.ReplicaSetList, error) { - f, cfg := mustExtractFactory(ctx), mustExtractConfig(ctx) - if cfg.Flags.StandAlone { - dial, err := f.Client().Dial() - if err != nil { - return nil, err - } - return dial.AppsV1().ReplicaSets(f.Client().ActiveNamespace()).List(ctx, metav1.ListOptions{}) - } - - var res dao.Resource - res.Init(f, client.NewGVR("apps/v1/replicasets")) - oo, err := res.List(ctx) - if err != nil { - return nil, err - } - var ll appsv1.ReplicaSetList - for _, o := range oo { - var rs appsv1.ReplicaSet - err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &rs) - if err != nil { - return nil, errors.New("expecting replicaset resource") - } - ll.Items = append(ll.Items, rs) - } - - return &ll, nil -} diff --git a/internal/dag/sa.go b/internal/dag/sa.go deleted file mode 100644 index 5965d941..00000000 --- a/internal/dag/sa.go +++ /dev/null @@ -1,66 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package dag - -import ( - "context" - "errors" - - "github.com/derailed/popeye/internal/client" - "github.com/derailed/popeye/internal/dao" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" -) - -// ListServiceAccounts list included ServiceAccounts. -func ListServiceAccounts(ctx context.Context) (map[string]*v1.ServiceAccount, error) { - return listAllServiceAccounts(ctx) -} - -// ListAllServiceAccounts fetch all ServiceAccounts on the cluster. -func listAllServiceAccounts(ctx context.Context) (map[string]*v1.ServiceAccount, error) { - ll, err := fetchServiceAccounts(ctx) - if err != nil { - return nil, err - } - sas := make(map[string]*v1.ServiceAccount, len(ll.Items)) - for i := range ll.Items { - sas[metaFQN(ll.Items[i].ObjectMeta)] = &ll.Items[i] - } - - return sas, nil -} - -// FetchServiceAccounts retrieves all ServiceAccounts on the cluster. -func fetchServiceAccounts(ctx context.Context) (*v1.ServiceAccountList, error) { - f, cfg := mustExtractFactory(ctx), mustExtractConfig(ctx) - if cfg.Flags.StandAlone { - dial, err := f.Client().Dial() - if err != nil { - return nil, err - } - return dial.CoreV1().ServiceAccounts(f.Client().ActiveNamespace()).List(ctx, metav1.ListOptions{}) - } - - var res dao.Resource - res.Init(f, client.NewGVR("v1/serviceaccounts")) - oo, err := res.List(ctx) - if err != nil { - return nil, err - } - var ll v1.ServiceAccountList - for _, o := range oo { - var sa v1.ServiceAccount - err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &sa) - if err != nil { - return nil, errors.New("expecting serviceaccount resource") - } - ll.Items = append(ll.Items, sa) - } - - return &ll, nil - -} diff --git a/internal/dag/sec.go b/internal/dag/sec.go deleted file mode 100644 index 58643a42..00000000 --- a/internal/dag/sec.go +++ /dev/null @@ -1,65 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package dag - -import ( - "context" - "errors" - - "github.com/derailed/popeye/internal/client" - "github.com/derailed/popeye/internal/dao" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" -) - -// ListSecrets list all included Secrets. -func ListSecrets(ctx context.Context) (map[string]*v1.Secret, error) { - return listAllSecrets(ctx) -} - -// ListAllSecrets fetch all Secrets on the cluster. -func listAllSecrets(ctx context.Context) (map[string]*v1.Secret, error) { - ll, err := fetchSecrets(ctx) - if err != nil { - return nil, err - } - secs := make(map[string]*v1.Secret, len(ll.Items)) - for i := range ll.Items { - secs[metaFQN(ll.Items[i].ObjectMeta)] = &ll.Items[i] - } - - return secs, nil -} - -// FetchSecrets retrieves all Secrets on the cluster. -func fetchSecrets(ctx context.Context) (*v1.SecretList, error) { - f, cfg := mustExtractFactory(ctx), mustExtractConfig(ctx) - if cfg.Flags.StandAlone { - dial, err := f.Client().Dial() - if err != nil { - return nil, err - } - return dial.CoreV1().Secrets(f.Client().ActiveNamespace()).List(ctx, metav1.ListOptions{}) - } - - var res dao.Resource - res.Init(f, client.NewGVR("v1/secrets")) - oo, err := res.List(ctx) - if err != nil { - return nil, err - } - var ll v1.SecretList - for _, o := range oo { - var sec v1.Secret - err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &sec) - if err != nil { - return nil, errors.New("expecting secret resource") - } - ll.Items = append(ll.Items, sec) - } - - return &ll, nil -} diff --git a/internal/dag/sts.go b/internal/dag/sts.go deleted file mode 100644 index 21a811e7..00000000 --- a/internal/dag/sts.go +++ /dev/null @@ -1,65 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package dag - -import ( - "context" - "errors" - - "github.com/derailed/popeye/internal/client" - "github.com/derailed/popeye/internal/dao" - appsv1 "k8s.io/api/apps/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" -) - -// ListStatefulSets list available StatefulSets. -func ListStatefulSets(ctx context.Context) (map[string]*appsv1.StatefulSet, error) { - return listAllStatefulSets(ctx) -} - -// ListAllStatefulSets fetch all StatefulSets on the cluster. -func listAllStatefulSets(ctx context.Context) (map[string]*appsv1.StatefulSet, error) { - ll, err := fetchStatefulSets(ctx) - if err != nil { - return nil, err - } - sts := make(map[string]*appsv1.StatefulSet, len(ll.Items)) - for i := range ll.Items { - sts[metaFQN(ll.Items[i].ObjectMeta)] = &ll.Items[i] - } - - return sts, nil -} - -// FetchStatefulSets retrieves all StatefulSets on the cluster. -func fetchStatefulSets(ctx context.Context) (*appsv1.StatefulSetList, error) { - f, cfg := mustExtractFactory(ctx), mustExtractConfig(ctx) - if cfg.Flags.StandAlone { - dial, err := f.Client().Dial() - if err != nil { - return nil, err - } - return dial.AppsV1().StatefulSets(f.Client().ActiveNamespace()).List(ctx, metav1.ListOptions{}) - } - - var res dao.Resource - res.Init(f, client.NewGVR("apps/v1/statefulsets")) - oo, err := res.List(ctx) - if err != nil { - return nil, err - } - var ll appsv1.StatefulSetList - for _, o := range oo { - var sts appsv1.StatefulSet - err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &sts) - if err != nil { - return nil, errors.New("expecting sts resource") - } - ll.Items = append(ll.Items, sts) - } - - return &ll, nil -} diff --git a/internal/dag/svc.go b/internal/dag/svc.go deleted file mode 100644 index aa8d1f3d..00000000 --- a/internal/dag/svc.go +++ /dev/null @@ -1,65 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package dag - -import ( - "context" - "errors" - - "github.com/derailed/popeye/internal/client" - "github.com/derailed/popeye/internal/dao" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" -) - -// ListServices list all included Services. -func ListServices(ctx context.Context) (map[string]*v1.Service, error) { - return listAllServices(ctx) -} - -// ListAllServices fetch all Services on the cluster. -func listAllServices(ctx context.Context) (map[string]*v1.Service, error) { - ll, err := fetchServices(ctx) - if err != nil { - return nil, err - } - svcs := make(map[string]*v1.Service, len(ll.Items)) - for i := range ll.Items { - svcs[metaFQN(ll.Items[i].ObjectMeta)] = &ll.Items[i] - } - - return svcs, nil -} - -// FetchServices retrieves all Services on the cluster. -func fetchServices(ctx context.Context) (*v1.ServiceList, error) { - f, cfg := mustExtractFactory(ctx), mustExtractConfig(ctx) - if cfg.Flags.StandAlone { - dial, err := f.Client().Dial() - if err != nil { - return nil, err - } - return dial.CoreV1().Services(f.Client().ActiveNamespace()).List(ctx, metav1.ListOptions{}) - } - - var res dao.Resource - res.Init(f, client.NewGVR("v1/services")) - oo, err := res.List(ctx) - if err != nil { - return nil, err - } - var ll v1.ServiceList - for _, o := range oo { - var svc v1.Service - err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &svc) - if err != nil { - return nil, errors.New("expecting service resource") - } - ll.Items = append(ll.Items, svc) - } - - return &ll, nil -} diff --git a/internal/dao/ev.go b/internal/dao/ev.go new file mode 100644 index 00000000..79b38b45 --- /dev/null +++ b/internal/dao/ev.go @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package dao + +import ( + "context" + "fmt" + "strings" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/client" + "github.com/derailed/popeye/types" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + WarnEvt = "Warning" +) + +type EventInfo struct { + Kind string + Reason string + Message string + Count int64 +} + +func (e EventInfo) IsIssue() bool { + return e.Kind == WarnEvt || !strings.Contains(e.Reason, "Success") +} + +type EventInfos []EventInfo + +func (ee EventInfos) Issues() []string { + if len(ee) == 0 { + return nil + } + ss := make([]string, 0, len(ee)) + for _, e := range ee { + if e.IsIssue() { + ss = append(ss, e.Message) + } + } + + return ss +} + +type Event struct { + Table +} + +func EventsFor(ctx context.Context, gvr types.GVR, level, kind, fqn string) (EventInfos, error) { + ns, n := client.Namespaced(fqn) + f, ok := ctx.Value(internal.KeyFactory).(types.Factory) + if !ok { + return nil, nil + } + + ss := make([]string, 0, 2) + if level != "" { + ss = append(ss, fmt.Sprintf("type=%s", level)) + } + if kind != "" { + ss = append(ss, fmt.Sprintf("involvedObject.name=%s,involvedObject.kind=%s", n, kind)) + } + ctx = context.WithValue(ctx, internal.KeyFields, strings.Join(ss, ",")) + + var t Table + t.Init(f, types.NewGVR("v1/events")) + + oo, err := t.List(ctx, ns) + if err != nil { + return nil, err + } + if len(oo) == 0 { + return nil, fmt.Errorf("No events found %s", fqn) + } + + tt := oo[0].(*metav1.Table) + ee := make(EventInfos, 0, len(tt.Rows)) + for _, r := range tt.Rows { + ee = append(ee, EventInfo{ + Kind: r.Cells[1].(string), + Reason: r.Cells[2].(string), + Message: r.Cells[6].(string), + Count: r.Cells[8].(int64), + }) + } + + return ee, nil +} diff --git a/internal/dao/generic.go b/internal/dao/generic.go index 79ed94be..e450065c 100644 --- a/internal/dao/generic.go +++ b/internal/dao/generic.go @@ -42,6 +42,7 @@ func (g *Generic) List(ctx context.Context) ([]runtime.Object, error) { if err != nil { return nil, err } + if client.IsClusterScoped(ns) { ll, err = dial.List(ctx, metav1.ListOptions{LabelSelector: labelSel}) } else { @@ -80,5 +81,6 @@ func (g *Generic) dynClient() (dynamic.NamespaceableResourceInterface, error) { if err != nil { return nil, err } + return dial.Resource(g.gvr.GVR()), nil } diff --git a/internal/dao/non_resource.go b/internal/dao/non_resource.go index b8b302f3..7ba1d7ae 100644 --- a/internal/dao/non_resource.go +++ b/internal/dao/non_resource.go @@ -7,7 +7,6 @@ import ( "context" "fmt" - "github.com/derailed/popeye/internal/client" "github.com/derailed/popeye/types" "k8s.io/apimachinery/pkg/runtime" ) @@ -16,11 +15,11 @@ import ( type NonResource struct { types.Factory - gvr client.GVR + gvr types.GVR } // Init initializes the resource. -func (n *NonResource) Init(f types.Factory, gvr client.GVR) { +func (n *NonResource) Init(f types.Factory, gvr types.GVR) { n.Factory, n.gvr = f, gvr } diff --git a/internal/dao/resource.go b/internal/dao/resource.go index ed3fa295..9e6394da 100644 --- a/internal/dao/resource.go +++ b/internal/dao/resource.go @@ -8,6 +8,7 @@ import ( "fmt" "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/client" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" ) @@ -30,11 +31,14 @@ func (r *Resource) List(ctx context.Context) ([]runtime.Object, error) { if !ok { panic(fmt.Sprintf("BOOM no namespace in context %s", r.gvr)) } + if r.gvr == internal.Glossary[internal.NS] { + ns = client.AllNamespaces + } - return r.Factory.List(r.gvr.String(), ns, true, lsel) + return r.Factory.List(r.gvr, ns, true, lsel) } // Get returns a resource instance if found, else an error. func (r *Resource) Get(_ context.Context, path string) (runtime.Object, error) { - return r.Factory.Get(r.gvr.String(), path, true, labels.Everything()) + return r.Factory.Get(r.gvr, path, true, labels.Everything()) } diff --git a/internal/dao/table.go b/internal/dao/table.go new file mode 100644 index 00000000..e4b8dbe9 --- /dev/null +++ b/internal/dao/table.go @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package dao + +import ( + "context" + "fmt" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/client" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/serializer" + "k8s.io/client-go/rest" +) + +// Table retrieves K8s resources as tabular data. +type Table struct { + Generic +} + +// Get returns a given resource. +func (t *Table) Get(ctx context.Context, path string) (runtime.Object, error) { + a := fmt.Sprintf(gvFmt, metav1.SchemeGroupVersion.Version, metav1.GroupName) + _, codec := t.codec() + + c, err := t.getClient() + if err != nil { + return nil, err + } + ns, n := client.Namespaced(path) + req := c.Get(). + SetHeader("Accept", a). + Name(n). + Resource(t.gvr.R()). + VersionedParams(&metav1.TableOptions{}, codec) + if ns != client.ClusterScope { + req = req.Namespace(ns) + } + + return req.Do(ctx).Get() +} + +// List all Resources in a given namespace. +func (t *Table) List(ctx context.Context, ns string) ([]runtime.Object, error) { + labelSel, _ := ctx.Value(internal.KeyLabels).(string) + fieldSel, _ := ctx.Value(internal.KeyFields).(string) + + a := fmt.Sprintf(gvFmt, metav1.SchemeGroupVersion.Version, metav1.GroupName) + _, codec := t.codec() + + c, err := t.getClient() + if err != nil { + return nil, err + } + o, err := c.Get(). + SetHeader("Accept", a). + Namespace(ns). + Resource(t.gvr.R()). + VersionedParams(&metav1.ListOptions{FieldSelector: fieldSel, LabelSelector: labelSel}, codec). + Do(ctx).Get() + if err != nil { + return nil, err + } + + return []runtime.Object{o}, nil +} + +// ---------------------------------------------------------------------------- +// Helpers... + +const gvFmt = "application/json;as=Table;v=%s;g=%s, application/json" + +func (t *Table) getClient() (*rest.RESTClient, error) { + cfg, err := t.Client().RestConfig() + if err != nil { + return nil, err + } + gv := t.gvr.GV() + cfg.GroupVersion = &gv + cfg.APIPath = "/apis" + if t.gvr.G() == "" { + cfg.APIPath = "/api" + } + codec, _ := t.codec() + cfg.NegotiatedSerializer = codec.WithoutConversion() + + crRestClient, err := rest.RESTClientFor(cfg) + if err != nil { + return nil, err + } + + return crRestClient, nil +} + +func (t *Table) codec() (serializer.CodecFactory, runtime.ParameterCodec) { + scheme := runtime.NewScheme() + gv := t.gvr.GV() + metav1.AddToGroupVersion(scheme, gv) + scheme.AddKnownTypes(gv, &metav1.Table{}, &metav1.TableOptions{IncludeObject: v1.IncludeObject}) + scheme.AddKnownTypes(metav1.SchemeGroupVersion, &metav1.Table{}, &metav1.TableOptions{IncludeObject: v1.IncludeObject}) + + return serializer.NewCodecFactory(scheme), runtime.NewParameterCodec(scheme) +} diff --git a/internal/dao/types.go b/internal/dao/types.go index 2872eef0..059d30e6 100644 --- a/internal/dao/types.go +++ b/internal/dao/types.go @@ -6,14 +6,13 @@ package dao import ( "context" - "github.com/derailed/popeye/internal/client" "github.com/derailed/popeye/types" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" ) // ResourceMetas represents a collection of resource metadata. -type ResourceMetas map[client.GVR]metav1.APIResource +type ResourceMetas map[types.GVR]metav1.APIResource // Getter represents a resource getter. type Getter interface { @@ -33,7 +32,7 @@ type Accessor interface { Getter // Init the resource with a factory object. - Init(types.Factory, client.GVR) + Init(types.Factory, types.GVR) // GVR returns a gvr a string. GVR() string diff --git a/internal/db/db.go b/internal/db/db.go new file mode 100644 index 00000000..03bdef59 --- /dev/null +++ b/internal/db/db.go @@ -0,0 +1,404 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package db + +import ( + "fmt" + + "github.com/rs/zerolog/log" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/client" + "github.com/derailed/popeye/types" + "github.com/hashicorp/go-memdb" + batchv1 "k8s.io/api/batch/v1" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" +) + +type DB struct { + *memdb.MemDB +} + +func NewDB(db *memdb.MemDB) *DB { + return &DB{ + MemDB: db, + } +} + +func (db *DB) ITFor(gvr types.GVR) (*memdb.Txn, memdb.ResultIterator, error) { + if gvr == types.BlankGVR { + panic(fmt.Errorf("invalid table")) + } + txn := db.Txn(false) + it, err := txn.Get(gvr.String(), "id") + if err != nil { + return nil, nil, err + } + + return txn, it, nil +} + +func (db *DB) MustITForNS(gvr types.GVR, ns string) (*memdb.Txn, memdb.ResultIterator) { + txn := db.Txn(false) + it, err := txn.Get(gvr.String(), "ns", ns) + if err != nil { + panic(fmt.Errorf("Db get failed for %q: %w", gvr, err)) + } + + return txn, it +} + +func (db *DB) MustITFor(gvr types.GVR) (*memdb.Txn, memdb.ResultIterator) { + txn := db.Txn(false) + it, err := txn.Get(gvr.String(), "id") + if err != nil { + panic(fmt.Errorf("Db get failed for %q: %w", gvr, err)) + } + + return txn, it +} + +func (db *DB) ListNodes() (map[string]*v1.Node, error) { + txn, it := db.MustITFor(internal.Glossary[internal.NO]) + defer txn.Abort() + + mm := make(map[string]*v1.Node) + for o := it.Next(); o != nil; o = it.Next() { + no, ok := o.(*v1.Node) + if !ok { + return nil, fmt.Errorf("expecting node but got %T", o) + } + mm[no.Name] = no + } + + return mm, nil +} + +func (db *DB) FindPMX(fqn string) (*mv1beta1.PodMetrics, error) { + gvr := internal.Glossary[internal.PMX] + if gvr == types.BlankGVR { + return nil, nil + } + txn := db.Txn(false) + defer txn.Abort() + o, err := txn.First(gvr.String(), "id", fqn) + if err != nil || o == nil { + return nil, fmt.Errorf("object not found: %q", fqn) + } + + pmx, ok := o.(*mv1beta1.PodMetrics) + if !ok { + return nil, fmt.Errorf("expecting PodMetrics but got %T", o) + } + + return pmx, nil +} + +func (db *DB) FindNMX(fqn string) (*mv1beta1.NodeMetrics, error) { + gvr := internal.Glossary[internal.NMX] + if gvr == types.BlankGVR { + return nil, nil + } + txn := db.Txn(false) + defer txn.Abort() + o, err := txn.First(gvr.String(), "id", fqn) + if err != nil || o == nil { + return nil, fmt.Errorf("object not found: %q", fqn) + } + + nmx, ok := o.(*mv1beta1.NodeMetrics) + if !ok { + return nil, fmt.Errorf("expecting NodeMetrics but got %T", o) + } + + return nmx, nil +} + +func (db *DB) ListNMX() ([]*mv1beta1.NodeMetrics, error) { + gvr := internal.Glossary[internal.NMX] + if gvr == types.BlankGVR { + return nil, nil + } + txn, it, err := db.ITFor(gvr) + if err != nil { + return nil, err + } + defer txn.Abort() + + mm := make([]*mv1beta1.NodeMetrics, 0, 10) + for o := it.Next(); o != nil; o = it.Next() { + nmx, ok := o.(*mv1beta1.NodeMetrics) + if !ok { + return nil, fmt.Errorf("expecting NodeMetrics but got %T", o) + } + mm = append(mm, nmx) + } + + return mm, nil +} + +func (db *DB) Find(kind types.GVR, fqn string) (any, error) { + txn := db.Txn(false) + defer txn.Abort() + o, err := txn.First(kind.String(), "id", fqn) + if err != nil || o == nil { + return nil, fmt.Errorf("object not found: %q", fqn) + } + + return o, nil +} + +func (db *DB) FindPod(ns string, sel map[string]string) (*v1.Pod, error) { + txn := db.Txn(false) + defer txn.Abort() + txn, it := db.MustITFor(internal.Glossary[internal.PO]) + defer txn.Abort() + for o := it.Next(); o != nil; o = it.Next() { + po, ok := o.(*v1.Pod) + if !ok { + return nil, fmt.Errorf("expecting pod") + } + if po.Namespace != ns { + continue + } + if MatchLabels(po.Labels, sel) { + return po, nil + } + } + + return nil, fmt.Errorf("no pods match selector: %v", sel) +} + +func (db *DB) FindJobs(fqn string) ([]*batchv1.Job, error) { + txn := db.Txn(false) + defer txn.Abort() + txn, it := db.MustITFor(internal.Glossary[internal.JOB]) + defer txn.Abort() + + cns, cn := client.Namespaced(fqn) + jj := make([]*batchv1.Job, 0, 10) + for o := it.Next(); o != nil; o = it.Next() { + jo, ok := o.(*batchv1.Job) + if !ok { + return nil, fmt.Errorf("expecting job") + } + if jo.Namespace != cns { + continue + } + for _, o := range jo.OwnerReferences { + if o.Controller == nil && !*o.Controller { + continue + } + if o.Name == cn { + jj = append(jj, jo) + } + } + } + + return jj, nil +} + +func (db *DB) FindPods(ns string, sel map[string]string) ([]*v1.Pod, error) { + txn := db.Txn(false) + defer txn.Abort() + txn, it := db.MustITFor(internal.Glossary[internal.PO]) + defer txn.Abort() + pp := make([]*v1.Pod, 0, 10) + for o := it.Next(); o != nil; o = it.Next() { + po, ok := o.(*v1.Pod) + if !ok { + return nil, fmt.Errorf("expecting pod but got %T", o) + } + if po.Namespace != ns { + continue + } + if MatchLabels(po.Labels, sel) { + pp = append(pp, po) + } + } + + return pp, nil +} + +func (db *DB) FindPodsBySel(ns string, sel *metav1.LabelSelector) ([]*v1.Pod, error) { + if sel == nil || sel.Size() == 0 { + return nil, fmt.Errorf("no pod selector given") + } + + txn := db.Txn(false) + defer txn.Abort() + txn, it := db.MustITFor(internal.Glossary[internal.PO]) + defer txn.Abort() + pp := make([]*v1.Pod, 0, 10) + for o := it.Next(); o != nil; o = it.Next() { + po, ok := o.(*v1.Pod) + if !ok { + return nil, fmt.Errorf("expecting pod") + } + if po.Namespace != ns { + continue + } + if MatchSelector(po.Labels, sel) { + pp = append(pp, po) + } + } + + return pp, nil +} + +func (db *DB) FindNSBySel(sel *metav1.LabelSelector) ([]*v1.Namespace, error) { + if sel == nil || sel.Size() == 0 { + return nil, nil + } + + txn := db.Txn(false) + defer txn.Abort() + txn, it := db.MustITFor(internal.Glossary[internal.NS]) + defer txn.Abort() + nss := make([]*v1.Namespace, 0, 10) + for o := it.Next(); o != nil; o = it.Next() { + ns, ok := o.(*v1.Namespace) + if !ok { + return nil, fmt.Errorf("expecting namespace") + } + if MatchSelector(ns.Labels, sel) { + nss = append(nss, ns) + } + } + + return nss, nil +} + +func (db *DB) DumpNS() error { + txn := db.Txn(false) + defer txn.Abort() + it, err := txn.Get(internal.Glossary[internal.NS].String(), "id") + if err != nil { + return err + } + for o := it.Next(); o != nil; o = it.Next() { + ns, _ := o.(*v1.Namespace) + log.Debug().Msgf("NS %q", ns.Name) + } + + return nil +} + +func (db *DB) FindNS(ns string) (*v1.Namespace, error) { + txn := db.Txn(false) + defer txn.Abort() + o, err := txn.First(internal.Glossary[internal.NS].String(), "ns", ns) + if err != nil { + return nil, err + } + nss, ok := o.(*v1.Namespace) + if !ok { + return nil, fmt.Errorf("expecting namespace but got %s", o) + } + + return nss, nil +} + +func (db *DB) FindNSNameBySel(sel *metav1.LabelSelector) ([]string, error) { + if sel == nil || sel.Size() == 0 { + return nil, nil + } + + txn := db.Txn(false) + defer txn.Abort() + txn, it := db.MustITFor(internal.Glossary[internal.NS]) + defer txn.Abort() + nss := make([]string, 0, 10) + for o := it.Next(); o != nil; o = it.Next() { + ns, ok := o.(*v1.Namespace) + if !ok { + return nil, fmt.Errorf("expecting namespace but got %s", o) + } + if MatchSelector(ns.Labels, sel) { + nss = append(nss, ns.Name) + } + } + + return nss, nil +} + +// Helpers... + +// MatchSelector check if pod labels match a selector. +func MatchSelector(labels map[string]string, sel *metav1.LabelSelector) bool { + if len(labels) == 0 || sel.Size() == 0 { + return false + } + if MatchLabels(labels, sel.MatchLabels) { + return true + } + + return matchExp(labels, sel.MatchExpressions) +} + +func matchExp(labels map[string]string, ee []metav1.LabelSelectorRequirement) bool { + for _, e := range ee { + if matchSel(labels, e) { + return true + } + } + + return false +} + +func matchSel(labels map[string]string, e metav1.LabelSelectorRequirement) bool { + _, ok := labels[e.Key] + if e.Operator == metav1.LabelSelectorOpDoesNotExist && !ok { + return true + } + if !ok { + return false + } + + switch e.Operator { + case metav1.LabelSelectorOpNotIn: + for _, v := range e.Values { + if v1, ok := labels[e.Key]; ok && v1 == v { + return false + } + } + return true + case metav1.LabelSelectorOpIn: + for _, v := range e.Values { + if v == labels[e.Key] { + return true + } + } + return false + case metav1.LabelSelectorOpExists: + return true + } + + return false +} + +// MatchLabels check if pod labels match a selector. +func MatchLabels(labels, sel map[string]string) bool { + if len(sel) == 0 { + return false + } + + for k, v := range sel { + if v1, ok := labels[k]; !ok || v1 != v { + return false + } + } + + return true +} + +func (db *DB) Exists(kind types.GVR, fqn string) bool { + txn := db.Txn(false) + defer txn.Abort() + o, err := txn.First(kind.String(), "id", fqn) + + return err == nil && o != nil +} diff --git a/internal/db/loader.go b/internal/db/loader.go new file mode 100644 index 00000000..7aa47124 --- /dev/null +++ b/internal/db/loader.go @@ -0,0 +1,230 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package db + +import ( + "context" + "fmt" + "strings" + "sync" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/client" + "github.com/derailed/popeye/internal/dao" + "github.com/derailed/popeye/types" + "github.com/rs/zerolog/log" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" +) + +type CastFn[T any] func(o runtime.Object) (*T, error) + +type Loader struct { + DB *DB + loaded map[types.GVR]struct{} + mx sync.RWMutex +} + +func NewLoader(db *DB) *Loader { + l := Loader{ + DB: db, + loaded: make(map[types.GVR]struct{}), + } + + return &l +} + +func (l *Loader) isLoaded(gvr types.GVR) bool { + l.mx.RLock() + defer l.mx.RUnlock() + + _, ok := l.loaded[gvr] + + return ok +} + +func (l *Loader) setLoaded(gvr types.GVR) { + l.mx.Lock() + defer l.mx.Unlock() + + l.loaded[gvr] = struct{}{} +} + +// LoadResource loads resource and save to db. +func LoadResource[T metav1.ObjectMetaAccessor](ctx context.Context, l *Loader, gvr types.GVR) error { + if l.isLoaded(gvr) || gvr == types.BlankGVR { + return nil + } + + oo, err := loadResource(ctx, gvr) + if err != nil { + return err + } + if err = Save[T](ctx, l.DB, gvr, oo); err != nil { + return err + } + l.setLoaded(gvr) + + return nil +} + +func Cast[T any](o runtime.Object) (T, error) { + var r T + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &r); err != nil { + return r, fmt.Errorf("expecting %T resource but got %T: %w", r, o, err) + } + + return r, nil +} + +func Save[T metav1.ObjectMetaAccessor](ctx context.Context, dba *DB, gvr types.GVR, oo []runtime.Object) error { + txn := dba.Txn(true) + defer txn.Commit() + + for _, o := range oo { + u, err := Cast[T](o) + if err != nil { + return err + } + if err := txn.Insert(gvr.String(), u); err != nil { + return err + } + } + + return nil +} + +func (l *Loader) LoadPodMX(ctx context.Context) error { + pmxGVR := internal.Glossary[internal.PMX] + if l.isLoaded(pmxGVR) { + return nil + } + + c := mustExtractFactory(ctx).Client() + + log.Debug().Msg("PRELOAD PMX") + ll, err := l.fetchPodsMetrics(c) + if err != nil { + return err + } + txn := l.DB.Txn(true) + defer txn.Commit() + for _, l := range ll.Items { + if err := txn.Insert(pmxGVR.String(), &l); err != nil { + return err + } + } + l.setLoaded(pmxGVR) + + return nil +} + +func (l *Loader) LoadNodeMX(ctx context.Context) error { + c := mustExtractFactory(ctx).Client() + if !c.HasMetrics() { + return nil + } + + nmxGVR := internal.Glossary[internal.NMX] + if l.isLoaded(nmxGVR) { + return nil + } + log.Debug().Msg("PRELOAD NMX") + ll, err := l.fetchNodesMetrics(c) + if err != nil { + return err + } + txn := l.DB.Txn(true) + defer txn.Commit() + for _, l := range ll.Items { + if err := txn.Insert(nmxGVR.String(), &l); err != nil { + return err + } + } + l.setLoaded(nmxGVR) + + return nil +} + +func (l *Loader) fetchPodsMetrics(c types.Connection) (*mv1beta1.PodMetricsList, error) { + vc, err := c.MXDial() + if err != nil { + return nil, err + } + ctx, cancel := context.WithTimeout(context.Background(), client.CallTimeout) + defer cancel() + return vc.MetricsV1beta1().PodMetricses(c.ActiveNamespace()).List(ctx, metav1.ListOptions{}) +} + +func (l *Loader) fetchNodesMetrics(c types.Connection) (*mv1beta1.NodeMetricsList, error) { + vc, err := c.MXDial() + if err != nil { + return nil, err + } + + ctx, cancel := context.WithTimeout(context.Background(), client.CallTimeout) + defer cancel() + return vc.MetricsV1beta1().NodeMetricses().List(ctx, metav1.ListOptions{}) +} + +func loadResource(ctx context.Context, gvr types.GVR) ([]runtime.Object, error) { + f := mustExtractFactory(ctx) + if strings.Contains(gvr.String(), "metrics") { + if !f.Client().HasMetrics() { + return nil, nil + } + } + if gvr.IsMetricsRes() { + var res dao.Generic + res.Init(f, gvr) + return res.List(ctx) + } + + var res dao.Resource + res.Init(f, gvr) + + return res.List(ctx) +} + +func (l *Loader) LoadGeneric(ctx context.Context, gvr types.GVR) error { + if l.isLoaded(gvr) { + return nil + } + + oo, err := l.fetchGeneric(ctx, gvr) + if err != nil { + return err + } + txn := l.DB.Txn(true) + defer txn.Commit() + for _, o := range oo { + if err := txn.Insert(gvr.String(), o); err != nil { + return err + } + } + l.setLoaded(gvr) + + return nil +} + +func (l *Loader) fetchGeneric(ctx context.Context, gvr types.GVR) ([]runtime.Object, error) { + f := mustExtractFactory(ctx) + var res dao.Resource + res.Init(f, types.NewGVR(gvr.String())) + + return res.List(ctx) +} + +// Helpers... + +func mustExtractFactory(ctx context.Context) types.Factory { + f, ok := ctx.Value(internal.KeyFactory).(types.Factory) + if !ok { + panic("expecting factory in context") + } + + return f +} diff --git a/internal/db/schema/indexer.go b/internal/db/schema/indexer.go new file mode 100644 index 00000000..bb3d8cde --- /dev/null +++ b/internal/db/schema/indexer.go @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package schema + +import ( + "fmt" + + "github.com/derailed/popeye/internal/client" + "github.com/hashicorp/go-memdb" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +type MetaAccessor interface { + GetNamespace() string + GetName() string +} + +var _ MetaAccessor = (*unstructured.Unstructured)(nil) + +type fqnIndexer struct{} + +func (fqnIndexer) FromArgs(args ...any) ([]byte, error) { + if len(args) != 1 { + return nil, fmt.Errorf("must provide only a single argument") + } + + return []byte(args[0].(string)), nil +} + +func (fqnIndexer) FromObject(o any) (bool, []byte, error) { + m, ok := o.(MetaAccessor) + if !ok { + return ok, nil, fmt.Errorf("indexer expected MetaAccessor but got %T", o) + } + + return true, []byte(client.FQN(m.GetNamespace(), m.GetName())), nil +} + +type nsIndexer struct{} + +func (nsIndexer) FromArgs(args ...any) ([]byte, error) { + if len(args) != 1 { + return nil, fmt.Errorf("must provide only a single argument") + } + + return []byte(args[0].(string)), nil +} + +func (nsIndexer) FromObject(o any) (bool, []byte, error) { + m, ok := o.(MetaAccessor) + if !ok { + return ok, nil, fmt.Errorf("indexer expected MetaAccessor but got %T", o) + } + + return true, []byte(m.GetNamespace()), nil +} + +func indexFor(table string) *memdb.TableSchema { + return &memdb.TableSchema{ + Name: table, + Indexes: map[string]*memdb.IndexSchema{ + "id": { + Name: "id", + Unique: true, + Indexer: &fqnIndexer{}, + }, + "ns": { + Name: "ns", + Indexer: &nsIndexer{}, + }, + }, + } +} diff --git a/internal/db/schema/k8s.go b/internal/db/schema/k8s.go new file mode 100644 index 00000000..ea2bd510 --- /dev/null +++ b/internal/db/schema/k8s.go @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package schema + +import ( + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/types" + "github.com/hashicorp/go-memdb" +) + +// Init initializes db tables. +func Init() *memdb.DBSchema { + var sc memdb.DBSchema + sc.Tables = make(map[string]*memdb.TableSchema) + for _, gvr := range internal.Glossary { + if gvr == types.BlankGVR { + continue + } + sc.Tables[gvr.String()] = indexFor(gvr.String()) + } + + return &sc +} diff --git a/internal/db/types.go b/internal/db/types.go new file mode 100644 index 00000000..b81bee6b --- /dev/null +++ b/internal/db/types.go @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package db + +import "k8s.io/apimachinery/pkg/runtime" + +type ConvertFn func(o runtime.Object) (any, error) diff --git a/internal/glossary.go b/internal/glossary.go new file mode 100644 index 00000000..c7f33741 --- /dev/null +++ b/internal/glossary.go @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package internal + +import ( + "slices" + + "github.com/derailed/popeye/types" + "github.com/rs/zerolog/log" +) + +type R string + +var Glossary = make(Linters) + +func init() { + for _, r := range Rs { + Glossary[r] = types.BlankGVR + } +} + +const ( + CM R = "configmaps" + CL R = "cluster" + EP R = "endpoints" + NS R = "namespaces" + NO R = "nodes" + PV R = "persistentvolumes" + PVC R = "persistentvolumeclaims" + PO R = "pods" + SEC R = "secrets" + SA R = "serviceaccounts" + SVC R = "services" + DP R = "deployments" + DS R = "daemonsets" + RS R = "replicasets" + STS R = "statefulsets" + CR R = "clusterroles" + CRB R = "clusterrolebindings" + RO R = "roles" + ROB R = "rolebindings" + ING R = "ingresses" + NP R = "networkpolicies" + PDB R = "poddisruptionbudgets" + HPA R = "horizontalpodautoscalers" + PMX R = "podmetrics" + NMX R = "nodemetrics" + CJOB R = "cronjobs" + JOB R = "jobs" + GW R = "gateways" + GWC R = "gatewayclasses" + GWR R = "httproutes" +) + +var Rs = []R{ + CL, CM, EP, NS, NO, PV, PVC, PO, SEC, SA, SVC, DP, DS, RS, STS, CR, + CRB, RO, ROB, ING, NP, PDB, HPA, PMX, NMX, CJOB, JOB, GW, GWC, GWR, +} + +type Linters map[R]types.GVR + +func (ll Linters) Dump() { + log.Debug().Msg("\nLinters...") + kk := make([]R, 0, len(ll)) + for k := range ll { + kk = append(kk, k) + } + slices.Sort(kk) + for _, k := range kk { + log.Debug().Msgf("%-25s: %s", k, ll[k]) + } +} + +func (ll Linters) Include(gvr types.GVR) (R, bool) { + for k, v := range ll { + g, r := v.G(), v.R() + if g == gvr.G() && r == gvr.R() { + return k, true + } + } + + return "", false +} diff --git a/internal/gvr/types.go b/internal/gvr/types.go new file mode 100644 index 00000000..e4a4ba6d --- /dev/null +++ b/internal/gvr/types.go @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package gvr + +// type GVR1 string + +// func (g GVR) String() string { +// return string(g) +// } + +// var ( +// CM GVR = "v1/configmaps" +// Pod GVR = "v1/pods" +// NS GVR = "v1/namespaces" +// Node GVR = "v1/nodes" +// SA GVR = "v1/serviceaccounts" +// PDB GVR = "policy/v1/poddisruptionbudgets" +// PV GVR = "v1/persistentvolumes" +// PVC GVR = "v1/persistentvolumeclaims" +// SEC GVR = "v1/secrets" +// ING GVR = "networking.k8s.io/v1/ingresses" +// NP GVR = "networking.k8s.io/v1/networkpolicies" +// SVC GVR = "v1/services" +// EP GVR = "v1/endpoints" +// RO GVR = "rbac.authorization.k8s.io/v1/roles" +// RB GVR = "rbac.authorization.k8s.io/v1/rolebindings" +// CR GVR = "rbac.authorization.k8s.io/v1/clusterroles" +// CRB GVR = "rbac.authorization.k8s.io/v1/clusterrolebindings" +// DP GVR = "apps/v1/deployments" +// DS GVR = "apps/v1/daemonsets" +// RS GVR = "apps/v1/replicasets" +// STS GVR = "apps/v1/statefulsets" +// HPA GVR = "autoscaling/v1/horizontalpodautoscalers" +// PMX GVR = "metrics.k8s.io/v1beta1/podmetrics" +// NMX GVR = "metrics.k8s.io/v1beta1/podmetrics" +// ) diff --git a/internal/issues/assets/codes.yml b/internal/issues/assets/codes.yaml similarity index 74% rename from internal/issues/assets/codes.yml rename to internal/issues/assets/codes.yaml index 371c60c3..cfd6917b 100644 --- a/internal/issues/assets/codes.yml +++ b/internal/issues/assets/codes.yaml @@ -16,7 +16,7 @@ codes: message: No readiness probe severity: 2 105: - message: '%s probe uses a port#, prefer a named port' + message: '%s uses a port#, prefer a named port' severity: 1 106: message: No resources requests/limits defined @@ -40,7 +40,7 @@ codes: message: Memory Current/Limit (%s/%s) reached user %d%% threshold (%d%%) severity: 3 113: - message: Container image %s is not hosted on an allowed docker registry + message: Container image %q is not hosted on an allowed docker registry severity: 3 # Pod @@ -63,7 +63,7 @@ codes: message: Pod was restarted (%d) %s severity: 2 206: - message: No PodDisruptionBudget defined + message: Pod has no associated PodDisruptionBudget severity: 1 207: message: Pod is in an unhappy phase (%s) @@ -77,7 +77,7 @@ codes: # Security 300: - message: Using "default" ServiceAccount + message: Uses "default" ServiceAccount severity: 2 301: message: Connects to API Server? ServiceAccount token is mounted @@ -92,11 +92,14 @@ codes: message: References a secret "%s" which does not exist severity: 3 305: - message: References a docker-image "%s" pull secret which does not exist + message: "References a pull secret which does not exist: %s" severity: 3 306: message: Container could be running as root user. Check SecurityContext/Image severity: 2 + 307: + message: "%s references a non existing ServiceAccount: %q" + severity: 2 # General 400: @@ -120,8 +123,11 @@ codes: 406: message: K8s version OK severity: 0 + 407: + message: "%s references %s %q which does not exist" + severity: 3 - # Deployment + StatefulSet + # Pod controllers 500: message: Zero scale detected severity: 2 @@ -143,13 +149,13 @@ codes: 507: message: Deployment references ServiceAccount %q which does not exist severity: 3 + 508: + message: "No pods match controller selector: %s" + severity: 3 # HPA 600: - message: HPA %s references a Deployment %s which does not exist - severity: 3 - 601: - message: HPA %s references a StatefulSet %s which does not exist + message: "HPA %s references a %s which does not exist: %s" severity: 3 602: message: Replicas (%d/%d) at burst will match/exceed cluster CPU(%s) capacity by %s @@ -169,8 +175,8 @@ codes: message: Found taint "%s" but no pod can tolerate severity: 2 701: - message: Node is in an unknown condition - severity: 3 + message: Node has an unknown condition + severity: 2 702: message: Node is not in ready state severity: 3 @@ -212,7 +218,7 @@ codes: # PodDisruptionBudget 900: - message: Used? No pods match selector + message: "No pods match pdb selector: %s" severity: 2 901: message: MinAvailable (%d) is greater than the number of pods(%d) currently running @@ -220,11 +226,11 @@ codes: # PV/PVC 1000: - message: Available + message: Available volume detected severity: 1 1001: message: Pending volume detected - severity: 3 + severity: 2 1002: message: Lost volume detected severity: 3 @@ -266,6 +272,9 @@ codes: 1109: message: Only one Pod associated with this endpoint severity: 2 + 1110: + message: Match EP has no subsets + severity: 2 # ReplicaSet 1120: @@ -274,10 +283,31 @@ codes: # NetworkPolicies 1200: - message: No pods match %s pod selector + message: "No pods match pod selector: %s" severity: 2 1201: - message: No namespaces match %s namespace selector + message: "No namespaces match %s namespace selector: %s" + severity: 2 + 1202: + message: "No pods match %s pod selector: %s" + severity: 2 + 1203: + message: "%s %s policy in effect" + severity: 1 + 1204: + message: "Pod %s is not secured by a network policy" + severity: 2 + 1205: + message: "Pod ingress and egress are not secured by a network policy" + severity: 2 + 1206: + message: "No pods matched %s IPBlock %s" + severity: 2 + 1207: + message: "No pods matched except %s IPBlock %s" + severity: 2 + 1208: + message: "No pods match %s pod selector: %s in namespace: %s" severity: 2 # RBAC @@ -285,3 +315,32 @@ codes: 1300: message: References a %s (%s) which does not exist severity: 2 + + + # Ingress + 1400: + message: "Ingress LoadBalancer port reported an error: %s" + severity: 3 + 1401: + message: "Ingress references a service backend which does not exist: %s" + severity: 3 + 1402: + message: "Ingress references a service port which is not defined: %s" + severity: 3 + 1403: + message: 'Ingress backend uses a port#, prefer a named port: %d' + severity: 1 + 1404: + message: 'Invalid Ingress backend spec. Must use port name or number' + severity: 3 + + # Cronjob + 1500: + message: "%s is suspended" + severity: 2 + 1501: + message: No active jobs detected + severity: 1 + 1502: + message: CronJob has not run yet or is failing + severity: 2 \ No newline at end of file diff --git a/internal/issues/codes.go b/internal/issues/codes.go index 8fc57f7a..0ea672e2 100644 --- a/internal/issues/codes.go +++ b/internal/issues/codes.go @@ -4,21 +4,21 @@ package issues import ( - // Pull in asset codes. _ "embed" - "github.com/derailed/popeye/pkg/config" + "github.com/derailed/popeye/internal/rules" "gopkg.in/yaml.v2" ) -type ( - // Codes represents a collection of sanitizer codes. - Codes struct { - Glossary config.Glossary `yaml:"codes"` - } -) +//go:embed assets/codes.yaml +var codes string + +// Codes represents a collection of linter codes. +type Codes struct { + Glossary rules.Glossary `yaml:"codes"` +} -// LoadCodes retrieves sanitizers codes from yaml file. +// LoadCodes retrieves linters codes from yaml file. func LoadCodes() (*Codes, error) { var cc Codes if err := yaml.Unmarshal([]byte(codes), &cc); err != nil { @@ -29,23 +29,23 @@ func LoadCodes() (*Codes, error) { } // Refine overrides code severity based on user input. -func (c *Codes) Refine(gloss config.Glossary) { - for k, v := range gloss { - c, ok := c.Glossary[k] +func (c *Codes) Refine(oo rules.Overrides) { + for _, ov := range oo { + c, ok := c.Glossary[ov.ID] if !ok { continue } - if validSeverity(v.Severity) { - c.Severity = v.Severity + if validSeverity(ov.Severity) { + c.Severity = ov.Severity + } + if ov.Message != "" { + c.Message = ov.Message } } } // Helpers... -func validSeverity(l config.Level) bool { +func validSeverity(l rules.Level) bool { return l > 0 && l < 4 } - -//go:embed assets/codes.yml -var codes string diff --git a/internal/issues/codes_test.go b/internal/issues/codes_test.go index 3c8bf10e..17942685 100644 --- a/internal/issues/codes_test.go +++ b/internal/issues/codes_test.go @@ -1,10 +1,13 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + package issues_test import ( "testing" "github.com/derailed/popeye/internal/issues" - "github.com/derailed/popeye/pkg/config" + "github.com/derailed/popeye/internal/rules" "github.com/stretchr/testify/assert" ) @@ -12,33 +15,36 @@ func TestCodesLoad(t *testing.T) { cc, err := issues.LoadCodes() assert.Nil(t, err) - assert.Equal(t, 86, len(cc.Glossary)) + assert.Equal(t, 104, len(cc.Glossary)) assert.Equal(t, "No liveness probe", cc.Glossary[103].Message) - assert.Equal(t, config.WarnLevel, cc.Glossary[103].Severity) + assert.Equal(t, rules.WarnLevel, cc.Glossary[103].Severity) } func TestRefine(t *testing.T) { cc, err := issues.LoadCodes() assert.Nil(t, err) - id1, id2 := config.ID(100), config.ID(101) - gloss := config.Glossary{ - 0: &config.Code{ + ov := rules.Overrides{ + rules.CodeOverride{ + ID: 0, Message: "blah", - Severity: config.InfoLevel, + Severity: rules.InfoLevel, }, - id1: &config.Code{ + rules.CodeOverride{ + ID: 100, Message: "blah", - Severity: config.InfoLevel, + Severity: rules.InfoLevel, }, - id2: &config.Code{ + + rules.CodeOverride{ + ID: 101, Message: "blah", Severity: 1000, }, } - cc.Refine(gloss) + cc.Refine(ov) - assert.Equal(t, config.InfoLevel, cc.Glossary[id1].Severity) - assert.Equal(t, config.WarnLevel, cc.Glossary[id2].Severity) + assert.Equal(t, rules.InfoLevel, cc.Glossary[100].Severity) + assert.Equal(t, rules.WarnLevel, cc.Glossary[101].Severity) } diff --git a/internal/issues/collector.go b/internal/issues/collector.go index e785b087..b5d3325d 100644 --- a/internal/issues/collector.go +++ b/internal/issues/collector.go @@ -8,11 +8,12 @@ import ( "fmt" "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/rules" "github.com/derailed/popeye/pkg/config" "github.com/rs/zerolog/log" ) -// Collector represents a sanitizer issue container. +// Collector tracks linter issues and codes. type Collector struct { *config.Config @@ -35,6 +36,12 @@ func (c *Collector) InitOutcome(fqn string) { c.outcomes[fqn] = Issues{} } +func (c *Collector) CloseOutcome(ctx context.Context, fqn string, cos []string) { + if c.NoConcerns(fqn) && c.Config.ExcludeFQN(internal.MustExtractSectionGVR(ctx), fqn, cos) { + c.ClearOutcome(fqn) + } +} + // ClearOutcome delete all fqn related issues. func (c *Collector) ClearOutcome(fqn string) { delete(c.outcomes, fqn) @@ -46,39 +53,42 @@ func (c *Collector) NoConcerns(fqn string) bool { } // MaxSeverity return the highest severity level for the given section. -func (c *Collector) MaxSeverity(fqn string) config.Level { +func (c *Collector) MaxSeverity(fqn string) rules.Level { return c.outcomes.MaxSeverity(fqn) } // AddSubCode add a sub error code. -func (c *Collector) AddSubCode(ctx context.Context, code config.ID, args ...interface{}) { +func (c *Collector) AddSubCode(ctx context.Context, code rules.ID, args ...interface{}) { run := internal.MustExtractRunInfo(ctx) co, ok := c.codes.Glossary[code] if !ok { log.Error().Err(fmt.Errorf("No code with ID %d", code)).Msg("AddSubCode failed") } - if co.Severity < config.Level(c.Config.LintLevel) { + if co.Severity < rules.Level(c.Config.LintLevel) { return } - if !c.ShouldExclude(run.SectionGVR.String(), run.FQN, code) { - c.addIssue(run.FQN, New(run.GroupGVR, run.Group, co.Severity, co.Format(code, args...))) + run.Spec.GVR, run.Spec.Code = run.SectionGVR, code + if !c.Match(run.Spec) { + c.addIssue(run.Spec.FQN, New(run.GroupGVR, run.Group, co.Severity, co.Format(code, args...))) } } // AddCode add an error code. -func (c *Collector) AddCode(ctx context.Context, code config.ID, args ...interface{}) { +func (c *Collector) AddCode(ctx context.Context, code rules.ID, args ...interface{}) { run := internal.MustExtractRunInfo(ctx) co, ok := c.codes.Glossary[code] if !ok { // BOZO!! refact once codes are in!! panic(fmt.Errorf("no codes found with id %d", code)) } - if co.Severity < config.Level(c.Config.LintLevel) { + if co.Severity < rules.Level(c.Config.LintLevel) { return } - if !c.ShouldExclude(run.SectionGVR.String(), run.FQN, code) { - c.addIssue(run.FQN, New(run.SectionGVR, Root, co.Severity, co.Format(code, args...))) + + run.Spec.GVR, run.Spec.Code = run.SectionGVR, code + if !c.Match(run.Spec) { + c.addIssue(run.Spec.FQN, New(run.SectionGVR, Root, co.Severity, co.Format(code, args...))) } } @@ -86,7 +96,7 @@ func (c *Collector) AddCode(ctx context.Context, code config.ID, args ...interfa func (c *Collector) AddErr(ctx context.Context, errs ...error) { run := internal.MustExtractRunInfo(ctx) for _, e := range errs { - c.addIssue(run.FQN, New(run.SectionGVR, Root, config.ErrorLevel, e.Error())) + c.addIssue(run.Spec.FQN, New(run.SectionGVR, Root, rules.ErrorLevel, e.Error())) } } diff --git a/internal/issues/collector_test.go b/internal/issues/collector_test.go index fafca39b..5f930061 100644 --- a/internal/issues/collector_test.go +++ b/internal/issues/collector_test.go @@ -9,8 +9,9 @@ import ( "testing" "github.com/derailed/popeye/internal" - "github.com/derailed/popeye/internal/client" + "github.com/derailed/popeye/internal/rules" "github.com/derailed/popeye/pkg/config" + "github.com/derailed/popeye/types" "github.com/stretchr/testify/assert" ) @@ -24,8 +25,8 @@ func TestNoConcerns(t *testing.T) { }, "issues": { issues: []Issue{ - New(client.NewGVR("blee"), Root, config.InfoLevel, "blee"), - New(client.NewGVR("blee"), Root, config.WarnLevel, "blee"), + New(types.NewGVR("blee"), Root, rules.InfoLevel, "blee"), + New(types.NewGVR("blee"), Root, rules.WarnLevel, "blee"), }, }, } @@ -45,40 +46,40 @@ func TestMaxSeverity(t *testing.T) { uu := map[string]struct { issues []Issue section string - severity config.Level + severity rules.Level count int }{ "noIssue": { section: Root, - severity: config.OkLevel, + severity: rules.OkLevel, count: 0, }, "mix": { issues: []Issue{ - New(client.NewGVR("fred"), Root, config.InfoLevel, "blee"), - New(client.NewGVR("fred"), Root, config.WarnLevel, "blee"), + New(types.NewGVR("fred"), Root, rules.InfoLevel, "blee"), + New(types.NewGVR("fred"), Root, rules.WarnLevel, "blee"), }, section: Root, - severity: config.WarnLevel, + severity: rules.WarnLevel, count: 2, }, "same": { issues: []Issue{ - New(client.NewGVR("fred"), Root, config.InfoLevel, "blee"), - New(client.NewGVR("fred"), Root, config.InfoLevel, "blee"), + New(types.NewGVR("fred"), Root, rules.InfoLevel, "blee"), + New(types.NewGVR("fred"), Root, rules.InfoLevel, "blee"), }, section: Root, - severity: config.InfoLevel, + severity: rules.InfoLevel, count: 2, }, "error": { issues: []Issue{ - New(client.NewGVR("fred"), Root, config.ErrorLevel, "blee"), - New(client.NewGVR("fred"), Root, config.InfoLevel, "blee"), - New(client.NewGVR("fred"), Root, config.InfoLevel, "blee"), + New(types.NewGVR("fred"), Root, rules.ErrorLevel, "blee"), + New(types.NewGVR("fred"), Root, rules.InfoLevel, "blee"), + New(types.NewGVR("fred"), Root, rules.InfoLevel, "blee"), }, section: Root, - severity: config.ErrorLevel, + severity: rules.ErrorLevel, count: 3, }, } @@ -126,43 +127,43 @@ func TestAddErr(t *testing.T) { c.AddErr(ctx, u.errors...) assert.Equal(t, u.count, len(c.outcomes[u.fqn])) - assert.Equal(t, config.ErrorLevel, c.MaxSeverity(u.fqn)) + assert.Equal(t, rules.ErrorLevel, c.MaxSeverity(u.fqn)) }) } } func TestAddCode(t *testing.T) { uu := map[string]struct { - code config.ID + code rules.ID fqn string args []interface{} - level config.Level + level rules.Level e string }{ "No params": { code: 100, fqn: Root, - level: config.ErrorLevel, + level: rules.ErrorLevel, e: "[POP-100] Untagged docker image in use", }, "Params": { code: 108, fqn: Root, - level: config.InfoLevel, + level: rules.InfoLevel, args: []interface{}{80}, e: "[POP-108] Unnamed port 80", }, "Dud!": { code: 0, fqn: Root, - level: config.InfoLevel, + level: rules.InfoLevel, args: []interface{}{80}, e: "[POP-108] Unnamed port 80", }, "Issue 169": { code: 1102, fqn: Root, - level: config.InfoLevel, + level: rules.InfoLevel, args: []interface{}{"123", "test-port"}, e: "[POP-1102] Use of target port #123 for service port test-port. Prefer named port", }, @@ -180,7 +181,6 @@ func TestAddCode(t *testing.T) { assert.Panics(t, subCode, "blee") } else { c.AddCode(ctx, u.code, u.args...) - assert.Equal(t, u.e, c.outcomes[u.fqn][0].Message) assert.Equal(t, u.level, c.outcomes[u.fqn][0].Level) } @@ -190,24 +190,24 @@ func TestAddCode(t *testing.T) { func TestAddSubCode(t *testing.T) { uu := map[string]struct { - code config.ID + code rules.ID section, group string args []interface{} - level config.Level + level rules.Level e string }{ "No params": { code: 100, section: Root, group: "blee", - level: config.ErrorLevel, + level: rules.ErrorLevel, e: "[POP-100] Untagged docker image in use", }, "Params": { code: 108, section: Root, group: "blee", - level: config.InfoLevel, + level: rules.InfoLevel, args: []interface{}{80}, e: "[POP-108] Unnamed port 80", }, @@ -215,7 +215,7 @@ func TestAddSubCode(t *testing.T) { code: 0, section: Root, group: "blee", - level: config.InfoLevel, + level: rules.InfoLevel, args: []interface{}{80}, e: "[POP-108] Unnamed port 80", }, @@ -245,11 +245,12 @@ func TestAddSubCode(t *testing.T) { // Helpers... -func loadCodes(t *testing.T) *Codes { - codes, err := LoadCodes() - assert.Nil(t, err) - - return codes +func makeContext(section, fqn, group string) context.Context { + return context.WithValue(context.Background(), internal.KeyRunInfo, internal.RunInfo{ + Section: section, + Group: group, + Spec: rules.Spec{FQN: fqn}, + }) } func makeConfig(t *testing.T) *config.Config { @@ -258,10 +259,9 @@ func makeConfig(t *testing.T) *config.Config { return c } -func makeContext(section, fqn, group string) context.Context { - return context.WithValue(context.Background(), internal.KeyRunInfo, internal.RunInfo{ - Section: section, - Group: group, - FQN: fqn, - }) +func loadCodes(t *testing.T) *Codes { + codes, err := LoadCodes() + assert.Nil(t, err) + + return codes } diff --git a/internal/issues/issue.go b/internal/issues/issue.go index c90553aa..932f4eec 100644 --- a/internal/issues/issue.go +++ b/internal/issues/issue.go @@ -5,34 +5,49 @@ package issues import ( "fmt" + "regexp" - "github.com/derailed/popeye/internal/client" - "github.com/derailed/popeye/pkg/config" + "github.com/derailed/popeye/internal/rules" + "github.com/derailed/popeye/types" ) +var codeRX = regexp.MustCompile(`\A\[POP-(\d+)\]`) + // Blank issue var Blank = Issue{} -type ( - // Issue tracks a sanitizer issue. - Issue struct { - Group string `yaml:"group" json:"group"` - GVR string `yaml:"gvr" json:"gvr"` - Level config.Level `yaml:"level" json:"level"` - Message string `yaml:"message" json:"message"` - } -) +// Issue tracks a linter issue. +type Issue struct { + Group string `yaml:"group" json:"group"` + GVR string `yaml:"gvr" json:"gvr"` + Level rules.Level `yaml:"level" json:"level"` + Message string `yaml:"message" json:"message"` +} // New returns a new lint issue. -func New(gvr client.GVR, group string, level config.Level, description string) Issue { +func New(gvr types.GVR, group string, level rules.Level, description string) Issue { return Issue{GVR: gvr.String(), Group: group, Level: level, Message: description} } // Newf returns a new lint issue using a formatter. -func Newf(gvr client.GVR, group string, level config.Level, format string, args ...interface{}) Issue { +func Newf(gvr types.GVR, group string, level rules.Level, format string, args ...interface{}) Issue { return New(gvr, group, level, fmt.Sprintf(format, args...)) } +func (i Issue) Code() (string, bool) { + mm := codeRX.FindStringSubmatch(i.Message) + if len(mm) < 2 { + return "", false + } + + return mm[1], true +} + +// Dump for debugging. +func (i Issue) Dump() { + fmt.Printf(" %s (%d) %s\n", i.GVR, i.Level, i.Message) +} + // Blank checks if an issue is blank. func (i Issue) Blank() bool { return i == Blank @@ -44,14 +59,14 @@ func (i Issue) IsSubIssue() bool { } // LevelToStr returns a severity level as a string. -func LevelToStr(l config.Level) string { +func LevelToStr(l rules.Level) string { // nolint:exhaustive switch l { - case config.ErrorLevel: + case rules.ErrorLevel: return "error" - case config.WarnLevel: + case rules.WarnLevel: return "warn" - case config.InfoLevel: + case rules.InfoLevel: return "info" default: return "ok" diff --git a/internal/issues/issue_test.go b/internal/issues/issue_test.go index cdf43132..7354e976 100644 --- a/internal/issues/issue_test.go +++ b/internal/issues/issue_test.go @@ -6,8 +6,8 @@ package issues import ( "testing" - "github.com/derailed/popeye/internal/client" - "github.com/derailed/popeye/pkg/config" + "github.com/derailed/popeye/internal/rules" + "github.com/derailed/popeye/types" "github.com/stretchr/testify/assert" ) @@ -16,10 +16,10 @@ func TestIsSubIssues(t *testing.T) { i Issue e bool }{ - "root": {New(client.NewGVR("fred"), Root, config.WarnLevel, "blah"), false}, - "rootf": {Newf(client.NewGVR("fred"), Root, config.WarnLevel, "blah %s", "blee"), false}, - "sub": {New(client.NewGVR("fred"), "sub1", config.WarnLevel, "blah"), true}, - "subf": {Newf(client.NewGVR("fred"), "sub1", config.WarnLevel, "blah %s", "blee"), true}, + "root": {New(types.NewGVR("fred"), Root, rules.WarnLevel, "blah"), false}, + "rootf": {Newf(types.NewGVR("fred"), Root, rules.WarnLevel, "blah %s", "blee"), false}, + "sub": {New(types.NewGVR("fred"), "sub1", rules.WarnLevel, "blah"), true}, + "subf": {Newf(types.NewGVR("fred"), "sub1", rules.WarnLevel, "blah %s", "blee"), true}, } for k := range uu { @@ -36,7 +36,7 @@ func TestBlank(t *testing.T) { e bool }{ "blank": {Issue{}, true}, - "notBlank": {New(client.NewGVR("fred"), Root, config.WarnLevel, "blah"), false}, + "notBlank": {New(types.NewGVR("fred"), Root, rules.WarnLevel, "blah"), false}, } for k := range uu { diff --git a/internal/issues/issues.go b/internal/issues/issues.go index 06204ea2..050fdc19 100644 --- a/internal/issues/issues.go +++ b/internal/issues/issues.go @@ -4,9 +4,15 @@ package issues import ( - "sort" - - "github.com/derailed/popeye/pkg/config" + "encoding/json" + "fmt" + "slices" + "strconv" + "strings" + + "github.com/derailed/popeye/internal/client" + "github.com/derailed/popeye/internal/issues/tally" + "github.com/derailed/popeye/internal/rules" ) // Root denotes a root issue group. @@ -20,9 +26,24 @@ type ( Outcome map[string]Issues ) +func (i Issues) CodeTally() tally.Code { + ss := make(tally.Code) + for _, issue := range i { + if c, ok := issue.Code(); ok { + if v, ok := ss[c]; ok { + ss[c] = v + 1 + } else { + ss[c] = 1 + } + } + } + + return ss +} + // MaxSeverity gather the max severity in a collection of issues. -func (i Issues) MaxSeverity() config.Level { - max := config.OkLevel +func (i Issues) MaxSeverity() rules.Level { + max := rules.OkLevel for _, is := range i { if is.Level > max { max = is.Level @@ -32,30 +53,39 @@ func (i Issues) MaxSeverity() config.Level { return max } +func (i Issues) HasIssues() bool { + return len(i) > 0 +} + +func SortKeys(k1, k2 string) int { + v1, err := strconv.Atoi(k1) + if err == nil { + v2, _ := strconv.Atoi(k2) + switch { + case v1 == v2: + return 0 + case v1 < v2: + return -1 + default: + return 1 + } + } + + return strings.Compare(k1, k2) +} + // Sort sorts issues. -func (i Issues) Sort(l config.Level) Issues { +func (i Issues) Sort(l rules.Level) Issues { ii := make(Issues, 0, len(i)) gg := i.Group() - keys := make(sort.StringSlice, 0, len(gg)) + kk := make([]string, 0, len(gg)) for k := range gg { - keys = append(keys, k) + kk = append(kk, k) } - keys.Sort() - for _, group := range keys { - sev := gg[group].MaxSeverity() - if sev < l { - continue - } - for _, i := range gg[group] { - if i.Level < l { - continue - } - if i.Group == Root { - ii = append(ii, i) - continue - } - ii = append(ii, i) - } + slices.SortFunc(kk, SortKeys) + + for _, k := range kk { + ii = append(ii, gg[k]...) } return ii } @@ -70,13 +100,69 @@ func (i Issues) Group() map[string]Issues { return res } +// NSTally collects Namespace code tally for a given linter. +func (o Outcome) NSTally() tally.Namespace { + nn := make(tally.Namespace, len(o)) + for fqn, v := range o { + ns, _ := client.Namespaced(fqn) + if ns == "" { + ns = "-" + } + tt := v.CodeTally() + if v1, ok := nn[ns]; ok { + v1.Merge(tt) + } else { + nn[ns] = tt + } + } + + return nn +} + // MaxSeverity scans the issues and reports the highest severity. -func (o Outcome) MaxSeverity(section string) config.Level { +func (o Outcome) MaxSeverity(section string) rules.Level { return o[section].MaxSeverity() } +func (o Outcome) MarshalJSON() ([]byte, error) { + out := make([]string, 0, len(o)) + for k, v := range o { + if len(v) == 0 { + continue + } + raw, err := json.Marshal(v) + if err != nil { + return nil, err + } + out = append(out, fmt.Sprintf("%q: %s", k, raw)) + } + + return []byte("{" + strings.Join(out, ",") + "}"), nil +} + +func (o Outcome) MarshalYAML() (interface{}, error) { + out := make(Outcome, len(o)) + for k, v := range o { + if len(v) == 0 { + continue + } + out[k] = v + } + + return out, nil +} + +func (o Outcome) HasIssues() bool { + var count int + for _, ii := range o { + count += len(ii) + } + + return count > 0 +} + // MaxGroupSeverity scans the issues and reports the highest severity. -func (o Outcome) MaxGroupSeverity(section, group string) config.Level { +func (o Outcome) MaxGroupSeverity(section, group string) rules.Level { return o.For(section, group).MaxSeverity() } @@ -93,8 +179,8 @@ func (o Outcome) For(section, group string) Issues { return ii } -// Filter filters outcomes based on sanitizer level. -func (o Outcome) Filter(level config.Level) Outcome { +// Filter filters outcomes based on lint level. +func (o Outcome) Filter(level rules.Level) Outcome { for k, issues := range o { vv := make(Issues, 0, len(issues)) for _, issue := range issues { @@ -106,3 +192,15 @@ func (o Outcome) Filter(level config.Level) Outcome { } return o } + +func (o Outcome) Dump() { + if len(o) == 0 { + fmt.Println("No ISSUES!") + } + for k, ii := range o { + fmt.Println(k) + for _, i := range ii { + i.Dump() + } + } +} diff --git a/internal/issues/issues_test.go b/internal/issues/issues_test.go index 1f5ddabf..26d5f3a7 100644 --- a/internal/issues/issues_test.go +++ b/internal/issues/issues_test.go @@ -6,37 +6,37 @@ package issues import ( "testing" - "github.com/derailed/popeye/internal/client" - "github.com/derailed/popeye/pkg/config" + "github.com/derailed/popeye/internal/rules" + "github.com/derailed/popeye/types" "github.com/stretchr/testify/assert" ) func TestMaxGroupSeverity(t *testing.T) { o := Outcome{ "s1": Issues{ - New(client.NewGVR("fred"), Root, config.OkLevel, "i1"), + New(types.NewGVR("fred"), Root, rules.OkLevel, "i1"), }, "s2": Issues{ - New(client.NewGVR("fred"), Root, config.OkLevel, "i1"), - New(client.NewGVR("fred"), Root, config.WarnLevel, "i2"), - New(client.NewGVR("fred"), "g1", config.WarnLevel, "i2"), + New(types.NewGVR("fred"), Root, rules.OkLevel, "i1"), + New(types.NewGVR("fred"), Root, rules.WarnLevel, "i2"), + New(types.NewGVR("fred"), "g1", rules.WarnLevel, "i2"), }, } - assert.Equal(t, config.OkLevel, o.MaxGroupSeverity("s1", Root)) - assert.Equal(t, config.WarnLevel, o.MaxGroupSeverity("s2", Root)) + assert.Equal(t, rules.OkLevel, o.MaxGroupSeverity("s1", Root)) + assert.Equal(t, rules.WarnLevel, o.MaxGroupSeverity("s2", Root)) } func TestIssuesForGroup(t *testing.T) { o := Outcome{ "s1": Issues{ - New(client.NewGVR("fred"), Root, config.OkLevel, "i1"), + New(types.NewGVR("fred"), Root, rules.OkLevel, "i1"), }, "s2": Issues{ - New(client.NewGVR("fred"), Root, config.OkLevel, "i1"), - New(client.NewGVR("fred"), Root, config.WarnLevel, "i2"), - New(client.NewGVR("fred"), "g1", config.WarnLevel, "i3"), - New(client.NewGVR("fred"), "g1", config.WarnLevel, "i4"), + New(types.NewGVR("fred"), Root, rules.OkLevel, "i1"), + New(types.NewGVR("fred"), Root, rules.WarnLevel, "i2"), + New(types.NewGVR("fred"), "g1", rules.WarnLevel, "i3"), + New(types.NewGVR("fred"), "g1", rules.WarnLevel, "i4"), }, } @@ -47,13 +47,13 @@ func TestIssuesForGroup(t *testing.T) { func TestGroup(t *testing.T) { o := Outcome{ "s2": Issues{ - New(client.NewGVR("fred"), Root, config.OkLevel, "i1"), - New(client.NewGVR("fred"), Root, config.WarnLevel, "i2"), - New(client.NewGVR("fred"), "g1", config.ErrorLevel, "i2"), + New(types.NewGVR("fred"), Root, rules.OkLevel, "i1"), + New(types.NewGVR("fred"), Root, rules.WarnLevel, "i2"), + New(types.NewGVR("fred"), "g1", rules.ErrorLevel, "i2"), }, } grp := o["s2"].Group() - assert.Equal(t, config.ErrorLevel, o["s2"].MaxSeverity()) + assert.Equal(t, rules.ErrorLevel, o["s2"].MaxSeverity()) assert.Equal(t, 2, len(grp)) } diff --git a/internal/issues/tally/code.go b/internal/issues/tally/code.go new file mode 100644 index 00000000..e1a70493 --- /dev/null +++ b/internal/issues/tally/code.go @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package tally + +import ( + "slices" + "strconv" + + "github.com/derailed/popeye/internal/rules" + "github.com/rs/zerolog/log" +) + +// SevScore tracks per level total score. +type SevScore map[rules.Level]int + +// Code tracks code issue counts. +type Code map[string]int + +// Compact removes zero entries. +func (cc Code) Compact() { + for c, v := range cc { + if v == 0 { + delete(cc, c) + } + } +} + +// Rollup rollups code scores per severity. +func (cc Code) Rollup(gg rules.Glossary) SevScore { + if len(cc) == 0 { + return nil + } + ss := make(SevScore, len(cc)) + for sid, count := range cc { + id, _ := strconv.Atoi(sid) + c := gg[rules.ID(id)] + ss[c.Severity] += count + } + + return ss +} + +// Merge merges two sets. +func (cc Code) Merge(cc1 Code) { + for code, count := range cc1 { + cc[code] += count + } +} + +// Dump for debugging. +func (cc Code) Dump(indent string) { + kk := make([]string, 0, len(cc)) + for k := range cc { + kk = append(kk, k) + } + slices.Sort(kk) + for _, k := range kk { + log.Debug().Msgf("%s%s: %d", indent, k, cc[k]) + } +} diff --git a/internal/issues/tally/code_test.go b/internal/issues/tally/code_test.go new file mode 100644 index 00000000..0f887acd --- /dev/null +++ b/internal/issues/tally/code_test.go @@ -0,0 +1,219 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package tally_test + +import ( + "testing" + + "github.com/derailed/popeye/internal/issues/tally" + "github.com/derailed/popeye/internal/rules" + "github.com/stretchr/testify/assert" +) + +func TestCodeMerge(t *testing.T) { + uu := map[string]struct { + c1, c2, e tally.Code + }{ + "empty": {}, + "empty-1": { + c1: tally.Code{}, + c2: tally.Code{ + "100": 1, + "101": 2, + "102": 5, + "103": 6, + }, + e: tally.Code{ + "100": 1, + "101": 2, + "102": 5, + "103": 6, + }, + }, + "empty-2": { + c1: tally.Code{ + "100": 1, + "101": 2, + "102": 5, + "103": 6, + }, + c2: tally.Code{}, + e: tally.Code{ + "100": 1, + "101": 2, + "102": 5, + "103": 6, + }, + }, + + "same": { + c1: tally.Code{ + "100": 1, + "101": 2, + "102": 5, + "103": 6, + }, + c2: tally.Code{ + "100": 1, + "101": 2, + "102": 5, + "103": 6, + }, + e: tally.Code{ + "100": 2, + "101": 4, + "102": 10, + "103": 12, + }, + }, + "delta": { + c1: tally.Code{ + "100": 1, + "101": 2, + "102": 5, + "103": 6, + }, + c2: tally.Code{ + "102": 5, + "200": 1, + "201": 2, + "203": 6, + }, + e: tally.Code{ + "100": 1, + "101": 2, + "102": 10, + "103": 6, + "200": 1, + "201": 2, + "203": 6, + }, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + u.c1.Merge(u.c2) + assert.Equal(t, u.e, u.c1) + }) + } +} + +func TestCodeCompact(t *testing.T) { + uu := map[string]struct { + c, e tally.Code + }{ + "empty": {}, + "none": { + c: tally.Code{ + "100": 1, + "101": 2, + "102": 5, + "103": 6, + }, + e: tally.Code{ + "100": 1, + "101": 2, + "102": 5, + "103": 6, + }, + }, + "happy": { + c: tally.Code{ + "100": 1, + "101": 2, + "200": 0, + "201": 6, + "202": 0, + "203": 0, + }, + e: tally.Code{ + "100": 1, + "101": 2, + "201": 6, + }, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + u.c.Compact() + assert.Equal(t, u.e, u.c) + }) + } +} + +func TestCodeRollup(t *testing.T) { + uu := map[string]struct { + c tally.Code + e tally.SevScore + }{ + "empty": {}, + "plain": { + c: tally.Code{ + "100": 1, + "101": 2, + "102": 5, + "103": 6, + }, + e: tally.SevScore{ + 0: 6, + 1: 5, + 2: 2, + 3: 1, + }, + }, + "singles": { + c: tally.Code{ + "100": 1, + "101": 2, + "200": 5, + "201": 6, + "202": 20, + "203": 10, + }, + e: tally.SevScore{ + 0: 10, + 1: 20, + 2: 8, + 3: 6, + }, + }, + } + + g := rules.Glossary{ + 100: { + Severity: rules.ErrorLevel, + }, + 101: { + Severity: rules.WarnLevel, + }, + 102: { + Severity: rules.InfoLevel, + }, + 103: { + Severity: rules.OkLevel, + }, + 200: { + Severity: rules.ErrorLevel, + }, + 201: { + Severity: rules.WarnLevel, + }, + 202: { + Severity: rules.InfoLevel, + }, + 203: { + Severity: rules.OkLevel, + }, + } + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, u.c.Rollup(g)) + }) + } +} diff --git a/internal/issues/tally/linter.go b/internal/issues/tally/linter.go new file mode 100644 index 00000000..4f263614 --- /dev/null +++ b/internal/issues/tally/linter.go @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package tally + +import ( + "slices" + + "github.com/rs/zerolog/log" +) + +// Linter tracks linters namespace tallies. +type Linter map[string]Namespace + +func (l Linter) Compact() { + for linter, v := range l { + v.Compact() + if len(v) == 0 { + delete(l, linter) + } + } +} + +func (s Linter) Dump() { + kk := make([]string, 0, len(s)) + for k := range s { + kk = append(kk, k) + } + slices.Sort(kk) + for _, k := range kk { + log.Debug().Msgf("%s", k) + s[k].Dump(" ") + } +} diff --git a/internal/issues/tally/linter_test.go b/internal/issues/tally/linter_test.go new file mode 100644 index 00000000..0d673bf3 --- /dev/null +++ b/internal/issues/tally/linter_test.go @@ -0,0 +1,173 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package tally_test + +import ( + "testing" + + "github.com/derailed/popeye/internal/issues/tally" + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" +) + +func init() { + zerolog.SetGlobalLevel(zerolog.FatalLevel) +} + +func TestLinterCompact(t *testing.T) { + uu := map[string]struct { + lt, e tally.Linter + }{ + "empty": {}, + "multi": { + lt: tally.Linter{ + "a": tally.Namespace{ + "ns1": tally.Code{ + "100": 0, + "101": 2, + "102": 5, + "103": 0, + }, + "ns2": tally.Code{ + "100": 1, + "101": 0, + "102": 0, + "103": 6, + }, + }, + "b": tally.Namespace{ + "ns1": tally.Code{ + "100": 0, + "101": 2, + "102": 5, + "103": 0, + }, + "ns3": tally.Code{ + "100": 1, + "101": 0, + "102": 0, + "103": 6, + }, + }, + }, + e: tally.Linter{ + "a": tally.Namespace{ + "ns1": tally.Code{ + "101": 2, + "102": 5, + }, + "ns2": tally.Code{ + "100": 1, + "103": 6, + }, + }, + "b": tally.Namespace{ + "ns1": tally.Code{ + "101": 2, + "102": 5, + }, + "ns3": tally.Code{ + "100": 1, + "103": 6, + }, + }, + }, + }, + "delete-ns": { + lt: tally.Linter{ + "a": tally.Namespace{ + "ns1": tally.Code{ + "100": 0, + "101": 0, + "102": 0, + "103": 0, + }, + "ns2": tally.Code{ + "100": 1, + "101": 0, + "102": 0, + "103": 6, + }, + }, + "b": tally.Namespace{ + "ns1": tally.Code{ + "100": 0, + "101": 0, + "102": 0, + "103": 0, + }, + "ns3": tally.Code{ + "100": 1, + "101": 0, + "102": 0, + "103": 6, + }, + }, + }, + e: tally.Linter{ + "a": tally.Namespace{ + "ns2": tally.Code{ + "100": 1, + "103": 6, + }, + }, + "b": tally.Namespace{ + "ns3": tally.Code{ + "100": 1, + "103": 6, + }, + }, + }, + }, + "delete-linter": { + lt: tally.Linter{ + "a": tally.Namespace{ + "ns1": tally.Code{ + "100": 0, + "101": 0, + "102": 0, + "103": 0, + }, + "ns2": tally.Code{ + "100": 0, + "101": 0, + "102": 0, + "103": 0, + }, + }, + "b": tally.Namespace{ + "ns1": tally.Code{ + "100": 0, + "101": 0, + "102": 0, + "103": 0, + }, + "ns3": tally.Code{ + "100": 1, + "101": 0, + "102": 0, + "103": 6, + }, + }, + }, + e: tally.Linter{ + "b": tally.Namespace{ + "ns3": tally.Code{ + "100": 1, + "103": 6, + }, + }, + }, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + u.lt.Compact() + u.lt.Dump() + assert.Equal(t, u.e, u.lt) + }) + } +} diff --git a/internal/issues/tally/ns.go b/internal/issues/tally/ns.go new file mode 100644 index 00000000..3f5c9f74 --- /dev/null +++ b/internal/issues/tally/ns.go @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package tally + +import ( + "slices" + "strings" + + "github.com/rs/zerolog/log" +) + +// Namespace tracks each namespace code tally. +type Namespace map[string]Code + +// Compact compacts set by removing zero entries. +func (nn Namespace) Compact() { + for ns, v := range nn { + v.Compact() + if len(v) == 0 { + delete(nn, ns) + } + } +} + +// Merge merges 2 sets. +func (nn Namespace) Merge(t Namespace) { + for k, v := range t { + if v1, ok := nn[k]; ok { + nn[k].Merge(v1) + } else { + nn[k] = v + } + } +} + +// Dump for debugging. +func (s Namespace) Dump(indent string) { + kk := make([]string, 0, len(s)) + for k := range s { + kk = append(kk, k) + } + slices.Sort(kk) + for _, k := range kk { + log.Debug().Msgf("%s%s", indent, k) + s[k].Dump(strings.Repeat(indent, 2)) + } +} diff --git a/internal/issues/tally/ns_test.go b/internal/issues/tally/ns_test.go new file mode 100644 index 00000000..6f8bcfc9 --- /dev/null +++ b/internal/issues/tally/ns_test.go @@ -0,0 +1,175 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package tally_test + +import ( + "testing" + + "github.com/derailed/popeye/internal/issues/tally" + "github.com/stretchr/testify/assert" +) + +func TestNSMerge(t *testing.T) { + uu := map[string]struct { + ns1, ns2, e tally.Namespace + }{ + "empty": {}, + "one-way": { + ns1: tally.Namespace{ + "ns1": tally.Code{ + "100": 1, + "101": 2, + "102": 5, + "103": 6, + }, + }, + ns2: tally.Namespace{}, + e: tally.Namespace{ + "ns1": tally.Code{ + "100": 1, + "101": 2, + "102": 5, + "103": 6, + }, + }, + }, + "union": { + ns1: tally.Namespace{ + "ns1": tally.Code{ + "100": 1, + "101": 2, + "102": 5, + "103": 6, + }, + }, + ns2: tally.Namespace{ + "ns2": tally.Code{ + "100": 1, + "101": 2, + "102": 5, + "103": 6, + }, + }, + e: tally.Namespace{ + "ns1": tally.Code{ + "100": 1, + "101": 2, + "102": 5, + "103": 6, + }, + "ns2": tally.Code{ + "100": 1, + "101": 2, + "102": 5, + "103": 6, + }, + }, + }, + "intersect": { + ns1: tally.Namespace{ + "ns1": tally.Code{ + "100": 1, + "101": 2, + "102": 5, + "103": 6, + }, + }, + ns2: tally.Namespace{ + "ns1": tally.Code{ + "100": 1, + "101": 2, + "102": 5, + "103": 6, + }, + "ns2": tally.Code{ + "100": 1, + "101": 2, + "102": 5, + "103": 6, + }, + }, + e: tally.Namespace{ + "ns1": tally.Code{ + "100": 2, + "101": 4, + "102": 10, + "103": 12, + }, + "ns2": tally.Code{ + "100": 1, + "101": 2, + "102": 5, + "103": 6, + }, + }, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + u.ns1.Merge(u.ns2) + assert.Equal(t, u.e, u.ns1) + }) + } +} + +func TestNSCompact(t *testing.T) { + uu := map[string]struct { + ns1, e tally.Namespace + }{ + "empty": {}, + "multi": { + ns1: tally.Namespace{ + "ns1": tally.Code{ + "100": 0, + "101": 2, + "102": 5, + "103": 0, + }, + "ns2": tally.Code{ + "100": 1, + "101": 0, + "102": 0, + "103": 6, + }, + }, + e: tally.Namespace{ + "ns1": tally.Code{ + "101": 2, + "102": 5, + }, + "ns2": tally.Code{ + "100": 1, + "103": 6, + }, + }, + }, + "single": { + ns1: tally.Namespace{ + "ns1": tally.Code{ + "100": 1, + "101": 0, + "102": 5, + "103": 6, + }, + }, + e: tally.Namespace{ + "ns1": tally.Code{ + "100": 1, + "102": 5, + "103": 6, + }, + }, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + u.ns1.Compact() + assert.Equal(t, u.e, u.ns1) + }) + } +} diff --git a/internal/keys.go b/internal/keys.go index 03a8aa09..ba1a57b4 100644 --- a/internal/keys.go +++ b/internal/keys.go @@ -16,4 +16,5 @@ const ( KeyConfig ContextKey = "config" KeyNamespace ContextKey = "namespace" KeyVersion ContextKey = "version" + KeyDB ContextKey = "db" ) diff --git a/internal/lint/cluster.go b/internal/lint/cluster.go new file mode 100644 index 00000000..cf35546a --- /dev/null +++ b/internal/lint/cluster.go @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "context" + + "github.com/Masterminds/semver" + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/issues" +) + +const ( + tolerableMajor = 1 + tolerableMinor = 21 +) + +type ( + // Cluster tracks cluster sanitization. + Cluster struct { + *issues.Collector + ClusterLister + } + + // ClusterLister list available Clusters on a cluster. + ClusterLister interface { + ListVersion() *semver.Version + HasMetrics() bool + } +) + +// NewCluster returns a new instance. +func NewCluster(co *issues.Collector, lister ClusterLister) *Cluster { + return &Cluster{ + Collector: co, + ClusterLister: lister, + } +} + +// Lint cleanse the resource. +func (c *Cluster) Lint(ctx context.Context) error { + return c.checkVersion(ctx) +} + +func (c *Cluster) checkVersion(ctx context.Context) error { + rev := c.ListVersion() + + ctx = internal.WithSpec(ctx, specFor("Version", nil)) + if rev.Major() != tolerableMajor || rev.Minor() < tolerableMinor { + c.AddCode(ctx, 405) + } else { + c.AddCode(ctx, 406) + } + + return nil +} diff --git a/internal/lint/cluster_test.go b/internal/lint/cluster_test.go new file mode 100644 index 00000000..c073388c --- /dev/null +++ b/internal/lint/cluster_test.go @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "testing" + + "github.com/Masterminds/semver" + "github.com/stretchr/testify/assert" + + "github.com/derailed/popeye/internal/issues" + "github.com/derailed/popeye/internal/rules" + "github.com/derailed/popeye/internal/test" +) + +func TestClusterLint(t *testing.T) { + uu := map[string]struct { + major, minor string + metrics bool + e issues.Outcome + }{ + "good": { + major: "1", minor: "29", + metrics: true, + e: map[string]issues.Issues{ + "Version": { + { + GVR: "clusters", + Group: issues.Root, + Message: "[POP-406] K8s version OK", + Level: rules.OkLevel, + }, + }, + }, + }, + "gizzard": { + major: "1", minor: "11", + metrics: false, + e: map[string]issues.Issues{ + "Version": { + { + GVR: "clusters", + Group: issues.Root, + Message: "[POP-405] Is this a jurassic cluster? Might want to upgrade K8s a bit", + Level: rules.WarnLevel, + }, + }, + }, + }, + } + + ctx := test.MakeContext("clusters", "cluster") + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + cl := NewCluster( + test.MakeCollector(t), + newMockCluster(u.major, u.minor, u.metrics), + ) + + assert.Nil(t, cl.Lint(ctx)) + assert.Equal(t, u.e, cl.Outcome()) + }) + } +} + +// Helpers... + +type mockCluster struct { + major, minor string + metrics bool +} + +func newMockCluster(major, minor string, metrics bool) mockCluster { + return mockCluster{major: major, minor: minor, metrics: metrics} +} + +func (c mockCluster) ListVersion() *semver.Version { + v, _ := semver.NewVersion(c.major + "." + c.minor) + return v +} + +func (c mockCluster) HasMetrics() bool { + return c.metrics +} diff --git a/internal/lint/cm.go b/internal/lint/cm.go new file mode 100644 index 00000000..84c5bc94 --- /dev/null +++ b/internal/lint/cm.go @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "context" + "sync" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/cache" + "github.com/derailed/popeye/internal/client" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/issues" + v1 "k8s.io/api/core/v1" +) + +// ConfigMap tracks ConfigMap sanitization. +type ConfigMap struct { + *issues.Collector + db *db.DB + system excludedFQN +} + +// NewConfigMap returns a new instance. +func NewConfigMap(c *issues.Collector, db *db.DB) *ConfigMap { + return &ConfigMap{ + Collector: c, + db: db, + system: excludedFQN{ + "rx:^kube-public": {}, + "rx:kube-root-ca.crt": {}, + }, + } +} + +// Lint lints the resource. +func (s *ConfigMap) Lint(ctx context.Context) error { + var cmRefs sync.Map + if err := cache.NewPod(s.db).PodRefs(&cmRefs); err != nil { + return err + } + + return s.checkStale(ctx, &cmRefs) +} + +func (s *ConfigMap) checkStale(ctx context.Context, refs *sync.Map) error { + txn, it := s.db.MustITFor(internal.Glossary[internal.CM]) + defer txn.Abort() + for o := it.Next(); o != nil; o = it.Next() { + cm := o.(*v1.ConfigMap) + fqn := client.FQN(cm.Namespace, cm.Name) + s.InitOutcome(fqn) + ctx = internal.WithSpec(ctx, specFor(fqn, cm)) + if s.system.skip(fqn) { + continue + } + + keys, ok := refs.Load(cache.ResFqn(cache.ConfigMapKey, fqn)) + if !ok { + s.AddCode(ctx, 400) + continue + } + if keys.(internal.StringSet).Has(internal.All) { + continue + } + kk := make(internal.StringSet, len(cm.Data)) + for k := range cm.Data { + kk.Add(k) + } + deltas := keys.(internal.StringSet).Diff(kk) + for k := range deltas { + s.AddCode(ctx, 401, k) + } + } + + return nil +} diff --git a/internal/lint/cm_test.go b/internal/lint/cm_test.go new file mode 100644 index 00000000..606226af --- /dev/null +++ b/internal/lint/cm_test.go @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "testing" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/rules" + "github.com/derailed/popeye/internal/test" + "github.com/stretchr/testify/assert" + v1 "k8s.io/api/core/v1" +) + +func TestConfigMapLint(t *testing.T) { + dba, err := test.NewTestDB() + assert.NoError(t, err) + l := db.NewLoader(dba) + + ctx := test.MakeCtx(t) + assert.NoError(t, test.LoadDB[*v1.ConfigMap](ctx, l.DB, "core/cm/1.yaml", internal.Glossary[internal.CM])) + assert.NoError(t, test.LoadDB[*v1.Pod](ctx, l.DB, "core/pod/1.yaml", internal.Glossary[internal.PO])) + + cm := NewConfigMap(test.MakeCollector(t), dba) + assert.Nil(t, cm.Lint(test.MakeContext("v1/configmaps", "configmaps"))) + assert.Equal(t, 4, len(cm.Outcome())) + + ii := cm.Outcome()["default/cm1"] + assert.Equal(t, 1, len(ii)) + assert.Equal(t, "[POP-401] Key \"fred.yaml\" used? Unable to locate key reference", ii[0].Message) + assert.Equal(t, rules.InfoLevel, ii[0].Level) + + ii = cm.Outcome()["default/cm2"] + assert.Equal(t, 1, len(ii)) + assert.Equal(t, "[POP-400] Used? Unable to locate resource reference", ii[0].Message) + assert.Equal(t, rules.InfoLevel, ii[0].Level) + + ii = cm.Outcome()["default/cm3"] + assert.Equal(t, 0, len(ii)) + + ii = cm.Outcome()["default/cm4"] + assert.Equal(t, 1, len(ii)) + assert.Equal(t, `[POP-400] Used? Unable to locate resource reference`, ii[0].Message) + assert.Equal(t, rules.InfoLevel, ii[0].Level) +} + +// ---------------------------------------------------------------------------- +// Helpers... + +// type mockConfigMap struct{} + +// func newMockConfigMap() mockConfigMap { +// return mockConfigMap{} +// } + +// func (c mockConfigMap) PodRefs(refs *sync.Map) { +// refs.Store("cm:default/cm1", internal.StringSet{ +// "k1": internal.Blank, +// "k2": internal.Blank, +// }) +// refs.Store("cm:default/cm2", internal.AllKeys) +// refs.Store("cm:default/cm4", internal.StringSet{ +// "k1": internal.Blank, +// }) +// } + +// func (c mockConfigMap) ListConfigMaps() map[string]*v1.ConfigMap { +// return map[string]*v1.ConfigMap{ +// "default/cm1": makeMockConfigMap("cm1"), +// "default/cm2": makeMockConfigMap("cm2"), +// "default/cm3": makeMockConfigMap("cm3"), +// "default/cm4": makeMockConfigMap("cm4"), +// } +// } + +// func makeMockConfigMap(n string) *v1.ConfigMap { +// return &v1.ConfigMap{ +// ObjectMeta: metav1.ObjectMeta{ +// Name: n, +// Namespace: "default", +// }, +// Data: map[string]string{ +// "k1": "", +// "k2": "", +// }, +// } +// } diff --git a/internal/sanitize/container.go b/internal/lint/container.go similarity index 93% rename from internal/sanitize/container.go rename to internal/lint/container.go index 60cdc925..b3be6aa9 100644 --- a/internal/sanitize/container.go +++ b/internal/lint/container.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright Authors of Popeye -package sanitize +package lint import ( "context" @@ -9,6 +9,7 @@ import ( "github.com/derailed/popeye/internal" "github.com/derailed/popeye/internal/client" + "github.com/derailed/popeye/types" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/util/intstr" ) @@ -33,14 +34,13 @@ type ( } ) -// NewContainer returns a new sanitizer. +// NewContainer returns a new instance. func NewContainer(fqn string, c LimitCollector) *Container { return &Container{fqn: fqn, LimitCollector: c} } func (c *Container) sanitize(ctx context.Context, co v1.Container, checkProbes bool) { - ctx = internal.WithFQN(ctx, c.fqn) - ctx = internal.WithGroup(ctx, client.NewGVR("containers"), co.Name) + ctx = internal.WithGroup(ctx, types.NewGVR("containers"), co.Name) c.checkImageTags(ctx, co.Image) if c.allowedRegistryListExists() { c.checkImageRegistry(ctx, co.Image) @@ -86,16 +86,16 @@ func (c *Container) checkProbes(ctx context.Context, co v1.Container) { c.AddSubCode(ctx, 102) return } - if co.LivenessProbe == nil { c.AddSubCode(ctx, 103) + } else { + c.checkNamedProbe(ctx, co.LivenessProbe, true) } - c.checkNamedProbe(ctx, co.LivenessProbe, true) - if co.ReadinessProbe == nil { c.AddSubCode(ctx, 104) + } else { + c.checkNamedProbe(ctx, co.ReadinessProbe, false) } - c.checkNamedProbe(ctx, co.ReadinessProbe, false) } func (c *Container) checkNamedProbe(ctx context.Context, p *v1.Probe, liveness bool) { diff --git a/internal/sanitize/container_bench_test.go b/internal/lint/container_bench_test.go similarity index 95% rename from internal/sanitize/container_bench_test.go rename to internal/lint/container_bench_test.go index c1c41a87..150dcc8d 100644 --- a/internal/sanitize/container_bench_test.go +++ b/internal/lint/container_bench_test.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright Authors of Popeye -package sanitize +package lint import ( "context" diff --git a/internal/sanitize/container_status.go b/internal/lint/container_status.go similarity index 84% rename from internal/sanitize/container_status.go rename to internal/lint/container_status.go index 3e93542e..4fde185a 100644 --- a/internal/sanitize/container_status.go +++ b/internal/lint/container_status.go @@ -1,14 +1,14 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright Authors of Popeye -package sanitize +package lint import ( "context" "github.com/derailed/popeye/internal" - "github.com/derailed/popeye/internal/client" - "github.com/derailed/popeye/pkg/config" + "github.com/derailed/popeye/internal/rules" + "github.com/derailed/popeye/types" v1 "k8s.io/api/core/v1" ) @@ -37,8 +37,8 @@ func newContainerStatus(c Collector, fqn string, count int, isInit bool, restart } func (c *containerStatus) sanitize(ctx context.Context, s v1.ContainerStatus) { - ctx = internal.WithFQN(ctx, c.fqn) - ctx = internal.WithGroup(ctx, client.NewGVR("containers"), s.Name) + ctx = internal.WithGroup(ctx, types.NewGVR("containers"), s.Name) + c.rollup(s) if c.terminated > 0 && c.ready == 0 { return @@ -75,10 +75,10 @@ func (c *containerStatus) rollup(s v1.ContainerStatus) { c.restarts += int(s.RestartCount) } -func (c *containerStatus) checkReason(ctx context.Context, code config.ID, reason string) { +func (c *containerStatus) checkReason(ctx context.Context, code rules.ID, reason string) { if reason == "" { c.collector.AddSubCode(ctx, code, c.ready, c.count) return } - c.collector.AddSubCode(ctx, config.ID(code+1), c.ready, c.count, c.reason) + c.collector.AddSubCode(ctx, rules.ID(code+1), c.ready, c.count, c.reason) } diff --git a/internal/sanitize/container_status_test.go b/internal/lint/container_status_test.go similarity index 67% rename from internal/sanitize/container_status_test.go rename to internal/lint/container_status_test.go index 66a9aeca..c928a467 100644 --- a/internal/sanitize/container_status_test.go +++ b/internal/lint/container_status_test.go @@ -1,19 +1,20 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright Authors of Popeye -package sanitize +package lint import ( "testing" - "github.com/derailed/popeye/internal/client" "github.com/derailed/popeye/internal/issues" - "github.com/derailed/popeye/pkg/config" + "github.com/derailed/popeye/internal/rules" + "github.com/derailed/popeye/internal/test" + "github.com/derailed/popeye/types" "github.com/stretchr/testify/assert" v1 "k8s.io/api/core/v1" ) -func TestContainerStatusSanitize(t *testing.T) { +func TestContainerStatusLint(t *testing.T) { uu := map[string]struct { cs v1.ContainerStatus issues int @@ -37,7 +38,7 @@ func TestContainerStatusSanitize(t *testing.T) { State: v1.ContainerState{}, }, 1, - issues.New(client.NewGVR("containers"), "c1", config.ErrorLevel, "[POP-204] Pod is not ready [0/1]"), + issues.New(types.NewGVR("containers"), "c1", rules.ErrorLevel, "[POP-204] Pod is not ready [0/1]"), }, "waitingNoReason": { v1.ContainerStatus{ @@ -49,7 +50,7 @@ func TestContainerStatusSanitize(t *testing.T) { }, }, 1, - issues.New(client.NewGVR("containers"), "c1", config.ErrorLevel, "[POP-203] Pod is waiting [0/1] blah"), + issues.New(types.NewGVR("containers"), "c1", rules.ErrorLevel, "[POP-203] Pod is waiting [0/1] blah"), }, "waiting": { v1.ContainerStatus{ @@ -61,7 +62,7 @@ func TestContainerStatusSanitize(t *testing.T) { }, }, 1, - issues.New(client.NewGVR("containers"), "c1", config.ErrorLevel, "[POP-202] Pod is waiting [0/1]"), + issues.New(types.NewGVR("containers"), "c1", rules.ErrorLevel, "[POP-202] Pod is waiting [0/1]"), }, "terminatedReason": { v1.ContainerStatus{ @@ -73,7 +74,7 @@ func TestContainerStatusSanitize(t *testing.T) { }, }, 1, - issues.New(client.NewGVR("containers"), "c1", config.WarnLevel, "[POP-201] Pod is terminating [1/1] blah"), + issues.New(types.NewGVR("containers"), "c1", rules.WarnLevel, "[POP-201] Pod is terminating [1/1] blah"), }, "terminated": { v1.ContainerStatus{ @@ -85,7 +86,7 @@ func TestContainerStatusSanitize(t *testing.T) { }, }, 1, - issues.New(client.NewGVR("containers"), "c1", config.WarnLevel, "[POP-200] Pod is terminating [1/1]"), + issues.New(types.NewGVR("containers"), "c1", rules.WarnLevel, "[POP-200] Pod is terminating [1/1]"), }, "terminatedNotReady": { v1.ContainerStatus{ @@ -106,21 +107,21 @@ func TestContainerStatusSanitize(t *testing.T) { RestartCount: 11, }, 1, - issues.New(client.NewGVR("containers"), "c1", config.WarnLevel, "[POP-205] Pod was restarted (11) times"), + issues.New(types.NewGVR("containers"), "c1", rules.WarnLevel, "[POP-205] Pod was restarted (11) times"), }, } - ctx := makeContext("containers", "containers") + ctx := test.MakeContext("containers", "containers") for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { - c := issues.NewCollector(loadCodes(t), makeConfig(t)) + c := test.MakeCollector(t) cs := newContainerStatus(c, "default/p1", 1, false, 10) cs.sanitize(ctx, u.cs) - assert.Equal(t, u.issues, len(c.Outcome()["default/p1"])) + assert.Equal(t, u.issues, len(c.Outcome()[""])) if u.issues != 0 { - assert.Equal(t, u.issue, c.Outcome()["default/p1"][0]) + assert.Equal(t, u.issue, c.Outcome()[""][0]) } }) } diff --git a/internal/sanitize/container_test.go b/internal/lint/container_test.go similarity index 69% rename from internal/sanitize/container_test.go rename to internal/lint/container_test.go index d8122b77..6e78fff5 100644 --- a/internal/sanitize/container_test.go +++ b/internal/lint/container_test.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright Authors of Popeye -package sanitize +package lint import ( "testing" @@ -9,10 +9,11 @@ import ( "github.com/derailed/popeye/internal" "github.com/derailed/popeye/internal/client" "github.com/derailed/popeye/internal/issues" - "github.com/derailed/popeye/pkg/config" + "github.com/derailed/popeye/internal/rules" + "github.com/derailed/popeye/internal/test" + "github.com/derailed/popeye/types" "github.com/stretchr/testify/assert" v1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/resource" "k8s.io/apimachinery/pkg/util/intstr" ) @@ -29,7 +30,7 @@ func TestContainerCheckUtilization(t *testing.T) { lcpu: "10m", lmem: "10Mi", }), - mx: client.Metrics{CurrentCPU: toQty("1m"), CurrentMEM: toQty("1Mi")}, + mx: client.Metrics{CurrentCPU: test.ToQty("1m"), CurrentMEM: test.ToQty("1Mi")}, }, "cpuOver": { co: makeContainer("c1", coOpts{ @@ -38,7 +39,7 @@ func TestContainerCheckUtilization(t *testing.T) { lcpu: "100m", lmem: "10Mi", }), - mx: client.Metrics{CurrentCPU: toQty("200m"), CurrentMEM: toQty("2Mi")}, + mx: client.Metrics{CurrentCPU: test.ToQty("200m"), CurrentMEM: test.ToQty("2Mi")}, issues: 1, }, "memOver": { @@ -48,7 +49,7 @@ func TestContainerCheckUtilization(t *testing.T) { lcpu: "100m", lmem: "10Mi", }), - mx: client.Metrics{CurrentCPU: toQty("10m"), CurrentMEM: toQty("20Mi")}, + mx: client.Metrics{CurrentCPU: test.ToQty("10m"), CurrentMEM: test.ToQty("20Mi")}, issues: 1, }, "bothOver": { @@ -58,7 +59,7 @@ func TestContainerCheckUtilization(t *testing.T) { lcpu: "100m", lmem: "10Mi", }), - mx: client.Metrics{CurrentCPU: toQty("5"), CurrentMEM: toQty("20Mi")}, + mx: client.Metrics{CurrentCPU: test.ToQty("5"), CurrentMEM: test.ToQty("20Mi")}, issues: 2, }, "LimOver": { @@ -68,18 +69,18 @@ func TestContainerCheckUtilization(t *testing.T) { lcpu: "100m", lmem: "10Mi", }), - mx: client.Metrics{CurrentCPU: toQty("5"), CurrentMEM: toQty("20Mi")}, + mx: client.Metrics{CurrentCPU: test.ToQty("5"), CurrentMEM: test.ToQty("20Mi")}, issues: 2, }, } - ctx := makeContext("containers", "container") - ctx = internal.WithFQN(ctx, "default/p1") + ctx := test.MakeContext("containers", "container") + ctx = internal.WithSpec(ctx, specFor("default/p1", nil)) for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { c := NewContainer("default/p1", newRangeCollector(t)) - ctx = internal.WithGroup(ctx, client.NewGVR("containers"), u.co.Name) + ctx = internal.WithGroup(ctx, types.NewGVR("containers"), u.co.Name) c.checkUtilization(ctx, u.co, u.mx) assert.Equal(t, u.issues, len(c.Outcome().For("default/p1", "c1"))) @@ -92,15 +93,15 @@ func TestContainerCheckResources(t *testing.T) { request bool limit bool issues int - severity config.Level + severity rules.Level }{ "cool": {request: true, limit: true, issues: 0}, - "noLim": {request: true, issues: 1, severity: config.WarnLevel}, + "noLim": {request: true, issues: 1, severity: rules.WarnLevel}, "noReq": {limit: true, issues: 0}, - "none": {issues: 1, severity: config.WarnLevel}, + "none": {issues: 1, severity: rules.WarnLevel}, } - ctx := makeContext("containers", "container") + ctx := test.MakeContext("containers", "container") for k := range uu { u := uu[k] opts := coOpts{} @@ -116,8 +117,8 @@ func TestContainerCheckResources(t *testing.T) { l := NewContainer("default/p1", newRangeCollector(t)) t.Run(k, func(t *testing.T) { - ctx = internal.WithFQN(ctx, "default/p1") - ctx = internal.WithGroup(ctx, client.NewGVR("containers"), co.Name) + ctx = internal.WithSpec(ctx, specFor("default/p1", nil)) + ctx = internal.WithGroup(ctx, types.NewGVR("containers"), co.Name) l.checkResources(ctx, co) assert.Equal(t, u.issues, len(l.Outcome()["default/p1"])) @@ -134,16 +135,16 @@ func TestContainerCheckProbes(t *testing.T) { readiness bool namedPort bool issues int - severity config.Level + severity rules.Level }{ "cool": {liveness: true, readiness: true}, - "noReady": {liveness: true, issues: 1, severity: config.WarnLevel}, - "noLive": {readiness: true, issues: 1, severity: config.WarnLevel}, - "noneProbes": {issues: 1, severity: config.WarnLevel}, - "Unnamed": {liveness: true, readiness: true, namedPort: true, issues: 2, severity: config.InfoLevel}, + "noReady": {liveness: true, issues: 1, severity: rules.WarnLevel}, + "noLive": {readiness: true, issues: 1, severity: rules.WarnLevel}, + "noneProbes": {issues: 1, severity: rules.WarnLevel}, + "Unnamed": {liveness: true, readiness: true, namedPort: true, issues: 2, severity: rules.InfoLevel}, } - ctx := makeContext("containers", "container") + ctx := test.MakeContext("containers", "container") for k := range uu { u := uu[k] co := makeContainer("c1", coOpts{}) @@ -175,16 +176,16 @@ func TestContainerCheckImageTags(t *testing.T) { image string pissues int issues int - severity config.Level + severity rules.Level }{ "cool": {image: "cool:1.2.3", issues: 0}, - "noRev": {pissues: 1, image: "fred", issues: 1, severity: config.ErrorLevel}, - "latest": {pissues: 1, image: "fred:latest", issues: 1, severity: config.WarnLevel}, + "noRev": {pissues: 1, image: "fred", issues: 1, severity: rules.ErrorLevel}, + "latest": {pissues: 1, image: "fred:latest", issues: 1, severity: rules.WarnLevel}, } - ctx := makeContext("containers", "container") - ctx = internal.WithFQN(ctx, "default/p1") - ctx = internal.WithGroup(ctx, client.NewGVR("containers"), "c1") + ctx := test.MakeContext("containers", "container") + ctx = internal.WithSpec(ctx, specFor("default/p1", nil)) + ctx = internal.WithGroup(ctx, types.NewGVR("containers"), "c1") for k := range uu { u := uu[k] co := makeContainer("c1", coOpts{}) @@ -208,16 +209,16 @@ func TestContainerCheckImageRegistry(t *testing.T) { image string pissues int issues int - severity config.Level + severity rules.Level }{ "dockerDefault": {image: "dockerhub:1.2.3", issues: 0}, "cool": {image: "docker.io/cool:1.2.3", issues: 0}, - "wrongRegistry": {pissues: 1, image: "wrong-registry.io/fred", issues: 1, severity: config.ErrorLevel}, + "wrongRegistry": {pissues: 1, image: "wrong-registry.io/fred", issues: 1, severity: rules.ErrorLevel}, } - ctx := makeContext("containers", "container") - ctx = internal.WithFQN(ctx, "default/p1") - ctx = internal.WithGroup(ctx, client.NewGVR("containers"), "c1") + ctx := test.MakeContext("containers", "container") + ctx = internal.WithSpec(ctx, specFor("default/p1", nil)) + ctx = internal.WithGroup(ctx, types.NewGVR("containers"), "c1") for k := range uu { u := uu[k] co := makeContainer("c1", coOpts{}) @@ -240,15 +241,15 @@ func TestContainerCheckNamedPorts(t *testing.T) { uu := map[string]struct { port string issues int - severity config.Level + severity rules.Level }{ "named": {port: "cool", issues: 0}, - "unamed": {port: "", issues: 1, severity: config.WarnLevel}, + "unamed": {port: "", issues: 1, severity: rules.WarnLevel}, } - ctx := makeContext("containers", "container") - ctx = internal.WithFQN(ctx, "p1") - ctx = internal.WithGroup(ctx, client.NewGVR("v1/pods"), "p1") + ctx := test.MakeContext("containers", "container") + ctx = internal.WithSpec(ctx, specFor("p1", nil)) + ctx = internal.WithGroup(ctx, types.NewGVR("v1/pods"), "p1") for k := range uu { u := uu[k] co := makeContainer("c1", coOpts{}) @@ -266,7 +267,7 @@ func TestContainerCheckNamedPorts(t *testing.T) { } } -func TestContainerSanitize(t *testing.T) { +func TestContainerLint(t *testing.T) { uu := map[string]struct { co v1.Container issues int @@ -274,15 +275,15 @@ func TestContainerSanitize(t *testing.T) { "NoImgNoProbs": {makeContainer("c1", coOpts{}), 3}, } - ctx := makeContext("containers", "container") + ctx := test.MakeContext("containers", "container") for k := range uu { u := uu[k] c := NewContainer("default/p1", newRangeCollector(t)) t.Run(k, func(t *testing.T) { c.sanitize(ctx, u.co, true) - assert.Equal(t, 3, len(c.Outcome()["default/p1"])) - assert.Equal(t, u.issues, len(c.Outcome().For("default/p1", "c1"))) + assert.Equal(t, 3, len(c.Outcome()[""])) + assert.Equal(t, u.issues, len(c.Outcome().For("", "c1"))) }) } } @@ -295,13 +296,14 @@ type rangeCollector struct { } func newRangeCollector(t *testing.T) *rangeCollector { - return &rangeCollector{issues.NewCollector(loadCodes(t), makeConfig(t))} + return &rangeCollector{test.MakeCollector(t)} } func newRangeCollectorWithRegistry(t *testing.T) *rangeCollector { - cfg := makeConfig(t) - cfg.Registries = append(cfg.Registries, "docker.io") - return &rangeCollector{issues.NewCollector(loadCodes(t), cfg)} + c := rangeCollector{test.MakeCollector(t)} + c.Config.Registries = []string{"docker.io"} + + return &c } func (*rangeCollector) RestartsLimit() int { @@ -331,10 +333,10 @@ func makeContainer(n string, opts coOpts) v1.Container { } if opts.rcpu != "" { - co.Resources.Requests = makeRes(opts.rcpu, opts.rmem) + co.Resources.Requests = test.MakeRes(opts.rcpu, opts.rmem) } if opts.lcpu != "" { - co.Resources.Limits = makeRes(opts.lcpu, opts.lmem) + co.Resources.Limits = test.MakeRes(opts.lcpu, opts.lmem) } if opts.lprob { co.LivenessProbe = &v1.Probe{} @@ -345,19 +347,3 @@ func makeContainer(n string, opts coOpts) v1.Container { return co } - -func makeRes(c, m string) v1.ResourceList { - return v1.ResourceList{ - v1.ResourceCPU: *makeQty(c), - v1.ResourceMemory: *makeQty(m), - } -} - -func makeQty(s string) *resource.Quantity { - if s == "" { - return nil - } - - qty, _ := resource.ParseQuantity(s) - return &qty -} diff --git a/internal/lint/cr.go b/internal/lint/cr.go new file mode 100644 index 00000000..f801892e --- /dev/null +++ b/internal/lint/cr.go @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "context" + "sync" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/cache" + "github.com/derailed/popeye/internal/client" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/issues" + "github.com/derailed/popeye/internal/rules" + rbacv1 "k8s.io/api/rbac/v1" +) + +type excludedFQN map[rules.Expression]struct{} + +func (e excludedFQN) skip(fqn string) bool { + if _, ok := e[rules.Expression(fqn)]; ok { + return true + } + for k := range e { + if k.IsRX() && k.MatchRX(fqn) { + return true + } + } + + return false +} + +// ClusterRole tracks ClusterRole sanitization. +type ClusterRole struct { + *issues.Collector + + db *db.DB + system excludedFQN +} + +// NewClusterRole returns a new instance. +func NewClusterRole(c *issues.Collector, db *db.DB) *ClusterRole { + return &ClusterRole{ + Collector: c, + db: db, + system: excludedFQN{ + "admin": {}, + "edit": {}, + "view": {}, + "rx:^system:": {}, + }, + } +} + +// Lint sanitizes the resource. +func (s *ClusterRole) Lint(ctx context.Context) error { + var crRefs sync.Map + crb := cache.NewClusterRoleBinding(s.db) + crb.ClusterRoleRefs(&crRefs) + rb := cache.NewRoleBinding(s.db) + rb.RoleRefs(&crRefs) + s.checkStale(ctx, &crRefs) + + return nil +} + +func (s *ClusterRole) checkStale(ctx context.Context, refs *sync.Map) { + txn, it := s.db.MustITFor(internal.Glossary[internal.CR]) + defer txn.Abort() + for o := it.Next(); o != nil; o = it.Next() { + cr := o.(*rbacv1.ClusterRole) + fqn := client.FQN(cr.Namespace, cr.Name) + s.InitOutcome(fqn) + ctx = internal.WithSpec(ctx, specFor(fqn, cr)) + if s.system.skip(fqn) { + continue + } + if _, ok := refs.Load(cache.ResFqn(cache.ClusterRoleKey, fqn)); !ok { + s.AddCode(ctx, 400) + } + } +} diff --git a/internal/lint/cr_test.go b/internal/lint/cr_test.go new file mode 100644 index 00000000..96a0782c --- /dev/null +++ b/internal/lint/cr_test.go @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "testing" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/rules" + "github.com/derailed/popeye/internal/test" + "github.com/stretchr/testify/assert" + v1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" +) + +func TestCRLint(t *testing.T) { + dba, err := test.NewTestDB() + assert.NoError(t, err) + l := db.NewLoader(dba) + + ctx := test.MakeCtx(t) + assert.NoError(t, test.LoadDB[*rbacv1.ClusterRole](ctx, l.DB, "auth/cr/1.yaml", internal.Glossary[internal.CR])) + assert.NoError(t, test.LoadDB[*rbacv1.ClusterRoleBinding](ctx, l.DB, "auth/crb/1.yaml", internal.Glossary[internal.CRB])) + assert.NoError(t, test.LoadDB[*rbacv1.RoleBinding](ctx, l.DB, "auth/rob/1.yaml", internal.Glossary[internal.ROB])) + assert.NoError(t, test.LoadDB[*v1.ServiceAccount](ctx, l.DB, "core/sa/1.yaml", internal.Glossary[internal.SA])) + + cr := NewClusterRole(test.MakeCollector(t), dba) + assert.Nil(t, cr.Lint(test.MakeContext("rbac.authorization.k8s.io/v1/clusterroles", "clusterroles"))) + assert.Equal(t, 3, len(cr.Outcome())) + + ii := cr.Outcome()["cr1"] + assert.Equal(t, 0, len(ii)) + + ii = cr.Outcome()["cr2"] + assert.Equal(t, 1, len(ii)) + assert.Equal(t, `[POP-400] Used? Unable to locate resource reference`, ii[0].Message) + assert.Equal(t, rules.InfoLevel, ii[0].Level) + + ii = cr.Outcome()["cr3"] + assert.Equal(t, 1, len(ii)) + assert.Equal(t, `[POP-400] Used? Unable to locate resource reference`, ii[0].Message) + assert.Equal(t, rules.InfoLevel, ii[0].Level) +} diff --git a/internal/lint/crb.go b/internal/lint/crb.go new file mode 100644 index 00000000..6b4e837d --- /dev/null +++ b/internal/lint/crb.go @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "context" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/cache" + "github.com/derailed/popeye/internal/client" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/issues" + rbacv1 "k8s.io/api/rbac/v1" +) + +type ( + // ClusterRoleBinding tracks ClusterRoleBinding sanitization. + ClusterRoleBinding struct { + *issues.Collector + + db *db.DB + } +) + +// NewClusterRoleBinding returns a new instance. +func NewClusterRoleBinding(c *issues.Collector, db *db.DB) *ClusterRoleBinding { + return &ClusterRoleBinding{ + Collector: c, + db: db, + } +} + +// Lint sanitizes the resource. +func (c *ClusterRoleBinding) Lint(ctx context.Context) error { + c.checkInUse(ctx) + + return nil +} + +func (c *ClusterRoleBinding) checkInUse(ctx context.Context) { + txn, it := c.db.MustITFor(internal.Glossary[internal.CRB]) + defer txn.Abort() + for o := it.Next(); o != nil; o = it.Next() { + crb := o.(*rbacv1.ClusterRoleBinding) + fqn := client.FQN(crb.Namespace, crb.Name) + + c.InitOutcome(fqn) + ctx = internal.WithSpec(ctx, specFor(fqn, crb)) + + switch crb.RoleRef.Kind { + case "ClusterRole": + if !c.db.Exists(internal.Glossary[internal.CR], crb.RoleRef.Name) { + c.AddCode(ctx, 1300, crb.RoleRef.Kind, crb.RoleRef.Name) + } + case "Role": + rFQN := cache.FQN(crb.Namespace, crb.RoleRef.Name) + if !c.db.Exists(internal.Glossary[internal.RO], rFQN) { + c.AddCode(ctx, 1300, crb.RoleRef.Kind, rFQN) + } + } + for _, s := range crb.Subjects { + if s.Kind == "ServiceAccount" { + safqn := cache.FQN(s.Namespace, s.Name) + if !c.db.Exists(internal.Glossary[internal.SA], safqn) { + c.AddCode(ctx, 1300, s.Kind, safqn) + } + } + } + } +} diff --git a/internal/lint/crb_test.go b/internal/lint/crb_test.go new file mode 100644 index 00000000..a3235914 --- /dev/null +++ b/internal/lint/crb_test.go @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "testing" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/rules" + "github.com/derailed/popeye/internal/test" + "github.com/stretchr/testify/assert" + v1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" +) + +func TestCRBLint(t *testing.T) { + dba, err := test.NewTestDB() + assert.NoError(t, err) + l := db.NewLoader(dba) + + ctx := test.MakeCtx(t) + assert.NoError(t, test.LoadDB[*rbacv1.ClusterRoleBinding](ctx, l.DB, "auth/crb/1.yaml", internal.Glossary[internal.CRB])) + assert.NoError(t, test.LoadDB[*rbacv1.ClusterRole](ctx, l.DB, "auth/cr/1.yaml", internal.Glossary[internal.CR])) + assert.NoError(t, test.LoadDB[*rbacv1.Role](ctx, l.DB, "auth/ro/1.yaml", internal.Glossary[internal.RO])) + assert.NoError(t, test.LoadDB[*v1.ServiceAccount](ctx, l.DB, "core/sa/1.yaml", internal.Glossary[internal.SA])) + + crb := NewClusterRoleBinding(test.MakeCollector(t), dba) + assert.Nil(t, crb.Lint(test.MakeContext("rbac.authorization.k8s.io/v1/clusterrolebindings", "clusterrolebindings"))) + assert.Equal(t, 3, len(crb.Outcome())) + + ii := crb.Outcome()["crb1"] + assert.Equal(t, 0, len(ii)) + + ii = crb.Outcome()["crb2"] + assert.Equal(t, 1, len(ii)) + assert.Equal(t, `[POP-1300] References a ClusterRole (cr-bozo) which does not exist`, ii[0].Message) + assert.Equal(t, rules.WarnLevel, ii[0].Level) + + ii = crb.Outcome()["crb3"] + assert.Equal(t, 2, len(ii)) + assert.Equal(t, `[POP-1300] References a Role (r-bozo) which does not exist`, ii[0].Message) + assert.Equal(t, rules.WarnLevel, ii[0].Level) + assert.Equal(t, `[POP-1300] References a ServiceAccount (default/sa-bozo) which does not exist`, ii[1].Message) + assert.Equal(t, rules.WarnLevel, ii[1].Level) +} diff --git a/internal/lint/cronjob.go b/internal/lint/cronjob.go new file mode 100644 index 00000000..c893407f --- /dev/null +++ b/internal/lint/cronjob.go @@ -0,0 +1,138 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "context" + "errors" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/cache" + "github.com/derailed/popeye/internal/client" + "github.com/derailed/popeye/internal/dao" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/issues" + batchv1 "k8s.io/api/batch/v1" + v1 "k8s.io/api/core/v1" +) + +// CronJob tracks CronJob linting. +type CronJob struct { + *issues.Collector + + db *db.DB +} + +// NewCronJob returns a new instance. +func NewCronJob(co *issues.Collector, db *db.DB) *CronJob { + return &CronJob{ + Collector: co, + db: db, + } +} + +// Lint cleanse the resource. +func (s *CronJob) Lint(ctx context.Context) error { + over := pullOverAllocs(ctx) + txn, it := s.db.MustITFor(internal.Glossary[internal.CJOB]) + defer txn.Abort() + for o := it.Next(); o != nil; o = it.Next() { + cj := o.(*batchv1.CronJob) + fqn := client.FQN(cj.Namespace, cj.Name) + s.InitOutcome(fqn) + ctx = internal.WithSpec(ctx, specFor(fqn, cj)) + s.checkCronJob(ctx, fqn, cj) + s.checkContainers(ctx, fqn, cj.Spec.JobTemplate.Spec.Template.Spec) + s.checkUtilization(ctx, over, fqn) + } + + return nil +} + +// CheckCronJob checks if CronJob contract is currently happy or not. +func (s *CronJob) checkCronJob(ctx context.Context, fqn string, cj *batchv1.CronJob) { + checkEvents(ctx, s.Collector, internal.CJOB, "", "CronJob", fqn) + + if cj.Spec.Suspend != nil && *cj.Spec.Suspend { + s.AddCode(ctx, 1500, cj.Kind) + } + + if len(cj.Status.Active) == 0 { + s.AddCode(ctx, 1501) + } + if cj.Status.LastSuccessfulTime == nil { + s.AddCode(ctx, 1502) + } + + if sa := cj.Spec.JobTemplate.Spec.Template.Spec.ServiceAccountName; sa != "" { + saFQN := client.FQN(cj.Namespace, sa) + if !s.db.Exists(internal.Glossary[internal.SA], saFQN) { + s.AddCode(ctx, 307, cj.Kind, sa) + } + } +} + +// CheckContainers runs thru CronJob template and checks pod configuration. +func (s *CronJob) checkContainers(ctx context.Context, fqn string, spec v1.PodSpec) { + c := NewContainer(fqn, s) + for _, co := range spec.InitContainers { + c.sanitize(ctx, co, false) + } + for _, co := range spec.Containers { + c.sanitize(ctx, co, false) + } +} + +// CheckUtilization checks CronJobs requested resources vs current utilization. +func (s *CronJob) checkUtilization(ctx context.Context, over bool, fqn string) { + jj, err := s.db.FindJobs(fqn) + if err != nil { + s.AddErr(ctx, err) + return + } + mx := jobResourceUsage(ctx, s.db, s, jj) + if mx.RequestCPU.IsZero() && mx.RequestMEM.IsZero() { + return + } + checkCPU(ctx, s, over, mx) + checkMEM(ctx, s, over, mx) +} + +// Helpers... + +func checkEvents(ctx context.Context, ii *issues.Collector, r internal.R, kind, object, fqn string) { + ee, err := dao.EventsFor(ctx, internal.Glossary[r], kind, object, fqn) + if err != nil { + ii.AddErr(ctx, err) + } + for _, e := range ee.Issues() { + ii.AddErr(ctx, errors.New(e)) + } +} + +func jobResourceUsage(ctx context.Context, dba *db.DB, c Collector, jobs []*batchv1.Job) ConsumptionMetrics { + var mx ConsumptionMetrics + + if len(jobs) == 0 { + return mx + } + + for _, job := range jobs { + fqn := cache.FQN(job.Namespace, job.Name) + cpu, mem := computePodResources(job.Spec.Template.Spec) + mx.RequestCPU.Add(cpu) + mx.RequestMEM.Add(mem) + + pmx, err := dba.FindPMX(fqn) + if err != nil { + continue + } + for _, cx := range pmx.Containers { + mx.CurrentCPU.Add(*cx.Usage.Cpu()) + mx.CurrentMEM.Add(*cx.Usage.Memory()) + } + } + + return mx +} diff --git a/internal/lint/cronjob_test.go b/internal/lint/cronjob_test.go new file mode 100644 index 00000000..eb6dbb20 --- /dev/null +++ b/internal/lint/cronjob_test.go @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "testing" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/rules" + "github.com/derailed/popeye/internal/test" + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" + batchv1 "k8s.io/api/batch/v1" + v1 "k8s.io/api/core/v1" + mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" +) + +func init() { + zerolog.SetGlobalLevel(zerolog.FatalLevel) +} + +func TestCronJobLint(t *testing.T) { + dba, err := test.NewTestDB() + assert.NoError(t, err) + l := db.NewLoader(dba) + + ctx := test.MakeCtx(t) + assert.NoError(t, test.LoadDB[*batchv1.CronJob](ctx, l.DB, "batch/cjob/1.yaml", internal.Glossary[internal.CJOB])) + assert.NoError(t, test.LoadDB[*batchv1.Job](ctx, l.DB, "batch/job/1.yaml", internal.Glossary[internal.JOB])) + assert.NoError(t, test.LoadDB[*v1.ServiceAccount](ctx, l.DB, "core/sa/1.yaml", internal.Glossary[internal.SA])) + assert.NoError(t, test.LoadDB[*v1.Pod](ctx, l.DB, "core/pod/1.yaml", internal.Glossary[internal.PO])) + assert.NoError(t, test.LoadDB[*mv1beta1.PodMetrics](ctx, l.DB, "mx/pod/1.yaml", internal.Glossary[internal.PMX])) + + cj := NewCronJob(test.MakeCollector(t), dba) + assert.Nil(t, cj.Lint(test.MakeContext("batch/v1/cronjobs", "cronjobs"))) + assert.Equal(t, 2, len(cj.Outcome())) + + ii := cj.Outcome()["default/cj1"] + assert.Equal(t, 2, len(ii)) + assert.Equal(t, `[POP-503] At current load, CPU under allocated. Current:2000m vs Requested:1m (200000.00%)`, ii[0].Message) + assert.Equal(t, rules.WarnLevel, ii[0].Level) + assert.Equal(t, `[POP-505] At current load, Memory under allocated. Current:20Mi vs Requested:1Mi (2000.00%)`, ii[1].Message) + assert.Equal(t, rules.WarnLevel, ii[1].Level) + + ii = cj.Outcome()["default/cj2"] + assert.Equal(t, 6, len(ii)) + assert.Equal(t, `[POP-1500] CronJob is suspended`, ii[0].Message) + assert.Equal(t, rules.WarnLevel, ii[0].Level) + assert.Equal(t, `[POP-1501] No active jobs detected`, ii[1].Message) + assert.Equal(t, rules.InfoLevel, ii[1].Level) + assert.Equal(t, `[POP-1502] CronJob has not run yet or is failing`, ii[2].Message) + assert.Equal(t, rules.WarnLevel, ii[2].Level) + assert.Equal(t, `[POP-307] CronJob references a non existing ServiceAccount: "sa-bozo"`, ii[3].Message) + assert.Equal(t, rules.WarnLevel, ii[3].Level) + assert.Equal(t, `[POP-100] Untagged docker image in use`, ii[4].Message) + assert.Equal(t, rules.ErrorLevel, ii[4].Level) + assert.Equal(t, `[POP-106] No resources requests/limits defined`, ii[5].Message) + assert.Equal(t, rules.WarnLevel, ii[5].Level) +} diff --git a/internal/lint/dp.go b/internal/lint/dp.go new file mode 100644 index 00000000..7c021496 --- /dev/null +++ b/internal/lint/dp.go @@ -0,0 +1,126 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "context" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/client" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/issues" + appsv1 "k8s.io/api/apps/v1" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" +) + +// Deployment tracks Deployment sanitization. +type Deployment struct { + *issues.Collector + + db *db.DB +} + +// NewDeployment returns a new instance. +func NewDeployment(co *issues.Collector, db *db.DB) *Deployment { + return &Deployment{ + Collector: co, + db: db, + } +} + +// Lint cleanse the resource. +func (s *Deployment) Lint(ctx context.Context) error { + over := pullOverAllocs(ctx) + txn, it := s.db.MustITFor(internal.Glossary[internal.DP]) + defer txn.Abort() + for o := it.Next(); o != nil; o = it.Next() { + dp := o.(*appsv1.Deployment) + fqn := client.FQN(dp.Namespace, dp.Name) + s.InitOutcome(fqn) + ctx = internal.WithSpec(ctx, specFor(fqn, dp)) + s.checkDeployment(ctx, dp) + s.checkContainers(ctx, fqn, dp.Spec.Template.Spec) + s.checkUtilization(ctx, over, dp) + } + + return nil +} + +// CheckDeployment checks if deployment contract is currently happy or not. +func (s *Deployment) checkDeployment(ctx context.Context, dp *appsv1.Deployment) { + if dp.Spec.Replicas == nil || (dp.Spec.Replicas != nil && *dp.Spec.Replicas == 0) { + s.AddCode(ctx, 500) + return + } + + if dp.Spec.Replicas != nil && *dp.Spec.Replicas != dp.Status.AvailableReplicas { + s.AddCode(ctx, 501, *dp.Spec.Replicas, dp.Status.AvailableReplicas) + } + + if dp.Spec.Template.Spec.ServiceAccountName == "" { + return + } + + saFQN := client.FQN(dp.Namespace, dp.Spec.Template.Spec.ServiceAccountName) + if !s.db.Exists(internal.Glossary[internal.SA], saFQN) { + s.AddCode(ctx, 507, dp.Spec.Template.Spec.ServiceAccountName) + } +} + +// CheckContainers runs thru deployment template and checks pod configuration. +func (s *Deployment) checkContainers(ctx context.Context, fqn string, spec v1.PodSpec) { + c := NewContainer(fqn, s) + for _, co := range spec.InitContainers { + c.sanitize(ctx, co, false) + } + for _, co := range spec.Containers { + c.sanitize(ctx, co, false) + } +} + +// CheckUtilization checks deployments requested resources vs current utilization. +func (s *Deployment) checkUtilization(ctx context.Context, over bool, dp *appsv1.Deployment) { + mx := resourceUsage(ctx, s.db, s, dp.Namespace, dp.Spec.Selector) + if mx.RequestCPU.IsZero() && mx.RequestMEM.IsZero() { + return + } + checkCPU(ctx, s, over, mx) + checkMEM(ctx, s, over, mx) +} + +// Helpers... + +// PullOverAllocs check for over allocation setting in context. +func pullOverAllocs(ctx context.Context) bool { + over := ctx.Value(internal.KeyOverAllocs) + if over == nil { + return false + } + return over.(bool) +} + +func computePodResources(spec v1.PodSpec) (cpu, mem resource.Quantity) { + for _, co := range spec.InitContainers { + c, m, _ := containerResources(co) + if c != nil { + cpu.Add(*c) + } + if m != nil { + mem.Add(*m) + } + } + + for _, co := range spec.Containers { + c, m, _ := containerResources(co) + if c != nil { + cpu.Add(*c) + } + if m != nil { + mem.Add(*m) + } + } + + return +} diff --git a/internal/lint/dp_test.go b/internal/lint/dp_test.go new file mode 100644 index 00000000..2f575db5 --- /dev/null +++ b/internal/lint/dp_test.go @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "testing" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/rules" + "github.com/derailed/popeye/internal/test" + "github.com/stretchr/testify/assert" + appsv1 "k8s.io/api/apps/v1" + v1 "k8s.io/api/core/v1" + mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" +) + +func TestDPLint(t *testing.T) { + dba, err := test.NewTestDB() + assert.NoError(t, err) + l := db.NewLoader(dba) + + ctx := test.MakeCtx(t) + assert.NoError(t, test.LoadDB[*appsv1.Deployment](ctx, l.DB, "apps/dp/1.yaml", internal.Glossary[internal.DP])) + assert.NoError(t, test.LoadDB[*v1.ServiceAccount](ctx, l.DB, "core/sa/1.yaml", internal.Glossary[internal.SA])) + assert.NoError(t, test.LoadDB[*v1.Pod](ctx, l.DB, "core/pod/1.yaml", internal.Glossary[internal.PO])) + assert.NoError(t, test.LoadDB[*mv1beta1.PodMetrics](ctx, l.DB, "mx/pod/1.yaml", internal.Glossary[internal.PMX])) + + dp := NewDeployment(test.MakeCollector(t), dba) + assert.Nil(t, dp.Lint(test.MakeContext("apps/v1/deployments", "deployments"))) + assert.Equal(t, 3, len(dp.Outcome())) + + ii := dp.Outcome()["default/dp1"] + assert.Equal(t, 2, len(ii)) + assert.Equal(t, `[POP-503] At current load, CPU under allocated. Current:20000m vs Requested:1000m (2000.00%)`, ii[0].Message) + assert.Equal(t, `[POP-505] At current load, Memory under allocated. Current:20Mi vs Requested:1Mi (2000.00%)`, ii[1].Message) + + ii = dp.Outcome()["default/dp2"] + assert.Equal(t, 5, len(ii)) + assert.Equal(t, `[POP-501] Unhealthy 1 desired but have 0 available`, ii[0].Message) + assert.Equal(t, rules.ErrorLevel, ii[0].Level) + assert.Equal(t, `[POP-507] Deployment references ServiceAccount "sa-bozo" which does not exist`, ii[1].Message) + assert.Equal(t, rules.ErrorLevel, ii[1].Level) + assert.Equal(t, `[POP-106] No resources requests/limits defined`, ii[2].Message) + assert.Equal(t, rules.WarnLevel, ii[2].Level) + assert.Equal(t, `[POP-108] Unnamed port 3000`, ii[3].Message) + assert.Equal(t, rules.InfoLevel, ii[3].Level) + assert.Equal(t, `[POP-508] No pods match controller selector: app=pod-bozo`, ii[4].Message) + assert.Equal(t, rules.ErrorLevel, ii[4].Level) + + ii = dp.Outcome()["default/dp3"] + assert.Equal(t, 2, len(ii)) + assert.Equal(t, `[POP-500] Zero scale detected`, ii[0].Message) + assert.Equal(t, rules.WarnLevel, ii[0].Level) + assert.Equal(t, `no pod selector given`, ii[1].Message) + assert.Equal(t, rules.ErrorLevel, ii[1].Level) +} diff --git a/internal/lint/ds.go b/internal/lint/ds.go new file mode 100644 index 00000000..7f5491ae --- /dev/null +++ b/internal/lint/ds.go @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "context" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/client" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/issues" + appsv1 "k8s.io/api/apps/v1" + v1 "k8s.io/api/core/v1" +) + +// DaemonSet tracks DaemonSet sanitization. +type DaemonSet struct { + *issues.Collector + + db *db.DB +} + +// NewDaemonSet returns a new instance. +func NewDaemonSet(co *issues.Collector, db *db.DB) *DaemonSet { + return &DaemonSet{ + Collector: co, + db: db, + } +} + +// Lint cleanse the resource. +func (s *DaemonSet) Lint(ctx context.Context) error { + over := pullOverAllocs(ctx) + txn, it := s.db.MustITFor(internal.Glossary[internal.DS]) + defer txn.Abort() + for o := it.Next(); o != nil; o = it.Next() { + ds := o.(*appsv1.DaemonSet) + fqn := client.FQN(ds.Namespace, ds.Name) + s.InitOutcome(fqn) + ctx = internal.WithSpec(ctx, specFor(fqn, ds)) + + s.checkDaemonSet(ctx, ds) + s.checkContainers(ctx, fqn, ds.Spec.Template.Spec) + s.checkUtilization(ctx, over, ds) + } + + return nil +} + +func (s *DaemonSet) checkDaemonSet(ctx context.Context, ds *appsv1.DaemonSet) { + if ds.Spec.Template.Spec.ServiceAccountName == "" { + return + } + _, err := s.db.Find(internal.Glossary[internal.SA], client.FQN(ds.Namespace, ds.Spec.Template.Spec.ServiceAccountName)) + if err != nil { + s.AddCode(ctx, 507, ds.Spec.Template.Spec.ServiceAccountName) + } +} + +// CheckContainers runs thru deployment template and checks pod configuration. +func (s *DaemonSet) checkContainers(ctx context.Context, fqn string, spec v1.PodSpec) { + c := NewContainer(fqn, s) + for _, co := range spec.InitContainers { + c.sanitize(ctx, co, false) + } + for _, co := range spec.Containers { + c.sanitize(ctx, co, false) + } +} + +// CheckUtilization checks deployments requested resources vs current utilization. +func (s *DaemonSet) checkUtilization(ctx context.Context, over bool, ds *appsv1.DaemonSet) { + mx := resourceUsage(ctx, s.db, s, ds.Namespace, ds.Spec.Selector) + if mx.RequestCPU.IsZero() && mx.RequestMEM.IsZero() { + return + } + + checkCPU(ctx, s, over, mx) + checkMEM(ctx, s, over, mx) +} diff --git a/internal/lint/ds_test.go b/internal/lint/ds_test.go new file mode 100644 index 00000000..66a0e9ce --- /dev/null +++ b/internal/lint/ds_test.go @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "testing" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/rules" + "github.com/derailed/popeye/internal/test" + "github.com/stretchr/testify/assert" + appsv1 "k8s.io/api/apps/v1" + v1 "k8s.io/api/core/v1" + mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" +) + +func TestDSLint(t *testing.T) { + dba, err := test.NewTestDB() + assert.NoError(t, err) + l := db.NewLoader(dba) + + ctx := test.MakeCtx(t) + assert.NoError(t, test.LoadDB[*appsv1.DaemonSet](ctx, l.DB, "apps/ds/1.yaml", internal.Glossary[internal.DS])) + assert.NoError(t, test.LoadDB[*v1.ServiceAccount](ctx, l.DB, "core/sa/1.yaml", internal.Glossary[internal.SA])) + assert.NoError(t, test.LoadDB[*v1.Pod](ctx, l.DB, "core/pod/1.yaml", internal.Glossary[internal.PO])) + assert.NoError(t, test.LoadDB[*mv1beta1.PodMetrics](ctx, l.DB, "mx/pod/1.yaml", internal.Glossary[internal.PMX])) + + ds := NewDaemonSet(test.MakeCollector(t), dba) + assert.Nil(t, ds.Lint(test.MakeContext("apps/v1/daemonsets", "daemonsets"))) + assert.Equal(t, 2, len(ds.Outcome())) + + ii := ds.Outcome()["default/ds1"] + assert.Equal(t, 2, len(ii)) + assert.Equal(t, `[POP-503] At current load, CPU under allocated. Current:20000m vs Requested:1000m (2000.00%)`, ii[0].Message) + assert.Equal(t, `[POP-505] At current load, Memory under allocated. Current:20Mi vs Requested:1Mi (2000.00%)`, ii[1].Message) + + ii = ds.Outcome()["default/ds2"] + assert.Equal(t, 6, len(ii)) + assert.Equal(t, `[POP-507] Deployment references ServiceAccount "sa-bozo" which does not exist`, ii[0].Message) + assert.Equal(t, rules.ErrorLevel, ii[0].Level) + assert.Equal(t, `[POP-100] Untagged docker image in use`, ii[1].Message) + assert.Equal(t, rules.ErrorLevel, ii[1].Level) + assert.Equal(t, `[POP-106] No resources requests/limits defined`, ii[2].Message) + assert.Equal(t, rules.WarnLevel, ii[2].Level) + assert.Equal(t, `[POP-100] Untagged docker image in use`, ii[3].Message) + assert.Equal(t, rules.ErrorLevel, ii[3].Level) + assert.Equal(t, `[POP-106] No resources requests/limits defined`, ii[4].Message) + assert.Equal(t, rules.WarnLevel, ii[4].Level) + assert.Equal(t, `[POP-508] No pods match controller selector: app=p10`, ii[5].Message) + assert.Equal(t, rules.ErrorLevel, ii[5].Level) +} diff --git a/internal/lint/gw.go b/internal/lint/gw.go new file mode 100644 index 00000000..f5b37539 --- /dev/null +++ b/internal/lint/gw.go @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "context" + "fmt" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/client" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/issues" + gwv1 "sigs.k8s.io/gateway-api/apis/v1" +) + +type ( + // Gateway tracks Gateway sanitization. + Gateway struct { + *issues.Collector + + db *db.DB + } +) + +// NewGateway returns a new instance. +func NewGateway(co *issues.Collector, db *db.DB) *Gateway { + return &Gateway{ + Collector: co, + db: db, + } +} + +// Lint cleanse the resource. +func (s *Gateway) Lint(ctx context.Context) error { + txn, it := s.db.MustITFor(internal.Glossary[internal.GW]) + defer txn.Abort() + for o := it.Next(); o != nil; o = it.Next() { + gw := o.(*gwv1.Gateway) + fqn := client.FQN(gw.Namespace, gw.Name) + s.InitOutcome(fqn) + ctx = internal.WithSpec(ctx, specFor(fqn, gw)) + s.checkRefs(ctx, gw) + } + + return nil +} + +func (s *Gateway) checkRefs(ctx context.Context, gw *gwv1.Gateway) { + txn := s.db.Txn(false) + defer txn.Abort() + txn, it := s.db.MustITFor(internal.Glossary[internal.GWC]) + defer txn.Abort() + + for o := it.Next(); o != nil; o = it.Next() { + gwc, ok := o.(*gwv1.GatewayClass) + if !ok { + s.AddErr(ctx, fmt.Errorf("expecting gatewayclass but got %T", o)) + continue + } + if gwc.Name == string(gw.Spec.GatewayClassName) { + return + } + } + + s.AddCode(ctx, 407, gw.Kind, "GatewayClass", gw.Spec.GatewayClassName) +} diff --git a/internal/lint/gw_test.go b/internal/lint/gw_test.go new file mode 100644 index 00000000..72558682 --- /dev/null +++ b/internal/lint/gw_test.go @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "testing" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/rules" + "github.com/derailed/popeye/internal/test" + "github.com/stretchr/testify/assert" + gwv1 "sigs.k8s.io/gateway-api/apis/v1" +) + +func TestGatewayLint(t *testing.T) { + dba, err := test.NewTestDB() + assert.NoError(t, err) + l := db.NewLoader(dba) + + ctx := test.MakeCtx(t) + assert.NoError(t, test.LoadDB[*gwv1.GatewayClass](ctx, l.DB, "net/gwc/1.yaml", internal.Glossary[internal.GWC])) + assert.NoError(t, test.LoadDB[*gwv1.Gateway](ctx, l.DB, "net/gw/1.yaml", internal.Glossary[internal.GW])) + + gw := NewGateway(test.MakeCollector(t), dba) + assert.Nil(t, gw.Lint(test.MakeContext("gateway.networking.k8s.io/v1/gateways", "gateways"))) + assert.Equal(t, 2, len(gw.Outcome())) + + ii := gw.Outcome()["default/gw1"] + assert.Equal(t, 0, len(ii)) + + ii = gw.Outcome()["default/gw2"] + assert.Equal(t, 1, len(ii)) + assert.Equal(t, `[POP-407] Gateway references GatewayClass "gwc-bozo" which does not exist`, ii[0].Message) + assert.Equal(t, rules.ErrorLevel, ii[0].Level) + // assert.Equal(t, `[POP-407] Gateway references GatewayClass "gwc-bozo" which does not exist`, ii[0].Message) + // assert.Equal(t, rules.ErrorLevel, ii[0].Level) +} diff --git a/internal/lint/gwc.go b/internal/lint/gwc.go new file mode 100644 index 00000000..1476b151 --- /dev/null +++ b/internal/lint/gwc.go @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "context" + "fmt" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/client" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/issues" + gwv1 "sigs.k8s.io/gateway-api/apis/v1" +) + +type ( + // GatewayClass tracks GatewayClass sanitization. + GatewayClass struct { + *issues.Collector + + db *db.DB + } +) + +// NewGatewayClass returns a new instance. +func NewGatewayClass(co *issues.Collector, db *db.DB) *GatewayClass { + return &GatewayClass{ + Collector: co, + db: db, + } +} + +// Lint cleanse the resource. +func (s *GatewayClass) Lint(ctx context.Context) error { + txn, it := s.db.MustITFor(internal.Glossary[internal.GWC]) + defer txn.Abort() + for o := it.Next(); o != nil; o = it.Next() { + gwc := o.(*gwv1.GatewayClass) + fqn := client.FQN(gwc.Namespace, gwc.Name) + s.InitOutcome(fqn) + ctx = internal.WithSpec(ctx, specFor(fqn, gwc)) + s.checkRefs(ctx, gwc.Name) + } + + return nil +} + +func (s *GatewayClass) checkRefs(ctx context.Context, n string) { + txn := s.db.Txn(false) + defer txn.Abort() + txn, it := s.db.MustITFor(internal.Glossary[internal.GW]) + defer txn.Abort() + + for o := it.Next(); o != nil; o = it.Next() { + gw, ok := o.(*gwv1.Gateway) + if !ok { + s.AddErr(ctx, fmt.Errorf("expecting gateway but got %T", o)) + continue + } + if n == string(gw.Spec.GatewayClassName) { + return + } + } + + s.AddCode(ctx, 400) +} diff --git a/internal/lint/gwc_test.go b/internal/lint/gwc_test.go new file mode 100644 index 00000000..36059f2b --- /dev/null +++ b/internal/lint/gwc_test.go @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "testing" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/rules" + "github.com/derailed/popeye/internal/test" + "github.com/stretchr/testify/assert" + gwv1 "sigs.k8s.io/gateway-api/apis/v1" +) + +func TestGatewayClassLint(t *testing.T) { + dba, err := test.NewTestDB() + assert.NoError(t, err) + l := db.NewLoader(dba) + + ctx := test.MakeCtx(t) + assert.NoError(t, test.LoadDB[*gwv1.GatewayClass](ctx, l.DB, "net/gwc/1.yaml", internal.Glossary[internal.GWC])) + assert.NoError(t, test.LoadDB[*gwv1.Gateway](ctx, l.DB, "net/gw/1.yaml", internal.Glossary[internal.GW])) + + gwc := NewGatewayClass(test.MakeCollector(t), dba) + assert.Nil(t, gwc.Lint(test.MakeContext("gateway.networking.k8s.io/v1/gatewayclasses", "gatewayclasses"))) + assert.Equal(t, 2, len(gwc.Outcome())) + + ii := gwc.Outcome()["gwc1"] + assert.Equal(t, 0, len(ii)) + + ii = gwc.Outcome()["gwc2"] + assert.Equal(t, 1, len(ii)) + assert.Equal(t, `[POP-400] Used? Unable to locate resource reference`, ii[0].Message) + assert.Equal(t, rules.InfoLevel, ii[0].Level) +} diff --git a/internal/lint/gwr.go b/internal/lint/gwr.go new file mode 100644 index 00000000..f241d891 --- /dev/null +++ b/internal/lint/gwr.go @@ -0,0 +1,136 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "context" + "fmt" + "strconv" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/client" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/issues" + v1 "k8s.io/api/core/v1" + gwv1 "sigs.k8s.io/gateway-api/apis/v1" +) + +type ( + // HTTPRoute tracks HTTPRoute sanitization. + HTTPRoute struct { + *issues.Collector + + db *db.DB + } +) + +// NewHTTPRoute returns a new instance. +func NewHTTPRoute(co *issues.Collector, db *db.DB) *HTTPRoute { + return &HTTPRoute{ + Collector: co, + db: db, + } +} + +// Lint cleanse the resource. +func (s *HTTPRoute) Lint(ctx context.Context) error { + txn, it := s.db.MustITFor(internal.Glossary[internal.GWR]) + defer txn.Abort() + for o := it.Next(); o != nil; o = it.Next() { + gwr := o.(*gwv1.HTTPRoute) + fqn := client.FQN(gwr.Namespace, gwr.Name) + s.InitOutcome(fqn) + ctx = internal.WithSpec(ctx, specFor(fqn, gwr)) + s.checkRoute(ctx, fqn, gwr) + } + + return nil +} + +// Check service ref +func (s *HTTPRoute) checkRoute(ctx context.Context, fqn string, gwr *gwv1.HTTPRoute) { + for _, r := range gwr.Spec.ParentRefs { + switch { + case r.Group == nil: + var kind string + if r.Kind == nil { + kind = "Gateway" + } else { + kind = string(*r.Kind) + } + switch kind { + case "Gateway": + s.checkGWRef(ctx, gwr.Namespace, &r) + case "Service": + s.checkSvcRef(ctx, gwr.Namespace, &r) + default: + s.AddErr(ctx, fmt.Errorf("unhandled parent kind: %s", kind)) + } + case *r.Group == "", *r.Group == "Service": + s.checkSvcRef(ctx, gwr.Namespace, &r) + } + } + + for _, r := range gwr.Spec.Rules { + for _, be := range r.BackendRefs { + switch { + case be.Kind == nil, *be.Kind == "Service": + s.checkSvcBE(ctx, gwr.Namespace, &be.BackendRef) + } + } + } +} + +func (s *HTTPRoute) checkSvcBE(ctx context.Context, ns string, be *gwv1.BackendRef) { + if be.BackendObjectReference.Kind == nil || *be.BackendObjectReference.Kind == "Service" { + txn := s.db.Txn(false) + defer txn.Abort() + + if be.Namespace != nil { + ns = string(*be.Namespace) + } + fqn := client.FQN(ns, string(be.Name)) + o, err := s.db.Find(internal.Glossary[internal.SVC], fqn) + if err != nil { + s.AddCode(ctx, 407, "Route", "Service", fqn) + return + } + svc, ok := o.(*v1.Service) + if !ok { + s.AddErr(ctx, fmt.Errorf("expecting service but got %T", o)) + return + } + if be.Port == nil { + return + } + for _, p := range svc.Spec.Ports { + if p.Port == int32(*be.Port) { + return + } + } + s.AddCode(ctx, 1106, strconv.Itoa(int(*be.Port))) + } +} + +func (s *HTTPRoute) checkGWRef(ctx context.Context, ns string, ref *gwv1.ParentReference) { + if ref.Namespace != nil { + ns = string(*ref.Namespace) + } + fqn := client.FQN(ns, string(ref.Name)) + _, err := s.db.Find(internal.Glossary[internal.GW], fqn) + if err != nil { + s.AddCode(ctx, 407, "HTTPRoute", "Gateway", fqn) + } +} + +func (s *HTTPRoute) checkSvcRef(ctx context.Context, ns string, ref *gwv1.ParentReference) { + if ref.Namespace != nil { + ns = string(*ref.Namespace) + } + fqn := client.FQN(ns, string(ref.Name)) + _, err := s.db.Find(internal.Glossary[internal.SVC], fqn) + if err != nil { + s.AddCode(ctx, 407, "HTTPRoute", "Service", fqn) + } +} diff --git a/internal/lint/gwr_test.go b/internal/lint/gwr_test.go new file mode 100644 index 00000000..3c7fb96b --- /dev/null +++ b/internal/lint/gwr_test.go @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "testing" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/rules" + "github.com/derailed/popeye/internal/test" + "github.com/stretchr/testify/assert" + v1 "k8s.io/api/core/v1" + gwv1 "sigs.k8s.io/gateway-api/apis/v1" +) + +func TestHttpRouteTestLint(t *testing.T) { + dba, err := test.NewTestDB() + assert.NoError(t, err) + l := db.NewLoader(dba) + + ctx := test.MakeCtx(t) + assert.NoError(t, test.LoadDB[*gwv1.HTTPRoute](ctx, l.DB, "net/gwr/1.yaml", internal.Glossary[internal.GWR])) + assert.NoError(t, test.LoadDB[*gwv1.GatewayClass](ctx, l.DB, "net/gwc/1.yaml", internal.Glossary[internal.GWC])) + assert.NoError(t, test.LoadDB[*gwv1.Gateway](ctx, l.DB, "net/gw/1.yaml", internal.Glossary[internal.GW])) + assert.NoError(t, test.LoadDB[*v1.Service](ctx, l.DB, "core/svc/1.yaml", internal.Glossary[internal.SVC])) + + hr := NewHTTPRoute(test.MakeCollector(t), dba) + assert.Nil(t, hr.Lint(test.MakeContext("gateway.networking.k8s.io/v1/httproutes", "httproutes"))) + assert.Equal(t, 3, len(hr.Outcome())) + + ii := hr.Outcome()["default/r1"] + assert.Equal(t, 0, len(ii)) + + ii = hr.Outcome()["default/r2"] + assert.Equal(t, 2, len(ii)) + assert.Equal(t, `[POP-407] HTTPRoute references Gateway "default/gw-bozo" which does not exist`, ii[0].Message) + assert.Equal(t, rules.ErrorLevel, ii[0].Level) + assert.Equal(t, `[POP-1106] No target ports match service port 8080`, ii[1].Message) + assert.Equal(t, rules.ErrorLevel, ii[1].Level) + + ii = hr.Outcome()["default/r3"] + assert.Equal(t, 2, len(ii)) + assert.Equal(t, `[POP-407] HTTPRoute references Service "default/svc-bozo" which does not exist`, ii[0].Message) + assert.Equal(t, rules.ErrorLevel, ii[0].Level) + assert.Equal(t, `[POP-1106] No target ports match service port 9090`, ii[1].Message) + assert.Equal(t, rules.ErrorLevel, ii[1].Level) + +} diff --git a/internal/sanitize/helper.go b/internal/lint/helper.go similarity index 72% rename from internal/sanitize/helper.go rename to internal/lint/helper.go index ca8a5ea4..7e6239f9 100644 --- a/internal/sanitize/helper.go +++ b/internal/lint/helper.go @@ -1,15 +1,20 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright Authors of Popeye -package sanitize +package lint import ( + "context" "fmt" "strconv" "strings" + "github.com/derailed/popeye/internal/cache" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/rules" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) const ( @@ -23,6 +28,53 @@ const ( type qos = int +func specFor(fqn string, o metav1.ObjectMetaAccessor) rules.Spec { + spec := rules.Spec{ + FQN: fqn, + } + if o == nil { + return spec + } + + m := o.GetObjectMeta() + spec.Labels, spec.Annotations = m.GetLabels(), m.GetAnnotations() + + return spec +} + +func resourceUsage(ctx context.Context, dba *db.DB, c Collector, ns string, sel *metav1.LabelSelector) ConsumptionMetrics { + var mx ConsumptionMetrics + + pp, err := dba.FindPodsBySel(ns, sel) + if err != nil { + c.AddErr(ctx, err) + return mx + } + if len(pp) == 0 { + c.AddCode(ctx, 508, dumpSel(sel)) + return mx + } + + for _, pod := range pp { + fqn := cache.FQN(pod.Namespace, pod.Name) + cpu, mem := computePodResources(pod.Spec) + mx.QOS = pod.Status.QOSClass + mx.RequestCPU.Add(cpu) + mx.RequestMEM.Add(mem) + + pmx, err := dba.FindPMX(fqn) + if err != nil || pmx == nil { + continue + } + for _, cx := range pmx.Containers { + mx.CurrentCPU.Add(*cx.Usage.Cpu()) + mx.CurrentMEM.Add(*cx.Usage.Memory()) + } + } + + return mx +} + // Poor man plural... func pluralOf(s string, count int) string { if count > 1 { @@ -49,17 +101,6 @@ func ToPerc(v1, v2 int64) int64 { return int64((float64(v1) / float64(v2)) * 100) } -// In checks if a string is in a list of strings. -func in(ll []string, s string) bool { - for _, l := range ll { - if l == s { - return true - } - } - - return false -} - // ToMC converts quantity to millicores. func toMC(q resource.Quantity) int64 { return q.MilliValue() diff --git a/internal/lint/helper_test.go b/internal/lint/helper_test.go new file mode 100644 index 00000000..c30d4a97 --- /dev/null +++ b/internal/lint/helper_test.go @@ -0,0 +1,186 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +// import ( +// "testing" + +// "github.com/stretchr/testify/assert" +// v1 "k8s.io/api/core/v1" +// "k8s.io/apimachinery/pkg/api/resource" +// ) + +// func TestNamepaced(t *testing.T) { +// uu := []struct { +// s string +// ns, n string +// }{ +// {"fred/blee", "fred", "blee"}, +// {"fred", "", "fred"}, +// } + +// for _, u := range uu { +// ns, n := namespaced(u.s) +// assert.Equal(t, u.ns, ns) +// assert.Equal(t, u.n, n) +// } +// } + +// func TestPluralOf(t *testing.T) { +// uu := []struct { +// n string +// count int +// e string +// }{ +// {"fred", 0, "fred"}, +// {"fred", 1, "fred"}, +// {"fred", 2, "freds"}, +// } + +// for _, u := range uu { +// assert.Equal(t, u.e, pluralOf(u.n, u.count)) +// } +// } + +// func TestToPerc(t *testing.T) { +// uu := []struct { +// v1, v2, e int64 +// }{ +// {50, 100, 50}, +// {100, 0, 0}, +// {100, 50, 200}, +// } + +// for _, u := range uu { +// assert.Equal(t, u.e, ToPerc(u.v1, u.v2)) +// } +// } + +// func TestIn(t *testing.T) { +// uu := []struct { +// l []string +// s string +// e bool +// }{ +// {[]string{"a", "b", "c"}, "a", true}, +// {[]string{"a", "b", "c"}, "z", false}, +// } + +// for _, u := range uu { +// assert.Equal(t, u.e, in(u.l, u.s)) +// } +// } + +// func TestToMCRatio(t *testing.T) { +// uu := []struct { +// q1, q2 resource.Quantity +// r float64 +// }{ +// {test.ToQty("100m"), test.ToQty("200m"), 50}, +// {test.ToQty("100m"), test.ToQty("50m"), 200}, +// {test.ToQty("0m"), test.ToQty("5m"), 0}, +// {test.ToQty("10m"), test.ToQty("0m"), 0}, +// } + +// for _, u := range uu { +// assert.Equal(t, u.r, toMCRatio(u.q1, u.q2)) +// } +// } + +// func TestToMEMRatio(t *testing.T) { +// uu := []struct { +// q1, q2 resource.Quantity +// r float64 +// }{ +// {test.ToQty("10Mi"), test.ToQty("20Mi"), 50}, +// {test.ToQty("20Mi"), test.ToQty("10Mi"), 200}, +// {test.ToQty("0Mi"), test.ToQty("5Mi"), 0}, +// {test.ToQty("10Mi"), test.ToQty("0Mi"), 0}, +// } + +// for _, u := range uu { +// assert.Equal(t, u.r, toMEMRatio(u.q1, u.q2)) +// } +// } + +// func TestContainerResources(t *testing.T) { +// uu := map[string]struct { +// co v1.Container +// cpu, mem *resource.Quantity +// qos qos +// }{ +// "none": { +// co: makeContainer("c1", coOpts{ +// image: "fred:1.0.1", +// }), +// qos: qosBestEffort, +// }, +// "guaranteed": { +// co: makeContainer("c1", coOpts{ +// image: "fred:1.0.1", +// rcpu: "100m", +// rmem: "10Mi", +// lcpu: "100m", +// lmem: "10Mi", +// }), +// cpu: makeQty("100m"), +// mem: makeQty("10Mi"), +// qos: qosGuaranteed, +// }, +// "bustableLimit": { +// co: makeContainer("c1", coOpts{ +// image: "fred:1.0.1", +// lcpu: "100m", +// lmem: "10Mi", +// }), +// cpu: makeQty("100m"), +// mem: makeQty("10Mi"), +// qos: qosBurstable, +// }, +// "burstableRequest": { +// co: makeContainer("c1", coOpts{ +// image: "fred:1.0.1", +// rcpu: "100m", +// rmem: "10Mi", +// }), +// cpu: makeQty("100m"), +// mem: makeQty("10Mi"), +// qos: qosBurstable, +// }, +// } + +// for k := range uu { +// u := uu[k] +// t.Run(k, func(t *testing.T) { +// cpu, mem, qos := containerResources(u.co) + +// assert.Equal(t, cpu, u.cpu) +// assert.Equal(t, mem, u.mem) +// assert.Equal(t, u.qos, qos) +// }) +// } +// } + +// func TestPortAsString(t *testing.T) { +// uu := []struct { +// port v1.ServicePort +// e string +// }{ +// {v1.ServicePort{Protocol: v1.ProtocolTCP, Name: "p1", Port: 80}, "TCP:p1:80"}, +// {v1.ServicePort{Protocol: v1.ProtocolUDP, Name: "", Port: 80}, "UDP::80"}, +// } + +// for _, u := range uu { +// assert.Equal(t, u.e, portAsStr(u.port)) +// } +// } + +// // ---------------------------------------------------------------------------- +// // Helpers... + +// func test.ToQty(s string) resource.Quantity { +// q, _ := resource.ParseQuantity(s) + +// return q +// } diff --git a/internal/sanitize/hpa.go b/internal/lint/hpa.go similarity index 53% rename from internal/sanitize/hpa.go rename to internal/lint/hpa.go index 7f6f07ee..b7f96364 100644 --- a/internal/sanitize/hpa.go +++ b/internal/lint/hpa.go @@ -1,84 +1,88 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright Authors of Popeye -package sanitize +package lint import ( "context" + "strings" "github.com/derailed/popeye/internal" "github.com/derailed/popeye/internal/cache" + "github.com/derailed/popeye/internal/client" + "github.com/derailed/popeye/internal/db" "github.com/derailed/popeye/internal/issues" + appsv1 "k8s.io/api/apps/v1" autoscalingv1 "k8s.io/api/autoscaling/v1" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" - mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" ) -type ( - // PodMetricsLister handles pods metrics. - PodMetricsLister interface { - ListPodsMetrics() map[string]*mv1beta1.PodMetrics - } - - // ClusterMetricsLister handles cluster metrics. - ClusterMetricsLister interface { - ListAvailableMetrics(map[string]*v1.Node) v1.ResourceList - } - - // HorizontalPodAutoscaler represents a HorizontalPodAutoscaler linter. - HorizontalPodAutoscaler struct { - *issues.Collector - HpaLister - } +// HorizontalPodAutoscaler represents a HorizontalPodAutoscaler linter. +type HorizontalPodAutoscaler struct { + *issues.Collector - // HpaLister list available hpas on a cluster. - HpaLister interface { - NodeLister - DeploymentLister - StatefulSetLister - ClusterMetricsLister - ListHorizontalPodAutoscalers() map[string]*autoscalingv1.HorizontalPodAutoscaler - } -) + db *db.DB +} -// NewHorizontalPodAutoscaler returns a new ServiceAccount linter. -func NewHorizontalPodAutoscaler(co *issues.Collector, lister HpaLister) *HorizontalPodAutoscaler { +// NewHorizontalPodAutoscaler returns a new instance. +func NewHorizontalPodAutoscaler(co *issues.Collector, db *db.DB) *HorizontalPodAutoscaler { return &HorizontalPodAutoscaler{ Collector: co, - HpaLister: lister, + db: db, } } -// Sanitize an horizontalpodautoscaler. -func (h *HorizontalPodAutoscaler) Sanitize(ctx context.Context) error { +// Lint sanitizes an hpa. +func (h *HorizontalPodAutoscaler) Lint(ctx context.Context) error { var ( tcpu, tmem resource.Quantity current int32 ) - res := h.ListAvailableMetrics(h.ListNodes()) - for fqn, hpa := range h.ListHorizontalPodAutoscalers() { + res, err := cache.ListAvailableMetrics(h.db) + if err != nil { + return err + } + txn, it := h.db.MustITFor(internal.Glossary[internal.HPA]) + defer txn.Abort() + for o := it.Next(); o != nil; o = it.Next() { + hpa := o.(*autoscalingv1.HorizontalPodAutoscaler) + fqn := client.FQN(hpa.Namespace, hpa.Name) h.InitOutcome(fqn) - ctx = internal.WithFQN(ctx, fqn) + ctx = internal.WithSpec(ctx, specFor(fqn, hpa)) var rcpu, rmem resource.Quantity ns, _ := namespaced(fqn) switch hpa.Spec.ScaleTargetRef.Kind { case "Deployment": - dpFqn, dps := cache.FQN(ns, hpa.Spec.ScaleTargetRef.Name), h.ListDeployments() - if dp, ok := dps[dpFqn]; ok { + rfqn := cache.FQN(ns, hpa.Spec.ScaleTargetRef.Name) + if o, err := h.db.Find(internal.Glossary[internal.DP], rfqn); err == nil { + dp := o.(*appsv1.Deployment) rcpu, rmem = podResources(dp.Spec.Template.Spec) current = dp.Status.AvailableReplicas } else { - h.AddCode(ctx, 600, fqn, dpFqn) + h.AddCode(ctx, 600, fqn, strings.ToLower(hpa.Spec.ScaleTargetRef.Kind), rfqn) + continue + } + + case "ReplicaSet": + rfqn := cache.FQN(ns, hpa.Spec.ScaleTargetRef.Name) + if o, err := h.db.Find(internal.Glossary[internal.RS], rfqn); err == nil { + rs := o.(*appsv1.ReplicaSet) + rcpu, rmem = podResources(rs.Spec.Template.Spec) + current = rs.Status.AvailableReplicas + } else { + h.AddCode(ctx, 600, fqn, strings.ToLower(hpa.Spec.ScaleTargetRef.Kind), rfqn) continue } + case "StatefulSet": - stsFqn, sts := cache.FQN(ns, hpa.Spec.ScaleTargetRef.Name), h.ListStatefulSets() - if st, ok := sts[stsFqn]; ok { - rcpu, rmem = podResources(st.Spec.Template.Spec) - current = st.Status.CurrentReplicas + rfqn := cache.FQN(ns, hpa.Spec.ScaleTargetRef.Name) + if o, err := h.db.Find(internal.Glossary[internal.STS], rfqn); err == nil { + sts := o.(*appsv1.StatefulSet) + rcpu, rmem = podResources(sts.Spec.Template.Spec) + current = sts.Status.CurrentReplicas } else { - h.AddCode(ctx, 601, fqn, stsFqn) + h.AddCode(ctx, 600, fqn, strings.ToLower(hpa.Spec.ScaleTargetRef.Kind), rfqn) continue } } @@ -87,10 +91,6 @@ func (h *HorizontalPodAutoscaler) Sanitize(ctx context.Context) error { list := h.checkResources(ctx, hpa.Spec.MaxReplicas, current, rList, res) tcpu.Add(*list.Cpu()) tmem.Add(*list.Memory()) - - if h.NoConcerns(fqn) && h.Config.ExcludeFQN(internal.MustExtractSectionGVR(ctx), fqn) { - h.ClearOutcome(fqn) - } } h.checkUtilization(ctx, tcpu, tmem, res) @@ -121,7 +121,7 @@ func (h *HorizontalPodAutoscaler) checkResources(ctx context.Context, max, curre func (h *HorizontalPodAutoscaler) checkUtilization(ctx context.Context, tcpu, tmem resource.Quantity, res v1.ResourceList) { acpu, amem := *res.Cpu(), *res.Memory() - ctx = internal.WithFQN(ctx, "HPA") + ctx = internal.WithSpec(ctx, specFor("HPA", nil)) if toMC(tcpu) > toMC(acpu) { cpu := tcpu.DeepCopy() cpu.Sub(acpu) diff --git a/internal/lint/hpa_test.go b/internal/lint/hpa_test.go new file mode 100644 index 00000000..03010ab1 --- /dev/null +++ b/internal/lint/hpa_test.go @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "testing" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/rules" + "github.com/derailed/popeye/internal/test" + "github.com/stretchr/testify/assert" + appsv1 "k8s.io/api/apps/v1" + autoscalingv1 "k8s.io/api/autoscaling/v1" + v1 "k8s.io/api/core/v1" + mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" +) + +func TestHPALint(t *testing.T) { + dba, err := test.NewTestDB() + assert.NoError(t, err) + l := db.NewLoader(dba) + + ctx := test.MakeCtx(t) + assert.NoError(t, test.LoadDB[*autoscalingv1.HorizontalPodAutoscaler](ctx, l.DB, "autoscaling/hpa/1.yaml", internal.Glossary[internal.HPA])) + assert.NoError(t, test.LoadDB[*appsv1.Deployment](ctx, l.DB, "apps/dp/1.yaml", internal.Glossary[internal.DP])) + assert.NoError(t, test.LoadDB[*appsv1.ReplicaSet](ctx, l.DB, "apps/rs/1.yaml", internal.Glossary[internal.RS])) + assert.NoError(t, test.LoadDB[*appsv1.StatefulSet](ctx, l.DB, "apps/sts/1.yaml", internal.Glossary[internal.STS])) + assert.NoError(t, test.LoadDB[*v1.Node](ctx, l.DB, "core/node/1.yaml", internal.Glossary[internal.NO])) + assert.NoError(t, test.LoadDB[*v1.Pod](ctx, l.DB, "core/pod/2.yaml", internal.Glossary[internal.PO])) + assert.NoError(t, test.LoadDB[*v1.ServiceAccount](ctx, l.DB, "core/sa/1.yaml", internal.Glossary[internal.SA])) + assert.NoError(t, test.LoadDB[*mv1beta1.PodMetrics](ctx, l.DB, "mx/pod/1.yaml", internal.Glossary[internal.PMX])) + assert.NoError(t, test.LoadDB[*mv1beta1.NodeMetrics](ctx, l.DB, "mx/node/1.yaml", internal.Glossary[internal.NMX])) + + hpa := NewHorizontalPodAutoscaler(test.MakeCollector(t), dba) + assert.Nil(t, hpa.Lint(test.MakeContext("autoscaling/v1/horizontalpodautoscalers", "horizontalpodautoscalers"))) + assert.Equal(t, 7, len(hpa.Outcome())) + + ii := hpa.Outcome()["default/hpa1"] + assert.Equal(t, 1, len(ii)) + + ii = hpa.Outcome()["default/hpa2"] + assert.Equal(t, 1, len(ii)) + assert.Equal(t, `[POP-600] HPA default/hpa2 references a deployment which does not exist: default/dp-toast`, ii[0].Message) + assert.Equal(t, rules.ErrorLevel, ii[0].Level) + + ii = hpa.Outcome()["default/hpa3"] + assert.Equal(t, 1, len(ii)) + assert.Equal(t, `[POP-600] HPA default/hpa3 references a replicaset which does not exist: default/rs-toast`, ii[0].Message) + assert.Equal(t, rules.ErrorLevel, ii[0].Level) + + ii = hpa.Outcome()["default/hpa4"] + assert.Equal(t, 1, len(ii)) + assert.Equal(t, `[POP-600] HPA default/hpa4 references a statefulset which does not exist: default/sts-toast`, ii[0].Message) + assert.Equal(t, rules.ErrorLevel, ii[0].Level) + + ii = hpa.Outcome()["default/hpa5"] + assert.Equal(t, 1, len(ii)) + + ii = hpa.Outcome()["default/hpa6"] + assert.Equal(t, 1, len(ii)) + +} diff --git a/internal/lint/ing.go b/internal/lint/ing.go new file mode 100644 index 00000000..4e18fdb9 --- /dev/null +++ b/internal/lint/ing.go @@ -0,0 +1,113 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "context" + "errors" + "fmt" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/cache" + "github.com/derailed/popeye/internal/client" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/issues" + v1 "k8s.io/api/core/v1" + netv1 "k8s.io/api/networking/v1" +) + +type ( + // Ingress tracks Ingress sanitization. + Ingress struct { + *issues.Collector + + db *db.DB + } +) + +// NewIngress returns a new instance. +func NewIngress(co *issues.Collector, db *db.DB) *Ingress { + return &Ingress{ + Collector: co, + db: db, + } +} + +// Lint cleanse the resource. +func (s *Ingress) Lint(ctx context.Context) error { + txn, it := s.db.MustITFor(internal.Glossary[internal.ING]) + defer txn.Abort() + for o := it.Next(); o != nil; o = it.Next() { + ing := o.(*netv1.Ingress) + fqn := client.FQN(ing.Namespace, ing.Name) + s.InitOutcome(fqn) + ctx = internal.WithSpec(ctx, specFor(fqn, ing)) + + for _, ing := range ing.Status.LoadBalancer.Ingress { + for _, p := range ing.Ports { + if p.Error != nil { + s.AddCode(ctx, 1400, *p.Error) + } + } + } + for _, r := range ing.Spec.Rules { + http := r.IngressRuleValue.HTTP + if http == nil { + continue + } + for _, h := range http.Paths { + s.checkBackendSvc(ctx, ing.Namespace, h.Backend.Service) + s.checkBackendRef(ctx, ing.Namespace, h.Backend.Resource) + } + } + } + + return nil +} + +func (s *Ingress) checkBackendRef(ctx context.Context, ns string, be *v1.TypedLocalObjectReference) { + if be == nil { + return + } + s.AddErr(ctx, errors.New("Ingress local obj refs not supported")) +} + +func (s *Ingress) checkBackendSvc(ctx context.Context, ns string, be *netv1.IngressServiceBackend) { + if be == nil { + return + } + o, err := s.db.Find(internal.Glossary[internal.SVC], cache.FQN(ns, be.Name)) + if err != nil { + s.AddCode(ctx, 1401, be.Name) + return + } + isvc, ok := o.(*v1.Service) + if !ok { + s.AddErr(ctx, fmt.Errorf("expecting service but got %T", o)) + return + } + if !s.findPortByNumberOrName(ctx, isvc.Spec.Ports, be.Port) { + s.AddCode(ctx, 1402, fmt.Sprintf("%s:%d", be.Port.Name, be.Port.Number)) + } + if be.Port.Name == "" { + if be.Port.Number == 0 { + s.AddCode(ctx, 1404) + return + } + s.AddCode(ctx, 1403, be.Port.Number) + } +} + +func (s *Ingress) findPortByNumberOrName(ctx context.Context, pp []v1.ServicePort, port netv1.ServiceBackendPort) bool { + for _, p := range pp { + if p.Name == port.Name { + return true + } + if p.Port == port.Number { + return true + } + } + + return false +} diff --git a/internal/lint/ing_test.go b/internal/lint/ing_test.go new file mode 100644 index 00000000..c9f4ebef --- /dev/null +++ b/internal/lint/ing_test.go @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "testing" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/rules" + "github.com/derailed/popeye/internal/test" + "github.com/stretchr/testify/assert" + v1 "k8s.io/api/core/v1" + netv1 "k8s.io/api/networking/v1" +) + +func TestIngLint(t *testing.T) { + dba, err := test.NewTestDB() + assert.NoError(t, err) + l := db.NewLoader(dba) + + ctx := test.MakeCtx(t) + assert.NoError(t, test.LoadDB[*netv1.Ingress](ctx, l.DB, "net/ingress/1.yaml", internal.Glossary[internal.ING])) + assert.NoError(t, test.LoadDB[*v1.Service](ctx, l.DB, "core/svc/1.yaml", internal.Glossary[internal.SVC])) + + ing := NewIngress(test.MakeCollector(t), dba) + assert.Nil(t, ing.Lint(test.MakeContext("networking.k8s.io/v1/ingresses", "ingresses"))) + assert.Equal(t, 6, len(ing.Outcome())) + + ii := ing.Outcome()["default/ing1"] + assert.Equal(t, 0, len(ii)) + + ii = ing.Outcome()["default/ing2"] + assert.Equal(t, 1, len(ii)) + assert.Equal(t, `[POP-1403] Ingress backend uses a port#, prefer a named port: 9090`, ii[0].Message) + assert.Equal(t, rules.InfoLevel, ii[0].Level) + + ii = ing.Outcome()["default/ing3"] + assert.Equal(t, 1, len(ii)) + assert.Equal(t, `[POP-1401] Ingress references a service backend which does not exist: s2`, ii[0].Message) + assert.Equal(t, rules.ErrorLevel, ii[0].Level) + + ii = ing.Outcome()["default/ing4"] + assert.Equal(t, 2, len(ii)) + assert.Equal(t, `[POP-1402] Ingress references a service port which is not defined: :0`, ii[0].Message) + assert.Equal(t, rules.ErrorLevel, ii[0].Level) + assert.Equal(t, `[POP-1404] Invalid Ingress backend spec. Must use port name or number`, ii[1].Message) + assert.Equal(t, rules.ErrorLevel, ii[1].Level) + + ii = ing.Outcome()["default/ing5"] + assert.Equal(t, 2, len(ii)) + assert.Equal(t, `[POP-1400] Ingress LoadBalancer port reported an error: boom`, ii[0].Message) + assert.Equal(t, rules.ErrorLevel, ii[0].Level) + assert.Equal(t, `Ingress local obj refs not supported`, ii[1].Message) + assert.Equal(t, rules.ErrorLevel, ii[1].Level) + + ii = ing.Outcome()["default/ing6"] + assert.Equal(t, 2, len(ii)) + assert.Equal(t, `[POP-1402] Ingress references a service port which is not defined: :9091`, ii[0].Message) + assert.Equal(t, rules.ErrorLevel, ii[0].Level) + assert.Equal(t, `[POP-1403] Ingress backend uses a port#, prefer a named port: 9091`, ii[1].Message) + assert.Equal(t, rules.InfoLevel, ii[1].Level) +} diff --git a/internal/lint/job.go b/internal/lint/job.go new file mode 100644 index 00000000..d028ee13 --- /dev/null +++ b/internal/lint/job.go @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "context" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/client" + "github.com/derailed/popeye/internal/dao" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/issues" + batchv1 "k8s.io/api/batch/v1" + v1 "k8s.io/api/core/v1" +) + +// Job tracks Job linting. +type Job struct { + *issues.Collector + + db *db.DB +} + +// NewJob returns a new instance. +func NewJob(co *issues.Collector, db *db.DB) *Job { + return &Job{ + Collector: co, + db: db, + } +} + +// Lint cleanse the resource. +func (s *Job) Lint(ctx context.Context) error { + over := pullOverAllocs(ctx) + txn, it := s.db.MustITFor(internal.Glossary[internal.JOB]) + defer txn.Abort() + for o := it.Next(); o != nil; o = it.Next() { + j := o.(*batchv1.Job) + fqn := client.FQN(j.Namespace, j.Name) + s.InitOutcome(fqn) + ctx = internal.WithSpec(ctx, specFor(fqn, j)) + s.checkJob(ctx, fqn, j) + s.checkContainers(ctx, fqn, j.Spec.Template.Spec) + s.checkUtilization(ctx, over, fqn) + } + + return nil +} + +// CheckJob checks if Job contract is currently happy or not. +func (s *Job) checkJob(ctx context.Context, fqn string, j *batchv1.Job) { + checkEvents(ctx, s.Collector, internal.JOB, dao.WarnEvt, "Job", fqn) + + if j.Spec.Suspend != nil && *j.Spec.Suspend { + s.AddCode(ctx, 1500, j.Kind) + } + + if sa := j.Spec.Template.Spec.ServiceAccountName; sa != "" { + saFQN := client.FQN(j.Namespace, sa) + if !s.db.Exists(internal.Glossary[internal.SA], saFQN) { + s.AddCode(ctx, 307, j.Kind, sa) + } + } +} + +// CheckContainers runs thru Job template and checks pod configuration. +func (s *Job) checkContainers(ctx context.Context, fqn string, spec v1.PodSpec) { + c := NewContainer(fqn, s) + for _, co := range spec.InitContainers { + c.sanitize(ctx, co, false) + } + for _, co := range spec.Containers { + c.sanitize(ctx, co, false) + } +} + +// CheckUtilization checks Jobs requested resources vs current utilization. +func (s *Job) checkUtilization(ctx context.Context, over bool, fqn string) { + jj, err := s.db.FindJobs(fqn) + if err != nil { + s.AddErr(ctx, err) + return + } + mx := jobResourceUsage(ctx, s.db, s, jj) + if mx.RequestCPU.IsZero() && mx.RequestMEM.IsZero() { + return + } + checkCPU(ctx, s, over, mx) + checkMEM(ctx, s, over, mx) +} diff --git a/internal/lint/job_test.go b/internal/lint/job_test.go new file mode 100644 index 00000000..e335d3c5 --- /dev/null +++ b/internal/lint/job_test.go @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "testing" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/rules" + "github.com/derailed/popeye/internal/test" + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" + batchv1 "k8s.io/api/batch/v1" + v1 "k8s.io/api/core/v1" + mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" +) + +func init() { + zerolog.SetGlobalLevel(zerolog.FatalLevel) +} + +func TestJobLint(t *testing.T) { + dba, err := test.NewTestDB() + assert.NoError(t, err) + l := db.NewLoader(dba) + + ctx := test.MakeCtx(t) + assert.NoError(t, test.LoadDB[*batchv1.Job](ctx, l.DB, "batch/job/1.yaml", internal.Glossary[internal.JOB])) + assert.NoError(t, test.LoadDB[*v1.ServiceAccount](ctx, l.DB, "core/sa/1.yaml", internal.Glossary[internal.SA])) + assert.NoError(t, test.LoadDB[*v1.Pod](ctx, l.DB, "core/pod/1.yaml", internal.Glossary[internal.PO])) + assert.NoError(t, test.LoadDB[*mv1beta1.PodMetrics](ctx, l.DB, "mx/pod/1.yaml", internal.Glossary[internal.PMX])) + + j := NewJob(test.MakeCollector(t), dba) + assert.Nil(t, j.Lint(test.MakeContext("batch/v1/jobs", "jobs"))) + assert.Equal(t, 3, len(j.Outcome())) + + ii := j.Outcome()["default/j1"] + assert.Equal(t, 0, len(ii)) + + ii = j.Outcome()["default/j2"] + assert.Equal(t, 2, len(ii)) + assert.Equal(t, `[POP-100] Untagged docker image in use`, ii[0].Message) + assert.Equal(t, `[POP-106] No resources requests/limits defined`, ii[1].Message) + assert.Equal(t, rules.WarnLevel, ii[1].Level) +} diff --git a/internal/sanitize/metrics.go b/internal/lint/metrics_helpers.go similarity index 91% rename from internal/sanitize/metrics.go rename to internal/lint/metrics_helpers.go index 8809c003..d2ed6355 100644 --- a/internal/sanitize/metrics.go +++ b/internal/lint/metrics_helpers.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright Authors of Popeye -package sanitize +package lint import ( v1 "k8s.io/api/core/v1" @@ -17,9 +17,9 @@ type ConsumptionMetrics struct { RequestedStorage resource.Quantity } -// ReqAbsCPURatio returns abasolute cpu ratio. +// ReqAbsCPURatio returns absolute cpu ratio. func (d *ConsumptionMetrics) ReqAbsCPURatio() float64 { - if d.CurrentCPU.Cmp(d.RequestCPU) > 1 { + if d.CurrentCPU.Cmp(d.RequestCPU) == 1 { return toMCRatio(d.CurrentCPU, d.RequestCPU) } return toMCRatio(d.RequestCPU, d.CurrentCPU) @@ -35,7 +35,7 @@ func (d *ConsumptionMetrics) ReqCPURatio() float64 { // ReqAbsMEMRatio returns absolute mem ratio. func (d *ConsumptionMetrics) ReqAbsMEMRatio() float64 { - if d.CurrentMEM.Cmp(d.RequestMEM) > 1 { + if d.CurrentMEM.Cmp(d.RequestMEM) == 1 { return toMEMRatio(d.CurrentMEM, d.RequestMEM) } return toMEMRatio(d.RequestMEM, d.CurrentMEM) diff --git a/internal/lint/metrics_helpers_test.go b/internal/lint/metrics_helpers_test.go new file mode 100644 index 00000000..ae04f41b --- /dev/null +++ b/internal/lint/metrics_helpers_test.go @@ -0,0 +1,198 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "k8s.io/apimachinery/pkg/api/resource" +) + +func TestLimitCPURatio(t *testing.T) { + uu := map[string]struct { + mx ConsumptionMetrics + e float64 + }{ + "empty": {}, + "same": { + mx: ConsumptionMetrics{ + CurrentCPU: *resource.NewQuantity(10, resource.DecimalExponent), + LimitCPU: *resource.NewQuantity(10, resource.DecimalExponent), + }, + e: 100, + }, + "delta": { + mx: ConsumptionMetrics{ + CurrentCPU: *resource.NewQuantity(100, resource.DecimalExponent), + LimitCPU: *resource.NewQuantity(10, resource.DecimalExponent), + }, + e: 1_000, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, u.mx.LimitCPURatio()) + }) + } +} + +func TestReqAbsCPURatio(t *testing.T) { + uu := map[string]struct { + mx ConsumptionMetrics + e float64 + }{ + "empty": {}, + "same": { + mx: ConsumptionMetrics{ + CurrentCPU: *resource.NewQuantity(10, resource.DecimalExponent), + RequestCPU: *resource.NewQuantity(10, resource.DecimalExponent), + }, + e: 100, + }, + "higher": { + mx: ConsumptionMetrics{ + CurrentCPU: *resource.NewQuantity(2, resource.DecimalExponent), + RequestCPU: *resource.NewQuantity(10, resource.DecimalExponent), + }, + e: 500, + }, + "lower": { + mx: ConsumptionMetrics{ + CurrentCPU: *resource.NewQuantity(10, resource.DecimalExponent), + RequestCPU: *resource.NewQuantity(100, resource.DecimalExponent), + }, + e: 1_000, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, u.mx.ReqAbsCPURatio()) + }) + } +} + +func TestReqCPURatio(t *testing.T) { + uu := map[string]struct { + mx ConsumptionMetrics + e float64 + }{ + "empty": {}, + "same": { + mx: ConsumptionMetrics{ + CurrentCPU: *resource.NewQuantity(10, resource.DecimalExponent), + RequestCPU: *resource.NewQuantity(10, resource.DecimalExponent), + }, + e: 100, + }, + "higher": { + mx: ConsumptionMetrics{ + CurrentCPU: *resource.NewQuantity(100, resource.DecimalExponent), + RequestCPU: *resource.NewQuantity(10, resource.DecimalExponent), + }, + e: 1000, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, u.mx.ReqCPURatio()) + }) + } +} + +func TestReqAbsMEMRatio(t *testing.T) { + uu := map[string]struct { + mx ConsumptionMetrics + e float64 + }{ + "empty": {}, + "same": { + mx: ConsumptionMetrics{ + CurrentMEM: *resource.NewQuantity(10*megaByte, resource.DecimalExponent), + RequestMEM: *resource.NewQuantity(10*megaByte, resource.DecimalExponent), + }, + e: 100, + }, + "higher": { + mx: ConsumptionMetrics{ + CurrentMEM: *resource.NewQuantity(100*megaByte, resource.DecimalExponent), + RequestMEM: *resource.NewQuantity(10*megaByte, resource.DecimalExponent), + }, + e: 1_000, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, u.mx.ReqAbsMEMRatio()) + }) + } +} + +func TestReqMEMRatio(t *testing.T) { + uu := map[string]struct { + mx ConsumptionMetrics + e float64 + }{ + "empty": {}, + "same": { + mx: ConsumptionMetrics{ + CurrentMEM: *resource.NewQuantity(10*megaByte, resource.DecimalExponent), + RequestMEM: *resource.NewQuantity(10*megaByte, resource.DecimalExponent), + }, + e: 100, + }, + "delta": { + mx: ConsumptionMetrics{ + CurrentMEM: *resource.NewQuantity(100*megaByte, resource.DecimalExponent), + RequestMEM: *resource.NewQuantity(10*megaByte, resource.DecimalExponent), + }, + e: 1_000, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, u.mx.ReqMEMRatio()) + }) + } +} + +func TestLimitMEMRatio(t *testing.T) { + uu := map[string]struct { + mx ConsumptionMetrics + e float64 + }{ + "empty": {}, + "same": { + mx: ConsumptionMetrics{ + CurrentMEM: *resource.NewQuantity(10*megaByte, resource.DecimalExponent), + LimitMEM: *resource.NewQuantity(10*megaByte, resource.DecimalExponent), + }, + e: 100, + }, + "delta": { + mx: ConsumptionMetrics{ + CurrentMEM: *resource.NewQuantity(100*megaByte, resource.DecimalExponent), + LimitMEM: *resource.NewQuantity(10*megaByte, resource.DecimalExponent), + }, + e: 1_000, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, u.mx.LimitMEMRatio()) + }) + } +} diff --git a/internal/lint/node.go b/internal/lint/node.go new file mode 100644 index 00000000..c1d38020 --- /dev/null +++ b/internal/lint/node.go @@ -0,0 +1,174 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "context" + "errors" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/client" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/issues" + v1 "k8s.io/api/core/v1" +) + +type ( + tolerations map[string]struct{} + + // Node represents a Node linter. + Node struct { + *issues.Collector + + db *db.DB + } +) + +// NewNode returns a new instance. +func NewNode(co *issues.Collector, db *db.DB) *Node { + return &Node{ + Collector: co, + db: db, + } +} + +// Lint cleanse the resource. +func (n *Node) Lint(ctx context.Context) error { + nmx := make(client.NodesMetrics) + n.nodesMetrics(nmx) + + tt, err := n.fetchPodTolerations() + if err != nil { + return err + } + txn, it := n.db.MustITFor(internal.Glossary[internal.NO]) + defer txn.Abort() + for o := it.Next(); o != nil; o = it.Next() { + no := o.(*v1.Node) + fqn := no.Name + n.InitOutcome(fqn) + ctx = internal.WithSpec(ctx, specFor(fqn, no)) + + n.checkConditions(ctx, no) + if err := n.checkTaints(ctx, no.Spec.Taints, tt); err != nil { + n.AddErr(ctx, err) + } + n.checkUtilization(ctx, nmx[fqn]) + } + + return nil +} + +func (n *Node) checkTaints(ctx context.Context, taints []v1.Taint, tt tolerations) error { + for _, ta := range taints { + if _, ok := tt[mkKey(ta.Key, ta.Value)]; !ok { + n.AddCode(ctx, 700, ta.Key) + } + } + + return nil +} + +func (n *Node) fetchPodTolerations() (tolerations, error) { + tt := make(tolerations) + txn, it := n.db.MustITFor(internal.Glossary[internal.PO]) + defer txn.Abort() + + for o := it.Next(); o != nil; o = it.Next() { + po, ok := o.(*v1.Pod) + if !ok { + return nil, errors.New("po conversion failed") + } + for _, t := range po.Spec.Tolerations { + tt[mkKey(t.Key, t.Value)] = struct{}{} + } + } + + return tt, nil +} + +func mkKey(k, v string) string { + return k + ":" + v +} + +func (n *Node) checkConditions(ctx context.Context, no *v1.Node) { + if no.Spec.Unschedulable { + n.AddCode(ctx, 711) + } + for _, c := range no.Status.Conditions { + if c.Status == v1.ConditionUnknown { + n.AddCode(ctx, 701) + } + if c.Type == v1.NodeReady && c.Status == v1.ConditionFalse { + n.AddCode(ctx, 702) + } + n.statusReport(ctx, c.Type, c.Status) + } +} + +func (n *Node) statusReport(ctx context.Context, cond v1.NodeConditionType, status v1.ConditionStatus) { + if status == v1.ConditionFalse { + return + } + + switch cond { + case v1.NodeMemoryPressure: + n.AddCode(ctx, 704) + case v1.NodeDiskPressure: + n.AddCode(ctx, 705) + case v1.NodePIDPressure: + n.AddCode(ctx, 706) + case v1.NodeNetworkUnavailable: + n.AddCode(ctx, 707) + } +} + +func (n *Node) checkUtilization(ctx context.Context, mx client.NodeMetrics) { + if mx.Empty() { + n.AddCode(ctx, 708) + return + } + + percCPU := ToPerc(toMC(mx.CurrentCPU), toMC(mx.AvailableCPU)) + cpuLimit := int64(n.NodeCPULimit()) + if percCPU > cpuLimit { + n.AddCode(ctx, 709, cpuLimit, percCPU) + } + + percMEM := ToPerc(toMB(mx.CurrentMEM), toMB(mx.AvailableMEM)) + memLimit := int64(n.NodeMEMLimit()) + if percMEM > memLimit { + n.AddCode(ctx, 710, memLimit, percMEM) + } +} + +func (n *Node) nodesMetrics(nmx client.NodesMetrics) { + mm, err := n.db.ListNMX() + if err != nil || len(mm) == 0 { + return + } + + txn, it := n.db.MustITFor(internal.Glossary[internal.NO]) + defer txn.Abort() + for o := it.Next(); o != nil; o = it.Next() { + no := o.(*v1.Node) + if len(no.Status.Allocatable) == 0 && len(no.Status.Capacity) == 0 { + continue + } + nmx[no.Name] = client.NodeMetrics{ + AvailableCPU: *no.Status.Allocatable.Cpu(), + AvailableMEM: *no.Status.Allocatable.Memory(), + TotalCPU: *no.Status.Capacity.Cpu(), + TotalMEM: *no.Status.Capacity.Memory(), + } + } + + for _, m := range mm { + if mx, ok := nmx[m.Name]; ok { + mx.CurrentCPU = *m.Usage.Cpu() + mx.CurrentMEM = *m.Usage.Memory() + nmx[m.Name] = mx + } + } +} diff --git a/internal/sanitize/node_bench_test.go b/internal/lint/node_bench_test.go similarity index 97% rename from internal/sanitize/node_bench_test.go rename to internal/lint/node_bench_test.go index b026e435..17f09c25 100644 --- a/internal/sanitize/node_bench_test.go +++ b/internal/lint/node_bench_test.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright Authors of Popeye -package sanitize +package lint // import ( // "testing" diff --git a/internal/lint/node_test.go b/internal/lint/node_test.go new file mode 100644 index 00000000..84586db6 --- /dev/null +++ b/internal/lint/node_test.go @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "testing" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/rules" + "github.com/derailed/popeye/internal/test" + "github.com/stretchr/testify/assert" + v1 "k8s.io/api/core/v1" + mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" +) + +func TestNodeSanitizer(t *testing.T) { + dba, err := test.NewTestDB() + assert.NoError(t, err) + l := db.NewLoader(dba) + + ctx := test.MakeCtx(t) + assert.NoError(t, test.LoadDB[*v1.Node](ctx, l.DB, "core/node/1.yaml", internal.Glossary[internal.NO])) + assert.NoError(t, test.LoadDB[*v1.Pod](ctx, l.DB, "core/pod/1.yaml", internal.Glossary[internal.PO])) + assert.NoError(t, test.LoadDB[*mv1beta1.NodeMetrics](ctx, l.DB, "mx/node/1.yaml", internal.Glossary[internal.NMX])) + + no := NewNode(test.MakeCollector(t), dba) + assert.Nil(t, no.Lint(test.MakeContext("v1/nodes", "nodes"))) + assert.Equal(t, 5, len(no.Outcome())) + + ii := no.Outcome()["n1"] + assert.Equal(t, 0, len(ii)) + + ii = no.Outcome()["n2"] + assert.Equal(t, 2, len(ii)) + assert.Equal(t, `[POP-707] No network configured on node`, ii[0].Message) + assert.Equal(t, rules.ErrorLevel, ii[0].Level) + assert.Equal(t, `[POP-700] Found taint "t2" but no pod can tolerate`, ii[1].Message) + assert.Equal(t, rules.WarnLevel, ii[1].Level) + + ii = no.Outcome()["n3"] + assert.Equal(t, 5, len(ii)) + assert.Equal(t, `[POP-704] Insufficient memory`, ii[0].Message) + assert.Equal(t, rules.WarnLevel, ii[0].Level) + assert.Equal(t, `[POP-705] Insufficient disk space`, ii[1].Message) + assert.Equal(t, rules.WarnLevel, ii[1].Level) + assert.Equal(t, `[POP-706] Insufficient PIDs on Node`, ii[2].Message) + assert.Equal(t, rules.ErrorLevel, ii[2].Level) + assert.Equal(t, `[POP-707] No network configured on node`, ii[3].Message) + assert.Equal(t, rules.ErrorLevel, ii[3].Level) + assert.Equal(t, `[POP-708] No node metrics available`, ii[4].Message) + assert.Equal(t, rules.InfoLevel, ii[4].Level) + + ii = no.Outcome()["n4"] + assert.Equal(t, 4, len(ii)) + assert.Equal(t, `[POP-711] Scheduling disabled`, ii[0].Message) + assert.Equal(t, rules.WarnLevel, ii[0].Level) + assert.Equal(t, `[POP-701] Node has an unknown condition`, ii[1].Message) + assert.Equal(t, rules.WarnLevel, ii[1].Level) + assert.Equal(t, `[POP-702] Node is not in ready state`, ii[2].Message) + assert.Equal(t, rules.ErrorLevel, ii[2].Level) + assert.Equal(t, `[POP-708] No node metrics available`, ii[3].Message) + assert.Equal(t, rules.InfoLevel, ii[3].Level) + + ii = no.Outcome()["n5"] + assert.Equal(t, 2, len(ii)) + assert.Equal(t, `[POP-709] CPU threshold (80%) reached 20000%`, ii[0].Message) + assert.Equal(t, rules.WarnLevel, ii[0].Level) + assert.Equal(t, `[POP-710] Memory threshold (80%) reached 400%`, ii[1].Message) + assert.Equal(t, rules.WarnLevel, ii[1].Level) +} diff --git a/internal/lint/np.go b/internal/lint/np.go new file mode 100644 index 00000000..4526abb0 --- /dev/null +++ b/internal/lint/np.go @@ -0,0 +1,261 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "context" + "fmt" + "net" + "strings" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/client" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/issues" + v1 "k8s.io/api/core/v1" + netv1 "k8s.io/api/networking/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type direction string + +const ( + dirIn direction = "Ingress" + dirOut direction = "Egress" + bothPols = "All" + noPols = "" +) + +// NetworkPolicy tracks NetworkPolicy sanitizatios. +type NetworkPolicy struct { + *issues.Collector + + db *db.DB + ipCache map[string][]v1.PodIP +} + +// NewNetworkPolicy returns a new instance. +func NewNetworkPolicy(co *issues.Collector, db *db.DB) *NetworkPolicy { + return &NetworkPolicy{ + Collector: co, + db: db, + ipCache: make(map[string][]v1.PodIP), + } +} + +// Lint cleanse the resource. +func (s *NetworkPolicy) Lint(ctx context.Context) error { + txn, it := s.db.MustITFor(internal.Glossary[internal.NP]) + defer txn.Abort() + for o := it.Next(); o != nil; o = it.Next() { + np := o.(*netv1.NetworkPolicy) + fqn := client.FQN(np.Namespace, np.Name) + s.InitOutcome(fqn) + ctx = internal.WithSpec(ctx, specFor(fqn, np)) + + s.checkSelector(ctx, fqn, np.Spec.PodSelector) + s.checkIngresses(ctx, fqn, np.Spec.Ingress) + s.checkEgresses(ctx, fqn, np.Spec.Egress) + s.checkRuleType(ctx, fqn, &np.Spec) + } + + return nil +} + +func (s *NetworkPolicy) checkRuleType(ctx context.Context, fqn string, spec *netv1.NetworkPolicySpec) { + if spec.PodSelector.Size() > 0 { + return + } + + switch { + case isAllowAll(spec): + s.AddCode(ctx, 1203, "Allow", bothPols) + case isAllowAllIngress(spec): + s.AddCode(ctx, 1203, "Allow All", dirIn) + case isAllowAllEgress(spec): + s.AddCode(ctx, 1203, "Allow All", dirOut) + case isDenyAll(spec): + s.AddCode(ctx, 1203, "Deny", bothPols) + case isDenyAllIngress(spec): + s.AddCode(ctx, 1203, "Deny All", dirIn) + case isDenyAllEgress(spec): + s.AddCode(ctx, 1203, "Deny All", dirOut) + } +} + +func isDefaultDenyAll(np *netv1.NetworkPolicy) bool { + if len(np.Spec.Ingress) > 0 { + return false + } + if len(np.Spec.Egress) > 0 { + return false + } + if np.Spec.PodSelector.Size() > 0 { + return false + } + + return len(np.Spec.PolicyTypes) == 2 +} + +func (s *NetworkPolicy) checkSelector(ctx context.Context, fqn string, sel metav1.LabelSelector) { + ns, _ := client.Namespaced(fqn) + if sel.Size() > 0 { + pp, err := s.db.FindPodsBySel(ns, &sel) + if err != nil || len(pp) == 0 { + s.AddCode(ctx, 1200, dumpSel(&sel)) + return + } + } +} + +func (s *NetworkPolicy) checkIngresses(ctx context.Context, fqn string, rr []netv1.NetworkPolicyIngressRule) { + for _, r := range rr { + for _, from := range r.From { + s.checkSelectors(ctx, fqn, from.NamespaceSelector, from.PodSelector, dirIn) + s.checkIPBlocks(ctx, fqn, from.IPBlock, dirIn) + } + } +} + +func (s *NetworkPolicy) checkEgresses(ctx context.Context, fqn string, rr []netv1.NetworkPolicyEgressRule) { + for _, r := range rr { + for _, to := range r.To { + s.checkSelectors(ctx, fqn, to.NamespaceSelector, to.PodSelector, dirOut) + s.checkIPBlocks(ctx, fqn, to.IPBlock, dirOut) + } + } +} + +func (s *NetworkPolicy) checkSelectors(ctx context.Context, fqn string, nsSel, podSel *metav1.LabelSelector, d direction) { + ns, _ := client.Namespaced(fqn) + if nsSel != nil && nsSel.Size() > 0 { + nss, err := s.db.FindNSBySel(nsSel) + if err != nil { + s.AddErr(ctx, fmt.Errorf("unable to locate namespace using selector: %s", dumpSel(nsSel))) + return + } + s.checkNSSelector(ctx, nsSel, nss, d) + s.checkPodSelector(ctx, nss, podSel, d) + return + } + nss, err := s.db.FindNS(ns) + if err != nil { + s.AddErr(ctx, fmt.Errorf("unable to locate namespace: %q", ns)) + return + } + s.checkPodSelector(ctx, []*v1.Namespace{nss}, podSel, d) +} + +func (s *NetworkPolicy) checkIPBlocks(ctx context.Context, fqn string, b *netv1.IPBlock, d direction) { + if b == nil { + return + } + ns, _ := client.Namespaced(fqn) + _, ipnet, err := net.ParseCIDR(b.CIDR) + if err != nil { + s.AddErr(ctx, err) + } + if !s.matchPips(ns, ipnet) { + s.AddCode(ctx, 1206, d, b.CIDR) + } + for _, ex := range b.Except { + _, ipnet, err := net.ParseCIDR(ex) + if err != nil { + s.AddErr(ctx, err) + continue + } + if !s.matchPips(ns, ipnet) { + s.AddCode(ctx, 1207, d, ex) + } + } +} + +func (s *NetworkPolicy) matchPips(ns string, ipnet *net.IPNet) bool { + if ipnet == nil { + return false + } + txn, it := s.db.MustITForNS(internal.Glossary[internal.PO], ns) + defer txn.Abort() + for o := it.Next(); o != nil; o = it.Next() { + po := o.(*v1.Pod) + for _, ip := range po.Status.PodIPs { + if ipnet.Contains(net.ParseIP(ip.IP)) { + return true + } + } + } + + return false +} + +func (s *NetworkPolicy) checkPodSelector(ctx context.Context, nss []*v1.Namespace, sel *metav1.LabelSelector, d direction) { + if sel == nil || sel.Size() == 0 { + return + } + + var found bool + nn := make([]string, 0, len(nss)) + for _, ns := range nss { + pp, err := s.db.FindPodsBySel(ns.Name, sel) + if err != nil { + s.AddErr(ctx, fmt.Errorf("unable to locate pods by selector: %w", err)) + return + } + if len(pp) > 0 { + found = true + } else { + nn = append(nn, ns.Name) + } + } + if !found { + if len(nn) > 0 { + s.AddCode(ctx, 1208, d, dumpSel(sel), strings.Join(nn, ",")) + } else { + s.AddCode(ctx, 1202, d, dumpSel(sel)) + } + } +} + +func (s *NetworkPolicy) checkNSSelector(ctx context.Context, sel *metav1.LabelSelector, nss []*v1.Namespace, d direction) bool { + if len(nss) == 0 { + s.AddCode(ctx, 1201, d, dumpSel(sel)) + return false + } + + return true +} + +// Helpers... + +func dumpLabels(labels map[string]string) string { + if len(labels) == 0 { + return "" + } + ll := make([]string, 0, len(labels)) + for k, v := range labels { + ll = append(ll, fmt.Sprintf("%s=%s", k, v)) + } + + return strings.Join(ll, ",") +} + +func dumpSel(sel *metav1.LabelSelector) string { + if sel == nil { + return "n/a" + } + + var out string + out = dumpLabels(sel.MatchLabels) + + ll := make([]string, 0, len(sel.MatchExpressions)) + for _, v := range sel.MatchExpressions { + ll = append(ll, fmt.Sprintf("%s-%s-%s", v.Key, v.Operator, strings.Join(v.Values, ","))) + } + if out != "" && len(ll) > 0 { + out += "|" + } + out += strings.Join(ll, ",") + + return out +} diff --git a/internal/lint/np_helpers.go b/internal/lint/np_helpers.go new file mode 100644 index 00000000..193f823f --- /dev/null +++ b/internal/lint/np_helpers.go @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import netv1 "k8s.io/api/networking/v1" + +func noPodSel(spec *netv1.NetworkPolicySpec) bool { + return spec.PodSelector.Size() == 0 +} + +func isAllowAll(spec *netv1.NetworkPolicySpec) bool { + return noPodSel(spec) && + blankIngress(spec.Ingress) && blankEgress(spec.Egress) && + len(spec.PolicyTypes) == 2 +} + +func isAllowAllIngress(spec *netv1.NetworkPolicySpec) bool { + return noPodSel(spec) && + blankIngress(spec.Ingress) && polInclude(spec.PolicyTypes, dirIn) +} + +func isAllowAllEgress(spec *netv1.NetworkPolicySpec) bool { + return noPodSel(spec) && + blankEgress(spec.Egress) && polInclude(spec.PolicyTypes, dirOut) +} + +func isDeny(spec *netv1.NetworkPolicySpec) bool { + return noPodSel(spec) && spec.Egress == nil && spec.Ingress == nil +} + +func isDenyAll(spec *netv1.NetworkPolicySpec) bool { + return isDeny(spec) && len(spec.PolicyTypes) == 2 +} + +func isDenyAllIngress(spec *netv1.NetworkPolicySpec) bool { + return noPodSel(spec) && spec.Ingress == nil && polInclude(spec.PolicyTypes, dirIn) +} + +func isDenyAllEgress(spec *netv1.NetworkPolicySpec) bool { + return noPodSel(spec) && spec.Egress == nil && polInclude(spec.PolicyTypes, dirOut) +} + +func blankEgress(rr []netv1.NetworkPolicyEgressRule) bool { + return len(rr) == 1 && len(rr[0].Ports) == 0 && len(rr[0].To) == 0 +} + +func blankIngress(rr []netv1.NetworkPolicyIngressRule) bool { + return len(rr) == 1 && len(rr[0].Ports) == 0 && len(rr[0].From) == 0 +} + +func polInclude(pp []netv1.PolicyType, d direction) bool { + for _, p := range pp { + if p == netv1.PolicyType(d) { + return true + } + } + + return false +} diff --git a/internal/lint/np_test.go b/internal/lint/np_test.go new file mode 100644 index 00000000..67d99889 --- /dev/null +++ b/internal/lint/np_test.go @@ -0,0 +1,147 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "testing" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/rules" + "github.com/derailed/popeye/internal/test" + "github.com/stretchr/testify/assert" + v1 "k8s.io/api/core/v1" + netv1 "k8s.io/api/networking/v1" +) + +func TestNPLintDenyAll(t *testing.T) { + dba, err := test.NewTestDB() + assert.NoError(t, err) + l := db.NewLoader(dba) + + ctx := test.MakeCtx(t) + assert.NoError(t, test.LoadDB[*netv1.NetworkPolicy](ctx, l.DB, "net/np/2.yaml", internal.Glossary[internal.NP])) + assert.NoError(t, test.LoadDB[*v1.Namespace](ctx, l.DB, "core/ns/1.yaml", internal.Glossary[internal.NS])) + assert.NoError(t, test.LoadDB[*v1.Pod](ctx, l.DB, "core/pod/1.yaml", internal.Glossary[internal.PO])) + + np := NewNetworkPolicy(test.MakeCollector(t), dba) + assert.Nil(t, np.Lint(test.MakeContext("networking.k8s.io/v1/networkpolicies", "networkpolicies"))) + assert.Equal(t, 8, len(np.Outcome())) + + ii := np.Outcome()["default/deny-all"] + assert.Equal(t, 1, len(ii)) + assert.Equal(t, `[POP-1203] Deny All policy in effect`, ii[0].Message) + assert.Equal(t, rules.InfoLevel, ii[0].Level) + + ii = np.Outcome()["default/deny-all-ing"] + assert.Equal(t, 1, len(ii)) + assert.Equal(t, `[POP-1203] Deny All Ingress policy in effect`, ii[0].Message) + assert.Equal(t, rules.InfoLevel, ii[0].Level) + + ii = np.Outcome()["default/deny-all-eg"] + assert.Equal(t, 1, len(ii)) + assert.Equal(t, `[POP-1203] Deny All Egress policy in effect`, ii[0].Message) + assert.Equal(t, rules.InfoLevel, ii[0].Level) + + ii = np.Outcome()["default/allow-all"] + assert.Equal(t, 1, len(ii)) + assert.Equal(t, `[POP-1203] Allow All policy in effect`, ii[0].Message) + assert.Equal(t, rules.InfoLevel, ii[0].Level) + + ii = np.Outcome()["default/allow-all-ing"] + assert.Equal(t, 1, len(ii)) + assert.Equal(t, `[POP-1203] Allow All Ingress policy in effect`, ii[0].Message) + assert.Equal(t, rules.InfoLevel, ii[0].Level) + + ii = np.Outcome()["default/allow-all-eg"] + assert.Equal(t, 1, len(ii)) + assert.Equal(t, `[POP-1203] Allow All Egress policy in effect`, ii[0].Message) + assert.Equal(t, rules.InfoLevel, ii[0].Level) + + ii = np.Outcome()["default/ip-block-all-ing"] + assert.Equal(t, 2, len(ii)) + assert.Equal(t, `[POP-1206] No pods matched Egress IPBlock 172.2.0.0/24`, ii[0].Message) + assert.Equal(t, rules.WarnLevel, ii[0].Level) + assert.Equal(t, `[POP-1203] Deny All Ingress policy in effect`, ii[1].Message) + assert.Equal(t, rules.InfoLevel, ii[1].Level) + + ii = np.Outcome()["default/ip-block-all-eg"] + assert.Equal(t, 2, len(ii)) + assert.Equal(t, `[POP-1206] No pods matched Ingress IPBlock 172.2.0.0/24`, ii[0].Message) + assert.Equal(t, rules.WarnLevel, ii[0].Level) + assert.Equal(t, `[POP-1203] Deny All Egress policy in effect`, ii[1].Message) + assert.Equal(t, rules.InfoLevel, ii[1].Level) +} + +func TestNPLint(t *testing.T) { + dba, err := test.NewTestDB() + assert.NoError(t, err) + l := db.NewLoader(dba) + + ctx := test.MakeCtx(t) + assert.NoError(t, test.LoadDB[*netv1.NetworkPolicy](ctx, l.DB, "net/np/1.yaml", internal.Glossary[internal.NP])) + assert.NoError(t, test.LoadDB[*v1.Namespace](ctx, l.DB, "core/ns/1.yaml", internal.Glossary[internal.NS])) + assert.NoError(t, test.LoadDB[*v1.Pod](ctx, l.DB, "core/pod/1.yaml", internal.Glossary[internal.PO])) + + np := NewNetworkPolicy(test.MakeCollector(t), dba) + assert.Nil(t, np.Lint(test.MakeContext("networking.k8s.io/v1/networkpolicies", "networkpolicies"))) + assert.Equal(t, 3, len(np.Outcome())) + + ii := np.Outcome()["default/np1"] + assert.Equal(t, 0, len(ii)) + + ii = np.Outcome()["default/np2"] + assert.Equal(t, 3, len(ii)) + assert.Equal(t, `[POP-1207] No pods matched except Ingress IPBlock 172.1.1.0/24`, ii[0].Message) + assert.Equal(t, rules.WarnLevel, ii[0].Level) + assert.Equal(t, `[POP-1208] No pods match Ingress pod selector: app=p2 in namespace: ns2`, ii[1].Message) + assert.Equal(t, rules.WarnLevel, ii[1].Level) + assert.Equal(t, `[POP-1206] No pods matched Egress IPBlock 172.0.0.0/24`, ii[2].Message) + assert.Equal(t, rules.WarnLevel, ii[2].Level) + + ii = np.Outcome()["default/np3"] + assert.Equal(t, 6, len(ii)) + assert.Equal(t, `[POP-1200] No pods match pod selector: app=p-bozo`, ii[0].Message) + assert.Equal(t, rules.WarnLevel, ii[0].Level) + assert.Equal(t, `[POP-1206] No pods matched Ingress IPBlock 172.2.0.0/16`, ii[1].Message) + assert.Equal(t, rules.WarnLevel, ii[1].Level) + assert.Equal(t, `[POP-1207] No pods matched except Ingress IPBlock 172.2.1.0/24`, ii[2].Message) + assert.Equal(t, rules.WarnLevel, ii[2].Level) + assert.Equal(t, `[POP-1201] No namespaces match Ingress namespace selector: app-In-ns-bozo`, ii[3].Message) + assert.Equal(t, rules.WarnLevel, ii[3].Level) + assert.Equal(t, `[POP-1202] No pods match Ingress pod selector: app=pod-bozo`, ii[4].Message) + assert.Equal(t, rules.WarnLevel, ii[4].Level) + assert.Equal(t, `[POP-1208] No pods match Egress pod selector: app=p1-missing in namespace: default`, ii[5].Message) + assert.Equal(t, rules.WarnLevel, ii[5].Level) +} + +func Test_npDefaultDenyAll(t *testing.T) { + uu := map[string]struct { + path string + e bool + }{ + "open": { + path: "net/np/a.yaml", + }, + "deny-all": { + path: "net/np/deny-all.yaml", + e: true, + }, + "allow-all-ing": { + path: "net/np/allow-all-ing.yaml", + }, + "no-selector": { + path: "net/np/d.yaml", + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + np, err := test.LoadRes[netv1.NetworkPolicy](u.path) + assert.NoError(t, err) + assert.Equal(t, u.e, isDefaultDenyAll(np)) + }) + } +} diff --git a/internal/lint/ns.go b/internal/lint/ns.go new file mode 100644 index 00000000..2d123290 --- /dev/null +++ b/internal/lint/ns.go @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "context" + "errors" + "sync" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/cache" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/issues" + v1 "k8s.io/api/core/v1" +) + +// Namespace represents a Namespace linter. +type Namespace struct { + *issues.Collector + db *db.DB +} + +// NewNamespace returns a new instance. +func NewNamespace(co *issues.Collector, db *db.DB) *Namespace { + return &Namespace{ + Collector: co, + db: db, + } +} + +// ReferencedNamespaces fetch all namespaces referenced by pods and service accounts. +func (s *Namespace) ReferencedNamespaces(res map[string]struct{}) error { + var refs sync.Map + pod := cache.NewPod(s.db) + if err := pod.PodRefs(&refs); err != nil { + return err + } + sa := cache.NewServiceAccount(s.db) + if err := sa.ServiceAccountRefs(&refs); err != nil { + return err + } + if ss, ok := refs.Load("ns"); ok { + for ns := range ss.(internal.StringSet) { + res[ns] = struct{}{} + } + } + + return nil +} + +// Lint cleanse the resource. +func (s *Namespace) Lint(ctx context.Context) error { + used := make(map[string]struct{}) + if err := s.ReferencedNamespaces(used); err != nil { + s.AddErr(ctx, err) + } + txn, it := s.db.MustITFor(internal.Glossary[internal.NS]) + defer txn.Abort() + + for o := it.Next(); o != nil; o = it.Next() { + ns, ok := o.(*v1.Namespace) + if !ok { + return errors.New("expected ns") + } + fqn := ns.Name + s.InitOutcome(fqn) + ctx = internal.WithSpec(ctx, specFor(fqn, ns)) + + if s.checkActive(ctx, ns.Status.Phase) { + if _, ok := used[fqn]; !ok { + s.AddCode(ctx, 400) + } + } + } + + return nil +} + +func (s *Namespace) checkActive(ctx context.Context, p v1.NamespacePhase) bool { + if !isNSActive(p) { + s.AddCode(ctx, 800) + return false + } + + return true +} + +// ---------------------------------------------------------------------------- +// Helpers... + +func isNSActive(phase v1.NamespacePhase) bool { + return phase == v1.NamespaceActive +} diff --git a/internal/lint/ns_test.go b/internal/lint/ns_test.go new file mode 100644 index 00000000..ddeb3742 --- /dev/null +++ b/internal/lint/ns_test.go @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "testing" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/rules" + "github.com/derailed/popeye/internal/test" + "github.com/stretchr/testify/assert" + v1 "k8s.io/api/core/v1" +) + +func TestNSSanitizer(t *testing.T) { + dba, err := test.NewTestDB() + assert.NoError(t, err) + l := db.NewLoader(dba) + ctx := test.MakeCtx(t) + assert.NoError(t, test.LoadDB[*v1.Namespace](ctx, l.DB, "core/ns/1.yaml", internal.Glossary[internal.NS])) + assert.NoError(t, test.LoadDB[*v1.Pod](ctx, l.DB, "core/pod/1.yaml", internal.Glossary[internal.PO])) + + ns := NewNamespace(test.MakeCollector(t), dba) + assert.Nil(t, ns.Lint(test.MakeContext("v1/namespaces", "ns"))) + assert.Equal(t, 3, len(ns.Outcome())) + + ii := ns.Outcome()["default"] + assert.Equal(t, 0, len(ii)) + + ii = ns.Outcome()["ns1"] + assert.Equal(t, 1, len(ii)) + assert.Equal(t, "[POP-400] Used? Unable to locate resource reference", ii[0].Message) + assert.Equal(t, rules.InfoLevel, ii[0].Level) + + ii = ns.Outcome()["ns2"] + assert.Equal(t, 1, len(ii)) + assert.Equal(t, "[POP-800] Namespace is inactive", ii[0].Message) + assert.Equal(t, rules.ErrorLevel, ii[0].Level) +} diff --git a/internal/lint/pdb.go b/internal/lint/pdb.go new file mode 100644 index 00000000..a8774c37 --- /dev/null +++ b/internal/lint/pdb.go @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "context" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/client" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/issues" + polv1 "k8s.io/api/policy/v1" +) + +// PodDisruptionBudget tracks PodDisruptionBudget sanitization. +type PodDisruptionBudget struct { + *issues.Collector + + db *db.DB +} + +// NewPodDisruptionBudget returns a new PodDisruptionBudget linter. +func NewPodDisruptionBudget(c *issues.Collector, db *db.DB) *PodDisruptionBudget { + return &PodDisruptionBudget{ + Collector: c, + db: db, + } +} + +// Lint cleanse the resource. +func (p *PodDisruptionBudget) Lint(ctx context.Context) error { + txn, it := p.db.MustITFor(internal.Glossary[internal.PDB]) + defer txn.Abort() + for o := it.Next(); o != nil; o = it.Next() { + pdb := o.(*polv1.PodDisruptionBudget) + fqn := client.FQN(pdb.Namespace, pdb.Name) + p.InitOutcome(fqn) + ctx = internal.WithSpec(ctx, specFor(fqn, pdb)) + + p.checkInUse(ctx, pdb) + } + + return nil +} + +func (p *PodDisruptionBudget) checkInUse(ctx context.Context, pdb *polv1.PodDisruptionBudget) { + pp, err := p.db.FindPodsBySel(pdb.Namespace, pdb.Spec.Selector) + if err != nil || len(pp) == 0 { + p.AddCode(ctx, 900, dumpSel(pdb.Spec.Selector)) + return + } +} diff --git a/internal/lint/pdb_test.go b/internal/lint/pdb_test.go new file mode 100644 index 00000000..5a80ec01 --- /dev/null +++ b/internal/lint/pdb_test.go @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "testing" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/rules" + "github.com/derailed/popeye/internal/test" + "github.com/stretchr/testify/assert" + v1 "k8s.io/api/core/v1" + polv1 "k8s.io/api/policy/v1" +) + +func TestPDBLint(t *testing.T) { + dba, err := test.NewTestDB() + assert.NoError(t, err) + l := db.NewLoader(dba) + + ctx := test.MakeCtx(t) + assert.NoError(t, test.LoadDB[*polv1.PodDisruptionBudget](ctx, l.DB, "pol/pdb/1.yaml", internal.Glossary[internal.PDB])) + assert.NoError(t, test.LoadDB[*v1.Pod](ctx, l.DB, "core/pod/1.yaml", internal.Glossary[internal.PO])) + + pdb := NewPodDisruptionBudget(test.MakeCollector(t), dba) + assert.Nil(t, pdb.Lint(test.MakeContext("policy/v1/poddisruptionbudgets", "poddisruptionbudgets"))) + assert.Equal(t, 5, len(pdb.Outcome())) + + ii := pdb.Outcome()["default/pdb1"] + assert.Equal(t, 0, len(ii)) + + ii = pdb.Outcome()["default/pdb2"] + assert.Equal(t, 1, len(ii)) + assert.Equal(t, `[POP-900] No pods match pdb selector: app=p2`, ii[0].Message) + assert.Equal(t, rules.WarnLevel, ii[0].Level) + + ii = pdb.Outcome()["default/pdb3"] + assert.Equal(t, 1, len(ii)) + assert.Equal(t, `[POP-900] No pods match pdb selector: app=test4`, ii[0].Message) + assert.Equal(t, rules.WarnLevel, ii[0].Level) + + ii = pdb.Outcome()["default/pdb4"] + assert.Equal(t, 1, len(ii)) + assert.Equal(t, `[POP-900] No pods match pdb selector: app=test5`, ii[0].Message) + assert.Equal(t, rules.WarnLevel, ii[0].Level) + + ii = pdb.Outcome()["default/pdb4-1"] + assert.Equal(t, 1, len(ii)) + assert.Equal(t, `[POP-900] No pods match pdb selector: app=test5`, ii[0].Message) + assert.Equal(t, rules.WarnLevel, ii[0].Level) +} diff --git a/internal/lint/pod.go b/internal/lint/pod.go new file mode 100644 index 00000000..0bd2ad32 --- /dev/null +++ b/internal/lint/pod.go @@ -0,0 +1,468 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "context" + "fmt" + "net" + "sort" + "strings" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/cache" + "github.com/derailed/popeye/internal/client" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/issues" + "github.com/derailed/popeye/types" + "github.com/rs/zerolog/log" + v1 "k8s.io/api/core/v1" + netv1 "k8s.io/api/networking/v1" + policyv1 "k8s.io/api/policy/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" +) + +type ( + // Pod represents a Pod linter. + Pod struct { + *issues.Collector + + db *db.DB + } + + // PodMetric tracks pod metrics available and current range. + PodMetric interface { + CurrentCPU() int64 + CurrentMEM() int64 + Empty() bool + } +) + +// NewPod returns a new instance. +func NewPod(co *issues.Collector, db *db.DB) *Pod { + return &Pod{ + Collector: co, + db: db, + } +} + +// Lint cleanse the resource.. +func (s *Pod) Lint(ctx context.Context) error { + txn, it := s.db.MustITFor(internal.Glossary[internal.PO]) + defer txn.Abort() + for o := it.Next(); o != nil; o = it.Next() { + po := o.(*v1.Pod) + fqn := client.FQN(po.Namespace, po.Name) + s.InitOutcome(fqn) + defer s.CloseOutcome(ctx, fqn, nil) + + ctx = internal.WithSpec(ctx, specFor(fqn, po)) + s.checkStatus(ctx, po) + s.checkContainerStatus(ctx, fqn, po) + s.checkContainers(ctx, fqn, po) + s.checkOwnedByAnything(ctx, po.OwnerReferences) + s.checkNPs(ctx, po) + if !ownedByDaemonSet(po) { + s.checkPdb(ctx, po.ObjectMeta.Labels) + } + s.checkForMultiplePdbMatches(ctx, po.Namespace, po.ObjectMeta.Labels) + s.checkSecure(ctx, fqn, po.Spec) + + pmx, err := s.db.FindPMX(fqn) + if err != nil { + continue + } + cmx := make(client.ContainerMetrics) + containerMetrics(pmx, cmx) + s.checkUtilization(ctx, fqn, po, cmx) + } + + return nil +} + +func ownedByDaemonSet(po *v1.Pod) bool { + for _, o := range po.OwnerReferences { + if o.Kind == "DaemonSet" { + return true + } + } + return false +} + +func (s *Pod) checkNPs(ctx context.Context, pod *v1.Pod) { + txn, it := s.db.MustITForNS(internal.Glossary[internal.NP], pod.Namespace) + defer txn.Abort() + + matches := [2]int{} + for o := it.Next(); o != nil; o = it.Next() { + np := o.(*netv1.NetworkPolicy) + if isDenyAll(&np.Spec) || isAllowAll(&np.Spec) { + return + } + if isDenyAllIngress(&np.Spec) || isAllowAllIngress(&np.Spec) { + matches[0]++ + if s.checkEgresses(ctx, pod, np.Spec.Egress) { + matches[1]++ + } + continue + } + if isDenyAllEgress(&np.Spec) || isAllowAllEgress(&np.Spec) { + matches[1]++ + if s.checkIngresses(ctx, pod, np.Spec.Ingress) { + matches[0]++ + } + continue + } + if labelsMatch(&np.Spec.PodSelector, pod.Labels) { + if s.checkIngresses(ctx, pod, np.Spec.Ingress) { + matches[0]++ + } + if s.checkEgresses(ctx, pod, np.Spec.Egress) { + matches[1]++ + } + } + } + + if matches[0] == 0 { + s.AddCode(ctx, 1204, dirIn) + } + if matches[1] == 0 { + s.AddCode(ctx, 1204, dirOut) + } +} + +type Labels map[string]string + +func (s *Pod) isPodTargeted(pod *v1.Pod, nsSel, podSel *metav1.LabelSelector, b *netv1.IPBlock) (bool, error) { + nn, err := s.db.FindNSNameBySel(nsSel) + if err != nil { + return false, err + } + if len(nn) == 0 && b == nil { + if podSel == nil { + return false, nil + } + return labelsMatch(podSel, pod.Labels), nil + } + for _, sns := range nn { + if sns != pod.Namespace { + continue + } + if podSel != nil && podSel.Size() > 0 { + if labelsMatch(podSel, pod.Labels) { + return true, nil + } + } + } + + if b == nil { + return false, nil + } + _, ipnet, err := net.ParseCIDR(b.CIDR) + if err != nil { + return false, err + } + for _, ip := range pod.Status.PodIPs { + if ipnet.Contains(net.ParseIP(ip.IP)) { + return true, nil + } + } + + return false, nil +} + +func (s *Pod) checkIngresses(ctx context.Context, pod *v1.Pod, rr []netv1.NetworkPolicyIngressRule) bool { + var match int + if rr == nil { + return false + } + for _, r := range rr { + if r.From == nil { + return true + } + for _, from := range r.From { + ok, err := s.isPodTargeted(pod, from.NamespaceSelector, from.PodSelector, from.IPBlock) + if err != nil { + s.AddErr(ctx, err) + return true + } + if ok { + match++ + } + } + } + + return match > 0 +} + +func (s *Pod) checkEgresses(ctx context.Context, pod *v1.Pod, rr []netv1.NetworkPolicyEgressRule) bool { + if rr == nil { + return false + } + var match int + for _, r := range rr { + if r.To == nil { + return true + } + for _, to := range r.To { + ok, err := s.isPodTargeted(pod, to.NamespaceSelector, to.PodSelector, to.IPBlock) + if err != nil { + s.AddErr(ctx, err) + return true + } + if ok { + match++ + } + } + } + + return match > 0 +} + +func labelsMatch(sel *metav1.LabelSelector, ll map[string]string) bool { + if sel == nil || sel.Size() == 0 { + return true + } + + return db.MatchSelector(ll, sel) +} + +func (s *Pod) checkOwnedByAnything(ctx context.Context, ownerRefs []metav1.OwnerReference) { + if len(ownerRefs) == 0 { + s.AddCode(ctx, 208) + return + } + + controlled := false + for _, or := range ownerRefs { + if or.Controller != nil && *or.Controller { + controlled = true + break + } + } + + if !controlled { + s.AddCode(ctx, 208) + } +} + +func (s *Pod) checkPdb(ctx context.Context, labels map[string]string) { + if s.ForLabels(labels) == nil { + s.AddCode(ctx, 206) + } +} + +// ForLabels returns a pdb whose selector match the given labels. Returns nil if no match. +func (s *Pod) ForLabels(labels map[string]string) *policyv1.PodDisruptionBudget { + txn, it := s.db.MustITFor(internal.Glossary[internal.PDB]) + defer txn.Abort() + for o := it.Next(); o != nil; o = it.Next() { + pdb := o.(*policyv1.PodDisruptionBudget) + m, err := metav1.LabelSelectorAsMap(pdb.Spec.Selector) + if err != nil { + continue + } + if cache.MatchLabels(labels, m) { + return pdb + } + } + return nil +} + +func (s *Pod) checkUtilization(ctx context.Context, fqn string, po *v1.Pod, cmx client.ContainerMetrics) { + if len(cmx) == 0 { + return + } + for _, co := range po.Spec.Containers { + cmx, ok := cmx[co.Name] + if !ok { + continue + } + NewContainer(fqn, s).checkUtilization(ctx, co, cmx) + } +} + +func (s *Pod) checkSecure(ctx context.Context, fqn string, spec v1.PodSpec) { + if err := s.checkSA(ctx, fqn, spec); err != nil { + s.AddErr(ctx, err) + } + s.checkSecContext(ctx, fqn, spec) +} + +func (s *Pod) checkSA(ctx context.Context, fqn string, spec v1.PodSpec) error { + ns, _ := namespaced(fqn) + if spec.ServiceAccountName == "default" { + s.AddCode(ctx, 300) + } + + txn := s.db.Txn(false) + defer txn.Abort() + saFQN := cache.FQN(ns, spec.ServiceAccountName) + o, err := txn.First(internal.Glossary[internal.SA].String(), "id", saFQN) + if err != nil || o == nil { + s.AddCode(ctx, 307, "Pod", spec.ServiceAccountName) + if isBoolSet(spec.AutomountServiceAccountToken) { + s.AddCode(ctx, 301) + } + return nil + } + sa, ok := o.(*v1.ServiceAccount) + if !ok { + return fmt.Errorf("expecting SA %q but got %T", saFQN, o) + } + if spec.AutomountServiceAccountToken == nil { + if isBoolSet(sa.AutomountServiceAccountToken) { + s.AddCode(ctx, 301) + } + } else if isBoolSet(spec.AutomountServiceAccountToken) { + s.AddCode(ctx, 301) + } + + return nil +} + +func (s *Pod) checkSecContext(ctx context.Context, fqn string, spec v1.PodSpec) { + if spec.SecurityContext == nil { + return + } + + // If pod security ctx is present and we have + podSec := hasPodNonRootUser(spec.SecurityContext) + var victims int + for _, co := range spec.InitContainers { + if !checkCOSecurityContext(co) && !podSec { + victims++ + s.AddSubCode(internal.WithGroup(ctx, types.NewGVR("containers"), co.Name), 306) + } + } + for _, co := range spec.Containers { + if !checkCOSecurityContext(co) && !podSec { + victims++ + s.AddSubCode(internal.WithGroup(ctx, types.NewGVR("containers"), co.Name), 306) + } + } + if victims > 0 && !podSec { + s.AddCode(ctx, 302) + } +} + +func checkCOSecurityContext(co v1.Container) bool { + return hasCoNonRootUser(co.SecurityContext) +} + +func hasPodNonRootUser(sec *v1.PodSecurityContext) bool { + if sec == nil { + return false + } + if sec.RunAsNonRoot != nil { + return *sec.RunAsNonRoot + } + if sec.RunAsUser != nil { + return *sec.RunAsUser != 0 + } + return false +} + +func hasCoNonRootUser(sec *v1.SecurityContext) bool { + if sec == nil { + return false + } + if sec.RunAsNonRoot != nil { + return *sec.RunAsNonRoot + } + if sec.RunAsUser != nil { + return *sec.RunAsUser != 0 + } + return false +} + +func (s *Pod) checkContainers(ctx context.Context, fqn string, po *v1.Pod) { + co := NewContainer(fqn, s) + for _, c := range po.Spec.InitContainers { + co.sanitize(ctx, c, false) + } + for _, c := range po.Spec.Containers { + co.sanitize(ctx, c, !isPartOfJob(po)) + } +} + +func (s *Pod) checkContainerStatus(ctx context.Context, fqn string, po *v1.Pod) { + limit := s.RestartsLimit() + size := len(po.Status.InitContainerStatuses) + for _, cs := range po.Status.InitContainerStatuses { + newContainerStatus(s, fqn, size, true, limit).sanitize(ctx, cs) + } + + size = len(po.Status.ContainerStatuses) + for _, cs := range po.Status.ContainerStatuses { + newContainerStatus(s, fqn, size, false, limit).sanitize(ctx, cs) + } +} + +func (s *Pod) checkStatus(ctx context.Context, po *v1.Pod) { + switch po.Status.Phase { + case v1.PodRunning: + case v1.PodSucceeded: + default: + s.AddCode(ctx, 207, po.Status.Phase) + } +} + +// !!BOZO!! Check +func (s *Pod) checkForMultiplePdbMatches(ctx context.Context, podNamespace string, podLabels map[string]string) { + matchedPdbs := make([]string, 0, 10) + txn, it := s.db.MustITFor(internal.Glossary[internal.PDB]) + defer txn.Abort() + for o := it.Next(); o != nil; o = it.Next() { + pdb := o.(*policyv1.PodDisruptionBudget) + if podNamespace != pdb.Namespace { + continue + } + selector, err := metav1.LabelSelectorAsSelector(pdb.Spec.Selector) + if err != nil { + log.Error().Err(err).Msg("No selectors found") + return + } + if selector.Empty() || !selector.Matches(labels.Set(podLabels)) { + continue + } + matchedPdbs = append(matchedPdbs, pdb.Name) + } + if len(matchedPdbs) > 1 { + sort.Strings(matchedPdbs) + s.AddCode(ctx, 209, strings.Join(matchedPdbs, ", ")) + } +} + +// ---------------------------------------------------------------------------- +// Helpers... + +func containerMetrics(pmx *mv1beta1.PodMetrics, mx client.ContainerMetrics) { + if pmx == nil { + return + } + + for _, co := range pmx.Containers { + mx[co.Name] = client.Metrics{ + CurrentCPU: *co.Usage.Cpu(), + CurrentMEM: *co.Usage.Memory(), + } + } +} + +func isPartOfJob(po *v1.Pod) bool { + for _, o := range po.OwnerReferences { + if o.Kind == "Job" { + return true + } + } + + return false +} + +func isBoolSet(b *bool) bool { + return b != nil && *b +} diff --git a/internal/lint/pod_test.go b/internal/lint/pod_test.go new file mode 100644 index 00000000..2d70a2e8 --- /dev/null +++ b/internal/lint/pod_test.go @@ -0,0 +1,239 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "context" + "testing" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/test" + "github.com/stretchr/testify/assert" + v1 "k8s.io/api/core/v1" + netv1 "k8s.io/api/networking/v1" + polv1 "k8s.io/api/policy/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" +) + +func TestPodNPLint(t *testing.T) { + dba, err := test.NewTestDB() + assert.NoError(t, err) + l := db.NewLoader(dba) + + ctx := test.MakeCtx(t) + assert.NoError(t, test.LoadDB[*v1.Pod](ctx, l.DB, "core/pod/3.yaml", internal.Glossary[internal.PO])) + assert.NoError(t, test.LoadDB[*v1.ServiceAccount](ctx, l.DB, "core/sa/2.yaml", internal.Glossary[internal.SA])) + assert.NoError(t, test.LoadDB[*v1.Namespace](ctx, l.DB, "core/ns/1.yaml", internal.Glossary[internal.NS])) + assert.NoError(t, test.LoadDB[*polv1.PodDisruptionBudget](ctx, l.DB, "pol/pdb/1.yaml", internal.Glossary[internal.PDB])) + assert.NoError(t, test.LoadDB[*netv1.NetworkPolicy](ctx, l.DB, "net/np/3.yaml", internal.Glossary[internal.NP])) + assert.NoError(t, test.LoadDB[*mv1beta1.PodMetrics](ctx, l.DB, "mx/pod/1.yaml", internal.Glossary[internal.PMX])) + + po := NewPod(test.MakeCollector(t), dba) + assert.Nil(t, po.Lint(test.MakeContext("v1/pods", "pods"))) + assert.Equal(t, 2, len(po.Outcome())) + + ii := po.Outcome()["ns1/p1"] + assert.Equal(t, 1, len(ii)) + assert.Equal(t, `[POP-1204] Pod Egress is not secured by a network policy`, ii[0].Message) + + ii = po.Outcome()["ns2/p2"] + assert.Equal(t, 0, len(ii)) +} + +func TestPodCheckSecure(t *testing.T) { + uu := map[string]struct { + pod v1.Pod + issues int + }{ + "cool_1": { + pod: makeSecPod(secNonRootSet, secNonRootSet, secNonRootSet, secNonRootSet), + issues: 1, + }, + "cool_2": { + pod: makeSecPod(secNonRootSet, secNonRootUnset, secNonRootUnset, secNonRootUnset), + issues: 1, + }, + "cool_3": { + pod: makeSecPod(secNonRootUnset, secNonRootSet, secNonRootSet, secNonRootSet), + issues: 1, + }, + "cool_4": { + pod: makeSecPod(secNonRootUndefined, secNonRootSet, secNonRootSet, secNonRootSet), + issues: 1, + }, + "cool_5": { + pod: makeSecPod(secNonRootSet, secNonRootUndefined, secNonRootUndefined, secNonRootUndefined), + issues: 1, + }, + "hacked_1": { + pod: makeSecPod(secNonRootUndefined, secNonRootUndefined, secNonRootUndefined, secNonRootUndefined), + issues: 5, + }, + "hacked_2": { + pod: makeSecPod(secNonRootUndefined, secNonRootUnset, secNonRootUndefined, secNonRootUndefined), + issues: 5, + }, + "hacked_3": { + pod: makeSecPod(secNonRootUndefined, secNonRootSet, secNonRootUndefined, secNonRootUndefined), + issues: 4, + }, + "hacked_4": { + pod: makeSecPod(secNonRootUndefined, secNonRootUnset, secNonRootSet, secNonRootUndefined), + issues: 4, + }, + "toast": { + pod: makeSecPod(secNonRootUndefined, secNonRootUndefined, secNonRootUndefined, secNonRootUndefined), + issues: 5, + }, + } + + ctx := test.MakeContext("v1/pods", "po") + ctx = internal.WithSpec(ctx, specFor("default/p1", nil)) + ctx = context.WithValue(ctx, internal.KeyConfig, test.MakeConfig(t)) + dba, err := test.NewTestDB() + assert.NoError(t, err) + l := db.NewLoader(dba) + + assert.NoError(t, test.LoadDB[*v1.Pod](ctx, l.DB, "core/pod/2.yaml", internal.Glossary[internal.PO])) + assert.NoError(t, test.LoadDB[*v1.ServiceAccount](ctx, l.DB, "core/sa/1.yaml", internal.Glossary[internal.SA])) + assert.NoError(t, test.LoadDB[*polv1.PodDisruptionBudget](ctx, l.DB, "pol/pdb/1.yaml", internal.Glossary[internal.PDB])) + assert.NoError(t, test.LoadDB[*mv1beta1.PodMetrics](ctx, l.DB, "mx/pod/1.yaml", internal.Glossary[internal.PMX])) + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + p := NewPod(test.MakeCollector(t), dba) + p.checkSecure(ctx, "default/p1", u.pod.Spec) + assert.Equal(t, u.issues, len(p.Outcome()["default/p1"])) + }) + } +} + +func TestPodLint(t *testing.T) { + dba, err := test.NewTestDB() + assert.NoError(t, err) + l := db.NewLoader(dba) + + ctx := test.MakeCtx(t) + assert.NoError(t, test.LoadDB[*v1.Pod](ctx, l.DB, "core/pod/2.yaml", internal.Glossary[internal.PO])) + assert.NoError(t, test.LoadDB[*v1.ServiceAccount](ctx, l.DB, "core/sa/1.yaml", internal.Glossary[internal.SA])) + assert.NoError(t, test.LoadDB[*polv1.PodDisruptionBudget](ctx, l.DB, "pol/pdb/1.yaml", internal.Glossary[internal.PDB])) + assert.NoError(t, test.LoadDB[*netv1.NetworkPolicy](ctx, l.DB, "net/np/1.yaml", internal.Glossary[internal.NP])) + assert.NoError(t, test.LoadDB[*mv1beta1.PodMetrics](ctx, l.DB, "mx/pod/1.yaml", internal.Glossary[internal.PMX])) + + po := NewPod(test.MakeCollector(t), dba) + po.Collector.Config.Registries = []string{"dorker.io"} + assert.Nil(t, po.Lint(test.MakeContext("v1/pods", "pods"))) + assert.Equal(t, 5, len(po.Outcome())) + + ii := po.Outcome()["default/p1"] + assert.Equal(t, 0, len(ii)) + + ii = po.Outcome()["default/p2"] + assert.Equal(t, 6, len(ii)) + assert.Equal(t, `[POP-207] Pod is in an unhappy phase ()`, ii[0].Message) + assert.Equal(t, `[POP-208] Unmanaged pod detected. Best to use a controller`, ii[1].Message) + assert.Equal(t, `[POP-1204] Pod Ingress is not secured by a network policy`, ii[2].Message) + assert.Equal(t, `[POP-1204] Pod Egress is not secured by a network policy`, ii[3].Message) + assert.Equal(t, `[POP-206] Pod has no associated PodDisruptionBudget`, ii[4].Message) + assert.Equal(t, `[POP-301] Connects to API Server? ServiceAccount token is mounted`, ii[5].Message) + + ii = po.Outcome()["default/p3"] + assert.Equal(t, 6, len(ii)) + assert.Equal(t, `[POP-105] Liveness uses a port#, prefer a named port`, ii[0].Message) + assert.Equal(t, `[POP-105] Readiness uses a port#, prefer a named port`, ii[1].Message) + assert.Equal(t, `[POP-1204] Pod Ingress is not secured by a network policy`, ii[2].Message) + assert.Equal(t, `[POP-1204] Pod Egress is not secured by a network policy`, ii[3].Message) + assert.Equal(t, `[POP-301] Connects to API Server? ServiceAccount token is mounted`, ii[4].Message) + assert.Equal(t, `[POP-109] CPU Current/Request (2000m/1000m) reached user 80% threshold (200%)`, ii[5].Message) + + ii = po.Outcome()["default/p4"] + assert.Equal(t, 15, len(ii)) + assert.Equal(t, `[POP-204] Pod is not ready [0/1]`, ii[0].Message) + assert.Equal(t, `[POP-204] Pod is not ready [0/2]`, ii[1].Message) + assert.Equal(t, `[POP-100] Untagged docker image in use`, ii[2].Message) + assert.Equal(t, `[POP-113] Container image "zorg" is not hosted on an allowed docker registry`, ii[3].Message) + assert.Equal(t, `[POP-106] No resources requests/limits defined`, ii[4].Message) + assert.Equal(t, `[POP-100] Untagged docker image in use`, ii[5].Message) + assert.Equal(t, `[POP-113] Container image "blee" is not hosted on an allowed docker registry`, ii[6].Message) + assert.Equal(t, `[POP-101] Image tagged "latest" in use`, ii[7].Message) + assert.Equal(t, `[POP-113] Container image "zorg:latest" is not hosted on an allowed docker registry`, ii[8].Message) + assert.Equal(t, `[POP-107] No resource limits defined`, ii[9].Message) + assert.Equal(t, `[POP-208] Unmanaged pod detected. Best to use a controller`, ii[10].Message) + assert.Equal(t, `[POP-1204] Pod Ingress is not secured by a network policy`, ii[11].Message) + assert.Equal(t, `[POP-1204] Pod Egress is not secured by a network policy`, ii[12].Message) + assert.Equal(t, `[POP-300] Uses "default" ServiceAccount`, ii[13].Message) + assert.Equal(t, `[POP-301] Connects to API Server? ServiceAccount token is mounted`, ii[14].Message) + + ii = po.Outcome()["default/p5"] + assert.Equal(t, 7, len(ii)) + assert.Equal(t, `[POP-113] Container image "blee:v1.2" is not hosted on an allowed docker registry`, ii[0].Message) + assert.Equal(t, `[POP-106] No resources requests/limits defined`, ii[1].Message) + assert.Equal(t, `[POP-102] No probes defined`, ii[2].Message) + assert.Equal(t, `[POP-1204] Pod Ingress is not secured by a network policy`, ii[3].Message) + assert.Equal(t, `[POP-1204] Pod Egress is not secured by a network policy`, ii[4].Message) + assert.Equal(t, `[POP-209] Pod is managed by multiple PodDisruptionBudgets (pdb4, pdb4-1)`, ii[5].Message) + assert.Equal(t, `[POP-301] Connects to API Server? ServiceAccount token is mounted`, ii[6].Message) +} + +// ---------------------------------------------------------------------------- +// Helpers... + +type nonRootUser int + +const ( + secNonRootUndefined nonRootUser = iota - 1 + secNonRootUnset = 0 + secNonRootSet = 1 +) + +func makeSecCO(name string, level nonRootUser) v1.Container { + t, f := true, false + var secCtx v1.SecurityContext + switch level { + case secNonRootUnset: + secCtx.RunAsNonRoot = &f + case secNonRootSet: + secCtx.RunAsNonRoot = &t + default: + secCtx.RunAsNonRoot = nil + } + + return v1.Container{Name: name, SecurityContext: &secCtx} +} + +func makeSecPod(pod, init, co1, co2 nonRootUser) v1.Pod { + t, f := true, false + var zero int64 + var secCtx v1.PodSecurityContext + switch pod { + case secNonRootUnset: + secCtx.RunAsNonRoot = &f + case secNonRootSet: + secCtx.RunAsNonRoot = &t + default: + secCtx.RunAsNonRoot = nil + secCtx.RunAsUser = &zero + } + + return v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "p1", + }, + Spec: v1.PodSpec{ + ServiceAccountName: "default", + AutomountServiceAccountToken: &f, + InitContainers: []v1.Container{ + makeSecCO("ic1", init), + }, + Containers: []v1.Container{ + makeSecCO("c1", co1), + makeSecCO("c2", co2), + }, + SecurityContext: &secCtx, + }, + } +} diff --git a/internal/lint/pv.go b/internal/lint/pv.go new file mode 100644 index 00000000..e55d642f --- /dev/null +++ b/internal/lint/pv.go @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "context" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/client" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/issues" + v1 "k8s.io/api/core/v1" +) + +type ( + // PersistentVolume represents a PersistentVolume linter. + PersistentVolume struct { + *issues.Collector + db *db.DB + } +) + +// NewPersistentVolume returns a new instance. +func NewPersistentVolume(co *issues.Collector, db *db.DB) *PersistentVolume { + return &PersistentVolume{ + Collector: co, + db: db, + } +} + +// Lint cleanse the resource. +func (s *PersistentVolume) Lint(ctx context.Context) error { + txn, it := s.db.MustITFor(internal.Glossary[internal.PV]) + defer txn.Abort() + for o := it.Next(); o != nil; o = it.Next() { + pv := o.(*v1.PersistentVolume) + fqn := client.FQN(pv.Namespace, pv.Name) + s.InitOutcome(fqn) + ctx = internal.WithSpec(ctx, specFor(fqn, pv)) + + s.checkBound(ctx, pv.Status.Phase) + } + + return nil +} + +func (s *PersistentVolume) checkBound(ctx context.Context, phase v1.PersistentVolumePhase) { + switch phase { + case v1.VolumeAvailable: + s.AddCode(ctx, 1000) + case v1.VolumePending: + s.AddCode(ctx, 1001) + case v1.VolumeFailed: + s.AddCode(ctx, 1002) + } +} diff --git a/internal/lint/pv_test.go b/internal/lint/pv_test.go new file mode 100644 index 00000000..269eee4f --- /dev/null +++ b/internal/lint/pv_test.go @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "testing" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/rules" + "github.com/derailed/popeye/internal/test" + "github.com/stretchr/testify/assert" + v1 "k8s.io/api/core/v1" +) + +func TestPVLint(t *testing.T) { + dba, err := test.NewTestDB() + assert.NoError(t, err) + l := db.NewLoader(dba) + + ctx := test.MakeCtx(t) + assert.NoError(t, test.LoadDB[*v1.PersistentVolume](ctx, l.DB, "core/pv/1.yaml", internal.Glossary[internal.PV])) + assert.NoError(t, test.LoadDB[*v1.Pod](ctx, l.DB, "core/pod/1.yaml", internal.Glossary[internal.PO])) + + pv := NewPersistentVolume(test.MakeCollector(t), dba) + assert.Nil(t, pv.Lint(test.MakeContext("v1/persistentvolumes", "persistentvolumes"))) + assert.Equal(t, 4, len(pv.Outcome())) + + ii := pv.Outcome()["default/pv1"] + assert.Equal(t, 0, len(ii)) + + ii = pv.Outcome()["default/pv2"] + assert.Equal(t, 1, len(ii)) + assert.Equal(t, `[POP-1002] Lost volume detected`, ii[0].Message) + assert.Equal(t, rules.ErrorLevel, ii[0].Level) + + ii = pv.Outcome()["default/pv3"] + assert.Equal(t, 1, len(ii)) + assert.Equal(t, `[POP-1000] Available volume detected`, ii[0].Message) + assert.Equal(t, rules.InfoLevel, ii[0].Level) + + ii = pv.Outcome()["default/pv4"] + assert.Equal(t, 1, len(ii)) + assert.Equal(t, `[POP-1001] Pending volume detected`, ii[0].Message) + assert.Equal(t, rules.WarnLevel, ii[0].Level) +} diff --git a/internal/lint/pvc.go b/internal/lint/pvc.go new file mode 100644 index 00000000..3b37c790 --- /dev/null +++ b/internal/lint/pvc.go @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "context" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/cache" + "github.com/derailed/popeye/internal/client" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/issues" + v1 "k8s.io/api/core/v1" +) + +type ( + // PersistentVolumeClaim represents a PersistentVolumeClaim linter. + PersistentVolumeClaim struct { + *issues.Collector + db *db.DB + } +) + +// NewPersistentVolumeClaim returns a new instance. +func NewPersistentVolumeClaim(co *issues.Collector, db *db.DB) *PersistentVolumeClaim { + return &PersistentVolumeClaim{ + Collector: co, + db: db, + } +} + +// Lint cleanse the resource. +func (s *PersistentVolumeClaim) Lint(ctx context.Context) error { + refs := make(map[string]struct{}) + txn, it := s.db.MustITFor(internal.Glossary[internal.PO]) + defer txn.Abort() + for o := it.Next(); o != nil; o = it.Next() { + pod := o.(*v1.Pod) + for _, v := range pod.Spec.Volumes { + if v.VolumeSource.PersistentVolumeClaim == nil { + continue + } + refs[cache.FQN(pod.Namespace, v.VolumeSource.PersistentVolumeClaim.ClaimName)] = struct{}{} + } + } + + txn, it = s.db.MustITFor(internal.Glossary[internal.PVC]) + defer txn.Abort() + for o := it.Next(); o != nil; o = it.Next() { + pvc := o.(*v1.PersistentVolumeClaim) + fqn := client.FQN(pvc.Namespace, pvc.Name) + s.InitOutcome(fqn) + ctx = internal.WithSpec(ctx, specFor(fqn, pvc)) + + s.checkBound(ctx, pvc.Status.Phase) + if _, ok := refs[fqn]; !ok { + s.AddCode(ctx, 400) + } + } + + return nil +} + +func (s *PersistentVolumeClaim) checkBound(ctx context.Context, phase v1.PersistentVolumeClaimPhase) { + switch phase { + case v1.ClaimPending: + s.AddCode(ctx, 1003) + case v1.ClaimLost: + s.AddCode(ctx, 1004) + } +} diff --git a/internal/lint/pvc_test.go b/internal/lint/pvc_test.go new file mode 100644 index 00000000..fd63677d --- /dev/null +++ b/internal/lint/pvc_test.go @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "testing" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/rules" + "github.com/derailed/popeye/internal/test" + "github.com/stretchr/testify/assert" + v1 "k8s.io/api/core/v1" +) + +func TestPVCLint(t *testing.T) { + dba, err := test.NewTestDB() + assert.NoError(t, err) + l := db.NewLoader(dba) + + ctx := test.MakeCtx(t) + assert.NoError(t, test.LoadDB[*v1.PersistentVolumeClaim](ctx, l.DB, "core/pvc/1.yaml", internal.Glossary[internal.PVC])) + assert.NoError(t, test.LoadDB[*v1.Pod](ctx, l.DB, "core/pod/1.yaml", internal.Glossary[internal.PO])) + + pvc := NewPersistentVolumeClaim(test.MakeCollector(t), dba) + assert.Nil(t, pvc.Lint(test.MakeContext("v1/persistentvolumeclaims", "persistentvolumeclaims"))) + assert.Equal(t, 3, len(pvc.Outcome())) + + ii := pvc.Outcome()["default/pvc1"] + assert.Equal(t, 0, len(ii)) + + ii = pvc.Outcome()["default/pvc2"] + assert.Equal(t, 2, len(ii)) + assert.Equal(t, `[POP-1004] Lost claim detected`, ii[0].Message) + assert.Equal(t, rules.ErrorLevel, ii[0].Level) + assert.Equal(t, `[POP-400] Used? Unable to locate resource reference`, ii[1].Message) + assert.Equal(t, rules.InfoLevel, ii[1].Level) + + ii = pvc.Outcome()["default/pvc3"] + assert.Equal(t, 2, len(ii)) + assert.Equal(t, `[POP-1003] Pending claim detected`, ii[0].Message) + assert.Equal(t, rules.ErrorLevel, ii[0].Level) + assert.Equal(t, `[POP-400] Used? Unable to locate resource reference`, ii[1].Message) + assert.Equal(t, rules.InfoLevel, ii[1].Level) +} diff --git a/internal/lint/rb.go b/internal/lint/rb.go new file mode 100644 index 00000000..ed09f40f --- /dev/null +++ b/internal/lint/rb.go @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "context" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/cache" + "github.com/derailed/popeye/internal/client" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/issues" + rbacv1 "k8s.io/api/rbac/v1" +) + +type ( + // RoleBinding tracks RoleBinding sanitization. + RoleBinding struct { + *issues.Collector + + db *db.DB + } +) + +// NewRoleBinding returns a new instance. +func NewRoleBinding(c *issues.Collector, db *db.DB) *RoleBinding { + return &RoleBinding{ + Collector: c, + db: db, + } +} + +// Lint cleanse the resource.. +func (r *RoleBinding) Lint(ctx context.Context) error { + r.checkInUse(ctx) + + return nil +} + +func (r *RoleBinding) checkInUse(ctx context.Context) { + txn, it := r.db.MustITFor(internal.Glossary[internal.ROB]) + defer txn.Abort() + for o := it.Next(); o != nil; o = it.Next() { + rb := o.(*rbacv1.RoleBinding) + fqn := client.FQN(rb.Namespace, rb.Name) + r.InitOutcome(fqn) + ctx = internal.WithSpec(ctx, specFor(fqn, rb)) + + switch rb.RoleRef.Kind { + case "ClusterRole": + if !r.db.Exists(internal.Glossary[internal.CR], rb.RoleRef.Name) { + r.AddCode(ctx, 1300, rb.RoleRef.Kind, rb.RoleRef.Name) + } + case "Role": + rFQN := cache.FQN(rb.Namespace, rb.RoleRef.Name) + if !r.db.Exists(internal.Glossary[internal.RO], rFQN) { + r.AddCode(ctx, 1300, rb.RoleRef.Kind, rFQN) + } + } + } +} diff --git a/internal/lint/rb_test.go b/internal/lint/rb_test.go new file mode 100644 index 00000000..086a405b --- /dev/null +++ b/internal/lint/rb_test.go @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "testing" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/rules" + "github.com/derailed/popeye/internal/test" + "github.com/stretchr/testify/assert" + v1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" +) + +func TestRBLint(t *testing.T) { + dba, err := test.NewTestDB() + assert.NoError(t, err) + l := db.NewLoader(dba) + + ctx := test.MakeCtx(t) + assert.NoError(t, test.LoadDB[*rbacv1.RoleBinding](ctx, l.DB, "auth/rob/1.yaml", internal.Glossary[internal.ROB])) + assert.NoError(t, test.LoadDB[*rbacv1.Role](ctx, l.DB, "auth/ro/1.yaml", internal.Glossary[internal.RO])) + assert.NoError(t, test.LoadDB[*rbacv1.ClusterRole](ctx, l.DB, "auth/cr/1.yaml", internal.Glossary[internal.CR])) + assert.NoError(t, test.LoadDB[*rbacv1.ClusterRoleBinding](ctx, l.DB, "auth/crb/1.yaml", internal.Glossary[internal.CRB])) + assert.NoError(t, test.LoadDB[*v1.ServiceAccount](ctx, l.DB, "core/sa/1.yaml", internal.Glossary[internal.SA])) + + rb := NewRoleBinding(test.MakeCollector(t), dba) + assert.Nil(t, rb.Lint(test.MakeContext("rbac.authorization.k8s.io/v1/rolebindings", "rolebindings"))) + assert.Equal(t, 3, len(rb.Outcome())) + + ii := rb.Outcome()["default/rb1"] + assert.Equal(t, 0, len(ii)) + + ii = rb.Outcome()["default/rb2"] + assert.Equal(t, 1, len(ii)) + assert.Equal(t, `[POP-1300] References a Role (default/r-bozo) which does not exist`, ii[0].Message) + assert.Equal(t, rules.WarnLevel, ii[0].Level) + + ii = rb.Outcome()["default/rb3"] + assert.Equal(t, 1, len(ii)) + assert.Equal(t, `[POP-1300] References a ClusterRole (cr-bozo) which does not exist`, ii[0].Message) + assert.Equal(t, rules.WarnLevel, ii[0].Level) +} diff --git a/internal/lint/ro.go b/internal/lint/ro.go new file mode 100644 index 00000000..ec604497 --- /dev/null +++ b/internal/lint/ro.go @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "context" + "sync" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/cache" + "github.com/derailed/popeye/internal/client" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/issues" + rbacv1 "k8s.io/api/rbac/v1" +) + +type ( + // Role tracks Role sanitization. + Role struct { + *issues.Collector + + db *db.DB + } +) + +// NewRole returns a new instance. +func NewRole(c *issues.Collector, db *db.DB) *Role { + return &Role{ + Collector: c, + db: db, + } +} + +// Lint cleanse the resource. +func (s *Role) Lint(ctx context.Context) error { + var roRefs sync.Map + crb := cache.NewClusterRoleBinding(s.db) + crb.ClusterRoleRefs(&roRefs) + rb := cache.NewRoleBinding(s.db) + rb.RoleRefs(&roRefs) + s.checkInUse(ctx, &roRefs) + + return nil +} + +func (s *Role) checkInUse(ctx context.Context, refs *sync.Map) { + txn, it := s.db.MustITFor(internal.Glossary[internal.RO]) + defer txn.Abort() + for o := it.Next(); o != nil; o = it.Next() { + ro := o.(*rbacv1.Role) + fqn := client.FQN(ro.Namespace, ro.Name) + s.InitOutcome(fqn) + ctx = internal.WithSpec(ctx, specFor(fqn, ro)) + + _, ok := refs.Load(cache.ResFqn(cache.RoleKey, fqn)) + if !ok { + s.AddCode(ctx, 400) + } + } +} diff --git a/internal/lint/ro_test.go b/internal/lint/ro_test.go new file mode 100644 index 00000000..4d36ce76 --- /dev/null +++ b/internal/lint/ro_test.go @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "testing" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/rules" + "github.com/derailed/popeye/internal/test" + "github.com/stretchr/testify/assert" + rbacv1 "k8s.io/api/rbac/v1" +) + +func TestROLint(t *testing.T) { + dba, err := test.NewTestDB() + assert.NoError(t, err) + l := db.NewLoader(dba) + + ctx := test.MakeCtx(t) + assert.NoError(t, test.LoadDB[*rbacv1.Role](ctx, l.DB, "auth/ro/1.yaml", internal.Glossary[internal.RO])) + assert.NoError(t, test.LoadDB[*rbacv1.RoleBinding](ctx, l.DB, "auth/rob/1.yaml", internal.Glossary[internal.ROB])) + assert.NoError(t, test.LoadDB[*rbacv1.ClusterRoleBinding](ctx, l.DB, "auth/crb/1.yaml", internal.Glossary[internal.CRB])) + + ro := NewRole(test.MakeCollector(t), dba) + assert.Nil(t, ro.Lint(test.MakeContext("rbac.authorization.k8s.io/v1/roles", "roles"))) + assert.Equal(t, 3, len(ro.Outcome())) + + ii := ro.Outcome()["default/r1"] + assert.Equal(t, 0, len(ii)) + + ii = ro.Outcome()["default/r2"] + assert.Equal(t, 1, len(ii)) + assert.Equal(t, `[POP-400] Used? Unable to locate resource reference`, ii[0].Message) + assert.Equal(t, rules.InfoLevel, ii[0].Level) + + ii = ro.Outcome()["default/r3"] + assert.Equal(t, 1, len(ii)) + assert.Equal(t, `[POP-400] Used? Unable to locate resource reference`, ii[0].Message) + assert.Equal(t, rules.InfoLevel, ii[0].Level) +} diff --git a/internal/lint/rs.go b/internal/lint/rs.go new file mode 100644 index 00000000..5dbfebc3 --- /dev/null +++ b/internal/lint/rs.go @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "context" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/client" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/issues" + appsv1 "k8s.io/api/apps/v1" +) + +// ReplicaSet tracks ReplicaSet sanitization. +type ReplicaSet struct { + *issues.Collector + + db *db.DB +} + +// NewReplicaSet returns a new instance. +func NewReplicaSet(co *issues.Collector, db *db.DB) *ReplicaSet { + return &ReplicaSet{ + Collector: co, + db: db, + } +} + +// Lint cleanse the resource. +func (s *ReplicaSet) Lint(ctx context.Context) error { + txn, it := s.db.MustITFor(internal.Glossary[internal.RS]) + defer txn.Abort() + for o := it.Next(); o != nil; o = it.Next() { + rs := o.(*appsv1.ReplicaSet) + fqn := client.FQN(rs.Namespace, rs.Name) + s.InitOutcome(fqn) + ctx = internal.WithSpec(ctx, specFor(fqn, rs)) + + s.checkHealth(ctx, rs) + } + + return nil +} + +func (s *ReplicaSet) checkHealth(ctx context.Context, rs *appsv1.ReplicaSet) { + if rs.Spec.Replicas != nil && *rs.Spec.Replicas != rs.Status.ReadyReplicas { + s.AddCode(ctx, 1120, *rs.Spec.Replicas, rs.Status.ReadyReplicas) + } +} diff --git a/internal/lint/rs_test.go b/internal/lint/rs_test.go new file mode 100644 index 00000000..75e4d62c --- /dev/null +++ b/internal/lint/rs_test.go @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "testing" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/rules" + "github.com/derailed/popeye/internal/test" + "github.com/stretchr/testify/assert" + appsv1 "k8s.io/api/apps/v1" + v1 "k8s.io/api/core/v1" +) + +func TestRSLint(t *testing.T) { + dba, err := test.NewTestDB() + assert.NoError(t, err) + l := db.NewLoader(dba) + + ctx := test.MakeCtx(t) + assert.NoError(t, test.LoadDB[*appsv1.ReplicaSet](ctx, l.DB, "apps/rs/1.yaml", internal.Glossary[internal.RS])) + assert.NoError(t, test.LoadDB[*v1.Pod](ctx, l.DB, "core/pod/1.yaml", internal.Glossary[internal.PO])) + + rs := NewReplicaSet(test.MakeCollector(t), dba) + assert.Nil(t, rs.Lint(test.MakeContext("apps/v1/replicasets", "replicasets"))) + assert.Equal(t, 2, len(rs.Outcome())) + + ii := rs.Outcome()["default/rs1"] + assert.Equal(t, 0, len(ii)) + + ii = rs.Outcome()["default/rs2"] + assert.Equal(t, 1, len(ii)) + assert.Equal(t, `[POP-1120] Unhealthy ReplicaSet 2 desired but have 0 ready`, ii[0].Message) + assert.Equal(t, rules.ErrorLevel, ii[0].Level) +} diff --git a/internal/lint/sa.go b/internal/lint/sa.go new file mode 100644 index 00000000..d44440e0 --- /dev/null +++ b/internal/lint/sa.go @@ -0,0 +1,160 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "context" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/cache" + "github.com/derailed/popeye/internal/client" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/issues" + + v1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" +) + +const defaultSA = "default" + +// ServiceAccount tracks ServiceAccount linter. +type ServiceAccount struct { + *issues.Collector + db *db.DB +} + +// NewServiceAccount returns a new instance. +func NewServiceAccount(co *issues.Collector, db *db.DB) *ServiceAccount { + return &ServiceAccount{ + Collector: co, + db: db, + } +} + +// Lint cleanse the resource. +func (s *ServiceAccount) Lint(ctx context.Context) error { + refs := make(map[string]struct{}, 20) + if err := s.crbRefs(refs); err != nil { + return err + } + if err := s.rbRefs(refs); err != nil { + return err + } + err := s.podRefs(refs) + if err != nil { + return err + } + + txn, it := s.db.MustITFor(internal.Glossary[internal.SA]) + defer txn.Abort() + for o := it.Next(); o != nil; o = it.Next() { + sa := o.(*v1.ServiceAccount) + fqn := client.FQN(sa.Namespace, sa.Name) + s.InitOutcome(fqn) + ctx = internal.WithSpec(ctx, specFor(fqn, sa)) + + s.checkMounts(ctx, sa.AutomountServiceAccountToken) + s.checkSecretRefs(ctx, fqn, sa.Secrets) + s.checkPullSecretRefs(ctx, fqn, sa.ImagePullSecrets) + if _, ok := refs[fqn]; !ok && sa.Name != defaultSA { + s.AddCode(ctx, 400) + } + } + + return nil +} + +func (s *ServiceAccount) checkSecretRefs(ctx context.Context, fqn string, refs []v1.ObjectReference) { + ns, _ := namespaced(fqn) + for _, ref := range refs { + if ref.Namespace != "" { + ns = ref.Namespace + } + sfqn := cache.FQN(ns, ref.Name) + if !s.db.Exists(internal.Glossary[internal.SEC], sfqn) { + s.AddCode(ctx, 304, sfqn) + } + } +} + +func (s *ServiceAccount) checkPullSecretRefs(ctx context.Context, fqn string, refs []v1.LocalObjectReference) { + ns, _ := namespaced(fqn) + for _, ref := range refs { + sfqn := cache.FQN(ns, ref.Name) + if !s.db.Exists(internal.Glossary[internal.SEC], sfqn) { + s.AddCode(ctx, 305, sfqn) + } + } +} + +func (s *ServiceAccount) checkMounts(ctx context.Context, b *bool) { + if b != nil && *b { + s.AddCode(ctx, 303) + } +} + +func (s *ServiceAccount) crbRefs(refs map[string]struct{}) error { + txn := s.db.Txn(false) + defer txn.Abort() + it, err := txn.Get(internal.Glossary[internal.CRB].String(), "id") + if err != nil { + return err + } + for o := it.Next(); o != nil; o = it.Next() { + crb := o.(*rbacv1.ClusterRoleBinding) + pullSas(crb.Subjects, refs) + } + + return nil +} + +func (s *ServiceAccount) rbRefs(refs map[string]struct{}) error { + txn := s.db.Txn(false) + defer txn.Abort() + it, err := txn.Get(internal.Glossary[internal.ROB].String(), "id") + if err != nil { + return err + } + for o := it.Next(); o != nil; o = it.Next() { + rb := o.(*rbacv1.RoleBinding) + pullSas(rb.Subjects, refs) + } + + return nil +} + +func (s *ServiceAccount) podRefs(refs map[string]struct{}) error { + txn := s.db.Txn(false) + defer txn.Abort() + it, err := txn.Get(internal.Glossary[internal.PO].String(), "id") + if err != nil { + return err + } + for o := it.Next(); o != nil; o = it.Next() { + p := o.(*v1.Pod) + if p.Spec.ServiceAccountName != "" { + refs[cache.FQN(p.Namespace, p.Spec.ServiceAccountName)] = struct{}{} + } + } + + return nil +} + +// ---------------------------------------------------------------------------- +// Helpers... + +func pullSas(ss []rbacv1.Subject, res map[string]struct{}) { + for _, s := range ss { + if s.Kind == "ServiceAccount" { + fqn := fqnSubject(s) + if _, ok := res[fqn]; !ok { + res[fqn] = struct{}{} + } + } + } +} + +func fqnSubject(s rbacv1.Subject) string { + return cache.FQN(s.Namespace, s.Name) +} diff --git a/internal/lint/sa_test.go b/internal/lint/sa_test.go new file mode 100644 index 00000000..2237f21c --- /dev/null +++ b/internal/lint/sa_test.go @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "testing" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/rules" + "github.com/derailed/popeye/internal/test" + "github.com/stretchr/testify/assert" + v1 "k8s.io/api/core/v1" + netv1 "k8s.io/api/networking/v1" + rbacv1 "k8s.io/api/rbac/v1" +) + +func TestSALint(t *testing.T) { + dba, err := test.NewTestDB() + assert.NoError(t, err) + l := db.NewLoader(dba) + + ctx := test.MakeCtx(t) + assert.NoError(t, test.LoadDB[*v1.ServiceAccount](ctx, l.DB, "core/sa/1.yaml", internal.Glossary[internal.SA])) + assert.NoError(t, test.LoadDB[*v1.Pod](ctx, l.DB, "core/pod/2.yaml", internal.Glossary[internal.PO])) + assert.NoError(t, test.LoadDB[*rbacv1.RoleBinding](ctx, l.DB, "auth/rob/1.yaml", internal.Glossary[internal.ROB])) + assert.NoError(t, test.LoadDB[*rbacv1.ClusterRoleBinding](ctx, l.DB, "auth/crb/1.yaml", internal.Glossary[internal.CRB])) + assert.NoError(t, test.LoadDB[*v1.Secret](ctx, l.DB, "core/secret/1.yaml", internal.Glossary[internal.SEC])) + assert.NoError(t, test.LoadDB[*v1.Service](ctx, l.DB, "core/svc/1.yaml", internal.Glossary[internal.SVC])) + assert.NoError(t, test.LoadDB[*netv1.Ingress](ctx, l.DB, "net/ingress/1.yaml", internal.Glossary[internal.ING])) + + sa := NewServiceAccount(test.MakeCollector(t), dba) + assert.Nil(t, sa.Lint(test.MakeContext("v1/serviceaccounts", "serviceaccounts"))) + assert.Equal(t, 6, len(sa.Outcome())) + + ii := sa.Outcome()["default/default"] + assert.Equal(t, 0, len(ii)) + + ii = sa.Outcome()["default/sa1"] + assert.Equal(t, 0, len(ii)) + + ii = sa.Outcome()["default/sa2"] + assert.Equal(t, 1, len(ii)) + assert.Equal(t, `[POP-303] Do you mean it? ServiceAccount is automounting APIServer credentials`, ii[0].Message) + assert.Equal(t, rules.WarnLevel, ii[0].Level) + + ii = sa.Outcome()["default/sa3"] + assert.Equal(t, 1, len(ii)) + assert.Equal(t, `[POP-303] Do you mean it? ServiceAccount is automounting APIServer credentials`, ii[0].Message) + assert.Equal(t, rules.WarnLevel, ii[0].Level) + + ii = sa.Outcome()["default/sa4"] + assert.Equal(t, 3, len(ii)) + assert.Equal(t, `[POP-304] References a secret "default/bozo" which does not exist`, ii[0].Message) + assert.Equal(t, rules.ErrorLevel, ii[0].Level) + assert.Equal(t, `[POP-305] References a pull secret which does not exist: default/s1`, ii[1].Message) + assert.Equal(t, rules.ErrorLevel, ii[1].Level) + assert.Equal(t, `[POP-400] Used? Unable to locate resource reference`, ii[2].Message) + assert.Equal(t, rules.InfoLevel, ii[2].Level) + + ii = sa.Outcome()["default/sa5"] + assert.Equal(t, 2, len(ii)) + assert.Equal(t, `[POP-304] References a secret "default/s1" which does not exist`, ii[0].Message) + assert.Equal(t, rules.ErrorLevel, ii[0].Level) + assert.Equal(t, `[POP-305] References a pull secret which does not exist: default/bozo`, ii[1].Message) + assert.Equal(t, rules.ErrorLevel, ii[1].Level) + +} diff --git a/internal/lint/sec.go b/internal/lint/sec.go new file mode 100644 index 00000000..24f86103 --- /dev/null +++ b/internal/lint/sec.go @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "context" + "sync" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/cache" + "github.com/derailed/popeye/internal/client" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/issues" + v1 "k8s.io/api/core/v1" +) + +// Secret tracks Secret sanitization. +type Secret struct { + *issues.Collector + + db *db.DB + system excludedFQN +} + +// NewSecret returns a new instance. +func NewSecret(co *issues.Collector, db *db.DB) *Secret { + return &Secret{ + Collector: co, + db: db, + system: excludedFQN{ + "rx:default-token": {}, + "rx:^kube-.*/.*-token-": {}, + "rx:^local-path-storage/.*token-": {}, + }, + } +} + +// Lint cleanse the resource. +func (s *Secret) Lint(ctx context.Context) error { + var refs sync.Map + + if err := cache.NewPod(s.db).PodRefs(&refs); err != nil { + s.AddErr(ctx, err) + } + if err := cache.NewServiceAccount(s.db).ServiceAccountRefs(&refs); err != nil { + s.AddErr(ctx, err) + } + if err := cache.NewIngress(s.db).IngressRefs(&refs); err != nil { + s.AddErr(ctx, err) + } + s.checkStale(ctx, &refs) + + return nil +} + +func (s *Secret) checkStale(ctx context.Context, refs *sync.Map) { + txn, it := s.db.MustITFor(internal.Glossary[internal.SEC]) + defer txn.Abort() + for o := it.Next(); o != nil; o = it.Next() { + sec := o.(*v1.Secret) + fqn := client.FQN(sec.Namespace, sec.Name) + s.InitOutcome(fqn) + ctx = internal.WithSpec(ctx, specFor(fqn, sec)) + + if s.system.skip(fqn) { + continue + } + refs.Range(func(k, v interface{}) bool { + return true + }) + + keys, ok := refs.Load(cache.ResFqn(cache.SecretKey, fqn)) + if !ok { + s.AddCode(ctx, 400) + continue + } + if keys.(internal.StringSet).Has(internal.All) { + continue + } + + kk := make(internal.StringSet, len(sec.Data)) + for k := range sec.Data { + kk.Add(k) + } + deltas := keys.(internal.StringSet).Diff(kk) + for k := range deltas { + s.AddCode(ctx, 401, k) + } + } +} diff --git a/internal/lint/sec_test.go b/internal/lint/sec_test.go new file mode 100644 index 00000000..9c0556c9 --- /dev/null +++ b/internal/lint/sec_test.go @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "testing" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/rules" + "github.com/derailed/popeye/internal/test" + "github.com/stretchr/testify/assert" + v1 "k8s.io/api/core/v1" + netv1 "k8s.io/api/networking/v1" +) + +func TestSecretLint(t *testing.T) { + dba, err := test.NewTestDB() + assert.NoError(t, err) + l := db.NewLoader(dba) + + ctx := test.MakeCtx(t) + assert.NoError(t, test.LoadDB[*v1.Secret](ctx, l.DB, "core/secret/1.yaml", internal.Glossary[internal.SEC])) + assert.NoError(t, test.LoadDB[*v1.Pod](ctx, l.DB, "core/pod/1.yaml", internal.Glossary[internal.PO])) + assert.NoError(t, test.LoadDB[*v1.ServiceAccount](ctx, l.DB, "core/sa/1.yaml", internal.Glossary[internal.SA])) + assert.NoError(t, test.LoadDB[*netv1.Ingress](ctx, l.DB, "net/ingress/1.yaml", internal.Glossary[internal.ING])) + + sec := NewSecret(test.MakeCollector(t), dba) + assert.Nil(t, sec.Lint(test.MakeContext("v1/secrets", "secrets"))) + assert.Equal(t, 3, len(sec.Outcome())) + + ii := sec.Outcome()["default/sec1"] + assert.Equal(t, 1, len(ii)) + assert.Equal(t, `[POP-401] Key "ns" used? Unable to locate key reference`, ii[0].Message) + assert.Equal(t, rules.InfoLevel, ii[0].Level) + + ii = sec.Outcome()["default/sec2"] + assert.Equal(t, 1, len(ii)) + assert.Equal(t, `[POP-400] Used? Unable to locate resource reference`, ii[0].Message) + assert.Equal(t, rules.InfoLevel, ii[0].Level) + + ii = sec.Outcome()["default/sec3"] + assert.Equal(t, 1, len(ii)) + assert.Equal(t, `[POP-400] Used? Unable to locate resource reference`, ii[0].Message) + assert.Equal(t, rules.InfoLevel, ii[0].Level) + +} diff --git a/internal/sanitize/sts.go b/internal/lint/sts.go similarity index 51% rename from internal/sanitize/sts.go rename to internal/lint/sts.go index 85f53398..0dcfb413 100644 --- a/internal/sanitize/sts.go +++ b/internal/lint/sts.go @@ -1,16 +1,16 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright Authors of Popeye -package sanitize +package lint import ( "context" "github.com/derailed/popeye/internal" "github.com/derailed/popeye/internal/client" + "github.com/derailed/popeye/internal/db" "github.com/derailed/popeye/internal/issues" appsv1 "k8s.io/api/apps/v1" - v1 "k8s.io/api/core/v1" ) type ( @@ -23,70 +23,41 @@ type ( ConfigLister } - // StatefulSetLister handles statefulsets. - StatefulSetLister interface { - PodLimiter - ConfigLister - PodSelectorLister - PodsMetricsLister - - ListStatefulSets() map[string]*appsv1.StatefulSet - ListServiceAccounts() map[string]*v1.ServiceAccount - } - - // StatefulSet represents a StatefulSet sanitizer. + // StatefulSet represents a StatefulSet linter. StatefulSet struct { *issues.Collector - StatefulSetLister + + db *db.DB } ) -// NewStatefulSet returns a new sanitizer. -func NewStatefulSet(co *issues.Collector, lister StatefulSetLister) *StatefulSet { +// NewStatefulSet returns a new instance. +func NewStatefulSet(co *issues.Collector, db *db.DB) *StatefulSet { return &StatefulSet{ - Collector: co, - StatefulSetLister: lister, + Collector: co, + db: db, } } -// Sanitize cleanse the resource. -func (s *StatefulSet) Sanitize(ctx context.Context) error { - pmx := client.PodsMetrics{} - podsMetrics(s, pmx) - +// Lint cleanse the resource. +func (s *StatefulSet) Lint(ctx context.Context) error { over := pullOverAllocs(ctx) - for fqn, st := range s.ListStatefulSets() { + txn, it := s.db.MustITFor(internal.Glossary[internal.STS]) + defer txn.Abort() + for o := it.Next(); o != nil; o = it.Next() { + sts := o.(*appsv1.StatefulSet) + fqn := client.FQN(sts.Namespace, sts.Name) s.InitOutcome(fqn) - ctx = internal.WithFQN(ctx, fqn) - - s.checkDeprecation(ctx, st) - s.checkStatefulSet(ctx, st) - s.checkContainers(ctx, st) - s.checkUtilization(ctx, over, st, pmx) + ctx = internal.WithSpec(ctx, specFor(fqn, sts)) - if s.NoConcerns(fqn) && s.Config.ExcludeFQN(internal.MustExtractSectionGVR(ctx), fqn) { - s.ClearOutcome(fqn) - } + s.checkStatefulSet(ctx, sts) + s.checkContainers(ctx, fqn, sts) + s.checkUtilization(ctx, over, sts) } return nil } -func (s *StatefulSet) checkDeprecation(ctx context.Context, st *appsv1.StatefulSet) { - const current = "apps/v1" - - rev, err := resourceRev(internal.MustExtractFQN(ctx), "StatefulSet", st.Annotations) - if err != nil { - if rev = revFromLink(st.SelfLink); rev == "" { - return - } - } - - if rev != current { - s.AddCode(ctx, 403, "StatefulSet", rev, current) - } -} - func (s *StatefulSet) checkStatefulSet(ctx context.Context, sts *appsv1.StatefulSet) { if sts.Spec.Replicas == nil || (sts.Spec.Replicas != nil && *sts.Spec.Replicas == 0) { s.AddCode(ctx, 500) @@ -101,16 +72,16 @@ func (s *StatefulSet) checkStatefulSet(ctx context.Context, sts *appsv1.Stateful return } - if _, ok := s.ListServiceAccounts()[client.FQN(sts.Namespace, sts.Spec.Template.Spec.ServiceAccountName)]; !ok { + saFQN := client.FQN(sts.Namespace, sts.Spec.Template.Spec.ServiceAccountName) + if !s.db.Exists(internal.Glossary[internal.SA], saFQN) { s.AddCode(ctx, 507, sts.Spec.Template.Spec.ServiceAccountName) } - } -func (s *StatefulSet) checkContainers(ctx context.Context, st *appsv1.StatefulSet) { +func (s *StatefulSet) checkContainers(ctx context.Context, fqn string, st *appsv1.StatefulSet) { spec := st.Spec.Template.Spec - l := NewContainer(internal.MustExtractFQN(ctx), s) + l := NewContainer(fqn, s) for _, co := range spec.InitContainers { l.sanitize(ctx, co, false) } @@ -144,8 +115,8 @@ func checkMEM(ctx context.Context, c CollectorLimiter, over bool, mx Consumption } } -func (s *StatefulSet) checkUtilization(ctx context.Context, over bool, st *appsv1.StatefulSet, pmx client.PodsMetrics) { - mx := s.statefulsetUsage(st, pmx) +func (s *StatefulSet) checkUtilization(ctx context.Context, over bool, sts *appsv1.StatefulSet) { + mx := resourceUsage(ctx, s.db, s, sts.Namespace, sts.Spec.Selector) if mx.RequestCPU.IsZero() && mx.RequestMEM.IsZero() { return } @@ -153,24 +124,3 @@ func (s *StatefulSet) checkUtilization(ctx context.Context, over bool, st *appsv checkCPU(ctx, s, over, mx) checkMEM(ctx, s, over, mx) } - -func (s *StatefulSet) statefulsetUsage(st *appsv1.StatefulSet, pmx client.PodsMetrics) ConsumptionMetrics { - var mx ConsumptionMetrics - for pfqn, pod := range s.ListPodsBySelector(st.Namespace, st.Spec.Selector) { - cpu, mem := computePodResources(pod.Spec) - mx.QOS = pod.Status.QOSClass - mx.RequestCPU.Add(cpu) - mx.RequestMEM.Add(mem) - - ccx, ok := pmx[pfqn] - if !ok { - continue - } - for _, cx := range ccx { - mx.CurrentCPU.Add(cx.CurrentCPU) - mx.CurrentMEM.Add(cx.CurrentMEM) - } - } - - return mx -} diff --git a/internal/lint/sts_test.go b/internal/lint/sts_test.go new file mode 100644 index 00000000..0d76d6fe --- /dev/null +++ b/internal/lint/sts_test.go @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "testing" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/rules" + "github.com/derailed/popeye/internal/test" + "github.com/stretchr/testify/assert" + appsv1 "k8s.io/api/apps/v1" + v1 "k8s.io/api/core/v1" + mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" +) + +func TestSTSLint(t *testing.T) { + dba, err := test.NewTestDB() + assert.NoError(t, err) + l := db.NewLoader(dba) + + ctx := test.MakeCtx(t) + assert.NoError(t, test.LoadDB[*appsv1.StatefulSet](ctx, l.DB, "apps/sts/1.yaml", internal.Glossary[internal.STS])) + assert.NoError(t, test.LoadDB[*v1.ServiceAccount](ctx, l.DB, "core/sa/1.yaml", internal.Glossary[internal.SA])) + assert.NoError(t, test.LoadDB[*v1.Pod](ctx, l.DB, "core/pod/1.yaml", internal.Glossary[internal.PO])) + assert.NoError(t, test.LoadDB[*mv1beta1.PodMetrics](ctx, l.DB, "mx/pod/1.yaml", internal.Glossary[internal.PMX])) + + sts := NewStatefulSet(test.MakeCollector(t), dba) + assert.Nil(t, sts.Lint(test.MakeContext("apps/v1/statefulsets", "statefulsets"))) + assert.Equal(t, 3, len(sts.Outcome())) + + ii := sts.Outcome()["default/sts1"] + assert.Equal(t, 2, len(ii)) + assert.Equal(t, `[POP-503] At current load, CPU under allocated. Current:20000m vs Requested:1000m (2000.00%)`, ii[0].Message) + assert.Equal(t, `[POP-505] At current load, Memory under allocated. Current:20Mi vs Requested:1Mi (2000.00%)`, ii[1].Message) + + ii = sts.Outcome()["default/sts2"] + assert.Equal(t, 2, len(ii)) + assert.Equal(t, `[POP-100] Untagged docker image in use`, ii[0].Message) + assert.Equal(t, rules.ErrorLevel, ii[0].Level) + assert.Equal(t, `[POP-508] No pods match controller selector: app=p2`, ii[1].Message) + assert.Equal(t, rules.ErrorLevel, ii[1].Level) + + ii = sts.Outcome()["default/sts3"] + assert.Equal(t, 4, len(ii)) + assert.Equal(t, `[POP-501] Unhealthy 1 desired but have 0 available`, ii[0].Message) + assert.Equal(t, rules.ErrorLevel, ii[0].Level) + assert.Equal(t, `[POP-507] Deployment references ServiceAccount "sa-bozo" which does not exist`, ii[1].Message) + assert.Equal(t, rules.ErrorLevel, ii[1].Level) + assert.Equal(t, `[POP-106] No resources requests/limits defined`, ii[2].Message) + assert.Equal(t, rules.WarnLevel, ii[2].Level) + assert.Equal(t, `[POP-508] No pods match controller selector: app=p3`, ii[3].Message) + assert.Equal(t, rules.ErrorLevel, ii[3].Level) +} diff --git a/internal/sanitize/svc.go b/internal/lint/svc.go similarity index 68% rename from internal/sanitize/svc.go rename to internal/lint/svc.go index 8e9bcdbe..1712b8c3 100644 --- a/internal/sanitize/svc.go +++ b/internal/lint/svc.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright Authors of Popeye -package sanitize +package lint import ( "context" @@ -9,66 +9,51 @@ import ( "github.com/derailed/popeye/internal" "github.com/derailed/popeye/internal/cache" + "github.com/derailed/popeye/internal/client" + "github.com/derailed/popeye/internal/db" "github.com/derailed/popeye/internal/issues" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/util/intstr" ) -type ( - // ServiceLister list available Services on a cluster. - ServiceLister interface { - PodGetter - EndPointLister - ListServices() map[string]*v1.Service - } - - // PodGetter find a single pod matching service selector. - PodGetter interface { - GetPod(ns string, sel map[string]string) *v1.Pod - } - - // EndPointLister find all service endpoints. - EndPointLister interface { - GetEndpoints(string) *v1.Endpoints - } - - // Service represents a service sanitizer. - Service struct { - *issues.Collector - ServiceLister - } -) +// Service represents a service linter. +type Service struct { + *issues.Collector + db *db.DB +} -// NewService returns a new sanitizer. -func NewService(co *issues.Collector, lister ServiceLister) *Service { +// NewService returns a new instance. +func NewService(co *issues.Collector, db *db.DB) *Service { return &Service{ - Collector: co, - ServiceLister: lister, + Collector: co, + db: db, } } -// Sanitize cleanse the resource. -func (s *Service) Sanitize(ctx context.Context) error { - for fqn, svc := range s.ListServices() { +// Lint cleanse the resource. +func (s *Service) Lint(ctx context.Context) error { + txn, it := s.db.MustITFor(internal.Glossary[internal.SVC]) + defer txn.Abort() + for o := it.Next(); o != nil; o = it.Next() { + svc := o.(*v1.Service) + fqn := client.FQN(svc.Namespace, svc.Name) s.InitOutcome(fqn) - ctx = internal.WithFQN(ctx, fqn) + ctx = internal.WithSpec(ctx, specFor(fqn, svc)) - s.checkPorts(ctx, svc.Namespace, svc.Spec.Selector, svc.Spec.Ports) - s.checkEndpoints(ctx, svc.Spec.Selector, svc.Spec.Type) + if len(svc.Spec.Selector) > 0 { + s.checkPorts(ctx, svc.Namespace, svc.Spec.Selector, svc.Spec.Ports) + s.checkEndpoints(ctx, fqn, svc.Spec.Type) + } s.checkType(ctx, svc.Spec.Type) s.checkExternalTrafficPolicy(ctx, svc.Spec.Type, svc.Spec.ExternalTrafficPolicy) - - if s.NoConcerns(fqn) && s.Config.ExcludeFQN(internal.MustExtractSectionGVR(ctx), fqn) { - s.ClearOutcome(fqn) - } } return nil } func (s *Service) checkPorts(ctx context.Context, ns string, sel map[string]string, ports []v1.ServicePort) { - po := s.GetPod(ns, sel) - if po == nil { + po, err := s.db.FindPod(ns, sel) + if err != nil || po == nil { if len(sel) > 0 { s.AddCode(ctx, 1100) } @@ -114,20 +99,22 @@ func (s *Service) checkExternalTrafficPolicy(ctx context.Context, kind v1.Servic } // CheckEndpoints runs a sanity check on this service endpoints. -func (s *Service) checkEndpoints(ctx context.Context, sel map[string]string, kind v1.ServiceType) { - // Service may not have selectors. - if len(sel) == 0 { - return - } +func (s *Service) checkEndpoints(ctx context.Context, fqn string, kind v1.ServiceType) { // External service bail -> no EPs. if kind == v1.ServiceTypeExternalName { return } - ep := s.GetEndpoints(internal.MustExtractFQN(ctx)) - if ep == nil || len(ep.Subsets) == 0 { + + o, err := s.db.Find(internal.Glossary[internal.EP], fqn) + if err != nil || o == nil { s.AddCode(ctx, 1105) return } + ep := o.(*v1.Endpoints) + if len(ep.Subsets) == 0 { + s.AddCode(ctx, 1110) + return + } numEndpointAddresses := 0 for _, s := range ep.Subsets { numEndpointAddresses += len(s.Addresses) diff --git a/internal/lint/svc_test.go b/internal/lint/svc_test.go new file mode 100644 index 00000000..76e66cdb --- /dev/null +++ b/internal/lint/svc_test.go @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "context" + "testing" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/issues" + "github.com/derailed/popeye/internal/rules" + "github.com/derailed/popeye/internal/test" + "github.com/stretchr/testify/assert" + v1 "k8s.io/api/core/v1" +) + +func TestSVCLint(t *testing.T) { + dba, err := test.NewTestDB() + assert.NoError(t, err) + l := db.NewLoader(dba) + + ctx := test.MakeCtx(t) + assert.NoError(t, test.LoadDB[*v1.Service](ctx, l.DB, "core/svc/1.yaml", internal.Glossary[internal.SVC])) + assert.NoError(t, test.LoadDB[*v1.Pod](ctx, l.DB, "core/pod/1.yaml", internal.Glossary[internal.PO])) + assert.NoError(t, test.LoadDB[*v1.Endpoints](ctx, l.DB, "core/ep/1.yaml", internal.Glossary[internal.EP])) + + svc := NewService(test.MakeCollector(t), dba) + assert.Nil(t, svc.Lint(test.MakeContext("v1/pods", "pods"))) + assert.Equal(t, 5, len(svc.Outcome())) + + ii := svc.Outcome()["default/p1"] + assert.Equal(t, 0, len(ii)) + +} + +func Test_svcCheckEndpoints(t *testing.T) { + uu := map[string]struct { + kind v1.ServiceType + fqn, key string + issues issues.Issues + }{ + "empty": { + issues: issues.Issues{ + { + Group: "__root__", + GVR: "v1/services", + Level: rules.ErrorLevel, + Message: "[POP-1105] No associated endpoints", + }, + }, + }, + "external": { + kind: v1.ServiceTypeExternalName, + }, + "no-ep": { + kind: v1.ServiceTypeNodePort, + fqn: "default/svc3", + issues: issues.Issues{ + { + Group: "__root__", + GVR: "v1/services", + Level: rules.ErrorLevel, + Message: "[POP-1105] No associated endpoints", + }, + }, + }, + "nodeport": { + kind: v1.ServiceTypeNodePort, + fqn: "default/svc2", + issues: issues.Issues{ + { + Group: "__root__", + GVR: "v1/services", + Level: rules.WarnLevel, + Message: "[POP-1109] Only one Pod associated with this endpoint", + }, + }, + }, + "no-subset": { + kind: v1.ServiceTypeNodePort, + fqn: "default/svc4", + issues: issues.Issues{ + { + Group: "__root__", + GVR: "v1/services", + Level: rules.WarnLevel, + Message: "[POP-1110] Match EP has no subsets", + }, + }, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + dba, err := test.NewTestDB() + assert.NoError(t, err) + l := db.NewLoader(dba) + ctx := test.MakeContext("v1/services", "services") + ctx = context.WithValue(ctx, internal.KeyConfig, test.MakeConfig(t)) + + assert.NoError(t, test.LoadDB[*v1.Endpoints](ctx, l.DB, "core/ep/1.yaml", internal.Glossary[internal.EP])) + + s := NewService(test.MakeCollector(t), dba) + if u.fqn != "" { + ctx = internal.WithSpec(ctx, specFor(u.fqn, nil)) + } + s.checkEndpoints(ctx, u.fqn, u.kind) + + assert.Equal(t, u.issues, s.Outcome()[u.fqn]) + }) + } +} diff --git a/internal/lint/testdata/apps/dp/1.yaml b/internal/lint/testdata/apps/dp/1.yaml new file mode 100644 index 00000000..61e29645 --- /dev/null +++ b/internal/lint/testdata/apps/dp/1.yaml @@ -0,0 +1,198 @@ +apiVersion: v1 +kind: List +items: +- apiVersion: apps/v1 + kind: Deployment + metadata: + name: dp1 + namespace: default + spec: + progressDeadlineSeconds: 600 + replicas: 1 + revisionHistoryLimit: 10 + selector: + matchLabels: + app: p1 + strategy: + rollingUpdate: + maxSurge: 25% + maxUnavailable: 25% + type: RollingUpdate + template: + metadata: + labels: + app: p1 + spec: + initContainers: + - name: ic1 + image: fred:v1.0.0 + resources: + limits: + cpu: 200m + memory: 30Mi + requests: + cpu: 100m + memory: 10Mi + containers: + - name: c1 + image: fred:v1.0.0 + imagePullPolicy: Always + ports: + - containerPort: 4000 + name: http + protocol: TCP + resources: + limits: + cpu: 200m + memory: 30Mi + requests: + cpu: 100m + memory: 10Mi + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + volumeMounts: + - mountPath: /data + name: om + dnsPolicy: ClusterFirst + restartPolicy: Always + schedulerName: default-scheduler + securityContext: {} + terminationGracePeriodSeconds: 30 + volumes: + - emptyDir: {} + name: om + status: + availableReplicas: 1 + conditions: + - lastTransitionTime: "2024-01-18T19:49:21Z" + lastUpdateTime: "2024-01-18T19:49:21Z" + message: Deployment has minimum availability. + reason: MinimumReplicasAvailable + status: "True" + type: Available + - lastTransitionTime: "2024-01-03T20:38:15Z" + lastUpdateTime: "2024-01-18T19:51:27Z" + message: ReplicaSet "dashb-7c46847b9" has successfully progressed. + reason: NewReplicaSetAvailable + status: "True" + type: Progressing + observedGeneration: 9 + readyReplicas: 1 + replicas: 1 + updatedReplicas: 1 +- apiVersion: apps/v1 + kind: Deployment + metadata: + name: dp2 + namespace: default + spec: + progressDeadlineSeconds: 600 + replicas: 1 + revisionHistoryLimit: 10 + selector: + matchLabels: + app: pod-bozo + strategy: + rollingUpdate: + maxSurge: 25% + maxUnavailable: 25% + type: RollingUpdate + template: + metadata: + spec: + automountServiceAccountToken: true + containers: + - image: blee:0.1.0 + imagePullPolicy: IfNotPresent + livenessProbe: + failureThreshold: 10 + httpGet: + path: /api/health + port: 3000 + scheme: HTTP + initialDelaySeconds: 60 + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 30 + name: grafana + ports: + - containerPort: 3000 + protocol: TCP + readinessProbe: + failureThreshold: 3 + httpGet: + path: /api/health + port: 3000 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 + resources: {} + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + seccompProfile: + type: RuntimeDefault + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + dnsPolicy: ClusterFirst + enableServiceLinks: true + restartPolicy: Always + schedulerName: default-scheduler + securityContext: + fsGroup: 472 + runAsGroup: 472 + runAsNonRoot: true + runAsUser: 472 + serviceAccount: sa-bozo + serviceAccountName: sa-bozo + terminationGracePeriodSeconds: 30 + volumes: + - configMap: + defaultMode: 420 + name: fred + name: config + - emptyDir: {} + name: storage + status: + availableReplicas: 0 + conditions: + - lastTransitionTime: "2024-01-03T21:21:50Z" + lastUpdateTime: "2024-01-03T21:21:50Z" + message: Deployment has minimum availability. + reason: MinimumReplicasAvailable + status: "True" + type: Available + - lastTransitionTime: "2024-01-03T21:21:40Z" + lastUpdateTime: "2024-01-03T21:21:50Z" + message: zorg + reason: NewReplicaSetAvailable + status: "True" + type: Progressing + observedGeneration: 1 + readyReplicas: 1 + replicas: 1 + updatedReplicas: 1 +- apiVersion: apps/v1 + kind: Deployment + metadata: + name: dp3 + namespace: default + spec: + progressDeadlineSeconds: 600 + replicas: 0 + revisionHistoryLimit: 10 + selector: + template: + metadata: + spec: + automountServiceAccountToken: true + containers: + status: + availableReplicas: 0 + observedGeneration: 1 + readyReplicas: 1 + replicas: 1 + updatedReplicas: 1 diff --git a/internal/lint/testdata/apps/ds/1.yaml b/internal/lint/testdata/apps/ds/1.yaml new file mode 100644 index 00000000..5203e848 --- /dev/null +++ b/internal/lint/testdata/apps/ds/1.yaml @@ -0,0 +1,328 @@ +apiVersion: v1 +kind: List +items: +- apiVersion: apps/v1 + kind: DaemonSet + metadata: + name: ds1 + namespace: default + spec: + revisionHistoryLimit: 10 + selector: + matchLabels: + app: p1 + template: + metadata: + labels: + app: p1 + spec: + automountServiceAccountToken: false + containers: + - name: c1 + image: fred:v0.0.0 + imagePullPolicy: IfNotPresent + livenessProbe: + failureThreshold: 3 + httpGet: + path: / + port: 9100 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 + ports: + - containerPort: 9100 + hostPort: 9100 + name: metrics + protocol: TCP + readinessProbe: + failureThreshold: 3 + httpGet: + path: / + port: 9100 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 + resources: + requests: + cpu: 1m + memory: 10Mi + limits: + cpu: 1m + memory: 10Mi + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + volumeMounts: + - mountPath: /host/proc + name: proc + readOnly: true + - mountPath: /host/sys + name: sys + readOnly: true + - mountPath: /host/root + mountPropagation: HostToContainer + name: root + readOnly: true + dnsPolicy: ClusterFirst + hostNetwork: true + hostPID: true + nodeSelector: + kubernetes.io/os: linux + restartPolicy: Always + schedulerName: default-scheduler + securityContext: + fsGroup: 65534 + runAsGroup: 65534 + runAsNonRoot: true + runAsUser: 65534 + serviceAccount: sa1 + serviceAccountName: sa1 + terminationGracePeriodSeconds: 30 + tolerations: + - effect: NoSchedule + operator: Exists + volumes: + - hostPath: + path: /proc + type: "" + name: proc + - hostPath: + path: /sys + type: "" + name: sys + - hostPath: + path: / + type: "" + name: root + updateStrategy: + rollingUpdate: + maxSurge: 0 + maxUnavailable: 1 + type: RollingUpdate + status: + currentNumberScheduled: 2 + desiredNumberScheduled: 2 + numberAvailable: 2 + numberMisscheduled: 0 + numberReady: 2 + observedGeneration: 1 + updatedNumberScheduled: 2 +- apiVersion: apps/v1 + kind: DaemonSet + metadata: + name: ds2 + namespace: default + spec: + revisionHistoryLimit: 10 + selector: + matchLabels: + app: p10 + template: + metadata: + labels: + app: p10 + spec: + automountServiceAccountToken: false + initContainers: + - name: ic1 + image: fred + containers: + - name: c1 + image: fred + imagePullPolicy: IfNotPresent + livenessProbe: + failureThreshold: 3 + httpGet: + path: / + port: 9100 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 + ports: + - containerPort: 9100 + hostPort: 9100 + name: metrics + protocol: TCP + readinessProbe: + failureThreshold: 3 + httpGet: + path: / + port: 9100 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 + resources: + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + volumeMounts: + - mountPath: /host/proc + name: proc + readOnly: true + - mountPath: /host/sys + name: sys + readOnly: true + - mountPath: /host/root + mountPropagation: HostToContainer + name: root + readOnly: true + dnsPolicy: ClusterFirst + hostNetwork: true + hostPID: true + nodeSelector: + kubernetes.io/os: linux + restartPolicy: Always + schedulerName: default-scheduler + securityContext: + fsGroup: 65534 + runAsGroup: 65534 + runAsNonRoot: true + runAsUser: 65534 + serviceAccount: sa-bozo + serviceAccountName: sa-bozo + terminationGracePeriodSeconds: 30 + tolerations: + - effect: NoSchedule + operator: Exists + volumes: + - hostPath: + path: /proc + type: "" + name: proc + - hostPath: + path: /sys + type: "" + name: sys + - hostPath: + path: / + type: "" + name: root + updateStrategy: + rollingUpdate: + maxSurge: 0 + maxUnavailable: 1 + type: RollingUpdate + status: + currentNumberScheduled: 2 + desiredNumberScheduled: 2 + numberAvailable: 2 + numberMisscheduled: 0 + numberReady: 2 + observedGeneration: 1 + updatedNumberScheduled: 2 +- apiVersion: apps/v1 + kind: DaemonSet + metadata: + name: ds2 + namespace: default + spec: + revisionHistoryLimit: 10 + selector: + matchLabels: + app: p10 + template: + metadata: + labels: + app: p10 + spec: + automountServiceAccountToken: false + initContainers: + - name: ic1 + image: fred + containers: + - name: c1 + image: fred + imagePullPolicy: IfNotPresent + livenessProbe: + failureThreshold: 3 + httpGet: + path: / + port: 9100 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 + ports: + - containerPort: 9100 + hostPort: 9100 + name: metrics + protocol: TCP + readinessProbe: + failureThreshold: 3 + httpGet: + path: / + port: 9100 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 + resources: + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + volumeMounts: + - mountPath: /host/proc + name: proc + readOnly: true + - mountPath: /host/sys + name: sys + readOnly: true + - mountPath: /host/root + mountPropagation: HostToContainer + name: root + readOnly: true + dnsPolicy: ClusterFirst + hostNetwork: true + hostPID: true + nodeSelector: + kubernetes.io/os: linux + restartPolicy: Always + schedulerName: default-scheduler + securityContext: + fsGroup: 65534 + runAsGroup: 65534 + runAsNonRoot: true + runAsUser: 65534 + serviceAccount: sa-bozo + serviceAccountName: sa-bozo + terminationGracePeriodSeconds: 30 + tolerations: + - effect: NoSchedule + operator: Exists + volumes: + - hostPath: + path: /proc + type: "" + name: proc + - hostPath: + path: /sys + type: "" + name: sys + - hostPath: + path: / + type: "" + name: root + updateStrategy: + rollingUpdate: + maxSurge: 0 + maxUnavailable: 1 + type: RollingUpdate + status: + currentNumberScheduled: 2 + desiredNumberScheduled: 2 + numberAvailable: 2 + numberMisscheduled: 0 + numberReady: 2 + observedGeneration: 1 + updatedNumberScheduled: 2 + diff --git a/internal/lint/testdata/apps/rs/1.yaml b/internal/lint/testdata/apps/rs/1.yaml new file mode 100644 index 00000000..ead707be --- /dev/null +++ b/internal/lint/testdata/apps/rs/1.yaml @@ -0,0 +1,103 @@ +apiVersion: v1 +kind: List +items: +- apiVersion: apps/v1 + kind: ReplicaSet + metadata: + name: rs1 + namespace: default + ownerReferences: + - apiVersion: apps/v1 + blockOwnerDeletion: true + controller: true + kind: Deployment + name: dp1 + spec: + replicas: 0 + selector: + matchLabels: + app: p1 + template: + metadata: + labels: + app: p1 + spec: + containers: + - image: fred + imagePullPolicy: Always + name: c1 + ports: + - containerPort: 4000 + name: http + protocol: TCP + resources: + limits: + cpu: 200m + memory: 30Mi + requests: + cpu: 100m + memory: 10Mi + volumeMounts: + - mountPath: /data + name: om + dnsPolicy: ClusterFirst + restartPolicy: Always + schedulerName: default-scheduler + securityContext: {} + terminationGracePeriodSeconds: 30 + volumes: + - emptyDir: {} + name: om + status: + observedGeneration: 2 + replicas: 0 +- apiVersion: apps/v1 + kind: ReplicaSet + metadata: + name: rs2 + namespace: default + ownerReferences: + - apiVersion: apps/v1 + blockOwnerDeletion: true + controller: true + kind: Deployment + name: dp2 + spec: + replicas: 2 + selector: + matchLabels: + app: p2 + template: + metadata: + labels: + app: p2 + spec: + containers: + - image: fred + imagePullPolicy: Always + name: c1 + ports: + - containerPort: 4000 + name: http + protocol: TCP + resources: + limits: + cpu: 200m + memory: 30Mi + requests: + cpu: 100m + memory: 10Mi + volumeMounts: + - mountPath: /data + name: om + dnsPolicy: ClusterFirst + restartPolicy: Always + schedulerName: default-scheduler + securityContext: {} + terminationGracePeriodSeconds: 30 + volumes: + - emptyDir: {} + name: om + status: + observedGeneration: 2 + replicas: 1 diff --git a/internal/lint/testdata/apps/sts/1.yaml b/internal/lint/testdata/apps/sts/1.yaml new file mode 100644 index 00000000..a3904a32 --- /dev/null +++ b/internal/lint/testdata/apps/sts/1.yaml @@ -0,0 +1,230 @@ +apiVersion: v1 +kind: List +items: +- apiVersion: apps/v1 + kind: StatefulSet + metadata: + name: sts1 + namespace: default + spec: + podManagementPolicy: OrderedReady + replicas: 1 + revisionHistoryLimit: 10 + selector: + matchLabels: + app: p1 + serviceName: svc1 + template: + metadata: + labels: + app: p1 + spec: + automountServiceAccountToken: true + containers: + - image: fred:v0.1.0 + imagePullPolicy: IfNotPresent + livenessProbe: + failureThreshold: 3 + httpGet: + path: / + port: http + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 + name: c1 + ports: + - containerPort: 9093 + name: http + protocol: TCP + readinessProbe: + failureThreshold: 3 + httpGet: + path: / + port: http + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 + resources: + limits: + cpu: 200m + memory: 30Mi + requests: + cpu: 100m + memory: 10Mi + securityContext: + runAsGroup: 65534 + runAsNonRoot: true + runAsUser: 65534 + dnsPolicy: ClusterFirst + restartPolicy: Always + schedulerName: default-scheduler + securityContext: + fsGroup: 65534 + runAsGroup: 65534 + runAsNonRoot: true + runAsUser: 65534 + serviceAccount: sa1 + serviceAccountName: sa1 + terminationGracePeriodSeconds: 30 + updateStrategy: + rollingUpdate: + partition: 0 + type: RollingUpdate + status: + collisionCount: 0 + currentReplicas: 1 + observedGeneration: 1 + readyReplicas: 1 + replicas: 1 + updatedReplicas: 1 +- apiVersion: apps/v1 + kind: StatefulSet + metadata: + name: sts2 + namespace: default + spec: + podManagementPolicy: OrderedReady + replicas: 1 + revisionHistoryLimit: 10 + selector: + matchLabels: + app: p2 + serviceName: svc2 + template: + metadata: + labels: + app: p2 + spec: + automountServiceAccountToken: true + containers: + - image: fred + imagePullPolicy: IfNotPresent + livenessProbe: + failureThreshold: 3 + httpGet: + path: / + port: http + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 + name: c1 + ports: + - containerPort: 9093 + name: http + protocol: TCP + readinessProbe: + failureThreshold: 3 + httpGet: + path: / + port: http + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 + resources: + requests: + cpu: 1 + memory: 1Mi + limits: + cpu: 1 + memory: 1Mi + securityContext: + runAsGroup: 65534 + runAsNonRoot: true + runAsUser: 65534 + dnsPolicy: ClusterFirst + restartPolicy: Always + schedulerName: default-scheduler + securityContext: + fsGroup: 65534 + runAsGroup: 65534 + runAsNonRoot: true + runAsUser: 65534 + serviceAccount: sa1 + serviceAccountName: sa1 + terminationGracePeriodSeconds: 30 + updateStrategy: + rollingUpdate: + partition: 0 + type: RollingUpdate + status: + collisionCount: 0 + currentReplicas: 1 + observedGeneration: 1 + readyReplicas: 1 + replicas: 1 + updatedReplicas: 1 +- apiVersion: apps/v1 + kind: StatefulSet + metadata: + name: sts3 + namespace: default + spec: + podManagementPolicy: OrderedReady + replicas: 1 + revisionHistoryLimit: 10 + selector: + matchLabels: + app: p3 + serviceName: svc3 + template: + metadata: + labels: + app: p3 + spec: + automountServiceAccountToken: true + containers: + - image: fred:0.0.1 + imagePullPolicy: IfNotPresent + livenessProbe: + failureThreshold: 3 + httpGet: + path: / + port: http + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 + name: c1 + ports: + - containerPort: 9093 + name: http + protocol: TCP + readinessProbe: + failureThreshold: 3 + httpGet: + path: / + port: http + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 + resources: {} + securityContext: + runAsGroup: 65534 + runAsNonRoot: true + runAsUser: 65534 + dnsPolicy: ClusterFirst + restartPolicy: Always + schedulerName: default-scheduler + securityContext: + fsGroup: 65534 + runAsGroup: 65534 + runAsNonRoot: true + runAsUser: 65534 + serviceAccountName: sa-bozo + terminationGracePeriodSeconds: 30 + updateStrategy: + rollingUpdate: + partition: 0 + type: RollingUpdate + status: + collisionCount: 0 + currentReplicas: 1 + observedGeneration: 1 + readyReplicas: 0 + replicas: 1 + updatedReplicas: 1 \ No newline at end of file diff --git a/internal/lint/testdata/auth/cr/1.yaml b/internal/lint/testdata/auth/cr/1.yaml new file mode 100644 index 00000000..7be46916 --- /dev/null +++ b/internal/lint/testdata/auth/cr/1.yaml @@ -0,0 +1,28 @@ +--- +apiVersion: v1 +kind: List +items: + - apiVersion: rbac.authorization.k8s.io/v1 + kind: ClusterRole + metadata: + name: cr1 + rules: + - apiGroups: [""] + resources: ["pods"] + verbs: ["get", "watch", "list"] + - apiVersion: rbac.authorization.k8s.io/v1 + kind: ClusterRole + metadata: + name: cr2 + rules: + - apiGroups: [""] + resources: ["configmaps"] + verbs: ["get", "watch", "list"] + - apiVersion: rbac.authorization.k8s.io/v1 + kind: ClusterRole + metadata: + name: cr3 + rules: + - apiGroups: [""] + resources: ["secrets"] + verbs: ["get", "watch", "list"] \ No newline at end of file diff --git a/internal/lint/testdata/auth/crb/1.yaml b/internal/lint/testdata/auth/crb/1.yaml new file mode 100644 index 00000000..9c7112a3 --- /dev/null +++ b/internal/lint/testdata/auth/crb/1.yaml @@ -0,0 +1,42 @@ +--- +apiVersion: v1 +kind: List +items: + - apiVersion: rbac.authorization.k8s.io/v1 + kind: ClusterRoleBinding + metadata: + name: crb1 + subjects: + - kind: User + name: fred + apiGroup: rbac.authorization.k8s.io + roleRef: + kind: ClusterRole + name: cr1 + apiGroup: rbac.authorization.k8s.io + - apiVersion: rbac.authorization.k8s.io/v1 + kind: ClusterRoleBinding + metadata: + name: crb2 + subjects: + - kind: ServiceAccount + name: sa2 + namespace: default + apiGroup: rbac.authorization.k8s.io + roleRef: + kind: ClusterRole + name: cr-bozo + apiGroup: rbac.authorization.k8s.io + - apiVersion: rbac.authorization.k8s.io/v1 + kind: ClusterRoleBinding + metadata: + name: crb3 + subjects: + - kind: ServiceAccount + name: sa-bozo + namespace: default + apiGroup: rbac.authorization.k8s.io + roleRef: + kind: Role + name: r-bozo + apiGroup: rbac.authorization.k8s.io \ No newline at end of file diff --git a/internal/lint/testdata/auth/ro/1.yaml b/internal/lint/testdata/auth/ro/1.yaml new file mode 100644 index 00000000..dd5c06a8 --- /dev/null +++ b/internal/lint/testdata/auth/ro/1.yaml @@ -0,0 +1,31 @@ +--- +apiVersion: v1 +kind: List +items: + - apiVersion: rbac.authorization.k8s.io/v1 + kind: Role + metadata: + name: r1 + namespace: default + rules: + - apiGroups: [""] + resources: ["pods"] + verbs: ["get", "watch", "list"] + - apiVersion: rbac.authorization.k8s.io/v1 + kind: Role + metadata: + name: r2 + namespace: default + rules: + - apiGroups: [""] + resources: ["configmaps"] + verbs: ["get", "watch", "list"] + - apiVersion: rbac.authorization.k8s.io/v1 + kind: Role + metadata: + name: r3 + namespace: default + rules: + - apiGroups: [""] + resources: ["secrets"] + verbs: ["get", "watch", "list"] \ No newline at end of file diff --git a/internal/lint/testdata/auth/rob/1.yaml b/internal/lint/testdata/auth/rob/1.yaml new file mode 100644 index 00000000..f338fe2c --- /dev/null +++ b/internal/lint/testdata/auth/rob/1.yaml @@ -0,0 +1,43 @@ +--- +apiVersion: v1 +kind: List +items: + - apiVersion: rbac.authorization.k8s.io/v1 + kind: RoleBinding + metadata: + name: rb1 + namespace: default + subjects: + - kind: User + name: fred + apiGroup: rbac.authorization.k8s.io + roleRef: + kind: Role + name: r1 + apiGroup: rbac.authorization.k8s.io + - apiVersion: rbac.authorization.k8s.io/v1 + kind: RoleBinding + metadata: + name: rb2 + namespace: default + subjects: + - kind: ServiceAccount + name: sa-bozo + apiGroup: rbac.authorization.k8s.io + roleRef: + kind: Role + name: r-bozo + apiGroup: rbac.authorization.k8s.io + - apiVersion: rbac.authorization.k8s.io/v1 + kind: RoleBinding + metadata: + name: rb3 + namespace: default + subjects: + - kind: ServiceAccount + name: sa-bozo + apiGroup: rbac.authorization.k8s.io + roleRef: + kind: ClusterRole + name: cr-bozo + apiGroup: rbac.authorization.k8s.io \ No newline at end of file diff --git a/internal/lint/testdata/autoscaling/hpa/1.yaml b/internal/lint/testdata/autoscaling/hpa/1.yaml new file mode 100644 index 00000000..9afc38de --- /dev/null +++ b/internal/lint/testdata/autoscaling/hpa/1.yaml @@ -0,0 +1,183 @@ +apiVersion: v1 +kind: List +items: +- apiVersion: autoscaling/v2 + kind: HorizontalPodAutoscaler + metadata: + name: hpa1 + namespace: default + spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: dp1 + minReplicas: 1 + maxReplicas: 10 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 50 + status: + observedGeneration: 1 + currentReplicas: 1 + desiredReplicas: 1 + currentMetrics: + - type: Resource + resource: + name: cpu + current: + averageUtilization: 0 + averageValue: 0 +- apiVersion: autoscaling/v2 + kind: HorizontalPodAutoscaler + metadata: + name: hpa2 + namespace: default + spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: dp-toast + minReplicas: 1 + maxReplicas: 10 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 50 + status: + observedGeneration: 1 + currentReplicas: 1 + desiredReplicas: 1 + currentMetrics: + - type: Resource + resource: + name: cpu + current: + averageUtilization: 0 + averageValue: 0 +- apiVersion: autoscaling/v2 + kind: HorizontalPodAutoscaler + metadata: + name: hpa3 + namespace: default + spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: ReplicaSet + name: rs-toast + minReplicas: 1 + maxReplicas: 10 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 50 + status: + observedGeneration: 1 + currentReplicas: 1 + desiredReplicas: 1 + currentMetrics: + - type: Resource + resource: + name: cpu + current: + averageUtilization: 0 + averageValue: 0 +- apiVersion: autoscaling/v2 + kind: HorizontalPodAutoscaler + metadata: + name: hpa4 + namespace: default + spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: StatefulSet + name: sts-toast + minReplicas: 1 + maxReplicas: 10 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 50 + status: + observedGeneration: 1 + currentReplicas: 1 + desiredReplicas: 1 + currentMetrics: + - type: Resource + resource: + name: cpu + current: + averageUtilization: 0 + averageValue: 0 +- apiVersion: autoscaling/v2 + kind: HorizontalPodAutoscaler + metadata: + name: hpa5 + namespace: default + spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: StatefulSet + name: sts1 + minReplicas: 1 + maxReplicas: 10 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 50 + status: + observedGeneration: 1 + currentReplicas: 1 + desiredReplicas: 1 + currentMetrics: + - type: Resource + resource: + name: cpu + current: + averageUtilization: 0 + averageValue: 0 +- apiVersion: autoscaling/v2 + kind: HorizontalPodAutoscaler + metadata: + name: hpa6 + namespace: default + spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: ReplicaSet + name: rs1 + minReplicas: 1 + maxReplicas: 10 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 50 + status: + observedGeneration: 1 + currentReplicas: 1 + desiredReplicas: 1 + currentMetrics: + - type: Resource + resource: + name: cpu + current: + averageUtilization: 0 + averageValue: 0 \ No newline at end of file diff --git a/internal/lint/testdata/batch/cjob/1.yaml b/internal/lint/testdata/batch/cjob/1.yaml new file mode 100644 index 00000000..e760b3e6 --- /dev/null +++ b/internal/lint/testdata/batch/cjob/1.yaml @@ -0,0 +1,76 @@ +apiVersion: v1 +kind: List +items: +- apiVersion: batch/v1 + kind: CronJob + metadata: + name: cj1 + namespace: default + spec: + concurrencyPolicy: Forbid + failedJobsHistoryLimit: 1 + jobTemplate: + metadata: + creationTimestamp: null + spec: + selector: + matchLabels: + app: j1 + template: + metadata: + creationTimestamp: null + spec: + containers: + - image: fred:1.0 + imagePullPolicy: Always + name: c1 + resources: + limits: + cpu: 500m + memory: 1Mi + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + dnsPolicy: ClusterFirst + restartPolicy: OnFailure + schedulerName: default-scheduler + securityContext: {} + terminationGracePeriodSeconds: 30 + schedule: '* * * * *' + successfulJobsHistoryLimit: 3 + suspend: false + status: + active: + - apiVersion: batch/v1 + kind: Job + name: j1 + namespace: default + lastScheduleTime: "2023-02-06T15:49:00Z" + lastSuccessfulTime: "2023-02-06T15:49:38Z" +- apiVersion: batch/v1 + kind: CronJob + metadata: + name: cj2 + namespace: default + spec: + concurrencyPolicy: Forbid + failedJobsHistoryLimit: 1 + jobTemplate: + spec: + template: + spec: + serviceAccountName: sa-bozo + containers: + - image: blang/busybox-bash + imagePullPolicy: Always + name: c1 + resources: {} + dnsPolicy: ClusterFirst + restartPolicy: OnFailure + schedulerName: default-scheduler + securityContext: {} + terminationGracePeriodSeconds: 30 + schedule: '* * * * *' + successfulJobsHistoryLimit: 3 + suspend: true + status: + active: [] diff --git a/internal/lint/testdata/batch/job/1.yaml b/internal/lint/testdata/batch/job/1.yaml new file mode 100644 index 00000000..6cfde3e7 --- /dev/null +++ b/internal/lint/testdata/batch/job/1.yaml @@ -0,0 +1,160 @@ +apiVersion: v1 +kind: List +items: +- apiVersion: batch/v1 + kind: Job + metadata: + labels: + job-name: j1 + app: j1 + name: j1 + namespace: default + ownerReferences: + - apiVersion: batch/v1 + blockOwnerDeletion: true + controller: true + kind: CronJob + name: cj1 + spec: + backoffLimit: 6 + completionMode: NonIndexed + completions: 1 + parallelism: 1 + selector: + matchLabels: + batch.kubernetes.io/controller-uid: xxx + suspend: false + template: + metadata: + creationTimestamp: null + labels: + batch.kubernetes.io/controller-uid: xxx + batch.kubernetes.io/job-name: j1 + job-name: j1 + spec: + containers: + - image: fred:1.0 + imagePullPolicy: Always + name: c1 + resources: + limits: + cpu: 1m + memory: 1Mi + dnsPolicy: ClusterFirst + restartPolicy: OnFailure + schedulerName: default-scheduler + securityContext: {} + terminationGracePeriodSeconds: 30 + status: + conditions: + - lastProbeTime: "2023-02-05T23:21:13Z" + lastTransitionTime: "2023-02-05T23:21:13Z" + status: "True" + type: Complete + ready: 0 + startTime: "2023-02-05T23:21:00Z" + succeeded: 1 + uncountedTerminatedPods: {} +- apiVersion: batch/v1 + kind: Job + metadata: + labels: + job-name: j2 + name: j2 + namespace: default + ownerReferences: + - apiVersion: batch/v1 + blockOwnerDeletion: true + controller: true + kind: CronJob + name: cj2 + spec: + backoffLimit: 6 + completionMode: NonIndexed + completions: 1 + parallelism: 1 + selector: + matchLabels: + batch.kubernetes.io/controller-uid: xxx + suspend: false + template: + metadata: + creationTimestamp: null + labels: + batch.kubernetes.io/job-name: j2 + job-name: j2 + spec: + containers: + - image: bozo + imagePullPolicy: Always + name: c1 + resources: {} + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + dnsPolicy: ClusterFirst + restartPolicy: OnFailure + schedulerName: default-scheduler + securityContext: {} + terminationGracePeriodSeconds: 30 + status: + active: 1 + ready: 0 + startTime: "2023-02-06T15:49:38Z" + uncountedTerminatedPods: {} +- apiVersion: batch/v1 + kind: Job + metadata: + labels: + job-name: j3 + name: j3 + namespace: default + ownerReferences: + - apiVersion: batch/v1 + blockOwnerDeletion: true + controller: true + kind: CronJob + name: cj3 + spec: + backoffLimit: 6 + completionMode: NonIndexed + completions: 1 + parallelism: 1 + selector: + matchLabels: + batch.kubernetes.io/controller-uid: xxx + suspend: true + template: + metadata: + creationTimestamp: null + labels: + batch.kubernetes.io/job-name: j2 + job-name: j2 + spec: + initContainers: + - image: bozo:1.0.0 + imagePullPolicy: Always + name: ic1 + resources: + limits: + cpu: 1m + memory: 1Mi + containers: + - image: bozo:1.0.0 + imagePullPolicy: Always + name: c1 + resources: + limits: + cpu: 1m + memory: 1Mi + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + dnsPolicy: ClusterFirst + restartPolicy: OnFailure + schedulerName: default-scheduler + securityContext: {} + terminationGracePeriodSeconds: 30 + status: + active: 1 + ready: 0 + startTime: "2023-02-06T15:49:38Z" + uncountedTerminatedPods: {} \ No newline at end of file diff --git a/internal/lint/testdata/core/cm/1.yaml b/internal/lint/testdata/core/cm/1.yaml new file mode 100644 index 00000000..462383a3 --- /dev/null +++ b/internal/lint/testdata/core/cm/1.yaml @@ -0,0 +1,37 @@ +--- +apiVersion: v1 +kind: List +items: +- apiVersion: v1 + kind: ConfigMap + metadata: + name: cm1 + namespace: default + data: + fred.yaml: | + k1: 1 + k2: blee +- apiVersion: v1 + kind: ConfigMap + metadata: + name: cm2 + namespace: default + data: + k1: apple + k2: bee +- apiVersion: v1 + kind: ConfigMap + metadata: + name: cm3 + namespace: default + data: + k1: apple + k2: bee +- apiVersion: v1 + kind: ConfigMap + metadata: + name: cm4 + namespace: default + data: + k1: apple + k2: bee diff --git a/internal/lint/testdata/core/ep/1.yaml b/internal/lint/testdata/core/ep/1.yaml new file mode 100644 index 00000000..78f37ff9 --- /dev/null +++ b/internal/lint/testdata/core/ep/1.yaml @@ -0,0 +1,68 @@ +apiVersion: v1 +kind: List +items: +- apiVersion: v1 + kind: Endpoints + metadata: + name: svc1 + namespace: default + labels: + app: p1 + subsets: + - addresses: + - ip: 10.244.1.27 + nodeName: n1 + targetRef: + kind: Pod + name: p1 + namespace: default + ports: + - name: http + port: 4000 + protocol: TCP +- apiVersion: v1 + kind: Endpoints + metadata: + name: svc2 + namespace: default + subsets: + - addresses: + - ip: 10.244.1.19 + nodeName: n1 + targetRef: + kind: Pod + name: p2 + namespace: default + ports: + - name: service + port: 3000 + protocol: TCP +- apiVersion: v1 + kind: Endpoints + metadata: + name: svc-none + namespace: default + subsets: + - addresses: + - ip: 10.244.1.19 + nodeName: n1 + targetRef: + kind: Pod + name: p5 + namespace: default + - ip: 10.244.1.19 + nodeName: n1 + targetRef: + kind: Pod + name: p4 + namespace: default + ports: + - name: service + port: 3000 + protocol: TCP +- apiVersion: v1 + kind: Endpoints + metadata: + name: svc4 + namespace: default + subsets: \ No newline at end of file diff --git a/internal/lint/testdata/core/node/1.yaml b/internal/lint/testdata/core/node/1.yaml new file mode 100644 index 00000000..39262c28 --- /dev/null +++ b/internal/lint/testdata/core/node/1.yaml @@ -0,0 +1,234 @@ +apiVersion: v1 +kind: List +items: +- apiVersion: v1 + kind: Node + metadata: + labels: + node-role.kubernetes.io/control-plane: "" + node-role.kubernetes.io/master: "" + node.kubernetes.io/exclude-from-external-load-balancers: "" + name: n1 + spec: + podCIDR: 10.244.0.0/24 + podCIDRs: + - 10.244.0.0/24 + status: + addresses: + - address: 192.168.228.3 + type: InternalIP + - address: dashb-control-plane + type: Hostname + allocatable: + cpu: "10" + ephemeral-storage: 816748224Ki + memory: 8124744Ki + pods: "110" + capacity: + cpu: "10" + ephemeral-storage: 816748224Ki + memory: 8124744Ki + pods: "110" + conditions: + - lastHeartbeatTime: "2024-01-27T15:31:39Z" + lastTransitionTime: "2024-01-03T20:35:11Z" + message: kubelet has sufficient memory available + reason: KubeletHasSufficientMemory + status: "False" + type: MemoryPressure + - lastHeartbeatTime: "2024-01-27T15:31:39Z" + lastTransitionTime: "2024-01-03T20:35:11Z" + message: kubelet has no disk pressure + reason: KubeletHasNoDiskPressure + status: "False" + type: DiskPressure + - lastHeartbeatTime: "2024-01-27T15:31:39Z" + lastTransitionTime: "2024-01-03T20:35:11Z" + message: kubelet has sufficient PID available + reason: KubeletHasSufficientPID + status: "False" + type: PIDPressure + - lastHeartbeatTime: "2024-01-27T15:31:39Z" + lastTransitionTime: "2024-01-03T20:35:38Z" + message: kubelet is posting ready status + reason: KubeletReady + status: "True" + type: Ready + daemonEndpoints: + kubeletEndpoint: + Port: 10250 + images: + nodeInfo: + architecture: arm64 + bootID: 0836e65d-3091-4cb5-8ad4-8f65425f87e3 + containerRuntimeVersion: containerd://1.5.1 + kernelVersion: 6.5.10-orbstack-00110-gbcfe04c86d2f + kubeProxyVersion: v1.21.1 + kubeletVersion: v1.21.1 + machineID: 6bbc44bb821d48b995092021d706d8e6 + operatingSystem: linux + osImage: Ubuntu 20.10 + systemUUID: 6bbc44bb821d48b995092021d706d8e6 +- apiVersion: v1 + kind: Node + metadata: + annotations: + kubeadm.alpha.kubernetes.io/cri-socket: unix:///run/containerd/containerd.sock + node.alpha.kubernetes.io/ttl: "0" + volumes.kubernetes.io/controller-managed-attach-detach: "true" + labels: + beta.kubernetes.io/arch: arm64 + beta.kubernetes.io/os: linux + kubernetes.io/arch: arm64 + kubernetes.io/hostname: n2 + kubernetes.io/os: linux + name: n2 + spec: + podCIDR: 10.244.1.0/24 + podCIDRs: + - 10.244.1.0/24 + taints: + - effect: NoSchedule + key: t2 + status: + addresses: + - address: 192.168.228.2 + type: InternalIP + - address: dashb-worker + type: Hostname + allocatable: + cpu: "10" + ephemeral-storage: 816748224Ki + memory: 8124744Ki + pods: "110" + capacity: + cpu: "10" + ephemeral-storage: 816748224Ki + memory: 8124744Ki + pods: "110" + conditions: + - lastHeartbeatTime: "2024-01-27T15:30:29Z" + lastTransitionTime: "2024-01-03T20:35:48Z" + message: kubelet has sufficient memory available + reason: KubeletHasSufficientMemory + status: "False" + type: MemoryPressure + - lastHeartbeatTime: "2024-01-27T15:30:29Z" + lastTransitionTime: "2024-01-03T20:35:48Z" + message: kubelet has no disk pressure + reason: KubeletHasNoDiskPressure + status: "False" + type: DiskPressure + - lastHeartbeatTime: "2024-01-27T15:30:29Z" + lastTransitionTime: "2024-01-03T20:35:48Z" + message: kubelet has sufficient PID available + reason: KubeletHasSufficientPID + status: "False" + type: PIDPressure + - lastHeartbeatTime: "2024-01-27T15:30:29Z" + lastTransitionTime: "2024-01-03T20:35:58Z" + message: kubelet is posting ready status + reason: KubeletReady + status: "True" + type: Ready + - lastHeartbeatTime: "2024-01-27T15:30:29Z" + lastTransitionTime: "2024-01-03T20:35:58Z" + message: blee + reason: blah + status: "True" + type: NetworkUnavailable + daemonEndpoints: + kubeletEndpoint: + Port: 10250 + images: + nodeInfo: + architecture: arm64 + containerRuntimeVersion: containerd://1.5.1 + kernelVersion: 6.5.10-orbstack-00110-gbcfe04c86d2f + kubeProxyVersion: v1.21.1 + kubeletVersion: v1.21.1 + operatingSystem: linux + osImage: Ubuntu 20.10 +- apiVersion: v1 + kind: Node + metadata: + labels: + beta.kubernetes.io/arch: arm64 + beta.kubernetes.io/os: linux + kubernetes.io/arch: arm64 + kubernetes.io/hostname: n3 + kubernetes.io/os: linux + name: n3 + spec: + status: + conditions: + - message: kubelet has sufficient memory available + reason: KubeletHasSufficientMemory + status: "True" + type: MemoryPressure + - message: kubelet has no disk pressure + reason: KubeletHasNoDiskPressure + status: "True" + type: DiskPressure + - message: kubelet has sufficient PID available + reason: KubeletHasSufficientPID + status: "True" + type: PIDPressure + - message: kubelet is posting ready status + reason: KubeletReady + status: "True" + type: Ready + - message: blee + reason: blah + status: "True" + type: NetworkUnavailable + images: + nodeInfo: +- apiVersion: v1 + kind: Node + metadata: + labels: + beta.kubernetes.io/arch: arm64 + beta.kubernetes.io/os: linux + kubernetes.io/arch: arm64 + kubernetes.io/hostname: n4 + kubernetes.io/os: linux + name: n4 + spec: + unschedulable: true + status: + conditions: + - message: bla + reason: blee + status: Unknown + type: "" + - message: bla + reason: blee + status: "False" + type: Ready + images: + nodeInfo: +- apiVersion: v1 + kind: Node + metadata: + labels: + beta.kubernetes.io/arch: arm64 + beta.kubernetes.io/os: linux + kubernetes.io/arch: arm64 + kubernetes.io/hostname: n5 + kubernetes.io/os: linux + name: n5 + spec: + status: + images: + nodeInfo: + allocatable: + cpu: "100m" + ephemeral-storage: 816748224Ki + memory: 10Mi + pods: "110" + capacity: + cpu: "1" + ephemeral-storage: 816748224Ki + memory: 10Mi + pods: "110" diff --git a/internal/lint/testdata/core/ns/1.yaml b/internal/lint/testdata/core/ns/1.yaml new file mode 100644 index 00000000..cd63ee8e --- /dev/null +++ b/internal/lint/testdata/core/ns/1.yaml @@ -0,0 +1,34 @@ +apiVersion: v1 +kind: List +items: +- apiVersion: v1 + kind: Namespace + metadata: + name: default + labels: + ns: default + spec: + finalizers: + - kubernetes + status: + phase: Active +- apiVersion: v1 + kind: Namespace + metadata: + name: ns1 + labels: + app: ns1 + spec: + finalizers: + - kubernetes + status: + phase: Active +- apiVersion: v1 + kind: Namespace + metadata: + name: ns2 + labels: + app: ns2 + spec: + finalizers: + - kubernetes \ No newline at end of file diff --git a/internal/lint/testdata/core/pod/1.yaml b/internal/lint/testdata/core/pod/1.yaml new file mode 100644 index 00000000..fe35bd0a --- /dev/null +++ b/internal/lint/testdata/core/pod/1.yaml @@ -0,0 +1,71 @@ +--- +apiVersion: v1 +kind: List +items: +- apiVersion: v1 + kind: Pod + metadata: + name: p1 + namespace: default + labels: + app: p1 + spec: + serviceAccountName: default + tolerations: + - key: t1 + operator: Exists + effect: NoSchedule + containers: + - name: c1 + image: alpine + resources: + requests: + cpu: 1 + memory: 1Mi + limits: + cpu: 1 + memory: 1Mi + ports: + - containerPort: 9090 + name: http + protocol: TCP + env: + - name: env1 + valueFrom: + configMapKeyRef: + name: cm1 + key: ns + - name: env2 + valueFrom: + secretKeyRef: + name: sec1 + key: k1 + volumeMounts: + - name: config + mountPath: "/config" + readOnly: true + volumes: + - name: mypd + persistentVolumeClaim: + claimName: pvc1 + - name: config + configMap: + name: cm3 + items: + - key: k1 + path: "game.properties" + - key: k2 + path: "user-interface.properties" + - name: secret + secret: + secretName: sec1 + optional: false + items: + - key: ca.crt + path: "game.properties" + - key: namespace + path: "user-interface.properties" + status: + podIPs: + - ip: 172.1.0.3 + # - ip: 172.1.4.5 \ No newline at end of file diff --git a/internal/lint/testdata/core/pod/2.yaml b/internal/lint/testdata/core/pod/2.yaml new file mode 100644 index 00000000..933ac556 --- /dev/null +++ b/internal/lint/testdata/core/pod/2.yaml @@ -0,0 +1,178 @@ +--- +apiVersion: v1 +kind: List +items: + - apiVersion: v1 + kind: Pod + metadata: + name: p1 + namespace: default + labels: + app: p1 + ownerReferences: + - apiVersion: apps/v1 + controller: true + kind: ReplicaSet + name: rs1 + spec: + serviceAccountName: sa1 + automountServiceAccountToken: false + status: + conditions: + phase: Running + - apiVersion: v1 + kind: Pod + metadata: + name: p2 + namespace: default + labels: + app: test2 + spec: + serviceAccountName: sa2 + - apiVersion: v1 + kind: Pod + metadata: + name: p3 + namespace: default + labels: + app: p3 + ownerReferences: + - apiVersion: apps/v1 + controller: true + kind: DaemonSet + name: rs3 + spec: + serviceAccountName: sa3 + containers: + - image: dorker.io/blee:1.0.1 + name: c1 + resources: + limits: + cpu: 1 + mem: 1Mi + livenessProbe: + httpGet: + path: /healthz + port: 8080 + initialDelaySeconds: 3 + periodSeconds: 3 + readinessProbe: + httpGet: + path: /healthz + port: 8080 + initialDelaySeconds: 3 + periodSeconds: 3 + status: + conditions: + - status: "False" + type: Initialized + - status: "False" + type: Ready + - status: "False" + type: ContainersReady + - status: "False" + type: PodScheduled + phase: Running + - apiVersion: v1 + kind: Pod + metadata: + name: p4 + namespace: default + labels: + app: test4 + ownerReferences: + - apiVersion: apps/v1 + controller: false + kind: Job + name: j4 + spec: + serviceAccountName: default + automountServiceAccountToken: true + initContainers: + - image: zorg + imagePullPolicy: IfNotPresent + name: ic1 + containers: + - image: blee + imagePullPolicy: IfNotPresent + name: c1 + resources: + limits: + cpu: 1 + mem: 1Mi + livenessProbe: + httpGet: + path: /healthz + port: 8080 + initialDelaySeconds: 3 + periodSeconds: 3 + volumeMounts: + - mountPath: /etc/config + name: config-volume + readOnly: true + - mountPath: /var/run/secrets/kubernetes.io/serviceaccount + name: kube-api-access-jgtlv + readOnly: true + - image: zorg:latest + imagePullPolicy: IfNotPresent + name: c2 + resources: + requests: + mem: 1Mi + readinessProbe: + httpGet: + path: /healthz + port: p1 + initialDelaySeconds: 3 + periodSeconds: 3 + volumeMounts: + - mountPath: /etc/config + name: config-volume + readOnly: true + - mountPath: /var/run/secrets/kubernetes.io/serviceaccount + name: kube-api-access-jgtlv + readOnly: true + status: + phase: Running + conditions: + initContainerStatuses: + - containerID: ic1 + image: blee + name: ic1 + ready: false + restartCount: 1000 + started: false + containerStatuses: + - containerID: c1 + image: blee + name: c1 + ready: false + restartCount: 1000 + started: false + - containerID: c2 + name: c2 + ready: true + restartCount: 0 + started: true + - apiVersion: v1 + kind: Pod + metadata: + name: p5 + namespace: default + labels: + app: test5 + ownerReferences: + - apiVersion: apps/v1 + controller: true + kind: ReplicaSet + name: rs5 + spec: + serviceAccountName: sa5 + automountServiceAccountToken: true + containers: + - image: blee:v1.2 + imagePullPolicy: IfNotPresent + name: c1 + status: + conditions: + phase: Running diff --git a/internal/lint/testdata/core/pod/3.yaml b/internal/lint/testdata/core/pod/3.yaml new file mode 100644 index 00000000..49b1419f --- /dev/null +++ b/internal/lint/testdata/core/pod/3.yaml @@ -0,0 +1,124 @@ +--- +apiVersion: v1 +kind: List +items: +- apiVersion: v1 + kind: Pod + metadata: + name: p1 + namespace: ns1 + labels: + app: p1 + ownerReferences: + - apiVersion: apps/v1 + controller: true + kind: DaemonSet + name: rs3 + spec: + serviceAccountName: sa1 + tolerations: + - key: t1 + operator: Exists + effect: NoSchedule + containers: + - name: c1 + image: alpine:v1.0 + livenessProbe: + httpGet: + path: /healthz + port: http + initialDelaySeconds: 3 + periodSeconds: 3 + readinessProbe: + httpGet: + path: /healthz + port: http + initialDelaySeconds: 3 + periodSeconds: 3 + resources: + requests: + cpu: 1 + memory: 1Mi + limits: + cpu: 1 + memory: 1Mi + ports: + - containerPort: 9090 + name: http + protocol: TCP + env: + - name: env1 + valueFrom: + configMapKeyRef: + name: cm1 + key: ns + - name: env2 + valueFrom: + secretKeyRef: + name: sec1 + key: k1 + status: + conditions: + phase: Running + podIPs: + - ip: 172.1.0.3 +- apiVersion: v1 + kind: Pod + metadata: + name: p2 + namespace: ns2 + labels: + app: p2 + ownerReferences: + - apiVersion: apps/v1 + controller: true + kind: DaemonSet + name: rs3 + spec: + serviceAccountName: sa2 + tolerations: + - key: t1 + operator: Exists + effect: NoSchedule + containers: + - name: c1 + image: alpine:v1.0 + livenessProbe: + httpGet: + path: /healthz + port: http + initialDelaySeconds: 3 + periodSeconds: 3 + readinessProbe: + httpGet: + path: /healthz + port: http + initialDelaySeconds: 3 + periodSeconds: 3 + resources: + requests: + cpu: 1 + memory: 1Mi + limits: + cpu: 1 + memory: 1Mi + ports: + - containerPort: 9090 + name: http + protocol: TCP + env: + - name: env1 + valueFrom: + configMapKeyRef: + name: cm1 + key: ns + - name: env2 + valueFrom: + secretKeyRef: + name: sec1 + key: k1 + status: + conditions: + phase: Running + podIPs: + - ip: 172.1.0.3 \ No newline at end of file diff --git a/internal/lint/testdata/core/pv/1.yaml b/internal/lint/testdata/core/pv/1.yaml new file mode 100644 index 00000000..bf74972f --- /dev/null +++ b/internal/lint/testdata/core/pv/1.yaml @@ -0,0 +1,124 @@ +apiVersion: v1 +kind: List +items: +- apiVersion: v1 + kind: PersistentVolume + metadata: + name: pv1 + namespace: default + spec: + accessModes: + - ReadWriteOnce + capacity: + storage: 2Gi + claimRef: + apiVersion: v1 + kind: PersistentVolumeClaim + name: pvc1 + namespace: default + nodeAffinity: + required: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/hostname + operator: In + values: + - n1 + persistentVolumeReclaimPolicy: Delete + storageClassName: standard + volumeMode: Filesystem + status: + phase: Bound +- apiVersion: v1 + kind: PersistentVolume + metadata: + name: pv2 + namespace: default + spec: + accessModes: + - ReadWriteOnce + capacity: + storage: 8Gi + claimRef: + apiVersion: v1 + kind: PersistentVolumeClaim + name: pv2 + namespace: default + hostPath: + path: /var/blee + type: DirectoryOrCreate + nodeAffinity: + required: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/hostname + operator: In + values: + - n2 + persistentVolumeReclaimPolicy: Delete + storageClassName: standard + volumeMode: Filesystem + status: + phase: Failed +- apiVersion: v1 + kind: PersistentVolume + metadata: + name: pv3 + namespace: default + spec: + accessModes: + - ReadWriteOnce + capacity: + storage: 8Gi + claimRef: + apiVersion: v1 + kind: PersistentVolumeClaim + name: pv3 + namespace: default + hostPath: + path: /var/blee + type: DirectoryOrCreate + nodeAffinity: + required: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/hostname + operator: In + values: + - n3 + persistentVolumeReclaimPolicy: Delete + storageClassName: standard + volumeMode: Filesystem + status: + phase: Available +- apiVersion: v1 + kind: PersistentVolume + metadata: + name: pv4 + namespace: default + spec: + accessModes: + - ReadWriteOnce + capacity: + storage: 8Gi + claimRef: + apiVersion: v1 + kind: PersistentVolumeClaim + name: pv4 + namespace: default + hostPath: + path: /var/blee + type: DirectoryOrCreate + nodeAffinity: + required: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/hostname + operator: In + values: + - n4 + persistentVolumeReclaimPolicy: Delete + storageClassName: standard + volumeMode: Filesystem + status: + phase: Pending diff --git a/internal/lint/testdata/core/pvc/1.yaml b/internal/lint/testdata/core/pvc/1.yaml new file mode 100644 index 00000000..6cae0c8d --- /dev/null +++ b/internal/lint/testdata/core/pvc/1.yaml @@ -0,0 +1,91 @@ +apiVersion: v1 +kind: List +items: +- apiVersion: v1 + kind: PersistentVolumeClaim + metadata: + annotations: + pv.kubernetes.io/bind-completed: "yes" + pv.kubernetes.io/bound-by-controller: "yes" + volume.kubernetes.io/selected-node: n1 + finalizers: + - kubernetes.io/pvc-protection + name: pvc1 + namespace: default + spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 2Gi + storageClassName: standard + volumeMode: Filesystem + volumeName: pvc-5a8a78fd-cc3c-4838-ab0b-2b1a475f555d + status: + accessModes: + - ReadWriteOnce + capacity: + storage: 2Gi + phase: Bound +- apiVersion: v1 + kind: PersistentVolumeClaim + metadata: + annotations: + pv.kubernetes.io/bind-completed: "yes" + pv.kubernetes.io/bound-by-controller: "yes" + volume.kubernetes.io/selected-node: n2 + finalizers: + - kubernetes.io/pvc-protection + labels: + app.kubernetes.io/component: server + app.kubernetes.io/instance: prom + app.kubernetes.io/name: prometheus + name: pvc2 + namespace: default + resourceVersion: "861" + spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 8Gi + storageClassName: standard + volumeMode: Filesystem + volumeName: pvc-86489da2-08df-4e95-800c-b8537e3ff03b + status: + accessModes: + - ReadWriteOnce + capacity: + storage: 8Gi + phase: Lost +- apiVersion: v1 + kind: PersistentVolumeClaim + metadata: + annotations: + pv.kubernetes.io/bind-completed: "yes" + pv.kubernetes.io/bound-by-controller: "yes" + volume.kubernetes.io/selected-node: n3 + finalizers: + - kubernetes.io/pvc-protection + labels: + app.kubernetes.io/component: server + app.kubernetes.io/instance: prom + app.kubernetes.io/name: prometheus + name: pvc3 + namespace: default + resourceVersion: "861" + spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 8Gi + storageClassName: standard + volumeMode: Filesystem + volumeName: pvc-86489da2-08df-4e95-800c-b8537e3ff03b + status: + accessModes: + - ReadWriteOnce + capacity: + storage: 8Gi + phase: Pending diff --git a/internal/lint/testdata/core/sa/1.yaml b/internal/lint/testdata/core/sa/1.yaml new file mode 100644 index 00000000..6ce3ea21 --- /dev/null +++ b/internal/lint/testdata/core/sa/1.yaml @@ -0,0 +1,53 @@ +--- +apiVersion: v1 +kind: List +items: + - apiVersion: v1 + kind: ServiceAccount + metadata: + name: default + namespace: default + - apiVersion: v1 + kind: ServiceAccount + metadata: + name: sa1 + namespace: default + automountServiceAccountToken: false + - apiVersion: v1 + kind: ServiceAccount + metadata: + name: sa2 + namespace: default + automountServiceAccountToken: true + - apiVersion: v1 + kind: ServiceAccount + metadata: + name: sa3 + namespace: default + automountServiceAccountToken: true + - apiVersion: v1 + kind: ServiceAccount + metadata: + name: sa4 + namespace: default + automountServiceAccountToken: false + secrets: + - kind: Secret + namespace: default + name: bozo + apiVersion: v1 + imagePullSecrets: + - name: s1 + - apiVersion: v1 + kind: ServiceAccount + metadata: + name: sa5 + namespace: default + automountServiceAccountToken: false + secrets: + - kind: Secret + namespace: default + name: s1 + apiVersion: v1 + imagePullSecrets: + - name: bozo diff --git a/internal/lint/testdata/core/sa/2.yaml b/internal/lint/testdata/core/sa/2.yaml new file mode 100644 index 00000000..a71e07c1 --- /dev/null +++ b/internal/lint/testdata/core/sa/2.yaml @@ -0,0 +1,15 @@ +--- +apiVersion: v1 +kind: List +items: + - apiVersion: v1 + kind: ServiceAccount + metadata: + name: sa1 + namespace: ns1 + - apiVersion: v1 + kind: ServiceAccount + metadata: + name: sa2 + namespace: ns2 + automountServiceAccountToken: false \ No newline at end of file diff --git a/internal/lint/testdata/core/secret/1.yaml b/internal/lint/testdata/core/secret/1.yaml new file mode 100644 index 00000000..e5c2cce0 --- /dev/null +++ b/internal/lint/testdata/core/secret/1.yaml @@ -0,0 +1,34 @@ +apiVersion: v1 +kind: List +items: +- apiVersion: v1 + data: + ca.crt: blee + ns: zorg + kind: Secret + metadata: + annotations: + kubernetes.io/service-account.name: default + name: sec1 + namespace: default + type: kubernetes.io/service-account-token +- apiVersion: v1 + data: + admin-password: zorg + admin-user: blee + kind: Secret + metadata: + labels: + name: sec2 + namespace: default + type: Opaque +- apiVersion: v1 + data: + ca.crt: crap + namespace: zorg + kind: Secret + metadata: + annotations: + name: sec3 + namespace: default + type: kubernetes.io/service-account-token diff --git a/internal/lint/testdata/core/svc/1.yaml b/internal/lint/testdata/core/svc/1.yaml new file mode 100644 index 00000000..cc1ff25a --- /dev/null +++ b/internal/lint/testdata/core/svc/1.yaml @@ -0,0 +1,122 @@ +apiVersion: v1 +kind: List +items: +- apiVersion: v1 + kind: Service + metadata: + labels: + app: p1 + name: svc1 + namespace: default + spec: + clusterIP: 10.96.66.245 + clusterIPs: + - 10.96.66.245 + externalTrafficPolicy: Local + ipFamilies: + - IPv4 + ipFamilyPolicy: SingleStack + ports: + - name: http + nodePort: 30400 + port: 9090 + protocol: TCP + targetPort: 9090 + selector: + app: p1 + sessionAffinity: None + type: NodePort + status: + loadBalancer: {} +- apiVersion: v1 + kind: Service + metadata: + name: svc2 + namespace: default + spec: + clusterIP: 10.96.12.148 + clusterIPs: + - 10.96.12.148 + ipFamilies: + - IPv4 + ipFamilyPolicy: SingleStack + ports: + - name: service + port: 80 + protocol: TCP + targetPort: 3000 + selector: + app: p2 + sessionAffinity: None + type: ClusterIP + status: + loadBalancer: {} +- apiVersion: v1 + kind: Service + metadata: + name: svc3 + namespace: default + spec: + clusterIP: 10.96.12.148 + clusterIPs: + - 10.96.12.148 + ipFamilies: + - IPv4 + ipFamilyPolicy: SingleStack + ports: + - name: service + port: 80 + protocol: TCP + targetPort: 3000 + selector: + app: p2 + sessionAffinity: None + type: ExternalName + status: + loadBalancer: {} +- apiVersion: v1 + kind: Service + metadata: + name: svc4 + namespace: default + spec: + externalTrafficPolicy: Cluster + clusterIP: 10.96.12.148 + clusterIPs: + - 10.96.12.148 + ipFamilies: + - IPv4 + ipFamilyPolicy: SingleStack + ports: + - name: service + port: 80 + protocol: TCP + targetPort: 3000 + selector: + app: p4 + sessionAffinity: None + type: LoadBalancer + status: + loadBalancer: {} +- apiVersion: v1 + kind: Service + metadata: + name: svc5 + namespace: default + spec: + clusterIP: 10.96.12.148 + clusterIPs: + - 10.96.12.148 + ipFamilies: + - IPv4 + ipFamilyPolicy: SingleStack + ports: + - name: service + port: 80 + protocol: TCP + targetPort: 3000 + selector: + app: p5 + sessionAffinity: None + status: + loadBalancer: {} \ No newline at end of file diff --git a/internal/lint/testdata/mx/node/1.yaml b/internal/lint/testdata/mx/node/1.yaml new file mode 100644 index 00000000..2186d04c --- /dev/null +++ b/internal/lint/testdata/mx/node/1.yaml @@ -0,0 +1,18 @@ +apiVersion: v1 +kind: List +items: +- apiVersion: metrics.k8s.io/v1beta1 + kind: NodeMetrics + metadata: + name: n1 + usage: + cpu: 144965184n + memory: 770776Ki +- apiVersion: metrics.k8s.io/v1beta1 + kind: NodeMetrics + metadata: + name: n5 + usage: + cpu: 20 + memory: 40Mi + window: 20.101s diff --git a/internal/lint/testdata/mx/pod/1.yaml b/internal/lint/testdata/mx/pod/1.yaml new file mode 100644 index 00000000..d4112bfe --- /dev/null +++ b/internal/lint/testdata/mx/pod/1.yaml @@ -0,0 +1,39 @@ +apiVersion: v1 +kind: List +items: +- apiVersion: metrics.k8s.io/v1beta1 + kind: PodMetrics + metadata: + labels: + app: p1 + name: p1 + namespace: default + containers: + - name: c1 + usage: + cpu: 20 + memory: 20Mi +- apiVersion: metrics.k8s.io/v1beta1 + kind: PodMetrics + metadata: + labels: + app: p3 + name: p3 + namespace: default + containers: + - name: c1 + usage: + cpu: 2000m + memory: 20Mi +- apiVersion: metrics.k8s.io/v1beta1 + kind: PodMetrics + metadata: + labels: + app: j1 + name: j1 + namespace: default + containers: + - name: c1 + usage: + cpu: 2000m + memory: 20Mi \ No newline at end of file diff --git a/internal/lint/testdata/net/gw/1.yaml b/internal/lint/testdata/net/gw/1.yaml new file mode 100644 index 00000000..0ed8b602 --- /dev/null +++ b/internal/lint/testdata/net/gw/1.yaml @@ -0,0 +1,25 @@ +apiVersion: v1 +kind: List +items: +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + name: gw1 + namespace: default + spec: + gatewayClassName: gwc1 + listeners: + - name: http + protocol: HTTP + port: 80 +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + name: gw2 + namespace: default + spec: + gatewayClassName: gwc-bozo + listeners: + - name: http + protocol: HTTP + port: 80 \ No newline at end of file diff --git a/internal/lint/testdata/net/gwc/1.yaml b/internal/lint/testdata/net/gwc/1.yaml new file mode 100644 index 00000000..f292d46c --- /dev/null +++ b/internal/lint/testdata/net/gwc/1.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: List +items: +- apiVersion: gateway.networking.k8s.io/v1 + kind: GatewayClass + metadata: + name: gwc1 + spec: + controllerName: example.com/gateway-controller +- apiVersion: gateway.networking.k8s.io/v1 + kind: GatewayClass + metadata: + name: gwc2 + spec: + controllerName: example.com/gateway-controller \ No newline at end of file diff --git a/internal/lint/testdata/net/gwr/1.yaml b/internal/lint/testdata/net/gwr/1.yaml new file mode 100644 index 00000000..4c0c2143 --- /dev/null +++ b/internal/lint/testdata/net/gwr/1.yaml @@ -0,0 +1,58 @@ +apiVersion: v1 +kind: List +items: +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + name: r1 + namespace: default + spec: + parentRefs: + - name: gw1 + hostnames: + - fred + rules: + - matches: + - path: + type: PathPrefix + value: /blee + backendRefs: + - name: svc1 + port: 9090 +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + name: r2 + namespace: default + spec: + parentRefs: + - name: gw-bozo + hostnames: + - bozo + rules: + - matches: + - path: + type: PathPrefix + value: /zorg + backendRefs: + - name: svc2 + port: 8080 +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + name: r3 + namespace: default + spec: + parentRefs: + - kind: Service + name: svc-bozo + hostnames: + - bozo + rules: + - matches: + - path: + type: PathPrefix + value: /zorg + backendRefs: + - name: svc2 + port: 9090 \ No newline at end of file diff --git a/internal/lint/testdata/net/ingress/1.yaml b/internal/lint/testdata/net/ingress/1.yaml new file mode 100644 index 00000000..12160400 --- /dev/null +++ b/internal/lint/testdata/net/ingress/1.yaml @@ -0,0 +1,109 @@ +apiVersion: v1 +kind: List +items: +- apiVersion: networking.k8s.io/v1 + kind: Ingress + metadata: + name: ing1 + namespace: default + spec: + ingressClassName: nginx + rules: + - http: + paths: + - path: /testpath + pathType: Prefix + backend: + service: + name: svc1 + port: + name: http +- apiVersion: networking.k8s.io/v1 + kind: Ingress + metadata: + name: ing2 + namespace: default + spec: + ingressClassName: nginx + rules: + - http: + paths: + - path: /testpath + pathType: Prefix + backend: + service: + name: svc1 + port: + number: 9090 +- apiVersion: networking.k8s.io/v1 + kind: Ingress + metadata: + name: ing3 + namespace: default + spec: + ingressClassName: nginx + rules: + - http: + paths: + - path: /testpath + pathType: Prefix + backend: + service: + name: s2 + port: + number: 80 +- apiVersion: networking.k8s.io/v1 + kind: Ingress + metadata: + name: ing4 + namespace: default + spec: + ingressClassName: nginx + rules: + - http: + paths: + - path: /testpath + pathType: Prefix + backend: + service: + name: svc2 +- apiVersion: networking.k8s.io/v1 + kind: Ingress + metadata: + name: ing5 + namespace: default + annotations: + spec: + ingressClassName: nginx + rules: + - http: + paths: + - path: /testpath + pathType: Prefix + backend: + resource: + apiGroup: fred.com + kind: Zorg + name: zorg + status: + loadBalancer: + ingress: + - ports: + - error: boom +- apiVersion: networking.k8s.io/v1 + kind: Ingress + metadata: + name: ing6 + namespace: default + spec: + ingressClassName: nginx + rules: + - http: + paths: + - path: /testpath + pathType: Prefix + backend: + service: + name: svc1 + port: + number: 9091 diff --git a/internal/lint/testdata/net/np/1.yaml b/internal/lint/testdata/net/np/1.yaml new file mode 100644 index 00000000..e25a10c4 --- /dev/null +++ b/internal/lint/testdata/net/np/1.yaml @@ -0,0 +1,107 @@ +apiVersion: v1 +kind: List +items: +- apiVersion: networking.k8s.io/v1 + kind: NetworkPolicy + metadata: + name: np1 + namespace: default + spec: + podSelector: + matchLabels: + app: p1 + policyTypes: + - Ingress + - Egress + ingress: + - from: + - ipBlock: + cidr: 172.1.0.0/16 + except: + - 172.1.0.0/24 + - namespaceSelector: + matchLabels: + ns: default + podSelector: + matchLabels: + app: p1 + ports: + - protocol: TCP + port: 6379 + egress: + - to: + - ipBlock: + cidr: 172.1.0.0/16 + ports: + - protocol: TCP + port: 5978 +- apiVersion: networking.k8s.io/v1 + kind: NetworkPolicy + metadata: + name: np2 + namespace: default + spec: + ingress: + - from: + - ipBlock: + cidr: 172.1.0.0/16 + except: + - 172.1.1.0/24 + - namespaceSelector: + matchLabels: + app: ns2 + podSelector: + matchLabels: + app: p2 + ports: + - protocol: TCP + port: 6379 + egress: + - to: + - podSelector: + matchLabels: + app: p1 + - ipBlock: + cidr: 172.0.0.0/24 + ports: + - protocol: TCP + port: 5978 +- apiVersion: networking.k8s.io/v1 + kind: NetworkPolicy + metadata: + name: np3 + namespace: default + spec: + podSelector: + matchLabels: + app: p-bozo + ingress: + - from: + - ipBlock: + cidr: 172.2.0.0/16 + except: + - 172.2.1.0/24 + - namespaceSelector: + matchExpressions: + - key: app + operator: In + values: [ns-bozo] + podSelector: + matchLabels: + app: pod-bozo + ports: + - protocol: TCP + port: 6379 + egress: + - to: + - namespaceSelector: + matchLabels: + app: ns1 + - podSelector: + matchLabels: + app: p1-missing + - ipBlock: + cidr: 172.1.0.0/24 + ports: + - protocol: TCP + port: 5978 \ No newline at end of file diff --git a/internal/lint/testdata/net/np/2.yaml b/internal/lint/testdata/net/np/2.yaml new file mode 100644 index 00000000..0d088d36 --- /dev/null +++ b/internal/lint/testdata/net/np/2.yaml @@ -0,0 +1,101 @@ +apiVersion: v1 +kind: List +items: +- apiVersion: networking.k8s.io/v1 + kind: NetworkPolicy + metadata: + name: deny-all + namespace: default + spec: + podSelector: {} + policyTypes: + - Ingress + - Egress +- apiVersion: networking.k8s.io/v1 + kind: NetworkPolicy + metadata: + name: deny-all-ing + namespace: default + spec: + podSelector: {} + policyTypes: + - Ingress +- apiVersion: networking.k8s.io/v1 + kind: NetworkPolicy + metadata: + name: deny-all-eg + namespace: default + spec: + podSelector: {} + policyTypes: + - Egress +- apiVersion: networking.k8s.io/v1 + kind: NetworkPolicy + metadata: + name: allow-all + namespace: default + spec: + podSelector: {} + ingress: + - {} + egress: + - {} + policyTypes: + - Ingress + - Egress +- apiVersion: networking.k8s.io/v1 + kind: NetworkPolicy + metadata: + name: allow-all-ing + namespace: default + spec: + podSelector: {} + ingress: + - {} + policyTypes: + - Ingress +- apiVersion: networking.k8s.io/v1 + kind: NetworkPolicy + metadata: + name: allow-all-eg + namespace: default + spec: + podSelector: {} + egress: + - {} + policyTypes: + - Egress +- apiVersion: networking.k8s.io/v1 + kind: NetworkPolicy + metadata: + name: ip-block-all-ing + namespace: default + spec: + podSelector: {} + egress: + - to: + - ipBlock: + cidr: 172.2.0.0/24 + ports: + - protocol: TCP + port: 5978 + policyTypes: + - Ingress + - Egress +- apiVersion: networking.k8s.io/v1 + kind: NetworkPolicy + metadata: + name: ip-block-all-eg + namespace: default + spec: + podSelector: {} + ingress: + - from: + - ipBlock: + cidr: 172.2.0.0/24 + ports: + - protocol: TCP + port: 5978 + policyTypes: + - Ingress + - Egress \ No newline at end of file diff --git a/internal/lint/testdata/net/np/3.yaml b/internal/lint/testdata/net/np/3.yaml new file mode 100644 index 00000000..3ae4bad3 --- /dev/null +++ b/internal/lint/testdata/net/np/3.yaml @@ -0,0 +1,37 @@ +apiVersion: v1 +kind: List +items: +- apiVersion: networking.k8s.io/v1 + kind: NetworkPolicy + metadata: + name: deny-all-ing + namespace: ns1 + spec: + podSelector: {} + policyTypes: + - Ingress +- apiVersion: networking.k8s.io/v1 + kind: NetworkPolicy + metadata: + name: allow-all-egress + namespace: ns2 + spec: + # podSelector: + # matchLabels: + # app: p2 + ingress: + - from: + - namespaceSelector: + matchLabels: + app: ns2 + podSelector: + matchLabels: + app: p2 + - podSelector: + matchLabels: + app: p2 + egress: + - {} + policyTypes: + - Ingress + - Egress \ No newline at end of file diff --git a/internal/lint/testdata/net/np/a.yaml b/internal/lint/testdata/net/np/a.yaml new file mode 100644 index 00000000..f806268b --- /dev/null +++ b/internal/lint/testdata/net/np/a.yaml @@ -0,0 +1,34 @@ +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: test-network-policy + namespace: default +spec: + podSelector: + matchLabels: + role: db + policyTypes: + - Ingress + - Egress + ingress: + - from: + - ipBlock: + cidr: 172.17.0.0/16 + except: + - 172.17.1.0/24 + - namespaceSelector: + matchLabels: + project: myproject + - podSelector: + matchLabels: + role: frontend + ports: + - protocol: TCP + port: 6379 + egress: + - to: + - ipBlock: + cidr: 10.0.0.0/24 + ports: + - protocol: TCP + port: 5978 \ No newline at end of file diff --git a/internal/lint/testdata/net/np/allow-all-egr.yaml b/internal/lint/testdata/net/np/allow-all-egr.yaml new file mode 100644 index 00000000..80e532a4 --- /dev/null +++ b/internal/lint/testdata/net/np/allow-all-egr.yaml @@ -0,0 +1,10 @@ +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: allow-all-egress +spec: + podSelector: {} + egress: + - {} + policyTypes: + - Egress \ No newline at end of file diff --git a/internal/lint/testdata/net/np/allow-all-ing.yaml b/internal/lint/testdata/net/np/allow-all-ing.yaml new file mode 100644 index 00000000..1c2e7be1 --- /dev/null +++ b/internal/lint/testdata/net/np/allow-all-ing.yaml @@ -0,0 +1,10 @@ +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: allow-all-egress +spec: + podSelector: {} + egress: + - {} + policyTypes: + - Ingress \ No newline at end of file diff --git a/internal/lint/testdata/net/np/b.yaml b/internal/lint/testdata/net/np/b.yaml new file mode 100644 index 00000000..ddb47718 --- /dev/null +++ b/internal/lint/testdata/net/np/b.yaml @@ -0,0 +1,34 @@ +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: np-b + namespace: default +spec: + ingress: + - from: + - ipBlock: + cidr: 172.17.0.0/16 + except: + - 172.17.1.0/24 + - namespaceSelector: + matchLabels: + ns: ns1 + - podSelector: + matchLabels: + po: po1 + ports: + - protocol: TCP + port: 6379 + egress: + - to: + - namespaceSelector: + matchLabels: + ns: ns1 + - podSelector: + matchLabels: + po: po1 + - ipBlock: + cidr: 10.0.0.0/24 + ports: + - protocol: TCP + port: 5978 \ No newline at end of file diff --git a/internal/lint/testdata/net/np/c.yaml b/internal/lint/testdata/net/np/c.yaml new file mode 100644 index 00000000..5ef86a89 --- /dev/null +++ b/internal/lint/testdata/net/np/c.yaml @@ -0,0 +1,34 @@ +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: np-c + namespace: default +spec: + ingress: + - from: + - ipBlock: + cidr: 172.17.0.0/16 + except: + - 172.17.1.0/24 + - namespaceSelector: + matchLabels: + ns: ns1 + - podSelector: + matchLabels: + po: p1-missing + ports: + - protocol: TCP + port: 6379 + egress: + - to: + - namespaceSelector: + matchLabels: + ns: ns1 + - podSelector: + matchLabels: + po: p1-missing + - ipBlock: + cidr: 10.0.0.0/24 + ports: + - protocol: TCP + port: 5978 \ No newline at end of file diff --git a/internal/lint/testdata/net/np/d.yaml b/internal/lint/testdata/net/np/d.yaml new file mode 100644 index 00000000..4d03a99f --- /dev/null +++ b/internal/lint/testdata/net/np/d.yaml @@ -0,0 +1,13 @@ +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: np-d + namespace: default +spec: + podSelector: + podSelector: + matchLabels: + role: db + policyTypes: + - Ingress + - Egress \ No newline at end of file diff --git a/internal/lint/testdata/net/np/deny-all-egr.yaml b/internal/lint/testdata/net/np/deny-all-egr.yaml new file mode 100644 index 00000000..6be3087c --- /dev/null +++ b/internal/lint/testdata/net/np/deny-all-egr.yaml @@ -0,0 +1,8 @@ +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: deny-all-eg +spec: + podSelector: {} + policyTypes: + - Egress \ No newline at end of file diff --git a/internal/lint/testdata/net/np/deny-all-ing.yaml b/internal/lint/testdata/net/np/deny-all-ing.yaml new file mode 100644 index 00000000..ba04fc9a --- /dev/null +++ b/internal/lint/testdata/net/np/deny-all-ing.yaml @@ -0,0 +1,8 @@ +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: deny-all-ing +spec: + podSelector: {} + policyTypes: + - Ingress \ No newline at end of file diff --git a/internal/lint/testdata/net/np/deny-all.yaml b/internal/lint/testdata/net/np/deny-all.yaml new file mode 100644 index 00000000..a7983980 --- /dev/null +++ b/internal/lint/testdata/net/np/deny-all.yaml @@ -0,0 +1,9 @@ +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: deny-all +spec: + podSelector: {} + policyTypes: + - Ingress + - Egress \ No newline at end of file diff --git a/internal/lint/testdata/pol/pdb/1.yaml b/internal/lint/testdata/pol/pdb/1.yaml new file mode 100644 index 00000000..e9a49fa0 --- /dev/null +++ b/internal/lint/testdata/pol/pdb/1.yaml @@ -0,0 +1,54 @@ +--- +apiVersion: v1 +kind: List +items: + - apiVersion: policy/v1 + kind: PodDisruptionBudget + metadata: + name: pdb1 + namespace: default + spec: + minAvailable: 2 + selector: + matchLabels: + app: p1 + - apiVersion: policy/v1 + kind: PodDisruptionBudget + metadata: + name: pdb2 + namespace: default + spec: + minAvailable: 1 + selector: + matchLabels: + app: p2 + - apiVersion: policy/v1 + kind: PodDisruptionBudget + metadata: + name: pdb3 + namespace: default + spec: + minAvailable: 1 + selector: + matchLabels: + app: test4 + - apiVersion: policy/v1 + kind: PodDisruptionBudget + metadata: + name: pdb4 + namespace: default + spec: + minAvailable: 1 + selector: + matchLabels: + app: test5 + - apiVersion: policy/v1 + kind: PodDisruptionBudget + metadata: + name: pdb4-1 + namespace: default + spec: + minAvailable: 1 + selector: + matchLabels: + app: test5 diff --git a/internal/sanitize/types.go b/internal/lint/types.go similarity index 85% rename from internal/sanitize/types.go rename to internal/lint/types.go index 11b073d9..86fc6e2f 100644 --- a/internal/sanitize/types.go +++ b/internal/lint/types.go @@ -1,12 +1,13 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright Authors of Popeye -package sanitize +package lint import ( "context" "github.com/derailed/popeye/internal/issues" + "github.com/derailed/popeye/internal/rules" "github.com/derailed/popeye/pkg/config" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -19,10 +20,13 @@ type Collector interface { Outcome() issues.Outcome // AddSubCode records a sub issue. - AddSubCode(ctx context.Context, id config.ID, args ...interface{}) + AddSubCode(context.Context, rules.ID, ...interface{}) // AddCode records a new issue. - AddCode(ctx context.Context, id config.ID, args ...interface{}) + AddCode(context.Context, rules.ID, ...interface{}) + + // AddErr records errors. + AddErr(context.Context, ...error) } // PodsMetricsLister handles pods metrics. diff --git a/internal/report/html.go b/internal/report/assets/report.html similarity index 64% rename from internal/report/html.go rename to internal/report/assets/report.html index 08a52914..ea2df526 100644 --- a/internal/report/html.go +++ b/internal/report/assets/report.html @@ -1,12 +1,6 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package report - -var htmlTemplate = ` - Popeye Sanitizer Report + Popeye Scan Report -
-
Popeye Sanitizer Report
+
+
Popeye Scan Report
Scanned - {{ .ClusterName }} + {{ .ClusterName }}/{{ .ContextName }}
{{ .Report.Timestamp }}
@@ -203,14 +158,10 @@ {{ .Report.Score }}
- - {{ range $section := .Report.Sections }} + {{ range $section := .Report.ListSections -}}

-
- {{ $count := len $section.Outcome }} - {{ toTitle $section.Title $count }} -
+
{{ $count := len $section.Outcome -}} {{ toTitle $section.Title $count -}}
{{ $section.Tally.MarshalYAML.Error }} {{ $section.Tally.MarshalYAML.Warn }} @@ -219,59 +170,44 @@ {{ $section.Tally.Score }}%
    - {{ range $issueName, $issues := $section.Outcome }} + {{ range $issueName, $issues := $section.Outcome -}} + {{ if not $issues.HasIssues -}} + {{ continue -}} + {{ end -}} +
  • -
    - {{ $issueName }} -
    -
    - -
    +
    {{ $issueName -}}
    +
      - {{ $group := "" }} - {{ range $_, $issue := $issues.Sort 0 }} - {{ if isRoot $issue.Group }} -
    • - - {{ $issue.Message }} - -
    • - {{ else }} - {{ if ne $group $issue.Group }} - {{ if ne $group ""}} + {{ $group := "" -}} + {{ range $_, $issue := $issues.Sort 0 -}} + {{ if isRoot $issue.Group -}} +
    • {{ $issue.Message -}}
    • + {{ else -}} + {{ if ne $group $issue.Group -}} + {{ if ne $group "" -}}
    - {{ end }} - {{ $group = $issue.Group }} -
  • - {{ $issue.Group }} + {{ end -}}
  • -
      -
    • - - {{ $issue.Message }} - -
    • - {{ else }} -
    • - - {{ $issue.Message }} - -
    • - {{ end }} - {{ end }} - {{ end }} - {{ if ne $group ""}} + {{ $group = $issue.Group -}} +
    • {{ $issue.Group -}}
    • +
        +
      • {{ $issue.Message }}
      • + {{ else -}} +
      • {{ $issue.Message }}
      • + {{ end -}} + {{ end -}} + {{ end -}} + {{ if ne $group "" -}} +
      + {{ end -}}
    - {{ end }} -
- - {{ end }} + + {{ end -}} -
- {{ end }} +
+ {{ end -}}
- - -` + \ No newline at end of file diff --git a/internal/report/builder.go b/internal/report/builder.go index 5ae9b55c..9fe1c6fd 100644 --- a/internal/report/builder.go +++ b/internal/report/builder.go @@ -4,6 +4,9 @@ package report import ( + _ "embed" + "slices" + "bytes" "encoding/json" "fmt" @@ -12,82 +15,20 @@ import ( "text/template" "time" - "github.com/derailed/popeye/internal/client" "github.com/derailed/popeye/internal/issues" + "github.com/derailed/popeye/internal/rules" "github.com/derailed/popeye/pkg/config" - "github.com/fvbommel/sortorder" + "github.com/derailed/popeye/types" "github.com/prometheus/client_golang/prometheus/push" + "github.com/rs/zerolog/log" "gopkg.in/yaml.v2" ) -const ( - // DefaultFormat dumps sanitizer with color, emojis, the works. - DefaultFormat = "standard" - - // JurassicFormat dumps sanitizer with 0 fancyness. - JurassicFormat = "jurassic" - - // YAMLFormat dumps sanitizer as YAML. - YAMLFormat = "yaml" - - // JSONFormat dumps sanitizer as JSON. - JSONFormat = "json" - - // HTMLFormat dumps sanitizer as HTML - HTMLFormat = "html" - - // JunitFormat dumps sanitizer as JUnit report. - JunitFormat = "junit" - - // PrometheusFormat pushes sanitizer as Prometheus metrics. - PrometheusFormat = "prometheus" - - // ScoreFormat pushes sanitizer as the value of the Score. - ScoreFormat = "score" -) - -// Builder represents sanitizer +// Builder tracks a scan report output. type Builder struct { Report Report `json:"popeye" yaml:"popeye"` - clusterName string -} - -// Report represents the output of a sanitization pass. -type Report struct { - Timestamp string `json:"report_time" yaml:"report_time"` - Score int `json:"score" yaml:"score"` - Grade string `json:"grade" yaml:"grade"` - Sections Sections `json:"sanitizers,omitempty" yaml:"sanitizers,omitempty"` - Errors []error `json:"errors,omitempty" yaml:"errors,omitempty"` - sectionsCount int - totalScore int -} - -// Sections represents a collection of sections. -type Sections []Section - -// Section represents a sanitizer pass -type Section struct { - Title string `json:"sanitizer" yaml:"sanitizer"` - GVR string `json:"gvr" yaml:"gvr"` - Tally *Tally `json:"tally" yaml:"tally"` - Outcome issues.Outcome `json:"issues,omitempty" yaml:"issues,omitempty"` - singular string -} - -// Len returns the list size. -func (s Sections) Len() int { - return len(s) -} - -// Swap swaps list values. -func (s Sections) Swap(i, j int) { - s[i], s[j] = s[j], s[i] -} - -// Less returns true if i < j. -func (s Sections) Less(i, j int) bool { - return sortorder.NaturalLess(s[i].singular, s[j].singular) + ClusterName string + ContextName string } // NewBuilder returns a new instance. @@ -95,35 +36,25 @@ func NewBuilder() *Builder { return &Builder{} } -// SetClusterName sets the current cluster name. -func (b *Builder) SetClusterName(s string) { +// SetClusterContext sets the current cluster name. +func (b *Builder) SetClusterContext(cl, ct string) { sort.Sort(b.Report.Sections) - b.clusterName = s + b.ClusterName, b.ContextName = cl, ct b.Report.Timestamp = time.Now().Format(time.RFC3339) } -// ClusterName returns the cluster name. -func (b *Builder) ClusterName() string { - return b.clusterName -} - -// // Timestamp returns the report time. -// func (b *Builder) Timestamp() string { -// return b.timeStamp -// } - // HasContent checks if we actually have anything to report. func (b *Builder) HasContent() bool { return b.Report.sectionsCount != 0 } -// AddError record an error associted with the report. +// AddError record an error associated with the report. func (b *Builder) AddError(err error) { b.Report.Errors = append(b.Report.Errors, err) } -// AddSection adds a sanitizer section to the report. -func (b *Builder) AddSection(gvr client.GVR, singular string, o issues.Outcome, t *Tally) { +// AddSection adds a linter section to the report. +func (b *Builder) AddSection(gvr types.GVR, singular string, o issues.Outcome, t *Tally) { section := Section{ Title: strings.ToLower(gvr.R()), GVR: gvr.String(), @@ -138,8 +69,8 @@ func (b *Builder) AddSection(gvr client.GVR, singular string, o issues.Outcome, } } -// ToJunit dumps sanitizer to JUnit. -func (b *Builder) ToJunit(level config.Level) (string, error) { +// ToJunit dumps scan to JUnit. +func (b *Builder) ToJunit(level rules.Level) (string, error) { b.finalize() raw, err := junitMarshal(b, level) if err != nil { @@ -155,7 +86,7 @@ func (b *Builder) finalize() { b.Report.Grade = Grade(score) } -// ToYAML dumps sanitizer to YAML. +// ToYAML dumps scan to YAML. func (b *Builder) ToYAML() (string, error) { b.finalize() raw, err := yaml.Marshal(b) @@ -166,7 +97,7 @@ func (b *Builder) ToYAML() (string, error) { return string(raw), nil } -// ToJSON dumps sanitizer to JSON. +// ToJSON dumps scan to JSON. func (b *Builder) ToJSON() (string, error) { b.finalize() raw, err := json.Marshal(b) @@ -177,7 +108,7 @@ func (b *Builder) ToJSON() (string, error) { return string(raw), nil } -// ToHTML dumps sanitizer to HTML. +// ToHTML dumps scan to HTML. func (b *Builder) ToHTML() (string, error) { b.finalize() @@ -185,8 +116,9 @@ func (b *Builder) ToHTML() (string, error) { "toEmoji": toEmoji, "toTitle": Titleize, "isRoot": isRoot, + "list": b.Report.ListSections, } - tpl, err := template.New("sanitize").Funcs(fMap).Parse(htmlTemplate) + tpl, err := template.New("sanitize").Funcs(fMap).Parse(htmlReport) if err != nil { return "", err } @@ -200,23 +132,27 @@ func (b *Builder) ToHTML() (string, error) { } // ToPrometheus returns prometheus pusher. -func (b *Builder) ToPrometheus(gtwy *config.PushGateway, namespace string) *push.Pusher { +func (b *Builder) ToPrometheus(gtwy *config.PushGateway, instance, ns, asset string, cc rules.Glossary) *push.Pusher { b.finalize() - if namespace == "" { - namespace = "all" + + log.Debug().Msgf("Pushing prom metrics from instance: %q", instance) + p := newPusher(gtwy, instance) + if ns == "" { + ns = "all" } + b.promCollect(ns, asset, cc) - return prometheusMarshal(b, gtwy, b.clusterName, namespace) + return p } -// ToScore dumps sanitizer to only the score value. +// ToScore dumps scan to only the score value. func (b *Builder) ToScore() (int, error) { b.finalize() return b.Report.Score, nil } // PrintSummary print outs summary report to screen. -func (b *Builder) PrintSummary(s *Sanitizer) { +func (b *Builder) PrintSummary(s *ScanReport) { if b.Report.sectionsCount == 0 { return } @@ -224,8 +160,7 @@ func (b *Builder) PrintSummary(s *Sanitizer) { b.finalize() s.Open("SUMMARY", nil) { - fmt.Fprintf(s, "Generated on: %s\n", b.Report.Timestamp) - fmt.Fprintf(s, "Your cluster score: %d -- %s\n", b.Report.Score, b.Report.Grade) + fmt.Fprint(s, s.Color(fmt.Sprintf("%-19s %s (%d)\n", "Your cluster score:", b.Report.Grade, b.Report.Score), ColorAqua)) for _, l := range s.Badge(b.Report.Score) { fmt.Fprintf(s, "%s%s\n", strings.Repeat(" ", Width-20), l) } @@ -233,25 +168,29 @@ func (b *Builder) PrintSummary(s *Sanitizer) { s.Close() } -// PrintContextInfo displays cluster information. -func (b *Builder) PrintContextInfo(s *Sanitizer, contextName string, metrics bool) { - if contextName == "" { - contextName = "n/a" +// PrintClusterInfo displays cluster information. +func (b *Builder) PrintClusterInfo(s *ScanReport, metrics bool) { + cl, ct := b.ClusterName, b.ContextName + if cl == "" { + cl = "n/a" + } + if ct == "" { + ct = "n/a" } - s.Open(Titleize(fmt.Sprintf("General [%s]", contextName), -1), nil) + s.Open(Titleize(fmt.Sprintf("General [%s/%s] (%s)", cl, ct, b.Report.Timestamp), -1), nil) { - s.Print(config.OkLevel, 1, "Connectivity") + s.Print(rules.OkLevel, 1, "Connectivity") if metrics { - s.Print(config.OkLevel, 1, "MetricServer") + s.Print(rules.OkLevel, 1, "MetricServer") } else { - s.Print(config.ErrorLevel, 1, "MetricServer") + s.Print(rules.ErrorLevel, 1, "MetricServer") } } s.Close() } // PrintHeader prints report header to screen. -func (b *Builder) PrintHeader(s *Sanitizer) { +func (b *Builder) PrintHeader(s *ScanReport) { fmt.Fprintln(s) for i, l := range Logo { switch { @@ -269,24 +208,24 @@ func (b *Builder) PrintHeader(s *Sanitizer) { fmt.Fprintln(s, "") } -// PrintReport prints out sanitizer report to screen -func (b *Builder) PrintReport(level config.Level, s *Sanitizer) { +// PrintReport prints out scan report to screen +func (b *Builder) PrintReport(level rules.Level, s *ScanReport) { for _, section := range b.Report.Sections { var any bool s.Open(Titleize(section.Title, len(section.Outcome)), section.Tally) { - keys := make([]string, 0, len(section.Outcome)) + kk := make([]string, 0, len(section.Outcome)) for k := range section.Outcome { - keys = append(keys, k) + kk = append(kk, k) } - sort.Strings(keys) + slices.SortFunc(kk, issues.SortKeys) - for _, res := range keys { + for _, res := range kk { ii := section.Outcome[res] if len(ii) == 0 { - if level <= config.OkLevel { + if level <= rules.OkLevel { any = true - s.Print(config.OkLevel, 1, res) + s.Print(rules.OkLevel, 1, res) } continue } @@ -320,15 +259,15 @@ func isRoot(g string) bool { return g == issues.Root } -func toEmoji(level config.Level) (s string) { +func toEmoji(level rules.Level) (s string) { switch level { - case config.ErrorLevel: + case rules.ErrorLevel: s = "fas fa-bomb" - case config.WarnLevel: + case rules.WarnLevel: s = "fas fa-radiation-alt" - case config.InfoLevel: + case rules.InfoLevel: s = "fas fa-info-circle" - case config.OkLevel: + case rules.OkLevel: s = "far fa-check-circle" default: s = "fas fa-info-circle" diff --git a/internal/report/builder_test.go b/internal/report/builder_test.go index 510363da..979a7946 100644 --- a/internal/report/builder_test.go +++ b/internal/report/builder_test.go @@ -6,13 +6,12 @@ package report_test import ( "bytes" "errors" - "strings" "testing" - "github.com/derailed/popeye/internal/client" "github.com/derailed/popeye/internal/issues" "github.com/derailed/popeye/internal/report" - "github.com/derailed/popeye/pkg/config" + "github.com/derailed/popeye/internal/rules" + "github.com/derailed/popeye/types" "github.com/stretchr/testify/assert" ) @@ -20,12 +19,12 @@ func TestBuilderHtml(t *testing.T) { b, ta := report.NewBuilder(), report.NewTally() o := issues.Outcome{ "blee": issues.Issues{ - issues.New(client.NewGVR("fred"), issues.Root, config.OkLevel, "Blah"), + issues.New(types.NewGVR("fred"), issues.Root, rules.OkLevel, "Blah"), }, } ta.Rollup(o) - b.AddSection(client.NewGVR("fred"), "fred", o, ta) + b.AddSection(types.NewGVR("fred"), "fred", o, ta) b.AddError(errors.New("boom")) s, err := b.ToHTML() @@ -37,14 +36,14 @@ func TestBuilderJunit(t *testing.T) { b, ta := report.NewBuilder(), report.NewTally() o := issues.Outcome{ "blee": issues.Issues{ - issues.New(client.NewGVR("fred"), issues.Root, config.OkLevel, "Blah"), + issues.New(types.NewGVR("fred"), issues.Root, rules.OkLevel, "Blah"), }, } ta.Rollup(o) - b.AddSection(client.NewGVR("fred"), "fred", o, ta) + b.AddSection(types.NewGVR("fred"), "fred", o, ta) b.AddError(errors.New("boom")) - s, err := b.ToJunit(config.OkLevel) + s, err := b.ToJunit(rules.OkLevel) assert.Nil(t, err) assert.Equal(t, reportJunit, s) @@ -54,12 +53,12 @@ func TestBuilderYAML(t *testing.T) { b, ta := report.NewBuilder(), report.NewTally() o := issues.Outcome{ "blee": issues.Issues{ - issues.New(client.NewGVR("fred"), issues.Root, config.OkLevel, "Blah"), + issues.New(types.NewGVR("fred"), issues.Root, rules.OkLevel, "Blah"), }, } ta.Rollup(o) - b.AddSection(client.NewGVR("fred"), "fred", o, ta) + b.AddSection(types.NewGVR("fred"), "fred", o, ta) b.AddError(errors.New("boom")) s, err := b.ToYAML() @@ -71,12 +70,12 @@ func TestBuilderJSON(t *testing.T) { b, ta := report.NewBuilder(), report.NewTally() o := issues.Outcome{ "blee": issues.Issues{ - issues.New(client.NewGVR("fred"), issues.Root, config.OkLevel, "Blah"), + issues.New(types.NewGVR("fred"), issues.Root, rules.OkLevel, "Blah"), }, } ta.Rollup(o) - b.AddSection(client.NewGVR("fred"), "fred", o, ta) + b.AddSection(types.NewGVR("fred"), "fred", o, ta) b.AddError(errors.New("boom")) s, err := b.ToJSON() @@ -88,16 +87,16 @@ func TestPrintSummary(t *testing.T) { b, ta := report.NewBuilder(), report.NewTally() o := issues.Outcome{ "blee": issues.Issues{ - issues.New(client.NewGVR("fred"), issues.Root, config.OkLevel, "Blah"), + issues.New(types.NewGVR("fred"), issues.Root, rules.OkLevel, "Blah"), }, } ta.Rollup(o) - b.AddSection(client.NewGVR("fred"), "fred", o, ta) + b.AddSection(types.NewGVR("fred"), "fred", o, ta) b.AddError(errors.New("boom")) buff := bytes.NewBuffer([]byte("")) - san := report.NewSanitizer(buff, false) + san := report.New(buff, false) b.PrintSummary(san) assert.Equal(t, summaryExp, buff.String()) @@ -107,16 +106,16 @@ func TestPrintHeader(t *testing.T) { b, ta := report.NewBuilder(), report.NewTally() o := issues.Outcome{ "blee": issues.Issues{ - issues.New(client.NewGVR("fred"), issues.Root, config.OkLevel, "Blah"), + issues.New(types.NewGVR("fred"), issues.Root, rules.OkLevel, "Blah"), }, } ta.Rollup(o) - b.AddSection(client.NewGVR("fred"), "fred", o, ta) + b.AddSection(types.NewGVR("fred"), "fred", o, ta) b.AddError(errors.New("boom")) buff := bytes.NewBuffer([]byte("")) - san := report.NewSanitizer(buff, false) + san := report.New(buff, false) b.PrintHeader(san) assert.Equal(t, headerExp, buff.String()) @@ -126,17 +125,17 @@ func TestPrintReport(t *testing.T) { b, ta := report.NewBuilder(), report.NewTally() o := issues.Outcome{ "blee": issues.Issues{ - issues.New(client.NewGVR("fred"), issues.Root, config.OkLevel, "Blah"), + issues.New(types.NewGVR("fred"), issues.Root, rules.OkLevel, "Blah"), }, } ta.Rollup(o) - b.AddSection(client.NewGVR("fred"), "fred", o, ta) + b.AddSection(types.NewGVR("fred"), "fred", o, ta) b.AddError(errors.New("boom")) buff := bytes.NewBuffer([]byte("")) - san := report.NewSanitizer(buff, false) - b.PrintReport(config.OkLevel, san) + san := report.New(buff, false) + b.PrintReport(rules.OkLevel, san) assert.Equal(t, reportExp, buff.String()) } @@ -163,34 +162,11 @@ func TestTitleize(t *testing.T) { // Helpers... var ( - reportHTML = "\n\n\n Popeye Sanitizer Report\n \n\n\n\n\n
\n
Popeye Sanitizer Report
\n
\n \n \n \n
\n Scanned\n \n
\n
\n
\n A\n 100 \n
\n
\n\n \n
\n
\n
\n \n FRED (1 SCANNED)\n
\n
\n 0 \n 0 \n 0 \n 1 \n 100%\n
\n
    \n \n
  • \n
    \n blee\n
    \n
    \n \n
    \n
    \n
      \n \n \n \n
    • \n \n Blah\n \n
    • \n \n \n \n
    \n
  • \n \n
\n
\n \n
\n\n\n\n" + reportHTML = "\n\n Popeye Scan Report\n \n\n\n\n\n
\n
Popeye Scan Report
\n
\n \n \n \n
\n Scanned\n /\n
\n
\n
\n A\n 100 \n
\n
\n
\n
\n
FRED (1 SCANNED)
\n
\n 0 \n 0 \n 0 \n 1 \n 100%\n
\n
    \n
  • \n
    blee
    \n
    \n
    \n
      \n
    • Blah
    • \n
    \n
  • \n
\n
\n
\n\n" reportJunit = "\n\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t\n\t\n" - reportJSON = "{\"popeye\":{\"report_time\":\"\",\"score\":100,\"grade\":\"A\",\"sanitizers\":[{\"sanitizer\":\"fred\",\"gvr\":\"fred\",\"tally\":{\"ok\":1,\"info\":0,\"warning\":0,\"error\":0,\"score\":100},\"issues\":{\"blee\":[{\"group\":\"__root__\",\"gvr\":\"fred\",\"level\":0,\"message\":\"Blah\"}]}}],\"errors\":[{}]}}" - - reportYAML = `popeye: - report_time: "" - score: 100 - grade: A - sanitizers: - - sanitizer: fred - gvr: fred - tally: - ok: 1 - info: 0 - warning: 0 - error: 0 - score: 100 - issues: - blee: - - group: __root__ - gvr: fred - level: 0 - message: Blah - errors: - - {} -` - - summaryExp = "\n\x1b[38;5;75mSUMMARY\x1b[0m\n\x1b[38;5;75m┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅\x1b[0m\nGenerated on: \nYour cluster score: 100 -- A\n \x1b[38;5;82mo .-'-. \x1b[0m\n \x1b[38;5;82m o __| A `\\ \x1b[0m\n \x1b[38;5;82m o `-,-`--._ `\\\x1b[0m\n \x1b[38;5;82m [] .->' a `|-'\x1b[0m\n \x1b[38;5;82m `=/ (__/_ / \x1b[0m\n \x1b[38;5;82m \\_, ` _) \x1b[0m\n \x1b[38;5;82m `----; | \x1b[0m\n\n" - headerExp = "\n\x1b[38;5;122m ___ ___ _____ _____ \x1b[0m \x1b[38;5;75mK .-'-. \x1b[0m\n\x1b[38;5;122m| _ \\___| _ \\ __\\ \\ / / __|\x1b[0m \x1b[38;5;75m 8 __| `\\ \x1b[0m\n\x1b[38;5;122m| _/ _ \\ _/ _| \\ V /| _| \x1b[0m \x1b[38;5;75m s `-,-`--._ `\\\x1b[0m\n\x1b[38;5;122m|_| \\___/_| |___| |_| |___|\x1b[0m \x1b[38;5;75m [] .->' a `|-'\x1b[0m\n\x1b[38;5;75m Biffs`em and Buffs`em!\x1b[0m \x1b[38;5;75m `=/ (__/_ / \x1b[0m\n \x1b[38;5;75m \\_, ` _) \x1b[0m\n \x1b[38;5;75m `----; | \x1b[0m\n\n" - reportExp = "\n\x1b[38;5;75mFRED (1 SCANNED)\x1b[0m" + strings.Repeat(" ", 61) + "💥 0 😱 0 🔊 0 ✅ 1 \x1b[38;5;122m100\x1b[0m٪\n\x1b[38;5;75m" + strings.Repeat("┅", 101) + "\x1b[0m\n · \x1b[38;5;155mblee\x1b[0m\x1b[38;5;250m" + strings.Repeat(".", 91) + "\x1b[0m✅\n ✅ \x1b[38;5;155mBlah.\x1b[0m\n\n" + reportJSON = "{\"popeye\":{\"report_time\":\"\",\"score\":100,\"grade\":\"A\",\"sections\":[{\"linter\":\"fred\",\"gvr\":\"fred\",\"tally\":{\"ok\":1,\"info\":0,\"warning\":0,\"error\":0,\"score\":100},\"issues\":{\"blee\":[{\"group\":\"__root__\",\"gvr\":\"fred\",\"level\":0,\"message\":\"Blah\"}]}}],\"errors\":{\"error\":\"boom\"}},\"ClusterName\":\"\",\"ContextName\":\"\"}" + reportYAML = "popeye:\n report_time: \"\"\n score: 100\n grade: A\n sections:\n - linter: fred\n gvr: fred\n tally:\n ok: 1\n info: 0\n warning: 0\n error: 0\n score: 100\n issues:\n blee:\n - group: __root__\n gvr: fred\n level: 0\n message: Blah\n errors:\n - boom\nclustername: \"\"\ncontextname: \"\"\n" + summaryExp = "\n\x1b[38;5;75mSUMMARY\x1b[0m\n\x1b[38;5;75m┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅\x1b[0m\n\x1b[38;5;122mYour cluster score: A (100)\n\x1b[0m \x1b[38;5;82mo .-'-. \x1b[0m\n \x1b[38;5;82m o __| A `\\ \x1b[0m\n \x1b[38;5;82m o `-,-`--._ `\\\x1b[0m\n \x1b[38;5;82m [] .->' a `|-'\x1b[0m\n \x1b[38;5;82m `=/ (__/_ / \x1b[0m\n \x1b[38;5;82m \\_, ` _) \x1b[0m\n \x1b[38;5;82m `----; | \x1b[0m\n\n" + headerExp = "\n\x1b[38;5;122m ___ ___ _____ _____ \x1b[0m \x1b[38;5;75mK .-'-. \x1b[0m\n\x1b[38;5;122m| _ \\___| _ \\ __\\ \\ / / __|\x1b[0m \x1b[38;5;75m 8 __| `\\ \x1b[0m\n\x1b[38;5;122m| _/ _ \\ _/ _| \\ V /| _| \x1b[0m \x1b[38;5;75m s `-,-`--._ `\\\x1b[0m\n\x1b[38;5;122m|_| \\___/_| |___| |_| |___|\x1b[0m \x1b[38;5;75m [] .->' a `|-'\x1b[0m\n\x1b[38;5;75m Biffs`em and Buffs`em!\x1b[0m \x1b[38;5;75m `=/ (__/_ / \x1b[0m\n \x1b[38;5;75m \\_, ` _) \x1b[0m\n \x1b[38;5;75m `----; | \x1b[0m\n\n" + reportExp = "\n\x1b[38;5;75mFRED (1 SCANNED)\x1b[0m 💥 0 😱 0 🔊 0 ✅ 1 \x1b[38;5;122m100\x1b[0m٪\n\x1b[38;5;75m┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅┅\x1b[0m\n · \x1b[38;5;155mblee\x1b[0m\x1b[38;5;250m...........................................................................................\x1b[0m✅\n ✅ \x1b[38;5;155mBlah.\x1b[0m\n\n" ) diff --git a/internal/report/color.go b/internal/report/color.go index 699088ed..92068d3d 100644 --- a/internal/report/color.go +++ b/internal/report/color.go @@ -7,7 +7,7 @@ import ( "fmt" "strconv" - "github.com/derailed/popeye/pkg/config" + "github.com/derailed/popeye/internal/rules" ) // Color ANSI palette (256!) @@ -42,15 +42,15 @@ func Colorize(s string, c Color) string { return "\033[38;5;" + strconv.Itoa(int(c)) + "m" + s + "\033[0m" } -func colorForLevel(l config.Level) Color { +func colorForLevel(l rules.Level) Color { switch l { - case config.ErrorLevel: + case rules.ErrorLevel: return ColorRed - case config.WarnLevel: + case rules.WarnLevel: return ColorOrangish - case config.InfoLevel: + case rules.InfoLevel: return ColorAqua - case config.OkLevel: + case rules.OkLevel: return ColorDarkOlive default: return ColorLighSlate diff --git a/internal/report/color_test.go b/internal/report/color_test.go index e820299a..87a33724 100644 --- a/internal/report/color_test.go +++ b/internal/report/color_test.go @@ -6,7 +6,7 @@ package report import ( "testing" - "github.com/derailed/popeye/pkg/config" + "github.com/derailed/popeye/internal/rules" "github.com/stretchr/testify/assert" ) @@ -20,6 +20,6 @@ func TestColorForLevel(t *testing.T) { } for k, v := range colors { - assert.Equal(t, v, colorForLevel(config.Level(k))) + assert.Equal(t, v, colorForLevel(rules.Level(k))) } } diff --git a/internal/report/delta_score.go b/internal/report/delta_score.go index 6a1d830c..7cf26caa 100644 --- a/internal/report/delta_score.go +++ b/internal/report/delta_score.go @@ -3,9 +3,7 @@ package report -import ( - "github.com/derailed/popeye/pkg/config" -) +import "github.com/derailed/popeye/internal/rules" const ( noChange = "not changed" @@ -15,13 +13,13 @@ const ( // DeltaScore tracks delta between 2 tally scores. type DeltaScore struct { - level config.Level + level rules.Level s1, s2 int inverse bool } // NewDeltaScore returns a new delta score. -func NewDeltaScore(level config.Level, s1, s2 int, inverse bool) DeltaScore { +func NewDeltaScore(level rules.Level, s1, s2 int, inverse bool) DeltaScore { return DeltaScore{ s1: s1, s2: s2, diff --git a/internal/report/delta_score_test.go b/internal/report/delta_score_test.go index 026521f7..a1c449c9 100644 --- a/internal/report/delta_score_test.go +++ b/internal/report/delta_score_test.go @@ -6,7 +6,7 @@ package report import ( "testing" - "github.com/derailed/popeye/pkg/config" + "github.com/derailed/popeye/internal/rules" "github.com/stretchr/testify/assert" ) @@ -35,7 +35,7 @@ func TestChanged(t *testing.T) { }, } - l := config.OkLevel + l := rules.OkLevel for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { @@ -77,7 +77,7 @@ func TestBetter(t *testing.T) { }, } - l := config.OkLevel + l := rules.OkLevel for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { @@ -119,7 +119,7 @@ func TestWorst(t *testing.T) { }, } - l := config.OkLevel + l := rules.OkLevel for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { @@ -164,7 +164,7 @@ func TestSummarize(t *testing.T) { }, } - l := config.OkLevel + l := rules.OkLevel for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { diff --git a/internal/report/emoji.go b/internal/report/emoji.go index 46386750..f2646b91 100644 --- a/internal/report/emoji.go +++ b/internal/report/emoji.go @@ -4,11 +4,11 @@ package report import ( - "github.com/derailed/popeye/pkg/config" + "github.com/derailed/popeye/internal/rules" ) const ( - containerLevel config.Level = 100 + containerLevel rules.Level = 100 ) var emojis = map[string]string{ @@ -28,17 +28,17 @@ var emojisUgry = map[string]string{ } // EmojiForLevel maps lint levels to emojis. -func EmojiForLevel(l config.Level, jurassic bool) string { +func EmojiForLevel(l rules.Level, jurassic bool) string { var key string // nolint:exhaustive switch l { case containerLevel: key = "container" - case config.ErrorLevel: + case rules.ErrorLevel: key = "farfromfok" - case config.WarnLevel: + case rules.WarnLevel: key = "warn" - case config.InfoLevel: + case rules.InfoLevel: key = "fyi" default: key = "peachy" diff --git a/internal/report/emoji_test.go b/internal/report/emoji_test.go index b7255cf2..a17e65ac 100644 --- a/internal/report/emoji_test.go +++ b/internal/report/emoji_test.go @@ -7,18 +7,18 @@ import ( "testing" "unicode/utf8" - "github.com/derailed/popeye/pkg/config" + "github.com/derailed/popeye/internal/rules" "github.com/stretchr/testify/assert" ) func TestEmojiForLevel(t *testing.T) { for k, v := range map[int]int{0: 1, 1: 1, 2: 1, 3: 1, 4: 1, 100: 1} { - assert.Equal(t, v, utf8.RuneCountInString(EmojiForLevel(config.Level(k), false))) + assert.Equal(t, v, utf8.RuneCountInString(EmojiForLevel(rules.Level(k), false))) } } func TestEmojiUgry(t *testing.T) { for k, v := range map[int]string{0: "OK", 1: "I", 2: "W", 3: "E", 100: "C"} { - assert.Equal(t, v, EmojiForLevel(config.Level(k), true)) + assert.Equal(t, v, EmojiForLevel(rules.Level(k), true)) } } diff --git a/internal/report/grade.go b/internal/report/grade.go index 7342d2ff..460e127f 100644 --- a/internal/report/grade.go +++ b/internal/report/grade.go @@ -24,7 +24,7 @@ func Grade(score int) string { } // Badge returns a popeye grade. -func (s *Sanitizer) Badge(score int) []string { +func (s *ScanReport) Badge(score int) []string { ic := make([]string, len(GraderLogo)) for i, l := range GraderLogo { switch i { diff --git a/internal/report/grade_test.go b/internal/report/grade_test.go index d973ef8d..f0ae5a21 100644 --- a/internal/report/grade_test.go +++ b/internal/report/grade_test.go @@ -41,7 +41,7 @@ func TestBadge(t *testing.T) { }, } - s := new(Sanitizer) + s := new(ScanReport) for _, u := range uu { assert.Equal(t, u.e, strings.Join(s.Badge(u.score), "\n")) } diff --git a/internal/report/junit.go b/internal/report/junit.go index 9442cecf..735f94ec 100644 --- a/internal/report/junit.go +++ b/internal/report/junit.go @@ -10,7 +10,7 @@ import ( "strings" "github.com/derailed/popeye/internal/issues" - "github.com/derailed/popeye/pkg/config" + "github.com/derailed/popeye/internal/rules" ) // TestSuites a collection of junit test suites. @@ -64,7 +64,7 @@ type Error struct { Type string `xml:"type,attr"` } -func junitMarshal(b *Builder, level config.Level) ([]byte, error) { +func junitMarshal(b *Builder, level rules.Level) ([]byte, error) { s := TestSuites{ Name: "Popeye", Timestamp: b.Report.Timestamp, @@ -79,7 +79,7 @@ func junitMarshal(b *Builder, level config.Level) ([]byte, error) { return xml.MarshalIndent(s, "", "\t") } -func newSuite(s Section, level config.Level) TestSuite { +func newSuite(s Section, level rules.Level) TestSuite { total, fails, errs := numTests(s.Outcome) ts := TestSuite{ Name: s.Title, @@ -105,9 +105,9 @@ func newTestCase(res string, ii issues.Issues) TestCase { for _, i := range ii { // nolint:exhaustive switch i.Level { - case config.WarnLevel: + case rules.WarnLevel: tc.Failures = append(tc.Failures, newFailure(i)) - case config.ErrorLevel: + case rules.ErrorLevel: tc.Errors = append(tc.Errors, newError(i)) } } @@ -119,10 +119,10 @@ func numTests(o issues.Outcome) (total, fails, errors int) { for _, v := range o { total += 1 for _, i := range v { - if i.Level >= config.WarnLevel { + if i.Level >= rules.WarnLevel { fails++ } - if i.Level == config.ErrorLevel { + if i.Level == rules.ErrorLevel { errors++ } } @@ -130,7 +130,7 @@ func numTests(o issues.Outcome) (total, fails, errors int) { return } -func tallyToProps(t *Tally, level config.Level) []Property { +func tallyToProps(t *Tally, level rules.Level) []Property { var p []Property for i, s := range t.counts { diff --git a/internal/report/prometheus.go b/internal/report/prometheus.go index 9bac3886..3e090c73 100644 --- a/internal/report/prometheus.go +++ b/internal/report/prometheus.go @@ -4,88 +4,130 @@ package report import ( - "fmt" + "strconv" "strings" + "github.com/derailed/popeye/internal/rules" "github.com/derailed/popeye/pkg/config" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/push" + "github.com/rs/zerolog/log" ) const namespace = "popeye" // Metrics var ( - score = prometheus.NewGaugeVec(prometheus.GaugeOpts{ + sevGauge = prometheus.NewGaugeVec(prometheus.GaugeOpts{ Namespace: namespace, - Name: "cluster_score_total", - Help: "Popeye's sanitizers overall cluster score.", + Name: "severity_total", + Help: "Popeye's severity scores totals.", }, []string{ "cluster", "namespace", - "grade", + "severity", }) - sanitizers = prometheus.NewGaugeVec(prometheus.GaugeOpts{ + + codeGauge = prometheus.NewGaugeVec(prometheus.GaugeOpts{ Namespace: namespace, - Name: "sanitizer_reports_count", - Help: "Popeye's sanitizer reports for resource group.", + Name: "code_total", + Help: "Popeye's report codes totals", }, []string{ "cluster", "namespace", - "resource", - "level", + "linter", + "code", + "severity", + }) + + linterGauge = prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Namespace: namespace, + Name: "linter_tally_total", + Help: "Popeye's linter tally totals", + }, + []string{ + "cluster", + "linter", + "severity", }) - sanitizersScore = prometheus.NewGaugeVec(prometheus.GaugeOpts{ + + errGauge = prometheus.NewGaugeVec(prometheus.GaugeOpts{ Namespace: namespace, - Name: "sanitizer_score_total", - Help: "Popeye's sanitizer score for resource group.", + Name: "report_errors_total", + Help: "Popeye's scan errors total.", }, []string{ "cluster", "namespace", - "resource", }) - errs = prometheus.NewGaugeVec(prometheus.GaugeOpts{ + + scoreGauge = prometheus.NewGaugeVec(prometheus.GaugeOpts{ Namespace: namespace, - Name: "errors_total", - Help: "Popeye's sanitizers errors.", + Name: "cluster_score", + Help: "Popeye's scan cluster score.", }, []string{ "cluster", "namespace", + "grade", + }) + + reportGauge = prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Namespace: namespace, + Name: "report_score", + Help: "Popeye's scan report score.", + }, + []string{ + "cluster", + "namespace", + "grade", + "scan", }) ) -func prometheusMarshal(b *Builder, gtwy *config.PushGateway, cluster, namespace string) *push.Pusher { - pusher := newPusher(gtwy) +func (b *Builder) promCollect(ns, scanReport string, codes rules.Glossary) { + cc := b.Report.Sections.CodeTallies() + cc.Compact() + cc.Dump() - score.WithLabelValues(cluster, namespace, b.Report.Grade).Set(float64(b.Report.Score)) - errs.WithLabelValues(cluster, namespace).Set(float64(len(b.Report.Errors))) + cl := b.ClusterName + scoreGauge.WithLabelValues(cl, ns, b.Report.Grade).Set(float64(b.Report.Score)) + reportGauge.WithLabelValues(cl, ns, b.Report.Grade, scanReport).Set(float64(b.Report.Score)) + errGauge.WithLabelValues(cl, ns).Set(float64(len(b.Report.Errors))) + for linter, nss := range cc { + for ns, st := range nss { + for level, count := range st.Rollup(codes) { + sevGauge.WithLabelValues(cl, ns, level.ToHumanLevel()).Add(float64(count)) + } + for code, count := range st { + cid, _ := strconv.Atoi(code) + c := codes[rules.ID(cid)] + codeGauge.WithLabelValues(cl, ns, linter, code, c.Severity.ToHumanLevel()).Add(float64(count)) + } + } + } for _, section := range b.Report.Sections { for i, v := range section.Tally.counts { - sanitizers.WithLabelValues(cluster, namespace, section.Title, - strings.ToLower(indexToTally(i))).Set(float64(v)) + linterGauge.WithLabelValues(cl, section.Title, strings.ToLower(indexToTally(i))).Add(float64(v)) } - sanitizersScore.WithLabelValues(cluster, namespace, section.Title).Set(float64(section.Tally.score)) } - return pusher } -func newPusher(gtwy *config.PushGateway) *push.Pusher { +func newPusher(gtwy *config.PushGateway, instance string) *push.Pusher { registry := prometheus.NewRegistry() - registry.MustRegister(score, errs, sanitizers, sanitizersScore) - p := push.New(*gtwy.Address, "popeye").Gatherer(registry) - if isSet(gtwy.BasicAuth.User) && isSet(gtwy.BasicAuth.Password) { - fmt.Println("Using auth! ", *gtwy.BasicAuth.User, *gtwy.BasicAuth.Password) - p = p.BasicAuth(*gtwy.BasicAuth.User, *gtwy.BasicAuth.Password) - } + registry.MustRegister(scoreGauge, errGauge, linterGauge, sevGauge, codeGauge, reportGauge) - return p -} + pusher := push.New(*gtwy.URL, "popeye"). + Gatherer(registry). + Grouping("instance", instance) -func isSet(s *string) bool { - return s != nil && *s != "" + if config.IsStrSet(gtwy.BasicAuth.User) && config.IsStrSet(gtwy.BasicAuth.Password) { + log.Debug().Msgf("Using basic auth: %s", *gtwy.BasicAuth.User) + pusher = pusher.BasicAuth(*gtwy.BasicAuth.User, *gtwy.BasicAuth.Password) + } + + return pusher } diff --git a/internal/report/report.go b/internal/report/report.go new file mode 100644 index 00000000..ec670305 --- /dev/null +++ b/internal/report/report.go @@ -0,0 +1,102 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package report + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/derailed/popeye/internal/issues" + "github.com/derailed/popeye/internal/issues/tally" + "github.com/fvbommel/sortorder" +) + +// Report represents a popeye scan report. +type Report struct { + Timestamp string `json:"report_time" yaml:"report_time"` + Score int `json:"score" yaml:"score"` + Grade string `json:"grade" yaml:"grade"` + Sections Sections `json:"sections,omitempty" yaml:"sections,omitempty"` + Errors Errors `json:"errors,omitempty" yaml:"errors,omitempty"` + sectionsCount int + totalScore int +} + +func (r Report) ListSections() Sections { + return r.Sections +} + +// Sections represents a collection of sections. +type Sections []Section + +// Section represents a linter pass +type Section struct { + Title string `json:"linter" yaml:"linter"` + GVR string `json:"gvr" yaml:"gvr"` + Tally *Tally `json:"tally" yaml:"tally"` + Outcome issues.Outcome `json:"issues,omitempty" yaml:"issues,omitempty"` + singular string +} + +// Len returns the list size. +func (s Sections) Len() int { + return len(s) +} + +func (s Sections) CodeTallies() tally.Linter { + ss := make(tally.Linter) + for _, section := range s { + ss[section.Title] = section.Outcome.NSTally() + } + + return ss +} + +// Swap swaps list values. +func (s Sections) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} + +// Less returns true if i < j. +func (s Sections) Less(i, j int) bool { + return sortorder.NaturalLess(s[i].singular, s[j].singular) +} + +type Errors []error + +func (ee Errors) MarshalJSON() ([]byte, error) { + if len(ee) == 0 { + return nil, nil + } + errs := make([]string, 0, len(ee)) + for _, e := range ee { + if e == nil { + continue + } + raw, err := json.Marshal(e.Error()) + if err != nil { + return nil, err + } + errs = append(errs, fmt.Sprintf(`"error": %s`, string(raw))) + } + s := "{" + strings.Join(errs, ",") + "}" + + return []byte(s), nil +} + +func (ee Errors) MarshalYAML() (interface{}, error) { + if len(ee) == 0 { + return nil, nil + } + out := make([]string, 0, len(ee)) + for _, e := range ee { + if e == nil || e.Error() == "" { + continue + } + out = append(out, e.Error()) + } + + return out, nil +} diff --git a/internal/report/tally.go b/internal/report/tally.go index 3f24a7f3..f69ebe89 100644 --- a/internal/report/tally.go +++ b/internal/report/tally.go @@ -11,8 +11,8 @@ import ( "strconv" "github.com/derailed/popeye/internal/issues" - "github.com/derailed/popeye/internal/sanitize" - "github.com/derailed/popeye/pkg/config" + "github.com/derailed/popeye/internal/lint" + "github.com/derailed/popeye/internal/rules" ) const targetScore = 80 @@ -26,7 +26,9 @@ type Tally struct { // NewTally returns a new tally. func NewTally() *Tally { - return &Tally{counts: make([]int, 4)} + return &Tally{ + counts: make([]int, 4), + } } // Score returns the tally computed score. @@ -67,22 +69,22 @@ func (t *Tally) Rollup(o issues.Outcome) *Tally { // ComputeScore calculates the completed run score. func (t *Tally) computeScore() int { - var total, ok int + var issues, ok int for i, v := range t.counts { if i < 2 { ok += v } - total += v + issues += v } - t.score = int(sanitize.ToPerc(int64(ok), int64(total))) + t.score = int(lint.ToPerc(int64(ok), int64(issues))) return t.score } // Write out a tally. -func (t *Tally) write(w io.Writer, s *Sanitizer) { +func (t *Tally) write(w io.Writer, s *ScanReport) { for i := len(t.counts) - 1; i >= 0; i-- { - emoji := EmojiForLevel(config.Level(i), s.jurassicMode) + emoji := EmojiForLevel(rules.Level(i), s.jurassicMode) fmat := "%s %d " if s.jurassicMode { fmat = "%s:%d " @@ -102,7 +104,7 @@ func (t *Tally) write(w io.Writer, s *Sanitizer) { } // Dump writes out tally and computes length -func (t *Tally) Dump(s *Sanitizer) string { +func (t *Tally) Dump(s *ScanReport) string { w := bytes.NewBufferString("") t.write(w, s) diff --git a/internal/report/tally_test.go b/internal/report/tally_test.go index 5f927e1b..8b31542b 100644 --- a/internal/report/tally_test.go +++ b/internal/report/tally_test.go @@ -7,9 +7,9 @@ import ( "bytes" "testing" - "github.com/derailed/popeye/internal/client" "github.com/derailed/popeye/internal/issues" - "github.com/derailed/popeye/pkg/config" + "github.com/derailed/popeye/internal/rules" + "github.com/derailed/popeye/types" "github.com/stretchr/testify/assert" ) @@ -25,7 +25,7 @@ func TestTallyWrite(t *testing.T) { for _, u := range uu { ta := NewTally() b := bytes.NewBuffer([]byte("")) - s := NewSanitizer(b, u.jurassic) + s := New(b, u.jurassic) ta.write(b, s) assert.Equal(t, u.e, b.String()) @@ -33,54 +33,59 @@ func TestTallyWrite(t *testing.T) { } func TestTallyRollup(t *testing.T) { - uu := []struct { + uu := map[string]struct { o issues.Outcome + s int e *Tally }{ - { - issues.Outcome{}, - &Tally{counts: []int{0, 0, 0, 0}, score: 100, valid: true}, + "no-issues": { + o: issues.Outcome{}, + e: &Tally{counts: []int{0, 0, 0, 0}, score: 100, valid: true}, }, - { - issues.Outcome{ + "plain": { + o: issues.Outcome{ "a": { - issues.New(client.NewGVR("fred"), issues.Root, config.InfoLevel, ""), - issues.New(client.NewGVR("fred"), issues.Root, config.WarnLevel, ""), + issues.New(types.NewGVR("fred"), issues.Root, rules.InfoLevel, ""), + issues.New(types.NewGVR("fred"), issues.Root, rules.WarnLevel, ""), }, "b": { - issues.New(client.NewGVR("fred"), issues.Root, config.ErrorLevel, ""), + issues.New(types.NewGVR("fred"), issues.Root, rules.ErrorLevel, ""), }, "c": {}, }, - &Tally{counts: []int{1, 0, 1, 1}, score: 33, valid: true}, + e: &Tally{counts: []int{1, 0, 1, 1}, score: 33, valid: true}, }, } - for _, u := range uu { - ta := NewTally() - ta.Rollup(u.o) + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + ta := NewTally() + ta.Rollup(u.o) - assert.Equal(t, u.e, ta) + assert.Equal(t, u.e, ta) + }) } } func TestTallyScore(t *testing.T) { uu := []struct { o issues.Outcome + s int e int }{ { - issues.Outcome{ + o: issues.Outcome{ "a": { - issues.New(client.NewGVR("fred"), issues.Root, config.InfoLevel, ""), - issues.New(client.NewGVR("fred"), issues.Root, config.WarnLevel, ""), + issues.New(types.NewGVR("fred"), issues.Root, rules.InfoLevel, ""), + issues.New(types.NewGVR("fred"), issues.Root, rules.WarnLevel, ""), }, "b": { - issues.New(client.NewGVR("fred"), issues.Root, config.ErrorLevel, ""), + issues.New(types.NewGVR("fred"), issues.Root, rules.ErrorLevel, ""), }, "c": {}, }, - 33, + e: 33, }, } @@ -95,24 +100,25 @@ func TestTallyScore(t *testing.T) { func TestTallyWidth(t *testing.T) { uu := []struct { o issues.Outcome + s int e string }{ { - issues.Outcome{ + o: issues.Outcome{ "a": { - issues.New(client.NewGVR("fred"), issues.Root, config.InfoLevel, ""), - issues.New(client.NewGVR("fred"), issues.Root, config.WarnLevel, ""), + issues.New(types.NewGVR("fred"), issues.Root, rules.InfoLevel, ""), + issues.New(types.NewGVR("fred"), issues.Root, rules.WarnLevel, ""), }, "b": { - issues.New(client.NewGVR("fred"), issues.Root, config.ErrorLevel, ""), + issues.New(types.NewGVR("fred"), issues.Root, rules.ErrorLevel, ""), }, "c": {}, }, - "💥 1 😱 1 🔊 0 ✅ 1 \x1b[38;5;196m33\x1b[0m٪", + e: "💥 1 😱 1 🔊 0 ✅ 1 \x1b[38;5;196m33\x1b[0m٪", }, } - s := new(Sanitizer) + s := new(ScanReport) for _, u := range uu { ta := NewTally() ta.Rollup(u.o) diff --git a/internal/report/types.go b/internal/report/types.go new file mode 100644 index 00000000..7cdb003f --- /dev/null +++ b/internal/report/types.go @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package report + +import _ "embed" + +const ( + // DefaultFormat dumps report with color, emojis, the works. + DefaultFormat = "standard" + + // JurassicFormat dumps report with dud fancy-ness. + JurassicFormat = "jurassic" + + // YAMLFormat dumps report as YAML. + YAMLFormat = "yaml" + + // JSONFormat dumps report as JSON. + JSONFormat = "json" + + // HTMLFormat dumps report as HTML + HTMLFormat = "html" + + // JunitFormat renders report as JUnit. + JunitFormat = "junit" + + // ScoreFormat renders report as the value of the Score. + ScoreFormat = "score" + + // PromFormat renders report to prom metrics. + PromFormat = "prometheus" +) + +//go:embed assets/report.html +var htmlReport string diff --git a/internal/report/writer.go b/internal/report/writer.go index 519a84d7..86c6bd68 100644 --- a/internal/report/writer.go +++ b/internal/report/writer.go @@ -11,13 +11,14 @@ import ( "unicode/utf8" "github.com/derailed/popeye/internal/issues" - "github.com/derailed/popeye/pkg/config" + "github.com/derailed/popeye/internal/rules" ) -// Issue represents a sanitizer issues. +// Issue represents a linter issue. type Issue interface { - MaxSeverity(string) config.Level - Severity() config.Level + // MaxSeverity + MaxSeverity(string) rules.Level + Severity() rules.Level Description() string HasSubIssues() bool SubIssues() map[string][]Issue @@ -27,14 +28,14 @@ const ( // FontBold style FontBold = 1 - // Width denotes the maximum width of the sanitizer report. + // Width denotes the maximum width of a report. Width = 100 tabSize = 2 ) -// Sanitizer represents a sanitizer report. -type Sanitizer struct { +// ScanReport represents a scan report. +type ScanReport struct { io.Writer jurassicMode bool @@ -42,16 +43,16 @@ type Sanitizer struct { // -// NewSanitizer returns a new sanitizer report writer. -func NewSanitizer(w io.Writer, jurassic bool) *Sanitizer { - return &Sanitizer{ +// New returns a new instance. +func New(w io.Writer, jurassic bool) *ScanReport { + return &ScanReport{ Writer: w, jurassicMode: jurassic, } } // Open begins a new report section. -func (s *Sanitizer) Open(msg string, t *Tally) { +func (s *ScanReport) Open(msg string, t *Tally) { fmt.Fprintf(s, "\n%s", s.Color(msg, ColorLighSlate)) if t != nil && t.IsValid() { out := t.Dump(s) @@ -72,11 +73,11 @@ func (s *Sanitizer) Open(msg string, t *Tally) { } // Close a report section. -func (s *Sanitizer) Close() { +func (s *ScanReport) Close() { fmt.Fprintln(s) } -func (s *Sanitizer) lineBreaks(msg string, width int, color Color) { +func (s *ScanReport) lineBreaks(msg string, width int, color Color) { for i := 0; len(msg) > width; i++ { fmt.Fprintln(s, s.Color(msg[:width], color)) msg = msg[width:] @@ -88,7 +89,7 @@ func (s *Sanitizer) lineBreaks(msg string, width int, color Color) { } // Error prints out error out. -func (s *Sanitizer) Error(msg string, err error) { +func (s *ScanReport) Error(msg string, err error) { fmt.Fprintln(s) msg = msg + ": " + err.Error() width := Width - 3 @@ -97,12 +98,12 @@ func (s *Sanitizer) Error(msg string, err error) { } // Comment writes a comment line. -func (s *Sanitizer) Comment(msg string) { +func (s *ScanReport) Comment(msg string) { fmt.Fprintf(s, " · "+msg+"\n") } // Dump all errors to output. -func (s *Sanitizer) Dump(l config.Level, ii issues.Issues) { +func (s *ScanReport) Dump(l rules.Level, ii issues.Issues) { groups := ii.Group() keys := make([]string, 0, len(groups)) for k := range groups { @@ -131,12 +132,12 @@ func (s *Sanitizer) Dump(l config.Level, ii issues.Issues) { } // Print a colorized message. -func (s *Sanitizer) Print(l config.Level, indent int, msg string) { +func (s *ScanReport) Print(l rules.Level, indent int, msg string) { s.write(l, indent, msg) } // Write a colorized message to stdout. -func (s *Sanitizer) write(l config.Level, indent int, msg string) { +func (s *ScanReport) write(l rules.Level, indent int, msg string) { if msg == "" || msg == "." { return } @@ -168,7 +169,7 @@ func (s *Sanitizer) write(l config.Level, indent int, msg string) { } // Color or not this message by inject ansi colors. -func (s *Sanitizer) Color(msg string, c Color) string { +func (s *ScanReport) Color(msg string, c Color) string { if s.jurassicMode { return msg } diff --git a/internal/report/writer_test.go b/internal/report/writer_test.go index ce3cccb8..4d604801 100644 --- a/internal/report/writer_test.go +++ b/internal/report/writer_test.go @@ -10,15 +10,15 @@ import ( "strings" "testing" - "github.com/derailed/popeye/internal/client" "github.com/derailed/popeye/internal/issues" - "github.com/derailed/popeye/pkg/config" + "github.com/derailed/popeye/internal/rules" + "github.com/derailed/popeye/types" "github.com/stretchr/testify/assert" ) func TestComment(t *testing.T) { w := bytes.NewBufferString("") - s := NewSanitizer(w, false) + s := New(w, false) s.Comment("blee") @@ -42,7 +42,7 @@ func TestError(t *testing.T) { for _, u := range uu { w := bytes.NewBufferString("") - s := NewSanitizer(w, false) + s := New(w, false) s.Error("blee", u.err) assert.Equal(t, u.e, w.String()) @@ -74,8 +74,8 @@ func TestPrint(t *testing.T) { for _, u := range uu { w := bytes.NewBufferString("") - s := NewSanitizer(w, false) - s.Print(config.OkLevel, u.indent, u.m) + s := New(w, false) + s.Print(rules.OkLevel, u.indent, u.m) assert.Equal(t, u.e, w.String()) } @@ -88,15 +88,15 @@ func TestDump(t *testing.T) { }{ { issues.Outcome{ - "fred": issues.Issues{issues.New(client.NewGVR("fred"), issues.Root, config.WarnLevel, "Yo Mama!")}, + "fred": issues.Issues{issues.New(types.NewGVR("fred"), issues.Root, rules.WarnLevel, "Yo Mama!")}, }, " 😱 \x1b[38;5;220mYo Mama!.\x1b[0m\n", }, { issues.Outcome{ "fred": issues.Issues{ - issues.New(client.NewGVR("fred"), "c1", config.ErrorLevel, "Yo Mama!"), - issues.New(client.NewGVR("fred"), "c1", config.ErrorLevel, "Yo!"), + issues.New(types.NewGVR("fred"), "c1", rules.ErrorLevel, "Yo Mama!"), + issues.New(types.NewGVR("fred"), "c1", rules.ErrorLevel, "Yo!"), }, }, " 🐳 \x1b[38;5;75mc1\x1b[0m\n 💥 \x1b[38;5;196mYo Mama!.\x1b[0m\n 💥 \x1b[38;5;196mYo!.\x1b[0m\n", @@ -105,15 +105,15 @@ func TestDump(t *testing.T) { for _, u := range uu { w := bytes.NewBufferString("") - s := NewSanitizer(w, false) - s.Dump(config.OkLevel, u.o["fred"]) + s := New(w, false) + s.Dump(rules.OkLevel, u.o["fred"]) assert.Equal(t, u.e, w.String()) } } func BenchmarkPrint(b *testing.B) { - s := NewSanitizer(io.Discard, false) + s := New(io.Discard, false) b.ResetTimer() b.ReportAllocs() @@ -125,19 +125,20 @@ func BenchmarkPrint(b *testing.B) { func TestOpen(t *testing.T) { uu := []struct { o issues.Outcome + s int e string }{ { - issues.Outcome{ - "fred": issues.Issues{issues.New(client.NewGVR("fred"), issues.Root, config.WarnLevel, "Yo Mama!")}, + o: issues.Outcome{ + "fred": issues.Issues{issues.New(types.NewGVR("fred"), issues.Root, rules.WarnLevel, "Yo Mama!")}, }, - "\n\x1b[38;5;75mblee\x1b[0m" + strings.Repeat(" ", 75) + "💥 0 😱 1 🔊 0 ✅ 0 \x1b[38;5;196m0\x1b[0m٪\n\x1b[38;5;75m" + strings.Repeat("┅", Width+1) + "\x1b[0m\n", + e: "\n\x1b[38;5;75mblee\x1b[0m" + strings.Repeat(" ", 75) + "💥 0 😱 1 🔊 0 ✅ 0 \x1b[38;5;196m0\x1b[0m٪\n\x1b[38;5;75m" + strings.Repeat("┅", Width+1) + "\x1b[0m\n", }, } for _, u := range uu { w := bytes.NewBufferString("") - s := NewSanitizer(w, false) + s := New(w, false) ta := NewTally().Rollup(u.o) s.Open("blee", ta) @@ -148,7 +149,7 @@ func TestOpen(t *testing.T) { func TestOpenClose(t *testing.T) { w := bytes.NewBufferString("") - s := NewSanitizer(w, false) + s := New(w, false) s.Open("fred", nil) s.Close() diff --git a/internal/rules/code.go b/internal/rules/code.go new file mode 100644 index 00000000..76300e13 --- /dev/null +++ b/internal/rules/code.go @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package rules + +import ( + "fmt" + "strconv" + "strings" +) + +// Code represents an issue code. +type Code struct { + Message string `yaml:"message"` + Severity Level `yaml:"severity"` +} + +// Format hydrates a message with arguments. +func (c *Code) Format(code ID, args ...any) string { + msg := "[POP-" + strconv.Itoa(int(code)) + "] " + if len(args) == 0 { + msg += c.Message + } else { + msg += fmt.Sprintf(c.Message, args...) + } + + return strings.TrimSpace(msg) +} diff --git a/internal/rules/code_test.go b/internal/rules/code_test.go new file mode 100644 index 00000000..4c9a43cf --- /dev/null +++ b/internal/rules/code_test.go @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package rules_test + +import ( + "testing" + + "github.com/derailed/popeye/internal/rules" + "github.com/stretchr/testify/assert" +) + +func TestCodeFormat(t *testing.T) { + uu := map[string]struct { + c rules.Code + aa []any + e string + }{ + "empty": { + e: "[POP-100]", + }, + "no-args": { + c: rules.Code{Message: "bla"}, + e: "[POP-100] bla", + }, + "args": { + c: rules.Code{Message: "bla %s %d"}, + aa: []any{"yo", 10}, + e: "[POP-100] bla yo 10", + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, u.c.Format(100, u.aa...)) + }) + } +} diff --git a/internal/rules/exclude.go b/internal/rules/exclude.go new file mode 100644 index 00000000..23b35cb2 --- /dev/null +++ b/internal/rules/exclude.go @@ -0,0 +1,147 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package rules + +import ( + "fmt" + "strings" + + "github.com/rs/zerolog/log" +) + +type Excludes []Exclude + +func (ee Excludes) Dump(indent string) { + for i, e := range ee { + fmt.Printf("%s[%d]\n", strings.Repeat(indent, 2), i) + e.Dump(strings.Repeat(indent, 2)) + } +} + +func (ee Excludes) Match(spec Spec, global bool) bool { + if len(ee) == 0 { + return false + } + + for _, e := range ee { + if e.Match(spec, global) { + return true + } + } + + return false +} + +type Exclude struct { + FQNs expressions `yam:"FQNs"` + Labels keyVals `yaml:"labels"` + Annotations keyVals `yaml:"annotations"` + Codes expressions `yaml:"codes"` + Containers expressions `yaml:"containers"` +} + +// NewExclude returns a new instance. +func NewExclude() Exclude { + return Exclude{ + Labels: make(keyVals), + Annotations: make(keyVals), + } +} + +func (e Exclude) Dump(indent string) { + fmt.Printf("%sFQNS\n", indent) + e.FQNs.dump(strings.Repeat(indent, 2)) + fmt.Printf("%sLABELS\n", indent) + e.Labels.dump(strings.Repeat(indent, 2)) + fmt.Printf("%sANNOTS\n", indent) + e.Annotations.dump(strings.Repeat(indent, 2)) + fmt.Printf("%sCODES\n", indent) + e.Codes.dump(strings.Repeat(indent, 2)) + fmt.Printf("%sCONTAINERS\n", indent) + e.Containers.dump(strings.Repeat(indent, 2)) +} + +func (e Exclude) String() string { + return fmt.Sprintf("ns: %s ll: %s aa: %s cds: %s cos: %s", e.FQNs, e.Labels, e.Annotations, e.Codes, e.Containers) +} + +func (e Exclude) isEmpty() bool { + return e.FQNs.isEmpty() && + e.Labels.isEmpty() && + e.Annotations.isEmpty() && + e.Codes.isEmpty() && + e.Containers.isEmpty() +} + +func (e Exclude) matchGlob(spec Spec) bool { + log.Debug().Msgf("GlobalEX -- %s", spec) + log.Debug().Msgf(" Rule: %s", e) + + var matches int + if len(e.FQNs) > 0 && e.FQNs.match(spec.FQN) { + log.Debug().Msgf(" match fqn: %q -- %s", spec.FQN, e.FQNs) + matches++ + } + if len(e.Labels) > 0 && e.Labels.match(spec.Labels) { + log.Debug().Msgf(" match labels: %s -- %s", spec.Labels, e.Labels) + matches++ + } + if len(e.Annotations) > 0 && e.Annotations.match(spec.Annotations) { + log.Debug().Msgf(" match anns: %s -- %s", spec.Annotations, e.Annotations) + matches++ + } + if len(e.Containers) > 0 && e.Containers.matches(spec.Containers) { + log.Debug().Msgf(" match co: %s", e.Containers) + matches++ + } + if len(e.Codes) > 0 && e.Codes.match(spec.Code.String()) { + log.Debug().Msgf(" match codes: %q -- %s", spec.Code, e.Codes) + matches++ + } + log.Debug().Msgf(" Matches %q (%d)", spec.FQN, matches) + + return matches > 0 +} + +// Match checks if a given named resource should be Excluded. +func (e Exclude) Match(spec Spec, global bool) bool { + if spec.isEmpty() || e.isEmpty() { + return false + } + if global { + return e.matchGlob(spec) + } + + log.Debug().Msgf("LinterEX -- %s", spec) + log.Debug().Msgf(" Rule: %s", e) + + if !e.FQNs.match(spec.FQN) { + log.Debug().Msgf(" fire skip fqn: %q -- %s", spec.FQN, e.FQNs) + return false + } + + if !e.Labels.match(spec.Labels) { + log.Debug().Msgf(" fire skip labels: %s -- %s", spec.Labels, e.Labels) + return false + } + + if !e.Annotations.match(spec.Annotations) { + log.Debug().Msgf(" fire skip anns: %s -- %s", spec.Annotations, e.Annotations) + return false + } + + if !e.Containers.matches(spec.Containers) { + log.Debug().Msgf(" fire skip co: %s", e.Containers) + return false + } + + if spec.Code != ZeroCode && !e.Codes.match(spec.Code.String()) { + log.Debug().Msgf(" fire skip codes: %q -- %s", spec.Code, e.Codes) + return false + } + + log.Debug().Msgf(" Matched! %q", spec.FQN) + + return true +} diff --git a/internal/rules/exclude_test.go b/internal/rules/exclude_test.go new file mode 100644 index 00000000..a4ae811e --- /dev/null +++ b/internal/rules/exclude_test.go @@ -0,0 +1,798 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package rules + +import ( + "testing" + + "github.com/derailed/popeye/types" + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" +) + +func init() { + zerolog.SetGlobalLevel(zerolog.FatalLevel) +} + +func Test_excludesMatchGlobal(t *testing.T) { + uu := map[string]struct { + exc Excludes + spec Spec + e bool + }{ + "empty": {}, + "empty-rule": { + spec: Spec{ + GVR: types.NewGVR("v1/pods"), + FQN: "ns1/p1", + Code: 100, + }, + }, + "happy-ns": { + exc: Excludes{ + { + FQNs: expressions{"rx:^ns1", "ns2"}, + }, + }, + spec: Spec{ + GVR: types.NewGVR("v1/pods"), + FQN: "ns1/p1", + Code: 100, + }, + e: true, + }, + "happy-labels": { + exc: Excludes{ + { + Labels: keyVals{ + "a": expressions{"b1", "b2"}, + "c": expressions{"d1", "d2"}, + }, + }, + }, + spec: Spec{ + GVR: types.NewGVR("v1/pods"), + FQN: "ns3/p1", + Labels: Labels{"a": "b1", "c": "d2"}, + Code: 100, + }, + e: true, + }, + "toast-labels": { + exc: Excludes{ + { + Labels: keyVals{ + "a": expressions{"b1", "b2"}, + "c": expressions{"d1", "d2"}, + }, + }, + }, + spec: Spec{ + GVR: types.NewGVR("v1/pods"), + FQN: "ns3/p1", + Labels: Labels{"a12": "b1", "c": "d12"}, + Code: 100, + }, + }, + "happy-annotations": { + exc: Excludes{ + { + Annotations: keyVals{ + "a": expressions{"b1", "b2"}, + "c": expressions{"d1", "d2"}, + }, + }, + }, + spec: Spec{ + GVR: types.NewGVR("v1/pods"), + FQN: "ns3/p1", + Annotations: Labels{"a": "b1", "c": "d2"}, + Code: 100, + }, + e: true, + }, + "toast-annotations": { + exc: Excludes{ + { + Annotations: keyVals{ + "a": expressions{"b1", "b2"}, + "c": expressions{"d1", "d2"}, + }, + }, + }, + spec: Spec{ + GVR: types.NewGVR("v1/pods"), + FQN: "ns3/p1", + Annotations: Labels{"a": "b12", "c1": "d2"}, + Code: 100, + }, + }, + "happy-co": { + exc: Excludes{ + { + Containers: expressions{"rx:^c"}, + }, + }, + spec: Spec{ + GVR: types.NewGVR("v1/pods"), + FQN: "ns3/p1", + Annotations: Labels{"a": "b1", "c": "d2"}, + Containers: []string{"c1"}, + Code: 100, + }, + e: true, + }, + "toast-co": { + exc: Excludes{ + { + Containers: expressions{"rx:^c"}, + }, + }, + spec: Spec{ + GVR: types.NewGVR("v1/pods"), + FQN: "ns3/p1", + Annotations: Labels{"a": "b1", "c": "d2"}, + Containers: []string{"fred"}, + Code: 100, + }, + }, + "happy-code": { + exc: Excludes{ + { + Codes: expressions{"rx:^1"}, + }, + }, + spec: Spec{ + GVR: types.NewGVR("v1/pods"), + FQN: "ns3/p1-code", + Annotations: Labels{"a": "b1", "c": "d2"}, + Containers: []string{"c1"}, + Code: 1666, + }, + e: true, + }, + "toast-code": { + exc: Excludes{ + { + Codes: expressions{"rx:^1"}, + }, + }, + spec: Spec{ + GVR: types.NewGVR("v1/pods"), + FQN: "ns3/p1-code", + Annotations: Labels{"a": "b1", "c": "d2"}, + Containers: []string{"c1"}, + Code: 666, + }, + }, + "toast": { + exc: Excludes{ + { + FQNs: expressions{"ns1", "ns2"}, + }, + }, + spec: Spec{ + GVR: types.NewGVR("v1/pods"), + FQN: "ns3/p1", + Code: 100, + }, + }, + "toast-rx": { + exc: Excludes{ + { + FQNs: expressions{"ns1", "rx:.*ns2$"}, + }, + }, + spec: Spec{ + GVR: types.NewGVR("v1/pods"), + FQN: "fred-ns2-blee/p1", + Code: 100, + }, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + ok := u.exc.Match(u.spec, true) + assert.Equal(t, u.e, ok) + }) + } +} + +func Test_excludesMatch(t *testing.T) { + uu := map[string]struct { + exc Excludes + spec Spec + e bool + }{ + "empty": {}, + "empty-rule": { + spec: Spec{ + GVR: types.NewGVR("v1/pods"), + FQN: "ns1/p1", + Code: 100, + }, + }, + "happy": { + exc: Excludes{ + { + FQNs: expressions{"rx:^ns1", "ns2"}, + }, + }, + spec: Spec{ + GVR: types.NewGVR("v1/pods"), + FQN: "ns1/p1", + Code: 100, + }, + e: true, + }, + "happy-rx": { + exc: Excludes{ + { + FQNs: expressions{"ns1", "rx:.*ns2"}, + }, + }, + spec: Spec{ + GVR: types.NewGVR("v1/pods"), + FQN: "fred-ns2/p1", + Code: 100, + }, + e: true, + }, + "toast": { + exc: Excludes{ + { + FQNs: expressions{"ns1", "ns2"}, + }, + }, + spec: Spec{ + GVR: types.NewGVR("v1/pods"), + FQN: "ns3/p1", + Code: 100, + }, + }, + "toast-rx": { + exc: Excludes{ + { + FQNs: expressions{"ns1", "rx:.*ns2$"}, + }, + }, + spec: Spec{ + GVR: types.NewGVR("v1/pods"), + FQN: "fred-ns2-blee/p1", + Code: 100, + }, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + ok := u.exc.Match(u.spec, false) + assert.Equal(t, u.e, ok) + }) + } +} + +func Test_excludeMatch(t *testing.T) { + uu := map[string]struct { + exclude Exclude + spec Spec + glob bool + e bool + }{ + "empty": {}, + "empty-rule": { + spec: Spec{ + GVR: types.NewGVR("v1/pods"), + FQN: "ns1/p1", + Code: 100, + }, + }, + "empty-spec": { + exclude: Exclude{ + FQNs: expressions{ + "ns1", + "ns2", + "rx:^fred", + "rx:blee", + }, + }, + }, + + "match-ns": { + exclude: Exclude{ + FQNs: expressions{ + "rx:^ns1", + "ns2", + "rx:^fred", + "rx:blee", + }, + }, + spec: Spec{ + FQN: "ns1/fred", + }, + e: true, + }, + "match-ns-rx": { + exclude: Exclude{ + FQNs: expressions{ + "ns1", + "ns2", + "rx:^fred", + "rx:blee", + }, + }, + spec: Spec{ + FQN: "fred/blee", + }, + e: true, + }, + "match-ns-rx1": { + exclude: Exclude{ + FQNs: expressions{ + "ns1", + "ns2", + "rx:^fred", + "rx:blee", + }, + }, + spec: Spec{ + FQN: "fred-blee/duh", + }, + e: true, + }, + "skip-ns-rx": { + exclude: Exclude{ + FQNs: expressions{ + "ns1", + "ns2", + "rx:^fred", + "rx:blee", + }, + }, + spec: Spec{ + FQN: "zorg-bozo/duh", + }, + }, + + "match-labels-no-rule": { + exclude: Exclude{ + FQNs: expressions{ + "ns1", + "ns2", + "rx:^fred", + "rx:blee", + }, + }, + spec: Spec{ + FQN: "fred/duh", + Labels: Labels{"a": "1", "b": "2"}, + }, + e: true, + }, + "exact-match-labels": { + exclude: Exclude{ + FQNs: expressions{ + "ns1", + "ns2", + "rx:^fred", + "rx:blee", + }, + Labels: keyVals{ + "a": expressions{"1"}, + "b": expressions{"2"}, + }, + }, + spec: Spec{ + FQN: "fred/duh", + Labels: Labels{"a": "1", "b": "2"}, + }, + e: true, + }, + "set-match-labels": { + exclude: Exclude{ + FQNs: expressions{ + "ns1", + "ns2", + "rx:^fred", + "rx:blee", + }, + Labels: keyVals{ + "a": expressions{"1", "2"}, + "b": expressions{"2", "3"}, + }, + }, + spec: Spec{ + FQN: "fred/duh", + Labels: Labels{"a": "1", "b": "2"}, + }, + e: true, + }, + "match-labels-partial": { + exclude: Exclude{ + FQNs: expressions{ + "ns1", + "ns2", + "rx:^fred", + "rx:blee", + }, + Labels: keyVals{ + "a": expressions{"1", "2"}, + "b": expressions{"2", "3"}, + }, + }, + spec: Spec{ + FQN: "fred/duh", + Labels: Labels{"a": "3", "b": "2"}, + }, + e: true, + }, + "skip-labels-partial": { + exclude: Exclude{ + FQNs: expressions{ + "ns1", + "ns2", + "rx:^fred", + "rx:blee", + }, + Labels: keyVals{ + "a": expressions{"1", "2"}, + "b": expressions{"2", "3"}, + }, + }, + spec: Spec{ + FQN: "fred/duh", + Labels: Labels{"a": "3", "b": "5"}, + }, + }, + "skip-labels-full": { + exclude: Exclude{ + FQNs: expressions{ + "ns1", + "ns2", + "rx:^fred", + "rx:blee", + }, + Labels: keyVals{ + "a": expressions{"1", "2"}, + "b": expressions{"2", "3"}, + }, + }, + spec: Spec{ + FQN: "fred/duh", + Labels: Labels{"c": "3", "d": "5"}, + }, + }, + "match-labels-rx": { + exclude: Exclude{ + FQNs: expressions{ + "ns1", + "ns2", + "rx:^fred", + "rx:blee", + }, + Labels: keyVals{ + "a": expressions{"rx:^fred", "2"}, + "b": expressions{"2", "3"}, + }, + }, + spec: Spec{ + FQN: "fred/duh", + Labels: Labels{"a": "fred-duh", "b": "5"}, + }, + e: true, + }, + "skip-labels-rx": { + exclude: Exclude{ + FQNs: expressions{ + "ns1", + "ns2", + "rx:^fred", + "rx:blee", + }, + Labels: keyVals{ + "a": expressions{"rx:^fred", "2"}, + "b": expressions{"2", "3"}, + }, + }, + spec: Spec{ + FQN: "fred/duh", + Labels: Labels{"a": "bozo-duh", "b": "5"}, + }, + }, + + "skip-annot-rx": { + exclude: Exclude{ + FQNs: expressions{ + "ns1", + "ns2", + "rx:^fred", + "rx:blee", + }, + Annotations: keyVals{ + "a": expressions{"rx:^fred", "2"}, + "b": expressions{"2", "3"}, + }, + }, + spec: Spec{ + FQN: "fred/duh", + Annotations: Labels{"a": "bozo-duh", "b": "5"}, + }, + }, + + "match-container": { + exclude: Exclude{ + FQNs: expressions{ + "ns1", + "ns2", + "rx:^fred", + "rx:blee", + }, + Labels: keyVals{ + "a": expressions{"rx:^fred", "2"}, + "b": expressions{"2", "3"}, + }, + Annotations: keyVals{ + "a": expressions{"rx:^fred", "2"}, + "b": expressions{"2", "3"}, + }, + Containers: expressions{ + "c1", + "c2", + "rx:^fred", + }, + }, + spec: Spec{ + FQN: "fred/duh", + Labels: Labels{"a": "2", "b": "5"}, + Annotations: Labels{"a": "bozo-duh", "b": "3"}, + Containers: []string{"c1"}, + }, + e: true, + }, + "match-container-rx": { + exclude: Exclude{ + FQNs: expressions{ + "ns1", + "ns2", + "rx:^fred", + "rx:blee", + }, + Labels: keyVals{ + "a": expressions{"rx:^fred", "2"}, + "b": expressions{"2", "3"}, + }, + Annotations: keyVals{ + "a": expressions{"rx:^fred", "2"}, + "b": expressions{"2", "3"}, + }, + Containers: expressions{ + "c1", + "c2", + "rx:^fred", + }, + }, + spec: Spec{ + FQN: "fred/duh", + Labels: Labels{"a": "2", "b": "5"}, + Annotations: Labels{"a": "bozo-duh", "b": "3"}, + Containers: []string{"fred-blee"}, + }, + e: true, + }, + "skip-container-rx": { + exclude: Exclude{ + FQNs: expressions{ + "ns1", + "ns2", + "rx:^fred", + "rx:blee", + }, + Labels: keyVals{ + "a": expressions{"rx:^fred", "2"}, + "b": expressions{"2", "3"}, + }, + Annotations: keyVals{ + "a": expressions{"rx:^fred", "2"}, + "b": expressions{"2", "3"}, + }, + Containers: expressions{ + "c1", + "c2", + "rx:^fred", + }, + }, + spec: Spec{ + FQN: "fred/duh", + Labels: Labels{"a": "2", "b": "5"}, + Annotations: Labels{"a": "bozo-duh", "b": "3"}, + Containers: []string{"blee-duh"}, + }, + }, + + "match-all-codes": { + exclude: Exclude{ + FQNs: expressions{ + "ns1", + "ns2", + "rx:^fred", + "rx:blee", + }, + Labels: keyVals{ + "a": expressions{"rx:^fred", "2"}, + "b": expressions{"2", "3"}, + }, + Annotations: keyVals{ + "a": expressions{"rx:^fred", "2"}, + "b": expressions{"2", "3"}, + }, + Containers: expressions{ + "c1", + "c2", + "rx:^fred", + }, + }, + spec: Spec{ + FQN: "fred/duh", + Labels: Labels{"a": "2", "b": "5"}, + Annotations: Labels{"a": "bozo-duh", "b": "3"}, + Containers: []string{"fred-blee"}, + Code: 100, + }, + e: true, + }, + "match-codes": { + exclude: Exclude{ + FQNs: expressions{ + "ns1", + "ns2", + "rx:^fred", + "rx:blee", + }, + Labels: keyVals{ + "a": expressions{"rx:^fred", "2"}, + "b": expressions{"2", "3"}, + }, + Annotations: keyVals{ + "a": expressions{"rx:^fred", "2"}, + "b": expressions{"2", "3"}, + }, + Containers: expressions{ + "c1", + "c2", + "rx:^fred", + }, + Codes: expressions{ + "100", + "200", + "rx:^3", + }, + }, + spec: Spec{ + FQN: "fred/duh", + Labels: Labels{"a": "2", "b": "5"}, + Annotations: Labels{"a": "bozo-duh", "b": "3"}, + Containers: []string{"fred-blee"}, + Code: 100, + }, + e: true, + }, + "match-code-rx": { + exclude: Exclude{ + FQNs: expressions{ + "ns1", + "ns2", + "rx:^fred", + "rx:blee", + }, + Labels: keyVals{ + "a": expressions{"rx:^fred", "2"}, + "b": expressions{"2", "3"}, + }, + Annotations: keyVals{ + "a": expressions{"rx:^fred", "2"}, + "b": expressions{"2", "3"}, + }, + Containers: expressions{ + "c1", + "c2", + "rx:^fred", + }, + Codes: expressions{ + "100", + "200", + "rx:^3", + }, + }, + spec: Spec{ + FQN: "fred/duh", + Labels: Labels{"a": "2", "b": "5"}, + Annotations: Labels{"a": "bozo-duh", "b": "3"}, + Containers: []string{"fred-blee"}, + Code: 333, + }, + e: true, + }, + "skip-code": { + exclude: Exclude{ + FQNs: expressions{ + "ns1", + "ns2", + "rx:^fred", + "rx:blee", + }, + Labels: keyVals{ + "a": expressions{"rx:^fred", "2"}, + "b": expressions{"2", "3"}, + }, + Annotations: keyVals{ + "a": expressions{"rx:^fred", "2"}, + "b": expressions{"2", "3"}, + }, + Containers: expressions{ + "c1", + "c2", + "rx:^fred", + }, + Codes: expressions{ + "100", + "102", + "200", + }, + }, + spec: Spec{ + FQN: "fred/duh", + Labels: Labels{"a": "2", "b": "5"}, + Annotations: Labels{"a": "bozo-duh", "b": "3"}, + Containers: []string{"fred-blee"}, + Code: 666, + }, + }, + "skip-code-rx": { + exclude: Exclude{ + FQNs: expressions{ + "ns1", + "ns2", + "rx:^fred", + "rx:blee", + }, + Labels: keyVals{ + "a": expressions{"rx:^fred", "2"}, + "b": expressions{"2", "3"}, + }, + Annotations: keyVals{ + "a": expressions{"rx:^fred", "2"}, + "b": expressions{"2", "3"}, + }, + Containers: expressions{ + "c1", + "c2", + "rx:^fred", + }, + Codes: expressions{ + "100", + "200", + "rx:^3", + }, + }, + spec: Spec{ + FQN: "fred/duh", + Labels: Labels{"a": "2", "b": "5"}, + Annotations: Labels{"a": "bozo-duh", "b": "3"}, + Containers: []string{"fred-blee"}, + Code: 633, + }, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + ok := u.exclude.Match(u.spec, u.glob) + assert.Equal(t, u.e, ok) + }) + } +} diff --git a/internal/rules/exclusions.go b/internal/rules/exclusions.go new file mode 100644 index 00000000..c12ac6c3 --- /dev/null +++ b/internal/rules/exclusions.go @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package rules + +import ( + "fmt" + + "github.com/rs/zerolog/log" +) + +type Exclusions struct { + // Excludes tracks exclusions + Global Exclude `yaml:"global"` + + // Linters tracks exclusions + Linters Linters `yaml:"linters"` +} + +func NewExclusions() Exclusions { + return Exclusions{ + Global: NewExclude(), + Linters: make(Linters), + } +} + +func (e Exclusions) Match(spec Spec) bool { + if e.Global.Match(spec, true) { + log.Debug().Msgf("Global exclude matched: %q::%q", spec.GVR, spec.FQN) + return true + } + + return e.Linters.Match(spec, false) +} + +func (e Exclusions) Dump() { + fmt.Println("Globals") + e.Global.Dump(" ") + + fmt.Println("Linters") + e.Linters.Dump(" ") +} diff --git a/internal/rules/expression.go b/internal/rules/expression.go new file mode 100644 index 00000000..26e81c83 --- /dev/null +++ b/internal/rules/expression.go @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package rules + +import ( + "fmt" + "regexp" + "strings" +) + +type Expression string + +func (e Expression) dump(indent string) { + fmt.Printf("%s%q\n", indent, e) +} + +func (e Expression) IsRX() bool { + return strings.HasPrefix(string(e), rxMarker) +} + +func (e Expression) MatchRX(s string) bool { + rx := regexp.MustCompile(strings.Replace(string(e), rxMarker, "", 1)) + + return rx.MatchString(s) +} + +func (e Expression) match(s string) bool { + if e == "" { + return true + } + if e.IsRX() { + return e.MatchRX(s) + } + + return s == string(e) +} + +type expressions []Expression + +func (ee expressions) dump(indent string) { + for _, e := range ee { + e.dump(indent) + } +} + +func (ee expressions) isEmpty() bool { + return len(ee) == 0 +} + +func (ee expressions) matches(ss []string) bool { + if len(ee) == 0 { + return true + } + + for _, s := range ss { + if ee.match(s) { + return true + } + } + + return false +} + +func (ee expressions) match(exp string) bool { + if len(ee) == 0 || exp == "" { + return true + } + for _, e := range ee { + if e.match(exp) { + return true + } + } + + return false +} diff --git a/internal/rules/expression_test.go b/internal/rules/expression_test.go new file mode 100644 index 00000000..cb4627e9 --- /dev/null +++ b/internal/rules/expression_test.go @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package rules + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_expressionMatch(t *testing.T) { + uu := map[string]struct { + exp Expression + s string + e bool + }{ + "empty": { + e: true, + }, + "empty-rule": { + s: "fred", + e: true, + }, + "happy": { + exp: "fred", + s: "fred", + e: true, + }, + "happy-rx": { + exp: "rx:^fred", + s: "fred", + e: true, + }, + "toast": { + exp: "freddy", + s: "fred", + }, + "toast-rx": { + exp: "rx:freddy", + s: "fred", + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + ok := u.exp.match(u.s) + assert.Equal(t, u.e, ok) + }) + } +} diff --git a/internal/rules/helpers.go b/internal/rules/helpers.go new file mode 100644 index 00000000..426f8542 --- /dev/null +++ b/internal/rules/helpers.go @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package rules + +import ( + "regexp" + "strings" +) + +const rxMarker = "rx:" + +func rxMatch(exp, name string) (bool, error) { + if !isRegex(exp) { + return false, nil + } + rx, err := regexp.Compile(strings.Replace(exp, rxMarker, "", 1)) + if err != nil { + return false, err + } + + return rx.MatchString(name), nil +} + +func isRegex(s string) bool { + return strings.HasPrefix(s, rxMarker) +} diff --git a/pkg/config/excludes_int_test.go b/internal/rules/helpers_int_test.go similarity index 59% rename from pkg/config/excludes_int_test.go rename to internal/rules/helpers_int_test.go index 8181576e..c3449017 100644 --- a/pkg/config/excludes_int_test.go +++ b/internal/rules/helpers_int_test.go @@ -1,89 +1,112 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright Authors of Popeye -package config +package rules import ( + "regexp/syntax" "testing" "github.com/stretchr/testify/assert" ) -func TestRxMatch(t *testing.T) { +func Test_rxMatch(t *testing.T) { uu := map[string]struct { exp, name string e bool + err error }{ - "match": { + "empty": {}, + "match-exact": { exp: "rx:blee", name: "blee", e: true, }, - "match_dash": { + "exclude-all": { + exp: "rx:.*", + name: "blee", + e: true, + }, + "match-dash": { exp: "rx:blee", name: "blee-aeou", e: true, }, - "no_match_dash": { + "no-match-dash": { exp: "rx:blee-", name: "blee1", }, - "no_match_dash_wild": { + "no-match-dash-wild": { exp: "rx:fred1*-blee", name: "fred1blee", }, - "match_dash_wild": { + "match-dash-wild": { exp: "rx:fred-*", name: "fred-1blee", e: true, }, - "match_ns": { + "match-ns": { exp: `rx:default\/\w+\.v1`, name: "default/cm.v1", e: true, }, - "match_ns1": { + "match-ns1": { exp: `rx:\.v\d+`, name: "default/cm.v1", e: true, }, - "match_ns2": { + "match-ns2": { exp: `rx:\.v\d+`, name: "default/cm.v2", e: true, }, - "match_ns3": { + "match-ns3": { exp: `rx:\.v\d+`, name: "fred/cm.v2", e: true, }, - - "match_slash": { + "match-slash": { exp: "rx:kube*", name: "kube-system/eks-certificates-controller", e: true, }, - "wild_version": { + "wild-version": { exp: `rx:fred\.v\d+`, name: "kube-system/fred.v23", e: true, }, - "wild_version_1": { + "wild-version-1": { exp: `rx:fred.+\.v\d+`, name: "kube-system/fredblee.v23", e: true, }, - "wild_version_2": { + "wild_version-2": { exp: `rx:fred.+\.v\d+`, name: "kube-system/fredblee.v2.3", e: true, }, + "starts-with": { + exp: `rx:\Ans`, + name: "ns-1", + e: true, + }, + "no-match-starts-with": { + exp: `rx:\Ans`, + name: "ans-1", + }, + "toast-rx": { + exp: `rx:\yns`, + name: "ans-1", + err: &syntax.Error{Code: "invalid escape sequence", Expr: "\\y"}, + }, } for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, rxMatch(u.exp, u.name)) + ok, err := rxMatch(u.exp, u.name) + assert.Equal(t, u.err, err) + assert.Equal(t, u.e, ok) }) } } diff --git a/internal/rules/keyvals.go b/internal/rules/keyvals.go new file mode 100644 index 00000000..4ce276db --- /dev/null +++ b/internal/rules/keyvals.go @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package rules + +import ( + "fmt" + "strings" +) + +type Labels map[string]string + +func (l Labels) String() string { + if len(l) == 0 { + return "n/a" + } + + kk := make([]string, 0, len(l)) + for k := range l { + kk = append(kk, k) + } + + return strings.Join(kk, ",") +} + +type keyVals map[string]expressions + +func (kv keyVals) dump(indent string) { + for k, v := range kv { + fmt.Printf("%s%s: %s\n", indent, k, v) + } +} + +func (kv keyVals) isEmpty() bool { + return len(kv) == 0 +} + +func (kv keyVals) match(ll Labels) bool { + if len(kv) == 0 { + return true + } + + var matches int + for k, ee := range kv { + v, ok := ll[k] + if !ok { + continue + } + if ee.match(v) { + matches++ + } + } + + return matches > 0 +} diff --git a/pkg/config/level.go b/internal/rules/level.go similarity index 71% rename from pkg/config/level.go rename to internal/rules/level.go index 9d71d3d2..a781c9ed 100644 --- a/pkg/config/level.go +++ b/internal/rules/level.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright Authors of Popeye -package config +package rules // Level tracks lint check level. type Level int @@ -19,7 +19,7 @@ const ( // ToIssueLevel convert a string to a issue level. func ToIssueLevel(level *string) Level { - if !isSet(level) { + if level == nil || *level == "" { return OkLevel } @@ -36,3 +36,18 @@ func ToIssueLevel(level *string) Level { return OkLevel } } + +func (l Level) ToHumanLevel() string { + switch l { + case OkLevel: + return "ok" + case InfoLevel: + return "info" + case WarnLevel: + return "warn" + case ErrorLevel: + return "error" + default: + return "n/a" + } +} diff --git a/pkg/config/level_test.go b/internal/rules/level_test.go similarity index 96% rename from pkg/config/level_test.go rename to internal/rules/level_test.go index 43e62429..afa31dea 100644 --- a/pkg/config/level_test.go +++ b/internal/rules/level_test.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright Authors of Popeye -package config +package rules import ( "testing" diff --git a/internal/rules/linters.go b/internal/rules/linters.go new file mode 100644 index 00000000..c0405a53 --- /dev/null +++ b/internal/rules/linters.go @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package rules + +import ( + "fmt" + "strings" + + "github.com/rs/zerolog/log" +) + +type LinterExcludes struct { + Codes expressions `yaml:"codes"` + Instances Excludes `yaml:"instances"` +} + +func (l LinterExcludes) Dump(indent string) { + l.Codes.dump(indent) + l.Instances.Dump(indent) +} + +func (l LinterExcludes) Match(spec Spec, global bool) bool { + if l.Instances.Match(spec, global) { + return true + } + + if spec.Code == ZeroCode || len(l.Codes) == 0 { + return false + } + + return l.Codes.match(spec.Code.String()) +} + +type Linters map[string]LinterExcludes + +func (l Linters) Dump(indent string) { + for k, v := range l { + fmt.Printf("%s%s\n", indent, k) + v.Dump(strings.Repeat(indent, 2)) + } +} + +func (l Linters) isEmpty() bool { + return len(l) == 0 +} + +func (l Linters) Match(spec Spec, global bool) bool { + if l.isEmpty() { + return false + } + + linter, ok := l[spec.GVR.R()] + if !ok { + log.Debug().Msgf("No exclusions found for linter: %q", spec.GVR.R()) + return false + } + + return linter.Match(spec, global) +} diff --git a/internal/rules/linters_test.go b/internal/rules/linters_test.go new file mode 100644 index 00000000..05465888 --- /dev/null +++ b/internal/rules/linters_test.go @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package rules + +import ( + "testing" + + "github.com/derailed/popeye/types" + "github.com/stretchr/testify/assert" +) + +func Test_lintersMatch(t *testing.T) { + uu := map[string]struct { + linters Linters + spec Spec + glob bool + e bool + }{ + "empty": {}, + "empty-rule": { + spec: Spec{ + GVR: types.NewGVR("v1/pods"), + FQN: "ns1/p1", + Code: 100, + }, + }, + "missing": { + linters: Linters{ + "pods": LinterExcludes{ + Instances: Excludes{ + { + FQNs: expressions{"ns1", "ns2"}, + }, + }, + }, + }, + spec: Spec{ + GVR: types.NewGVR("v1/configmaps"), + FQN: "ns1/p1", + Code: 100, + }, + }, + + "happy": { + linters: Linters{ + "pods": LinterExcludes{ + Instances: Excludes{ + { + FQNs: expressions{"rx:^ns1", "ns2"}, + }, + }, + }, + }, + spec: Spec{ + GVR: types.NewGVR("v1/pods"), + FQN: "ns1/p1", + Code: 100, + }, + e: true, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, u.linters.Match(u.spec, u.glob)) + }) + } +} diff --git a/internal/rules/spec.go b/internal/rules/spec.go new file mode 100644 index 00000000..74893261 --- /dev/null +++ b/internal/rules/spec.go @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package rules + +import ( + "fmt" + "strings" + + "github.com/derailed/popeye/types" +) + +// Spec tracks an issue spec +type Spec struct { + GVR types.GVR + FQN string + Labels Labels + Annotations Labels + Containers []string + Code ID +} + +func (s Spec) isEmpty() bool { + return s.GVR == types.BlankGVR && + s.FQN == "" && + len(s.Labels) == 0 && + len(s.Annotations) == 0 && + len(s.Containers) == 0 && + s.Code == ZeroCode +} + +func (s Spec) String() string { + ss := fmt.Sprintf("[%s] %s", s.GVR, s.FQN) + if len(s.Containers) != 0 { + ss += fmt.Sprintf("::%s", strings.Join(s.Containers, ",")) + } + if s.Code != ZeroCode { + ss += fmt.Sprintf("(%q)", s.Code) + } + ss += fmt.Sprintf("-- %s::%s", s.Labels, s.Annotations) + + return ss +} diff --git a/internal/rules/types.go b/internal/rules/types.go new file mode 100644 index 00000000..6f9eacee --- /dev/null +++ b/internal/rules/types.go @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package rules + +import "strconv" + +const ZeroCode ID = 0 + +// ID represents a issue code identifier. +type ID int + +func (i ID) String() string { + return strconv.Itoa(int(i)) +} + +// IDS tracks a collection of ids. +type IDS map[Code]struct{} + +type CodeOverride struct { + ID ID `yaml:"code"` + Message string `yaml:"message"` + Severity Level `yaml:"severity"` +} + +// Glossary represents a collection of codes. +type Overrides []CodeOverride + +// Glossary represents a collection of codes. +type Glossary map[ID]*Code diff --git a/internal/sanitize/cluster.go b/internal/sanitize/cluster.go deleted file mode 100644 index 4dabcd01..00000000 --- a/internal/sanitize/cluster.go +++ /dev/null @@ -1,77 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package sanitize - -import ( - "context" - "strconv" - - "github.com/derailed/popeye/internal" - "github.com/derailed/popeye/internal/issues" -) - -const ( - tolerableMajor = 1 - tolerableMinor = 12 -) - -type ( - // Cluster tracks cluster sanitization. - Cluster struct { - *issues.Collector - ClusterLister - } - - // ClusterLister list available Clusters on a cluster. - ClusterLister interface { - ListVersion() (string, string) - HasMetrics() bool - } -) - -// NewCluster returns a new sanitizer. -func NewCluster(co *issues.Collector, lister ClusterLister) *Cluster { - return &Cluster{ - Collector: co, - ClusterLister: lister, - } -} - -// Sanitize cleanse the resource. -func (c *Cluster) Sanitize(ctx context.Context) error { - c.checkMetricsServer(ctx) - if err := c.checkVersion(ctx); err != nil { - return err - } - return nil -} - -func (c *Cluster) checkMetricsServer(ctx context.Context) { - ctx = internal.WithFQN(ctx, "Metrics") - if !c.HasMetrics() { - c.AddCode(ctx, 402) - } -} - -func (c *Cluster) checkVersion(ctx context.Context) error { - major, minor := c.ListVersion() - - m, err := strconv.Atoi(major) - if err != nil { - return err - } - p, err := strconv.Atoi(minor) - if err != nil { - return err - } - - ctx = internal.WithFQN(ctx, "Version") - if m != tolerableMajor || p < tolerableMinor { - c.AddCode(ctx, 405) - } else { - c.AddCode(ctx, 406) - } - - return nil -} diff --git a/internal/sanitize/cluster_test.go b/internal/sanitize/cluster_test.go deleted file mode 100644 index 3df63589..00000000 --- a/internal/sanitize/cluster_test.go +++ /dev/null @@ -1,104 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package sanitize - -import ( - "context" - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/derailed/popeye/internal" - "github.com/derailed/popeye/internal/client" - "github.com/derailed/popeye/internal/issues" - "github.com/derailed/popeye/pkg/config" -) - -func TestClusterSanitize(t *testing.T) { - uu := map[string]struct { - major, minor string - metrics bool - e issues.Outcome - }{ - "good": { - major: "1", minor: "15", - metrics: true, - e: map[string]issues.Issues{ - "Version": { - { - GVR: "clusters", - Group: issues.Root, - Message: "[POP-406] K8s version OK", - Level: config.OkLevel, - }, - }, - }, - }, - "guizard": { - major: "1", minor: "11", - metrics: false, - e: map[string]issues.Issues{ - "Version": { - { - GVR: "clusters", - Group: issues.Root, - Message: "[POP-405] Is this a jurassic cluster? Might want to upgrade K8s a bit", - Level: config.WarnLevel, - }, - }, - "Metrics": { - { - GVR: "clusters", - Group: issues.Root, - Message: "[POP-402] No metrics-server detected", - Level: config.InfoLevel, - }, - }, - }, - }, - } - - ctx := makeContext("clusters", "cluster") - for k := range uu { - u := uu[k] - t.Run(k, func(t *testing.T) { - cl := NewCluster(issues.NewCollector(loadCodes(t), makeConfig(t)), newCluster(u.major, u.minor, u.metrics)) - - assert.Nil(t, cl.Sanitize(ctx)) - assert.Equal(t, u.e, cl.Outcome()) - }) - } -} - -// Helpers... - -func makeConfig(t *testing.T) *config.Config { - c, err := config.NewConfig(config.NewFlags()) - assert.Nil(t, err) - return c -} - -func makeContext(gvr, section string) context.Context { - return context.WithValue(context.Background(), internal.KeyRunInfo, internal.RunInfo{ - Section: section, - SectionGVR: client.NewGVR(gvr), - }) -} - -type cluster struct { - major, minor string - metrics bool -} - -func newCluster(major, minor string, metrics bool) cluster { - return cluster{major: major, minor: minor, metrics: metrics} -} - -func (c cluster) ListVersion() (string, string) { - return c.major, c.minor -} - -func (c cluster) HasMetrics() bool { - return c.metrics -} diff --git a/internal/sanitize/cm.go b/internal/sanitize/cm.go deleted file mode 100644 index 864927ff..00000000 --- a/internal/sanitize/cm.go +++ /dev/null @@ -1,79 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package sanitize - -import ( - "context" - "sync" - - "github.com/derailed/popeye/internal" - "github.com/derailed/popeye/internal/cache" - "github.com/derailed/popeye/internal/issues" - v1 "k8s.io/api/core/v1" -) - -type ( - // PodRefs tracks pods object references. - PodRefs interface { - PodRefs(*sync.Map) - } - - // ConfigMapLister list available ConfigMaps on a cluster. - ConfigMapLister interface { - PodRefs - ListConfigMaps() map[string]*v1.ConfigMap - } - - // ConfigMap tracks ConfigMap sanitization. - ConfigMap struct { - *issues.Collector - ConfigMapLister - } -) - -// NewConfigMap returns a new sanitizer. -func NewConfigMap(c *issues.Collector, lister ConfigMapLister) *ConfigMap { - return &ConfigMap{ - Collector: c, - ConfigMapLister: lister, - } -} - -// Sanitize cleanse the resource. -func (c *ConfigMap) Sanitize(ctx context.Context) error { - var cmRefs sync.Map - c.PodRefs(&cmRefs) - c.checkInUse(ctx, &cmRefs) - - return nil -} - -func (c *ConfigMap) checkInUse(ctx context.Context, refs *sync.Map) { - for fqn, cm := range c.ListConfigMaps() { - c.InitOutcome(fqn) - ctx = internal.WithFQN(ctx, fqn) - keys, ok := refs.Load(cache.ResFqn(cache.ConfigMapKey, fqn)) - defer func(ctx context.Context, fqn string) { - if c.NoConcerns(fqn) && c.Config.ExcludeFQN(internal.MustExtractSectionGVR(ctx), fqn) { - c.ClearOutcome(fqn) - } - }(ctx, fqn) - if !ok { - c.AddCode(ctx, 400) - continue - } - if keys.(internal.StringSet).Has(internal.All) { - continue - } - - kk := make(internal.StringSet, len(cm.Data)) - for k := range cm.Data { - kk.Add(k) - } - deltas := keys.(internal.StringSet).Diff(kk) - for k := range deltas { - c.AddCode(ctx, 401, k) - } - } -} diff --git a/internal/sanitize/cm_test.go b/internal/sanitize/cm_test.go deleted file mode 100644 index a98e201e..00000000 --- a/internal/sanitize/cm_test.go +++ /dev/null @@ -1,83 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package sanitize - -import ( - "sync" - "testing" - - "github.com/derailed/popeye/internal" - "github.com/derailed/popeye/internal/issues" - "github.com/derailed/popeye/pkg/config" - "github.com/stretchr/testify/assert" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -func loadCodes(t *testing.T) *issues.Codes { - codes, err := issues.LoadCodes() - assert.Nil(t, err) - return codes - -} - -func TestConfigMapSanitize(t *testing.T) { - cm := NewConfigMap(issues.NewCollector(loadCodes(t), makeConfig(t)), newConfigMap()) - - ctx := makeContext("v1/configmaps", "configmaps") - assert.Nil(t, cm.Sanitize(ctx)) - assert.Equal(t, 4, len(cm.Outcome())) - - ii := cm.Outcome()["default/cm3"] - assert.Equal(t, 1, len(ii)) - assert.Equal(t, "[POP-400] Used? Unable to locate resource reference", ii[0].Message) - assert.Equal(t, config.InfoLevel, ii[0].Level) - - ii = cm.Outcome()["default/cm4"] - assert.Equal(t, 1, len(ii)) - assert.Equal(t, `[POP-401] Key "k2" used? Unable to locate key reference`, ii[0].Message) - assert.Equal(t, config.InfoLevel, ii[0].Level) -} - -// ---------------------------------------------------------------------------- -// Helpers... - -type configMap struct{} - -func newConfigMap() configMap { - return configMap{} -} - -func (c configMap) PodRefs(refs *sync.Map) { - refs.Store("cm:default/cm1", internal.StringSet{ - "k1": internal.Blank, - "k2": internal.Blank, - }) - refs.Store("cm:default/cm2", internal.AllKeys) - refs.Store("cm:default/cm4", internal.StringSet{ - "k1": internal.Blank, - }) -} - -func (c configMap) ListConfigMaps() map[string]*v1.ConfigMap { - return map[string]*v1.ConfigMap{ - "default/cm1": makeConfigMap("cm1"), - "default/cm2": makeConfigMap("cm2"), - "default/cm3": makeConfigMap("cm3"), - "default/cm4": makeConfigMap("cm4"), - } -} - -func makeConfigMap(n string) *v1.ConfigMap { - return &v1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Name: n, - Namespace: "default", - }, - Data: map[string]string{ - "k1": "", - "k2": "", - }, - } -} diff --git a/internal/sanitize/cr.go b/internal/sanitize/cr.go deleted file mode 100644 index 3c8d56a1..00000000 --- a/internal/sanitize/cr.go +++ /dev/null @@ -1,61 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package sanitize - -import ( - "context" - "sync" - - "github.com/derailed/popeye/internal" - "github.com/derailed/popeye/internal/cache" - "github.com/derailed/popeye/internal/issues" -) - -type ( - // CRLister lists roles and rolebindings. - CRLister interface { - ClusterRoleLister - ClusterRoleBindingLister - RoleBindingLister - } - - // ClusterRole tracks ClusterRole sanitization. - ClusterRole struct { - *issues.Collector - CRLister - } -) - -// NewClusterRole returns a new ClusterRole sanitizer. -func NewClusterRole(c *issues.Collector, lister CRLister) *ClusterRole { - return &ClusterRole{ - Collector: c, - CRLister: lister, - } -} - -// Sanitize a configmap. -func (c *ClusterRole) Sanitize(ctx context.Context) error { - var crRefs sync.Map - c.ClusterRoleRefs(&crRefs) - c.RoleRefs(&crRefs) - c.checkInUse(ctx, &crRefs) - - return nil -} - -func (c *ClusterRole) checkInUse(ctx context.Context, refs *sync.Map) { - for fqn := range c.ListClusterRoles() { - c.InitOutcome(fqn) - ctx = internal.WithFQN(ctx, fqn) - - _, ok := refs.Load(cache.ResFqn(cache.ClusterRoleKey, fqn)) - if !ok { - c.AddCode(ctx, 400) - } - if c.NoConcerns(fqn) && c.Config.ExcludeFQN(internal.MustExtractSectionGVR(ctx), fqn) { - c.ClearOutcome(fqn) - } - } -} diff --git a/internal/sanitize/cr_test.go b/internal/sanitize/cr_test.go deleted file mode 100644 index f73cd3bf..00000000 --- a/internal/sanitize/cr_test.go +++ /dev/null @@ -1,140 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package sanitize - -import ( - "regexp" - "strconv" - "sync" - "testing" - - "github.com/derailed/popeye/internal" - "github.com/derailed/popeye/internal/cache" - "github.com/derailed/popeye/internal/issues" - "github.com/derailed/popeye/pkg/config" - "github.com/stretchr/testify/assert" - rbacv1 "k8s.io/api/rbac/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -func TestCRSanitize(t *testing.T) { - uu := map[string]struct { - lister CRLister - key string - issues []config.ID - }{ - "usedCRBS": { - key: "cr1", - lister: makeCRLister("cr1"), - }, - "usedRBS": { - key: "cr2", - lister: makeCRLister("cr2"), - }, - "unused": { - key: "cr3", - lister: makeCRLister("cr3"), - issues: []config.ID{400}, - }, - } - - ctx := makeContext("rbac.authorization.k8s.io/v1/clusterroles", "cr") - for k := range uu { - u := uu[k] - t.Run(k, func(t *testing.T) { - c := NewClusterRole(issues.NewCollector(loadCodes(t), makeConfig(t)), u.lister) - - assert.Nil(t, c.Sanitize(ctx)) - validateIssues(t, u.key, c.Outcome(), u.issues) - }) - } -} - -// ---------------------------------------------------------------------------- -// Helpers... - -var issueRX = regexp.MustCompile(`\A\[POP-(\d+)\].`) - -func validateIssues(t *testing.T, key string, actual issues.Outcome, expected []config.ID) { - _, ok := actual[key] - assert.True(t, ok, key) - assert.Equal(t, len(expected), len(actual[key])) - for _, id := range expected { - a := actual[key] - assert.Equal(t, 1, len(a)) - strs := issueRX.FindStringSubmatch(a[0].Message) - assert.Equal(t, 2, len(strs)) - assert.Equal(t, strconv.Itoa(int(id)), strs[1]) - _ = id - } -} - -type crOpts struct { - name, refKind, refName string -} - -type cr struct { - name string - opts crOpts -} - -var _ CRLister = (*cr)(nil) - -func makeCRLister(n string) *cr { - return &cr{name: n} -} - -func (c *cr) ListClusterRoleBindings() map[string]*rbacv1.ClusterRoleBinding { - return map[string]*rbacv1.ClusterRoleBinding{ - "default/crb1": makeCRB(c.opts.name, c.opts.refKind, c.opts.refName), - } -} - -func (c *cr) ListClusterRoles() map[string]*rbacv1.ClusterRole { - return map[string]*rbacv1.ClusterRole{ - c.name: makeCR(c.name), - } -} - -func (c *cr) ListRoles() map[string]*rbacv1.Role { - return map[string]*rbacv1.Role{ - "default/ro1": makeRO("ro1"), - } -} - -func (c *cr) ListRoleBindings() map[string]*rbacv1.RoleBinding { - return map[string]*rbacv1.RoleBinding{ - "default/rb1": makeRB("rb1", "ClusterRole", "cr1"), - } -} - -func (c *cr) RoleRefs(refs *sync.Map) { - refs.Store(cache.ResFqn(cache.ClusterRoleKey, "cr2"), internal.AllKeys) -} -func (c *cr) ClusterRoleRefs(refs *sync.Map) { - refs.Store(cache.ResFqn(cache.ClusterRoleKey, "cr1"), internal.AllKeys) -} -func (c *cr) ClusterRoleBindingRefs(*sync.Map) {} - -func makeRB(name, refKind, refName string) *rbacv1.RoleBinding { - return &rbacv1.RoleBinding{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: "default", - }, - RoleRef: rbacv1.RoleRef{ - Kind: refKind, - Name: refName, - }, - } -} - -func makeRO(n string) *rbacv1.Role { - return &rbacv1.Role{ - ObjectMeta: metav1.ObjectMeta{ - Name: n, - Namespace: "default", - }, - } -} diff --git a/internal/sanitize/crb.go b/internal/sanitize/crb.go deleted file mode 100644 index ce3148dc..00000000 --- a/internal/sanitize/crb.go +++ /dev/null @@ -1,76 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package sanitize - -import ( - "context" - - "github.com/derailed/popeye/internal" - "github.com/derailed/popeye/internal/cache" - "github.com/derailed/popeye/internal/issues" - rbacv1 "k8s.io/api/rbac/v1" -) - -type ( - // ClusterRoleLister list out CRs. - ClusterRoleLister interface { - ListClusterRoles() map[string]*rbacv1.ClusterRole - } - - // RoleLister list out roles. - RoleLister interface { - ListRoles() map[string]*rbacv1.Role - } - - // CRBLister represents a cluster role lister. - CRBLister interface { - ClusterRoleBindingLister - ClusterRoleLister - RoleLister - } - - // ClusterRoleBinding tracks ClusterRoleBinding sanitization. - ClusterRoleBinding struct { - *issues.Collector - CRBLister - } -) - -// NewClusterRoleBinding returns a new ClusterRoleBinding sanitizer. -func NewClusterRoleBinding(c *issues.Collector, lister CRBLister) *ClusterRoleBinding { - return &ClusterRoleBinding{ - Collector: c, - CRBLister: lister, - } -} - -// Sanitize a configmap. -func (c *ClusterRoleBinding) Sanitize(ctx context.Context) error { - c.checkInUse(ctx) - - return nil -} - -func (c *ClusterRoleBinding) checkInUse(ctx context.Context) { - for fqn, crb := range c.ListClusterRoleBindings() { - c.InitOutcome(fqn) - ctx = internal.WithFQN(ctx, fqn) - - switch crb.RoleRef.Kind { - case "ClusterRole": - if _, ok := c.ListClusterRoles()[crb.RoleRef.Name]; !ok { - c.AddCode(ctx, 1300, crb.RoleRef.Kind, crb.RoleRef.Name) - } - case "Role": - rFQN := cache.FQN(crb.Namespace, crb.RoleRef.Name) - if _, ok := c.ListRoles()[rFQN]; !ok { - c.AddCode(ctx, 1300, crb.RoleRef.Kind, rFQN) - } - } - - if c.NoConcerns(fqn) && c.Config.ExcludeFQN(internal.MustExtractSectionGVR(ctx), fqn) { - c.ClearOutcome(fqn) - } - } -} diff --git a/internal/sanitize/crb_test.go b/internal/sanitize/crb_test.go deleted file mode 100644 index bd736411..00000000 --- a/internal/sanitize/crb_test.go +++ /dev/null @@ -1,93 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package sanitize - -import ( - "sync" - "testing" - - "github.com/derailed/popeye/internal/issues" - "github.com/derailed/popeye/pkg/config" - "github.com/stretchr/testify/assert" - rbacv1 "k8s.io/api/rbac/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -func TestCRBSanitize(t *testing.T) { - uu := map[string]struct { - lister CRBLister - key string - issues []config.ID - }{ - "exists": { - key: "crb1", - lister: makeCRBLister(crbOpts{name: "crb1", refKind: "ClusterRole", refName: "cr1"}), - }, - "not_exists": { - key: "crb1", - lister: makeCRBLister(crbOpts{name: "crb1", refKind: "ClusterRole", refName: "blah"}), - issues: []config.ID{1300}, - }, - } - - ctx := makeContext("crbs", "crb") - for k := range uu { - u := uu[k] - t.Run(k, func(t *testing.T) { - c := NewClusterRoleBinding(issues.NewCollector(loadCodes(t), makeConfig(t)), u.lister) - - assert.Nil(t, c.Sanitize(ctx)) - validateIssues(t, u.key, c.Outcome(), u.issues) - }) - } -} - -// ---------------------------------------------------------------------------- -// Helpers... - -type crbOpts struct { - name, refKind, refName string -} - -type crb struct { - name string - opts crbOpts -} - -var _ CRBLister = (*crb)(nil) - -func makeCRBLister(opts crbOpts) *crb { - return &crb{name: "crb1", opts: opts} -} - -func (c *crb) ListClusterRoleBindings() map[string]*rbacv1.ClusterRoleBinding { - return map[string]*rbacv1.ClusterRoleBinding{ - c.opts.name: makeCRB(c.opts.name, c.opts.refKind, c.opts.refName), - } -} - -func (c *crb) ListClusterRoles() map[string]*rbacv1.ClusterRole { - return map[string]*rbacv1.ClusterRole{ - "cr1": makeCR("cr1"), - "cr2": makeCR("cr2"), - } -} - -func (c *crb) ListRoles() map[string]*rbacv1.Role { - return map[string]*rbacv1.Role{ - "default/ro1": makeRO("ro1"), - } -} - -func (c *crb) ClusterRoleRefs(*sync.Map) {} -func (c *crb) ClusterRoleBindingRefs(*sync.Map) {} - -func makeCR(n string) *rbacv1.ClusterRole { - return &rbacv1.ClusterRole{ - ObjectMeta: metav1.ObjectMeta{ - Name: n, - Namespace: "default", - }, - } -} diff --git a/internal/sanitize/dp.go b/internal/sanitize/dp.go deleted file mode 100644 index 96f8c6a4..00000000 --- a/internal/sanitize/dp.go +++ /dev/null @@ -1,244 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package sanitize - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "path" - "strings" - - "github.com/derailed/popeye/internal" - "github.com/derailed/popeye/internal/client" - "github.com/derailed/popeye/internal/issues" - appsv1 "k8s.io/api/apps/v1" - v1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/resource" - mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" -) - -// DeploymentLister list available Deployments on a cluster. -type DeploymentLister interface { - ListDeployments() map[string]*appsv1.Deployment -} - -// DPLister represents deployments and deps listers. -type DPLister interface { - PodLimiter - PodsMetricsLister - PodSelectorLister - ConfigLister - DeploymentLister - ListServiceAccounts() map[string]*v1.ServiceAccount -} - -// Deployment tracks Deployment sanitization. -type Deployment struct { - *issues.Collector - DPLister -} - -// NewDeployment returns a new sanitizer. -func NewDeployment(co *issues.Collector, lister DPLister) *Deployment { - return &Deployment{ - Collector: co, - DPLister: lister, - } -} - -// Sanitize cleanse the resource. -func (d *Deployment) Sanitize(ctx context.Context) error { - over := pullOverAllocs(ctx) - for fqn, dp := range d.ListDeployments() { - d.InitOutcome(fqn) - ctx = internal.WithFQN(ctx, fqn) - - d.checkDeprecation(ctx, dp) - d.checkDeployment(ctx, dp) - d.checkContainers(ctx, dp.Spec.Template.Spec) - pmx := client.PodsMetrics{} - podsMetrics(d, pmx) - d.checkUtilization(ctx, over, dp, pmx) - - if d.NoConcerns(fqn) && d.Config.ExcludeFQN(internal.MustExtractSectionGVR(ctx), fqn) { - d.ClearOutcome(fqn) - } - } - - return nil -} - -func (d *Deployment) checkDeprecation(ctx context.Context, dp *appsv1.Deployment) { - const current = "apps/v1" - - fqn := internal.MustExtractFQN(ctx) - rev, err := resourceRev(fqn, "Deployment", dp.Annotations) - if err != nil { - if rev = revFromLink(dp.SelfLink); rev == "" { - return - } - } - if rev != current { - d.AddCode(ctx, 403, "Deployment", rev, current) - } -} - -// CheckDeployment checks if deployment contract is currently happy or not. -func (d *Deployment) checkDeployment(ctx context.Context, dp *appsv1.Deployment) { - if dp.Spec.Replicas == nil || (dp.Spec.Replicas != nil && *dp.Spec.Replicas == 0) { - d.AddCode(ctx, 500) - return - } - - if dp.Spec.Replicas != nil && *dp.Spec.Replicas != dp.Status.AvailableReplicas { - d.AddCode(ctx, 501, *dp.Spec.Replicas, dp.Status.AvailableReplicas) - } - - if dp.Spec.Template.Spec.ServiceAccountName == "" { - return - } - - if _, ok := d.ListServiceAccounts()[client.FQN(dp.Namespace, dp.Spec.Template.Spec.ServiceAccountName)]; !ok { - d.AddCode(ctx, 507, dp.Spec.Template.Spec.ServiceAccountName) - } -} - -// CheckContainers runs thru deployment template and checks pod configuration. -func (d *Deployment) checkContainers(ctx context.Context, spec v1.PodSpec) { - c := NewContainer(internal.MustExtractFQN(ctx), d) - for _, co := range spec.InitContainers { - c.sanitize(ctx, co, false) - } - for _, co := range spec.Containers { - c.sanitize(ctx, co, false) - } -} - -// CheckUtilization checks deployments requested resources vs current utilization. -func (d *Deployment) checkUtilization(ctx context.Context, over bool, dp *appsv1.Deployment, pmx client.PodsMetrics) { - mx := d.deploymentUsage(dp, pmx) - if mx.RequestCPU.IsZero() && mx.RequestMEM.IsZero() { - return - } - checkCPU(ctx, d, over, mx) - checkMEM(ctx, d, over, mx) -} - -// DeploymentUsage finds deployment running pods and compute current vs requested resource usage. -func (d *Deployment) deploymentUsage(dp *appsv1.Deployment, pmx client.PodsMetrics) ConsumptionMetrics { - var mx ConsumptionMetrics - for pfqn, pod := range d.ListPodsBySelector(dp.Namespace, dp.Spec.Selector) { - cpu, mem := computePodResources(pod.Spec) - mx.QOS = pod.Status.QOSClass - mx.RequestCPU.Add(cpu) - mx.RequestMEM.Add(mem) - - ccx, ok := pmx[pfqn] - if !ok { - continue - } - for _, cx := range ccx { - mx.CurrentCPU.Add(cx.CurrentCPU) - mx.CurrentMEM.Add(cx.CurrentMEM) - } - } - - return mx -} - -// Helpers... - -// PullOverAllocs check for over allocation setting in context. -func pullOverAllocs(ctx context.Context) bool { - over := ctx.Value(internal.KeyOverAllocs) - if over == nil { - return false - } - return over.(bool) -} - -// PodsMetrics gathers pod's container metrics from metrics server. -func podsMetrics(l PodsMetricsLister, pmx client.PodsMetrics) { - for fqn, mx := range l.ListPodsMetrics() { - cmx := client.ContainerMetrics{} - podToContainerMetrics(mx, cmx) - pmx[fqn] = cmx - } -} - -// PodToContainerMetrics gather pod's container metrics from metrics server. -func podToContainerMetrics(pmx *mv1beta1.PodMetrics, cmx client.ContainerMetrics) { - for _, co := range pmx.Containers { - cmx[co.Name] = client.Metrics{ - CurrentCPU: *co.Usage.Cpu(), - CurrentMEM: *co.Usage.Memory(), - } - } -} - -func computePodResources(spec v1.PodSpec) (cpu, mem resource.Quantity) { - for _, co := range spec.InitContainers { - c, m, _ := containerResources(co) - if c != nil { - cpu.Add(*c) - } - if m != nil { - mem.Add(*m) - } - } - - for _, co := range spec.Containers { - c, m, _ := containerResources(co) - if c != nil { - cpu.Add(*c) - } - if m != nil { - mem.Add(*m) - } - } - - return -} - -// ResourceRev is resource was deployed via kubectl check annotation for manifest rev. -func resourceRev(fqn string, kind string, a map[string]string) (string, error) { - raw, ok := a["kubectl.kubernetes.io/last-applied-configuration"] - if !ok { - return "", fmt.Errorf("Raw resource manifest not available for %s", fqn) - } - - var m map[string]interface{} - if err := json.Unmarshal([]byte(raw), &m); err != nil { - return "", err - } - if m["kind"] == kind { - return m["apiVersion"].(string), nil - } - - return "", errors.New("no matching resource kind") -} - -// RevFromLink. extract resource version from selflink. -func revFromLink(link string) string { - tokens := strings.Split(link, "/") - if len(tokens) < 4 { - return "" - } - if isVersion(tokens[2]) { - return tokens[2] - } - return path.Join(tokens[2], tokens[3]) -} - -func isVersion(s string) bool { - vers := []string{"v1", "v1beta1", "v1beta2", "v2beta1", "v2beta2"} - for _, v := range vers { - if s == v { - return true - } - } - return false -} diff --git a/internal/sanitize/dp_test.go b/internal/sanitize/dp_test.go deleted file mode 100644 index 1f3d0f2d..00000000 --- a/internal/sanitize/dp_test.go +++ /dev/null @@ -1,460 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package sanitize - -import ( - "context" - "testing" - - "github.com/derailed/popeye/internal" - "github.com/derailed/popeye/internal/cache" - "github.com/derailed/popeye/internal/client" - "github.com/derailed/popeye/internal/issues" - "github.com/derailed/popeye/pkg/config" - "github.com/stretchr/testify/assert" - appsv1 "k8s.io/api/apps/v1" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" -) - -func TestRevFromLink(t *testing.T) { - uu := map[string]struct { - l, e string - }{ - "single.namespaced": { - "/api/v1/namespaces/fred/pods/blee", - "v1", - }, - "single.notnamespaced": { - "/api/v1/pv/blee", - "v1", - }, - "multi.namespaced": { - "/api/extensions/v1beta1/namespaces/fred/pods/blee", - "extensions/v1beta1", - }, - "multi.notnamespaced": { - "/api/rbac.authorization.k8s.io/v1beta1/blee/duh", - "rbac.authorization.k8s.io/v1beta1", - }, - } - - for k := range uu { - u := uu[k] - t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, revFromLink(u.l)) - }) - } -} - -func TestDPSanitize(t *testing.T) { - uu := map[string]struct { - lister DPLister - issues issues.Issues - }{ - "good": { - lister: makeDPLister(dpOpts{ - rev: "apps/v1", - reps: 1, - availReps: 1, - coOpts: coOpts{ - image: "fred:0.0.1", - rcpu: "10m", - rmem: "10Mi", - lcpu: "10m", - lmem: "10Mi", - }, - ccpu: "10m", - cmem: "10Mi", - }), - issues: issues.Issues{}, - }, - "deprecated": { - lister: makeDPLister(dpOpts{ - rev: "extensions/v1", - reps: 1, - availReps: 1, - coOpts: coOpts{ - image: "fred:0.0.1", - rcpu: "10m", - rmem: "10Mi", - lcpu: "10m", - lmem: "10Mi", - }, - ccpu: "10m", - cmem: "10Mi", - }), - issues: issues.Issues{ - issues.New( - client.NewGVR("apps/v1/deployments"), - issues.Root, - config.WarnLevel, - `[POP-403] Deprecated Deployment API group "extensions/v1". Use "apps/v1" instead`, - ), - }, - }, - "zeroReps": { - lister: makeDPLister(dpOpts{ - rev: "apps/v1", - reps: 0, - availReps: 1, - coOpts: coOpts{ - image: "fred:0.0.1", - rcpu: "10m", - rmem: "10Mi", - lcpu: "10m", - lmem: "10Mi", - }, - ccpu: "10m", - cmem: "10Mi", - }), - issues: issues.Issues{ - issues.New(client.NewGVR("apps/v1/deployments"), issues.Root, config.WarnLevel, "[POP-500] Zero scale detected"), - }, - }, - "noAvailReps": { - lister: makeDPLister(dpOpts{ - rev: "apps/v1", - reps: 1, - availReps: 0, - collisions: 0, - coOpts: coOpts{ - image: "fred:0.0.1", - rcpu: "10m", - rmem: "10Mi", - lcpu: "10m", - lmem: "10Mi", - }, - ccpu: "10m", - cmem: "10Mi", - }), - issues: issues.Issues{ - issues.New(client.NewGVR("apps/v1/deployments"), issues.Root, config.ErrorLevel, "[POP-501] Unhealthy 1 desired but have 0 available"), - }, - }, - } - - ctx := makeContext("apps/v1/deployments", "deployment") - for k := range uu { - u := uu[k] - t.Run(k, func(t *testing.T) { - dp := NewDeployment(issues.NewCollector(loadCodes(t), makeConfig(t)), u.lister) - - assert.Nil(t, dp.Sanitize(ctx)) - assert.Equal(t, u.issues, dp.Outcome()["default/d1"]) - }) - } -} - -func TestDPSanitizeUtilization(t *testing.T) { - uu := map[string]struct { - lister DPLister - issues issues.Issues - }{ - "bestEffort": { - lister: makeDPLister(dpOpts{ - rev: "apps/v1", - reps: 2, - availReps: 2, - collisions: 0, - coOpts: coOpts{ - image: "fred:0.0.1", - }, - ccpu: "10m", - cmem: "10Mi", - }), - issues: issues.Issues{ - issues.New(client.NewGVR("containers"), "i1", config.WarnLevel, "[POP-106] No resources requests/limits defined"), - issues.New(client.NewGVR("containers"), "c1", config.WarnLevel, "[POP-106] No resources requests/limits defined"), - }, - }, - "cpuUnderBurstable": { - lister: makeDPLister(dpOpts{ - rev: "apps/v1", - reps: 2, - availReps: 2, - collisions: 0, - coOpts: coOpts{ - image: "fred:0.0.1", - rcpu: "5m", - rmem: "10Mi", - lcpu: "10m", - lmem: "10Mi", - }, - ccpu: "10m", - cmem: "10Mi", - }), - issues: issues.Issues{ - issues.New(client.NewGVR("containers"), issues.Root, config.WarnLevel, "[POP-503] At current load, CPU under allocated. Current:20m vs Requested:10m (200.00%)"), - }, - }, - "cpuUnderGuaranteed": { - lister: makeDPLister(dpOpts{ - rev: "apps/v1", - reps: 2, - availReps: 2, - collisions: 0, - coOpts: coOpts{ - image: "fred:0.0.1", - rcpu: "5m", - rmem: "10Mi", - lcpu: "5m", - lmem: "10Mi", - }, - ccpu: "10m", - cmem: "10Mi", - }), - issues: issues.Issues{ - issues.New(client.NewGVR("containers"), issues.Root, config.WarnLevel, "[POP-503] At current load, CPU under allocated. Current:20m vs Requested:10m (200.00%)"), - }, - }, - "cpuOverBustable": { - lister: makeDPLister(dpOpts{ - rev: "apps/v1", - reps: 2, - availReps: 2, - collisions: 0, - coOpts: coOpts{ - image: "fred:0.0.1", - rcpu: "30m", - rmem: "10Mi", - lcpu: "50m", - lmem: "10Mi", - }, - ccpu: "10m", - cmem: "10Mi", - }), - issues: issues.Issues{ - issues.New(client.NewGVR("containers"), issues.Root, config.WarnLevel, "[POP-504] At current load, CPU over allocated. Current:20m vs Requested:60m (300.00%)"), - }, - }, - "cpuOverGuaranteed": { - lister: makeDPLister(dpOpts{ - rev: "apps/v1", - reps: 2, - availReps: 2, - collisions: 0, - coOpts: coOpts{ - image: "fred:0.0.1", - rcpu: "30m", - rmem: "10Mi", - lcpu: "30m", - lmem: "10Mi", - }, - ccpu: "10m", - cmem: "10Mi", - }), - issues: issues.Issues{ - issues.New(client.NewGVR("containers"), issues.Root, config.WarnLevel, "[POP-504] At current load, CPU over allocated. Current:20m vs Requested:60m (300.00%)"), - }, - }, - "memUnderBurstable": { - lister: makeDPLister(dpOpts{ - rev: "apps/v1", - reps: 2, - availReps: 2, - collisions: 0, - coOpts: coOpts{ - image: "fred:0.0.1", - rcpu: "10m", - rmem: "5Mi", - lcpu: "20m", - lmem: "20Mi", - }, - ccpu: "10m", - cmem: "10Mi", - }), - issues: issues.Issues{ - issues.New(client.NewGVR("containers"), issues.Root, config.WarnLevel, "[POP-505] At current load, Memory under allocated. Current:20Mi vs Requested:10Mi (200.00%)"), - }, - }, - "memUnderGuaranteed": { - lister: makeDPLister(dpOpts{ - rev: "apps/v1", - reps: 2, - availReps: 2, - collisions: 0, - coOpts: coOpts{ - image: "fred:0.0.1", - rcpu: "10m", - rmem: "5Mi", - lcpu: "10m", - lmem: "5Mi", - }, - ccpu: "10m", - cmem: "10Mi", - }), - issues: issues.Issues{ - issues.New(client.NewGVR("containers"), issues.Root, config.WarnLevel, "[POP-505] At current load, Memory under allocated. Current:20Mi vs Requested:10Mi (200.00%)"), - }, - }, - "memOverBurstable": { - lister: makeDPLister(dpOpts{ - rev: "apps/v1", - reps: 2, - availReps: 2, - collisions: 0, - coOpts: coOpts{ - image: "fred:0.0.1", - rcpu: "10m", - rmem: "30Mi", - lcpu: "20m", - lmem: "60Mi", - }, - ccpu: "10m", - cmem: "10Mi", - }), - issues: issues.Issues{ - issues.New(client.NewGVR("containers"), issues.Root, config.WarnLevel, "[POP-506] At current load, Memory over allocated. Current:20Mi vs Requested:60Mi (300.00%)"), - }, - }, - "memOverGuaranteed": { - lister: makeDPLister(dpOpts{ - rev: "apps/v1", - reps: 2, - availReps: 2, - collisions: 0, - coOpts: coOpts{ - image: "fred:0.0.1", - rcpu: "10m", - rmem: "30Mi", - lcpu: "10m", - lmem: "30Mi", - }, - ccpu: "10m", - cmem: "10Mi", - }), - issues: issues.Issues{ - issues.New(client.NewGVR("containers"), issues.Root, config.WarnLevel, "[POP-506] At current load, Memory over allocated. Current:20Mi vs Requested:60Mi (300.00%)"), - }, - }, - } - - ctx := makeContext("containers", "deploy") - ctx = context.WithValue(ctx, internal.KeyOverAllocs, true) - for k := range uu { - u := uu[k] - t.Run(k, func(t *testing.T) { - dp := NewDeployment(issues.NewCollector(loadCodes(t), makeConfig(t)), u.lister) - - assert.Nil(t, dp.Sanitize(ctx)) - assert.Equal(t, u.issues, dp.Outcome()["default/d1"]) - }) - } -} - -// ---------------------------------------------------------------------------- -// Helpers... - -type ( - dpOpts struct { - coOpts - rev string - reps int32 - availReps int32 - collisions int32 - ccpu, cmem string - } - - dp struct { - name string - opts dpOpts - } -) - -var _ DPLister = (*dp)(nil) - -func makeDPLister(opts dpOpts) *dp { - return &dp{ - name: "d1", - opts: opts, - } -} - -func (d *dp) CPUResourceLimits() config.Allocations { - return config.Allocations{ - UnderPerc: 100, - OverPerc: 50, - } -} - -func (d *dp) MEMResourceLimits() config.Allocations { - return config.Allocations{ - UnderPerc: 100, - OverPerc: 50, - } -} - -func (d *dp) ListPodsBySelector(ns string, sel *metav1.LabelSelector) map[string]*v1.Pod { - return map[string]*v1.Pod{ - "default/p1": makeFullPod(podOpts{ - coOpts: d.opts.coOpts, - }), - } -} - -func (d *dp) RestartsLimit() int { - return 10 -} - -func (d *dp) PodCPULimit() float64 { - return 100 -} - -func (d *dp) PodMEMLimit() float64 { - return 100 -} - -func (d *dp) ListServiceAccounts() map[string]*v1.ServiceAccount { - return nil -} - -func (d *dp) ListPodsMetrics() map[string]*mv1beta1.PodMetrics { - return map[string]*mv1beta1.PodMetrics{ - cache.FQN("default", "p1"): makeMxPod(d.opts.ccpu, d.opts.cmem), - } -} - -func (d *dp) ListDeployments() map[string]*appsv1.Deployment { - return map[string]*appsv1.Deployment{ - cache.FQN("default", d.name): makeDP(d.name, d.opts), - } -} - -func (d *dp) DeploymentPreferredRev() string { - return "apps/v1" -} - -func makeDP(n string, o dpOpts) *appsv1.Deployment { - return &appsv1.Deployment{ - ObjectMeta: metav1.ObjectMeta{ - Name: n, - Namespace: "default", - SelfLink: "/api/" + o.rev, - }, - Spec: appsv1.DeploymentSpec{ - Replicas: &o.reps, - Selector: &metav1.LabelSelector{ - MatchLabels: map[string]string{ - "fred": "blee", - }, - }, - Template: v1.PodTemplateSpec{ - Spec: v1.PodSpec{ - InitContainers: []v1.Container{ - makeContainer("i1", o.coOpts), - }, - Containers: []v1.Container{ - makeContainer("c1", o.coOpts), - }, - }, - }, - }, - Status: appsv1.DeploymentStatus{ - AvailableReplicas: o.availReps, - CollisionCount: &o.collisions, - }, - } -} diff --git a/internal/sanitize/ds.go b/internal/sanitize/ds.go deleted file mode 100644 index 7f2085b9..00000000 --- a/internal/sanitize/ds.go +++ /dev/null @@ -1,134 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package sanitize - -import ( - "context" - - "github.com/derailed/popeye/internal" - "github.com/derailed/popeye/internal/client" - "github.com/derailed/popeye/internal/issues" - appsv1 "k8s.io/api/apps/v1" - v1 "k8s.io/api/core/v1" -) - -type ( - // DaemonSet tracks DaemonSet sanitization. - DaemonSet struct { - *issues.Collector - DaemonSetLister - } - - // DaemonLister list DaemonSets. - DaemonLister interface { - ListDaemonSets() map[string]*appsv1.DaemonSet - ListServiceAccounts() map[string]*v1.ServiceAccount - } - - // DaemonSetLister list available DaemonSets on a cluster. - DaemonSetLister interface { - PodLimiter - PodsMetricsLister - PodSelectorLister - ConfigLister - DaemonLister - } -) - -// NewDaemonSet returns a new sanitizer. -func NewDaemonSet(co *issues.Collector, lister DaemonSetLister) *DaemonSet { - return &DaemonSet{ - Collector: co, - DaemonSetLister: lister, - } -} - -// Sanitize cleanse the resource. -func (d *DaemonSet) Sanitize(ctx context.Context) error { - over := pullOverAllocs(ctx) - for fqn, ds := range d.ListDaemonSets() { - d.InitOutcome(fqn) - ctx = internal.WithFQN(ctx, fqn) - - d.checkDaemonSet(ctx, ds) - d.checkDeprecation(ctx, ds) - d.checkContainers(ctx, ds.Spec.Template.Spec) - pmx := client.PodsMetrics{} - podsMetrics(d, pmx) - d.checkUtilization(ctx, over, ds, pmx) - - if d.NoConcerns(fqn) && d.Config.ExcludeFQN(internal.MustExtractSectionGVR(ctx), fqn) { - d.ClearOutcome(fqn) - } - } - - return nil -} - -func (d *DaemonSet) checkDaemonSet(ctx context.Context, ds *appsv1.DaemonSet) { - if ds.Spec.Template.Spec.ServiceAccountName == "" { - return - } - if _, ok := d.ListServiceAccounts()[client.FQN(ds.Namespace, ds.Spec.Template.Spec.ServiceAccountName)]; !ok { - d.AddCode(ctx, 507, ds.Spec.Template.Spec.ServiceAccountName) - } -} - -func (d *DaemonSet) checkDeprecation(ctx context.Context, ds *appsv1.DaemonSet) { - const current = "apps/v1" - - rev, err := resourceRev(internal.MustExtractFQN(ctx), "DaemonSet", ds.Annotations) - if err != nil { - if rev = revFromLink(ds.SelfLink); rev == "" { - return - } - } - if rev != current { - d.AddCode(ctx, 403, "DaemonSet", rev, current) - } -} - -// CheckContainers runs thru deployment template and checks pod configuration. -func (d *DaemonSet) checkContainers(ctx context.Context, spec v1.PodSpec) { - c := NewContainer(internal.MustExtractFQN(ctx), d) - for _, co := range spec.InitContainers { - c.sanitize(ctx, co, false) - } - for _, co := range spec.Containers { - c.sanitize(ctx, co, false) - } -} - -// CheckUtilization checks deployments requested resources vs current utilization. -func (d *DaemonSet) checkUtilization(ctx context.Context, over bool, ds *appsv1.DaemonSet, pmx client.PodsMetrics) { - mx := d.daemonsetUsage(ds, pmx) - if mx.RequestCPU.IsZero() && mx.RequestMEM.IsZero() { - return - } - - checkCPU(ctx, d, over, mx) - checkMEM(ctx, d, over, mx) -} - -// DaemonSetUsage finds deployment running pods and compute current vs requested resource usage. -func (d *DaemonSet) daemonsetUsage(ds *appsv1.DaemonSet, pmx client.PodsMetrics) ConsumptionMetrics { - var mx ConsumptionMetrics - for pfqn, pod := range d.ListPodsBySelector(ds.Namespace, ds.Spec.Selector) { - cpu, mem := computePodResources(pod.Spec) - mx.QOS = pod.Status.QOSClass - mx.RequestCPU.Add(cpu) - mx.RequestMEM.Add(mem) - - ccx, ok := pmx[pfqn] - if !ok { - continue - } - for _, cx := range ccx { - mx.CurrentCPU.Add(cx.CurrentCPU) - mx.CurrentMEM.Add(cx.CurrentMEM) - } - } - - return mx -} diff --git a/internal/sanitize/ds_test.go b/internal/sanitize/ds_test.go deleted file mode 100644 index ebaa1219..00000000 --- a/internal/sanitize/ds_test.go +++ /dev/null @@ -1,344 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package sanitize - -import ( - "context" - "testing" - - "github.com/derailed/popeye/internal" - "github.com/derailed/popeye/internal/cache" - "github.com/derailed/popeye/internal/client" - "github.com/derailed/popeye/internal/issues" - "github.com/derailed/popeye/pkg/config" - "github.com/stretchr/testify/assert" - appsv1 "k8s.io/api/apps/v1" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" -) - -func TestDSSanitize(t *testing.T) { - uu := map[string]struct { - lister DaemonSetLister - issues issues.Issues - }{ - "good": { - lister: makeDSLister(dsOpts{ - coOpts: coOpts{ - image: "fred:0.0.1", - rcpu: "10m", - rmem: "10Mi", - lcpu: "10m", - lmem: "10Mi", - }, - ccpu: "10m", - cmem: "10Mi", - rev: "apps/v1", - }), - issues: issues.Issues{}, - }, - "deprecated": { - lister: makeDSLister(dsOpts{ - coOpts: coOpts{ - image: "fred:0.0.1", - rcpu: "10m", - rmem: "10Mi", - lcpu: "10m", - lmem: "10Mi", - }, - ccpu: "10m", - cmem: "10Mi", - rev: "extensions/v1", - }), - issues: issues.Issues{ - issues.Issue{GVR: "apps/v1/deployments", Group: "__root__", Level: config.WarnLevel, Message: `[POP-403] Deprecated DaemonSet API group "extensions/v1". Use "apps/v1" instead`}, - }, - }, - } - - ctx := makeContext("apps/v1/deployments", "ds") - for k := range uu { - u := uu[k] - t.Run(k, func(t *testing.T) { - ds := NewDaemonSet(issues.NewCollector(loadCodes(t), makeConfig(t)), u.lister) - - assert.Nil(t, ds.Sanitize(ctx)) - assert.Equal(t, u.issues, ds.Outcome()["default/d1"]) - }) - } -} - -func TestDSSanitizeUtilization(t *testing.T) { - uu := map[string]struct { - lister DaemonSetLister - issues issues.Issues - }{ - "bestEffort": { - lister: makeDSLister(dsOpts{ - coOpts: coOpts{ - image: "fred:0.0.1", - }, - ccpu: "10m", - cmem: "10Mi", - rev: "apps/v1", - }), - issues: issues.Issues{ - issues.New(client.NewGVR("containers"), "i1", config.WarnLevel, "[POP-106] No resources requests/limits defined"), - issues.New(client.NewGVR("containers"), "c1", config.WarnLevel, "[POP-106] No resources requests/limits defined"), - }, - }, - "cpuUnderBurstable": { - lister: makeDSLister(dsOpts{ - coOpts: coOpts{ - image: "fred:0.0.1", - rcpu: "5m", - rmem: "10Mi", - lcpu: "10m", - lmem: "10Mi", - }, - ccpu: "10m", - cmem: "10Mi", - rev: "apps/v1", - }), - issues: issues.Issues{ - issues.New(client.NewGVR("containers"), issues.Root, config.WarnLevel, "[POP-503] At current load, CPU under allocated. Current:20m vs Requested:10m (200.00%)"), - }, - }, - "cpuUnderGuaranteed": { - lister: makeDSLister(dsOpts{ - coOpts: coOpts{ - image: "fred:0.0.1", - rcpu: "5m", - rmem: "10Mi", - lcpu: "5m", - lmem: "10Mi", - }, - ccpu: "10m", - cmem: "10Mi", - rev: "apps/v1", - }), - issues: issues.Issues{ - issues.New(client.NewGVR("containers"), issues.Root, config.WarnLevel, "[POP-503] At current load, CPU under allocated. Current:20m vs Requested:10m (200.00%)"), - }, - }, - // c=20 r=60 20/60=1/3 over is 50% req=3*c 33 > 100 - // c=60 r=20 60/20 3 under - "cpuOverBustable": { - lister: makeDSLister(dsOpts{ - coOpts: coOpts{ - image: "fred:0.0.1", - rcpu: "30m", - rmem: "10Mi", - lcpu: "50m", - lmem: "10Mi", - }, - ccpu: "10m", - cmem: "10Mi", - rev: "apps/v1", - }), - issues: issues.Issues{ - issues.New(client.NewGVR("containers"), issues.Root, config.WarnLevel, "[POP-504] At current load, CPU over allocated. Current:20m vs Requested:60m (300.00%)"), - }, - }, - "cpuOverGuaranteed": { - lister: makeDSLister(dsOpts{ - coOpts: coOpts{ - image: "fred:0.0.1", - rcpu: "30m", - rmem: "10Mi", - lcpu: "30m", - lmem: "10Mi", - }, - ccpu: "10m", - cmem: "10Mi", - rev: "apps/v1", - }), - issues: issues.Issues{ - issues.New(client.NewGVR("containers"), issues.Root, config.WarnLevel, "[POP-504] At current load, CPU over allocated. Current:20m vs Requested:60m (300.00%)"), - }, - }, - "memUnderBurstable": { - lister: makeDSLister(dsOpts{ - coOpts: coOpts{ - image: "fred:0.0.1", - rcpu: "10m", - rmem: "5Mi", - lcpu: "20m", - lmem: "20Mi", - }, - ccpu: "10m", - cmem: "10Mi", - rev: "apps/v1", - }), - issues: issues.Issues{ - issues.New(client.NewGVR("containers"), issues.Root, config.WarnLevel, "[POP-505] At current load, Memory under allocated. Current:20Mi vs Requested:10Mi (200.00%)"), - }, - }, - "memUnderGuaranteed": { - lister: makeDSLister(dsOpts{ - coOpts: coOpts{ - image: "fred:0.0.1", - rcpu: "10m", - rmem: "5Mi", - lcpu: "10m", - lmem: "5Mi", - }, - ccpu: "10m", - cmem: "10Mi", - rev: "apps/v1", - }), - issues: issues.Issues{ - issues.New(client.NewGVR("containers"), issues.Root, config.WarnLevel, "[POP-505] At current load, Memory under allocated. Current:20Mi vs Requested:10Mi (200.00%)"), - }, - }, - "memOverBurstable": { - lister: makeDSLister(dsOpts{ - coOpts: coOpts{ - image: "fred:0.0.1", - rcpu: "10m", - rmem: "30Mi", - lcpu: "20m", - lmem: "60Mi", - }, - ccpu: "10m", - cmem: "10Mi", - rev: "apps/v1", - }), - issues: issues.Issues{ - issues.New(client.NewGVR("containers"), issues.Root, config.WarnLevel, "[POP-506] At current load, Memory over allocated. Current:20Mi vs Requested:60Mi (300.00%)"), - }, - }, - "memOverGuaranteed": { - lister: makeDSLister(dsOpts{ - coOpts: coOpts{ - image: "fred:0.0.1", - rcpu: "10m", - rmem: "30Mi", - lcpu: "10m", - lmem: "30Mi", - }, - ccpu: "10m", - cmem: "10Mi", - rev: "apps/v1", - }), - issues: issues.Issues{ - issues.New(client.NewGVR("containers"), issues.Root, config.WarnLevel, "[POP-506] At current load, Memory over allocated. Current:20Mi vs Requested:60Mi (300.00%)"), - }, - }, - } - - ctx := makeContext("containers", "ds") - ctx = context.WithValue(ctx, internal.KeyOverAllocs, true) - for k := range uu { - u := uu[k] - t.Run(k, func(t *testing.T) { - ds := NewDaemonSet(issues.NewCollector(loadCodes(t), makeConfig(t)), u.lister) - - assert.Nil(t, ds.Sanitize(ctx)) - assert.Equal(t, u.issues, ds.Outcome()["default/d1"]) - }) - } -} - -// ---------------------------------------------------------------------------- -// Helpers... - -type ( - dsOpts struct { - coOpts - rev string - ccpu, cmem string - } - - ds struct { - name string - opts dsOpts - } -) - -func makeDSLister(opts dsOpts) *ds { - return &ds{ - name: "d1", - opts: opts, - } -} - -func (d *ds) CPUResourceLimits() config.Allocations { - return config.Allocations{ - UnderPerc: 100, - OverPerc: 50, - } -} - -func (d *ds) MEMResourceLimits() config.Allocations { - return config.Allocations{ - UnderPerc: 100, - OverPerc: 50, - } -} - -func (d *ds) ListPodsBySelector(ns string, sel *metav1.LabelSelector) map[string]*v1.Pod { - return map[string]*v1.Pod{ - "default/p1": makeFullPod(podOpts{ - coOpts: d.opts.coOpts, - }), - } -} - -func (d *ds) RestartsLimit() int { - return 10 -} - -func (d *ds) PodCPULimit() float64 { - return 100 -} - -func (d *ds) PodMEMLimit() float64 { - return 100 -} - -func (d *ds) ListServiceAccounts() map[string]*v1.ServiceAccount { - return nil -} - -func (d *ds) ListPodsMetrics() map[string]*mv1beta1.PodMetrics { - return map[string]*mv1beta1.PodMetrics{ - cache.FQN("default", "p1"): makeMxPod(d.opts.ccpu, d.opts.cmem), - } -} - -func (d *ds) ListDaemonSets() map[string]*appsv1.DaemonSet { - return map[string]*appsv1.DaemonSet{ - cache.FQN("default", d.name): makeDS(d.name, d.opts), - } -} - -func makeDS(n string, o dsOpts) *appsv1.DaemonSet { - return &appsv1.DaemonSet{ - ObjectMeta: metav1.ObjectMeta{ - Name: n, - Namespace: "default", - SelfLink: "/api/" + o.rev, - }, - Spec: appsv1.DaemonSetSpec{ - Selector: &metav1.LabelSelector{ - MatchLabels: map[string]string{ - "fred": "blee", - }, - }, - Template: v1.PodTemplateSpec{ - Spec: v1.PodSpec{ - InitContainers: []v1.Container{ - makeContainer("i1", o.coOpts), - }, - Containers: []v1.Container{ - makeContainer("c1", o.coOpts), - }, - }, - }, - }, - Status: appsv1.DaemonSetStatus{}, - } -} diff --git a/internal/sanitize/helper_test.go b/internal/sanitize/helper_test.go deleted file mode 100644 index 3c06ddf2..00000000 --- a/internal/sanitize/helper_test.go +++ /dev/null @@ -1,186 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package sanitize - -import ( - "testing" - - "github.com/stretchr/testify/assert" - v1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/resource" -) - -func TestNamepaced(t *testing.T) { - uu := []struct { - s string - ns, n string - }{ - {"fred/blee", "fred", "blee"}, - {"fred", "", "fred"}, - } - - for _, u := range uu { - ns, n := namespaced(u.s) - assert.Equal(t, u.ns, ns) - assert.Equal(t, u.n, n) - } -} - -func TestPluralOf(t *testing.T) { - uu := []struct { - n string - count int - e string - }{ - {"fred", 0, "fred"}, - {"fred", 1, "fred"}, - {"fred", 2, "freds"}, - } - - for _, u := range uu { - assert.Equal(t, u.e, pluralOf(u.n, u.count)) - } -} - -func TestToPerc(t *testing.T) { - uu := []struct { - v1, v2, e int64 - }{ - {50, 100, 50}, - {100, 0, 0}, - {100, 50, 200}, - } - - for _, u := range uu { - assert.Equal(t, u.e, ToPerc(u.v1, u.v2)) - } -} - -func TestIn(t *testing.T) { - uu := []struct { - l []string - s string - e bool - }{ - {[]string{"a", "b", "c"}, "a", true}, - {[]string{"a", "b", "c"}, "z", false}, - } - - for _, u := range uu { - assert.Equal(t, u.e, in(u.l, u.s)) - } -} - -func TestToMCRatio(t *testing.T) { - uu := []struct { - q1, q2 resource.Quantity - r float64 - }{ - {toQty("100m"), toQty("200m"), 50}, - {toQty("100m"), toQty("50m"), 200}, - {toQty("0m"), toQty("5m"), 0}, - {toQty("10m"), toQty("0m"), 0}, - } - - for _, u := range uu { - assert.Equal(t, u.r, toMCRatio(u.q1, u.q2)) - } -} - -func TestToMEMRatio(t *testing.T) { - uu := []struct { - q1, q2 resource.Quantity - r float64 - }{ - {toQty("10Mi"), toQty("20Mi"), 50}, - {toQty("20Mi"), toQty("10Mi"), 200}, - {toQty("0Mi"), toQty("5Mi"), 0}, - {toQty("10Mi"), toQty("0Mi"), 0}, - } - - for _, u := range uu { - assert.Equal(t, u.r, toMEMRatio(u.q1, u.q2)) - } -} - -func TestContainerResources(t *testing.T) { - uu := map[string]struct { - co v1.Container - cpu, mem *resource.Quantity - qos qos - }{ - "none": { - co: makeContainer("c1", coOpts{ - image: "fred:1.0.1", - }), - qos: qosBestEffort, - }, - "guaranteed": { - co: makeContainer("c1", coOpts{ - image: "fred:1.0.1", - rcpu: "100m", - rmem: "10Mi", - lcpu: "100m", - lmem: "10Mi", - }), - cpu: makeQty("100m"), - mem: makeQty("10Mi"), - qos: qosGuaranteed, - }, - "bustableLimit": { - co: makeContainer("c1", coOpts{ - image: "fred:1.0.1", - lcpu: "100m", - lmem: "10Mi", - }), - cpu: makeQty("100m"), - mem: makeQty("10Mi"), - qos: qosBurstable, - }, - "burstableRequest": { - co: makeContainer("c1", coOpts{ - image: "fred:1.0.1", - rcpu: "100m", - rmem: "10Mi", - }), - cpu: makeQty("100m"), - mem: makeQty("10Mi"), - qos: qosBurstable, - }, - } - - for k := range uu { - u := uu[k] - t.Run(k, func(t *testing.T) { - cpu, mem, qos := containerResources(u.co) - - assert.Equal(t, cpu, u.cpu) - assert.Equal(t, mem, u.mem) - assert.Equal(t, u.qos, qos) - }) - } -} - -func TestPortAsString(t *testing.T) { - uu := []struct { - port v1.ServicePort - e string - }{ - {v1.ServicePort{Protocol: v1.ProtocolTCP, Name: "p1", Port: 80}, "TCP:p1:80"}, - {v1.ServicePort{Protocol: v1.ProtocolUDP, Name: "", Port: 80}, "UDP::80"}, - } - - for _, u := range uu { - assert.Equal(t, u.e, portAsStr(u.port)) - } -} - -// ---------------------------------------------------------------------------- -// Helpers... - -func toQty(s string) resource.Quantity { - q, _ := resource.ParseQuantity(s) - - return q -} diff --git a/internal/sanitize/hpa_test.go b/internal/sanitize/hpa_test.go deleted file mode 100644 index 39583b23..00000000 --- a/internal/sanitize/hpa_test.go +++ /dev/null @@ -1,283 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package sanitize - -import ( - "testing" - - "github.com/derailed/popeye/internal/cache" - "github.com/derailed/popeye/internal/issues" - "github.com/stretchr/testify/assert" - autoscalingv1 "k8s.io/api/autoscaling/v1" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" -) - -func TestHPASanitizeDP(t *testing.T) { - uu := map[string]struct { - l HpaLister - issues int - hissues int - }{ - "cool": { - newDpHpa( - hpaOpts{ - name: "d1", - ccpu: "20m", - cmem: "20Mi", - max: 1, - coOpts: coOpts{ - rcpu: "1m", - rmem: "10Mi", - }, - }), - 0, - 0, - }, - "noDeployments": { - newDpHpa( - hpaOpts{ - name: "bozo", - ccpu: "20m", - cmem: "20Mi", - max: 1, - coOpts: coOpts{ - rcpu: "1m", - rmem: "10Mi", - }, - }), - 1, - 0, - }, - "overCpu": { - newDpHpa( - hpaOpts{ - name: "d1", - ccpu: "10m", - cmem: "20Mi", - max: 1, - coOpts: coOpts{ - rcpu: "10m", - rmem: "10Mi", - }, - }), - 1, - 1, - }, - "overMem": { - newDpHpa( - hpaOpts{ - name: "d1", - ccpu: "10m", - cmem: "10Mi", - max: 1, - coOpts: coOpts{ - rcpu: "1m", - rmem: "10Mi", - }, - }), - 1, - 1, - }, - } - - ctx := makeContext("autoscaling/v1/horizontalpodautoscalers", "hpa") - for k := range uu { - u := uu[k] - t.Run(k, func(t *testing.T) { - h := NewHorizontalPodAutoscaler(issues.NewCollector(loadCodes(t), makeConfig(t)), u.l) - - assert.Nil(t, h.Sanitize(ctx)) - assert.Equal(t, u.issues, len(h.Outcome()["default/h1"])) - assert.Equal(t, u.hissues, len(h.Outcome()["HPA"])) - }) - } -} - -func TestHPASanitizeSTS(t *testing.T) { - uu := map[string]struct { - l HpaLister - issues int - hissues int - }{ - "cool": { - newStsHpa( - hpaOpts{ - name: "sts1", - ccpu: "10m", - cmem: "10Mi", - max: 1, - coOpts: coOpts{ - rcpu: "1m", - rmem: "1Mi", - }, - }), - 0, - 0, - }, - "noSTS": { - newStsHpa( - hpaOpts{ - name: "bozo", - ccpu: "20m", - cmem: "20Mi", - max: 1, - coOpts: coOpts{ - rcpu: "1m", - rmem: "10Mi", - }, - }), - 1, - 0, - }, - "overCpu": { - newStsHpa( - hpaOpts{ - name: "sts1", - ccpu: "10m", - cmem: "10Mi", - max: 2, - coOpts: coOpts{ - rcpu: "10m", - rmem: "1Mi", - lcpu: "10m", - lmem: "1Mi", - }, - }), - 1, - 1, - }, - "overMem": { - newStsHpa( - hpaOpts{ - name: "sts1", - ccpu: "10m", - cmem: "10Mi", - max: 1, - coOpts: coOpts{ - rcpu: "1m", - rmem: "10Mi", - lcpu: "1m", - lmem: "10Mi", - }, - }), - 1, - 1, - }, - } - - ctx := makeContext("autoscaling/v1/horizontalpodautoscalers", "hpa") - for k := range uu { - u := uu[k] - t.Run(k, func(t *testing.T) { - h := NewHorizontalPodAutoscaler(issues.NewCollector(loadCodes(t), makeConfig(t)), u.l) - - assert.Nil(t, h.Sanitize(ctx)) - assert.Equal(t, u.issues, len(h.Outcome()["default/h1"])) - assert.Equal(t, u.hissues, len(h.Outcome()["HPA"])) - }) - } -} - -// ---------------------------------------------------------------------------- -// Helpers... - -type hpaOpts struct { - coOpts - name string - refType, ref, ccpu, cmem string - max int32 -} - -type hpa struct { - StatefulSetLister - DeploymentLister - name string - opts hpaOpts -} - -func newDpHpa(opts hpaOpts) *hpa { - h := hpa{ - DeploymentLister: makeDPLister(dpOpts{ - coOpts: opts.coOpts, - reps: 1, - availReps: 1, - }), - name: "h1", - opts: opts, - } - h.opts.refType, h.opts.ref = "Deployment", opts.name - - return &h -} - -func newStsHpa(opts hpaOpts) *hpa { - h := hpa{ - StatefulSetLister: makeSTSLister(stsOpts{ - coOpts: opts.coOpts, - replicas: 1, - currentReps: 1, - }), - name: "h1", - opts: opts, - } - h.opts.refType, h.opts.ref = "StatefulSet", opts.name - - return &h -} - -func (h *hpa) ListHorizontalPodAutoscalers() map[string]*autoscalingv1.HorizontalPodAutoscaler { - return map[string]*autoscalingv1.HorizontalPodAutoscaler{ - cache.FQN("default", h.name): makeHPA(h.name, h.opts.refType, h.opts.ref, h.opts.max), - } -} - -func (h *hpa) ListNodesMetrics() map[string]*mv1beta1.NodeMetrics { - return map[string]*mv1beta1.NodeMetrics{} -} - -func (h *hpa) ListNodes() map[string]*v1.Node { - return map[string]*v1.Node{} -} - -func (h *hpa) ListPods() map[string]*v1.Pod { - return map[string]*v1.Pod{} -} - -func (h *hpa) NodeCPULimit() float64 { return 0 } -func (h *hpa) NodeMEMLimit() float64 { return 0 } - -func (h *hpa) ListPodsMetrics() map[string]*mv1beta1.PodMetrics { - return map[string]*mv1beta1.PodMetrics{ - "default/p1": makeMxPod(h.opts.rcpu, h.opts.rmem), - } -} - -func (h *hpa) ListAvailableMetrics(map[string]*v1.Node) v1.ResourceList { - return v1.ResourceList{ - v1.ResourceCPU: toQty(h.opts.ccpu), - v1.ResourceMemory: toQty(h.opts.cmem), - } -} - -func (h *hpa) GetPod(string, map[string]string) *v1.Pod { - return &v1.Pod{} -} - -func makeHPA(n, kind, dp string, max int32) *autoscalingv1.HorizontalPodAutoscaler { - return &autoscalingv1.HorizontalPodAutoscaler{ - ObjectMeta: metav1.ObjectMeta{ - Name: n, - Namespace: "default", - }, - Spec: autoscalingv1.HorizontalPodAutoscalerSpec{ - MaxReplicas: max, - ScaleTargetRef: autoscalingv1.CrossVersionObjectReference{ - Kind: kind, - Name: dp, - }, - }, - } -} diff --git a/internal/sanitize/ing.go b/internal/sanitize/ing.go deleted file mode 100644 index 06b53645..00000000 --- a/internal/sanitize/ing.go +++ /dev/null @@ -1,68 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package sanitize - -import ( - "context" - - netv1 "k8s.io/api/networking/v1" - - "github.com/derailed/popeye/internal" - "github.com/derailed/popeye/internal/issues" -) - -type ( - // Ingress tracks Ingress sanitization. - Ingress struct { - *issues.Collector - IngressLister - } - - // IngLister list ingresses. - IngLister interface { - ListIngresses() map[string]*netv1.Ingress - } - - // IngressLister list available Ingresss on a cluster. - IngressLister interface { - IngLister - } -) - -// NewIngress returns a new sanitizer. -func NewIngress(co *issues.Collector, lister IngressLister) *Ingress { - return &Ingress{ - Collector: co, - IngressLister: lister, - } -} - -// Sanitize cleanse the resource. -func (i *Ingress) Sanitize(ctx context.Context) error { - for fqn, ing := range i.ListIngresses() { - i.InitOutcome(fqn) - ctx = internal.WithFQN(ctx, fqn) - - i.checkDeprecation(ctx, ing) - - if i.NoConcerns(fqn) && i.Config.ExcludeFQN(internal.MustExtractSectionGVR(ctx), fqn) { - i.ClearOutcome(fqn) - } - } - - return nil -} - -func (i *Ingress) checkDeprecation(ctx context.Context, ing *netv1.Ingress) { - const current = "networking.k8s.io/v1" - rev, err := resourceRev(internal.MustExtractFQN(ctx), "Ingress", ing.Annotations) - if err != nil { - if rev = revFromLink(ing.SelfLink); rev == "" { - return - } - } - if rev != current { - i.AddCode(ctx, 403, "Ingress", rev, current) - } -} diff --git a/internal/sanitize/ingress_test.go b/internal/sanitize/ingress_test.go deleted file mode 100644 index 43e73e1a..00000000 --- a/internal/sanitize/ingress_test.go +++ /dev/null @@ -1,72 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package sanitize - -import ( - "testing" - - "github.com/derailed/popeye/internal/issues" - "github.com/derailed/popeye/pkg/config" - "github.com/stretchr/testify/assert" - netv1 "k8s.io/api/networking/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -func TestIngressSanitize(t *testing.T) { - uu := map[string]struct { - rev string - e issues.Issues - }{ - "good": { - rev: "networking.k8s.io/v1", - e: issues.Issues{}, - }, - "guizard": { - rev: "extensions/v1beta1", - e: issues.Issues{ - { - GVR: "networking.k8s.io/v1", - Group: issues.Root, - Message: `[POP-403] Deprecated Ingress API group "extensions/v1beta1". Use "networking.k8s.io/v1" instead`, - Level: config.WarnLevel, - }, - }, - }, - } - - ctx := makeContext("networking.k8s.io/v1", "ing") - for k := range uu { - u := uu[k] - t.Run(k, func(t *testing.T) { - cl := NewIngress(issues.NewCollector(loadCodes(t), makeConfig(t)), newIngress(u.rev)) - - assert.Nil(t, cl.Sanitize(ctx)) - assert.Equal(t, u.e, cl.Outcome()["default/ing1"]) - }) - } -} - -// Helpers... - -type ingress struct { - rev string -} - -func newIngress(rev string) ingress { - return ingress{rev: rev} -} - -func (i ingress) ListIngresses() map[string]*netv1.Ingress { - return map[string]*netv1.Ingress{ - "default/ing1": makeIngress(i.rev), - } -} - -func makeIngress(url string) *netv1.Ingress { - return &netv1.Ingress{ - ObjectMeta: metav1.ObjectMeta{ - SelfLink: "/api/" + url, - }, - } -} diff --git a/internal/sanitize/node.go b/internal/sanitize/node.go deleted file mode 100644 index 5a5a8cb0..00000000 --- a/internal/sanitize/node.go +++ /dev/null @@ -1,222 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package sanitize - -import ( - "context" - - "github.com/derailed/popeye/internal" - "github.com/derailed/popeye/internal/client" - "github.com/derailed/popeye/internal/issues" - v1 "k8s.io/api/core/v1" - mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" -) - -const ( - // Label for master nodes since v1.6 - labelNodeRoleMaster = "node-role.kubernetes.io/master" - - // Future label for master nodes as of v1.20, - // according to https://github.com/kubernetes/kubeadm/issues/2200 - labelNodeRoleControlPlane = "node-role.kubernetes.io/control-plane" -) - -type ( - tolerations map[string]struct{} - - // NodeLimiter tracks metrics limit range. - NodeLimiter interface { - NodeCPULimit() float64 - NodeMEMLimit() float64 - } - - // NodeLister lists available nodes. - NodeLister interface { - NodeMetricsLister - PodLister - NodeLimiter - ListNodes() map[string]*v1.Node - } - - // NodeMetricsLister handle - NodeMetricsLister interface { - ListNodesMetrics() map[string]*mv1beta1.NodeMetrics - } - - // Node represents a Node sanitizer. - Node struct { - *issues.Collector - NodeLister - } -) - -// NewNode returns a new sanitizer. -func NewNode(co *issues.Collector, lister NodeLister) *Node { - return &Node{ - Collector: co, - NodeLister: lister, - } -} - -// Sanitize cleanse the resource. -func (n *Node) Sanitize(ctx context.Context) error { - nmx := client.NodesMetrics{} - nodesMetrics(n.ListNodes(), n.ListNodesMetrics(), nmx) - - var numMasters int - var masterCtx context.Context - for fqn, no := range n.ListNodes() { - n.InitOutcome(fqn) - ctx = internal.WithFQN(ctx, fqn) - - if n.checkMasterRole(no) { - masterCtx = ctx - numMasters++ - } - - ready := n.checkConditions(ctx, no) - if ready { - n.checkTaints(ctx, no.Spec.Taints) - n.checkUtilization(ctx, nmx[fqn]) - } - - if n.NoConcerns(fqn) && n.Config.ExcludeFQN(internal.MustExtractSectionGVR(ctx), fqn) { - n.ClearOutcome(fqn) - } - } - - if numMasters == 1 { - n.AddCode(masterCtx, 712) - } - - return nil -} - -func (n *Node) checkTaints(ctx context.Context, taints []v1.Taint) { - tols := n.fetchPodTolerations() - for _, ta := range taints { - if _, ok := tols[mkKey(ta.Key, ta.Value)]; !ok { - n.AddCode(ctx, 700, ta.Key) - } - } -} - -func (n *Node) fetchPodTolerations() tolerations { - tt := tolerations{} - for _, po := range n.ListPods() { - for _, t := range po.Spec.Tolerations { - tt[mkKey(t.Key, t.Value)] = struct{}{} - } - } - - return tt -} - -func mkKey(k, v string) string { - return k + ":" + v -} - -func (n *Node) checkConditions(ctx context.Context, no *v1.Node) bool { - var ready bool - if no.Spec.Unschedulable { - n.AddCode(ctx, 711) - } - for _, c := range no.Status.Conditions { - // Unknow type - if c.Status == v1.ConditionUnknown { - n.AddCode(ctx, 701) - return false - } - - // Node is not ready bail other checks - if c.Type == v1.NodeReady && c.Status == v1.ConditionFalse { - n.AddCode(ctx, 702) - return ready - } - ready = n.statusReport(ctx, c.Type, c.Status) - } - - return ready -} - -// checkMasterRole checks whether the node is a master node. -func (n *Node) checkMasterRole(no *v1.Node) bool { - if _, ok := no.Labels[labelNodeRoleMaster]; ok { - return true - } - if _, ok := no.Labels[labelNodeRoleControlPlane]; ok { - return true - } - - return false -} - -func (n *Node) statusReport(ctx context.Context, cond v1.NodeConditionType, status v1.ConditionStatus) bool { - var ready bool - - // Status is good ie no condition detected -> bail! - if status == v1.ConditionFalse { - return true - } - - switch cond { - case v1.NodeMemoryPressure: - n.AddCode(ctx, 704) - case v1.NodeDiskPressure: - n.AddCode(ctx, 705) - case v1.NodePIDPressure: - n.AddCode(ctx, 706) - case v1.NodeNetworkUnavailable: - n.AddCode(ctx, 707) - case v1.NodeReady: - ready = true - } - - return ready -} - -func (n *Node) checkUtilization(ctx context.Context, mx client.NodeMetrics) { - if mx.Empty() { - n.AddCode(ctx, 708) - return - } - - percCPU := ToPerc(toMC(mx.CurrentCPU), toMC(mx.AvailableCPU)) - cpuLimit := int64(n.NodeCPULimit()) - if percCPU > cpuLimit { - n.AddCode(ctx, 709, cpuLimit, percCPU) - } - - percMEM := ToPerc(toMB(mx.CurrentMEM), toMB(mx.AvailableMEM)) - memLimit := int64(n.NodeMEMLimit()) - if percMEM > memLimit { - n.AddCode(ctx, 710, memLimit, percMEM) - } -} - -// ---------------------------------------------------------------------------- -// Helpers... - -func nodesMetrics(nodes map[string]*v1.Node, metrics map[string]*mv1beta1.NodeMetrics, nmx client.NodesMetrics) { - if len(metrics) == 0 { - return - } - - for fqn, n := range nodes { - nmx[fqn] = client.NodeMetrics{ - AvailableCPU: *n.Status.Allocatable.Cpu(), - AvailableMEM: *n.Status.Allocatable.Memory(), - TotalCPU: *n.Status.Capacity.Cpu(), - TotalMEM: *n.Status.Capacity.Memory(), - } - } - - for fqn, c := range metrics { - if mx, ok := nmx[fqn]; ok { - mx.CurrentCPU = *c.Usage.Cpu() - mx.CurrentMEM = *c.Usage.Memory() - nmx[fqn] = mx - } - } -} diff --git a/internal/sanitize/node_test.go b/internal/sanitize/node_test.go deleted file mode 100644 index c06ffc82..00000000 --- a/internal/sanitize/node_test.go +++ /dev/null @@ -1,295 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package sanitize - -import ( - "testing" - - "github.com/derailed/popeye/internal/cache" - "github.com/derailed/popeye/internal/issues" - "github.com/stretchr/testify/assert" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" - v1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" -) - -func TestNodeSanitizer(t *testing.T) { - uu := map[string]struct { - lister NodeLister - issues int - }{ - "good": { - makeNodeLister(nodeOpts{ - nodes: map[string]*v1.Node{ - "n1": makeNode("1000m", "200Mi"), - }, - metrics: map[string]*mv1beta1.NodeMetrics{ - "n1": makeNodeMX("500m", "100Mi"), - }, - }), - 0, - }, - "noMetrics": { - makeNodeLister(nodeOpts{ - noMetrics: true, - nodes: map[string]*v1.Node{ - "n1": makeNode("", ""), - }, - }), - 1, - }, - "overCPU": { - makeNodeLister(nodeOpts{ - nodes: map[string]*v1.Node{ - "n1": makeNode("1000m", "200Mi"), - }, - metrics: map[string]*mv1beta1.NodeMetrics{ - "n1": makeNodeMX("2000m", "100Mi"), - }, - }), - 1, - }, - "overMem": { - makeNodeLister(nodeOpts{ - nodes: map[string]*v1.Node{ - "n1": makeNode("1", "100Mi"), - }, - metrics: map[string]*mv1beta1.NodeMetrics{ - "n1": makeNodeMX("500m", "250Mi"), - }, - }), - 1, - }, - "missingToleration": { - makeNodeLister(nodeOpts{ - nodes: map[string]*v1.Node{ - "n1": makeTaintedNode("fred", "blee"), - }, - pods: map[string]*v1.Pod{ - cache.FQN("default", "p1"): makePod("p1"), - cache.FQN("default", "p2"): makePodToleration("p2", "k1", "v1"), - }, - metrics: map[string]*mv1beta1.NodeMetrics{ - "n1": makeNodeMX("10m", "10Mi"), - }, - }), - 1, - }, - "notReady": { - makeNodeLister(nodeOpts{ - nodes: map[string]*v1.Node{ - "n1": makeCondNode(v1.NodeReady, v1.ConditionFalse), - }, - metrics: map[string]*mv1beta1.NodeMetrics{ - "n1": makeNodeMX("500m", "100Mi"), - }, - }), - 1, - }, - "unknownState": { - makeNodeLister(nodeOpts{ - nodes: map[string]*v1.Node{ - "n1": makeCondNode(v1.NodeReady, v1.ConditionUnknown), - }, - metrics: map[string]*mv1beta1.NodeMetrics{ - "n1": makeNodeMX("500m", "100Mi"), - }, - }), - 1, - }, - "outOfDisk": { - makeNodeLister(nodeOpts{ - nodes: map[string]*v1.Node{ - "n1": makeCondNode(v1.NodeDiskPressure, v1.ConditionTrue), - }, - metrics: map[string]*mv1beta1.NodeMetrics{ - "n1": makeNodeMX("500m", "100Mi"), - }, - }), - 1, - }, - "outOfMemory": { - makeNodeLister(nodeOpts{ - nodes: map[string]*v1.Node{ - "n1": makeCondNode(v1.NodeMemoryPressure, v1.ConditionTrue), - }, - metrics: map[string]*mv1beta1.NodeMetrics{ - "n1": makeNodeMX("500m", "100Mi"), - }, - }), - 1, - }, - "diskPressure": { - makeNodeLister(nodeOpts{ - nodes: map[string]*v1.Node{ - "n1": makeCondNode(v1.NodeDiskPressure, v1.ConditionTrue), - }, - metrics: map[string]*mv1beta1.NodeMetrics{ - "n1": makeNodeMX("500m", "100Mi"), - }, - }), - 1, - }, - "outOfPID": { - makeNodeLister(nodeOpts{ - nodes: map[string]*v1.Node{ - "n1": makeCondNode(v1.NodePIDPressure, v1.ConditionTrue), - }, - metrics: map[string]*mv1beta1.NodeMetrics{ - "n1": makeNodeMX("500m", "100Mi"), - }, - }), - 1, - }, - "noNetwork": { - makeNodeLister(nodeOpts{ - nodes: map[string]*v1.Node{ - "n1": makeCondNode(v1.NodeNetworkUnavailable, v1.ConditionTrue), - }, - metrics: map[string]*mv1beta1.NodeMetrics{ - "n1": makeNodeMX("500m", "100Mi"), - }, - }), - 1, - }, - } - - ctx := makeContext("v1/nodes", "nodes") - for k := range uu { - u := uu[k] - t.Run(k, func(t *testing.T) { - n := NewNode(issues.NewCollector(loadCodes(t), makeConfig(t)), u.lister) - - assert.Nil(t, n.Sanitize(ctx)) - assert.Equal(t, u.issues, len(n.Outcome()["n1"])) - }) - } -} - -// ---------------------------------------------------------------------------- -// Helpers... - -type ( - nodeOpts struct { - noMetrics bool - nodes map[string]*v1.Node - metrics map[string]*v1beta1.NodeMetrics - pods map[string]*v1.Pod - } - - node struct { - name string - opts nodeOpts - } -) - -func makeNodeLister(opts nodeOpts) *node { - return &node{ - name: "n1", - opts: opts, - } -} - -func (*node) RestartsLimit() int { - return 10 -} - -func (*node) PodCPULimit() float64 { - return 90 -} - -func (*node) PodMEMLimit() float64 { - return 90 -} - -func (*node) NodeCPULimit() float64 { - return 90 -} - -func (*node) NodeMEMLimit() float64 { - return 90 -} - -func (n *node) ListNodesMetrics() map[string]*v1beta1.NodeMetrics { - if n.opts.noMetrics { - return map[string]*v1beta1.NodeMetrics{} - } - - return n.opts.metrics -} - -func (n *node) ListPods() map[string]*v1.Pod { - return n.opts.pods -} - -func (n *node) GetPod(string, map[string]string) *v1.Pod { - return nil -} - -func (n *node) ListPodsMetrics() map[string]*v1beta1.PodMetrics { - return map[string]*v1beta1.PodMetrics{} -} - -func makePodToleration(n, k, v string) *v1.Pod { - p := makePod(n) - p.Spec.Tolerations = []v1.Toleration{ - {Key: k, Value: v}, - } - return p -} - -func (n *node) ListNodes() map[string]*v1.Node { - return n.opts.nodes -} - -func makeCondNode(c v1.NodeConditionType, s v1.ConditionStatus) *v1.Node { - no := makeNode("100m", "100Mi") - no.Status = v1.NodeStatus{ - Conditions: []v1.NodeCondition{ - {Type: c, Status: s}, - }, - } - return no -} - -func makeNode(cpu, mem string) *v1.Node { - no := v1.Node{ - ObjectMeta: metav1.ObjectMeta{ - Name: "n1", - }, - Spec: v1.NodeSpec{}, - Status: v1.NodeStatus{ - Conditions: []v1.NodeCondition{ - {Type: v1.NodeReady, Status: v1.ConditionTrue}, - }, - }, - } - - if cpu != "" { - no.Status.Allocatable = v1.ResourceList{ - v1.ResourceCPU: toQty(cpu), - v1.ResourceMemory: toQty(mem), - } - } - - return &no -} - -func makeTaintedNode(k, v string) *v1.Node { - no := makeNode("100m", "100Mi") - no.Spec.Taints = []v1.Taint{ - {Key: k, Value: v}, - } - return no -} - -func makeNodeMX(cpu, mem string) *v1beta1.NodeMetrics { - return &v1beta1.NodeMetrics{ - Usage: v1.ResourceList{ - v1.ResourceCPU: toQty(cpu), - v1.ResourceMemory: toQty(mem), - }, - } -} diff --git a/internal/sanitize/np.go b/internal/sanitize/np.go deleted file mode 100644 index 682e2753..00000000 --- a/internal/sanitize/np.go +++ /dev/null @@ -1,133 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package sanitize - -import ( - "context" - - "github.com/derailed/popeye/internal" - "github.com/derailed/popeye/internal/issues" - v1 "k8s.io/api/core/v1" - nv1 "k8s.io/api/networking/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -type ( - // NetworkPolicy tracks NetworkPolicy sanitization. - NetworkPolicy struct { - *issues.Collector - NetworkPolicyLister - } - - // NamespaceSelectorLister list a collection of namespaces matching a selector. - NamespaceSelectorLister interface { - ListNamespacesBySelector(sel *metav1.LabelSelector) map[string]*v1.Namespace - } - - // NetworkPolicyLister list available NetworkPolicys on a cluster. - NetworkPolicyLister interface { - PodSelectorLister - NamespaceSelectorLister - ListNetworkPolicies() map[string]*nv1.NetworkPolicy - } -) - -// NewNetworkPolicy returns a new sanitizer. -func NewNetworkPolicy(co *issues.Collector, lister NetworkPolicyLister) *NetworkPolicy { - return &NetworkPolicy{ - Collector: co, - NetworkPolicyLister: lister, - } -} - -// Sanitize cleanse the resource. -func (n *NetworkPolicy) Sanitize(ctx context.Context) error { - for fqn, np := range n.ListNetworkPolicies() { - n.InitOutcome(fqn) - ctx = internal.WithFQN(ctx, fqn) - - n.checkDeprecation(ctx, np) - n.checkRefs(ctx, np) - - if n.NoConcerns(fqn) && n.Config.ExcludeFQN(internal.MustExtractSectionGVR(ctx), fqn) { - n.ClearOutcome(fqn) - } - } - - return nil -} - -func (n *NetworkPolicy) checkPodSelector(ctx context.Context, nss map[string]*v1.Namespace, sel *metav1.LabelSelector, kind string) { - if sel == nil || sel.Size() == 0 { - return - } - - var found bool - for ns := range nss { - if pods := n.ListPodsBySelector(ns, sel); len(pods) > 0 { - found = true - } - } - if !found { - n.AddCode(ctx, 1200, kind) - } -} - -func (n *NetworkPolicy) checkNSSelector(ctx context.Context, sel *metav1.LabelSelector, kind string) map[string]*v1.Namespace { - if sel == nil || sel.Size() == 0 { - return nil - } - - nss := n.ListNamespacesBySelector(sel) - if len(nss) == 0 { - n.AddCode(ctx, 1201, kind) - } - - return nss -} - -func (n *NetworkPolicy) checkRefs(ctx context.Context, np *nv1.NetworkPolicy) { - const ( - ingress = "Ingress" - egress = "Egress" - ) - - for _, ing := range np.Spec.Ingress { - for _, from := range ing.From { - if from.NamespaceSelector != nil { - if nss := n.checkNSSelector(ctx, from.NamespaceSelector, ingress); len(nss) > 0 { - n.checkPodSelector(ctx, nss, from.PodSelector, ingress) - } - } else { - n.checkPodSelector(ctx, map[string]*v1.Namespace{np.Namespace: nil}, from.PodSelector, ingress) - } - } - } - - for _, eg := range np.Spec.Egress { - for _, to := range eg.To { - if to.NamespaceSelector != nil { - if nss := n.checkNSSelector(ctx, to.NamespaceSelector, egress); len(nss) > 0 { - n.checkPodSelector(ctx, nss, to.PodSelector, egress) - } - } else { - n.checkPodSelector(ctx, map[string]*v1.Namespace{np.Namespace: nil}, to.PodSelector, egress) - } - } - } -} - -func (n *NetworkPolicy) checkDeprecation(ctx context.Context, np *nv1.NetworkPolicy) { - const current = "networking.k8s.io/v1" - - rev, err := resourceRev(internal.MustExtractFQN(ctx), "NetworkPolicy", np.Annotations) - if err != nil { - if rev = revFromLink(np.SelfLink); rev == "" { - return - } - } - if rev != current { - n.AddCode(ctx, 403, "NetworkPolicy", rev, current) - } -} diff --git a/internal/sanitize/np_int_test.go b/internal/sanitize/np_int_test.go deleted file mode 100644 index 88dc5e7d..00000000 --- a/internal/sanitize/np_int_test.go +++ /dev/null @@ -1,56 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package sanitize - -import ( - "testing" - - "github.com/derailed/popeye/internal/issues" - "github.com/stretchr/testify/assert" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -func TestCheckPodSelector(t *testing.T) { - uu := map[string]struct { - nss map[string]*v1.Namespace - sel *metav1.LabelSelector - issues issues.Issues - }{ - "empty": { - nss: map[string]*v1.Namespace{ - "ns1": {ObjectMeta: metav1.ObjectMeta{Name: "ns1"}}, - }, - }, - "duh": { - nss: map[string]*v1.Namespace{ - "ns1": {ObjectMeta: metav1.ObjectMeta{Name: "ns1"}}, - }, - sel: &metav1.LabelSelector{MatchLabels: map[string]string{"fred": "blee"}}, - issues: issues.Issues{ - issues.Issue{ - GVR: "networking.k8s.io/v1/networkpolicies", - Group: "__root__", - Level: 2, - Message: "[POP-1200] No pods match Ingress pod selector", - }, - }, - }, - } - - l := makeNPLister(npOpts{ - rev: "networking.k8s.io/v1", - pod: true, - }) - np := NewNetworkPolicy(issues.NewCollector(loadCodes(t), makeConfig(t)), l) - ctx := makeContext("networking.k8s.io/v1/networkpolicies", "np") - for k := range uu { - u := uu[k] - t.Run(k, func(t *testing.T) { - np.checkPodSelector(ctx, u.nss, u.sel, "Ingress") - assert.Equal(t, u.issues, np.Outcome()[""]) - }) - - } -} diff --git a/internal/sanitize/np_test.go b/internal/sanitize/np_test.go deleted file mode 100644 index 911a6131..00000000 --- a/internal/sanitize/np_test.go +++ /dev/null @@ -1,197 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package sanitize - -import ( - "testing" - - "github.com/derailed/popeye/internal/cache" - "github.com/derailed/popeye/internal/issues" - "github.com/stretchr/testify/assert" - v1 "k8s.io/api/core/v1" - nv1 "k8s.io/api/networking/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -func TestNPSanitize(t *testing.T) { - uu := map[string]struct { - lister NetworkPolicyLister - issues issues.Issues - }{ - "good": { - lister: makeNPLister(npOpts{ - rev: "networking.k8s.io/v1", - }), - issues: issues.Issues{}, - }, - "deprecated": { - lister: makeNPLister(npOpts{ - rev: "policy/v1beta1", - }), - issues: issues.Issues{ - issues.Issue{ - GVR: "networking.k8s.io/v1/networkpolicies", - Group: "__root__", - Level: 2, - Message: `[POP-403] Deprecated NetworkPolicy API group "policy/v1beta1". Use "networking.k8s.io/v1" instead`}, - }, - }, - "noPodRef": { - lister: makeNPLister(npOpts{ - rev: "networking.k8s.io/v1", - pod: true, - }), - issues: issues.Issues{ - issues.Issue{ - GVR: "networking.k8s.io/v1/networkpolicies", - Group: "__root__", - Level: 2, - Message: "[POP-1200] No pods match Ingress pod selector", - }, - issues.Issue{ - GVR: "networking.k8s.io/v1/networkpolicies", - Group: "__root__", - Level: 2, - Message: "[POP-1200] No pods match Egress pod selector", - }, - }, - }, - "noNSRef": { - lister: makeNPLister(npOpts{ - rev: "networking.k8s.io/v1", - ns: true, - }), - issues: issues.Issues{ - issues.Issue{ - GVR: "networking.k8s.io/v1/networkpolicies", - Group: "__root__", - Level: 2, - Message: "[POP-1201] No namespaces match Ingress namespace selector", - }, - issues.Issue{ - GVR: "networking.k8s.io/v1/networkpolicies", - Group: "__root__", - Level: 2, - Message: "[POP-1201] No namespaces match Egress namespace selector", - }, - }, - }, - } - - ctx := makeContext("networking.k8s.io/v1/networkpolicies", "np") - for k := range uu { - u := uu[k] - t.Run(k, func(t *testing.T) { - np := NewNetworkPolicy(issues.NewCollector(loadCodes(t), makeConfig(t)), u.lister) - - assert.Nil(t, np.Sanitize(ctx)) - assert.Equal(t, u.issues, np.Outcome()["default/np"]) - }) - } -} - -// Helpers... - -type ( - npOpts struct { - rev string - pod, ns bool - } - - np struct { - name string - opts npOpts - } -) - -func makeNPLister(opts npOpts) *np { - return &np{ - name: "np", - opts: opts, - } -} - -func (n *np) ListNetworkPolicies() map[string]*nv1.NetworkPolicy { - return map[string]*nv1.NetworkPolicy{ - cache.FQN("default", n.name): makeNP(n.name, n.opts), - } -} - -func (n *np) ListNamespacesBySelector(sel *metav1.LabelSelector) map[string]*v1.Namespace { - if n.opts.ns { - return map[string]*v1.Namespace{} - } - - return map[string]*v1.Namespace{ - "ns1": makeNS("ns1", true), - } -} - -func (n *np) ListPodsBySelector(ns string, sel *metav1.LabelSelector) map[string]*v1.Pod { - if n.opts.pod { - return map[string]*v1.Pod{} - } - - return map[string]*v1.Pod{ - "default/p1": makePod("p1"), - } -} - -func (n *np) ListPods() map[string]*v1.Pod { - return map[string]*v1.Pod{ - "default/p1": makePodSa("p1", "fred"), - } -} - -func (n *np) GetPod(string, map[string]string) *v1.Pod { - return nil -} - -func makeNP(n string, o npOpts) *nv1.NetworkPolicy { - return &nv1.NetworkPolicy{ - ObjectMeta: metav1.ObjectMeta{ - Name: n, - Namespace: "default", - SelfLink: "/api/" + o.rev, - }, - Spec: nv1.NetworkPolicySpec{ - Ingress: []nv1.NetworkPolicyIngressRule{ - { - From: []nv1.NetworkPolicyPeer{ - { - PodSelector: &metav1.LabelSelector{ - MatchLabels: map[string]string{ - "po": "po1", - }, - }, - NamespaceSelector: &metav1.LabelSelector{ - MatchLabels: map[string]string{ - "ns": "ns1", - }, - }, - }, - }, - }, - }, - Egress: []nv1.NetworkPolicyEgressRule{ - { - To: []nv1.NetworkPolicyPeer{ - { - PodSelector: &metav1.LabelSelector{ - MatchLabels: map[string]string{ - "po": "po1", - }, - }, - NamespaceSelector: &metav1.LabelSelector{ - MatchLabels: map[string]string{ - "ns": "ns1", - }, - }, - }, - }, - }, - }, - }, - } -} diff --git a/internal/sanitize/ns.go b/internal/sanitize/ns.go deleted file mode 100644 index cb6303a8..00000000 --- a/internal/sanitize/ns.go +++ /dev/null @@ -1,76 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package sanitize - -import ( - "context" - - "github.com/derailed/popeye/internal" - "github.com/derailed/popeye/internal/issues" - v1 "k8s.io/api/core/v1" -) - -type ( - // NamespaceLister lists all namespaces. - NamespaceLister interface { - NamespaceRefs - ListNamespaces() map[string]*v1.Namespace - } - - // NamespaceRefs tracks namespace references in the cluster. - NamespaceRefs interface { - ReferencedNamespaces(map[string]struct{}) - } - - // Namespace represents a Namespace sanitizer. - Namespace struct { - *issues.Collector - NamespaceLister - } -) - -// NewNamespace returns a new sanitizer. -func NewNamespace(co *issues.Collector, lister NamespaceLister) *Namespace { - return &Namespace{ - Collector: co, - NamespaceLister: lister, - } -} - -// Sanitize cleanse the resource. -func (n *Namespace) Sanitize(ctx context.Context) error { - available := n.ListNamespaces() - used := make(map[string]struct{}, len(available)) - n.ReferencedNamespaces(used) - for fqn, ns := range available { - n.InitOutcome(fqn) - ctx = internal.WithFQN(ctx, fqn) - if n.checkActive(ctx, ns.Status.Phase) { - if _, ok := used[fqn]; !ok { - n.AddCode(ctx, 400) - } - } - if n.NoConcerns(fqn) && n.Config.ExcludeFQN(internal.MustExtractSectionGVR(ctx), fqn) { - n.ClearOutcome(fqn) - } - } - - return nil -} - -func (n *Namespace) checkActive(ctx context.Context, p v1.NamespacePhase) bool { - if !isNSActive(p) { - n.AddCode(ctx, 800) - return false - } - - return true -} - -// ---------------------------------------------------------------------------- -// Helpers... - -func isNSActive(phase v1.NamespacePhase) bool { - return phase == v1.NamespaceActive -} diff --git a/internal/sanitize/ns_test.go b/internal/sanitize/ns_test.go deleted file mode 100644 index 5e96a701..00000000 --- a/internal/sanitize/ns_test.go +++ /dev/null @@ -1,115 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package sanitize - -import ( - "testing" - - "github.com/derailed/popeye/internal/issues" - "github.com/stretchr/testify/assert" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -func TestNSSanitizer(t *testing.T) { - uu := map[string]struct { - l NamespaceLister - issues map[string]int - }{ - "good": { - makeNsLister(nsOpts{ - active: true, - used: []string{ - "ns1", - "ns2", - "ns3", - }, - }), - map[string]int{"ns1": 0, "ns2": 0, "ns3": 0}, - }, - "inactive": { - makeNsLister(nsOpts{ - active: false, - used: []string{ - "ns1", - "ns2", - "ns3", - }, - }), - map[string]int{"ns1": 0, "ns2": 1, "ns3": 0}, - }, - "unused": { - makeNsLister(nsOpts{ - active: true, - used: []string{ - "ns1", - "ns2", - }, - }), - map[string]int{"ns1": 0, "ns2": 0, "ns3": 1}, - }, - } - - ctx := makeContext("v1/namespaces", "ns") - for k := range uu { - u := uu[k] - t.Run(k, func(t *testing.T) { - n := NewNamespace(issues.NewCollector(loadCodes(t), makeConfig(t)), u.l) - - assert.Nil(t, n.Sanitize(ctx)) - for ns, v := range u.issues { - assert.Equal(t, v, len(n.Outcome()[ns])) - } - }) - } -} - -// ---------------------------------------------------------------------------- -// Helpers... - -type ( - nsOpts struct { - active bool - used []string - } - - ns struct { - opts nsOpts - } -) - -func makeNsLister(opts nsOpts) *ns { - return &ns{ - opts: opts, - } -} - -func (n *ns) ReferencedNamespaces(nn map[string]struct{}) { - for _, u := range n.opts.used { - nn[u] = struct{}{} - } -} - -func (n *ns) ListNamespaces() map[string]*v1.Namespace { - return map[string]*v1.Namespace{ - "ns1": makeNS("ns1", true), - "ns2": makeNS("ns2", n.opts.active), - "ns3": makeNS("ns3", true), - } -} - -func makeNS(n string, active bool) *v1.Namespace { - ns := v1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - Name: n, - }, - } - - ns.Status.Phase = v1.NamespaceTerminating - if active { - ns.Status.Phase = v1.NamespaceActive - } - - return &ns -} diff --git a/internal/sanitize/pdb.go b/internal/sanitize/pdb.go deleted file mode 100644 index f7348cfd..00000000 --- a/internal/sanitize/pdb.go +++ /dev/null @@ -1,81 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package sanitize - -import ( - "context" - - "github.com/derailed/popeye/internal" - "github.com/derailed/popeye/internal/issues" - "github.com/rs/zerolog/log" - policyv1 "k8s.io/api/policy/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -type ( - // PodDisruptionBudget tracks PodDisruptionBudget sanitization. - PodDisruptionBudget struct { - *issues.Collector - PodDisruptionBudgetLister - } - - // PodDisruptionBudgetLister list available PodDisruptionBudgets on a cluster. - PodDisruptionBudgetLister interface { - PodLister - ListPodDisruptionBudgets() map[string]*policyv1.PodDisruptionBudget - } -) - -// NewPodDisruptionBudget returns a new PodDisruptionBudget sanitizer. -func NewPodDisruptionBudget(c *issues.Collector, lister PodDisruptionBudgetLister) *PodDisruptionBudget { - return &PodDisruptionBudget{ - Collector: c, - PodDisruptionBudgetLister: lister, - } -} - -// Sanitize cleanse the resource. -func (p *PodDisruptionBudget) Sanitize(ctx context.Context) error { - for fqn, pdb := range p.ListPodDisruptionBudgets() { - p.InitOutcome(fqn) - ctx = internal.WithFQN(ctx, fqn) - - p.checkInUse(ctx, pdb) - p.checkDeprecation(ctx, pdb) - - if p.NoConcerns(fqn) && p.Config.ExcludeFQN(internal.MustExtractSectionGVR(ctx), fqn) { - p.ClearOutcome(fqn) - } - } - - return nil -} - -func (p *PodDisruptionBudget) checkDeprecation(ctx context.Context, pdb *policyv1.PodDisruptionBudget) { - const current = "policy/v1" - - fqn := internal.MustExtractFQN(ctx) - rev, err := resourceRev(fqn, "PodDisruptionBudget", pdb.Annotations) - if err != nil { - rev = revFromLink(pdb.SelfLink) - if rev == "" { - return - } - } - if rev != current { - p.AddCode(ctx, 403, "PodDisruptionBudget", rev, current) - } -} - -func (p *PodDisruptionBudget) checkInUse(ctx context.Context, pdb *policyv1.PodDisruptionBudget) { - m, err := metav1.LabelSelectorAsMap(pdb.Spec.Selector) - if err != nil { - log.Error().Err(err).Msg("No selectors found") - return - } - if p.GetPod(pdb.Namespace, m) == nil { - p.AddCode(ctx, 900) - return - } -} diff --git a/internal/sanitize/pdb_test.go b/internal/sanitize/pdb_test.go deleted file mode 100644 index 695fed66..00000000 --- a/internal/sanitize/pdb_test.go +++ /dev/null @@ -1,103 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package sanitize - -import ( - "testing" - - "github.com/derailed/popeye/internal/cache" - "github.com/derailed/popeye/internal/issues" - "github.com/stretchr/testify/assert" - v1 "k8s.io/api/core/v1" - policyv1 "k8s.io/api/policy/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/util/intstr" -) - -func TestPDBSanitize(t *testing.T) { - uu := map[string]struct { - lister PodDisruptionBudgetLister - issues issues.Issues - }{ - "good": { - lister: makePDBLister(pdbOpts{}), - issues: issues.Issues{}, - }, - "noPods": { - lister: makePDBLister(pdbOpts{pod: true}), - issues: issues.Issues{ - issues.Issue{ - GVR: "policy/v1beta1/poddisruptionbudgets", - Group: "__root__", - Level: 2, - Message: "[POP-900] Used? No pods match selector"}, - }, - }, - } - - ctx := makeContext("policy/v1beta1/poddisruptionbudgets", "pdb") - for k := range uu { - u := uu[k] - t.Run(k, func(t *testing.T) { - pdb := NewPodDisruptionBudget(issues.NewCollector(loadCodes(t), makeConfig(t)), u.lister) - - assert.Nil(t, pdb.Sanitize(ctx)) - assert.Equal(t, u.issues, pdb.Outcome()["default/pdb"]) - }) - } -} - -// Helpers... - -type ( - pdbOpts struct { - pod bool - } - - pdb struct { - name string - opts pdbOpts - } -) - -func makePDBLister(opts pdbOpts) *pdb { - return &pdb{ - name: "pdb", - opts: opts, - } -} - -func (r *pdb) ListPodDisruptionBudgets() map[string]*policyv1.PodDisruptionBudget { - return map[string]*policyv1.PodDisruptionBudget{ - cache.FQN("default", r.name): makePDB(r.name), - } -} - -func (r *pdb) ListPods() map[string]*v1.Pod { - return map[string]*v1.Pod{ - "default/p1": makePodSa("p1", "fred"), - } -} - -func (r *pdb) GetPod(ns string, sel map[string]string) *v1.Pod { - if r.opts.pod { - return nil - } - return makePod("p1") -} - -func makePDB(n string) *policyv1.PodDisruptionBudget { - min, max := intstr.FromInt(1), intstr.FromInt(1) - return &policyv1.PodDisruptionBudget{ - ObjectMeta: metav1.ObjectMeta{ - Name: n, - Namespace: "default", - }, - Spec: policyv1.PodDisruptionBudgetSpec{ - Selector: &metav1.LabelSelector{}, - MinAvailable: &min, - MaxUnavailable: &max, - }, - } -} diff --git a/internal/sanitize/pod.go b/internal/sanitize/pod.go deleted file mode 100644 index b4ddc747..00000000 --- a/internal/sanitize/pod.go +++ /dev/null @@ -1,319 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package sanitize - -import ( - "context" - "sort" - "strings" - - "github.com/rs/zerolog/log" - "k8s.io/apimachinery/pkg/labels" - - "github.com/derailed/popeye/internal" - "github.com/derailed/popeye/internal/cache" - "github.com/derailed/popeye/internal/client" - "github.com/derailed/popeye/internal/issues" - v1 "k8s.io/api/core/v1" - policyv1 "k8s.io/api/policy/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" -) - -const ( - // SecNonRootUndefined denotes no root user set - SecNonRootUndefined NonRootUser = iota - 1 - // SecNonRootUnset denotes root user - SecNonRootUnset = 0 - // SecNonRootSet denotes non root user - SecNonRootSet = 1 -) - -// NonRootUser identifies if a security context for nonRootUser is set/unset or undefined. -type NonRootUser int - -type ( - // Pod represents a Pod linter. - Pod struct { - *issues.Collector - PodMXLister - } - - // PdbLister list pdb matching a given selector - PdbLister interface { - ListPodDisruptionBudgets() map[string]*policyv1.PodDisruptionBudget - ForLabels(labels map[string]string) *policyv1.PodDisruptionBudget - } - - // PodLister lists available pods. - PodLister interface { - ListPods() map[string]*v1.Pod - GetPod(ns string, sel map[string]string) *v1.Pod - } - - // PodMXLister list available pods. - PodMXLister interface { - PodLimiter - PodMetricsLister - PodLister - PdbLister - ConfigLister - ListServiceAccounts() map[string]*v1.ServiceAccount - } - - // PodMetric tracks node metrics available and current range. - PodMetric interface { - CurrentCPU() int64 - CurrentMEM() int64 - Empty() bool - } -) - -// NewPod returns a new sanitizer. -func NewPod(co *issues.Collector, lister PodMXLister) *Pod { - return &Pod{ - Collector: co, - PodMXLister: lister, - } -} - -// Sanitize cleanse the resource.. -func (p *Pod) Sanitize(ctx context.Context) error { - mx := p.ListPodsMetrics() - pdbs := p.ListPodDisruptionBudgets() - for fqn, po := range p.ListPods() { - p.InitOutcome(fqn) - ctx = internal.WithFQN(ctx, fqn) - - p.checkStatus(ctx, po) - p.checkContainerStatus(ctx, po) - p.checkContainers(ctx, fqn, po) - - p.checkOwnedByAnything(ctx, po.OwnerReferences) - - if !ownedByDaemonSet(po) { - p.checkPdb(ctx, po.ObjectMeta.Labels) - } - p.checkForMultiplePdbMatches(ctx, po.Namespace, po.ObjectMeta.Labels, pdbs) - p.checkSecure(ctx, fqn, po.Spec) - pmx, cmx := mx[fqn], client.ContainerMetrics{} - containerMetrics(pmx, cmx) - p.checkUtilization(ctx, po, cmx) - - if p.NoConcerns(fqn) && p.Config.ExcludeFQN(internal.MustExtractSectionGVR(ctx), fqn) { - p.ClearOutcome(fqn) - } - } - return nil -} - -func ownedByDaemonSet(po *v1.Pod) bool { - for _, o := range po.OwnerReferences { - if o.Kind == "DaemonSet" { - return true - } - } - return false -} - -func (p *Pod) checkOwnedByAnything(ctx context.Context, ownerRefs []metav1.OwnerReference) { - if len(ownerRefs) == 0 { - p.AddCode(ctx, 208) - return - } - - controlled := false - for _, or := range ownerRefs { - if or.Controller != nil && *or.Controller { - controlled = true - break - } - } - - if !controlled { - p.AddCode(ctx, 208) - } -} - -func (p *Pod) checkPdb(ctx context.Context, labels map[string]string) { - if p.ForLabels(labels) == nil { - p.AddCode(ctx, 206) - } -} - -func (p *Pod) checkUtilization(ctx context.Context, po *v1.Pod, cmx client.ContainerMetrics) { - if len(cmx) == 0 { - return - } - - for _, co := range po.Spec.Containers { - cmx, ok := cmx[co.Name] - if !ok { - continue - } - NewContainer(internal.MustExtractFQN(ctx), p).checkUtilization(ctx, co, cmx) - } -} - -func (p *Pod) checkSecure(ctx context.Context, fqn string, spec v1.PodSpec) { - ns, _ := namespaced(fqn) - if spec.ServiceAccountName == "default" { - p.AddCode(ctx, 300) - } - - if p.PodMXLister != nil { - if sa, ok := p.ListServiceAccounts()[cache.FQN(ns, spec.ServiceAccountName)]; ok { - if spec.AutomountServiceAccountToken == nil { - if sa.AutomountServiceAccountToken == nil || *sa.AutomountServiceAccountToken { - p.AddCode(ctx, 301) - } - } else if *spec.AutomountServiceAccountToken { - p.AddCode(ctx, 301) - } - } else if spec.AutomountServiceAccountToken == nil || *spec.AutomountServiceAccountToken { - p.AddCode(ctx, 301) - } - } - - if spec.SecurityContext == nil { - return - } - - // If pod security ctx is present and we have - podSec := hasPodNonRootUser(spec.SecurityContext) - gvr := internal.MustExtractSectionGVR(ctx) - var victims int - for _, co := range spec.InitContainers { - if !p.Config.ExcludeContainer(gvr, fqn, co.Name) && !checkCOSecurityContext(co) && !podSec { - victims++ - p.AddSubCode(internal.WithGroup(ctx, client.NewGVR("containers"), co.Name), 306) - } - } - for _, co := range spec.Containers { - if !p.Config.ExcludeContainer(gvr, fqn, co.Name) && !checkCOSecurityContext(co) && !podSec { - victims++ - p.AddSubCode(internal.WithGroup(ctx, client.NewGVR("containers"), co.Name), 306) - } - } - if victims > 0 && !podSec { - p.AddCode(ctx, 302) - } -} - -func checkCOSecurityContext(co v1.Container) bool { - return hasCoNonRootUser(co.SecurityContext) -} - -func hasPodNonRootUser(sec *v1.PodSecurityContext) bool { - if sec == nil { - return false - } - if sec.RunAsNonRoot != nil { - return *sec.RunAsNonRoot - } - if sec.RunAsUser != nil { - return *sec.RunAsUser != 0 - } - return false -} - -func hasCoNonRootUser(sec *v1.SecurityContext) bool { - if sec == nil { - return false - } - if sec.RunAsNonRoot != nil { - return *sec.RunAsNonRoot - } - if sec.RunAsUser != nil { - return *sec.RunAsUser != 0 - } - return false -} - -func (p *Pod) checkContainers(ctx context.Context, fqn string, po *v1.Pod) { - co := NewContainer(internal.MustExtractFQN(ctx), p) - gvr := internal.MustExtractSectionGVR(ctx) - for _, c := range po.Spec.InitContainers { - if !p.Config.ExcludeContainer(gvr, fqn, c.Name) { - co.sanitize(ctx, c, false) - } - } - for _, c := range po.Spec.Containers { - if !p.Config.ExcludeContainer(gvr, fqn, c.Name) { - co.sanitize(ctx, c, !isPartOfJob(po)) - } - } -} - -func (p *Pod) checkContainerStatus(ctx context.Context, po *v1.Pod) { - limit := p.RestartsLimit() - for _, s := range po.Status.InitContainerStatuses { - cs := newContainerStatus(p, internal.MustExtractFQN(ctx), len(po.Status.InitContainerStatuses), true, limit) - cs.sanitize(ctx, s) - } - - for _, s := range po.Status.ContainerStatuses { - cs := newContainerStatus(p, internal.MustExtractFQN(ctx), len(po.Status.ContainerStatuses), false, limit) - cs.sanitize(ctx, s) - } -} - -func (p *Pod) checkStatus(ctx context.Context, po *v1.Pod) { - // nolint:exhaustive - switch po.Status.Phase { - case v1.PodRunning: - case v1.PodSucceeded: - default: - p.AddCode(ctx, 207, po.Status.Phase) - } -} - -// ---------------------------------------------------------------------------- -// Helpers... - -func containerMetrics(pmx *mv1beta1.PodMetrics, mx client.ContainerMetrics) { - // No metrics -> Bail! - if pmx == nil { - return - } - - for _, co := range pmx.Containers { - mx[co.Name] = client.Metrics{ - CurrentCPU: *co.Usage.Cpu(), - CurrentMEM: *co.Usage.Memory(), - } - } -} - -func isPartOfJob(po *v1.Pod) bool { - for _, o := range po.OwnerReferences { - if o.Kind == "Job" { - return true - } - } - - return false -} - -func (p *Pod) checkForMultiplePdbMatches(ctx context.Context, podNamespace string, podLabels map[string]string, pdbs map[string]*policyv1.PodDisruptionBudget) { - matchedPdbs := make([]string, 0, len(pdbs)) - for _, pdb := range pdbs { - if podNamespace != pdb.Namespace { - continue - } - selector, err := metav1.LabelSelectorAsSelector(pdb.Spec.Selector) - if err != nil { - log.Error().Err(err).Msg("No selectors found") - return - } - if selector.Empty() || !selector.Matches(labels.Set(podLabels)) { - continue - } - matchedPdbs = append(matchedPdbs, pdb.Name) - } - if len(matchedPdbs) > 1 { - sort.Strings(matchedPdbs) - p.AddCode(ctx, 209, strings.Join(matchedPdbs, ", ")) - } -} diff --git a/internal/sanitize/pod_test.go b/internal/sanitize/pod_test.go deleted file mode 100644 index a1c83556..00000000 --- a/internal/sanitize/pod_test.go +++ /dev/null @@ -1,574 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package sanitize - -import ( - "testing" - - "github.com/derailed/popeye/internal" - "github.com/derailed/popeye/internal/issues" - "github.com/derailed/popeye/pkg/config" - "github.com/stretchr/testify/assert" - v1 "k8s.io/api/core/v1" - policyv1 "k8s.io/api/policy/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - v1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" -) - -func TestPodCheckSecure(t *testing.T) { - uu := map[string]struct { - pod v1.Pod - issues int - }{ - "cool_1": { - pod: makeSecPod(SecNonRootSet, SecNonRootSet, SecNonRootSet, SecNonRootSet), - issues: 0, - }, - "cool_2": { - pod: makeSecPod(SecNonRootSet, SecNonRootUnset, SecNonRootUnset, SecNonRootUnset), - issues: 0, - }, - "cool_3": { - pod: makeSecPod(SecNonRootUnset, SecNonRootSet, SecNonRootSet, SecNonRootSet), - issues: 0, - }, - "cool_4": { - pod: makeSecPod(SecNonRootUndefined, SecNonRootSet, SecNonRootSet, SecNonRootSet), - issues: 0, - }, - "cool_5": { - pod: makeSecPod(SecNonRootSet, SecNonRootUndefined, SecNonRootUndefined, SecNonRootUndefined), - issues: 0, - }, - "hacked_1": { - pod: makeSecPod(SecNonRootUndefined, SecNonRootUndefined, SecNonRootUndefined, SecNonRootUndefined), - issues: 4, - }, - "hacked_2": { - pod: makeSecPod(SecNonRootUndefined, SecNonRootUnset, SecNonRootUndefined, SecNonRootUndefined), - issues: 4, - }, - "hacked_3": { - pod: makeSecPod(SecNonRootUndefined, SecNonRootSet, SecNonRootUndefined, SecNonRootUndefined), - issues: 3, - }, - "hacked_4": { - pod: makeSecPod(SecNonRootUndefined, SecNonRootUnset, SecNonRootSet, SecNonRootUndefined), - issues: 3, - }, - "toast": { - pod: makeSecPod(SecNonRootUndefined, SecNonRootUndefined, SecNonRootUndefined, SecNonRootUndefined), - issues: 4, - }, - } - - ctx := makeContext("v1/pods", "po") - ctx = internal.WithFQN(ctx, "default/p1") - for k := range uu { - u := uu[k] - t.Run(k, func(t *testing.T) { - p := NewPod(issues.NewCollector(loadCodes(t), makeConfig(t)), nil) - - p.checkSecure(ctx, "default/p1", u.pod.Spec) - assert.Equal(t, u.issues, len(p.Outcome()["default/p1"])) - }) - } -} - -func TestPodSanitize(t *testing.T) { - uu := map[string]struct { - lister PodMXLister - issues int - }{ - "cool": { - makePodLister(podOpts{ - pods: map[string]*v1.Pod{ - "default/p1": makeFullPod(podOpts{ - serviceAcct: "fred", - certs: false, - coOpts: coOpts{ - rcpu: "100m", - rmem: "20Mi", - lcpu: "100m", - lmem: "200Mi", - }, - csOpts: csOpts{ - ready: true, - restarts: 0, - state: running, - }, - phase: v1.PodRunning, - controlled: true, - }), - }, - }), - 0, - }, - "unhappy": { - makePodLister(podOpts{ - pods: map[string]*v1.Pod{ - "default/p1": makeFullPod(podOpts{ - coOpts: coOpts{ - rcpu: "100m", - rmem: "20Mi", - lcpu: "100m", - lmem: "200Mi", - }, - csOpts: csOpts{ - ready: true, - restarts: 0, - state: running, - }, - serviceAcct: "fred", - phase: v1.PodPending, - }), - }, - }), - 2, - }, - "defaultSA": { - makePodLister(podOpts{ - pods: map[string]*v1.Pod{ - "default/p1": makeFullPod(podOpts{ - coOpts: coOpts{ - rcpu: "100m", - rmem: "20Mi", - lcpu: "100m", - lmem: "200Mi", - }, - serviceAcct: "default", - certs: true, - phase: v1.PodRunning, - csOpts: csOpts{ - ready: true, - restarts: 0, - state: running, - }, - controlled: true, - }), - }, - }), - 2, - }, - } - - ctx := makeContext("v1/pods", "po") - for k := range uu { - u := uu[k] - t.Run(k, func(t *testing.T) { - p := NewPod(issues.NewCollector(loadCodes(t), makeConfig(t)), u.lister) - - assert.Nil(t, p.Sanitize(ctx)) - assert.Equal(t, u.issues, len(p.Outcome()["default/p1"])) - }) - } -} - -// ---------------------------------------------------------------------------- -// Helpers... - -type ( - podOpts struct { - coOpts - csOpts - phase v1.PodPhase - pods map[string]*v1.Pod - serviceAcct string - certs bool - controlled bool - } - - pod struct { - opts podOpts - } -) - -func makePodLister(opts podOpts) *pod { - return &pod{ - opts: opts, - } -} - -func (p *pod) ListPods() map[string]*v1.Pod { - return p.opts.pods -} - -func (p *pod) ListServiceAccounts() map[string]*v1.ServiceAccount { - return make(map[string]*v1.ServiceAccount) -} - -func (p *pod) GetPod(string, map[string]string) *v1.Pod { - return nil -} - -func (*pod) RestartsLimit() int { - return 10 -} - -func (*pod) PodCPULimit() float64 { - return 90 -} - -func (*pod) PodMEMLimit() float64 { - return 90 -} - -func (*pod) CPUResourceLimits() config.Allocations { - return config.Allocations{ - UnderPerc: 100, - OverPerc: 50, - } -} - -func (*pod) MEMResourceLimits() config.Allocations { - return config.Allocations{ - UnderPerc: 100, - OverPerc: 50, - } -} - -func (p *pod) ListPodsMetrics() map[string]*v1beta1.PodMetrics { - return map[string]*v1beta1.PodMetrics{ - "default/p1": makeMxPod("10m", "10Mi"), - } -} - -func (p *pod) ForLabels(l map[string]string) *policyv1.PodDisruptionBudget { - return &policyv1.PodDisruptionBudget{} -} - -func (p *pod) ListPodDisruptionBudgets() map[string]*policyv1.PodDisruptionBudget { - return nil -} - -func makePod(n string) *v1.Pod { - po := &v1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Name: n, - Namespace: "default", - }, - } - - po.Status.Phase = v1.PodRunning - - return po -} - -func makeMxPod(cpu, mem string) *v1beta1.PodMetrics { - return &v1beta1.PodMetrics{ - ObjectMeta: metav1.ObjectMeta{ - Name: "p1", - Namespace: "default", - }, - Containers: []v1beta1.ContainerMetrics{ - {Name: "i1", Usage: makeRes(cpu, mem)}, - {Name: "c1", Usage: makeRes(cpu, mem)}, - }, - } -} - -func makeFullPod(opts podOpts) *v1.Pod { - po := makePod("p1") - po.Spec = v1.PodSpec{ - InitContainers: []v1.Container{ - makeContainer("i1", coOpts{ - image: "fred:0.0.1", - rcpu: opts.rcpu, - rmem: opts.rmem, - lcpu: opts.lcpu, - lmem: opts.lmem, - }), - }, - Containers: []v1.Container{ - makeContainer("c1", coOpts{ - image: "fred:0.0.1", - rcpu: opts.rcpu, - rmem: opts.rmem, - lcpu: opts.lcpu, - lmem: opts.lmem, - lprob: true, - rprob: true, - }), - }, - } - if opts.serviceAcct != "" { - po.Spec.ServiceAccountName = opts.serviceAcct - } - po.Spec.AutomountServiceAccountToken = &opts.certs - - if opts.controlled { - truthful := true - po.OwnerReferences = append(po.OwnerReferences, metav1.OwnerReference{ - Kind: "ReplicaSet", - Name: "mock-replica-set", - Controller: &truthful, - }) - } - - po.Status = v1.PodStatus{ - Phase: opts.phase, - InitContainerStatuses: []v1.ContainerStatus{ - makeCS("i1", opts.csOpts), - }, - ContainerStatuses: []v1.ContainerStatus{ - makeCS("c1", opts.csOpts), - }, - } - - return po -} - -const ( - running int = iota - waiting - terminated -) - -type csOpts struct { - ready bool - restarts int32 - state int -} - -func makeCS(n string, opts csOpts) v1.ContainerStatus { - cs := v1.ContainerStatus{ - Name: n, - Ready: opts.ready, - RestartCount: opts.restarts, - } - - switch opts.state { - case waiting: - cs.State = v1.ContainerState{ - Waiting: &v1.ContainerStateWaiting{}, - } - case terminated: - cs.State = v1.ContainerState{ - Terminated: &v1.ContainerStateTerminated{}, - } - default: - cs.State = v1.ContainerState{ - Running: &v1.ContainerStateRunning{}, - } - } - - return cs -} - -func makeSecCO(name string, level NonRootUser) v1.Container { - t, f := true, false - secCtx := v1.SecurityContext{} - // nolint:exhaustive - switch level { - case SecNonRootUnset: - secCtx.RunAsNonRoot = &f - case SecNonRootSet: - secCtx.RunAsNonRoot = &t - } - - return v1.Container{Name: name, SecurityContext: &secCtx} -} - -func makeSecPod(pod, init, co1, co2 NonRootUser) v1.Pod { - t, f := true, false - - secCtx := v1.PodSecurityContext{} - // nolint:exhaustive - switch pod { - case SecNonRootUnset: - secCtx.RunAsNonRoot = &f - case SecNonRootSet: - secCtx.RunAsNonRoot = &t - } - - var auto bool - return v1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Name: "p1", - }, - Spec: v1.PodSpec{ - AutomountServiceAccountToken: &auto, - InitContainers: []v1.Container{makeSecCO("i1", init)}, - Containers: []v1.Container{ - makeSecCO("co1", co1), - makeSecCO("co2", co2), - }, - SecurityContext: &secCtx, - }, - } -} - -func TestPodCheckForMultiplePdbMatches(t *testing.T) { - type fields struct { - Collector *issues.Collector - PodMXLister PodMXLister - } - type args struct { - podLabels map[string]string - podNamespace string - pdbs map[string]*policyv1.PodDisruptionBudget - } - tests := []struct { - name string - fields fields - args args - want issues.Issues - }{ - { - name: "pod with one label - two pdb matches", - args: args{ - podNamespace: "namespace-1", - podLabels: map[string]string{"app": "test"}, - pdbs: map[string]*policyv1.PodDisruptionBudget{ - "pdb": { - Spec: policyv1.PodDisruptionBudgetSpec{ - Selector: &metav1.LabelSelector{ - MatchLabels: map[string]string{"app": "test"}, - }, - }, - ObjectMeta: metav1.ObjectMeta{ - Name: "pdb-1", - Namespace: "namespace-1", - }, - }, - "pdb2": { - Spec: policyv1.PodDisruptionBudgetSpec{ - Selector: &metav1.LabelSelector{ - MatchLabels: map[string]string{"app": "test"}, - }, - }, - ObjectMeta: metav1.ObjectMeta{ - Name: "pdb-2", - Namespace: "namespace-1", - }, - }, - }, - }, - want: issues.Issues{ - issues.Issue{ - GVR: "v1/pods", - Group: "__root__", - Level: 2, - Message: "[POP-209] Pod is managed by multiple PodDisruptionBudgets (pdb-1, pdb-2)"}, - }, - }, - { - name: "pod with one label - three pdbs - only two in pod namespace - expecting two matches", - args: args{ - podNamespace: "namespace-1", - podLabels: map[string]string{"app": "test"}, - pdbs: map[string]*policyv1.PodDisruptionBudget{ - "pdb": { - Spec: policyv1.PodDisruptionBudgetSpec{ - Selector: &metav1.LabelSelector{ - MatchLabels: map[string]string{"app": "test"}, - }, - }, - ObjectMeta: metav1.ObjectMeta{ - Name: "pdb-1", - Namespace: "namespace-1", - }, - }, - "pdb2": { - Spec: policyv1.PodDisruptionBudgetSpec{ - Selector: &metav1.LabelSelector{ - MatchLabels: map[string]string{"app": "test"}, - }, - }, - ObjectMeta: metav1.ObjectMeta{ - Name: "pdb-2", - Namespace: "namespace-1", - }, - }, - "pdb3": { - Spec: policyv1.PodDisruptionBudgetSpec{ - Selector: &metav1.LabelSelector{ - MatchLabels: map[string]string{"app": "test"}, - }, - }, - ObjectMeta: metav1.ObjectMeta{ - Name: "pdb-3", - Namespace: "namespace-2", - }, - }, - }, - }, - want: issues.Issues{ - issues.Issue{ - GVR: "v1/pods", - Group: "__root__", - Level: 2, - Message: "[POP-209] Pod is managed by multiple PodDisruptionBudgets (pdb-1, pdb-2)"}, - }, - }, - { - name: "one pdb match, no issue expected", - args: args{ - podNamespace: "namespace-1", - podLabels: map[string]string{"app": "test", "app2": "test2"}, - pdbs: map[string]*policyv1.PodDisruptionBudget{ - "pdb": { - Spec: policyv1.PodDisruptionBudgetSpec{ - Selector: &metav1.LabelSelector{ - MatchLabels: map[string]string{"app": "test", "app2": "test2"}, - }, - }, - ObjectMeta: metav1.ObjectMeta{ - Name: "pdb-1", - Namespace: "namespace-1", - }, - }, - "pdb2": { - Spec: policyv1.PodDisruptionBudgetSpec{ - Selector: &metav1.LabelSelector{ - MatchLabels: map[string]string{"app3": "test3"}, - }, - }, - ObjectMeta: metav1.ObjectMeta{ - Name: "pdb-2", - Namespace: "namespace-1", - }, - }, - }, - }, - want: issues.Issues(nil), - }, - { - name: "pod with no label - no issue expected", - args: args{ - podLabels: map[string]string{}, - pdbs: map[string]*policyv1.PodDisruptionBudget{ - "pdb": { - Spec: policyv1.PodDisruptionBudgetSpec{ - Selector: &metav1.LabelSelector{ - MatchLabels: map[string]string{"app": "test"}, - }, - }, - ObjectMeta: metav1.ObjectMeta{ - Name: "pdb-1"}, - }, - "pdb2": { - Spec: policyv1.PodDisruptionBudgetSpec{ - Selector: &metav1.LabelSelector{ - MatchLabels: map[string]string{"app": "test"}, - }, - }, - ObjectMeta: metav1.ObjectMeta{ - Name: "pdb-2"}, - }, - }, - }, - want: issues.Issues(nil), - }, - } - ctx := makeContext("v1/pods", "po") - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - p := NewPod(issues.NewCollector(loadCodes(t), makeConfig(t)), tt.fields.PodMXLister) - - p.checkForMultiplePdbMatches(ctx, tt.args.podNamespace, tt.args.podLabels, tt.args.pdbs) - assert.Equal(t, tt.want, p.Outcome()[""]) - }) - } -} diff --git a/internal/sanitize/pv.go b/internal/sanitize/pv.go deleted file mode 100644 index 382cb9be..00000000 --- a/internal/sanitize/pv.go +++ /dev/null @@ -1,61 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package sanitize - -import ( - "context" - - "github.com/derailed/popeye/internal" - "github.com/derailed/popeye/internal/issues" - v1 "k8s.io/api/core/v1" -) - -type ( - // PersistentVolumeLister list available PersistentVolume on a cluster. - PersistentVolumeLister interface { - ListPersistentVolumes() map[string]*v1.PersistentVolume - } - - // PersistentVolume represents a PersistentVolume sanitizer. - PersistentVolume struct { - *issues.Collector - PersistentVolumeLister - } -) - -// NewPersistentVolume returns a new sanitizer. -func NewPersistentVolume(co *issues.Collector, lister PersistentVolumeLister) *PersistentVolume { - return &PersistentVolume{ - Collector: co, - PersistentVolumeLister: lister, - } -} - -// Sanitize cleanse the resource. -func (p *PersistentVolume) Sanitize(ctx context.Context) error { - for fqn, pv := range p.ListPersistentVolumes() { - p.InitOutcome(fqn) - ctx = internal.WithFQN(ctx, fqn) - - p.checkBound(ctx, pv.Status.Phase) - - if p.NoConcerns(fqn) && p.Config.ExcludeFQN(internal.MustExtractSectionGVR(ctx), fqn) { - p.ClearOutcome(fqn) - } - } - - return nil -} - -func (p *PersistentVolume) checkBound(ctx context.Context, phase v1.PersistentVolumePhase) { - // nolint:exhaustive - switch phase { - case v1.VolumeAvailable: - p.AddCode(ctx, 1000) - case v1.VolumePending: - p.AddCode(ctx, 1001) - case v1.VolumeFailed: - p.AddCode(ctx, 1002) - } -} diff --git a/internal/sanitize/pv_test.go b/internal/sanitize/pv_test.go deleted file mode 100644 index e053cc24..00000000 --- a/internal/sanitize/pv_test.go +++ /dev/null @@ -1,69 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package sanitize - -import ( - "testing" - - "github.com/derailed/popeye/internal/issues" - "github.com/stretchr/testify/assert" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -func TestPVSanitize(t *testing.T) { - uu := map[string]struct { - lister PersistentVolumeLister - issues int - }{ - "bound": {makePVLister(pvOpts{phase: v1.VolumeBound}), 0}, - "available": {makePVLister(pvOpts{phase: v1.VolumeAvailable}), 1}, - "pending": {makePVLister(pvOpts{phase: v1.VolumePending}), 1}, - "failed": {makePVLister(pvOpts{phase: v1.VolumeFailed}), 1}, - } - - ctx := makeContext("v1/persistentvolumes", "pv") - for k := range uu { - u := uu[k] - t.Run(k, func(t *testing.T) { - p := NewPersistentVolume(issues.NewCollector(loadCodes(t), makeConfig(t)), u.lister) - - assert.Nil(t, p.Sanitize(ctx)) - assert.Equal(t, u.issues, len(p.Outcome()["default/pv1"])) - }) - } -} - -// ---------------------------------------------------------------------------- -// Helpers... - -type pvOpts struct { - phase v1.PersistentVolumePhase -} - -type pv struct { - name string - opts pvOpts -} - -func makePVLister(opts pvOpts) pv { - return pv{name: "pv1", opts: opts} -} - -func (p pv) ListPersistentVolumes() map[string]*v1.PersistentVolume { - return map[string]*v1.PersistentVolume{ - "default/pv1": makePV(p.name, p.opts.phase), - } -} - -func makePV(n string, p v1.PersistentVolumePhase) *v1.PersistentVolume { - return &v1.PersistentVolume{ - ObjectMeta: metav1.ObjectMeta{ - Name: n, - }, - Status: v1.PersistentVolumeStatus{ - Phase: p, - }, - } -} diff --git a/internal/sanitize/pvc.go b/internal/sanitize/pvc.go deleted file mode 100644 index f32b5782..00000000 --- a/internal/sanitize/pvc.go +++ /dev/null @@ -1,81 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package sanitize - -import ( - "context" - - "github.com/derailed/popeye/internal" - "github.com/derailed/popeye/internal/cache" - "github.com/derailed/popeye/internal/issues" - v1 "k8s.io/api/core/v1" -) - -type ( - // PersistentVolumeClaimLister list available PersistentVolumeClaim on a cluster. - PersistentVolumeClaimLister interface { - ListPersistentVolumeClaims() map[string]*v1.PersistentVolumeClaim - PodLister - } - - // PersistentVolumeClaim represents a PersistentVolumeClaim sanitizer. - PersistentVolumeClaim struct { - *issues.Collector - PersistentVolumeClaimLister - } -) - -// NewPersistentVolumeClaim returns a new sanitizer. -func NewPersistentVolumeClaim(co *issues.Collector, lister PersistentVolumeClaimLister) *PersistentVolumeClaim { - return &PersistentVolumeClaim{ - Collector: co, - PersistentVolumeClaimLister: lister, - } -} - -// Sanitize cleanse the resource. -func (p *PersistentVolumeClaim) Sanitize(ctx context.Context) error { - refs := map[string]struct{}{} - for fqn, pod := range p.ListPods() { - ns, _ := namespaced(fqn) - for _, v := range pod.Spec.Volumes { - if v.VolumeSource.PersistentVolumeClaim == nil { - continue - } - refs[cache.FQN(ns, v.VolumeSource.PersistentVolumeClaim.ClaimName)] = struct{}{} - } - } - - for fqn, pvc := range p.ListPersistentVolumeClaims() { - p.InitOutcome(fqn) - ctx = internal.WithFQN(ctx, fqn) - defer func(fqn string, ctx context.Context) { - if p.NoConcerns(fqn) && p.Config.ExcludeFQN(internal.MustExtractSectionGVR(ctx), fqn) { - p.ClearOutcome(fqn) - } - }(fqn, ctx) - - if !p.checkBound(ctx, pvc.Status.Phase) { - continue - } - if _, ok := refs[fqn]; !ok { - p.AddCode(ctx, 400) - } - } - - return nil -} - -func (p *PersistentVolumeClaim) checkBound(ctx context.Context, phase v1.PersistentVolumeClaimPhase) bool { - switch phase { - case v1.ClaimPending: - p.AddCode(ctx, 1003) - case v1.ClaimLost: - p.AddCode(ctx, 1004) - case v1.ClaimBound: - return true - } - - return false -} diff --git a/internal/sanitize/pvc_test.go b/internal/sanitize/pvc_test.go deleted file mode 100644 index 0cb9d4f8..00000000 --- a/internal/sanitize/pvc_test.go +++ /dev/null @@ -1,96 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package sanitize - -import ( - "testing" - - "github.com/derailed/popeye/internal/issues" - "github.com/stretchr/testify/assert" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -func TestPVCSanitize(t *testing.T) { - uu := map[string]struct { - lister PersistentVolumeClaimLister - issues int - }{ - "bound": {makePVCLister(pvcOpts{used: "pvc1", phase: v1.ClaimBound}), 0}, - "lost": {makePVCLister(pvcOpts{used: "pvc1", phase: v1.ClaimLost}), 1}, - "pending": {makePVCLister(pvcOpts{used: "pvc1", phase: v1.ClaimPending}), 1}, - "used": {makePVCLister(pvcOpts{used: "pvc2", phase: v1.ClaimBound}), 1}, - } - - ctx := makeContext("v1/persistentvolumeclaims", "pvc") - for k := range uu { - u := uu[k] - t.Run(k, func(t *testing.T) { - p := NewPersistentVolumeClaim(issues.NewCollector(loadCodes(t), makeConfig(t)), u.lister) - - assert.Nil(t, p.Sanitize(ctx)) - assert.Equal(t, u.issues, len(p.Outcome()["default/pvc1"])) - }) - } -} - -// ---------------------------------------------------------------------------- -// Helpers... - -type pvcOpts struct { - phase v1.PersistentVolumeClaimPhase - used string -} - -type pvc struct { - name string - opts pvcOpts -} - -func makePVCLister(opts pvcOpts) pvc { - return pvc{name: "pvc1", opts: opts} -} - -func (p pvc) ListPersistentVolumeClaims() map[string]*v1.PersistentVolumeClaim { - return map[string]*v1.PersistentVolumeClaim{ - "default/pvc1": makePVC(p.opts.used, p.opts.phase), - } -} - -func (p pvc) ListPods() map[string]*v1.Pod { - return map[string]*v1.Pod{ - "default/p1": makePodPVC("p1", p.opts.used), - } -} - -func (p pvc) GetPod(string, map[string]string) *v1.Pod { - return nil -} - -func makePVC(n string, p v1.PersistentVolumeClaimPhase) *v1.PersistentVolumeClaim { - return &v1.PersistentVolumeClaim{ - ObjectMeta: metav1.ObjectMeta{ - Name: n, - Namespace: "default", - }, - Status: v1.PersistentVolumeClaimStatus{ - Phase: p, - }, - } -} - -func makePodPVC(n, pvc string) *v1.Pod { - po := makePod(n) - po.Spec.Volumes = []v1.Volume{ - { - VolumeSource: v1.VolumeSource{ - PersistentVolumeClaim: &v1.PersistentVolumeClaimVolumeSource{ - ClaimName: pvc, - }, - }, - }, - } - - return po -} diff --git a/internal/sanitize/rb.go b/internal/sanitize/rb.go deleted file mode 100644 index 0f96e0e0..00000000 --- a/internal/sanitize/rb.go +++ /dev/null @@ -1,60 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package sanitize - -import ( - "context" - - "github.com/derailed/popeye/internal" - "github.com/derailed/popeye/internal/cache" - "github.com/derailed/popeye/internal/issues" -) - -type ( - // RBLister represents RB dependencies. - RBLister interface { - RoleBindingLister - ClusterRoleLister - RoleLister - } - - // RoleBinding tracks RoleBinding sanitization. - RoleBinding struct { - *issues.Collector - RBLister - } -) - -// NewRoleBinding returns a new sanitizer. -func NewRoleBinding(c *issues.Collector, lister RBLister) *RoleBinding { - return &RoleBinding{ - Collector: c, - RBLister: lister, - } -} - -// Sanitize cleanse the resource.. -func (r *RoleBinding) Sanitize(ctx context.Context) error { - for fqn, rb := range r.ListRoleBindings() { - r.InitOutcome(fqn) - ctx = internal.WithFQN(ctx, fqn) - - switch rb.RoleRef.Kind { - case "ClusterRole": - if _, ok := r.ListClusterRoles()[rb.RoleRef.Name]; !ok { - r.AddCode(ctx, 1300, rb.RoleRef.Kind, rb.RoleRef.Name) - } - case "Role": - rFQN := cache.FQN(rb.Namespace, rb.RoleRef.Name) - if _, ok := r.ListRoles()[rFQN]; !ok { - r.AddCode(ctx, 1300, rb.RoleRef.Kind, rFQN) - } - } - - if r.NoConcerns(fqn) && r.Config.ExcludeFQN(internal.MustExtractSectionGVR(ctx), fqn) { - r.ClearOutcome(fqn) - } - } - return nil -} diff --git a/internal/sanitize/rb_test.go b/internal/sanitize/rb_test.go deleted file mode 100644 index 3cda0f69..00000000 --- a/internal/sanitize/rb_test.go +++ /dev/null @@ -1,83 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package sanitize - -import ( - "sync" - "testing" - - "github.com/derailed/popeye/internal" - "github.com/derailed/popeye/internal/issues" - "github.com/derailed/popeye/pkg/config" - "github.com/stretchr/testify/assert" - rbacv1 "k8s.io/api/rbac/v1" -) - -func TestRBSanitize(t *testing.T) { - uu := map[string]struct { - lister RBLister - key string - issues []config.ID - }{ - "used": { - key: "default/rb1", - lister: makeRBLister(rbOpts{name: "rb1", refKind: "Role", refName: "r1"}), - }, - "unused": { - key: "default/rb1", - lister: makeRBLister(rbOpts{name: "rb1", refKind: "Role", refName: "blah"}), - issues: []config.ID{1300}, - }, - } - - ctx := makeContext("rbac.authorization.k8s.io/v1/rolebindings", "rb") - for k := range uu { - u := uu[k] - t.Run(k, func(t *testing.T) { - r := NewRoleBinding(issues.NewCollector(loadCodes(t), makeConfig(t)), u.lister) - - assert.Nil(t, r.Sanitize(ctx)) - validateIssues(t, u.key, r.Outcome(), u.issues) - }) - } -} - -// ---------------------------------------------------------------------------- -// Helpers... - -type rbOpts struct { - name, refKind, refName string -} - -type rb struct { - opts rbOpts -} - -var _ RBLister = (*rb)(nil) - -func makeRBLister(opts rbOpts) *rb { - return &rb{opts: opts} -} - -func (r *rb) ListRoleBindings() map[string]*rbacv1.RoleBinding { - return map[string]*rbacv1.RoleBinding{ - "default/" + r.opts.name: makeRB(r.opts.name, r.opts.refKind, r.opts.refName), - } -} - -func (r *rb) ListClusterRoles() map[string]*rbacv1.ClusterRole { - return map[string]*rbacv1.ClusterRole{ - "cr1": makeCR("cr1"), - } -} - -func (r *rb) ListRoles() map[string]*rbacv1.Role { - return map[string]*rbacv1.Role{ - "default/r1": makeRO("r1"), - } -} - -func (r *rb) RoleRefs(refs *sync.Map) { - refs.Store("default/ro1", internal.AllKeys) -} diff --git a/internal/sanitize/ro.go b/internal/sanitize/ro.go deleted file mode 100644 index 64c2b73b..00000000 --- a/internal/sanitize/ro.go +++ /dev/null @@ -1,62 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package sanitize - -import ( - "context" - "sync" - - "github.com/derailed/popeye/internal" - "github.com/derailed/popeye/internal/cache" - "github.com/derailed/popeye/internal/issues" -) - -type ( - // ROLister list out roles and deps. - ROLister interface { - RoleLister - ClusterRoleBindingLister - RoleBindingLister - } - - // Role tracks Role sanitization. - Role struct { - *issues.Collector - ROLister - } -) - -// NewRole returns a new sanitizer. -func NewRole(c *issues.Collector, lister ROLister) *Role { - return &Role{ - Collector: c, - ROLister: lister, - } -} - -// Sanitize cleanse the resource. -func (r *Role) Sanitize(ctx context.Context) error { - var roRefs sync.Map - r.ClusterRoleRefs(&roRefs) - r.RoleRefs(&roRefs) - r.checkInUse(ctx, &roRefs) - - return nil -} - -func (r *Role) checkInUse(ctx context.Context, refs *sync.Map) { - for fqn := range r.ListRoles() { - r.InitOutcome(fqn) - ctx = internal.WithFQN(ctx, fqn) - - _, ok := refs.Load(cache.ResFqn(cache.RoleKey, fqn)) - if !ok { - r.AddCode(ctx, 400) - } - - if r.NoConcerns(fqn) && r.Config.ExcludeFQN(internal.MustExtractSectionGVR(ctx), fqn) { - r.ClearOutcome(fqn) - } - } -} diff --git a/internal/sanitize/ro_test.go b/internal/sanitize/ro_test.go deleted file mode 100644 index 80abc0a5..00000000 --- a/internal/sanitize/ro_test.go +++ /dev/null @@ -1,108 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package sanitize - -import ( - "sync" - "testing" - - "github.com/derailed/popeye/internal" - "github.com/derailed/popeye/internal/cache" - "github.com/derailed/popeye/internal/issues" - "github.com/derailed/popeye/pkg/config" - "github.com/stretchr/testify/assert" - rbacv1 "k8s.io/api/rbac/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -func TestROSanitize(t *testing.T) { - uu := map[string]struct { - lister ROLister - key string - issues []config.ID - }{ - "used": { - key: "default/ro1", - lister: makeROLister("ro1", refOpts{refKind: "ClusterRole", refName: "cr1"}), - }, - "unused": { - key: "default/ro3", - lister: makeROLister("ro3", refOpts{refKind: "ClusterRole", refName: "cr1"}), - issues: []config.ID{400}, - }, - } - - ctx := makeContext("rbac.authorization.k8s.io/v1/roles", "roles") - for k := range uu { - u := uu[k] - t.Run(k, func(t *testing.T) { - r := NewRole(issues.NewCollector(loadCodes(t), makeConfig(t)), u.lister) - - assert.Nil(t, r.Sanitize(ctx)) - validateIssues(t, u.key, r.Outcome(), u.issues) - }) - } -} - -// ---------------------------------------------------------------------------- -// Helpers... - -type refOpts struct { - refKind, refName string -} - -type ro struct { - name string - opts refOpts -} - -var _ ROLister = (*ro)(nil) - -func makeROLister(n string, opts refOpts) *ro { - return &ro{name: n, opts: opts} -} - -func (r *ro) ListRoleBindings() map[string]*rbacv1.RoleBinding { - return map[string]*rbacv1.RoleBinding{ - "default/rb1": makeRB("rb1", "Role", r.name), - } -} - -func (r *ro) ListClusterRoleBindings() map[string]*rbacv1.ClusterRoleBinding { - return map[string]*rbacv1.ClusterRoleBinding{ - "default/crb1": makeCRB("crb1", "ClusterRole", "cr2"), - } -} - -func (r *ro) ListClusterRoles() map[string]*rbacv1.ClusterRole { - return map[string]*rbacv1.ClusterRole{ - "cr1": makeCR("cr1"), - } -} - -func (r *ro) ListRoles() map[string]*rbacv1.Role { - return map[string]*rbacv1.Role{ - "default/" + r.name: makeRO(r.name), - } -} - -func (r *ro) ClusterRoleRefs(refs *sync.Map) { - refs.Store(cache.ResFqn(cache.RoleKey, "default/ro2"), internal.AllKeys) -} -func (r *ro) RoleRefs(refs *sync.Map) { - refs.Store(cache.ResFqn(cache.RoleKey, "default/ro1"), internal.AllKeys) -} - -func makeCRB(name, refKind, refName string) *rbacv1.ClusterRoleBinding { - return &rbacv1.ClusterRoleBinding{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: "default", - }, - RoleRef: rbacv1.RoleRef{ - Kind: refKind, - Name: refName, - }, - } -} diff --git a/internal/sanitize/rs.go b/internal/sanitize/rs.go deleted file mode 100644 index b0e28411..00000000 --- a/internal/sanitize/rs.go +++ /dev/null @@ -1,75 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package sanitize - -import ( - "context" - - "github.com/derailed/popeye/internal" - "github.com/derailed/popeye/internal/issues" - appsv1 "k8s.io/api/apps/v1" -) - -type ( - // ReplicaSet tracks ReplicaSet sanitization. - ReplicaSet struct { - *issues.Collector - ReplicaSetLister - } - - // ReplicaLister list replicaset. - ReplicaLister interface { - ListReplicaSets() map[string]*appsv1.ReplicaSet - } - - // ReplicaSetLister list available ReplicaSets on a cluster. - ReplicaSetLister interface { - ReplicaLister - } -) - -// NewReplicaSet returns a new ReplicaSet sanitizer. -func NewReplicaSet(co *issues.Collector, lister ReplicaSetLister) *ReplicaSet { - return &ReplicaSet{ - Collector: co, - ReplicaSetLister: lister, - } -} - -// Sanitize cleanse the resource. -func (r *ReplicaSet) Sanitize(ctx context.Context) error { - for fqn, rs := range r.ListReplicaSets() { - r.InitOutcome(fqn) - ctx = internal.WithFQN(ctx, fqn) - - r.checkHealth(ctx, rs) - r.checkDeprecation(ctx, rs) - - if r.NoConcerns(fqn) && r.Config.ExcludeFQN(internal.MustExtractSectionGVR(ctx), fqn) { - r.ClearOutcome(fqn) - } - } - - return nil -} - -func (r *ReplicaSet) checkHealth(ctx context.Context, rs *appsv1.ReplicaSet) { - if rs.Spec.Replicas != nil && *rs.Spec.Replicas != rs.Status.ReadyReplicas { - r.AddCode(ctx, 1120, *rs.Spec.Replicas, rs.Status.ReadyReplicas) - } -} - -func (r *ReplicaSet) checkDeprecation(ctx context.Context, rs *appsv1.ReplicaSet) { - const current = "apps/v1" - - rev, err := resourceRev(internal.MustExtractFQN(ctx), "ReplicaSet", rs.Annotations) - if err != nil { - if rev = revFromLink(rs.SelfLink); rev == "" { - return - } - } - if rev != current { - r.AddCode(ctx, 403, "ReplicaSet", rev, current) - } -} diff --git a/internal/sanitize/rs_test.go b/internal/sanitize/rs_test.go deleted file mode 100644 index 8684bd93..00000000 --- a/internal/sanitize/rs_test.go +++ /dev/null @@ -1,86 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package sanitize - -import ( - "testing" - - "github.com/derailed/popeye/internal/cache" - "github.com/derailed/popeye/internal/issues" - "github.com/stretchr/testify/assert" - appsv1 "k8s.io/api/apps/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -func TestRSSanitize(t *testing.T) { - uu := map[string]struct { - lister ReplicaSetLister - issues issues.Issues - }{ - "good": { - lister: makeRSLister("rs", rsOpts{ - rev: "apps/v1", - }), - issues: issues.Issues{}, - }, - "deprecated": { - lister: makeRSLister("rs", rsOpts{ - rev: "extensions/v1beta1", - }), - issues: issues.Issues{ - issues.Issue{ - GVR: "apps/v1/replicasets", - Group: "__root__", - Level: 2, - Message: `[POP-403] Deprecated ReplicaSet API group "extensions/v1beta1". Use "apps/v1" instead`}, - }, - }, - } - - ctx := makeContext("apps/v1/replicasets", "rs") - for k := range uu { - u := uu[k] - t.Run(k, func(t *testing.T) { - rs := NewReplicaSet(issues.NewCollector(loadCodes(t), makeConfig(t)), u.lister) - - assert.Nil(t, rs.Sanitize(ctx)) - assert.Equal(t, u.issues, rs.Outcome()["default/rs"]) - }) - } -} - -type ( - rsOpts struct { - rev string - } - - rs struct { - name string - opts rsOpts - } -) - -func makeRSLister(n string, opts rsOpts) *rs { - return &rs{ - name: n, - opts: opts, - } -} - -func (r *rs) ListReplicaSets() map[string]*appsv1.ReplicaSet { - return map[string]*appsv1.ReplicaSet{ - cache.FQN("default", r.name): makeRS(r.name, r.opts), - } -} - -func makeRS(n string, o rsOpts) *appsv1.ReplicaSet { - return &appsv1.ReplicaSet{ - ObjectMeta: metav1.ObjectMeta{ - Name: n, - Namespace: "default", - SelfLink: "/api/" + o.rev, - }, - Spec: appsv1.ReplicaSetSpec{}, - } -} diff --git a/internal/sanitize/sa.go b/internal/sanitize/sa.go deleted file mode 100644 index c434bf9a..00000000 --- a/internal/sanitize/sa.go +++ /dev/null @@ -1,174 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package sanitize - -import ( - "context" - "sync" - - "github.com/derailed/popeye/internal" - "github.com/derailed/popeye/internal/cache" - "github.com/derailed/popeye/internal/issues" - - v1 "k8s.io/api/core/v1" - rbacv1 "k8s.io/api/rbac/v1" -) - -// BOZO!! Check policy for potential dups or override priviledges - -type ( - // ServiceAccountLister list available ServiceAccounts on a cluster. - ServiceAccountLister interface { - PodLister - ClusterRoleBindingLister - RoleBindingLister - SecretLister - - ListServiceAccounts() map[string]*v1.ServiceAccount - } - - // ClusterRoleBindingRefs tracks crb references. - ClusterRoleBindingRefs interface { - ClusterRoleRefs(*sync.Map) - } - - // RoleBindingRefs tracks rb references. - RoleBindingRefs interface { - RoleRefs(*sync.Map) - } - - // ClusterRoleBindingLister list all available ClusterRoleBindings. - ClusterRoleBindingLister interface { - ClusterRoleBindingRefs - ListClusterRoleBindings() map[string]*rbacv1.ClusterRoleBinding - } - - // RoleBindingLister list all available ClusterRoleBindings. - RoleBindingLister interface { - RoleBindingRefs - ListRoleBindings() map[string]*rbacv1.RoleBinding - } - - // ServiceAccount tracks ServiceAccount sanitizer. - ServiceAccount struct { - *issues.Collector - - ServiceAccountLister - } -) - -// NewServiceAccount returns a new sanitizer. -func NewServiceAccount(co *issues.Collector, lister ServiceAccountLister) *ServiceAccount { - return &ServiceAccount{ - Collector: co, - ServiceAccountLister: lister, - } - -} - -// Sanitize cleanse the resource. -func (s *ServiceAccount) Sanitize(ctx context.Context) error { - refs := make(map[string]struct{}, 20) - if err := s.crbRefs(refs); err != nil { - return err - } - if err := s.rbRefs(refs); err != nil { - return err - } - err := s.podRefs(refs) - if err != nil { - return err - } - - for fqn, sa := range s.ListServiceAccounts() { - s.InitOutcome(fqn) - ctx = internal.WithFQN(ctx, fqn) - - s.checkMounts(ctx, sa.AutomountServiceAccountToken) - s.checkSecretRefs(ctx, sa.Secrets) - s.checkPullSecretRefs(ctx, sa.ImagePullSecrets) - if _, ok := refs[fqn]; !ok { - s.AddCode(ctx, 400) - } - - if s.NoConcerns(fqn) && s.Config.ExcludeFQN(internal.MustExtractSectionGVR(ctx), fqn) { - s.ClearOutcome(fqn) - } - } - - return nil -} - -func (s *ServiceAccount) checkSecretRefs(ctx context.Context, refs []v1.ObjectReference) { - ns, _ := namespaced(internal.MustExtractFQN(ctx)) - for _, ref := range refs { - if ref.Namespace != "" { - ns = ref.Namespace - } - sfqn := cache.FQN(ns, ref.Name) - if _, ok := s.ListSecrets()[sfqn]; !ok { - s.AddCode(ctx, 304, sfqn) - } - } -} - -func (s *ServiceAccount) checkPullSecretRefs(ctx context.Context, refs []v1.LocalObjectReference) { - ns, _ := namespaced(internal.MustExtractFQN(ctx)) - for _, ref := range refs { - sfqn := cache.FQN(ns, ref.Name) - if _, ok := s.ListSecrets()[sfqn]; !ok { - s.AddCode(ctx, 305, sfqn) - } - } -} - -func (s *ServiceAccount) checkMounts(ctx context.Context, b *bool) { - if b != nil && *b { - s.AddCode(ctx, 303) - } -} - -func (s *ServiceAccount) crbRefs(refs map[string]struct{}) error { - for _, crb := range s.ListClusterRoleBindings() { - pullSas(crb.Subjects, refs) - } - - return nil -} - -func (s *ServiceAccount) rbRefs(refs map[string]struct{}) error { - for _, rb := range s.ListRoleBindings() { - pullSas(rb.Subjects, refs) - } - - return nil -} - -func (s *ServiceAccount) podRefs(refs map[string]struct{}) error { - for _, p := range s.ListPods() { - if p.Spec.ServiceAccountName != "" { - refs[cache.FQN(p.Namespace, p.Spec.ServiceAccountName)] = struct{}{} - } - } - - return nil -} - -// ---------------------------------------------------------------------------- -// Helpers... - -func pullSas(ss []rbacv1.Subject, res map[string]struct{}) { - for _, s := range ss { - if s.Kind == "ServiceAccount" { - fqn := fqnSubject(s) - if _, ok := res[fqn]; !ok { - res[fqn] = struct{}{} - } - } - } -} - -func fqnSubject(s rbacv1.Subject) string { - return cache.FQN(s.Namespace, s.Name) -} diff --git a/internal/sanitize/sa_test.go b/internal/sanitize/sa_test.go deleted file mode 100644 index 94cea66d..00000000 --- a/internal/sanitize/sa_test.go +++ /dev/null @@ -1,182 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package sanitize - -import ( - "sync" - "testing" - - "github.com/derailed/popeye/internal/cache" - "github.com/derailed/popeye/internal/issues" - "github.com/stretchr/testify/assert" - v1 "k8s.io/api/core/v1" - rbacv1 "k8s.io/api/rbac/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -func TestSASanitize(t *testing.T) { - uu := map[string]struct { - lister ServiceAccountLister - issues int - }{ - "cool": { - makeSALister("sa1", saOpts{ - used: "sa1", - }), - 0, - }, - "notUsed": { - makeSALister("sa1", saOpts{ - used: "sa2", - }), - 1, - }, - "missingSecret": { - makeSALister("sa1", saOpts{ - used: "sa1", - secret: "blee", - pullSecret: "fred", - }), - 2, - }, - } - - ctx := makeContext("v1/serviceaccounts", "sa") - for k := range uu { - u := uu[k] - t.Run(k, func(t *testing.T) { - s := NewServiceAccount(issues.NewCollector(loadCodes(t), makeConfig(t)), u.lister) - - assert.Nil(t, s.Sanitize(ctx)) - assert.Equal(t, u.issues, len(s.Outcome()["default/sa1"])) - }) - } -} - -// ---------------------------------------------------------------------------- -// Helpers... - -type saOpts struct { - used string - secret, pullSecret string -} - -type sa struct { - name string - opts saOpts -} - -func makeSALister(n string, opts saOpts) sa { - return sa{name: n, opts: opts} -} - -func (s sa) ActiveNamespace() string { - return "" -} - -func (s sa) ExcludedNS(ns string) bool { - return false -} - -func (s sa) ListClusterRoleBindings() map[string]*rbacv1.ClusterRoleBinding { - return map[string]*rbacv1.ClusterRoleBinding{ - "crb1": makeSACRB("crb1", s.opts.used), - } -} - -func (s sa) ListRoleBindings() map[string]*rbacv1.RoleBinding { - return map[string]*rbacv1.RoleBinding{ - "default/rb1": makeSARB("rb1", s.opts.used), - } -} - -func (s sa) ListPods() map[string]*v1.Pod { - return map[string]*v1.Pod{ - "default/p1": makePodSa("p1", s.opts.used), - } -} - -func (s sa) RoleRefs(*sync.Map) {} -func (s sa) ClusterRoleRefs(*sync.Map) {} -func (s sa) IngressRefs(*sync.Map) {} -func (s sa) ServiceAccountRefs(*sync.Map) {} -func (s sa) PodRefs(*sync.Map) {} - -func (s sa) ListSecrets() map[string]*v1.Secret { - return map[string]*v1.Secret{ - "default/s1": makeSecret("s1"), - "default/dks1": makeDockerSecret("dks1"), - } -} - -func (s sa) GetPod(ns string, sel map[string]string) *v1.Pod { - return nil -} - -func (s sa) ListServiceAccounts() map[string]*v1.ServiceAccount { - return map[string]*v1.ServiceAccount{ - cache.FQN("default", s.name): makeSa(s.name, s.opts.secret, s.opts.pullSecret), - } -} - -func makeSa(n, secret, pullSecret string) *v1.ServiceAccount { - sa := v1.ServiceAccount{ - ObjectMeta: metav1.ObjectMeta{ - Name: n, - Namespace: "default", - }, - } - - if secret != "" { - sa.Secrets = []v1.ObjectReference{ - {Namespace: "default", Name: secret, Kind: "secret"}, - } - } - - if pullSecret != "" { - sa.ImagePullSecrets = []v1.LocalObjectReference{ - {Name: pullSecret}, - } - } - - return &sa -} - -func makePodSa(s, sa string) *v1.Pod { - po := makePod(s) - po.Spec.ServiceAccountName = sa - - return po -} - -func makeSACRB(s, sa string) *rbacv1.ClusterRoleBinding { - return &rbacv1.ClusterRoleBinding{ - ObjectMeta: metav1.ObjectMeta{ - Name: s, - }, - Subjects: []rbacv1.Subject{ - { - Kind: "ServiceAccount", - Name: sa, - Namespace: "default", - }, - }, - } -} - -func makeSARB(s, sa string) *rbacv1.RoleBinding { - return &rbacv1.RoleBinding{ - ObjectMeta: metav1.ObjectMeta{ - Name: s, - Namespace: "default", - }, - Subjects: []rbacv1.Subject{ - { - Kind: "ServiceAccount", - Name: sa, - Namespace: "default", - }, - }, - } -} diff --git a/internal/sanitize/secret.go b/internal/sanitize/secret.go deleted file mode 100644 index 09680d70..00000000 --- a/internal/sanitize/secret.go +++ /dev/null @@ -1,94 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package sanitize - -import ( - "context" - "sync" - - "github.com/derailed/popeye/internal" - "github.com/derailed/popeye/internal/cache" - "github.com/derailed/popeye/internal/issues" - v1 "k8s.io/api/core/v1" -) - -type ( - // Secret tracks Secret sanitization. - Secret struct { - *issues.Collector - SecretLister - } - - // SARefs tracks ServiceAccount object references. - SARefs interface { - ServiceAccountRefs(*sync.Map) - } - - // IngressRefs tracks Ingress object references. - IngressRefs interface { - IngressRefs(*sync.Map) - } - - // SecretLister list available Secrets on a cluster. - SecretLister interface { - PodRefs - SARefs - IngressRefs - ListSecrets() map[string]*v1.Secret - } -) - -// NewSecret returns a new sanitizer. -func NewSecret(co *issues.Collector, lister SecretLister) *Secret { - return &Secret{ - Collector: co, - SecretLister: lister, - } -} - -// Sanitize cleanse the resource. -func (s *Secret) Sanitize(ctx context.Context) error { - var refs sync.Map - - s.PodRefs(&refs) - s.ServiceAccountRefs(&refs) - s.IngressRefs(&refs) - s.checkInUse(ctx, &refs) - - return nil -} - -func (s *Secret) checkInUse(ctx context.Context, refs *sync.Map) { - for fqn, sec := range s.ListSecrets() { - s.InitOutcome(fqn) - ctx = internal.WithFQN(ctx, fqn) - defer func(fqn string, ctx context.Context) { - if s.NoConcerns(fqn) && s.Config.ExcludeFQN(internal.MustExtractSectionGVR(ctx), fqn) { - s.ClearOutcome(fqn) - } - }(fqn, ctx) - - refs.Range(func(k, v interface{}) bool { - return true - }) - - keys, ok := refs.Load(cache.ResFqn(cache.SecretKey, fqn)) - if !ok { - s.AddCode(ctx, 400) - continue - } - if keys.(internal.StringSet).Has(internal.All) { - continue - } - - kk := make(internal.StringSet, len(sec.Data)) - for k := range sec.Data { - kk.Add(k) - } - deltas := keys.(internal.StringSet).Diff(kk) - for k := range deltas { - s.AddCode(ctx, 401, k) - } - } -} diff --git a/internal/sanitize/secret_test.go b/internal/sanitize/secret_test.go deleted file mode 100644 index 0ccdfe72..00000000 --- a/internal/sanitize/secret_test.go +++ /dev/null @@ -1,97 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package sanitize - -import ( - "sync" - "testing" - - "github.com/derailed/popeye/internal" - "github.com/derailed/popeye/internal/issues" - "github.com/derailed/popeye/pkg/config" - "github.com/stretchr/testify/assert" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -func TestSecretSanitize(t *testing.T) { - ctx := makeContext("v1/secrets", "secret") - s := NewSecret(issues.NewCollector(loadCodes(t), makeConfig(t)), newSecret()) - - assert.Nil(t, s.Sanitize(ctx)) - assert.Equal(t, 5, len(s.Outcome())) - - ii := s.Outcome()["default/sec3"] - assert.Equal(t, 1, len(ii)) - assert.Equal(t, "[POP-400] Used? Unable to locate resource reference", ii[0].Message) - assert.Equal(t, config.InfoLevel, ii[0].Level) - - ii = s.Outcome()["default/sec4"] - assert.Equal(t, 1, len(ii)) - assert.Equal(t, `[POP-401] Key "k2" used? Unable to locate key reference`, ii[0].Message) - assert.Equal(t, config.InfoLevel, ii[0].Level) -} - -// ---------------------------------------------------------------------------- -// Helpers... - -type secretMock struct{} - -func newSecret() secretMock { - return secretMock{} -} - -func (m secretMock) PodRefs(refs *sync.Map) { - refs.Store("sec:default/sec1", internal.StringSet{ - "k1": internal.Blank, - "k2": internal.Blank, - }) - refs.Store("sec:default/sec2", internal.AllKeys) - refs.Store("sec:default/sec4", internal.StringSet{ - "k1": internal.Blank, - }) -} - -func (m secretMock) IngressRefs(*sync.Map) {} - -func (m secretMock) ServiceAccountRefs(refs *sync.Map) { - refs.Store("sec:default/sec5", internal.AllKeys) -} - -func (m secretMock) ListSecrets() map[string]*v1.Secret { - return map[string]*v1.Secret{ - "default/sec1": makeSecret("sec1"), - "default/sec2": makeSecret("sec2"), - "default/sec3": makeSecret("sec3"), - "default/sec4": makeSecret("sec4"), - "default/sec5": makeSecret("sec5"), - } -} - -func makeSecret(n string) *v1.Secret { - return &v1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: n, - Namespace: "default", - }, - Data: map[string][]byte{ - "k1": {}, - "k2": {}, - }, - } -} - -func makeDockerSecret(n string) *v1.Secret { - return &v1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: n, - Namespace: "default", - }, - Type: v1.SecretTypeDockercfg, - Data: map[string][]byte{ - "k1": {}, - "k2": {}, - }, - } -} diff --git a/internal/sanitize/sts_test.go b/internal/sanitize/sts_test.go deleted file mode 100644 index 460d88c0..00000000 --- a/internal/sanitize/sts_test.go +++ /dev/null @@ -1,369 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package sanitize - -import ( - "context" - "testing" - - "github.com/derailed/popeye/internal" - "github.com/derailed/popeye/internal/cache" - "github.com/derailed/popeye/internal/client" - "github.com/derailed/popeye/internal/issues" - "github.com/derailed/popeye/pkg/config" - "github.com/stretchr/testify/assert" - appsv1 "k8s.io/api/apps/v1" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" -) - -func TestSTSSanitizer(t *testing.T) { - uu := map[string]struct { - lister StatefulSetLister - issues issues.Issues - }{ - "good": { - lister: makeSTSLister(stsOpts{ - coOpts: coOpts{rcpu: "100m", rmem: "10Mi"}, - replicas: 1, - currentReps: 1, - rev: "apps/v1", - ccpu: "100m", cmem: "10Mi", - }), - issues: issues.Issues{}, - }, - "deprecated": { - lister: makeSTSLister(stsOpts{ - coOpts: coOpts{rcpu: "100m", rmem: "10Mi"}, - replicas: 1, - currentReps: 1, - rev: "extensions/v1", - ccpu: "100m", cmem: "10Mi", - }), - issues: issues.Issues{ - issues.New(client.NewGVR("apps/v1/statefulsets"), issues.Root, config.WarnLevel, `[POP-403] Deprecated StatefulSet API group "extensions/v1". Use "apps/v1" instead`), - }, - }, - "unhealthy": { - lister: makeSTSLister(stsOpts{ - coOpts: coOpts{rcpu: "100m", rmem: "10Mi"}, - replicas: 1, - currentReps: 0, - rev: "apps/v1", - ccpu: "100m", cmem: "10Mi", - }), - issues: issues.Issues{ - issues.New(client.NewGVR("apps/v1/statefulsets"), issues.Root, config.ErrorLevel, "[POP-501] Unhealthy 1 desired but have 0 available"), - }, - }, - "zeroReplicas": { - lister: makeSTSLister(stsOpts{ - coOpts: coOpts{rcpu: "100m", rmem: "10Mi"}, - replicas: 0, - currentReps: 1, - rev: "apps/v1", - ccpu: "100m", cmem: "10Mi", - }), - issues: issues.Issues{ - issues.New(client.NewGVR("apps/v1/statefulsets"), issues.Root, config.WarnLevel, "[POP-500] Zero scale detected"), - }, - }, - } - - ctx := makeContext("apps/v1/statefulsets", "sts") - for k := range uu { - u := uu[k] - t.Run(k, func(t *testing.T) { - sts := NewStatefulSet(issues.NewCollector(loadCodes(t), makeConfig(t)), u.lister) - - assert.Nil(t, sts.Sanitize(ctx)) - assert.Equal(t, u.issues, sts.Outcome()["default/sts1"]) - }) - } -} - -func TestSTSSanitizerUtilization(t *testing.T) { - uu := map[string]struct { - lister StatefulSetLister - issues issues.Issues - }{ - "bestEffort": { - lister: makeSTSLister(stsOpts{ - replicas: 1, - currentReps: 1, - rev: "apps/v1", - ccpu: "200m", cmem: "10Mi", - }), - issues: issues.Issues{}, - }, - "underCPUBurstable": { - lister: makeSTSLister(stsOpts{ - coOpts: coOpts{ - rcpu: "100m", rmem: "10Mi", - }, - replicas: 1, - currentReps: 1, - rev: "apps/v1", - ccpu: "200m", cmem: "10Mi", - }), - issues: issues.Issues{ - issues.New(client.NewGVR("apps/v1/statefulsets"), issues.Root, config.WarnLevel, "[POP-503] At current load, CPU under allocated. Current:400m vs Requested:200m (200.00%)"), - }, - }, - "underCPUGuaranteed": { - lister: makeSTSLister(stsOpts{ - coOpts: coOpts{ - rcpu: "100m", rmem: "10Mi", - lcpu: "100m", lmem: "10Mi", - }, - replicas: 1, - currentReps: 1, - rev: "apps/v1", - ccpu: "200m", cmem: "10Mi", - }), - issues: issues.Issues{ - issues.New(client.NewGVR("apps/v1/statefulsets"), issues.Root, config.WarnLevel, "[POP-503] At current load, CPU under allocated. Current:400m vs Requested:200m (200.00%)"), - }, - }, - "overCPUBurstable": { - lister: makeSTSLister(stsOpts{ - coOpts: coOpts{ - rcpu: "400m", rmem: "10Mi", - }, - replicas: 1, - currentReps: 1, - rev: "apps/v1", - ccpu: "100m", cmem: "10Mi", - }), - issues: issues.Issues{ - issues.New(client.NewGVR("apps/v1/statefulsets"), issues.Root, config.WarnLevel, "[POP-504] At current load, CPU over allocated. Current:200m vs Requested:800m (400.00%)"), - }, - }, - "overCPUGuarenteed": { - lister: makeSTSLister(stsOpts{ - coOpts: coOpts{ - rcpu: "400m", rmem: "10Mi", - lcpu: "400m", lmem: "10Mi", - }, - replicas: 1, - currentReps: 1, - rev: "apps/v1", - ccpu: "100m", cmem: "10Mi", - }), - issues: issues.Issues{ - issues.New(client.NewGVR("apps/v1/statefulsets"), issues.Root, config.WarnLevel, "[POP-504] At current load, CPU over allocated. Current:200m vs Requested:800m (400.00%)"), - }, - }, - "underMEMBurstable": { - lister: makeSTSLister(stsOpts{ - coOpts: coOpts{ - rcpu: "100m", rmem: "10Mi", - }, - replicas: 1, - currentReps: 1, - rev: "apps/v1", - ccpu: "100m", cmem: "20Mi", - }), - issues: issues.Issues{ - issues.New(client.NewGVR("apps/v1/statefulsets"), issues.Root, config.WarnLevel, "[POP-505] At current load, Memory under allocated. Current:40Mi vs Requested:20Mi (200.00%)"), - }, - }, - "underMEMGuaranteed": { - lister: makeSTSLister(stsOpts{ - coOpts: coOpts{ - rcpu: "100m", rmem: "10Mi", - lcpu: "100m", lmem: "10Mi", - }, - replicas: 1, - currentReps: 1, - rev: "apps/v1", - ccpu: "100m", cmem: "20Mi", - }), - issues: issues.Issues{ - issues.New(client.NewGVR("apps/v1/statefulsets"), issues.Root, config.WarnLevel, "[POP-505] At current load, Memory under allocated. Current:40Mi vs Requested:20Mi (200.00%)"), - }, - }, - "overMEMBurstable": { - lister: makeSTSLister(stsOpts{ - coOpts: coOpts{ - rcpu: "100m", rmem: "100Mi", - }, - replicas: 1, - currentReps: 1, - rev: "apps/v1", - ccpu: "100m", cmem: "20Mi", - }), - issues: issues.Issues{ - issues.New(client.NewGVR("apps/v1/statefulsets"), issues.Root, config.WarnLevel, "[POP-506] At current load, Memory over allocated. Current:40Mi vs Requested:200Mi (500.00%)"), - }, - }, - "overMEMGuaranteed": { - lister: makeSTSLister(stsOpts{ - coOpts: coOpts{ - rcpu: "100m", rmem: "100Mi", - lcpu: "100m", lmem: "100Mi", - }, - replicas: 1, - currentReps: 1, - rev: "apps/v1", - ccpu: "100m", cmem: "20Mi", - }), - issues: issues.Issues{ - issues.New(client.NewGVR("apps/v1/statefulsets"), issues.Root, config.WarnLevel, "[POP-506] At current load, Memory over allocated. Current:40Mi vs Requested:200Mi (500.00%)"), - }, - }, - } - - ctx := makeContext("apps/v1/statefulsets", "sts") - ctx = context.WithValue(ctx, internal.KeyOverAllocs, true) - for k := range uu { - u := uu[k] - t.Run(k, func(t *testing.T) { - sts := NewStatefulSet(issues.NewCollector(loadCodes(t), makeConfig(t)), u.lister) - - assert.Nil(t, sts.Sanitize(ctx)) - assert.Equal(t, u.issues, sts.Outcome()["default/sts1"]) - }) - } -} - -// ---------------------------------------------------------------------------- -// Helpers... - -type ( - stsOpts struct { - coOpts - replicas int32 - currentReps int32 - collisions int32 - ccpu, cmem string - rev string - } - - sts struct { - name string - opts stsOpts - } -) - -func makeSTSLister(opts stsOpts) *sts { - return &sts{ - name: "sts1", - opts: opts, - } -} - -func (s *sts) CPUResourceLimits() config.Allocations { - return config.Allocations{ - UnderPerc: 100, - OverPerc: 50, - } -} - -func (s *sts) MEMResourceLimits() config.Allocations { - return config.Allocations{ - UnderPerc: 100, - OverPerc: 50, - } -} - -func (*sts) RestartsLimit() int { - return 10 -} - -func (*sts) PodCPULimit() float64 { - return 100 -} - -func (*sts) PodMEMLimit() float64 { - return 100 -} - -func (s *sts) ListStatefulSets() map[string]*appsv1.StatefulSet { - return map[string]*appsv1.StatefulSet{ - cache.FQN("default", s.name): makeSTS(s.name, s.opts), - } -} - -func (s *sts) ListPodsBySelector(ns string, sel *metav1.LabelSelector) map[string]*v1.Pod { - return map[string]*v1.Pod{ - "default/p1": makeFullPod(podOpts{ - coOpts: coOpts{ - rcpu: s.opts.rcpu, - rmem: s.opts.rmem, - lcpu: s.opts.lcpu, - lmem: s.opts.lmem, - }}), - } -} - -func (s *sts) ListServiceAccounts() map[string]*v1.ServiceAccount { - return nil -} - -func (s *sts) ListPodsMetrics() map[string]*mv1beta1.PodMetrics { - return map[string]*mv1beta1.PodMetrics{ - "default/p1": makeMxPod(s.opts.ccpu, s.opts.cmem), - } -} - -func makeSTS(n string, opts stsOpts) *appsv1.StatefulSet { - return &appsv1.StatefulSet{ - ObjectMeta: metav1.ObjectMeta{ - Name: n, - Namespace: "default", - SelfLink: "/api/" + opts.rev, - }, - Spec: appsv1.StatefulSetSpec{ - Replicas: &opts.replicas, - Selector: &metav1.LabelSelector{ - MatchLabels: map[string]string{ - "fred": "blee", - }, - }, - Template: v1.PodTemplateSpec{ - Spec: v1.PodSpec{ - Containers: []v1.Container{ - { - Name: "c1", - Image: "fred:0.0.1", - Resources: v1.ResourceRequirements{ - Requests: v1.ResourceList{ - v1.ResourceCPU: toQty(opts.coOpts.rcpu), - v1.ResourceMemory: toQty(opts.coOpts.rmem), - }, - Limits: v1.ResourceList{ - v1.ResourceCPU: toQty(opts.coOpts.lcpu), - v1.ResourceMemory: toQty(opts.coOpts.lmem), - }, - }, - }, - }, - InitContainers: []v1.Container{ - { - Name: "i1", - Image: "fred:0.0.1", - Resources: v1.ResourceRequirements{ - Requests: v1.ResourceList{ - v1.ResourceCPU: toQty(opts.coOpts.rcpu), - v1.ResourceMemory: toQty(opts.coOpts.rmem), - }, - Limits: v1.ResourceList{ - v1.ResourceCPU: toQty(opts.coOpts.lcpu), - v1.ResourceMemory: toQty(opts.coOpts.lmem), - }, - }, - }, - }, - }, - }, - }, - Status: appsv1.StatefulSetStatus{ - CurrentReplicas: opts.currentReps, - ReadyReplicas: opts.currentReps, - CollisionCount: &opts.collisions, - }, - } -} diff --git a/internal/sanitize/svc_test.go b/internal/sanitize/svc_test.go deleted file mode 100644 index 888e2210..00000000 --- a/internal/sanitize/svc_test.go +++ /dev/null @@ -1,327 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package sanitize - -import ( - "testing" - - "github.com/derailed/popeye/internal/cache" - "github.com/derailed/popeye/internal/issues" - "github.com/stretchr/testify/assert" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/util/intstr" -) - -func TestSVCSanitize(t *testing.T) { - uu := map[string]struct { - lister ServiceLister - issues int - }{ - "cool": { - makeSvcLister( - svcOpts{ - kind: v1.ServiceTypeClusterIP, - hasEndPoints: true, - hasSelector: true, - hasPod: true, - }, - ), - 0, - }, - "noEp": { - makeSvcLister( - svcOpts{ - kind: v1.ServiceTypeClusterIP, - hasSelector: true, - hasPod: true, - }, - ), - 1, - }, - "noMatchingPods": { - makeSvcLister( - svcOpts{ - kind: v1.ServiceTypeClusterIP, - hasSelector: true, - hasEndPoints: true, - }, - ), - 1, - }, - "lbType": { - makeSvcLister( - svcOpts{ - kind: v1.ServiceTypeLoadBalancer, - hasEndPoints: true, - hasSelector: true, - hasPod: true, - }, - ), - 1, - }, - "nodePortType": { - makeSvcLister( - svcOpts{ - kind: v1.ServiceTypeNodePort, - hasEndPoints: true, - hasSelector: true, - hasPod: true, - }, - ), - 1, - }, - "noSelector": { - makeSvcLister( - svcOpts{ - kind: v1.ServiceTypeClusterIP, - hasEndPoints: true, - hasPod: true, - }, - ), - 0, - }, - "externalSvc": { - makeSvcLister( - svcOpts{ - kind: v1.ServiceTypeExternalName, - hasSelector: true, - hasPod: true, - }, - ), - 0, - }, - "portProtoFail": { - makeSvcLister( - svcOpts{ - kind: v1.ServiceTypeExternalName, - hasSelector: true, - hasPod: true, - ports: []v1.ServicePort{ - { - Name: "p1", - Port: 80, - TargetPort: intstr.FromInt(80), - Protocol: v1.ProtocolUDP, - }, - }, - }, - ), - 1, - }, - "badTargetPortNumb": { - makeSvcLister( - svcOpts{ - kind: v1.ServiceTypeClusterIP, - hasSelector: true, - hasPod: true, - hasEndPoints: true, - ports: []v1.ServicePort{ - { - Name: "p1", - Port: 80, - TargetPort: intstr.Parse("90"), - Protocol: v1.ProtocolTCP, - }, - }, - }, - ), - 1, - }, - "badNamedTargetPort": { - makeSvcLister( - svcOpts{ - kind: v1.ServiceTypeClusterIP, - hasSelector: true, - hasPod: true, - hasEndPoints: true, - ports: []v1.ServicePort{ - { - Name: "p1", - Port: 80, - TargetPort: intstr.Parse("toast"), - Protocol: v1.ProtocolTCP, - }, - }, - }, - ), - 1, - }, - "unnamedTargetPort": { - makeSvcLister( - svcOpts{ - kind: v1.ServiceTypeClusterIP, - hasSelector: true, - hasPod: true, - hasEndPoints: true, - ports: []v1.ServicePort{ - { - Name: "p1", - Port: 80, - TargetPort: intstr.Parse("80"), - Protocol: v1.ProtocolTCP, - }, - }, - }, - ), - 1, - }, - "unamedSvcPort": { - makeSvcLister( - svcOpts{ - kind: v1.ServiceTypeClusterIP, - hasSelector: true, - hasPod: true, - hasEndPoints: true, - ports: []v1.ServicePort{ - { - Port: 80, - Protocol: v1.ProtocolTCP, - TargetPort: intstr.Parse("p1"), - }, - }, - }, - ), - 0, - }, - "unmatchedSvcPort": { - makeSvcLister( - svcOpts{ - kind: v1.ServiceTypeClusterIP, - hasSelector: true, - hasPod: true, - hasEndPoints: true, - ports: []v1.ServicePort{ - { - Name: "p3", - Port: 15014, - Protocol: v1.ProtocolTCP, - TargetPort: intstr.Parse("15014"), - }, - }, - }, - ), - 1, - }, - } - - ctx := makeContext("v1/services", "svc") - for k := range uu { - u := uu[k] - t.Run(k, func(t *testing.T) { - s := NewService(issues.NewCollector(loadCodes(t), makeConfig(t)), u.lister) - - assert.Nil(t, s.Sanitize(ctx)) - assert.Equal(t, u.issues, len(s.Outcome()["default/s1"])) - }) - } -} - -// ---------------------------------------------------------------------------- -// Helpers... - -type ( - svcOpts struct { - hasEndPoints bool - hasPod bool - hasSelector bool - kind v1.ServiceType - ports []v1.ServicePort - } - - svc struct { - name string - opts svcOpts - } -) - -func makeSvcLister(opts svcOpts) *svc { - return &svc{ - name: "s1", - opts: opts, - } -} - -func (s *svc) ListServices() map[string]*v1.Service { - return map[string]*v1.Service{ - cache.FQN("default", s.name): makeSvc(s.name, s.opts), - } -} - -func (s *svc) GetPod(string, map[string]string) *v1.Pod { - if s.opts.hasPod { - return makeSvcPod("p1") - } - - return nil -} - -func (s *svc) GetEndpoints(string) *v1.Endpoints { - if s.opts.hasEndPoints { - return makeEp(s.name, []string{"1.1.1.1", "2.2.2.2"}...) - } - - return nil -} - -func makeSvcPod(n string) *v1.Pod { - po := makePod(n) - - po.Spec = v1.PodSpec{ - Containers: []v1.Container{ - { - Name: "c1", - Image: "freddy:0.0.1", - Ports: []v1.ContainerPort{ - {Name: "p1", ContainerPort: 80, Protocol: v1.ProtocolTCP}, - {Name: "p2", ContainerPort: 81, Protocol: v1.ProtocolUDP}, - }, - }, - }, - InitContainers: []v1.Container{ - { - Name: "i1", - Image: "freddo:0.0.1", - }, - }, - } - - return po -} - -func makeSvc(s string, opts svcOpts) *v1.Service { - svc := v1.Service{ - ObjectMeta: metav1.ObjectMeta{ - Name: s, - Namespace: "default", - }, - Spec: v1.ServiceSpec{ - Type: opts.kind, - }, - } - if opts.hasSelector { - svc.Spec.Selector = map[string]string{"app": "fred"} - } - svc.Spec.Ports = opts.ports - - return &svc -} - -func makeEp(s string, ips ...string) *v1.Endpoints { - ep := &v1.Endpoints{ - ObjectMeta: metav1.ObjectMeta{ - Name: s, - Namespace: "default", - }, - } - add := make([]v1.EndpointAddress, 0, len(ips)) - for _, ip := range ips { - add = append(add, v1.EndpointAddress{IP: ip}) - } - ep.Subsets = []v1.EndpointSubset{ - {Addresses: add}, - } - - return ep -} diff --git a/internal/scrub/cache.go b/internal/scrub/cache.go index 456a9664..98aafebd 100644 --- a/internal/scrub/cache.go +++ b/internal/scrub/cache.go @@ -4,43 +4,80 @@ package scrub import ( + "context" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/cache" + "github.com/derailed/popeye/internal/dag" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/issues" "github.com/derailed/popeye/pkg/config" "github.com/derailed/popeye/types" ) -type dial struct { +// Cache tracks commonly used resources. +type Cache struct { factory types.Factory - config *config.Config + Config *config.Config + DB *db.DB + Loader *db.Loader + cl *cache.Cluster } -func newDial(f types.Factory, cfg *config.Config) *dial { - return &dial{ +func NewCache(dba *db.DB, f types.Factory, c *config.Config) *Cache { + return &Cache{ + DB: dba, factory: f, - config: cfg, + Config: c, + Loader: db.NewLoader(dba), } } -// Cache tracks commonly used resources. -type Cache struct { - *dial - *core - *apps - *rbac - *policy - *ext - *mx +func (c *Cache) cluster(ctx context.Context) (*cache.Cluster, error) { + if c.cl != nil { + return c.cl, nil + } + ctx, cancel := context.WithCancel(ctx) + defer cancel() + v, err := dag.ListVersion(ctx) + if err != nil { + return nil, err + } + c.cl = cache.NewCluster(v) + + return c.cl, nil } -// NewCache returns a new resource cache -func NewCache(f types.Factory, cfg *config.Config) *Cache { - d := newDial(f, cfg) - return &Cache{ - dial: d, - core: newCore(d), - apps: newApps(d), - rbac: newRBAC(d), - policy: newPolicy(d), - ext: newExt(d), - mx: newMX(d), +type scrubFn func(context.Context, *Cache, *issues.Codes) Linter + +func Scrubers() map[internal.R]scrubFn { + return map[internal.R]scrubFn{ + internal.CL: NewCluster, + internal.CM: NewConfigMap, + internal.NS: NewNamespace, + internal.NO: NewNode, + internal.PO: NewPod, + internal.PV: NewPersistentVolume, + internal.PVC: NewPersistentVolumeClaim, + internal.SEC: NewSecret, + internal.SVC: NewService, + internal.SA: NewServiceAccount, + internal.DS: NewDaemonSet, + internal.DP: NewDeployment, + internal.RS: NewReplicaSet, + internal.STS: NewStatefulSet, + internal.NP: NewNetworkPolicy, + internal.ING: NewIngress, + internal.CR: NewClusterRole, + internal.CRB: NewClusterRoleBinding, + internal.RO: NewRole, + internal.ROB: NewRoleBinding, + internal.PDB: NewPodDisruptionBudget, + internal.HPA: NewHorizontalPodAutoscaler, + internal.CJOB: NewCronJob, + internal.JOB: NewJob, + internal.GWC: NewGatewayClass, + internal.GW: NewGateway, + internal.GWR: NewHTTPRoute, } } diff --git a/internal/scrub/cache_apps.go b/internal/scrub/cache_apps.go deleted file mode 100644 index 63c7035b..00000000 --- a/internal/scrub/cache_apps.go +++ /dev/null @@ -1,107 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package scrub - -import ( - "context" - "sync" - - "github.com/derailed/popeye/internal" - "github.com/derailed/popeye/internal/cache" - "github.com/derailed/popeye/internal/client" - "github.com/derailed/popeye/internal/dag" -) - -type apps struct { - *dial - - mx sync.Mutex - dp *cache.Deployment - ds *cache.DaemonSet - sts *cache.StatefulSet - rs *cache.ReplicaSet -} - -func newApps(d *dial) *apps { - return &apps{dial: d} -} - -func (a *apps) deployments() (*cache.Deployment, error) { - a.mx.Lock() - defer a.mx.Unlock() - - if a.dp != nil { - return a.dp, nil - } - ctx, cancel := a.context() - defer cancel() - dps, err := dag.ListDeployments(ctx) - a.dp = cache.NewDeployment(dps) - - return a.dp, err -} - -func (a *apps) replicasets() (*cache.ReplicaSet, error) { - a.mx.Lock() - defer a.mx.Unlock() - - if a.rs != nil { - return a.rs, nil - } - ctx, cancel := a.context() - defer cancel() - rss, err := dag.ListReplicaSets(ctx) - a.rs = cache.NewReplicaSet(rss) - - return a.rs, err -} - -func (a *apps) daemonSets() (*cache.DaemonSet, error) { - a.mx.Lock() - defer a.mx.Unlock() - - if a.ds != nil { - return a.ds, nil - } - ctx, cancel := a.context() - defer cancel() - ds, err := dag.ListDaemonSets(ctx) - a.ds = cache.NewDaemonSet(ds) - - return a.ds, err -} - -func (a *apps) statefulsets() (*cache.StatefulSet, error) { - a.mx.Lock() - defer a.mx.Unlock() - - if a.sts != nil { - return a.sts, nil - } - - ctx, cancel := a.context() - defer cancel() - sts, err := dag.ListStatefulSets(ctx) - a.sts = cache.NewStatefulSet(sts) - - return a.sts, err -} - -// Helpers... - -func (a *apps) context() (context.Context, context.CancelFunc) { - ctx := context.WithValue(context.Background(), internal.KeyFactory, a.factory) - ctx = context.WithValue(ctx, internal.KeyConfig, a.config) - if a.config.Flags.ActiveNamespace != nil { - ctx = context.WithValue(ctx, internal.KeyNamespace, *a.config.Flags.ActiveNamespace) - } else { - ns, err := a.factory.Client().Config().CurrentNamespaceName() - if err != nil { - ns = client.AllNamespaces - } - ctx = context.WithValue(ctx, internal.KeyNamespace, ns) - } - - return context.WithCancel(ctx) -} diff --git a/internal/scrub/cache_core.go b/internal/scrub/cache_core.go deleted file mode 100644 index 441f0f06..00000000 --- a/internal/scrub/cache_core.go +++ /dev/null @@ -1,201 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package scrub - -import ( - "context" - "sync" - - "github.com/derailed/popeye/internal" - "github.com/derailed/popeye/internal/cache" - "github.com/derailed/popeye/internal/client" - "github.com/derailed/popeye/internal/dag" -) - -type core struct { - *dial - - mx sync.Mutex - namespace *cache.Namespace - cm *cache.ConfigMap - pod *cache.Pod - node *cache.Node - sa *cache.ServiceAccount - pv *cache.PersistentVolume - pvc *cache.PersistentVolumeClaim - sec *cache.Secret - svc *cache.Service - ep *cache.Endpoints -} - -func newCore(d *dial) *core { - return &core{dial: d} -} - -func (c *core) services() (*cache.Service, error) { - c.mx.Lock() - defer c.mx.Unlock() - - if c.svc != nil { - return c.svc, nil - } - ctx, cancel := c.context() - defer cancel() - ss, err := dag.ListServices(ctx) - c.svc = cache.NewService(ss) - - return c.svc, err -} - -func (c *core) endpoints() (*cache.Endpoints, error) { - c.mx.Lock() - defer c.mx.Unlock() - - if c.ep != nil { - return c.ep, nil - } - ctx, cancel := c.context() - defer cancel() - eps, err := dag.ListEndpoints(ctx) - c.ep = cache.NewEndpoints(eps) - - return c.ep, err -} - -func (c *core) secrets() (*cache.Secret, error) { - c.mx.Lock() - defer c.mx.Unlock() - - if c.sec != nil { - return c.sec, nil - } - ctx, cancel := c.context() - defer cancel() - secs, err := dag.ListSecrets(ctx) - c.sec = cache.NewSecret(secs) - - return c.sec, err -} - -func (c *core) persistentvolumes() (*cache.PersistentVolume, error) { - c.mx.Lock() - defer c.mx.Unlock() - - if c.pv != nil { - return c.pv, nil - } - ctx, cancel := c.context() - defer cancel() - pvs, err := dag.ListPersistentVolumes(ctx) - c.pv = cache.NewPersistentVolume(pvs) - - return c.pv, err -} - -func (c *core) persistentvolumeclaims() (*cache.PersistentVolumeClaim, error) { - c.mx.Lock() - defer c.mx.Unlock() - - if c.pvc != nil { - return c.pvc, nil - } - ctx, cancel := c.context() - defer cancel() - pvcs, err := dag.ListPersistentVolumeClaims(ctx) - c.pvc = cache.NewPersistentVolumeClaim(pvcs) - - return c.pvc, err -} - -func (c *core) configmaps() (*cache.ConfigMap, error) { - c.mx.Lock() - defer c.mx.Unlock() - - if c.cm != nil { - return c.cm, nil - } - ctx, cancel := c.context() - defer cancel() - cms, err := dag.ListConfigMaps(ctx) - c.cm = cache.NewConfigMap(cms) - - return c.cm, err -} - -func (c *core) namespaces() (*cache.Namespace, error) { - c.mx.Lock() - defer c.mx.Unlock() - - if c.namespace != nil { - return c.namespace, nil - } - ctx, cancel := c.context() - defer cancel() - nss, err := dag.ListNamespaces(ctx) - c.namespace = cache.NewNamespace(nss) - - return c.namespace, err -} - -func (c *core) nodes() (*cache.Node, error) { - c.mx.Lock() - defer c.mx.Unlock() - - if c.node != nil { - return c.node, nil - } - ctx, cancel := c.context() - defer cancel() - nodes, err := dag.ListNodes(ctx) - c.node = cache.NewNode(nodes) - - return c.node, err -} - -func (c *core) pods() (*cache.Pod, error) { - c.mx.Lock() - defer c.mx.Unlock() - - if c.pod != nil { - return c.pod, nil - } - ctx, cancel := c.context() - defer cancel() - pods, err := dag.ListPods(ctx) - c.pod = cache.NewPod(pods) - - return c.pod, err -} - -func (c *core) serviceaccounts() (*cache.ServiceAccount, error) { - c.mx.Lock() - defer c.mx.Unlock() - - if c.sa != nil { - return c.sa, nil - } - ctx, cancel := c.context() - defer cancel() - sas, err := dag.ListServiceAccounts(ctx) - c.sa = cache.NewServiceAccount(sas) - - return c.sa, err -} - -// Helpers... - -func (c *core) context() (context.Context, context.CancelFunc) { - ctx := context.WithValue(context.Background(), internal.KeyFactory, c.factory) - ctx = context.WithValue(ctx, internal.KeyConfig, c.config) - if c.config.Flags.ActiveNamespace != nil { - ctx = context.WithValue(ctx, internal.KeyNamespace, *c.config.Flags.ActiveNamespace) - } else { - ns, err := c.factory.Client().Config().CurrentNamespaceName() - if err != nil { - ns = client.AllNamespaces - } - ctx = context.WithValue(ctx, internal.KeyNamespace, ns) - } - return context.WithCancel(ctx) -} diff --git a/internal/scrub/cache_ext.go b/internal/scrub/cache_ext.go deleted file mode 100644 index 555965aa..00000000 --- a/internal/scrub/cache_ext.go +++ /dev/null @@ -1,90 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package scrub - -import ( - "context" - "sync" - - "github.com/derailed/popeye/internal" - "github.com/derailed/popeye/internal/cache" - "github.com/derailed/popeye/internal/client" - "github.com/derailed/popeye/internal/dag" -) - -type ext struct { - *dial - - mx sync.Mutex - pdb *cache.PodDisruptionBudget - ing *cache.Ingress - cl *cache.Cluster -} - -func newExt(d *dial) *ext { - return &ext{dial: d} -} - -func (e *ext) cluster() (*cache.Cluster, error) { - e.mx.Lock() - defer e.mx.Unlock() - - if e.cl != nil { - return e.cl, nil - } - ctx, cancel := e.context() - defer cancel() - major, minor, err := dag.ListVersion(ctx) - e.cl = cache.NewCluster(major, minor) - - return e.cl, err -} - -func (e *ext) ingresses() (*cache.Ingress, error) { - e.mx.Lock() - defer e.mx.Unlock() - - if e.ing != nil { - return e.ing, nil - } - ctx, cancel := e.context() - defer cancel() - ings, err := dag.ListIngresses(ctx) - e.ing = cache.NewIngress(ings) - - return e.ing, err -} - -func (e *ext) podDisruptionBudgets() (*cache.PodDisruptionBudget, error) { - e.mx.Lock() - defer e.mx.Unlock() - - if e.pdb != nil { - return e.pdb, nil - } - ctx, cancel := e.context() - defer cancel() - pdbs, err := dag.ListPodDisruptionBudgets(ctx) - e.pdb = cache.NewPodDisruptionBudget(pdbs) - - return e.pdb, err -} - -// Helpers... - -func (e *ext) context() (context.Context, context.CancelFunc) { - ctx := context.WithValue(context.Background(), internal.KeyFactory, e.factory) - ctx = context.WithValue(ctx, internal.KeyConfig, e.config) - if e.config.Flags.ActiveNamespace != nil { - ctx = context.WithValue(ctx, internal.KeyNamespace, *e.config.Flags.ActiveNamespace) - } else { - ns, err := e.factory.Client().Config().CurrentNamespaceName() - if err != nil { - ns = client.AllNamespaces - } - ctx = context.WithValue(ctx, internal.KeyNamespace, ns) - } - - return context.WithCancel(ctx) -} diff --git a/internal/scrub/cache_mx.go b/internal/scrub/cache_mx.go deleted file mode 100644 index 12deb458..00000000 --- a/internal/scrub/cache_mx.go +++ /dev/null @@ -1,49 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package scrub - -import ( - "sync" - - "github.com/derailed/popeye/internal/cache" - "github.com/derailed/popeye/internal/dag" -) - -type mx struct { - *dial - - mx sync.Mutex - nodeMx *cache.NodesMetrics - podMx *cache.PodsMetrics -} - -func newMX(d *dial) *mx { - return &mx{dial: d} -} - -func (m *mx) podsMx() (*cache.PodsMetrics, error) { - m.mx.Lock() - defer m.mx.Unlock() - - if m.podMx != nil { - return m.podMx, nil - } - pmx, err := dag.ListPodsMetrics(m.factory.Client()) - m.podMx = cache.NewPodsMetrics(pmx) - - return m.podMx, err -} - -func (m *mx) nodesMx() (*cache.NodesMetrics, error) { - m.mx.Lock() - defer m.mx.Unlock() - - if m.nodeMx != nil { - return m.nodeMx, nil - } - nmx, err := dag.ListNodesMetrics(m.factory.Client()) - m.nodeMx = cache.NewNodesMetrics(nmx) - - return m.nodeMx, err -} diff --git a/internal/scrub/cache_policy.go b/internal/scrub/cache_policy.go deleted file mode 100644 index 9e31964f..00000000 --- a/internal/scrub/cache_policy.go +++ /dev/null @@ -1,58 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package scrub - -import ( - "context" - "sync" - - "github.com/derailed/popeye/internal" - "github.com/derailed/popeye/internal/cache" - "github.com/derailed/popeye/internal/client" - "github.com/derailed/popeye/internal/dag" -) - -type policy struct { - *dial - - mx sync.Mutex - np *cache.NetworkPolicy -} - -func newPolicy(d *dial) *policy { - return &policy{dial: d} -} - -func (p *policy) networkpolicies() (*cache.NetworkPolicy, error) { - p.mx.Lock() - defer p.mx.Unlock() - - if p.np != nil { - return p.np, nil - } - ctx, cancel := p.context() - defer cancel() - nps, err := dag.ListNetworkPolicies(ctx) - p.np = cache.NewNetworkPolicy(nps) - - return p.np, err -} - -// Helpers... - -func (p *policy) context() (context.Context, context.CancelFunc) { - ctx := context.WithValue(context.Background(), internal.KeyFactory, p.factory) - ctx = context.WithValue(ctx, internal.KeyConfig, p.config) - if p.config.Flags.ActiveNamespace != nil { - ctx = context.WithValue(ctx, internal.KeyNamespace, *p.config.Flags.ActiveNamespace) - } else { - ns, err := p.factory.Client().Config().CurrentNamespaceName() - if err != nil { - ns = client.AllNamespaces - } - ctx = context.WithValue(ctx, internal.KeyNamespace, ns) - } - - return context.WithCancel(ctx) -} diff --git a/internal/scrub/cache_rbac.go b/internal/scrub/cache_rbac.go deleted file mode 100644 index 189de501..00000000 --- a/internal/scrub/cache_rbac.go +++ /dev/null @@ -1,106 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package scrub - -import ( - "context" - "sync" - - "github.com/derailed/popeye/internal" - "github.com/derailed/popeye/internal/cache" - "github.com/derailed/popeye/internal/client" - "github.com/derailed/popeye/internal/dag" -) - -type rbac struct { - *dial - - mx sync.Mutex - crb *cache.ClusterRoleBinding - cr *cache.ClusterRole - rb *cache.RoleBinding - ro *cache.Role -} - -func newRBAC(d *dial) *rbac { - return &rbac{dial: d} -} - -func (r *rbac) roles() (*cache.Role, error) { - r.mx.Lock() - defer r.mx.Unlock() - - if r.ro != nil { - return r.ro, nil - } - ctx, cancel := r.context() - defer cancel() - ros, err := dag.ListRoles(ctx) - r.ro = cache.NewRole(ros) - - return r.ro, err -} - -func (r *rbac) rolebindings() (*cache.RoleBinding, error) { - r.mx.Lock() - defer r.mx.Unlock() - - if r.rb != nil { - return r.rb, nil - } - ctx, cancel := r.context() - defer cancel() - rbs, err := dag.ListRoleBindings(ctx) - r.rb = cache.NewRoleBinding(rbs) - - return r.rb, err -} - -func (r *rbac) clusterroles() (*cache.ClusterRole, error) { - r.mx.Lock() - defer r.mx.Unlock() - - if r.cr != nil { - return r.cr, nil - } - ctx, cancel := r.context() - defer cancel() - crs, err := dag.ListClusterRoles(ctx) - r.cr = cache.NewClusterRole(crs) - - return r.cr, err -} - -func (r *rbac) clusterrolebindings() (*cache.ClusterRoleBinding, error) { - r.mx.Lock() - defer r.mx.Unlock() - - if r.crb != nil { - return r.crb, nil - } - ctx, cancel := r.context() - defer cancel() - crbs, err := dag.ListClusterRoleBindings(ctx) - r.crb = cache.NewClusterRoleBinding(crbs) - - return r.crb, err -} - -// Helpers... - -func (r *rbac) context() (context.Context, context.CancelFunc) { - ctx := context.WithValue(context.Background(), internal.KeyFactory, r.factory) - ctx = context.WithValue(ctx, internal.KeyConfig, r.config) - if r.config.Flags.ActiveNamespace != nil { - ctx = context.WithValue(ctx, internal.KeyNamespace, *r.config.Flags.ActiveNamespace) - } else { - ns, err := r.factory.Client().Config().CurrentNamespaceName() - if err != nil { - ns = client.AllNamespaces - } - ctx = context.WithValue(ctx, internal.KeyNamespace, ns) - } - - return context.WithCancel(ctx) -} diff --git a/internal/scrub/cjob.go b/internal/scrub/cjob.go new file mode 100644 index 00000000..453d957c --- /dev/null +++ b/internal/scrub/cjob.go @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package scrub + +import ( + "context" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/issues" + "github.com/derailed/popeye/internal/lint" + batchv1 "k8s.io/api/batch/v1" + v1 "k8s.io/api/core/v1" + mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" +) + +// CronJob represents a CronJob scruber. +type CronJob struct { + *issues.Collector + *Cache +} + +// NewCronJob return a new instance. +func NewCronJob(ctx context.Context, c *Cache, codes *issues.Codes) Linter { + return &CronJob{ + Collector: issues.NewCollector(codes, c.Config), + Cache: c, + } +} + +func (s *CronJob) Preloads() Preloads { + return Preloads{ + internal.CJOB: db.LoadResource[*batchv1.CronJob], + internal.JOB: db.LoadResource[*batchv1.Job], + internal.PO: db.LoadResource[*v1.Pod], + internal.SA: db.LoadResource[*v1.ServiceAccount], + internal.PMX: db.LoadResource[*mv1beta1.PodMetrics], + } +} + +// Lint all available CronJobs. +func (s *CronJob) Lint(ctx context.Context) error { + for k, f := range s.Preloads() { + if err := f(ctx, s.Loader, internal.Glossary[k]); err != nil { + return err + } + } + + return lint.NewCronJob(s.Collector, s.DB).Lint(ctx) +} diff --git a/internal/scrub/cluster.go b/internal/scrub/cluster.go index 1f1efd54..0fb80884 100644 --- a/internal/scrub/cluster.go +++ b/internal/scrub/cluster.go @@ -8,7 +8,7 @@ import ( "github.com/derailed/popeye/internal/cache" "github.com/derailed/popeye/internal/issues" - "github.com/derailed/popeye/internal/sanitize" + "github.com/derailed/popeye/internal/lint" "github.com/derailed/popeye/pkg/config" "github.com/derailed/popeye/types" ) @@ -22,16 +22,16 @@ type Cluster struct { client types.Connection } -// NewCluster return a new Cluster scruber. -func NewCluster(ctx context.Context, c *Cache, codes *issues.Codes) Sanitizer { +// NewCluster returns a new instance. +func NewCluster(ctx context.Context, c *Cache, codes *issues.Codes) Linter { cl := Cluster{ client: c.factory.Client(), - Config: c.config, - Collector: issues.NewCollector(codes, c.config), + Config: c.Config, + Collector: issues.NewCollector(codes, c.Config), } var err error - cl.Cluster, err = c.cluster() + cl.Cluster, err = c.cluster(ctx) if err != nil { cl.AddErr(ctx, err) } @@ -39,9 +39,13 @@ func NewCluster(ctx context.Context, c *Cache, codes *issues.Codes) Sanitizer { return &cl } -// Sanitize all available Clusters. -func (d *Cluster) Sanitize(ctx context.Context) error { - return sanitize.NewCluster(d.Collector, d).Sanitize(ctx) +func (d *Cluster) Preloads() Preloads { + return nil +} + +// Lint all available Clusters. +func (d *Cluster) Lint(ctx context.Context) error { + return lint.NewCluster(d.Collector, d).Lint(ctx) } func (d *Cluster) HasMetrics() bool { diff --git a/internal/scrub/cm.go b/internal/scrub/cm.go index fa9d7d1a..fca6edec 100644 --- a/internal/scrub/cm.go +++ b/internal/scrub/cm.go @@ -6,37 +6,41 @@ package scrub import ( "context" - "github.com/derailed/popeye/internal/cache" + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" "github.com/derailed/popeye/internal/issues" - "github.com/derailed/popeye/internal/sanitize" + "github.com/derailed/popeye/internal/lint" + v1 "k8s.io/api/core/v1" ) // ConfigMap represents a configMap scruber. type ConfigMap struct { *issues.Collector - *cache.Pod - *cache.ConfigMap + *Cache } -// NewConfigMap return a new ConfigMap scruber. -func NewConfigMap(ctx context.Context, c *Cache, codes *issues.Codes) Sanitizer { - s := ConfigMap{Collector: issues.NewCollector(codes, c.config)} - - var err error - s.ConfigMap, err = c.configmaps() - if err != nil { - s.AddErr(ctx, err) +// NewConfigMap returns a new instance. +func NewConfigMap(ctx context.Context, c *Cache, codes *issues.Codes) Linter { + return &ConfigMap{ + Collector: issues.NewCollector(codes, c.Config), + Cache: c, } +} - s.Pod, err = c.pods() - if err != nil { - s.AddErr(ctx, err) +func (s *ConfigMap) Preloads() Preloads { + return Preloads{ + internal.CM: db.LoadResource[*v1.ConfigMap], + internal.PO: db.LoadResource[*v1.Pod], } - - return &s } -// Sanitize all available ConfigMaps. -func (c *ConfigMap) Sanitize(ctx context.Context) error { - return sanitize.NewConfigMap(c.Collector, c).Sanitize(ctx) +// Lint all available ConfigMaps. +func (s *ConfigMap) Lint(ctx context.Context) error { + for k, f := range s.Preloads() { + if err := f(ctx, s.Loader, internal.Glossary[k]); err != nil { + return err + } + } + + return lint.NewConfigMap(s.Collector, s.DB).Lint(ctx) } diff --git a/internal/scrub/cr.go b/internal/scrub/cr.go index e0ed941d..7b2de24e 100644 --- a/internal/scrub/cr.go +++ b/internal/scrub/cr.go @@ -6,52 +6,44 @@ package scrub import ( "context" - "github.com/derailed/popeye/internal/cache" + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" "github.com/derailed/popeye/internal/issues" - "github.com/derailed/popeye/internal/sanitize" - "github.com/derailed/popeye/pkg/config" - "github.com/derailed/popeye/types" + "github.com/derailed/popeye/internal/lint" + v1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" ) // ClusterRole represents a ClusterRole scruber. type ClusterRole struct { - client types.Connection - *config.Config *issues.Collector - - *cache.ClusterRole - *cache.ClusterRoleBinding - *cache.RoleBinding + *Cache } -// NewClusterRole return a new ClusterRole scruber. -func NewClusterRole(ctx context.Context, c *Cache, codes *issues.Codes) Sanitizer { - cr := ClusterRole{ - client: c.factory.Client(), - Config: c.config, - Collector: issues.NewCollector(codes, c.config), - } - - var err error - cr.ClusterRole, err = c.clusterroles() - if err != nil { - cr.AddErr(ctx, err) +// NewClusterRole returns a new instance. +func NewClusterRole(ctx context.Context, c *Cache, codes *issues.Codes) Linter { + return &ClusterRole{ + Collector: issues.NewCollector(codes, c.Config), + Cache: c, } +} - cr.ClusterRoleBinding, err = c.clusterrolebindings() - if err != nil { - cr.AddErr(ctx, err) +func (s *ClusterRole) Preloads() Preloads { + return Preloads{ + internal.CR: db.LoadResource[*rbacv1.ClusterRole], + internal.CRB: db.LoadResource[*rbacv1.ClusterRoleBinding], + internal.RO: db.LoadResource[*rbacv1.Role], + internal.SA: db.LoadResource[*v1.ServiceAccount], } +} - cr.RoleBinding, err = c.rolebindings() - if err != nil { - cr.AddErr(ctx, err) +// Lint all available ClusterRoles. +func (s *ClusterRole) Lint(ctx context.Context) error { + for k, f := range s.Preloads() { + if err := f(ctx, s.Loader, internal.Glossary[k]); err != nil { + return err + } } - return &cr -} - -// Sanitize all available ClusterRoles. -func (c *ClusterRole) Sanitize(ctx context.Context) error { - return sanitize.NewClusterRole(c.Collector, c).Sanitize(ctx) + return lint.NewClusterRole(s.Collector, s.DB).Lint(ctx) } diff --git a/internal/scrub/crb.go b/internal/scrub/crb.go index 7f0d8ce2..c0453463 100644 --- a/internal/scrub/crb.go +++ b/internal/scrub/crb.go @@ -6,52 +6,44 @@ package scrub import ( "context" - "github.com/derailed/popeye/internal/cache" + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" "github.com/derailed/popeye/internal/issues" - "github.com/derailed/popeye/internal/sanitize" - "github.com/derailed/popeye/pkg/config" - "github.com/derailed/popeye/types" + "github.com/derailed/popeye/internal/lint" + v1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" ) // ClusterRoleBinding represents a ClusterRoleBinding scruber. type ClusterRoleBinding struct { - client types.Connection - *config.Config *issues.Collector - - *cache.ClusterRoleBinding - *cache.ClusterRole - *cache.Role + *Cache } -// NewClusterRoleBinding return a new ClusterRoleBinding scruber. -func NewClusterRoleBinding(ctx context.Context, c *Cache, codes *issues.Codes) Sanitizer { - crb := ClusterRoleBinding{ - client: c.factory.Client(), - Config: c.config, - Collector: issues.NewCollector(codes, c.config), - } - - var err error - crb.ClusterRoleBinding, err = c.clusterrolebindings() - if err != nil { - crb.AddErr(ctx, err) +// NewClusterRoleBinding returns a new instance. +func NewClusterRoleBinding(ctx context.Context, c *Cache, codes *issues.Codes) Linter { + return &ClusterRoleBinding{ + Collector: issues.NewCollector(codes, c.Config), + Cache: c, } +} - crb.ClusterRole, err = c.clusterroles() - if err != nil { - crb.AddErr(ctx, err) +func (s *ClusterRoleBinding) Preloads() Preloads { + return Preloads{ + internal.CRB: db.LoadResource[*rbacv1.ClusterRoleBinding], + internal.CR: db.LoadResource[*rbacv1.ClusterRole], + internal.RO: db.LoadResource[*rbacv1.Role], + internal.SA: db.LoadResource[*v1.ServiceAccount], } +} - crb.Role, err = c.roles() - if err != nil { - crb.AddErr(ctx, err) +// Lint all available ClusterRoleBindings. +func (s *ClusterRoleBinding) Lint(ctx context.Context) error { + for k, f := range s.Preloads() { + if err := f(ctx, s.Loader, internal.Glossary[k]); err != nil { + return err + } } - return &crb -} - -// Sanitize all available ClusterRoleBindings. -func (c *ClusterRoleBinding) Sanitize(ctx context.Context) error { - return sanitize.NewClusterRoleBinding(c.Collector, c).Sanitize(ctx) + return lint.NewClusterRoleBinding(s.Collector, s.DB).Lint(ctx) } diff --git a/internal/scrub/dp.go b/internal/scrub/dp.go index 3c0955f1..f31d2094 100644 --- a/internal/scrub/dp.go +++ b/internal/scrub/dp.go @@ -6,55 +6,45 @@ package scrub import ( "context" - "github.com/derailed/popeye/internal/cache" + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" "github.com/derailed/popeye/internal/issues" - "github.com/derailed/popeye/internal/sanitize" - "github.com/derailed/popeye/pkg/config" - "github.com/derailed/popeye/types" + "github.com/derailed/popeye/internal/lint" + appsv1 "k8s.io/api/apps/v1" + v1 "k8s.io/api/core/v1" + mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" ) // Deployment represents a Deployment scruber. type Deployment struct { *issues.Collector - *cache.Deployment - *cache.PodsMetrics - *cache.Pod - *cache.ServiceAccount - *config.Config - - client types.Connection + *Cache } -// NewDeployment return a new Deployment scruber. -func NewDeployment(ctx context.Context, c *Cache, codes *issues.Codes) Sanitizer { - d := Deployment{ - client: c.factory.Client(), - Config: c.config, - Collector: issues.NewCollector(codes, c.config), - } - - var err error - d.Deployment, err = c.deployments() - if err != nil { - d.AddErr(ctx, err) +// NewDeployment returns a new instance. +func NewDeployment(ctx context.Context, c *Cache, codes *issues.Codes) Linter { + return &Deployment{ + Collector: issues.NewCollector(codes, c.Config), + Cache: c, } +} - d.PodsMetrics, _ = c.podsMx() - - d.Pod, err = c.pods() - if err != nil { - d.AddErr(ctx, err) +func (s *Deployment) Preloads() Preloads { + return Preloads{ + internal.DP: db.LoadResource[*appsv1.Deployment], + internal.PO: db.LoadResource[*v1.Pod], + internal.SA: db.LoadResource[*v1.ServiceAccount], + internal.PMX: db.LoadResource[*mv1beta1.PodMetrics], } +} - d.ServiceAccount, err = c.serviceaccounts() - if err != nil { - d.AddErr(ctx, err) +// Lint all available Deployments. +func (s *Deployment) Lint(ctx context.Context) error { + for k, f := range s.Preloads() { + if err := f(ctx, s.Loader, internal.Glossary[k]); err != nil { + return err + } } - return &d -} - -// Sanitize all available Deployments. -func (d *Deployment) Sanitize(ctx context.Context) error { - return sanitize.NewDeployment(d.Collector, d).Sanitize(ctx) + return lint.NewDeployment(s.Collector, s.DB).Lint(ctx) } diff --git a/internal/scrub/ds.go b/internal/scrub/ds.go index 05689154..1237ee26 100644 --- a/internal/scrub/ds.go +++ b/internal/scrub/ds.go @@ -6,54 +6,46 @@ package scrub import ( "context" - "github.com/derailed/popeye/internal/cache" + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" "github.com/derailed/popeye/internal/issues" - "github.com/derailed/popeye/internal/sanitize" - "github.com/derailed/popeye/pkg/config" - "github.com/derailed/popeye/types" + "github.com/derailed/popeye/internal/lint" + appsv1 "k8s.io/api/apps/v1" + v1 "k8s.io/api/core/v1" + mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" ) // DaemonSet represents a DaemonSet scruber. type DaemonSet struct { *issues.Collector - *cache.DaemonSet - *cache.PodsMetrics - *cache.Pod - *cache.ServiceAccount - *config.Config - - client types.Connection + *Cache } -// NewDaemonSet return a new DaemonSet scruber. -func NewDaemonSet(ctx context.Context, c *Cache, codes *issues.Codes) Sanitizer { - d := DaemonSet{ - client: c.factory.Client(), - Config: c.config, - Collector: issues.NewCollector(codes, c.config), +// NewDaemonSet return a new instance. +func NewDaemonSet(ctx context.Context, c *Cache, codes *issues.Codes) Linter { + return &DaemonSet{ + Collector: issues.NewCollector(codes, c.Config), + Cache: c, } - var err error - d.DaemonSet, err = c.daemonSets() - if err != nil { - d.AddErr(ctx, err) - } +} - d.Pod, err = c.pods() - if err != nil { - d.AddErr(ctx, err) +func (s *DaemonSet) Preloads() Preloads { + return Preloads{ + internal.DS: db.LoadResource[*appsv1.DaemonSet], + internal.PO: db.LoadResource[*v1.Pod], + internal.SA: db.LoadResource[*v1.ServiceAccount], + internal.PMX: db.LoadResource[*mv1beta1.PodMetrics], } - d.PodsMetrics, _ = c.podsMx() +} - d.ServiceAccount, err = c.serviceaccounts() - if err != nil { - d.AddErr(ctx, err) +// Lint all available DaemonSets. +func (s *DaemonSet) Lint(ctx context.Context) error { + for k, f := range s.Preloads() { + if err := f(ctx, s.Loader, internal.Glossary[k]); err != nil { + return err + } } - return &d -} - -// Sanitize all available DaemonSets. -func (d *DaemonSet) Sanitize(ctx context.Context) error { - return sanitize.NewDaemonSet(d.Collector, d).Sanitize(ctx) + return lint.NewDaemonSet(s.Collector, s.DB).Lint(ctx) } diff --git a/internal/scrub/gw-route.go b/internal/scrub/gw-route.go new file mode 100644 index 00000000..b2d82cb6 --- /dev/null +++ b/internal/scrub/gw-route.go @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package scrub + +import ( + "context" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/issues" + "github.com/derailed/popeye/internal/lint" + gwv1 "sigs.k8s.io/gateway-api/apis/v1" +) + +// HTTPRoute represents a HTTPRoute scruber. +type HTTPRoute struct { + *issues.Collector + *Cache +} + +// NewHTTPRoute return a new instance. +func NewHTTPRoute(ctx context.Context, c *Cache, codes *issues.Codes) Linter { + return &HTTPRoute{ + Collector: issues.NewCollector(codes, c.Config), + Cache: c, + } +} + +func (s *HTTPRoute) Preloads() Preloads { + return Preloads{ + internal.GW: db.LoadResource[*gwv1.Gateway], + internal.GWC: db.LoadResource[*gwv1.GatewayClass], + internal.GWR: db.LoadResource[*gwv1.HTTPRoute], + } +} + +// Lint all available HTTPRoute. +func (s *HTTPRoute) Lint(ctx context.Context) error { + for k, f := range s.Preloads() { + if err := f(ctx, s.Loader, internal.Glossary[k]); err != nil { + return err + } + } + + return lint.NewHTTPRoute(s.Collector, s.DB).Lint(ctx) +} diff --git a/internal/scrub/gw.go b/internal/scrub/gw.go new file mode 100644 index 00000000..12882cab --- /dev/null +++ b/internal/scrub/gw.go @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package scrub + +import ( + "context" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/issues" + "github.com/derailed/popeye/internal/lint" + gwv1 "sigs.k8s.io/gateway-api/apis/v1" +) + +// Gateway represents a Gateway scruber. +type Gateway struct { + *issues.Collector + *Cache +} + +// NewGateway return a new instance. +func NewGateway(ctx context.Context, c *Cache, codes *issues.Codes) Linter { + return &Gateway{ + Collector: issues.NewCollector(codes, c.Config), + Cache: c, + } +} + +func (s *Gateway) Preloads() Preloads { + return Preloads{ + internal.GW: db.LoadResource[*gwv1.Gateway], + internal.GWC: db.LoadResource[*gwv1.GatewayClass], + } +} + +// Lint all available Gateway. +func (s *Gateway) Lint(ctx context.Context) error { + for k, f := range s.Preloads() { + if err := f(ctx, s.Loader, internal.Glossary[k]); err != nil { + return err + } + } + + return lint.NewGateway(s.Collector, s.DB).Lint(ctx) +} diff --git a/internal/scrub/gwc.go b/internal/scrub/gwc.go new file mode 100644 index 00000000..f1773695 --- /dev/null +++ b/internal/scrub/gwc.go @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package scrub + +import ( + "context" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/issues" + "github.com/derailed/popeye/internal/lint" + gwv1 "sigs.k8s.io/gateway-api/apis/v1" +) + +// GatewayClass represents a GatewayClass scruber. +type GatewayClass struct { + *issues.Collector + *Cache +} + +// NewGatewayClass return a new instance. +func NewGatewayClass(ctx context.Context, c *Cache, codes *issues.Codes) Linter { + return &GatewayClass{ + Collector: issues.NewCollector(codes, c.Config), + Cache: c, + } +} + +func (s *GatewayClass) Preloads() Preloads { + return Preloads{ + internal.GW: db.LoadResource[*gwv1.Gateway], + internal.GWC: db.LoadResource[*gwv1.GatewayClass], + } +} + +// Lint all available GatewayClass. +func (s *GatewayClass) Lint(ctx context.Context) error { + for k, f := range s.Preloads() { + if err := f(ctx, s.Loader, internal.Glossary[k]); err != nil { + return err + } + } + + return lint.NewGatewayClass(s.Collector, s.DB).Lint(ctx) +} diff --git a/internal/scrub/hpa.go b/internal/scrub/hpa.go index de00d591..f37242be 100644 --- a/internal/scrub/hpa.go +++ b/internal/scrub/hpa.go @@ -7,89 +7,50 @@ import ( "context" "github.com/derailed/popeye/internal" - "github.com/derailed/popeye/internal/cache" - "github.com/derailed/popeye/internal/client" - "github.com/derailed/popeye/internal/dag" + "github.com/derailed/popeye/internal/db" "github.com/derailed/popeye/internal/issues" - "github.com/derailed/popeye/internal/sanitize" - "github.com/derailed/popeye/pkg/config" + "github.com/derailed/popeye/internal/lint" + appsv1 "k8s.io/api/apps/v1" + autoscalingv1 "k8s.io/api/autoscaling/v1" + v1 "k8s.io/api/core/v1" + mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" ) // HorizontalPodAutoscaler represents a HorizontalPodAutoscaler scruber. type HorizontalPodAutoscaler struct { *issues.Collector - *cache.HorizontalPodAutoscaler - *cache.Pod - *cache.Node - *cache.PodsMetrics - *cache.NodesMetrics - *cache.Deployment - *cache.StatefulSet - *cache.ServiceAccount - *config.Config + *Cache } -// NewHorizontalPodAutoscaler return a new HorizontalPodAutoscaler scruber. -func NewHorizontalPodAutoscaler(ctx context.Context, c *Cache, codes *issues.Codes) Sanitizer { - h := HorizontalPodAutoscaler{ - Collector: issues.NewCollector(codes, c.config), - Config: c.config, +// NewHorizontalPodAutoscaler returns a new instance. +func NewHorizontalPodAutoscaler(ctx context.Context, c *Cache, codes *issues.Codes) Linter { + return &HorizontalPodAutoscaler{ + Collector: issues.NewCollector(codes, c.Config), + Cache: c, } +} - ctx = context.WithValue(ctx, internal.KeyFactory, c.factory) - ctx = context.WithValue(ctx, internal.KeyConfig, c.config) - ctx = context.WithValue(ctx, internal.KeyConfig, c.config) - if c.config.Flags.ActiveNamespace != nil { - ctx = context.WithValue(ctx, internal.KeyNamespace, *c.config.Flags.ActiveNamespace) - } else { - ns, err := c.factory.Client().Config().CurrentNamespaceName() - if err != nil { - ns = client.AllNamespaces - } - ctx = context.WithValue(ctx, internal.KeyNamespace, ns) - } - - ctx, cancel := context.WithCancel(ctx) - defer cancel() - var err error - ss, err := dag.ListHorizontalPodAutoscalers(ctx) - if err != nil { - h.AddErr(ctx, err) - } - h.HorizontalPodAutoscaler = cache.NewHorizontalPodAutoscaler(ss) - - h.Deployment, err = c.deployments() - if err != nil { - h.AddErr(ctx, err) - } - - h.StatefulSet, err = c.statefulsets() - if err != nil { - h.AddErr(ctx, err) - } - - h.Node, err = c.nodes() - if err != nil { - h.AddErr(ctx, err) - } - - h.NodesMetrics, _ = c.nodesMx() - - h.Pod, err = c.pods() - if err != nil { - h.AddErr(ctx, err) +func (s *HorizontalPodAutoscaler) Preloads() Preloads { + return Preloads{ + internal.HPA: db.LoadResource[*autoscalingv1.HorizontalPodAutoscaler], + internal.DP: db.LoadResource[*appsv1.Deployment], + internal.STS: db.LoadResource[*appsv1.StatefulSet], + internal.RS: db.LoadResource[*appsv1.ReplicaSet], + internal.NO: db.LoadResource[*v1.Node], + internal.PO: db.LoadResource[*v1.Pod], + internal.SA: db.LoadResource[*v1.ServiceAccount], + internal.PMX: db.LoadResource[*mv1beta1.PodMetrics], + internal.NMX: db.LoadResource[*mv1beta1.NodeMetrics], } - h.PodsMetrics, _ = c.podsMx() +} - h.ServiceAccount, err = c.serviceaccounts() - if err != nil { - h.AddErr(ctx, err) +// Lint all available HorizontalPodAutoscalers. +func (s *HorizontalPodAutoscaler) Lint(ctx context.Context) error { + for k, f := range s.Preloads() { + if err := f(ctx, s.Loader, internal.Glossary[k]); err != nil { + return err + } } - return &h -} - -// Sanitize all available HorizontalPodAutoscalers. -func (h *HorizontalPodAutoscaler) Sanitize(ctx context.Context) error { - return sanitize.NewHorizontalPodAutoscaler(h.Collector, h).Sanitize(ctx) + return lint.NewHorizontalPodAutoscaler(s.Collector, s.DB).Lint(ctx) } diff --git a/internal/scrub/ing.go b/internal/scrub/ing.go index 10ea2f08..599b0d46 100644 --- a/internal/scrub/ing.go +++ b/internal/scrub/ing.go @@ -6,40 +6,42 @@ package scrub import ( "context" - "github.com/derailed/popeye/internal/cache" + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" "github.com/derailed/popeye/internal/issues" - "github.com/derailed/popeye/internal/sanitize" - "github.com/derailed/popeye/pkg/config" - "github.com/derailed/popeye/types" + "github.com/derailed/popeye/internal/lint" + v1 "k8s.io/api/core/v1" + netv1 "k8s.io/api/networking/v1" ) // Ingress represents a Ingress scruber. type Ingress struct { *issues.Collector - *cache.Ingress - *config.Config - - client types.Connection + *Cache } -// NewIngress return a new Ingress scruber. -func NewIngress(ctx context.Context, c *Cache, codes *issues.Codes) Sanitizer { - d := Ingress{ - client: c.factory.Client(), - Config: c.config, - Collector: issues.NewCollector(codes, c.config), +// NewIngress return a new instance. +func NewIngress(ctx context.Context, c *Cache, codes *issues.Codes) Linter { + return &Ingress{ + Collector: issues.NewCollector(codes, c.Config), + Cache: c, } +} - var err error - d.Ingress, err = c.ingresses() - if err != nil { - d.AddErr(ctx, err) +func (s *Ingress) Preloads() Preloads { + return Preloads{ + internal.ING: db.LoadResource[*netv1.Ingress], + internal.SVC: db.LoadResource[*v1.Service], } - - return &d } -// Sanitize all available Ingresss. -func (i *Ingress) Sanitize(ctx context.Context) error { - return sanitize.NewIngress(i.Collector, i).Sanitize(ctx) +// Lint all available Ingress. +func (s *Ingress) Lint(ctx context.Context) error { + for k, f := range s.Preloads() { + if err := f(ctx, s.Loader, internal.Glossary[k]); err != nil { + return err + } + } + + return lint.NewIngress(s.Collector, s.DB).Lint(ctx) } diff --git a/internal/scrub/job.go b/internal/scrub/job.go new file mode 100644 index 00000000..880b9937 --- /dev/null +++ b/internal/scrub/job.go @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package scrub + +import ( + "context" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/issues" + "github.com/derailed/popeye/internal/lint" + batchv1 "k8s.io/api/batch/v1" + v1 "k8s.io/api/core/v1" + mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" +) + +// Job represents a Job scruber. +type Job struct { + *issues.Collector + *Cache +} + +// NewJob return a new instance. +func NewJob(ctx context.Context, c *Cache, codes *issues.Codes) Linter { + return &Job{ + Collector: issues.NewCollector(codes, c.Config), + Cache: c, + } +} + +func (s *Job) Preloads() Preloads { + return Preloads{ + internal.CJOB: db.LoadResource[*batchv1.CronJob], + internal.JOB: db.LoadResource[*batchv1.Job], + internal.PO: db.LoadResource[*v1.Pod], + internal.SA: db.LoadResource[*v1.ServiceAccount], + internal.PMX: db.LoadResource[*mv1beta1.PodMetrics], + } +} + +// Lint all available Jobs. +func (s *Job) Lint(ctx context.Context) error { + for k, f := range s.Preloads() { + if err := f(ctx, s.Loader, internal.Glossary[k]); err != nil { + return err + } + } + + return lint.NewJob(s.Collector, s.DB).Lint(ctx) +} diff --git a/internal/scrub/no.go b/internal/scrub/no.go index e790c967..9bd9d8c6 100644 --- a/internal/scrub/no.go +++ b/internal/scrub/no.go @@ -6,45 +6,43 @@ package scrub import ( "context" - "github.com/derailed/popeye/internal/cache" + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" "github.com/derailed/popeye/internal/issues" - "github.com/derailed/popeye/internal/sanitize" - "github.com/derailed/popeye/pkg/config" + "github.com/derailed/popeye/internal/lint" + v1 "k8s.io/api/core/v1" + mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" ) // Node represents a Node scruber. type Node struct { *issues.Collector - *cache.Node - *cache.Pod - *cache.NodesMetrics - *config.Config + *Cache } -// NewNode return a new Node scruber. -func NewNode(ctx context.Context, c *Cache, codes *issues.Codes) Sanitizer { - n := Node{ - Collector: issues.NewCollector(codes, c.config), - Config: c.config, +// NewNode return a new instance. +func NewNode(ctx context.Context, c *Cache, codes *issues.Codes) Linter { + return &Node{ + Collector: issues.NewCollector(codes, c.Config), + Cache: c, } +} - var err error - n.Node, err = c.nodes() - if err != nil { - n.AddErr(ctx, err) +func (s *Node) Preloads() Preloads { + return Preloads{ + internal.NO: db.LoadResource[*v1.Node], + internal.PO: db.LoadResource[*v1.Pod], + internal.NMX: db.LoadResource[*mv1beta1.NodeMetrics], } +} - n.Pod, err = c.pods() - if err != nil { - n.AddErr(ctx, err) +// Lint all available Nodes. +func (s *Node) Lint(ctx context.Context) error { + for k, f := range s.Preloads() { + if err := f(ctx, s.Loader, internal.Glossary[k]); err != nil { + return err + } } - n.NodesMetrics, _ = c.nodesMx() - - return &n -} - -// Sanitize all available Nodes. -func (n *Node) Sanitize(ctx context.Context) error { - return sanitize.NewNode(n.Collector, n).Sanitize(ctx) + return lint.NewNode(s.Collector, s.DB).Lint(ctx) } diff --git a/internal/scrub/np.go b/internal/scrub/np.go index cf141676..53191a8d 100644 --- a/internal/scrub/np.go +++ b/internal/scrub/np.go @@ -6,52 +6,43 @@ package scrub import ( "context" - "github.com/derailed/popeye/internal/cache" + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" "github.com/derailed/popeye/internal/issues" - "github.com/derailed/popeye/internal/sanitize" - "github.com/derailed/popeye/pkg/config" - "github.com/derailed/popeye/types" + "github.com/derailed/popeye/internal/lint" + v1 "k8s.io/api/core/v1" + netv1 "k8s.io/api/networking/v1" ) // NetworkPolicy represents a NetworkPolicy scruber. type NetworkPolicy struct { *issues.Collector - *cache.NetworkPolicy - *cache.Namespace - *cache.Pod - *config.Config - - client types.Connection + *Cache } -// NewNetworkPolicy return a new NetworkPolicy scruber. -func NewNetworkPolicy(ctx context.Context, c *Cache, codes *issues.Codes) Sanitizer { - n := NetworkPolicy{ - client: c.factory.Client(), - Config: c.config, - Collector: issues.NewCollector(codes, c.config), - } - - var err error - n.NetworkPolicy, err = c.networkpolicies() - if err != nil { - n.AddErr(ctx, err) +// NewNetworkPolicy return a new instance. +func NewNetworkPolicy(ctx context.Context, c *Cache, codes *issues.Codes) Linter { + return &NetworkPolicy{ + Collector: issues.NewCollector(codes, c.Config), + Cache: c, } +} - n.Namespace, err = c.namespaces() - if err != nil { - n.AddErr(ctx, err) +func (s *NetworkPolicy) Preloads() Preloads { + return Preloads{ + internal.NP: db.LoadResource[*netv1.NetworkPolicy], + internal.NS: db.LoadResource[*v1.Namespace], + internal.PO: db.LoadResource[*v1.Pod], } +} - n.Pod, err = c.pods() - if err != nil { - n.AddErr(ctx, err) +// Lint all available NetworkPolicies. +func (s *NetworkPolicy) Lint(ctx context.Context) error { + for k, f := range s.Preloads() { + if err := f(ctx, s.Loader, internal.Glossary[k]); err != nil { + return err + } } - return &n -} - -// Sanitize all available NetworkPolicys. -func (n *NetworkPolicy) Sanitize(ctx context.Context) error { - return sanitize.NewNetworkPolicy(n.Collector, n).Sanitize(ctx) + return lint.NewNetworkPolicy(s.Collector, s.DB).Lint(ctx) } diff --git a/internal/scrub/ns.go b/internal/scrub/ns.go index d5cbbac1..60a7650f 100644 --- a/internal/scrub/ns.go +++ b/internal/scrub/ns.go @@ -5,51 +5,43 @@ package scrub import ( "context" - "sync" "github.com/derailed/popeye/internal" - "github.com/derailed/popeye/internal/cache" + "github.com/derailed/popeye/internal/db" "github.com/derailed/popeye/internal/issues" - "github.com/derailed/popeye/internal/sanitize" + "github.com/derailed/popeye/internal/lint" + v1 "k8s.io/api/core/v1" ) // Namespace represents a Namespace scruber. type Namespace struct { *issues.Collector - *cache.Namespace - *cache.Pod + *Cache } -// NewNamespace return a new Namespace scruber. -func NewNamespace(ctx context.Context, c *Cache, codes *issues.Codes) Sanitizer { - n := Namespace{Collector: issues.NewCollector(codes, c.config)} - - var err error - n.Namespace, err = c.namespaces() - if err != nil { - n.AddErr(ctx, err) +// NewNamespace returns a new instance. +func NewNamespace(ctx context.Context, c *Cache, codes *issues.Codes) Linter { + return &Namespace{ + Collector: issues.NewCollector(codes, c.Config), + Cache: c, } +} - n.Pod, err = c.pods() - if err != nil { - n.AddErr(ctx, err) +func (s *Namespace) Preloads() Preloads { + return Preloads{ + internal.NS: db.LoadResource[*v1.Namespace], + internal.PO: db.LoadResource[*v1.Pod], + internal.SA: db.LoadResource[*v1.ServiceAccount], } - - return &n } -// ReferencedNamespaces fetch all namespaces referenced by pods. -func (n *Namespace) ReferencedNamespaces(res map[string]struct{}) { - var refs sync.Map - n.Pod.PodRefs(&refs) - if ss, ok := refs.Load("ns"); ok { - for ns := range ss.(internal.StringSet) { - res[ns] = struct{}{} +// Lint all available Namespaces. +func (s *Namespace) Lint(ctx context.Context) error { + for k, f := range s.Preloads() { + if err := f(ctx, s.Loader, internal.Glossary[k]); err != nil { + return err } } -} -// Sanitize all available Namespaces. -func (n *Namespace) Sanitize(ctx context.Context) error { - return sanitize.NewNamespace(n.Collector, n).Sanitize(ctx) + return lint.NewNamespace(s.Collector, s.DB).Lint(ctx) } diff --git a/internal/scrub/pdb.go b/internal/scrub/pdb.go index 9525571c..c94a3835 100644 --- a/internal/scrub/pdb.go +++ b/internal/scrub/pdb.go @@ -6,37 +6,42 @@ package scrub import ( "context" - "github.com/derailed/popeye/internal/cache" + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" "github.com/derailed/popeye/internal/issues" - "github.com/derailed/popeye/internal/sanitize" + "github.com/derailed/popeye/internal/lint" + v1 "k8s.io/api/core/v1" + polv1 "k8s.io/api/policy/v1" ) // PodDisruptionBudget represents a pdb scruber. type PodDisruptionBudget struct { *issues.Collector - *cache.Pod - *cache.PodDisruptionBudget + *Cache } // NewPodDisruptionBudget return a new PodDisruptionBudget scruber. -func NewPodDisruptionBudget(ctx context.Context, c *Cache, codes *issues.Codes) Sanitizer { - s := PodDisruptionBudget{Collector: issues.NewCollector(codes, c.config)} - - var err error - s.PodDisruptionBudget, err = c.podDisruptionBudgets() - if err != nil { - s.AddErr(ctx, err) +func NewPodDisruptionBudget(ctx context.Context, c *Cache, codes *issues.Codes) Linter { + return &PodDisruptionBudget{ + Collector: issues.NewCollector(codes, c.Config), + Cache: c, } +} - s.Pod, err = c.pods() - if err != nil { - s.AddErr(ctx, err) +func (s *PodDisruptionBudget) Preloads() Preloads { + return Preloads{ + internal.PDB: db.LoadResource[*polv1.PodDisruptionBudget], + internal.PO: db.LoadResource[*v1.Pod], } - - return &s } -// Sanitize all available PodDisruptionBudgets. -func (c *PodDisruptionBudget) Sanitize(ctx context.Context) error { - return sanitize.NewPodDisruptionBudget(c.Collector, c).Sanitize(ctx) +// Lint all available PodDisruptionBudgets. +func (s *PodDisruptionBudget) Lint(ctx context.Context) error { + for k, f := range s.Preloads() { + if err := f(ctx, s.Loader, internal.Glossary[k]); err != nil { + return err + } + } + + return lint.NewPodDisruptionBudget(s.Collector, s.DB).Lint(ctx) } diff --git a/internal/scrub/pod.go b/internal/scrub/pod.go index 9209a9a4..03586930 100644 --- a/internal/scrub/pod.go +++ b/internal/scrub/pod.go @@ -6,51 +6,47 @@ package scrub import ( "context" - "github.com/derailed/popeye/internal/cache" + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" "github.com/derailed/popeye/internal/issues" - "github.com/derailed/popeye/internal/sanitize" - "github.com/derailed/popeye/pkg/config" + "github.com/derailed/popeye/internal/lint" + v1 "k8s.io/api/core/v1" + netv1 "k8s.io/api/networking/v1" + polv1 "k8s.io/api/policy/v1" + mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" ) // Pod represents a Pod scruber. type Pod struct { *issues.Collector - *cache.Pod - *cache.PodsMetrics - *config.Config - *cache.PodDisruptionBudget - *cache.ServiceAccount + *Cache } -// NewPod return a new Pod scruber. -func NewPod(ctx context.Context, c *Cache, codes *issues.Codes) Sanitizer { - p := Pod{ - Collector: issues.NewCollector(codes, c.config), - Config: c.config, +// NewPod return a new instance. +func NewPod(ctx context.Context, c *Cache, codes *issues.Codes) Linter { + return &Pod{ + Collector: issues.NewCollector(codes, c.Config), + Cache: c, } +} - var err error - p.Pod, err = c.pods() - if err != nil { - p.AddErr(ctx, err) - } - - p.PodsMetrics, _ = c.podsMx() - - p.PodDisruptionBudget, err = c.podDisruptionBudgets() - if err != nil { - p.AddErr(ctx, err) +func (s *Pod) Preloads() Preloads { + return Preloads{ + internal.PO: db.LoadResource[*v1.Pod], + internal.SA: db.LoadResource[*v1.ServiceAccount], + internal.PDB: db.LoadResource[*polv1.PodDisruptionBudget], + internal.NP: db.LoadResource[*netv1.NetworkPolicy], + internal.PMX: db.LoadResource[*mv1beta1.PodMetrics], } +} - p.ServiceAccount, err = c.serviceaccounts() - if err != nil { - p.AddErr(ctx, err) +// Lint all available Pods. +func (s *Pod) Lint(ctx context.Context) error { + for k, f := range s.Preloads() { + if err := f(ctx, s.Loader, internal.Glossary[k]); err != nil { + return err + } } - return &p -} - -// Sanitize all available Pods. -func (p *Pod) Sanitize(ctx context.Context) error { - return sanitize.NewPod(p.Collector, p).Sanitize(ctx) + return lint.NewPod(s.Collector, s.DB).Lint(ctx) } diff --git a/internal/scrub/preload.go b/internal/scrub/preload.go new file mode 100644 index 00000000..1f3129bb --- /dev/null +++ b/internal/scrub/preload.go @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package scrub + +import "github.com/derailed/popeye/internal" + +type Preloads map[internal.R]LoaderFn + +func (p Preloads) Merge(ll Preloads) { + for k, v := range ll { + if _, ok := p[k]; ok { + continue + } + p[k] = v + } +} diff --git a/internal/scrub/pv.go b/internal/scrub/pv.go index f06a2eba..047eabe7 100644 --- a/internal/scrub/pv.go +++ b/internal/scrub/pv.go @@ -6,37 +6,41 @@ package scrub import ( "context" - "github.com/derailed/popeye/internal/cache" + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" "github.com/derailed/popeye/internal/issues" - "github.com/derailed/popeye/internal/sanitize" + "github.com/derailed/popeye/internal/lint" + v1 "k8s.io/api/core/v1" ) // PersistentVolume represents a PersistentVolume scruber. type PersistentVolume struct { *issues.Collector - *cache.PersistentVolume - *cache.Pod + *Cache } -// NewPersistentVolume return a new PersistentVolume scruber. -func NewPersistentVolume(ctx context.Context, c *Cache, codes *issues.Codes) Sanitizer { - p := PersistentVolume{Collector: issues.NewCollector(codes, c.config)} - - var err error - p.PersistentVolume, err = c.persistentvolumes() - if err != nil { - p.AddErr(ctx, err) +// NewPersistentVolume return a new instance. +func NewPersistentVolume(ctx context.Context, c *Cache, codes *issues.Codes) Linter { + return &PersistentVolume{ + Collector: issues.NewCollector(codes, c.Config), + Cache: c, } +} - p.Pod, err = c.pods() - if err != nil { - p.AddErr(ctx, err) +func (s *PersistentVolume) Preloads() Preloads { + return Preloads{ + internal.PV: db.LoadResource[*v1.PersistentVolume], + internal.PO: db.LoadResource[*v1.Pod], } - - return &p } -// Sanitize all available PersistentVolumes. -func (s *PersistentVolume) Sanitize(ctx context.Context) error { - return sanitize.NewPersistentVolume(s.Collector, s).Sanitize(ctx) +// Lint all available PersistentVolumes. +func (s *PersistentVolume) Lint(ctx context.Context) error { + for k, f := range s.Preloads() { + if err := f(ctx, s.Loader, internal.Glossary[k]); err != nil { + return err + } + } + + return lint.NewPersistentVolume(s.Collector, s.DB).Lint(ctx) } diff --git a/internal/scrub/pvc.go b/internal/scrub/pvc.go index dc9e4c54..5b39362d 100644 --- a/internal/scrub/pvc.go +++ b/internal/scrub/pvc.go @@ -6,39 +6,41 @@ package scrub import ( "context" - "github.com/derailed/popeye/internal/cache" + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" "github.com/derailed/popeye/internal/issues" - "github.com/derailed/popeye/internal/sanitize" + "github.com/derailed/popeye/internal/lint" + v1 "k8s.io/api/core/v1" ) // PersistentVolumeClaim represents a PersistentVolumeClaim scruber. type PersistentVolumeClaim struct { *issues.Collector - *cache.PersistentVolumeClaim - *cache.Pod + *Cache } -// NewPersistentVolumeClaim return a new PersistentVolumeClaim scruber. -func NewPersistentVolumeClaim(ctx context.Context, c *Cache, codes *issues.Codes) Sanitizer { - p := PersistentVolumeClaim{ - Collector: issues.NewCollector(codes, c.config), +// NewPersistentVolumeClaim returns a new instance. +func NewPersistentVolumeClaim(ctx context.Context, c *Cache, codes *issues.Codes) Linter { + return &PersistentVolumeClaim{ + Collector: issues.NewCollector(codes, c.Config), + Cache: c, } +} - var err error - p.PersistentVolumeClaim, err = c.persistentvolumeclaims() - if err != nil { - p.AddErr(ctx, err) +func (s *PersistentVolumeClaim) Preloads() Preloads { + return Preloads{ + internal.PVC: db.LoadResource[*v1.PersistentVolumeClaim], + internal.PO: db.LoadResource[*v1.Pod], } +} - p.Pod, err = c.pods() - if err != nil { - p.AddErr(ctx, err) +// Lint all available PersistentVolumeClaims. +func (s *PersistentVolumeClaim) Lint(ctx context.Context) error { + for k, f := range s.Preloads() { + if err := f(ctx, s.Loader, internal.Glossary[k]); err != nil { + return err + } } - return &p -} - -// Sanitize all available PersistentVolumeClaims. -func (s *PersistentVolumeClaim) Sanitize(ctx context.Context) error { - return sanitize.NewPersistentVolumeClaim(s.Collector, s).Sanitize(ctx) + return lint.NewPersistentVolumeClaim(s.Collector, s.DB).Lint(ctx) } diff --git a/internal/scrub/rb.go b/internal/scrub/rb.go index 956b905f..acf8cfa2 100644 --- a/internal/scrub/rb.go +++ b/internal/scrub/rb.go @@ -6,52 +6,45 @@ package scrub import ( "context" - "github.com/derailed/popeye/internal/cache" + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" "github.com/derailed/popeye/internal/issues" - "github.com/derailed/popeye/internal/sanitize" - "github.com/derailed/popeye/pkg/config" - "github.com/derailed/popeye/types" + "github.com/derailed/popeye/internal/lint" + v1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" ) // RoleBinding represents a RoleBinding scruber. type RoleBinding struct { - client types.Connection - *config.Config *issues.Collector - - *cache.RoleBinding - *cache.ClusterRole - *cache.Role + *Cache } -// NewRoleBinding return a new RoleBinding scruber. -func NewRoleBinding(ctx context.Context, c *Cache, codes *issues.Codes) Sanitizer { - rb := RoleBinding{ - client: c.factory.Client(), - Config: c.config, - Collector: issues.NewCollector(codes, c.config), - } - - var err error - rb.RoleBinding, err = c.rolebindings() - if err != nil { - rb.AddErr(ctx, err) +// NewRoleBinding returns a new instance. +func NewRoleBinding(ctx context.Context, c *Cache, codes *issues.Codes) Linter { + return &RoleBinding{ + Collector: issues.NewCollector(codes, c.Config), + Cache: c, } +} - rb.ClusterRole, err = c.clusterroles() - if err != nil { - rb.AddErr(ctx, err) +func (s *RoleBinding) Preloads() Preloads { + return Preloads{ + internal.ROB: db.LoadResource[*rbacv1.RoleBinding], + internal.RO: db.LoadResource[*rbacv1.Role], + internal.CR: db.LoadResource[*rbacv1.ClusterRole], + internal.CRB: db.LoadResource[*rbacv1.ClusterRoleBinding], + internal.SA: db.LoadResource[*v1.ServiceAccount], } +} - rb.Role, err = c.roles() - if err != nil { - rb.AddErr(ctx, err) +// Lint all available RoleBindings. +func (s *RoleBinding) Lint(ctx context.Context) error { + for k, f := range s.Preloads() { + if err := f(ctx, s.Loader, internal.Glossary[k]); err != nil { + return err + } } - return &rb -} - -// Sanitize all available RoleBindings. -func (c *RoleBinding) Sanitize(ctx context.Context) error { - return sanitize.NewRoleBinding(c.Collector, c).Sanitize(ctx) + return lint.NewRoleBinding(s.Collector, s.DB).Lint(ctx) } diff --git a/internal/scrub/ro.go b/internal/scrub/ro.go index c0eeb13c..103bd571 100644 --- a/internal/scrub/ro.go +++ b/internal/scrub/ro.go @@ -6,52 +6,42 @@ package scrub import ( "context" - "github.com/derailed/popeye/internal/cache" + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" "github.com/derailed/popeye/internal/issues" - "github.com/derailed/popeye/internal/sanitize" - "github.com/derailed/popeye/pkg/config" - "github.com/derailed/popeye/types" + "github.com/derailed/popeye/internal/lint" + rbacv1 "k8s.io/api/rbac/v1" ) // Role represents a Role scruber. type Role struct { - client types.Connection - *config.Config *issues.Collector - - *cache.Role - *cache.ClusterRoleBinding - *cache.RoleBinding + *Cache } -// NewRole return a new Role scruber. -func NewRole(ctx context.Context, c *Cache, codes *issues.Codes) Sanitizer { - ro := Role{ - client: c.factory.Client(), - Config: c.config, - Collector: issues.NewCollector(codes, c.config), - } - - var err error - ro.Role, err = c.roles() - if err != nil { - ro.AddErr(ctx, err) +// NewRole returns a new instance. +func NewRole(ctx context.Context, c *Cache, codes *issues.Codes) Linter { + return &Role{ + Collector: issues.NewCollector(codes, c.Config), + Cache: c, } +} - ro.ClusterRoleBinding, err = c.clusterrolebindings() - if err != nil { - ro.AddErr(ctx, err) +func (s *Role) Preloads() Preloads { + return Preloads{ + internal.RO: db.LoadResource[*rbacv1.Role], + internal.ROB: db.LoadResource[*rbacv1.RoleBinding], + internal.CRB: db.LoadResource[*rbacv1.ClusterRoleBinding], } +} - ro.RoleBinding, err = c.rolebindings() - if err != nil { - ro.AddErr(ctx, err) +// Lint all available Roles. +func (s *Role) Lint(ctx context.Context) error { + for k, f := range s.Preloads() { + if err := f(ctx, s.Loader, internal.Glossary[k]); err != nil { + return err + } } - return &ro -} - -// Sanitize all available Roles. -func (c *Role) Sanitize(ctx context.Context) error { - return sanitize.NewRole(c.Collector, c).Sanitize(ctx) + return lint.NewRole(s.Collector, s.DB).Lint(ctx) } diff --git a/internal/scrub/rs.go b/internal/scrub/rs.go index 25e6b30d..495113b4 100644 --- a/internal/scrub/rs.go +++ b/internal/scrub/rs.go @@ -6,46 +6,42 @@ package scrub import ( "context" - "github.com/derailed/popeye/internal/cache" + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" "github.com/derailed/popeye/internal/issues" - "github.com/derailed/popeye/internal/sanitize" - "github.com/derailed/popeye/pkg/config" - "github.com/derailed/popeye/types" + "github.com/derailed/popeye/internal/lint" + appsv1 "k8s.io/api/apps/v1" + v1 "k8s.io/api/core/v1" ) // ReplicaSet represents a ReplicaSet scruber. type ReplicaSet struct { *issues.Collector - *cache.ReplicaSet - *cache.Pod - *config.Config - - client types.Connection + *Cache } -// NewReplicaSet return a new ReplicaSet scruber. -func NewReplicaSet(ctx context.Context, c *Cache, codes *issues.Codes) Sanitizer { - d := ReplicaSet{ - client: c.factory.Client(), - Config: c.config, - Collector: issues.NewCollector(codes, c.config), +// NewReplicaSet returns a new instance. +func NewReplicaSet(ctx context.Context, c *Cache, codes *issues.Codes) Linter { + return &ReplicaSet{ + Collector: issues.NewCollector(codes, c.Config), + Cache: c, } +} - var err error - d.ReplicaSet, err = c.replicasets() - if err != nil { - d.AddErr(ctx, err) +func (s *ReplicaSet) Preloads() Preloads { + return Preloads{ + internal.RS: db.LoadResource[*appsv1.ReplicaSet], + internal.PO: db.LoadResource[*v1.Pod], } +} - d.Pod, err = c.pods() - if err != nil { - d.AddErr(ctx, err) +// Lint all available ReplicaSets. +func (s *ReplicaSet) Lint(ctx context.Context) error { + for k, f := range s.Preloads() { + if err := f(ctx, s.Loader, internal.Glossary[k]); err != nil { + return err + } } - return &d -} - -// Sanitize all available ReplicaSets. -func (d *ReplicaSet) Sanitize(ctx context.Context) error { - return sanitize.NewReplicaSet(d.Collector, d).Sanitize(ctx) + return lint.NewReplicaSet(s.Collector, s.DB).Lint(ctx) } diff --git a/internal/scrub/sa.go b/internal/scrub/sa.go index 78228745..6b0daf7f 100644 --- a/internal/scrub/sa.go +++ b/internal/scrub/sa.go @@ -6,61 +6,47 @@ package scrub import ( "context" - "github.com/derailed/popeye/internal/cache" + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" "github.com/derailed/popeye/internal/issues" - "github.com/derailed/popeye/internal/sanitize" + "github.com/derailed/popeye/internal/lint" + v1 "k8s.io/api/core/v1" + netv1 "k8s.io/api/networking/v1" + rbacv1 "k8s.io/api/rbac/v1" ) // ServiceAccount represents a ServiceAccount scruber. type ServiceAccount struct { *issues.Collector - *cache.ServiceAccount - *cache.Pod - *cache.ClusterRoleBinding - *cache.RoleBinding - *cache.Secret - *cache.Ingress + *Cache } -// NewServiceAccount return a new ServiceAccount scruber. -func NewServiceAccount(ctx context.Context, c *Cache, codes *issues.Codes) Sanitizer { - s := ServiceAccount{Collector: issues.NewCollector(codes, c.config)} - - var err error - s.ServiceAccount, err = c.serviceaccounts() - if err != nil { - s.AddErr(ctx, err) - } - - s.Pod, err = c.pods() - if err != nil { - s.AddErr(ctx, err) - } - - s.ClusterRoleBinding, err = c.clusterrolebindings() - if err != nil { - s.AddErr(ctx, err) - } - - s.RoleBinding, err = c.rolebindings() - if err != nil { - s.AddErr(ctx, err) +// NewServiceAccount returns a new instance. +func NewServiceAccount(ctx context.Context, c *Cache, codes *issues.Codes) Linter { + return &ServiceAccount{ + Collector: issues.NewCollector(codes, c.Config), + Cache: c, } +} - s.Secret, err = c.secrets() - if err != nil { - s.AddErr(ctx, err) +func (s *ServiceAccount) Preloads() Preloads { + return Preloads{ + internal.SA: db.LoadResource[*v1.ServiceAccount], + internal.PO: db.LoadResource[*v1.Pod], + internal.ROB: db.LoadResource[*rbacv1.RoleBinding], + internal.CRB: db.LoadResource[*rbacv1.ClusterRoleBinding], + internal.SEC: db.LoadResource[*v1.Secret], + internal.ING: db.LoadResource[*netv1.Ingress], } +} - s.Ingress, err = c.ingresses() - if err != nil { - s.AddErr(ctx, err) +// Lint all available ServiceAccounts. +func (s *ServiceAccount) Lint(ctx context.Context) error { + for k, f := range s.Preloads() { + if err := f(ctx, s.Loader, internal.Glossary[k]); err != nil { + return err + } } - return &s -} - -// Sanitize all available ServiceAccounts. -func (s *ServiceAccount) Sanitize(ctx context.Context) error { - return sanitize.NewServiceAccount(s.Collector, s).Sanitize(ctx) + return lint.NewServiceAccount(s.Collector, s.DB).Lint(ctx) } diff --git a/internal/scrub/sec.go b/internal/scrub/sec.go index 5351e628..dab300fb 100644 --- a/internal/scrub/sec.go +++ b/internal/scrub/sec.go @@ -6,49 +6,44 @@ package scrub import ( "context" - "github.com/derailed/popeye/internal/cache" + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" "github.com/derailed/popeye/internal/issues" - "github.com/derailed/popeye/internal/sanitize" + "github.com/derailed/popeye/internal/lint" + v1 "k8s.io/api/core/v1" + netv1 "k8s.io/api/networking/v1" ) // Secret represents a Secret scruber. type Secret struct { *issues.Collector - *cache.Secret - *cache.Pod - *cache.ServiceAccount - *cache.Ingress + *Cache } // NewSecret return a new Secret scruber. -func NewSecret(ctx context.Context, c *Cache, codes *issues.Codes) Sanitizer { - s := Secret{Collector: issues.NewCollector(codes, c.config)} - - var err error - s.Secret, err = c.secrets() - if err != nil { - s.AddErr(ctx, err) - } - - s.Pod, err = c.pods() - if err != nil { - s.AddErr(ctx, err) +func NewSecret(ctx context.Context, c *Cache, codes *issues.Codes) Linter { + return &Secret{ + Collector: issues.NewCollector(codes, c.Config), + Cache: c, } +} - s.ServiceAccount, err = c.serviceaccounts() - if err != nil { - s.AddErr(ctx, err) +func (s *Secret) Preloads() Preloads { + return Preloads{ + internal.SEC: db.LoadResource[*v1.Secret], + internal.PO: db.LoadResource[*v1.Pod], + internal.SA: db.LoadResource[*v1.ServiceAccount], + internal.ING: db.LoadResource[*netv1.Ingress], } +} - s.Ingress, err = c.ingresses() - if err != nil { - s.AddErr(ctx, err) +// Lint all available Secrets. +func (s *Secret) Lint(ctx context.Context) error { + for k, f := range s.Preloads() { + if err := f(ctx, s.Loader, internal.Glossary[k]); err != nil { + return err + } } - return &s -} - -// Sanitize all available Secrets. -func (c *Secret) Sanitize(ctx context.Context) error { - return sanitize.NewSecret(c.Collector, c).Sanitize(ctx) + return lint.NewSecret(s.Collector, s.DB).Lint(ctx) } diff --git a/internal/scrub/sts.go b/internal/scrub/sts.go index b57944fc..7b90b13f 100644 --- a/internal/scrub/sts.go +++ b/internal/scrub/sts.go @@ -6,51 +6,45 @@ package scrub import ( "context" - "github.com/derailed/popeye/internal/cache" + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" "github.com/derailed/popeye/internal/issues" - "github.com/derailed/popeye/internal/sanitize" - "github.com/derailed/popeye/pkg/config" + "github.com/derailed/popeye/internal/lint" + appsv1 "k8s.io/api/apps/v1" + v1 "k8s.io/api/core/v1" + mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" ) // StatefulSet represents a StatefulSet scruber. type StatefulSet struct { *issues.Collector - *cache.Pod - *cache.StatefulSet - *cache.PodsMetrics - *cache.ServiceAccount - *config.Config + *Cache } // NewStatefulSet return a new StatefulSet scruber. -func NewStatefulSet(ctx context.Context, c *Cache, codes *issues.Codes) Sanitizer { - s := StatefulSet{ - Collector: issues.NewCollector(codes, c.config), - Config: c.config, - } - - var err error - s.StatefulSet, err = c.statefulsets() - if err != nil { - s.AddErr(ctx, err) +func NewStatefulSet(ctx context.Context, c *Cache, codes *issues.Codes) Linter { + return &StatefulSet{ + Collector: issues.NewCollector(codes, c.Config), + Cache: c, } +} - s.Pod, err = c.pods() - if err != nil { - s.AddErr(ctx, err) +func (s *StatefulSet) Preloads() Preloads { + return Preloads{ + internal.STS: db.LoadResource[*appsv1.StatefulSet], + internal.PO: db.LoadResource[*v1.Pod], + internal.SA: db.LoadResource[*v1.ServiceAccount], + internal.PMX: db.LoadResource[*mv1beta1.PodMetrics], } +} - s.PodsMetrics, _ = c.podsMx() - - s.ServiceAccount, err = c.serviceaccounts() - if err != nil { - s.AddErr(ctx, err) +// Lint all available StatefulSets. +func (s *StatefulSet) Lint(ctx context.Context) error { + for k, f := range s.Preloads() { + if err := f(ctx, s.Loader, internal.Glossary[k]); err != nil { + return err + } } - return &s -} - -// Sanitize all available StatefulSets. -func (c *StatefulSet) Sanitize(ctx context.Context) error { - return sanitize.NewStatefulSet(c.Collector, c).Sanitize(ctx) + return lint.NewStatefulSet(s.Collector, s.DB).Lint(ctx) } diff --git a/internal/scrub/svc.go b/internal/scrub/svc.go index 8b64cbbc..fde05b49 100644 --- a/internal/scrub/svc.go +++ b/internal/scrub/svc.go @@ -6,43 +6,42 @@ package scrub import ( "context" - "github.com/derailed/popeye/internal/cache" + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" "github.com/derailed/popeye/internal/issues" - "github.com/derailed/popeye/internal/sanitize" + "github.com/derailed/popeye/internal/lint" + v1 "k8s.io/api/core/v1" ) // Service represents a Service scruber. type Service struct { *issues.Collector - *cache.Service - *cache.Pod - *cache.Endpoints + *Cache } -// NewService return a new Service scruber. -func NewService(ctx context.Context, c *Cache, codes *issues.Codes) Sanitizer { - s := Service{Collector: issues.NewCollector(codes, c.config)} - - var err error - s.Service, err = c.services() - if err != nil { - s.AddErr(ctx, err) +// NewService return a new instance. +func NewService(ctx context.Context, c *Cache, codes *issues.Codes) Linter { + return &Service{ + Collector: issues.NewCollector(codes, c.Config), + Cache: c, } +} - s.Pod, err = c.pods() - if err != nil { - s.AddErr(ctx, err) +func (s *Service) Preloads() Preloads { + return Preloads{ + internal.SVC: db.LoadResource[*v1.Service], + internal.PO: db.LoadResource[*v1.Pod], + internal.EP: db.LoadResource[*v1.Endpoints], } +} - s.Endpoints, err = c.endpoints() - if err != nil { - s.AddErr(ctx, err) +// Lint all available Services. +func (s *Service) Lint(ctx context.Context) error { + for k, f := range s.Preloads() { + if err := f(ctx, s.Loader, internal.Glossary[k]); err != nil { + return err + } } - return &s -} - -// Sanitize all available Services. -func (s *Service) Sanitize(ctx context.Context) error { - return sanitize.NewService(s.Collector, s).Sanitize(ctx) + return lint.NewService(s.Collector, s.DB).Lint(ctx) } diff --git a/internal/scrub/types.go b/internal/scrub/types.go index f3d942ec..735a9d28 100644 --- a/internal/scrub/types.go +++ b/internal/scrub/types.go @@ -6,18 +6,35 @@ package scrub import ( "context" + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" "github.com/derailed/popeye/internal/issues" - "github.com/derailed/popeye/pkg/config" + "github.com/derailed/popeye/internal/rules" + "github.com/derailed/popeye/types" ) -// Sanitizer represents a resource sanitizer. -type Sanitizer interface { - Collector - Sanitize(context.Context) error -} +type Scrubs map[internal.R]ScrubFn + +// ScrubFn represents a resource scruber. +type ScrubFn func(context.Context, *Cache, *issues.Codes) Linter + +// LoaderFn represents a resource loader. +type LoaderFn func(context.Context, *db.Loader, types.GVR) error // Collector collects sanitization issues. type Collector interface { - MaxSeverity(res string) config.Level + MaxSeverity(res string) rules.Level Outcome() issues.Outcome } + +// Linter represents a resource linter. +type Linter interface { + // Collector tracks issues. + Collector + + // Lint runs checks on a resource. + Lint(context.Context) error + + // Preloads Preloads resource requirements. + Preloads() Preloads +} diff --git a/internal/test/helpers.go b/internal/test/helpers.go new file mode 100644 index 00000000..b31904f4 --- /dev/null +++ b/internal/test/helpers.go @@ -0,0 +1,147 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package test + +import ( + "context" + "os" + "path/filepath" + "testing" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/db/schema" + "github.com/derailed/popeye/internal/issues" + "github.com/derailed/popeye/pkg/config" + "github.com/derailed/popeye/types" + "github.com/hashicorp/go-memdb" + "github.com/stretchr/testify/assert" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/yaml" +) + +func NewTestDB() (*db.DB, error) { + initLinters() + d, err := memdb.NewMemDB(schema.Init()) + if err != nil { + return nil, err + } + + return db.NewDB(d), nil +} + +func initLinters() { + internal.Glossary = internal.Linters{ + internal.CM: types.NewGVR("v1/configmaps"), + internal.EP: types.NewGVR("v1/endpoints"), + internal.NS: types.NewGVR("v1/namespaces"), + internal.NO: types.NewGVR("v1/nodes"), + internal.PV: types.NewGVR("v1/persistentvolumes"), + internal.PVC: types.NewGVR("v1/persistentvolumeclaims"), + internal.PO: types.NewGVR("v1/pods"), + internal.SEC: types.NewGVR("v1/secrets"), + internal.SA: types.NewGVR("v1/serviceaccounts"), + internal.SVC: types.NewGVR("v1/services"), + internal.DS: types.NewGVR("apps/v1/daemonsets"), + internal.DP: types.NewGVR("apps/v1/deployments"), + internal.RS: types.NewGVR("apps/v1/replicasets"), + internal.STS: types.NewGVR("apps/v1/statefulsets"), + internal.CR: types.NewGVR("rbac.authorization.k8s.io/v1/clusterroles"), + internal.CRB: types.NewGVR("rbac.authorization.k8s.io/v1/clusterrolebindings"), + internal.RO: types.NewGVR("rbac.authorization.k8s.io/v1/roles"), + internal.ROB: types.NewGVR("rbac.authorization.k8s.io/v1/rolebindings"), + internal.ING: types.NewGVR("networking.k8s.io/v1/ingresses"), + internal.NP: types.NewGVR("networking.k8s.io/v1/networkpolicies"), + internal.PDB: types.NewGVR("policy/v1/poddisruptionbudgets"), + internal.HPA: types.NewGVR("autoscaling/v1/horizontalpodautoscalers"), + internal.PMX: types.NewGVR("metrics.k8s.io/v1beta1/podmetrics"), + internal.NMX: types.NewGVR("metrics.k8s.io/v1beta1/nodemetrics"), + internal.CJOB: types.NewGVR("batch/v1/cronjobs"), + internal.JOB: types.NewGVR("batch/v1/jobs"), + internal.GW: types.NewGVR("gateway.networking.k8s.io/v1/gateways"), + internal.GWC: types.NewGVR("gateway.networking.k8s.io/v1/gatewayclasses"), + internal.GWR: types.NewGVR("gateway.networking.k8s.io/v1/httproutes"), + } +} + +func MakeRes(c, m string) v1.ResourceList { + return v1.ResourceList{ + v1.ResourceCPU: *MakeQty(c), + v1.ResourceMemory: *MakeQty(m), + } +} + +func MakeQty(s string) *resource.Quantity { + if s == "" { + return nil + } + + qty, _ := resource.ParseQuantity(s) + return &qty +} + +func ToQty(s string) resource.Quantity { + q, _ := resource.ParseQuantity(s) + + return q +} + +func LoadRes[T any](p string) (*T, error) { + bb, err := os.ReadFile(filepath.Join("testdata", p)) + if err != nil { + return nil, err + } + var l T + if err := yaml.Unmarshal(bb, &l); err != nil { + return nil, err + } + + return &l, nil +} + +func LoadDB[T metav1.ObjectMetaAccessor](ctx context.Context, dba *db.DB, p string, gvr types.GVR) error { + ucc, err := LoadRes[unstructured.UnstructuredList](p) + if err != nil { + return err + } + cc := make([]runtime.Object, 0, len(ucc.Items)) + for i := range ucc.Items { + u := ucc.Items[i] + cc = append(cc, &u) + } + + return db.Save[T](ctx, dba, gvr, cc) +} + +func MakeCollector(t *testing.T) *issues.Collector { + return issues.NewCollector(loadCodes(t), MakeConfig(t)) +} + +func MakeCtx(t *testing.T) context.Context { + return context.WithValue(context.Background(), internal.KeyConfig, MakeConfig(t)) +} + +func loadCodes(t *testing.T) *issues.Codes { + codes, err := issues.LoadCodes() + assert.Nil(t, err) + + return codes +} + +func MakeConfig(t *testing.T) *config.Config { + c, err := config.NewConfig(config.NewFlags()) + assert.Nil(t, err) + return c +} + +func MakeContext(gvr, section string) context.Context { + return context.WithValue(context.Background(), internal.KeyRunInfo, internal.RunInfo{ + Section: section, + SectionGVR: types.NewGVR(gvr), + }) +} diff --git a/internal/types.go b/internal/types.go index ee0074e8..05a4d705 100644 --- a/internal/types.go +++ b/internal/types.go @@ -3,34 +3,35 @@ package internal -type ( - GVR int - GVRs map[GVR]string -) +// !!BOZO!! +// type ( +// Linter int +// Linters map[Linter]types.GVR +// ) -const ( - LrGVR GVR = iota - SvcGVR - EpGVR - NoGVR - NsGVR - PoGVR - CmGVR - SecGVR - SaGVR - PvGVR - PvcGVR - DpGVR - RsGVR - DsGVR - StsGVR - NpGVR - CrGVR - CrbGVR - RoGVR - RobGVR - IngGVR - PdbGVR - PspGVR - HpaGVR -) +// const ( +// LR Linter = iota +// SVC +// EP +// NO +// NS +// PO +// CM +// SEC +// SA +// PV +// PVC +// DP +// RS +// DS +// STS +// NP +// CR +// CRB +// RO +// ROB +// ING +// PDB +// PSP +// HPA +// ) diff --git a/pkg/config/config.go b/pkg/config/config.go index 49bd2bb8..3bd9dde8 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -8,6 +8,9 @@ import ( "os" "github.com/derailed/popeye/internal/client" + "github.com/derailed/popeye/internal/rules" + "github.com/derailed/popeye/pkg/config/json" + "github.com/derailed/popeye/types" "gopkg.in/yaml.v2" ) @@ -22,14 +25,19 @@ type Config struct { // NewConfig create a new Popeye configuration. func NewConfig(flags *Flags) (*Config, error) { - cfg := Config{Popeye: NewPopeye()} + cfg := Config{ + Popeye: NewPopeye(), + } if isSet(flags.Spinach) { - f, err := os.ReadFile(*flags.Spinach) + bb, err := os.ReadFile(*flags.Spinach) if err != nil { return nil, err } - if err := yaml.Unmarshal(f, &cfg); err != nil { + if err := json.NewValidator().Validate(json.SpinachSchema, bb); err != nil { + return nil, fmt.Errorf("validation failed for %q: %w", *flags.Spinach, err) + } + if err := yaml.Unmarshal(bb, &cfg); err != nil { return nil, fmt.Errorf("Invalid spinach config file -- %w", err) } } @@ -42,17 +50,32 @@ func NewConfig(flags *Flags) (*Config, error) { all := client.AllNamespaces flags.Namespace = &all } - cfg.LintLevel = int(ToIssueLevel(flags.LintLevel)) + cfg.LintLevel = int(rules.ToIssueLevel(flags.LintLevel)) return &cfg, nil } -// LinterLevel returns the current lint level. -func (c *Config) LinterLevel() int { - return c.LintLevel +func (c *Config) Match(s rules.Spec) bool { + return c.Popeye.Match(s) +} + +func (c *Config) ExcludeFQN(gvr types.GVR, fqn string, cos []string) bool { + return c.Popeye.Match(rules.Spec{ + GVR: gvr, + FQN: fqn, + Containers: cos, + }) +} + +func (c *Config) ExcludeContainer(gvr types.GVR, fqn, co string) bool { + return c.Popeye.Match(rules.Spec{ + GVR: gvr, + FQN: fqn, + Containers: []string{co}, + }) } -// Sections returns a collection of sanitizers categories. +// Sections tracks a collection of internal. func (c *Config) Sections() []string { if c.Flags.Sections != nil { return *c.Flags.Sections @@ -73,7 +96,7 @@ func (c *Config) MEMResourceLimits() Allocations { // NodeCPULimit returns the node cpu threshold if set otherwise the default. func (c *Config) NodeCPULimit() float64 { - l := c.Node.Limits.CPU + l := c.Resources.Node.Limits.CPU if l == 0 { return defaultCPULimit } @@ -82,7 +105,7 @@ func (c *Config) NodeCPULimit() float64 { // PodCPULimit returns the pod cpu threshold if set otherwise the default. func (c *Config) PodCPULimit() float64 { - l := c.Pod.Limits.CPU + l := c.Resources.Pod.Limits.CPU if l == 0 { return defaultCPULimit } @@ -91,7 +114,7 @@ func (c *Config) PodCPULimit() float64 { // RestartsLimit returns pod restarts limit. func (c *Config) RestartsLimit() int { - l := c.Pod.Restarts + l := c.Resources.Pod.Restarts if l == 0 { return defaultRestarts } @@ -100,7 +123,7 @@ func (c *Config) RestartsLimit() int { // PodMEMLimit returns the pod mem threshold if set otherwise the default. func (c *Config) PodMEMLimit() float64 { - l := c.Pod.Limits.Memory + l := c.Resources.Pod.Limits.Memory if l == 0 { return defaultMEMLimit } @@ -109,13 +132,14 @@ func (c *Config) PodMEMLimit() float64 { // NodeMEMLimit returns the pod mem threshold if set otherwise the default. func (c *Config) NodeMEMLimit() float64 { - l := c.Node.Limits.Memory + l := c.Resources.Node.Limits.Memory if l == 0 { return defaultMEMLimit } return l } +// AllowedRegistries tracks allowed docker registries. func (c *Config) AllowedRegistries() []string { return c.Registries } @@ -123,7 +147,6 @@ func (c *Config) AllowedRegistries() []string { // ---------------------------------------------------------------------------- // Helpers... -// IsSet checks if a string flag is set. func isSet(s *string) bool { return s != nil && *s != "" } diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 05699252..1ff25070 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -1,50 +1,278 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright Authors of Popeye -package config +package config_test import ( "testing" + "github.com/derailed/popeye/internal/rules" + "github.com/derailed/popeye/pkg/config" + "github.com/derailed/popeye/types" + "github.com/rs/zerolog" "github.com/stretchr/testify/assert" ) +func init() { + zerolog.SetGlobalLevel(zerolog.FatalLevel) +} + +func TestConfigGlobalExcludes(t *testing.T) { + uu := map[string]struct { + spec rules.Spec + e bool + }{ + "exact-ns": { + spec: rules.Spec{ + GVR: types.NewGVR("v1/pods"), + FQN: "gns1/blee", + Containers: []string{"c1"}, + }, + e: true, + }, + } + + sp := "testdata/sp3.yml" + f := config.NewFlags() + f.Spinach = &sp + cfg, err := config.NewConfig(f) + assert.NoError(t, err) + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, cfg.Match(u.spec)) + }) + } +} + +func TestConfigExcludes(t *testing.T) { + uu := map[string]struct { + spec rules.Spec + e bool + }{ + "exact-ns": { + spec: rules.Spec{ + GVR: types.NewGVR("v1/pods"), + FQN: "ns1/blee", + Containers: []string{"c1"}, + }, + e: true, + }, + "skip-exact-ns": { + spec: rules.Spec{ + GVR: types.NewGVR("v1/pods"), + FQN: "ns5/blee", + }, + }, + + "skip-gvr-no-rules": { + spec: rules.Spec{ + GVR: types.NewGVR("v1/configmaps"), + FQN: "fred/cm1", + }, + }, + + "match-annotations": { + spec: rules.Spec{ + GVR: types.NewGVR("v1/nodes"), + Annotations: rules.Labels{"a1": "b1"}, + Code: 100, + }, + e: true, + }, + "skip-annotations-key": { + spec: rules.Spec{ + GVR: types.NewGVR("v1/nodes"), + FQN: "fred/annot1", + Annotations: rules.Labels{"a5": "b1"}, + Code: 100, + }, + }, + "skip-annotations-val": { + spec: rules.Spec{ + GVR: types.NewGVR("v1/nodes"), + FQN: "fred/annot2", + Annotations: rules.Labels{"a1": "b2"}, + Code: 100, + }, + }, + + "match-labels": { + spec: rules.Spec{ + GVR: types.NewGVR("v1/pods"), + FQN: "fred/bozo", + Labels: rules.Labels{"kube-system": "fred"}, + Code: 300, + }, + e: true, + }, + "skip-labels": { + spec: rules.Spec{ + GVR: types.NewGVR("v1/pods"), + FQN: "fred/bozo", + Labels: rules.Labels{"kube-system": "fred1"}, + Code: 300, + }, + }, + + "exact-container": { + spec: rules.Spec{ + GVR: types.NewGVR("v1/pods"), + FQN: "ns1/blee", + Containers: []string{"c1"}, + }, + e: true, + }, + "skip-container": { + spec: rules.Spec{ + GVR: types.NewGVR("v1/pods"), + FQN: "ns1/blee-1", + Containers: []string{"bozo"}, + }, + }, + + "exact-code": { + spec: rules.Spec{ + GVR: types.NewGVR("v1/services"), + FQN: "blee/svc1", + Labels: rules.Labels{"default": "dictionary"}, + Code: 100, + }, + e: true, + }, + "skip-exact-code": { + spec: rules.Spec{ + GVR: types.NewGVR("v1/services"), + FQN: "blee/svc2", + Code: 301, + }, + }, + + "regex-start": { + spec: rules.Spec{ + GVR: types.NewGVR("v1/pods"), + FQN: "istio/fred", + }, + e: true, + }, + "skip-regex-start": { + spec: rules.Spec{ + GVR: types.NewGVR("v1/pods"), + FQN: "ns-10/fred", + }, + }, + "regex-contains": { + spec: rules.Spec{ + GVR: types.NewGVR("v1/secrets"), + FQN: "istio-fred", + }, + e: true, + }, + "skip-regex-contains": { + spec: rules.Spec{ + GVR: types.NewGVR("v1/secrets"), + FQN: "click-clack/bozo", + }, + }, + } + + sp := "testdata/sp3.yml" + f := config.NewFlags() + f.Spinach = &sp + cfg, err := config.NewConfig(f) + assert.NoError(t, err) + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, cfg.Match(u.spec)) + }) + } +} + func TestNewConfig(t *testing.T) { - cfg, err := NewConfig(NewFlags()) + cfg, err := config.NewConfig(config.NewFlags()) assert.Nil(t, err) assert.Equal(t, 80.0, cfg.NodeCPULimit()) assert.Equal(t, 80.0, cfg.NodeMEMLimit()) assert.Equal(t, 80.0, cfg.PodCPULimit()) assert.Equal(t, 80.0, cfg.PodMEMLimit()) - assert.False(t, cfg.ShouldExclude("node", "n1", 100)) - assert.False(t, cfg.ShouldExclude("namespace", "kube-public", 100)) - assert.False(t, cfg.ShouldExclude("service", "default/kubernetes", 100)) + + ok := cfg.Match(rules.Spec{ + GVR: types.NewGVR("v1/nodes"), + FQN: "no1", + Code: 100, + }) + assert.False(t, ok) + + ok = cfg.Match(rules.Spec{ + GVR: types.NewGVR("v1/namespaces"), + FQN: "kube-public", + Code: 100, + }) + assert.False(t, ok) + + ok = cfg.Match(rules.Spec{ + GVR: types.NewGVR("v1/services"), + FQN: "default/svc1", + Code: 100, + }) + assert.False(t, ok) + assert.Equal(t, 5, cfg.RestartsLimit()) - assert.Equal(t, Allocations{UnderPerc: 200, OverPerc: 50}, cfg.CPUResourceLimits()) - assert.Equal(t, Allocations{UnderPerc: 200, OverPerc: 50}, cfg.MEMResourceLimits()) - assert.Equal(t, 0, cfg.LinterLevel()) - assert.Equal(t, []string{}, cfg.Registries) + assert.Equal(t, config.Allocations{UnderPerc: 200, OverPerc: 50}, cfg.CPUResourceLimits()) + assert.Equal(t, config.Allocations{UnderPerc: 200, OverPerc: 50}, cfg.MEMResourceLimits()) + assert.Equal(t, 0, cfg.LintLevel) + assert.Nil(t, cfg.Registries) } func TestNewConfigWithFile(t *testing.T) { var ( - dir = "testdata/sp1.yml" - ss = []string{"s1", "s2"} - f = NewFlags() + dir = "testdata/sp1.yml" + ss = []string{"s1", "s2"} + f = config.NewFlags() + true = true ) f.Sections = &ss - f.AllNamespaces = boolPtr(true) + f.AllNamespaces = &true f.Spinach = &dir - cfg, err := NewConfig(f) + cfg, err := config.NewConfig(f) assert.Nil(t, err) assert.Equal(t, 3, cfg.RestartsLimit()) - assert.True(t, cfg.ShouldExclude("node", "n1", 100)) - assert.False(t, cfg.ShouldExclude("pod", "default/fred", 100)) - assert.True(t, cfg.ShouldExclude("service", "default/dictionary", 100)) - assert.True(t, cfg.ShouldExclude("namespace", "kube-public", 100)) + + ok := cfg.Match(rules.Spec{ + GVR: types.NewGVR("v1/nodes"), + FQN: "n1", + Code: 100, + Labels: rules.Labels{"fred": "blee", "k8s-app": "app1"}, + }) + assert.True(t, ok) + + ok = cfg.Match(rules.Spec{ + GVR: types.NewGVR("v1/pods"), + FQN: "default/fred", + Code: 111, + }) + assert.False(t, ok) + + ok = cfg.Match(rules.Spec{ + GVR: types.NewGVR("v1/services"), + FQN: "default/dictionary", + Code: 100, + }) + assert.False(t, ok) + + ok = cfg.Match(rules.Spec{ + GVR: types.NewGVR("v1/namespaces"), + FQN: "kube-public", + Code: 100, + }) + assert.False(t, ok) + assert.Equal(t, 90.0, cfg.NodeCPULimit()) assert.Equal(t, 80.0, cfg.NodeMEMLimit()) assert.Equal(t, 80.0, cfg.PodCPULimit()) @@ -59,11 +287,11 @@ func TestNewConfigWithFile(t *testing.T) { func TestNewConfigNoResourceSpec(t *testing.T) { var ( dir = "testdata/sp2.yml" - f = NewFlags() + f = config.NewFlags() ) f.Spinach = &dir - cfg, err := NewConfig(f) + cfg, err := config.NewConfig(f) assert.Nil(t, err) assert.Equal(t, 80.0, cfg.NodeCPULimit()) @@ -74,22 +302,22 @@ func TestNewConfigNoResourceSpec(t *testing.T) { func TestNewConfigFileToast(t *testing.T) { var ( - dir = "testdata/sp_old.yml" - f = NewFlags() + dir = "testdata/sp-toast.yml" + f = config.NewFlags() ) f.Spinach = &dir - _, err := NewConfig(f) + _, err := config.NewConfig(f) assert.NotNil(t, err) } func TestNewConfigFileNoExists(t *testing.T) { var ( dir = "testdata/spinach.yml" - f = NewFlags() + f = config.NewFlags() ) f.Spinach = &dir - _, err := NewConfig(f) + _, err := config.NewConfig(f) assert.NotNil(t, err) } diff --git a/pkg/config/excludes.go b/pkg/config/excludes.go deleted file mode 100644 index 6cf2387b..00000000 --- a/pkg/config/excludes.go +++ /dev/null @@ -1,138 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package config - -import ( - "regexp" - "strings" - - "github.com/rs/zerolog" -) - -const rxMarker = "rx:" - -var regExp = regexp.MustCompile(`\A` + rxMarker) - -type ( - // Exclusion represents a resource exclusion. - Exclusion struct { - Name string - Containers []string - Codes []ID - } - - // Exclusions represents a collection of excludes items. - // This can be a straight string match of regex using an rx: prefix. - Exclusions []Exclusion - - // Excludes represents a set of resources that should be excluded - // from the sanitizer. - Excludes map[string]Exclusions -) - -func init() { - zerolog.SetGlobalLevel(zerolog.FatalLevel) -} - -func newExcludes() Excludes { - return Excludes{} -} - -// ExcludeContainer checks if a given container should be excluded. -func (e Excludes) ExcludeContainer(gvr, fqn, container string) bool { - excludes, ok := e[gvr] - if !ok { - return false - } - - for _, exclude := range excludes { - if exclude.Match(fqn) && in(exclude.Containers, container) { - return true - } - } - - return false -} - -func in(ss []string, victim string) bool { - for _, s := range ss { - if s == victim { - return true - } - } - return false -} - -// ExcludeFQN checks if a given named resource should be excluded. -func (e Excludes) ExcludeFQN(gvr, fqn string) bool { - excludes, ok := e[gvr] - if !ok { - return false - } - - for _, exclude := range excludes { - if exclude.Match(fqn) && len(exclude.Containers) == 0 { - return true - } - } - - return false -} - -// ShouldExclude checks if a given named resource should be excluded. -func (e Excludes) ShouldExclude(section, fqn string, code ID) bool { - // Not mentioned in config. Allow all - excludes, ok := e[section] - if !ok { - return false - } - - return excludes.Match(fqn, code) -} - -// Match checks if a given named should be excluded. -func (e Exclusions) Match(resource string, code ID) bool { - for _, exclude := range e { - if len(exclude.Containers) == 0 && exclude.Match(resource) && hasCode(exclude.Codes, code) { - return true - } - } - - return false -} - -// Match check if a resource matches the configuration. -func (e Exclusion) Match(fqn string) bool { - if !isRegex(e.Name) { - return fqn == e.Name - } - - return rxMatch(e.Name, fqn) -} - -// ---------------------------------------------------------------------------- -// Helpers... - -func rxMatch(exp, name string) bool { - rx := regexp.MustCompile(strings.Replace(exp, rxMarker, "", 1)) - b := rx.MatchString(name) - return b -} - -func isRegex(f string) bool { - return regExp.MatchString(f) -} - -func hasCode(codes []ID, code ID) bool { - if len(codes) == 0 { - return true - } - - for _, c := range codes { - if c == code { - return true - } - } - return false -} diff --git a/pkg/config/excludes_test.go b/pkg/config/excludes_test.go deleted file mode 100644 index fb37fe55..00000000 --- a/pkg/config/excludes_test.go +++ /dev/null @@ -1,174 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package config_test - -import ( - "testing" - - "github.com/derailed/popeye/pkg/config" - "github.com/stretchr/testify/assert" -) - -func TestExclusion(t *testing.T) { - uu := map[string]struct { - ex config.Exclusion - res string - code config.ID - e bool - }{ - "empty": { - ex: config.Exclusion{Name: "", Codes: []config.ID{}}, - res: "fred", - code: 100, - }, - "plain_match_both": { - ex: config.Exclusion{Name: "fred", Codes: []config.ID{100}}, - res: "fred", - code: 100, - e: true, - }, - "plain_match_none": { - ex: config.Exclusion{Name: "fred", Codes: []config.ID{100}}, - res: "blee", - code: 101, - }, - "plain_match_name": { - ex: config.Exclusion{Name: "fred", Codes: []config.ID{100}}, - res: "fred", - code: 200, - }, - "plain_match_code": { - ex: config.Exclusion{Name: "fred", Codes: []config.ID{100}}, - res: "blee", - code: 100, - }, - "rx_match_both": { - ex: config.Exclusion{Name: "rx:fred", Codes: []config.ID{100}}, - res: "freddy", - code: 100, - e: true, - }, - "rx_match_none": { - ex: config.Exclusion{Name: "rx:fred", Codes: []config.ID{100}}, - res: "frued", - code: 101, - }, - "rx_match_name": { - ex: config.Exclusion{Name: "rx:fred", Codes: []config.ID{100}}, - res: "freddy", - code: 200, - }, - "rx_match_code": { - ex: config.Exclusion{Name: "rx:fred", Codes: []config.ID{100}}, - res: "blee", - code: 100, - }, - "rx_match_all_codes": { - ex: config.Exclusion{Name: "rx:fred", Codes: []config.ID{}}, - res: "freddo", - code: 100, - e: true, - }, - "plain_match_all_codes": { - ex: config.Exclusion{Name: "fred", Codes: []config.ID{}}, - res: "fred", - code: 100, - e: true, - }, - } - - for k := range uu { - u := uu[k] - t.Run(k, func(t *testing.T) { - ee := config.Excludes{"test": config.Exclusions{u.ex}} - assert.Equal(t, u.e, ee.ShouldExclude("test", u.res, u.code)) - }) - } -} - -func TestExcludes(t *testing.T) { - uu := map[string]struct { - excludes config.Excludes - section string - res string - code config.ID - e bool - }{ - "empty": { - excludes: config.Excludes{}, - section: "fred", - res: "blee", - code: 100, - }, - "plain_no_match": { - excludes: config.Excludes{ - "fred": { - config.Exclusion{Name: "aa", Codes: []config.ID{100, 200, 300}}, - config.Exclusion{Name: "bb", Codes: []config.ID{100, 200, 300}}, - config.Exclusion{Name: "cc", Codes: []config.ID{100, 200, 300}}, - }, - }, - section: "fred", - res: "blee", - code: 100, - }, - "plain_match": { - excludes: config.Excludes{ - "fred": { - config.Exclusion{Name: "aa", Codes: []config.ID{100, 200, 300}}, - config.Exclusion{Name: "bb", Codes: []config.ID{100, 200, 300}}, - config.Exclusion{Name: "cc", Codes: []config.ID{100, 200, 300}}, - }, - }, - section: "fred", - res: "aa", - code: 100, - e: true, - }, - "rx_match": { - excludes: config.Excludes{ - "fred": { - config.Exclusion{Name: `rx:\Ablee`, Codes: []config.ID{100, 200, 300}}, - config.Exclusion{Name: "bb", Codes: []config.ID{100, 200, 300}}, - config.Exclusion{Name: "cc", Codes: []config.ID{100, 200, 300}}, - }, - }, - section: "fred", - res: "bleeblah", - code: 100, - e: true, - }, - "rx_no_match": { - excludes: config.Excludes{ - "fred": { - config.Exclusion{Name: `rx:\Ablee`, Codes: []config.ID{100, 200, 300}}, - config.Exclusion{Name: "bb", Codes: []config.ID{100, 200, 300}}, - config.Exclusion{Name: "cc", Codes: []config.ID{100, 200, 300}}, - }, - }, - section: "fred", - res: "blahblee", - code: 100, - }, - "rx_match_nocode": { - excludes: config.Excludes{ - "fred": { - config.Exclusion{Name: "rx:blee", Codes: []config.ID{100, 200, 300}}, - config.Exclusion{Name: "bb", Codes: []config.ID{100, 200, 300}}, - config.Exclusion{Name: "cc", Codes: []config.ID{100, 200, 300}}, - }, - }, - section: "fred", - res: "bleeblah", - code: 101, - }, - } - - for k := range uu { - u := uu[k] - t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, u.excludes.ShouldExclude(u.section, u.res, u.code)) - }) - } -} diff --git a/pkg/config/flags.go b/pkg/config/flags.go index ec7ac436..9212be11 100644 --- a/pkg/config/flags.go +++ b/pkg/config/flags.go @@ -4,45 +4,39 @@ package config import ( + "errors" + "fmt" + "strings" + "k8s.io/cli-runtime/pkg/genericclioptions" ) -// BasicAuth tracks basic authentication. -type BasicAuth struct { - User *string - Password *string -} - -// PushGateway tracks gateway representations. -type PushGateway struct { - Address *string - BasicAuth BasicAuth -} - -func newPushGateway() *PushGateway { - return &PushGateway{ - Address: strPtr(""), - BasicAuth: BasicAuth{User: strPtr(""), Password: strPtr("")}, - } +var outputs = []string{ + "standard", + "jurassic", + "yaml", + "json", + "html", + "junit", + "score", + "prometheus", } // Flags represents Popeye CLI flags. type Flags struct { *genericclioptions.ConfigFlags + PushGateway *PushGateway + S3 *S3Info LintLevel *string Output *string ClearScreen *bool Save *bool OutputFile *string - S3Bucket *string - S3Region *string - S3Endpoint *string CheckOverAllocs *bool AllNamespaces *bool Spinach *string Sections *[]string - PushGateway *PushGateway InClusterName *string StandAlone bool ActiveNamespace *string @@ -58,9 +52,7 @@ func NewFlags() *Flags { AllNamespaces: boolPtr(false), Save: boolPtr(false), OutputFile: strPtr(""), - S3Bucket: strPtr(""), - S3Region: strPtr(""), - S3Endpoint: strPtr(""), + S3: newS3Info(), InClusterName: strPtr(""), ClearScreen: boolPtr(false), CheckOverAllocs: boolPtr(false), @@ -73,6 +65,31 @@ func NewFlags() *Flags { } } +func (f *Flags) Validate() error { + if !IsBoolSet(f.Save) && IsStrSet(f.OutputFile) { + return errors.New("'--save' must be used in conjunction with 'output-file'.") + } + if IsBoolSet(f.Save) && IsStrSet(f.S3.Bucket) { + return errors.New("'--save' cannot be used in conjunction with 's3-bucket'.") + } + + if !in(outputs, f.Output) { + return fmt.Errorf("invalid output format. [%s]", strings.Join(outputs, ",")) + } + + if IsStrSet(f.Output) && *f.Output == "prometheus" { + if f.PushGateway == nil || !IsStrSet(f.PushGateway.URL) { + return errors.New("you must set --push-gtwy-url when prometheus report is enabled") + } + } + + return nil +} + +func (f *Flags) IsPersistent() bool { + return IsBoolSet(f.Save) || IsStrSet(f.OutputFile) || (f.S3 != nil && IsStrSet(f.S3.Bucket)) +} + // OutputFormat returns the report output format. func (f *Flags) OutputFormat() string { if f.Output != nil && *f.Output != "" { @@ -82,17 +99,13 @@ func (f *Flags) OutputFormat() string { return "cool" } -// ---------------------------------------------------------------------------- -// Helpers... - -func boolPtr(b bool) *bool { - return &b -} - -func strPtr(s string) *string { - return &s -} +func (f *Flags) Exhaust() string { + if f.S3 != nil && IsStrSet(f.S3.Bucket) { + return *f.S3.Bucket + } + if IsStrSet(f.OutputFile) { + return *f.OutputFile + } -func intPtr(i int) *int { - return &i + return "" } diff --git a/pkg/config/flags_test.go b/pkg/config/flags_test.go index e94c4460..e232fb54 100644 --- a/pkg/config/flags_test.go +++ b/pkg/config/flags_test.go @@ -4,11 +4,90 @@ package config import ( + "errors" + "net/url" "testing" "github.com/stretchr/testify/assert" ) +func TestIsPersistent(t *testing.T) { + uu := map[string]struct { + f Flags + e bool + }{ + "empty": {}, + "blank": { + f: Flags{}, + }, + "save": { + f: Flags{ + Save: boolPtr(true), + }, + e: true, + }, + "s3": { + f: Flags{ + S3: &S3Info{ + Bucket: strPtr("blah"), + }, + }, + e: true, + }, + "output-file": { + f: Flags{ + OutputFile: strPtr("blah"), + }, + e: true, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, u.f.IsPersistent()) + }) + } +} + +func TestExhaust(t *testing.T) { + uu := map[string]struct { + f Flags + e string + }{ + "empty": {}, + "blank": { + f: Flags{}, + }, + "save": { + f: Flags{ + Save: boolPtr(true), + }, + }, + "s3": { + f: Flags{ + S3: &S3Info{ + Bucket: strPtr("blah"), + }, + }, + e: "blah", + }, + "output-file": { + f: Flags{ + OutputFile: strPtr("blah"), + }, + e: "blah", + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, u.f.Exhaust()) + }) + } +} + func TestOutputFormat(t *testing.T) { uu := map[string]struct { f Flags @@ -27,3 +106,55 @@ func TestOutputFormat(t *testing.T) { }) } } + +func TestParseBucket(t *testing.T) { + var uu = map[string]struct { + uri string + bucket string + key string + err error + }{ + "empty": { + err: errors.New(`invalid S3 bucket URI: ""`), + }, + "no-scheme": { + uri: ":bozo", + err: &url.Error{Op: "parse", URL: ":bozo", Err: errors.New("missing protocol scheme")}, + }, + "s3_bucket": { + uri: "s3://bucketName/", + bucket: "bucketName", + }, + "toast": { + uri: "s4://bucketName/", + err: errors.New(`invalid S3 bucket URI: "s4://bucketName/"`), + }, + "with_full_key": { + uri: "s3://bucketName/fred/blee", + bucket: "bucketName", + key: "fred/blee", + }, + "with_key": { + uri: "bucket/with/subkey", + bucket: "bucket", + key: "with/subkey", + }, + "with_trailer": { + uri: "/bucket/with/leading/slashes/", + bucket: "bucket", + key: "with/leading/slashes", + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + s3 := S3Info{Bucket: &u.uri} + b, k, err := s3.parse() + + assert.Equal(t, u.err, err) + assert.Equal(t, u.bucket, b) + assert.Equal(t, u.key, k) + }) + } +} diff --git a/pkg/config/helpers.go b/pkg/config/helpers.go new file mode 100644 index 00000000..8a6e2aaf --- /dev/null +++ b/pkg/config/helpers.go @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package config + +import "regexp" + +var invalidPathCharsRX = regexp.MustCompile(`[:/]+`) + +func in(oo []string, p *string) bool { + if p == nil { + return true + } + for _, o := range oo { + if o == *p { + return true + } + } + + return false +} + +// SanitizeFileName ensure file spec is valid. +func SanitizeFileName(name string) string { + return invalidPathCharsRX.ReplaceAllString(name, "-") +} + +// IsStrSet checks string option is set. +func IsStrSet(s *string) bool { + return s != nil && *s != "" +} + +// IsBoolSet checks bool option is set +func IsBoolSet(s *bool) bool { + return s != nil && *s +} + +func boolPtr(b bool) *bool { + return &b +} + +func strPtr(s string) *string { + return &s +} + +func intPtr(i int) *int { + return &i +} diff --git a/pkg/config/helpers_test.go b/pkg/config/helpers_test.go new file mode 100644 index 00000000..0bfd1df8 --- /dev/null +++ b/pkg/config/helpers_test.go @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package config + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestIsBoolSet(t *testing.T) { + true, false := true, false + uu := map[string]struct { + b *bool + e bool + }{ + "empty": {}, + "happy": { + b: &true, + e: true, + }, + "false": { + b: &false, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, IsBoolSet(u.b)) + }) + } +} + +func TestIsStrSet(t *testing.T) { + uu := map[string]struct { + s *string + e bool + }{ + "empty": {}, + "happy": { + s: strPtr("fred"), + e: true, + }, + "blank": { + s: strPtr(""), + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, IsStrSet(u.s)) + }) + } +} + +func TestSanitizeFile(t *testing.T) { + uu := map[string]struct { + f, e string + }{ + "empty": {}, + "plain": { + f: "fred-bozo", + e: "fred-bozo", + }, + "full": { + f: "fred/blee///duh::bozo", + e: "fred-blee-duh-bozo", + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, SanitizeFileName(u.f)) + }) + } +} diff --git a/pkg/config/json/schemas/spinach.json b/pkg/config/json/schemas/spinach.json new file mode 100644 index 00000000..e36f4863 --- /dev/null +++ b/pkg/config/json/schemas/spinach.json @@ -0,0 +1,165 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Popeye spinach schema", + "type": "object", + "additionalProperties": false, + "properties": { + "popeye": { + "additionalProperties": false, + "properties": { + "allocations": { + "additionalProperties": false, + "properties": { + "cpu": { + "additionalProperties": false, + "properties": { + "underPercUtilization": {"type": "integer"}, + "overPercUtilization": {"type": "integer"} + } + }, + "memory": { + "additionalProperties": false, + "properties": { + "underPercUtilization": {"type": "integer"}, + "overPercUtilization": {"type": "integer"} + } + } + } + }, + "excludes": { + "additionalProperties": false, + "properties": { + "global": { + "additionalProperties": false, + "properties": { + "fqns": { + "type": "array", + "items": {"type": "string"} + }, + "labels": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { "type": "string"} + } + }, + "annotations": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { "type": "string"} + } + }, + "containers": { + "type": "array", + "items": {"type": "string"} + }, + "codes": { + "type": "array", + "items": {"type": "string"} + } + } + }, + "linters": { + "type": "object", + "additionalProperties": { + "properties": { + "codes": { + "type": "array", + "additionalProperties": false, + "items": {"type": "string"} + }, + "instances": { + "properties": { + "additionalProperties": false, + "fqns": { + "type": "array", + "items": {"type": "string"} + }, + "labels": { + "type": "object", + "additionalProperties": { + "type": "array", + "additionalProperties": false, + "items": { "type": "string"} + } + }, + "annotations": { + "type": "object", + "additionalProperties": { + "type": "array", + "additionalProperties": false, + "items": { "type": "string"} + } + }, + "containers": { + "type": "array", + "additionalProperties": false, + "items": {"type": "string"} + }, + "codes": { + "type": "array", + "additionalProperties": false, + "items": {"type": "string"} + } + } + } + } + } + } + } + }, + "resources": { + "additionalProperties": false, + "properties": { + "node": { + "additionalProperties": false, + "properties": { + "limits": { + "type": "object", + "properties": { + "cpu": {"type": "integer" }, + "memory": {"type": "integer" } + } + } + } + }, + "pod": { + "additionalProperties": false, + "properties": { + "limits": { + "type": "object", + "properties": { + "cpu": {"type": "integer" }, + "memory": {"type": "integer" } + } + }, + "restarts": {"type": "integer"} + } + } + } + }, + "overrides": { + "additionalProperties": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "message": {"type": "string"}, + "severity": {"type": "integer"} + } + } + } + }, + "registries": { + "additionalProperties": { + "type": "array", + "items": {"type": "string"} + } + } + } + } + }, + "required": ["popeye"] +} diff --git a/pkg/config/json/testdata/1.yaml b/pkg/config/json/testdata/1.yaml new file mode 100644 index 00000000..bf327ecd --- /dev/null +++ b/pkg/config/json/testdata/1.yaml @@ -0,0 +1,61 @@ +# A Sample AKS Popeye configuration. +popeye: + allocations: + cpu: + underPercUtilization: 200 + overPercUtilization: 50 + memory: + underPercUtilization: 200 + overPercUtilization: 50 + + excludes: + # all linters + global: + fqns: [ns1, ns2, rx:bozo] + labels: + l1: [lv1, lv2] + l2: [lv1, lv2] + annotations: + a1: [av1, av2] + a2: [bv1, bv2] + containers: [c1, c2, rx:c3] + codes: ["100", "200", "rx:^3"] + + # Specific linters + linters: + pods: + - labels: + l1: [v1,v2] + containers: [c1, c2] + codes: ["101", "200"] + - fqns: [n1, n2, n3] + + configmaps: + - labels: + l1: [v1,v2] + containers: [c1, c2] + codes: ["101", "200"] + + resources: + node: + limits: + cpu: 90 + memory: 80 + + pod: + restarts: 3 + limits: + cpu: 80 + memory: 75 + + overrides: + - code: 206 + message: blee + severity: 1 + - code: 210 + message: fred + severity: 2 + + registries: + - docker.io + - pocker.io \ No newline at end of file diff --git a/pkg/config/json/testdata/toast.yaml b/pkg/config/json/testdata/toast.yaml new file mode 100644 index 00000000..0aeabaf1 --- /dev/null +++ b/pkg/config/json/testdata/toast.yaml @@ -0,0 +1,60 @@ +# A Sample AKS Popeye configuration. +popeye: + allocations: + cpu: + # Checks if cpu is under allocated by more than 200% at current load. + underPercUtilization: 200 + # Checks if cpu is over allocated by more than 50% at current load. + overPercUtilization: 50 + memory: + # Checks if mem is under allocated by more than 200% at current load. + underPercUtilization: 200 + # Checks if mem is over allocated by more than 50% at current load. + overPercUtilization: 50 + # Excludes define rules to exampt resources from sanitization + excludes: + rbac.authorization.k8s.io/v1/clusterroles: + - name: omsagent-reader + codes: + - 400 + - name: rx:system + codes: + - 400 + - name: admin + codes: + - 400 + - name: edit + codes: + - 400 + - name: view + codes: + - 400 + - name: cluster-admin + codes: + - 400 + + resources: + # Nodes specific sanitization + node: + limits: + cpu: 90 + memory: 80 + + # Pods specific sanitization + pod: + limits: + # Fail if cpu is over 80% + cpu: 80 + # Fail if pod mem is over 75% + memory: 75 + # Fail if more than 3 restarts on any pods + restarts: 3 + + # Code specifies a custom severity level ie critical=3, warn=2, info=1 + overrides: + - code: 206 + message: blee + severity: 1 + - code: 210 + message: fred + severity: 2 diff --git a/pkg/config/json/validator.go b/pkg/config/json/validator.go new file mode 100644 index 00000000..d15d9c5b --- /dev/null +++ b/pkg/config/json/validator.go @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package json + +import ( + "cmp" + _ "embed" + "errors" + "fmt" + "slices" + + "github.com/rs/zerolog/log" + "github.com/xeipuuv/gojsonschema" + "gopkg.in/yaml.v3" +) + +// SpinachSchema describes spinach schema. +const SpinachSchema = "spinach.json" + +var ( + //go:embed schemas/spinach.json + spinachSchema string +) + +// Validator tracks schemas validation. +type Validator struct { + schemas map[string]gojsonschema.JSONLoader + loader *gojsonschema.SchemaLoader +} + +// NewValidator returns a new instance. +func NewValidator() *Validator { + v := Validator{ + schemas: map[string]gojsonschema.JSONLoader{ + SpinachSchema: gojsonschema.NewStringLoader(spinachSchema), + }, + } + v.register() + + return &v +} + +// Init initializes the schemas. +func (v *Validator) register() { + v.loader = gojsonschema.NewSchemaLoader() + v.loader.Validate = true + for k, s := range v.schemas { + if err := v.loader.AddSchema(k, s); err != nil { + log.Error().Err(err).Msgf("schema initialization failed: %q", k) + } + } +} + +// Validate runs document thru given schema validation. +func (v *Validator) Validate(k string, bb []byte) error { + var m interface{} + err := yaml.Unmarshal(bb, &m) + if err != nil { + return err + } + + s, ok := v.schemas[k] + if !ok { + return fmt.Errorf("no schema found for: %q", k) + } + result, err := gojsonschema.Validate(s, gojsonschema.NewGoLoader(m)) + if err != nil { + return err + } + if result.Valid() { + return nil + } + + slices.SortFunc(result.Errors(), func(a, b gojsonschema.ResultError) int { + return cmp.Compare(a.Description(), b.Description()) + }) + var errs error + for _, re := range result.Errors() { + errs = errors.Join(errs, errors.New(re.Description())) + } + + return errs +} diff --git a/pkg/config/json/validator_test.go b/pkg/config/json/validator_test.go new file mode 100644 index 00000000..949ce98f --- /dev/null +++ b/pkg/config/json/validator_test.go @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package json_test + +import ( + "os" + "testing" + + "github.com/derailed/popeye/pkg/config/json" + "github.com/stretchr/testify/assert" +) + +func TestValidateSpinach(t *testing.T) { + uu := map[string]struct { + f string + err string + }{ + "happy": { + f: "testdata/1.yaml", + }, + "toast": { + f: "testdata/toast.yaml", + err: "Additional property rbac.authorization.k8s.io/v1/clusterroles is not allowed", + }, + } + + v := json.NewValidator() + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + bb, err := os.ReadFile(u.f) + assert.NoError(t, err) + if err := v.Validate(json.SpinachSchema, bb); err != nil { + assert.Equal(t, u.err, err.Error()) + } + }) + } +} diff --git a/pkg/config/no.go b/pkg/config/no.go index 5b083f02..8c1c9ba4 100644 --- a/pkg/config/no.go +++ b/pkg/config/no.go @@ -16,8 +16,6 @@ type Limits struct { // Node tracks node configurations. type Node struct { - Excludes `yaml:"exclude"` - Limits Limits `yaml:"limits"` } diff --git a/pkg/config/po.go b/pkg/config/po.go index 6c827818..b52a3574 100644 --- a/pkg/config/po.go +++ b/pkg/config/po.go @@ -9,7 +9,6 @@ const defaultRestarts = 5 type Pod struct { Restarts int `yaml:"restarts"` Limits Limits `yaml:"limits"` - Excludes `yaml:"exclude"` } // NewPod create a new pod configuration. diff --git a/pkg/config/popeye.go b/pkg/config/popeye.go index d9867b24..86af2135 100644 --- a/pkg/config/popeye.go +++ b/pkg/config/popeye.go @@ -4,8 +4,7 @@ package config import ( - "fmt" - "strconv" + "github.com/derailed/popeye/internal/rules" ) const ( @@ -16,18 +15,6 @@ const ( ) type ( - // ID represents a sanitizer code indentifier. - ID int - - // Glossary represents a collection of codes. - Glossary map[ID]*Code - - // Code represents a sanitizer code. - Code struct { - Message string `yaml:"message"` - Severity Level `yaml:"severity"` - } - // AllocationLimits tracks limit thresholds cpu and memory thresholds. AllocationLimits struct { CPU Allocations `yaml:"cpu"` @@ -40,14 +27,26 @@ type ( OverPerc int `yanl:"overPercUtilization"` } + Resources struct { + Node Node `yaml:"node"` + Pod Pod `yaml:"pod"` + } + // Popeye tracks Popeye configuration options. Popeye struct { + // AllocationLimits tracks global resource allocations. AllocationLimits `yaml:"allocations"` - Excludes `yaml:"excludes"` - Node Node `yaml:"node"` - Pod Pod `yaml:"pod"` - Codes Glossary `yaml:"codes"` + // Excludes tracks linter exclusions. + Exclusions rules.Exclusions `yaml:"excludes"` + + // Resources tracks cpu/mem limits. + Resources Resources `yaml:"resources"` + + // Codes provides to override codes severity. + Overrides rules.Overrides `yaml:"overrides"` + + // Registries tracks allowed docker registries. Registries []string `yaml:"registries"` } ) @@ -56,21 +55,23 @@ type ( func NewPopeye() Popeye { return Popeye{ AllocationLimits: AllocationLimits{ - CPU: Allocations{UnderPerc: defaultUnderPerc, OverPerc: defaultOverPerc}, - MEM: Allocations{UnderPerc: defaultUnderPerc, OverPerc: defaultOverPerc}, + CPU: Allocations{ + UnderPerc: defaultUnderPerc, + OverPerc: defaultOverPerc, + }, + MEM: Allocations{ + UnderPerc: defaultUnderPerc, + OverPerc: defaultOverPerc, + }, + }, + Exclusions: rules.NewExclusions(), + Resources: Resources{ + Node: newNode(), + Pod: newPod(), }, - Excludes: newExcludes(), - Node: newNode(), - Pod: newPod(), - Registries: []string{}, } } -// Format hydrates a message with arguments. -func (c *Code) Format(code ID, args ...interface{}) string { - msg := "[POP-" + strconv.Itoa(int(code)) + "] " - if len(args) == 0 { - return msg + c.Message - } - return msg + fmt.Sprintf(c.Message, args...) +func (p Popeye) Match(spec rules.Spec) bool { + return p.Exclusions.Match(spec) } diff --git a/pkg/config/popeye_test.go b/pkg/config/popeye_test.go index e479efee..4deb29f7 100644 --- a/pkg/config/popeye_test.go +++ b/pkg/config/popeye_test.go @@ -6,13 +6,27 @@ package config import ( "testing" + "github.com/derailed/popeye/internal/rules" + "github.com/derailed/popeye/types" "github.com/stretchr/testify/assert" ) func TestNewPopeye(t *testing.T) { p := NewPopeye() - assert.False(t, p.ShouldExclude("node", "n1", 600)) - assert.Equal(t, 5, p.Pod.Restarts) - assert.False(t, p.ShouldExclude("namespace", "kube-public", 100)) + assert.Equal(t, 5, p.Resources.Pod.Restarts) + + ok := p.Match(rules.Spec{ + GVR: types.NewGVR("v1/nodes"), + FQN: "-/n1", + Code: 600, + }) + assert.False(t, ok) + + ok = p.Match(rules.Spec{ + GVR: types.NewGVR("v1/namespaces"), + FQN: "kube-public", + Code: 100, + }) + assert.False(t, ok) } diff --git a/pkg/config/prom.go b/pkg/config/prom.go new file mode 100644 index 00000000..d5bbd602 --- /dev/null +++ b/pkg/config/prom.go @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package config + +// BasicAuth tracks basic authentication. +type BasicAuth struct { + User *string + Password *string +} + +func newBasicAuth() BasicAuth { + return BasicAuth{ + User: strPtr(""), + Password: strPtr(""), + } +} + +// PushGateway tracks prometheus gateway representations. +type PushGateway struct { + URL *string + BasicAuth BasicAuth +} + +func newPushGateway() *PushGateway { + return &PushGateway{ + URL: strPtr(""), + BasicAuth: newBasicAuth(), + } +} diff --git a/pkg/config/s3.go b/pkg/config/s3.go new file mode 100644 index 00000000..9edd5a30 --- /dev/null +++ b/pkg/config/s3.go @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package config + +import ( + "fmt" + "io" + "net/url" + "path/filepath" + "strings" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/s3/s3manager" + "github.com/rs/zerolog/log" +) + +type s3Logger struct{} + +func (s *s3Logger) Log(mm ...any) { + for _, m := range mm { + log.Debug().Msgf("S3 %s", m) + } +} + +type S3Info struct { + Bucket *string + Region *string + Endpoint *string +} + +func (s *S3Info) Upload(asset string, rwc io.ReadWriteCloser) error { + defer rwc.Close() + + log.Debug().Msgf("S3 bucket path: %q", asset) + bucket, key, err := s.parse() + if err != nil { + return err + } + + // Create a single AWS session (we can re use this if we're uploading many files) + session, err := session.NewSession(&aws.Config{ + Logger: &s3Logger{}, + LogLevel: aws.LogLevel(aws.LogDebugWithRequestErrors), + Endpoint: s.Endpoint, + Region: s.Region, + }) + if err != nil { + return err + } + + // Create an uploader with the session and default options + uploader := s3manager.NewUploader(session) + // Upload input parameters + upParams := s3manager.UploadInput{ + Bucket: aws.String(bucket), + Key: aws.String(filepath.Join(key, asset)), + Body: rwc, + } + // Perform an upload. + if _, err = uploader.Upload(&upParams); err != nil { + return err + } + + return nil +} + +func (s *S3Info) parse() (string, string, error) { + if !IsStrSet(s.Bucket) { + return "", "", fmt.Errorf("invalid S3 bucket URI: %q", *s.Bucket) + } + u, err := url.Parse(*s.Bucket) + if err != nil { + return "", "", err + } + switch u.Scheme { + // s3://bucket or s3://bucket/ + case "s3": + var key string + if u.Path != "" { + key = strings.Trim(u.Path, "/") + } + return u.Host, key, nil + // bucket/ or bucket/path/to/key + case "": + tokens := strings.SplitAfterN(strings.Trim(u.Path, "/"), "/", 2) + if len(tokens) == 0 { + return "", "", fmt.Errorf("invalid S3 bucket URI: %q", u.String()) + } + key, bucket := "", strings.Trim(tokens[0], "/") + if len(tokens) > 1 { + key = tokens[1] + } + return bucket, key, nil + default: + return "", "", fmt.Errorf("invalid S3 bucket URI: %q", u.String()) + } +} + +func newS3Info() *S3Info { + return &S3Info{ + Bucket: strPtr(""), + Region: strPtr(""), + Endpoint: strPtr(""), + } +} diff --git a/pkg/config/testdata/sp_old.yml b/pkg/config/testdata/sp-toast.yml similarity index 98% rename from pkg/config/testdata/sp_old.yml rename to pkg/config/testdata/sp-toast.yml index e94d55f4..297b338b 100644 --- a/pkg/config/testdata/sp_old.yml +++ b/pkg/config/testdata/sp-toast.yml @@ -1,5 +1,5 @@ # A Sample Popeye configuration. -popeye: +popeye1: # Allocations ratios current to resources. allocations: cpu: diff --git a/pkg/config/testdata/sp1.yml b/pkg/config/testdata/sp1.yml index 40fa8aee..6a96795d 100644 --- a/pkg/config/testdata/sp1.yml +++ b/pkg/config/testdata/sp1.yml @@ -1,40 +1,45 @@ -# A Sample Popeye configuration. popeye: - # Allocations ratios current to resources. allocations: cpu: - over: 200 - under: 50 + overPercUtilization: 200 + underPercUtilization: 50 memory: - over: 200 - under: 50 + overPercUtilization: 200 + underPercUtilization: 50 - # Excludes excludes: - node: - - name: n1 - codes: - - 100 - namespace: - - name: kube-system - - name: kube-node-lease - - name: kube-public - - name: istio-system - service: - - name: default/dictionary + global: + fqns: [rx:^ns1, rx:^ns2] + + linters: + nodes: + instances: + - labels: + k8s-app: [app1] + codes: ["100"] - # Node... - node: - limits: - cpu: 90 - memory: 80 + namespaces: + instances: + - labels: + group: [ns1] + codes: ["100"] - # Pod... - pod: - limits: - cpu: 80 - memory: 75 - restarts: 3 + services: + instances: + - labels: + group: [ns1] + codes: ["100"] + + resources: + node: + limits: + cpu: 90 + memory: 80 + pod: + restarts: 3 + limits: + cpu: 80 + memory: 75 registries: - - docker.io \ No newline at end of file + - docker.io \ No newline at end of file diff --git a/pkg/config/testdata/sp2.yml b/pkg/config/testdata/sp2.yml index b0cc2ad2..1cb7abc9 100644 --- a/pkg/config/testdata/sp2.yml +++ b/pkg/config/testdata/sp2.yml @@ -3,33 +3,8 @@ popeye: # Allocations ratios current to resources. allocations: cpu: - over: 200 - under: 50 + overPercUtilization: 200 + underPercUtilization: 50 memory: - over: 200 - under: 50 - - # Excludes - excludes: - node: - - name: n1 - namespace: - - name: kube-system - - name: kube-node-lease - - name: kube-public - - name: istio-system - service: - - name: default/dictionary - - # Node... - node: - limits: - cpu: 0 - memory: 0 - - # Pod... - pod: - limits: - cpu: 0 - memory: 0 - restarts: 3 + overPercUtilization: 200 + underPercUtilization: 50 \ No newline at end of file diff --git a/pkg/config/testdata/sp3.yml b/pkg/config/testdata/sp3.yml new file mode 100644 index 00000000..bc75fd8e --- /dev/null +++ b/pkg/config/testdata/sp3.yml @@ -0,0 +1,60 @@ +popeye: + allocations: + cpu: + overPercUtilization: 200 + underPercUtilization: 50 + memory: + overPercUtilization: 200 + underPercUtilization: 50 + + excludes: + global: + fqns: [rx:^gns1, rx:^gns2] + labels: + l1: [a, aa, aaa] + l2: [f, ff, fff] + annotations: + a1: [a, b,c] + a2: [a1, b1, c1] + + linters: + configmaps: + + secrets: + instances: + - fqns: [rx:fred] + + nodes: + instances: + - annotations: + a1: [b1] + codes: ["100"] + + pods: + instances: + - fqns: [rx:^istio] + - labels: + kube-system: [fred] + codes: ["300"] + - fqns: [rx:^ns1] + containers: [c1, c2, c3] + + services: + instances: + - labels: + default: [dictionary] + codes: ["100"] + + resources: + node: + limits: + cpu: 90 + memory: 80 + pod: + restarts: 3 + limits: + cpu: 80 + memory: 75 + + registries: + - docker.io \ No newline at end of file diff --git a/pkg/config/types.go b/pkg/config/types.go new file mode 100644 index 00000000..8b12c1e1 --- /dev/null +++ b/pkg/config/types.go @@ -0,0 +1,4 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package config diff --git a/pkg/helpers.go b/pkg/helpers.go new file mode 100644 index 00000000..43347bb8 --- /dev/null +++ b/pkg/helpers.go @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package pkg + +import ( + "fmt" + "io" + "os" + "path/filepath" +) + +func ensureDir(path string, mod os.FileMode) error { + dir, err := filepath.Abs(path) + if err != nil { + return err + } + _, err = os.Stat(dir) + if err == nil || !os.IsNotExist(err) { + return nil + } + if err = os.MkdirAll(dir, mod); err != nil { + return fmt.Errorf("fail to create popeye scan dump dir: %w", err) + } + + return nil +} + +func dumpDir() string { + if d := os.Getenv("POPEYE_REPORT_DIR"); d != "" { + return d + } + + return filepath.Join(os.TempDir(), "popeye") +} + +type readWriteCloser struct { + io.ReadWriter +} + +// Close close read stream. +func (readWriteCloser) Close() error { + return nil +} + +// NopCloser fake closer. +func NopCloser(i io.ReadWriter) io.ReadWriteCloser { + return &readWriteCloser{i} +} diff --git a/pkg/popeye.go b/pkg/popeye.go index be1f4137..19efbeb4 100644 --- a/pkg/popeye.go +++ b/pkg/popeye.go @@ -12,57 +12,58 @@ import ( "fmt" "io" "net/http" - "net/url" "os" "path/filepath" "runtime/debug" - "strings" "time" - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/session" - "github.com/aws/aws-sdk-go/service/s3/s3manager" "github.com/derailed/popeye/internal" "github.com/derailed/popeye/internal/client" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/db/schema" "github.com/derailed/popeye/internal/issues" "github.com/derailed/popeye/internal/report" + "github.com/derailed/popeye/internal/rules" "github.com/derailed/popeye/internal/scrub" "github.com/derailed/popeye/pkg/config" "github.com/derailed/popeye/types" + "github.com/hashicorp/go-memdb" "github.com/prometheus/common/expfmt" "github.com/rs/zerolog" "github.com/rs/zerolog/log" ) -const outFmt = "sanitizer_%s_%d.%s" +const ( + dumpFileFMT = "popeye-scan-%s-%d.%s" + defaultFileMode = 0755 + defaultInstance = "popeye" + defaultGtwyTimeout = 30 * time.Second +) var ( // LogFile the path to our logs. LogFile = filepath.Join(os.TempDir(), "popeye.log") - // DumpDir indicates a directory location for sanitizer reports. - DumpDir = dumpDir() - // ErrUnknownS3BucketProtocol defines the error if we can't parse the S3 URI - ErrUnknownS3BucketProtocol = errors.New("invalid S3 URI: hostname not valid") - gvrs internal.GVRs + // DumpDir track scan report directory location. + DumpDir = dumpDir() ) -type scrubFn func(context.Context, *scrub.Cache, *issues.Codes) scrub.Sanitizer - type run struct { outcome issues.Outcome - gvr client.GVR + gvr types.GVR } -// Popeye represents a kubernetes linter/sanitizer. +// Popeye represents a kubernetes linter/linter. type Popeye struct { factory types.Factory + db *db.DB config *config.Config outputTarget io.ReadWriteCloser log *zerolog.Logger flags *config.Flags builder *report.Builder aliases *internal.Aliases + codes *issues.Codes } // NewPopeye returns a new instance. @@ -77,9 +78,19 @@ func NewPopeye(flags *config.Flags, log *zerolog.Logger) (*Popeye, error) { log: log, flags: flags, builder: report.NewBuilder(), + aliases: internal.NewAliases(), }, nil } +func (p *Popeye) initDB() (*db.DB, error) { + d, err := memdb.NewMemDB(schema.Init()) + if err != nil { + return nil, err + } + + return db.NewDB(d), nil +} + // Init configures popeye prior to sanitization. func (p *Popeye) Init() error { if p.factory == nil { @@ -87,21 +98,19 @@ func (p *Popeye) Init() error { return err } } - rev, err := p.revision() - if err != nil { + if err := p.aliases.Init(p.client()); err != nil { return err } - gvrs = p.scannedGVRs(rev) - p.aliases = internal.NewAliases() - if err := p.aliases.Init(p.factory, gvrs); err != nil { + var err error + p.db, err = p.initDB() + if err != nil { return err } - - if !isSet(p.flags.Save) { + if !config.IsBoolSet(p.flags.Save) { return p.ensureOutput() } - if err := ensurePath(DumpDir, 0755); err != nil { + if err := ensureDir(DumpDir, defaultFileMode); err != nil { return err } @@ -113,49 +122,6 @@ func (p *Popeye) SetFactory(f types.Factory) { p.factory = f } -func (p *Popeye) scannedGVRs(rev *client.Revision) internal.GVRs { - mm := internal.GVRs{ - internal.LrGVR: "v1/limitranges", - internal.SvcGVR: "v1/services", - internal.EpGVR: "v1/endpoints", - internal.NoGVR: "v1/nodes", - internal.NsGVR: "v1/namespaces", - internal.PoGVR: "v1/pods", - internal.CmGVR: "v1/configmaps", - internal.SecGVR: "v1/secrets", - internal.SaGVR: "v1/serviceaccounts", - internal.PvGVR: "v1/persistentvolumes", - internal.PvcGVR: "v1/persistentvolumeclaims", - internal.DpGVR: "apps/v1/deployments", - internal.RsGVR: "apps/v1/replicasets", - internal.DsGVR: "apps/v1/daemonsets", - internal.StsGVR: "apps/v1/statefulsets", - internal.NpGVR: "networking.k8s.io/v1/networkpolicies", - internal.CrGVR: "rbac.authorization.k8s.io/v1/clusterroles", - internal.CrbGVR: "rbac.authorization.k8s.io/v1/clusterrolebindings", - internal.RoGVR: "rbac.authorization.k8s.io/v1/roles", - internal.RobGVR: "rbac.authorization.k8s.io/v1/rolebindings", - internal.IngGVR: "networking.k8s.io/v1/ingresses", - internal.PdbGVR: "policy/v1/poddisruptionbudgets", - internal.HpaGVR: "autoscaling/v2/horizontalpodautoscalers", - } - - if rev.Minor < 18 { - mm[internal.IngGVR] = "networking.k8s.io/v1beta1/ingresses" - } - if rev.Minor < 21 { - mm[internal.PdbGVR] = "policy/v1beta1/poddisruptionbudgets" - } - if rev.Minor < 23 { - mm[internal.HpaGVR] = "autoscaling/v1/horizontalpodautoscalers" - } - if rev.Minor < 25 { - mm[internal.PspGVR] = "policy/v1beta1/podsecuritypolicies" - } - - return mm -} - func (p *Popeye) initFactory() error { clt, err := client.InitConnectionOrDie(client.NewConfig(p.flags.ConfigFlags)) if err != nil { @@ -168,23 +134,18 @@ func (p *Popeye) initFactory() error { return nil } - info, err := p.factory.Client().ServerVersion() - if err != nil { - return err - } - rev, err := client.NewRevision(info) - if err != nil { - return err - } - ns := client.AllNamespaces if p.flags.ConfigFlags.Namespace != nil { ns = *p.flags.ConfigFlags.Namespace } f.Start(ns) - for _, gvr := range p.scannedGVRs(rev) { - ok, err := clt.CanI(client.AllNamespaces, gvr, types.ReadAllAccess) + for k, gvr := range internal.Glossary { + if gvr == types.BlankGVR { + log.Debug().Msgf("Skipping linter %q", k) + continue + } + ok, err := clt.CanI(client.AllNamespaces, gvr, types.ReadAllAccess...) if !ok || err != nil { return fmt.Errorf("Current user does not have read access for resource %q -- %w", gvr, err) } @@ -197,142 +158,100 @@ func (p *Popeye) initFactory() error { return nil } -func (p *Popeye) revision() (*client.Revision, error) { - info, err := p.factory.Client().ServerVersion() - if err != nil { - return nil, err - } - - return client.NewRevision(info) -} - -func (p *Popeye) sanitizers(rev *client.Revision) map[string]scrubFn { - mm := map[string]scrubFn{ - "cluster": scrub.NewCluster, - gvrs[internal.CmGVR]: scrub.NewConfigMap, - gvrs[internal.NsGVR]: scrub.NewNamespace, - gvrs[internal.NoGVR]: scrub.NewNode, - gvrs[internal.PoGVR]: scrub.NewPod, - gvrs[internal.PvGVR]: scrub.NewPersistentVolume, - gvrs[internal.PvcGVR]: scrub.NewPersistentVolumeClaim, - gvrs[internal.SecGVR]: scrub.NewSecret, - gvrs[internal.SvcGVR]: scrub.NewService, - gvrs[internal.SaGVR]: scrub.NewServiceAccount, - gvrs[internal.DsGVR]: scrub.NewDaemonSet, - gvrs[internal.DpGVR]: scrub.NewDeployment, - gvrs[internal.RsGVR]: scrub.NewReplicaSet, - gvrs[internal.StsGVR]: scrub.NewStatefulSet, - gvrs[internal.NpGVR]: scrub.NewNetworkPolicy, - gvrs[internal.IngGVR]: scrub.NewIngress, - gvrs[internal.CrGVR]: scrub.NewClusterRole, - gvrs[internal.CrbGVR]: scrub.NewClusterRoleBinding, - gvrs[internal.RoGVR]: scrub.NewRole, - gvrs[internal.RobGVR]: scrub.NewRoleBinding, - gvrs[internal.PdbGVR]: scrub.NewPodDisruptionBudget, - gvrs[internal.HpaGVR]: scrub.NewHorizontalPodAutoscaler, - } - - return mm -} - -// SetOutputTarget sets up a new output stream writer. -func (p *Popeye) SetOutputTarget(s io.ReadWriteCloser) { - p.outputTarget = s +func (p *Popeye) clusterPath() string { + return filepath.Join( + config.SanitizeFileName(p.client().ActiveCluster()), + config.SanitizeFileName(p.client().ActiveContext()), + ) } -// Sanitize scans a cluster for potential issues. -func (p *Popeye) Sanitize() (int, int, error) { +// Lint scans a cluster for potential issues. +func (p *Popeye) Lint() (int, int, error) { defer func() { switch { - case isSet(p.flags.Save): - if err := p.outputTarget.Close(); err != nil { - log.Fatal().Err(err).Msg("Closing report") - } - case isSetStr(p.flags.S3Bucket): - bucket, key, err := parseBucket(*p.flags.S3Bucket) - if err != nil { - log.Fatal().Err(err).Msg("Parse S3 bucket URI") - } - - // Create a single AWS session (we can re use this if we're uploading many files) - s, err := session.NewSession(&aws.Config{ - LogLevel: aws.LogLevel(aws.LogDebugWithRequestErrors), - Region: p.flags.S3Region, - Endpoint: p.flags.S3Endpoint, - }) - if err != nil { - log.Fatal().Err(err).Msg("Create S3 Session") - } - - // Create an uploader with the session and default options - uploader := s3manager.NewUploader(s) - // Upload input parameters - upParams := &s3manager.UploadInput{ - Bucket: aws.String(bucket), - Key: aws.String(key + "/" + p.fileName()), - Body: p.outputTarget, + case config.IsBoolSet(p.flags.Save): + if p.outputTarget != nil { + p.outputTarget.Close() } - - // Perform an upload. - if _, err = uploader.Upload(upParams); err != nil { - log.Fatal().Err(err).Msg("S3 Upload") + case config.IsStrSet(p.flags.S3.Bucket): + asset := filepath.Join(p.clusterPath(), p.scanFileName()) + if err := p.flags.S3.Upload(asset, p.outputTarget); err != nil { + log.Fatal().Msgf("S3 upload failed: %s", err) } } }() - if err := client.Load(p.factory); err != nil { + errCount, score, err := p.lint() + if err != nil { return 0, 0, err } + log.Debug().Msgf("Score [%d]", score) - errCount, score, err := p.sanitize() - if err != nil { - return 0, 0, err + if config.IsStrSet(p.flags.S3.Bucket) { } - return errCount, score, p.dump(true) + return errCount, score, p.dump(true, p.flags.Exhaust()) } -func (p *Popeye) sanitize() (int, int, error) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() +func (p *Popeye) buildCtx(ctx context.Context) context.Context { ctx = context.WithValue(ctx, internal.KeyOverAllocs, *p.flags.CheckOverAllocs) ctx = context.WithValue(ctx, internal.KeyFactory, p.factory) - if version, err := p.factory.Client().ServerVersion(); err == nil { + ctx = context.WithValue(ctx, internal.KeyConfig, p.config) + if version, err := p.client().ServerVersion(); err == nil { ctx = context.WithValue(ctx, internal.KeyVersion, version) } - - codes, err := issues.LoadCodes() + ns, err := p.client().Config().CurrentNamespaceName() if err != nil { - return 0, 0, err + ns = client.AllNamespaces } - codes.Refine(p.config.Codes) + ctx = context.WithValue(ctx, internal.KeyNamespace, ns) - c := make(chan run, 2) - var total, errCount int - var nodeGVR = client.NewGVR("v1/nodes") - cache := scrub.NewCache(p.factory, p.config) + return ctx +} + +func (p *Popeye) lint() (int, int, error) { + defer func(t time.Time) { + log.Debug().Msgf("Lint %v", time.Since(t)) + }(time.Now()) - rev, err := p.revision() + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + ctx = p.buildCtx(ctx) + + codes, err := issues.LoadCodes() if err != nil { return 0, 0, err } - for k, fn := range p.sanitizers(rev) { - gvr := client.NewGVR(k) + codes.Refine(p.config.Overrides) + p.codes = codes + + cache := scrub.NewCache(p.db, p.factory, p.config) + + runners := make(map[types.GVR]scrub.Linter) + for k, fn := range scrub.Scrubers() { + gvr, ok := internal.Glossary[k] + if !ok || gvr == types.BlankGVR { + continue + } if p.aliases.Exclude(gvr, p.config.Sections()) { continue } - // Skip node sanitizer if active namespace is set. - if gvr == nodeGVR && p.factory.Client().ActiveNamespace() != client.AllNamespaces { + // Skip node linter if active namespace is set. + if gvr == internal.Glossary[internal.NO] && p.client().ActiveNamespace() != client.AllNamespaces { continue } - total++ - ctx = context.WithValue(ctx, internal.KeyRunInfo, internal.RunInfo{Section: gvr.R(), SectionGVR: gvr}) - go p.sanitizer(ctx, gvr, fn, c, cache, codes) + runners[gvr] = fn(ctx, cache, codes) } + total, errCount := len(runners), 0 if total == 0 { return 0, 0, nil } + c := make(chan run, 2) + for gvr, r := range runners { + ctx = context.WithValue(ctx, internal.KeyRunInfo, internal.NewRunInfo(gvr)) + go p.runLinter(ctx, gvr, r, c, cache, codes) + } var score, count int for run := range c { @@ -353,7 +272,7 @@ func (p *Popeye) sanitize() (int, int, error) { return errCount, score / count, nil } -func (p *Popeye) sanitizer(ctx context.Context, gvr client.GVR, f scrubFn, c chan run, cache *scrub.Cache, codes *issues.Codes) { +func (p *Popeye) runLinter(ctx context.Context, gvr types.GVR, l scrub.Linter, c chan run, cache *scrub.Cache, codes *issues.Codes) { defer func() { if e := recover(); e != nil { log.Error().Msgf("Popeye CHOKED! %#v", e) @@ -361,16 +280,15 @@ func (p *Popeye) sanitizer(ctx context.Context, gvr client.GVR, f scrubFn, c cha } }() - resource := f(ctx, cache, codes) - if err := resource.Sanitize(ctx); err != nil { + if err := l.Lint(ctx); err != nil { p.builder.AddError(err) } - o := resource.Outcome().Filter(config.Level(p.config.LinterLevel())) + o := l.Outcome().Filter(rules.Level(p.config.LintLevel)) c <- run{gvr: gvr, outcome: o} } func (p *Popeye) dumpJunit() error { - res, err := p.builder.ToJunit(config.Level(p.config.LinterLevel())) + res, err := p.builder.ToJunit(rules.Level(p.config.LintLevel)) if err != nil { return err } @@ -422,17 +340,17 @@ func (p *Popeye) dumpScore() error { return nil } -func (p *Popeye) dumpStd(mode, header bool) error { +func (p *Popeye) dumpStd(header bool) error { var ( w = bufio.NewWriter(p.outputTarget) - s = report.NewSanitizer(w, mode) + s = report.New(w, p.flags.OutputFormat() == report.JurassicFormat) ) if header { p.builder.PrintHeader(s) } - p.builder.PrintContextInfo(s, p.factory.Client().ActiveContext(), p.factory.Client().HasMetrics()) - p.builder.PrintReport(config.Level(p.config.LinterLevel()), s) + p.builder.PrintClusterInfo(s, p.client().HasMetrics()) + p.builder.PrintReport(rules.Level(p.config.LintLevel), s) p.builder.PrintSummary(s) return w.Flush() @@ -444,79 +362,108 @@ func (p *Popeye) Do(req *http.Request) (*http.Response, error) { // Avoid panic when the pusher tries to close the body Body: io.NopCloser(bytes.NewBufferString("Dummy response from file writer")), } - out, err := io.ReadAll(req.Body) if err != nil { resp.StatusCode = http.StatusInternalServerError return &resp, err } - fmt.Fprintf(p.outputTarget, "%s\n", out) - resp.StatusCode = http.StatusOK + return &resp, nil } -func (p *Popeye) dumpPrometheus() error { +func (p *Popeye) client() types.Connection { + return p.factory.Client() +} + +func (p *Popeye) dumpPrometheus(ctx context.Context, asset string, persist bool) error { + if !config.IsStrSet(p.flags.PushGateway.URL) { + return nil + } + + instance := defaultInstance + if config.IsStrSet(p.flags.InClusterName) { + instance += "-" + *p.flags.InClusterName + } + pusher := p.builder.ToPrometheus( p.flags.PushGateway, - p.factory.Client().ActiveNamespace(), + instance, + p.client().ActiveNamespace(), + asset, + p.codes.Glossary, ) - // Enable saving to file - if isSet(p.flags.Save) || isSetStr(p.flags.S3Bucket) { - pusher.Client(p) - pusher.Format(expfmt.FmtText) + if persist { + pusher = pusher.Client(p) + pusher = pusher.Format(expfmt.FmtText) } - return pusher.Add() + return pusher.AddContext(ctx) } -func (p *Popeye) fetchContextName() string { +func (p *Popeye) fetchClusterName() string { switch { - case p.factory.Client().ActiveContext() != "": - return p.factory.Client().ActiveContext() - case p.flags.InClusterName != nil && *p.flags.InClusterName != "": + case config.IsStrSet(p.flags.InClusterName): return *p.flags.InClusterName + case p.client().ActiveCluster() != "": + return p.client().ActiveCluster() default: return "n/a" } } -// Dump prints out sanitizer report. -func (p *Popeye) dump(printHeader bool) error { +func (p *Popeye) fetchContextName() string { + if ct := p.client().ActiveContext(); ct != "" { + return ct + } + + return "n/a" +} + +// Dump dumps out scan report. +func (p *Popeye) dump(printHeader bool, asset string) error { if !p.builder.HasContent() { - return errors.New("Nothing to report, check section name or permissions") + return nil } - p.builder.SetClusterName(p.fetchContextName()) - var err error + ctx, cancel := context.WithTimeout(context.Background(), defaultGtwyTimeout) + defer cancel() + p.builder.SetClusterContext(p.fetchClusterName(), p.fetchContextName()) + var errs error switch p.flags.OutputFormat() { case report.JunitFormat: - err = p.dumpJunit() + errs = errors.Join(errs, p.dumpJunit()) case report.YAMLFormat: - err = p.dumpYAML() + errs = errors.Join(errs, p.dumpYAML()) case report.JSONFormat: - err = p.dumpJSON() + errs = errors.Join(errs, p.dumpJSON()) case report.HTMLFormat: - err = p.dumpHTML() - case report.PrometheusFormat: - err = p.dumpPrometheus() + errs = errors.Join(errs, p.dumpHTML()) case report.ScoreFormat: - err = p.dumpScore() + errs = errors.Join(errs, p.dumpScore()) + case report.PromFormat: + errs = errors.Join(errs, p.dumpPrometheus(ctx, asset, true)) default: - err = p.dumpStd(p.flags.OutputFormat() == report.JurassicFormat, printHeader) + errs = errors.Join(errs, p.dumpStd(printHeader)) } - return err + if p.flags.OutputFormat() != report.PromFormat && config.IsStrSet(p.flags.PushGateway.URL) { + if config.IsStrSet(p.flags.S3.Bucket) { + asset = *p.flags.S3.Bucket + "/" + filepath.Join(p.clusterPath(), p.scanFileName()) + } + errs = errors.Join(p.dumpPrometheus(ctx, asset, false)) + } + + return errs } func (p *Popeye) ensureOutput() error { p.outputTarget = os.Stdout - if !isSet(p.flags.Save) && !isSetStr(p.flags.S3Bucket) { + if !config.IsBoolSet(p.flags.Save) && !config.IsStrSet(p.flags.S3.Bucket) { return nil } - if p.flags.Output == nil { *p.flags.Output = "standard" } @@ -526,114 +473,48 @@ func (p *Popeye) ensureOutput() error { err error ) switch { - case isSet(p.flags.Save): - fPath := filepath.Join(DumpDir, p.fileName()) - f, err = os.Create(fPath) + case config.IsBoolSet(p.flags.Save): + dir := filepath.Join( + DumpDir, + p.clusterPath(), + ) + if err := ensureDir(dir, defaultFileMode); err != nil { + return err + } + file := filepath.Join(dir, config.SanitizeFileName(p.scanFileName())) + p.flags.OutputFile = &file + f, err = os.Create(file) if err != nil { return err } - fmt.Println(fPath) - case isSetStr(p.flags.S3Bucket): - f = NopWriter(bytes.NewBufferString("")) + fmt.Println(file) + case config.IsStrSet(p.flags.S3.Bucket): + f = NopCloser(bytes.NewBufferString("")) } p.outputTarget = f return nil } -func (p *Popeye) fileName() string { - if *p.flags.OutputFile == "" { - return fmt.Sprintf(outFmt, p.factory.Client().ActiveContext(), time.Now().UnixNano(), p.fileExt()) +func (p *Popeye) scanFileName() string { + if config.IsStrSet(p.flags.OutputFile) { + return *p.flags.OutputFile + } + + ns := p.client().ActiveNamespace() + if ns == client.BlankNamespace { + ns = client.NamespaceAll } - return fmt.Sprintf(*p.flags.OutputFile) + return fmt.Sprintf(dumpFileFMT, ns, time.Now().UnixNano(), p.fileExt()) } func (p *Popeye) fileExt() string { switch *p.flags.Output { - case "json": - return "json" case "junit": return "xml" - case "yaml": - return "yml" - case "html": - return "html" + case "json", "yaml", "html": + return *p.flags.Output default: return "txt" } } - -// ---------------------------------------------------------------------------- -// Helpers... - -func isSet(b *bool) bool { - return b != nil && *b -} - -func isSetStr(s *string) bool { - return s != nil && *s != "" -} - -func ensurePath(path string, mod os.FileMode) error { - dir, err := filepath.Abs(path) - if err != nil { - return err - } - - _, err = os.Stat(dir) - if err == nil || !os.IsNotExist(err) { - return nil - } - - if err = os.MkdirAll(dir, mod); err != nil { - return fmt.Errorf("Fail to create popeye sanitizers dump dir: %w", err) - } - return nil -} - -func dumpDir() string { - if d := os.Getenv("POPEYE_REPORT_DIR"); d != "" { - return d - } - return filepath.Join(os.TempDir(), "popeye") -} - -func parseBucket(bucketURI string) (string, string, error) { - u, err := url.Parse(bucketURI) - if err != nil { - return "", "", err - } - switch u.Scheme { - // s3://bucket or s3://bucket/ - case "s3": - var key string - if u.Path != "" { - key = strings.Trim(u.Path, "/") - } - return u.Host, key, nil - // bucket/ or bucket/path/to/key - case "": - tokens := strings.SplitAfterN(strings.Trim(u.Path, "/"), "/", 2) - key, bucket := "", strings.Trim(tokens[0], "/") - if len(tokens) > 1 { - key = tokens[1] - } - return bucket, key, nil - default: - return "", "", ErrUnknownS3BucketProtocol - } -} - -type readWriteCloser struct { - io.ReadWriter -} - -// Close close read stream. -func (wC readWriteCloser) Close() error { - return nil -} - -// NopWriter fake writer. -func NopWriter(i io.ReadWriter) io.ReadWriteCloser { - return &readWriteCloser{i} -} diff --git a/pkg/popeye_test.go b/pkg/popeye_test.go deleted file mode 100644 index 2c829e15..00000000 --- a/pkg/popeye_test.go +++ /dev/null @@ -1,54 +0,0 @@ -package pkg - -import ( - "errors" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestParseBucket(t *testing.T) { - var uu = map[string]struct { - uri string - bucket string - key string - err error - }{ - "s3_bucket": { - uri: "s3://bucketName/", - bucket: "bucketName", - }, - "toast": { - uri: "s4://bucketName/", - err: ErrUnknownS3BucketProtocol, - }, - "with_full_key": { - uri: "s3://bucketName/fred/blee", - bucket: "bucketName", - key: "fred/blee", - }, - "with_key": { - uri: "bucket/with/subkey", - bucket: "bucket", - key: "with/subkey", - }, - "with_trailer": { - uri: "/bucket/with/leading/slashes/", - bucket: "bucket", - key: "with/leading/slashes", - }, - } - - for k := range uu { - u := uu[k] - t.Run(k, func(t *testing.T) { - b, k, err := parseBucket(u.uri) - if !errors.Is(err, u.err) { - t.Fatalf("error got %v, want none", err) - return - } - assert.Equal(t, u.bucket, b) - assert.Equal(t, u.key, k) - }) - } -} diff --git a/spinach/spinach_aks.yml b/spinach/spinach_aks.yml index 299274f6..d74248c6 100644 --- a/spinach/spinach_aks.yml +++ b/spinach/spinach_aks.yml @@ -13,71 +13,73 @@ popeye: overPercUtilization: 50 # Excludes define rules to exampt resources from sanitization excludes: - rbac.authorization.k8s.io/v1/clusterrolebindings: - - name: rx:system - - name: rx:aks - - name: rx:omsagent - rbac.authorization.k8s.io/v1/clusterroles: - - name: omsagent-reader - codes: - - 400 - - name: rx:system - codes: - - 400 - - name: admin - codes: - - 400 - - name: edit - codes: - - 400 - - name: view - codes: - - 400 - - name: cluster-admin - codes: - - 400 - rbac.authorization.k8s.io/v1/rolebindings: - - name: rx:kube - rbac.authorization.k8s.io/v1/roles: - - name: rx:kube - apps/v1/daemonsets: - - name: rx:kube-system - apps/v1/deployments: - - name: rx:kube-system - apps/v1/replicasets: - - name: rx:kube - networking.k8s.io/v1/networkpolicies: - - name: rx:kube - policy/v1beta1/poddisruptionbudgets: - - name: rx:kube - v1/configmaps: - - name: rx:kube - v1/namespaces: - - name: rx:kube - v1/pods: - - name: rx:kube - v1/serviceaccounts: - - name: rx:kube - v1/secrets: - - name: rx:kube - v1/services: - - name: rx:kube + gvrs: + rbac.authorization.k8s.io/v1/clusterrolebindings: + - name: rx:system + - name: rx:aks + - name: rx:omsagent + rbac.authorization.k8s.io/v1/clusterroles: + - name: omsagent-reader + codes: + - 400 + - name: rx:system + codes: + - 400 + - name: admin + codes: + - 400 + - name: edit + codes: + - 400 + - name: view + codes: + - 400 + - name: cluster-admin + codes: + - 400 + rbac.authorization.k8s.io/v1/rolebindings: + - name: rx:kube + rbac.authorization.k8s.io/v1/roles: + - name: rx:kube + apps/v1/daemonsets: + - name: rx:kube-system + apps/v1/deployments: + - name: rx:kube-system + apps/v1/replicasets: + - name: rx:kube + networking.k8s.io/v1/networkpolicies: + - name: rx:kube + policy/v1beta1/poddisruptionbudgets: + - name: rx:kube + v1/configmaps: + - name: rx:kube + v1/namespaces: + - name: rx:kube + v1/pods: + - name: rx:kube + v1/serviceaccounts: + - name: rx:kube + v1/secrets: + - name: rx:kube + v1/services: + - name: rx:kube - # Nodes specific sanitization - node: - limits: - cpu: 90 - memory: 80 + resources: + # Nodes specific sanitization + node: + limits: + cpu: 90 + memory: 80 - # Pods specific sanitization - pod: - limits: - # Fail if cpu is over 80% - cpu: 80 - # Fail if pod mem is over 75% - memory: 75 - # Fail if more than 3 restarts on any pods - restarts: 3 + # Pods specific sanitization + pod: + limits: + # Fail if cpu is over 80% + cpu: 80 + # Fail if pod mem is over 75% + memory: 75 + # Fail if more than 3 restarts on any pods + restarts: 3 # Code specifies a custom severity level ie critical=3, warn=2, info=1 codes: diff --git a/spinach/spinach_eks.yml b/spinach/spinach_eks.yml index f1a4abaa..048d3a07 100644 --- a/spinach/spinach_eks.yml +++ b/spinach/spinach_eks.yml @@ -13,75 +13,77 @@ popeye: overPercUtilization: 50 # Excludes define rules to exampt resources from sanitization excludes: - rbac.authorization.k8s.io/v1/clusterrolebindings: - - name: rx:system - - name: rx:eks - rbac.authorization.k8s.io/v1/clusterroles: - - name: rx:eks - codes: - - 400 - - name: aws-node - codes: - - 400 - - name: rx:system - codes: - - 400 - - name: admin - codes: - - 400 - - name: edit - codes: - - 400 - - name: view - codes: - - 400 - - name: cluster-admin - codes: - - 400 - rbac.authorization.k8s.io/v1/rolebindings: - - name: rx:kube - rbac.authorization.k8s.io/v1/roles: - - name: rx:kube - apps/v1/daemonsets: - - name: rx:kube-system - apps/v1/deployments: - - name: rx:kube-system - apps/v1/replicasets: - - name: rx:kube - networking.k8s.io/v1/networkpolicies: - - name: rx:freddy - policy/v1beta1/podsecuritypolicies: - - name: rx:eks - v1/configmaps: - - name: rx:kube - v1/namespaces: - - name: rx:kube - v1/pods: - - name: rx:kube - v1/serviceaccounts: - - name: rx:kube - v1/secrets: - - name: rx:kube - v1/services: - - name: rx:kube - codes: - - 404 + gvrs: + rbac.authorization.k8s.io/v1/clusterrolebindings: + - name: rx:system + - name: rx:eks + rbac.authorization.k8s.io/v1/clusterroles: + - name: rx:eks + codes: + - 400 + - name: aws-node + codes: + - 400 + - name: rx:system + codes: + - 400 + - name: admin + codes: + - 400 + - name: edit + codes: + - 400 + - name: view + codes: + - 400 + - name: cluster-admin + codes: + - 400 + rbac.authorization.k8s.io/v1/rolebindings: + - name: rx:kube + rbac.authorization.k8s.io/v1/roles: + - name: rx:kube + apps/v1/daemonsets: + - name: rx:kube-system + apps/v1/deployments: + - name: rx:kube-system + apps/v1/replicasets: + - name: rx:kube + networking.k8s.io/v1/networkpolicies: + - name: rx:freddy + policy/v1beta1/podsecuritypolicies: + - name: rx:eks + v1/configmaps: + - name: rx:kube + v1/namespaces: + - name: rx:kube + v1/pods: + - name: rx:kube + v1/serviceaccounts: + - name: rx:kube + v1/secrets: + - name: rx:kube + v1/services: + - name: rx:kube + codes: + - 404 - # Nodes specific sanitization - node: - limits: - cpu: 90 - memory: 80 + resources: + # Nodes specific sanitization + node: + limits: + cpu: 90 + memory: 80 - # Pods specific sanitization - pod: - limits: - # Fail if cpu is over 80% - cpu: 80 - # Fail if pod mem is over 75% - memory: 75 - # Fail if more than 3 restarts on any pods - restarts: 3 + # Pods specific sanitization + pod: + limits: + # Fail if cpu is over 80% + cpu: 80 + # Fail if pod mem is over 75% + memory: 75 + # Fail if more than 3 restarts on any pods + restarts: 3 # Code specifies a custom severity level ie critical=3, warn=2, info=1 codes: diff --git a/spinach/spinach_metakube.yml b/spinach/spinach_metakube.yml index 2be16b04..eed5ad99 100644 --- a/spinach/spinach_metakube.yml +++ b/spinach/spinach_metakube.yml @@ -1,140 +1,141 @@ popeye: excludes: - v1/serviceaccounts: - # Those are managed by SysEleven - - name: rx:^syseleven - - # We don’t check the kube* service accounts - this is part of the platform - - name: rx:^kube - - name: default/default - codes: - - 400 - - # Exclude some codes for default services - v1/services: - # Those are managed by SysEleven - - name: rx:^syseleven - - # This service is of type NodePort, which is intentional (1104) - - name: default/kubernetes - codes: - - 1104 - - # The ports here are not named yet (1102) - - name: kube-system/kube-dns - codes: - - 1102 - - # The port here is not named yet (1102) - - name: kube-system/node-exporter - codes: - - 1102 - - # We don’t want to check tiller, it’s only here for backwards compatibility to helm2 - - name: kube-system/tiller-deploy - - # We don’t need to check the metrics-server, this is managed by MetaKube - - name: kube-system/metrics-server - - # Exclude Secrets in the system namespaces - v1/secrets: - # Don’t check helm release secrets - - name: rx:sh.helm.release - - - name: rx:^kube - - # Those are managed by SysEleven - - name: rx:^syseleven - - # The default token may be unused - - name: rx:default/default-token - codes: - - 400 - - # RoleBindings for platform services can be excluded - rbac.authorization.k8s.io/v1/rolebindings: - - name: rx:^kube - - name: rx:^default/system - - name: default/machine-controller - - # Those are managed by SysEleven - - name: rx:^syseleven - - # Roles for platform services can be excluded - rbac.authorization.k8s.io/v1/roles: - - name: rx:^kube - - name: rx:^default/system - - name: default/machine-controller - - # Those are managed by SysEleven - - name: rx:^syseleven - - # ReplicaSets for platform services can be excluded - v1/replicasets: - - name: rx:^kube - - # Those are managed by SysEleven - - name: rx:^syseleven - - # MetaKube provides you with some SysEleven PodSecurityPolicies that we don’t want to scan here - policy/v1beta1/podsecuritypolicies: - # Those are managed by SysEleven - - name: rx:^syseleven - - # PodDisruptionBudgets for platform services can be excluded - policy/v1beta1/poddisruptionbudgets: - - name: kube-system/coredns - - # Those are managed by SysEleven - - name: rx:^syseleven - - # Pods for platform services can be excluded - v1/pods: - - name: rx:^kube-system/ - - # Those are managed by SysEleven - - name: rx:^syseleven - - # Nodes are platform services and can be excluded - v1/nodes: - - name: rx:.* - - # We don’t want to sanitize the default namespaces: - v1/namespaces: - - name: default - - name: kube-node-lease - - name: kube-public - - name: kube-system - - # Those are managed by SysEleven - - name: rx:^syseleven + gvrs: + v1/serviceaccounts: + # Those are managed by SysEleven + - name: rx:^syseleven + + # We don’t check the kube* service accounts - this is part of the platform + - name: rx:^kube + - name: default/default + codes: + - 400 + + # Exclude some codes for default services + v1/services: + # Those are managed by SysEleven + - name: rx:^syseleven + + # This service is of type NodePort, which is intentional (1104) + - name: default/kubernetes + codes: + - 1104 + + # The ports here are not named yet (1102) + - name: kube-system/kube-dns + codes: + - 1102 + + # The port here is not named yet (1102) + - name: kube-system/node-exporter + codes: + - 1102 + + # We don’t want to check tiller, it’s only here for backwards compatibility to helm2 + - name: kube-system/tiller-deploy + + # We don’t need to check the metrics-server, this is managed by MetaKube + - name: kube-system/metrics-server + + # Exclude Secrets in the system namespaces + v1/secrets: + # Don’t check helm release secrets + - name: rx:sh.helm.release + + - name: rx:^kube + + # Those are managed by SysEleven + - name: rx:^syseleven + + # The default token may be unused + - name: rx:default/default-token + codes: + - 400 + + # RoleBindings for platform services can be excluded + rbac.authorization.k8s.io/v1/rolebindings: + - name: rx:^kube + - name: rx:^default/system + - name: default/machine-controller + + # Those are managed by SysEleven + - name: rx:^syseleven + + # Roles for platform services can be excluded + rbac.authorization.k8s.io/v1/roles: + - name: rx:^kube + - name: rx:^default/system + - name: default/machine-controller + + # Those are managed by SysEleven + - name: rx:^syseleven + + # ReplicaSets for platform services can be excluded + v1/replicasets: + - name: rx:^kube + + # Those are managed by SysEleven + - name: rx:^syseleven + + # MetaKube provides you with some SysEleven PodSecurityPolicies that we don’t want to scan here + policy/v1beta1/podsecuritypolicies: + # Those are managed by SysEleven + - name: rx:^syseleven + + # PodDisruptionBudgets for platform services can be excluded + policy/v1beta1/poddisruptionbudgets: + - name: kube-system/coredns + + # Those are managed by SysEleven + - name: rx:^syseleven + + # Pods for platform services can be excluded + v1/pods: + - name: rx:^kube-system/ + + # Those are managed by SysEleven + - name: rx:^syseleven + + # Nodes are platform services and can be excluded + v1/nodes: + - name: rx:.* + + # We don’t want to sanitize the default namespaces: + v1/namespaces: + - name: default + - name: kube-node-lease + - name: kube-public + - name: kube-system + + # Those are managed by SysEleven + - name: rx:^syseleven - # Deployments for platform services can be excluded - apps/v1/deployments: - - name: rx:^kube-system + # Deployments for platform services can be excluded + apps/v1/deployments: + - name: rx:^kube-system - # Those are managed by SysEleven - - name: rx:^syseleven + # Those are managed by SysEleven + - name: rx:^syseleven - # Daemonsets for platform services can be excluded - apps/v1/daemonsets: - - name: rx:^kube-system + # Daemonsets for platform services can be excluded + apps/v1/daemonsets: + - name: rx:^kube-system - # Those are managed by SysEleven - - name: rx:^syseleven + # Those are managed by SysEleven + - name: rx:^syseleven - # ConfigMaps for platform services can be excluded - v1/configmaps: - - name: rx:^kube-system - - name: kube-public/cluster-info + # ConfigMaps for platform services can be excluded + v1/configmaps: + - name: rx:^kube-system + - name: kube-public/cluster-info - # Those are managed by SysEleven - - name: rx:^syseleven + # Those are managed by SysEleven + - name: rx:^syseleven - rbac.authorization.k8s.io/v1/clusterroles: - - name: rx:.* - codes: - - 400 + rbac.authorization.k8s.io/v1/clusterroles: + - name: rx:.* + codes: + - 400 - # Those are managed by SysEleven - - name: rx:^syseleven + # Those are managed by SysEleven + - name: rx:^syseleven diff --git a/internal/client/gvr.go b/types/gvr.go similarity index 89% rename from internal/client/gvr.go rename to types/gvr.go index c00890cb..7b5199bc 100644 --- a/internal/client/gvr.go +++ b/types/gvr.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright Authors of Popeye -package client +package types import ( "fmt" @@ -14,10 +14,15 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" ) +type GVRS []GVR + +var BlankGVR GVR + // GVR represents a kubernetes resource schema as a string. // Format is group/version/resources:subresource type GVR struct { raw, g, v, r, sr string + apiRes *metav1.APIResource } // NewGVR builds a new gvr from a group, version, resource. @@ -44,16 +49,21 @@ func NewGVR(gvr string) GVR { return GVR{raw: gvr, g: g, v: v, r: r, sr: sr} } -// NewGVRFromMeta builds a gvr from resource metadata. -func NewGVRFromMeta(a metav1.APIResource) GVR { +// NewGVRFromAPIRes builds a gvr from server resource. +func NewGVRFromAPIRes(gv schema.GroupVersion, api metav1.APIResource) GVR { return GVR{ - raw: path.Join(a.Group, a.Version, a.Name), - g: a.Group, - v: a.Version, - r: a.Name, + raw: path.Join(gv.Group, gv.Version, api.Name), + g: gv.Group, + v: gv.Version, + r: api.Name, + apiRes: &api, } } +func (g GVR) IsMetricsRes() bool { + return strings.Contains(g.String(), "metrics") +} + // FromGVAndR builds a gvr from a group/version and resource. func FromGVAndR(gv, r string) GVR { return NewGVR(path.Join(gv, r)) diff --git a/internal/client/gvr_test.go b/types/gvr_test.go similarity index 80% rename from internal/client/gvr_test.go rename to types/gvr_test.go index 29bcba0c..71bb2312 100644 --- a/internal/client/gvr_test.go +++ b/types/gvr_test.go @@ -1,29 +1,29 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright Authors of Popeye -package client_test +package types_test import ( "path" "sort" "testing" - "github.com/derailed/popeye/internal/client" + "github.com/derailed/popeye/types" "github.com/stretchr/testify/assert" "k8s.io/apimachinery/pkg/runtime/schema" ) func TestGVRSort(t *testing.T) { - gg := client.GVRs{ - client.NewGVR("v1/pods"), - client.NewGVR("v1/services"), - client.NewGVR("apps/v1/deployments"), + gg := types.GVRs{ + types.NewGVR("v1/pods"), + types.NewGVR("v1/services"), + types.NewGVR("apps/v1/deployments"), } sort.Sort(gg) - assert.Equal(t, client.GVRs{ - client.NewGVR("v1/pods"), - client.NewGVR("v1/services"), - client.NewGVR("apps/v1/deployments"), + assert.Equal(t, types.GVRs{ + types.NewGVR("v1/pods"), + types.NewGVR("v1/services"), + types.NewGVR("apps/v1/deployments"), }, gg) } @@ -44,7 +44,7 @@ func TestGVRCan(t *testing.T) { for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, client.Can(u.vv, u.v)) + assert.Equal(t, u.e, types.Can(u.vv, u.v)) }) } } @@ -62,7 +62,7 @@ func TestAsGVR(t *testing.T) { for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, client.NewGVR(u.gvr).GVR()) + assert.Equal(t, u.e, types.NewGVR(u.gvr).GVR()) }) } } @@ -80,7 +80,7 @@ func TestAsGV(t *testing.T) { for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, client.NewGVR(u.gvr).GV()) + assert.Equal(t, u.e, types.NewGVR(u.gvr).GV()) }) } } @@ -97,7 +97,7 @@ func TestNewGVR(t *testing.T) { for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, client.NewGVR(path.Join(u.g, u.v, u.r)).String()) + assert.Equal(t, u.e, types.NewGVR(path.Join(u.g, u.v, u.r)).String()) }) } } @@ -116,7 +116,7 @@ func TestGVRAsResourceName(t *testing.T) { for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, client.NewGVR(u.gvr).AsResourceName()) + assert.Equal(t, u.e, types.NewGVR(u.gvr).AsResourceName()) }) } } @@ -135,7 +135,7 @@ func TestToR(t *testing.T) { for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, client.NewGVR(u.gvr).R()) + assert.Equal(t, u.e, types.NewGVR(u.gvr).R()) }) } } @@ -154,7 +154,7 @@ func TestToG(t *testing.T) { for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, client.NewGVR(u.gvr).G()) + assert.Equal(t, u.e, types.NewGVR(u.gvr).G()) }) } } @@ -173,7 +173,7 @@ func TestToV(t *testing.T) { for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, client.NewGVR(u.gvr).V()) + assert.Equal(t, u.e, types.NewGVR(u.gvr).V()) }) } } @@ -191,7 +191,7 @@ func TestToString(t *testing.T) { for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { - assert.Equal(t, u.gvr, client.NewGVR(u.gvr).String()) + assert.Equal(t, u.gvr, types.NewGVR(u.gvr).String()) }) } } diff --git a/types/types.go b/types/types.go index d2c713c2..d8cb98e7 100644 --- a/types/types.go +++ b/types/types.go @@ -52,18 +52,33 @@ var ( ReadAllAccess = []string{GetVerb, ListVerb, WatchVerb} ) +// NamespaceNames tracks a collection of namespace names. +type NamespaceNames map[string]struct{} + // Authorizer checks what a user can or cannot do to a resource. type Authorizer interface { // CanI returns true if the user can use these actions for a given resource. - CanI(ns, gvr string, verbs []string) (bool, error) + CanI(string, GVR, ...string) (bool, error) } // Config represents an api server configuration. type Config interface { + // CurrentNamespaceName returns the current context namespace. CurrentNamespaceName() (string, error) + + // CurrentContextName returns the current context. CurrentContextName() (string, error) + + // CurrentClusterName returns the current cluster. + CurrentClusterName() (string, error) + + // Flags tracks k8s cli flags. Flags() *genericclioptions.ConfigFlags + + // RESTConfig tracks k8s client conn. RESTConfig() (*restclient.Config, error) + + // CallTimeout tracks api server ttl. CallTimeout() time.Duration } @@ -74,6 +89,9 @@ type Connection interface { // Config returns current config. Config() Config + // ConnectionOK checks api server connection status. + ConnectionOK() bool + // Dial connects to api server. Dial() (kubernetes.Interface, error) @@ -95,14 +113,20 @@ type Connection interface { // ServerVersion returns current server version. ServerVersion() (*version.Info, error) + // CheckConnectivity checks if api server connection is happy or not. + CheckConnectivity() bool + // ActiveContext returns the current context name. ActiveContext() string + // ActiveCluster returns the current cluster name. + ActiveCluster() string + // ActiveNamespace returns the current namespace. ActiveNamespace() string - // // IsActiveNamespace checks if given ns is active. - // IsActiveNamespace(string) bool + // IsActiveNamespace checks if given ns is active. + IsActiveNamespace(string) bool } // Factory represents a resource factory. @@ -111,16 +135,16 @@ type Factory interface { Client() Connection // Get fetch a given resource. - Get(gvr, path string, wait bool, sel labels.Selector) (runtime.Object, error) + Get(GVR, string, bool, labels.Selector) (runtime.Object, error) // List fetch a collection of resources. - List(gvr, ns string, wait bool, sel labels.Selector) ([]runtime.Object, error) + List(GVR, string, bool, labels.Selector) ([]runtime.Object, error) // ForResource fetch an informer for a given resource. - ForResource(ns, gvr string) (informers.GenericInformer, error) + ForResource(string, GVR) (informers.GenericInformer, error) // CanForResource fetch an informer for a given resource if authorized - CanForResource(ns, gvr string, verbs []string) (informers.GenericInformer, error) + CanForResource(string, GVR, ...string) (informers.GenericInformer, error) // WaitForCacheSync synchronize the cache. WaitForCacheSync()