├── hack ├── pr_check.sh ├── generate-operator-bundle-contents.py └── olm-registry │ ├── olm-artifacts-template.yaml │ └── hypershift-template.yaml ├── version └── version.go ├── pkg ├── validations │ ├── all │ │ └── all.go │ ├── gen.go │ ├── test-resources │ │ ├── default-config.yaml │ │ ├── config-with-some-excluded-checks.yaml │ │ └── config-with-custom-check.yaml │ ├── lint_context.go │ ├── base.go │ ├── base_test.go │ └── utils.go ├── stringutils │ ├── ternary.go │ ├── repeat.go │ ├── README │ ├── default.go │ ├── split.go │ └── consume.go ├── utils │ ├── labels.go │ ├── object.go │ ├── selector.go │ └── selector_test.go ├── controller │ ├── const.go │ ├── watchnamespaces_test.go │ ├── reconcileobjects_test.go │ ├── watchnamespaces.go │ ├── validationscache.go │ ├── reconcileobjects.go │ └── validationscache_test.go ├── prometheus │ ├── prometheus_test.go │ └── prometheus.go ├── testutils │ ├── testutils.go │ └── templates │ │ ├── ReplicaSet.yaml.tpl │ │ └── Deployment.yaml.tpl └── configmap │ ├── configmap_watcher_test.go │ └── configmap_watcher.go ├── renovate.json ├── deploy ├── openshift │ ├── service-account.yaml │ ├── cluster-role.yaml │ ├── service.yaml │ ├── role.yaml │ ├── cluster-role-binding.yaml │ ├── role-binding.yaml │ ├── network-policies.yaml │ ├── configmap.yaml │ └── operator.yaml └── bundle │ └── template │ └── deployment-validation-operator.clusterserviceversion.yaml ├── .ci-operator.yaml ├── OWNERS ├── docs ├── diagrams │ ├── DVO_Observability.vsdx │ ├── DVO_Operator_Managed.vsdx │ ├── DVO_Internal_Codebase.vsdx │ ├── DVO_Human_Consumer_Workflow.vsdx │ └── DVO_Human_Developer_Workflow.vsdx ├── images │ ├── DVO_Observability.drawio.png │ ├── DVO_Internal_Codebase.drawio.png │ ├── DVO_Operator_Managed.drawio.png │ ├── DVO_Human_Consumer_Workflow.drawio.png │ └── DVO_Human_Developer_Workflow.drawio.png ├── checks.md ├── architecture.md └── new-releases.md ├── tools.go ├── DoD.md ├── ci ├── golangci-lint.sh └── codecov.sh ├── .github └── dependabot.yml ├── .golangci.yml ├── api └── apis.go ├── config └── config.go ├── .gitignore ├── bundle ├── manifests │ ├── deployment-validation-operator-metrics_v1_service.yaml │ ├── deployment-validation-operator-config_v1_configmap.yaml │ └── deployment-validation-operator.clusterserviceversion.yaml ├── metadata │ └── annotations.yaml └── Dockerfile.bundle ├── konflux-ci ├── bundle │ ├── manifests │ │ ├── deployment-validation-operator-metrics_v1_service.yaml │ │ ├── deployment-validation-operator-config_v1_configmap.yaml │ │ └── deployment-validation-operator.clusterserviceversion.yaml │ ├── metadata │ │ └── annotations.yaml │ └── bundle.Dockerfile ├── cli-manifests │ ├── user-rolebinding.yaml │ ├── staging-release.yaml │ ├── staging-release-fbc.yaml │ ├── prod-release.yaml │ ├── release-payloads │ │ ├── prod-release-0.7.11-operator.yaml │ │ ├── prod-release-0.7.12-operator.yaml │ │ ├── prod-release-0.7.13-operator.yaml │ │ ├── prod-release-0.7.9-fbc.yaml │ │ ├── prod-release-0.7.11-fbc.yaml │ │ ├── prod-release-0.7.12-fbc.yaml │ │ └── prod-release-0.7.13-fbc.yaml │ ├── prod-snapshot.yaml │ └── staging-snapshot.yaml └── fbc │ └── catalog.Dockerfile ├── .tekton └── images-mirror-set.yaml ├── .codecov.yml ├── config.example.yaml ├── bundle.Dockerfile ├── internal └── options │ ├── options_test.go │ └── options.go ├── OWNERS_ALIASES ├── Makefile ├── go.mod ├── main.go └── README.md /hack/pr_check.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | make docker-test 4 | -------------------------------------------------------------------------------- /version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | var ( 4 | Version = "0.0.1" 5 | ) 6 | -------------------------------------------------------------------------------- /pkg/validations/all/all.go: -------------------------------------------------------------------------------- 1 | package all 2 | 3 | /* 4 | import ( 5 | // Import all check templates. 6 | ) 7 | */ 8 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "enabledManagers": ["tekton"] 4 | } 5 | -------------------------------------------------------------------------------- /deploy/openshift/service-account.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: deployment-validation-operator 5 | -------------------------------------------------------------------------------- /.ci-operator.yaml: -------------------------------------------------------------------------------- 1 | build_root_image: 2 | namespace: openshift 3 | name: release 4 | tag: rhel-9-release-golang-1.24-openshift-4.20 5 | -------------------------------------------------------------------------------- /OWNERS: -------------------------------------------------------------------------------- 1 | approvers: 2 | - BaiyangZhou 3 | - ncaak 4 | - opokornyy 5 | reviewers: 6 | - BaiyangZhou 7 | - ncaak 8 | - opokornyy 9 | 10 | -------------------------------------------------------------------------------- /docs/diagrams/DVO_Observability.vsdx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/app-sre/deployment-validation-operator/HEAD/docs/diagrams/DVO_Observability.vsdx -------------------------------------------------------------------------------- /docs/diagrams/DVO_Operator_Managed.vsdx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/app-sre/deployment-validation-operator/HEAD/docs/diagrams/DVO_Operator_Managed.vsdx -------------------------------------------------------------------------------- /docs/diagrams/DVO_Internal_Codebase.vsdx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/app-sre/deployment-validation-operator/HEAD/docs/diagrams/DVO_Internal_Codebase.vsdx -------------------------------------------------------------------------------- /docs/images/DVO_Observability.drawio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/app-sre/deployment-validation-operator/HEAD/docs/images/DVO_Observability.drawio.png -------------------------------------------------------------------------------- /docs/images/DVO_Internal_Codebase.drawio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/app-sre/deployment-validation-operator/HEAD/docs/images/DVO_Internal_Codebase.drawio.png -------------------------------------------------------------------------------- /docs/images/DVO_Operator_Managed.drawio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/app-sre/deployment-validation-operator/HEAD/docs/images/DVO_Operator_Managed.drawio.png -------------------------------------------------------------------------------- /docs/diagrams/DVO_Human_Consumer_Workflow.vsdx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/app-sre/deployment-validation-operator/HEAD/docs/diagrams/DVO_Human_Consumer_Workflow.vsdx -------------------------------------------------------------------------------- /docs/diagrams/DVO_Human_Developer_Workflow.vsdx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/app-sre/deployment-validation-operator/HEAD/docs/diagrams/DVO_Human_Developer_Workflow.vsdx -------------------------------------------------------------------------------- /docs/images/DVO_Human_Consumer_Workflow.drawio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/app-sre/deployment-validation-operator/HEAD/docs/images/DVO_Human_Consumer_Workflow.drawio.png -------------------------------------------------------------------------------- /docs/images/DVO_Human_Developer_Workflow.drawio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/app-sre/deployment-validation-operator/HEAD/docs/images/DVO_Human_Developer_Workflow.drawio.png -------------------------------------------------------------------------------- /tools.go: -------------------------------------------------------------------------------- 1 | //go:build tools 2 | // +build tools 3 | 4 | // Place any runtime dependencies as imports in this file. 5 | // Go modules will be forced to download and install them. 6 | package tools 7 | -------------------------------------------------------------------------------- /pkg/validations/gen.go: -------------------------------------------------------------------------------- 1 | //go:build tools 2 | // +build tools 3 | 4 | package validations 5 | 6 | import _ "golang.stackrox.io/kube-linter/pkg/templates/codegen" 7 | 8 | //go:generate go run golang.stackrox.io/kube-linter/pkg/templates/codegen 9 | -------------------------------------------------------------------------------- /pkg/stringutils/ternary.go: -------------------------------------------------------------------------------- 1 | package stringutils 2 | 3 | // Ternary does a ternary based on the condition. 4 | func Ternary(condition bool, ifTrue, ifFalse string) string { 5 | if condition { 6 | return ifTrue 7 | } 8 | return ifFalse 9 | } 10 | -------------------------------------------------------------------------------- /pkg/utils/labels.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 5 | "k8s.io/apimachinery/pkg/labels" 6 | ) 7 | 8 | func GetLabels(object *unstructured.Unstructured) labels.Set { 9 | return labels.Set(object.GetLabels()) 10 | } 11 | -------------------------------------------------------------------------------- /deploy/openshift/cluster-role.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: deployment-validation-operator 6 | rules: 7 | - apiGroups: 8 | - "*" 9 | resources: 10 | - "*" 11 | verbs: 12 | - get 13 | - list 14 | - watch 15 | -------------------------------------------------------------------------------- /DoD.md: -------------------------------------------------------------------------------- 1 | # Definition of Done 2 | 3 | 1. Pull request/commit includes a bug number or a JIRA number 4 | 2. Unit tests are written and running cleanly in the CI environment 5 | 3. All linters are running cleanly in the CI environment 6 | 4. Failing CI blocks release/merge 7 | 5. Code changes reviewed 8 | 6. Acceptance criteria are verified and met -------------------------------------------------------------------------------- /ci/golangci-lint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -ex 4 | 5 | if [ -z "${GOPATH:-}" ]; then 6 | eval "$(go env | grep GOPATH)" 7 | fi 8 | 9 | export BINARY=bin/golangci-lint 10 | if [ ! -f "$BINARY" ]; then 11 | curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/HEAD/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.64.7 12 | fi 13 | -------------------------------------------------------------------------------- /pkg/stringutils/repeat.go: -------------------------------------------------------------------------------- 1 | package stringutils 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | // Repeat repeats the given string `n` times efficiently. 8 | func Repeat(s string, n int) string { 9 | var sb strings.Builder 10 | sb.Grow(len([]byte(s)) * n) 11 | for i := 0; i < n; i++ { 12 | sb.WriteString(s) 13 | } 14 | return sb.String() 15 | } 16 | -------------------------------------------------------------------------------- /deploy/openshift/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: deployment-validation-operator-metrics 5 | labels: 6 | name: deployment-validation-operator 7 | spec: 8 | ports: 9 | - name: http-metrics 10 | port: 8383 11 | protocol: TCP 12 | targetPort: 8383 13 | selector: 14 | name: deployment-validation-operator 15 | -------------------------------------------------------------------------------- /pkg/stringutils/README: -------------------------------------------------------------------------------- 1 | These functions are copyed from kube-linter because they are internal functions. The project plans to move these to a common library that will be open sourced, but that is a ways off. For now these functions should stay here until the kube-linter library is available. At that time, these should all be removed and the upstream library should be used 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "docker" 4 | directory: "/build" 5 | labels: 6 | - "area/dependency" 7 | - "ok-to-test" 8 | schedule: 9 | interval: "weekly" 10 | ignore: 11 | - dependency-name: "openshift4/ose-operator-registry" 12 | # don't upgrade ose-operator-registry via these means 13 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | timeout: 5m 3 | 4 | linters: 5 | disable-all: true 6 | enable: 7 | - gofmt 8 | - goimports 9 | - revive 10 | - govet 11 | - misspell 12 | - unused 13 | - gocyclo 14 | - unconvert 15 | - goconst 16 | - gosec 17 | - lll 18 | - staticcheck 19 | 20 | linters-settings: 21 | lll: 22 | line-length: 120 23 | tab-width: 8 24 | -------------------------------------------------------------------------------- /api/apis.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "k8s.io/apimachinery/pkg/runtime" 5 | ) 6 | 7 | // AddToSchemes may be used to add all resources defined in the project to a Scheme 8 | var AddToSchemes runtime.SchemeBuilder 9 | 10 | // AddToScheme adds all Resources to the Scheme 11 | func AddToScheme(s *runtime.Scheme) error { 12 | return AddToSchemes.AddToScheme(s) 13 | } 14 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | const ( 4 | // OperatorName stores the name used by this code for the Deployment Validation Operator 5 | OperatorName string = "deployment-validation-operator" 6 | 7 | // OperatorNamespace stores a string indicating the Kubernetes namespace in which the operator runs 8 | OperatorNamespace string = "deployment-validation-operator" 9 | ) 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Build artifacts 2 | build/_output/* 3 | 4 | # Output of the go coverage tool 5 | *.out 6 | 7 | # docker login 8 | .docker/* 9 | 10 | # Vim 11 | .sw[a-p] 12 | .*.sw[a-p] 13 | *.orig 14 | 15 | # Boilerplate 16 | .venv/* 17 | .operator-sdk/* 18 | .grpcurl/* 19 | .opm/* 20 | 21 | # IDE 22 | .idea 23 | .vscode 24 | 25 | # go 26 | vendor 27 | 28 | # local dev default configmap 29 | config/deployment-validation-operator-config.yaml -------------------------------------------------------------------------------- /bundle/manifests/deployment-validation-operator-metrics_v1_service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | creationTimestamp: null 5 | labels: 6 | name: deployment-validation-operator 7 | name: deployment-validation-operator-metrics 8 | spec: 9 | ports: 10 | - name: http-metrics 11 | port: 8383 12 | protocol: TCP 13 | targetPort: 8383 14 | selector: 15 | name: deployment-validation-operator 16 | status: 17 | loadBalancer: {} 18 | -------------------------------------------------------------------------------- /deploy/openshift/role.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: Role 4 | metadata: 5 | name: deployment-validation-operator 6 | rules: 7 | - apiGroups: 8 | - "" 9 | resources: 10 | - configmaps 11 | - services 12 | verbs: 13 | - get 14 | - create 15 | - list 16 | - delete 17 | - update 18 | - watch 19 | - patch 20 | - apiGroups: 21 | - monitoring.coreos.com 22 | resources: 23 | - servicemonitors 24 | verbs: 25 | - '*' 26 | -------------------------------------------------------------------------------- /konflux-ci/bundle/manifests/deployment-validation-operator-metrics_v1_service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | creationTimestamp: null 5 | labels: 6 | name: deployment-validation-operator 7 | name: deployment-validation-operator-metrics 8 | spec: 9 | ports: 10 | - name: http-metrics 11 | port: 8383 12 | protocol: TCP 13 | targetPort: 8383 14 | selector: 15 | name: deployment-validation-operator 16 | status: 17 | loadBalancer: {} 18 | -------------------------------------------------------------------------------- /deploy/openshift/cluster-role-binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: deployment-validation-operator 5 | subjects: 6 | - kind: ServiceAccount 7 | name: deployment-validation-operator 8 | namespace: deployment-validation-operator # if using a custom namespace to deploy DVO components, change this line !!! 9 | roleRef: 10 | kind: ClusterRole 11 | name: deployment-validation-operator 12 | apiGroup: rbac.authorization.k8s.io 13 | -------------------------------------------------------------------------------- /pkg/stringutils/default.go: -------------------------------------------------------------------------------- 1 | package stringutils 2 | 3 | // OrDefault returns the string if it's not empty, or the default. 4 | func OrDefault(s, defaultValue string) string { 5 | if s != "" { 6 | return s 7 | } 8 | return defaultValue 9 | } 10 | 11 | // PointerOrDefault returns the string if it's not nil nor empty, or the default. 12 | func PointerOrDefault(s *string, defaultValue string) string { 13 | if s == nil { 14 | return defaultValue 15 | } 16 | 17 | return OrDefault(*s, defaultValue) 18 | } 19 | -------------------------------------------------------------------------------- /pkg/validations/test-resources/default-config.yaml: -------------------------------------------------------------------------------- 1 | checks: 2 | doNotAutoAddDefaults: false 3 | addAllBuiltIn: true 4 | include: 5 | - "host-ipc" 6 | - "host-network" 7 | - "host-pid" 8 | - "non-isolated-pod" 9 | - "pdb-max-unavailable" 10 | - "pdb-min-available" 11 | - "privilege-escalation-container" 12 | - "privileged-container" 13 | - "run-as-non-root" 14 | - "unsafe-sysctls" 15 | - "unset-cpu-requirements" 16 | - "unset-memory-requirements" -------------------------------------------------------------------------------- /deploy/openshift/role-binding.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: RoleBinding 4 | metadata: 5 | name: deployment-validation-operator 6 | namespace: deployment-validation-operator 7 | subjects: 8 | - kind: ServiceAccount 9 | name: deployment-validation-operator 10 | namespace: deployment-validation-operator # if using a custom namespace to deploy DVO components, change this line !!! 11 | roleRef: 12 | kind: Role 13 | name: deployment-validation-operator 14 | apiGroup: rbac.authorization.k8s.io 15 | -------------------------------------------------------------------------------- /.tekton/images-mirror-set.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: config.openshift.io/v1 3 | kind: ImageDigestMirrorSet 4 | metadata: 5 | name: prod-mirror-set 6 | spec: 7 | imageDigestMirrors: 8 | - mirrors: 9 | - quay.io/redhat-user-workloads/dvo-obsint-tenant/deployment-validation-operator/deployment-validation-operator@sha256:74728420bb74a5a9a8040b48413da48f506e42b333df53c2067f53f614a2342d 10 | source: registry.redhat.io/dvo/deployment-validation-rhel8-operator@sha256:74728420bb74a5a9a8040b48413da48f506e42b333df53c2067f53f614a2342d 11 | -------------------------------------------------------------------------------- /pkg/stringutils/split.go: -------------------------------------------------------------------------------- 1 | package stringutils 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | // Split2 splits the given string at the given separator, returning the part before and after the separator as two 8 | // separate return values. 9 | // If the string does not contain `sep`, the entire string is returned as the first return value. 10 | func Split2(str, sep string) (string, string) { 11 | splitIdx := strings.Index(str, sep) 12 | if splitIdx == -1 { 13 | return str, "" 14 | } 15 | return str[:splitIdx], str[splitIdx+len(sep):] 16 | } 17 | -------------------------------------------------------------------------------- /.codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | notify: 3 | require_ci_to_pass: no 4 | 5 | coverage: 6 | precision: 2 7 | round: down 8 | range: "20...100" 9 | 10 | status: 11 | project: no 12 | patch: no 13 | changes: no 14 | 15 | parsers: 16 | gcov: 17 | branch_detection: 18 | conditional: yes 19 | loop: yes 20 | method: no 21 | macro: no 22 | 23 | comment: 24 | layout: "reach,diff,flags,tree" 25 | behavior: default 26 | require_changes: no 27 | 28 | ignore: 29 | - "**/mocks" 30 | - "**/zz_generated*.go" 31 | -------------------------------------------------------------------------------- /pkg/validations/test-resources/config-with-some-excluded-checks.yaml: -------------------------------------------------------------------------------- 1 | checks: 2 | doNotAutoAddDefaults: false 3 | addAllBuiltIn: true 4 | exclude: 5 | - "unset-cpu-requirements" 6 | - "unset-memory-requirements" 7 | include: 8 | - "host-ipc" 9 | - "host-network" 10 | - "host-pid" 11 | - "non-isolated-pod" 12 | - "pdb-max-unavailable" 13 | - "pdb-min-available" 14 | - "privilege-escalation-container" 15 | - "privileged-container" 16 | - "run-as-non-root" 17 | - "unsafe-sysctls" 18 | -------------------------------------------------------------------------------- /bundle/metadata/annotations.yaml: -------------------------------------------------------------------------------- 1 | annotations: 2 | # Core bundle annotations. 3 | operators.operatorframework.io.bundle.mediatype.v1: registry+v1 4 | operators.operatorframework.io.bundle.manifests.v1: manifests/ 5 | operators.operatorframework.io.bundle.metadata.v1: metadata/ 6 | operators.operatorframework.io.bundle.package.v1: deployment-validation-operator 7 | operators.operatorframework.io.bundle.channels.v1: alpha 8 | operators.operatorframework.io.metrics.builder: operator-sdk-v1.31.0+git 9 | operators.operatorframework.io.metrics.mediatype.v1: metrics+v1 10 | operators.operatorframework.io.metrics.project_layout: unknown 11 | -------------------------------------------------------------------------------- /konflux-ci/bundle/metadata/annotations.yaml: -------------------------------------------------------------------------------- 1 | annotations: 2 | # Core bundle annotations. 3 | operators.operatorframework.io.bundle.mediatype.v1: registry+v1 4 | operators.operatorframework.io.bundle.manifests.v1: manifests/ 5 | operators.operatorframework.io.bundle.metadata.v1: metadata/ 6 | operators.operatorframework.io.bundle.package.v1: deployment-validation-operator 7 | operators.operatorframework.io.bundle.channels.v1: alpha 8 | operators.operatorframework.io.metrics.builder: operator-sdk-v1.31.0+git 9 | operators.operatorframework.io.metrics.mediatype.v1: metrics+v1 10 | operators.operatorframework.io.metrics.project_layout: unknown 11 | -------------------------------------------------------------------------------- /config.example.yaml: -------------------------------------------------------------------------------- 1 | checks: 2 | # if doNotAutoAddDefaults is true, default checks are not automatically added. 3 | doNotAutoAddDefaults: false 4 | 5 | # addAllBuiltIn, if set, adds all built-in checks. This allows users to 6 | # explicitly opt-out of checks that are not relevant using Exclude. 7 | # Takes precedence over doNotAutoAddDefaults, if both are set. 8 | addAllBuiltIn: true 9 | 10 | # exclude will remove a set of checks from the validations. 11 | # It is used when addAllBuiltIn is set to true. 12 | exclude: ["check1", "check2"] 13 | 14 | # include will add a set of checks to the validations. 15 | # It is used when addAllBuiltIn is set to false. 16 | include: ["check1", "check2"] -------------------------------------------------------------------------------- /deploy/openshift/network-policies.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: template.openshift.io/v1 3 | kind: Template 4 | metadata: 5 | name: deployment-validation-operator-network-policy-${NAMESPACE} 6 | objects: 7 | - apiVersion: networking.k8s.io/v1 8 | kind: NetworkPolicy 9 | metadata: 10 | name: allow-from-${NAMESPACE} 11 | spec: 12 | podSelector: {} 13 | ingress: 14 | - from: 15 | - namespaceSelector: 16 | matchLabels: 17 | name: ${NAMESPACE} 18 | policyTypes: 19 | - Ingress 20 | parameters: 21 | - name: NAMESPACE 22 | value: "" 23 | displayName: a namespace with access to DVO data 24 | description: the namespace that should be allowed to access DVO data 25 | required: true 26 | -------------------------------------------------------------------------------- /konflux-ci/cli-manifests/user-rolebinding.yaml: -------------------------------------------------------------------------------- 1 | # This manifest adds a new RoleBinding for the user to access the Konflux UI. 2 | # * role param specifies the level of permissions among: admin, maintainer, contributor 3 | # * username param specifies the RH user who will be granted the permissions 4 | --- 5 | apiVersion: rbac.authorization.k8s.io/v1 6 | kind: RoleBinding 7 | metadata: 8 | labels: 9 | konflux-ci.dev/type: user 10 | name: konflux-${role}-${username}-actions-user 11 | namespace: dvo-obsint-tenant 12 | roleRef: 13 | apiGroup: rbac.authorization.k8s.io 14 | kind: ClusterRole 15 | name: konflux-${role}-user-actions 16 | subjects: 17 | - apiGroup: rbac.authorization.k8s.io 18 | kind: User 19 | name: ${username} 20 | -------------------------------------------------------------------------------- /docs/checks.md: -------------------------------------------------------------------------------- 1 | ### Enabled Check Documentation 2 | 3 | [minimum-three-replicas](https://cloud.redhat.com/blog/deploying-highly-available-applications-openshift-kubernetes) 4 | 5 | [no-anti-affinity](https://docs.openshift.com/container-platform/4.10/nodes/scheduling/nodes-scheduler-pod-affinity.html) 6 | 7 | [no-node-affinity](https://docs.openshift.com/container-platform/4.10/nodes/scheduling/nodes-scheduler-node-affinity.html#nodes-scheduler-node-affinity-about_nodes-scheduler-node-affinity) 8 | 9 | [unset-cpu-requirements](https://cloud.redhat.com/blog/deploying-highly-available-applications-openshift-kubernetes) 10 | 11 | [unset-memory-requirements](https://cloud.redhat.com/blog/deploying-highly-available-applications-openshift-kubernetes) -------------------------------------------------------------------------------- /pkg/validations/lint_context.go: -------------------------------------------------------------------------------- 1 | package validations 2 | 3 | import "golang.stackrox.io/kube-linter/pkg/lintcontext" 4 | 5 | type lintContextImpl struct { 6 | objects []lintcontext.Object 7 | } 8 | 9 | // Objects returns the (valid) objects loaded from this LintContext. 10 | func (l *lintContextImpl) Objects() []lintcontext.Object { 11 | return l.objects 12 | } 13 | 14 | // addObject adds a valid object to this LintContext 15 | func (l *lintContextImpl) addObjects(objs ...lintcontext.Object) { 16 | l.objects = append(l.objects, objs...) 17 | } 18 | 19 | // InvalidObjects returns any objects that we attempted to load, but which were invalid. 20 | func (l *lintContextImpl) InvalidObjects() []lintcontext.InvalidObject { 21 | return nil 22 | } 23 | -------------------------------------------------------------------------------- /deploy/openshift/configmap.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: ConfigMap 4 | metadata: 5 | name: deployment-validation-operator-config 6 | labels: 7 | name: deployment-validation-operator 8 | data: 9 | deployment-validation-operator-config.yaml: |- 10 | checks: 11 | doNotAutoAddDefaults: true 12 | addAllBuiltIn: false 13 | include: 14 | - "host-ipc" 15 | - "host-network" 16 | - "host-pid" 17 | - "non-isolated-pod" 18 | - "pdb-max-unavailable" 19 | - "pdb-min-available" 20 | - "privilege-escalation-container" 21 | - "privileged-container" 22 | - "run-as-non-root" 23 | - "unsafe-sysctls" 24 | - "unset-cpu-requirements" 25 | - "unset-memory-requirements" 26 | -------------------------------------------------------------------------------- /konflux-ci/cli-manifests/staging-release.yaml: -------------------------------------------------------------------------------- 1 | # Applying this manifest will trigger a new release. 2 | # Temporary or automatic konflux snapshots may deliver a failed release 3 | # To check the process and logs, connect to the UI and navigate to Application > Releases > [release-name] > PipelineRun 4 | --- 5 | apiVersion: appstudio.redhat.com/v1alpha1 6 | kind: Release 7 | metadata: 8 | name: staging-release-snapshot-1311-01-rc1 9 | namespace: dvo-obsint-tenant 10 | spec: 11 | releasePlan: release-plan-staging 12 | snapshot: deployment-validation-operator-4x9kk 13 | data: 14 | releaseNotes: 15 | topic: Test Release 16 | synopsis: Test to debug Release process 17 | description: Test to debug Release process 18 | solution: "" 19 | references: [] 20 | -------------------------------------------------------------------------------- /konflux-ci/cli-manifests/staging-release-fbc.yaml: -------------------------------------------------------------------------------- 1 | # Applying this manifest will trigger a new release of the FBC. 2 | # Temporary or automatic konflux snapshots may deliver a failed release 3 | # To check the process and logs, connect to the UI and navigate to Application > Releases > [release-name] > PipelineRun 4 | --- 5 | apiVersion: appstudio.redhat.com/v1alpha1 6 | kind: Release 7 | metadata: 8 | name: staging-release-fbc-snapshot-1117-02 9 | namespace: dvo-obsint-tenant 10 | spec: 11 | releasePlan: release-plan-fbc-staging-4-15 12 | snapshot: fbc-ocp4-15-47b94 13 | data: 14 | releaseNotes: 15 | topic: Test FBC index Release 16 | synopsis: Test to debug Release process 17 | description: Test to debug Release process 18 | solution: "" 19 | references: [] 20 | -------------------------------------------------------------------------------- /bundle.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM scratch 2 | 3 | # Core bundle labels. 4 | LABEL operators.operatorframework.io.bundle.mediatype.v1=registry+v1 5 | LABEL operators.operatorframework.io.bundle.manifests.v1=manifests/ 6 | LABEL operators.operatorframework.io.bundle.metadata.v1=metadata/ 7 | LABEL operators.operatorframework.io.bundle.package.v1=deployment-validation-operator 8 | LABEL operators.operatorframework.io.bundle.channels.v1=alpha 9 | LABEL operators.operatorframework.io.metrics.builder=operator-sdk-v1.31.0+git 10 | LABEL operators.operatorframework.io.metrics.mediatype.v1=metrics+v1 11 | LABEL operators.operatorframework.io.metrics.project_layout=unknown 12 | 13 | # Copy files to locations specified by labels. 14 | COPY bundle/manifests /manifests/ 15 | COPY bundle/metadata /metadata/ 16 | -------------------------------------------------------------------------------- /bundle/manifests/deployment-validation-operator-config_v1_configmap.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | data: 3 | deployment-validation-operator-config.yaml: |- 4 | checks: 5 | doNotAutoAddDefaults: true 6 | addAllBuiltIn: false 7 | include: 8 | - "host-ipc" 9 | - "host-network" 10 | - "host-pid" 11 | - "non-isolated-pod" 12 | - "pdb-max-unavailable" 13 | - "pdb-min-available" 14 | - "privilege-escalation-container" 15 | - "privileged-container" 16 | - "run-as-non-root" 17 | - "unsafe-sysctls" 18 | - "unset-cpu-requirements" 19 | - "unset-memory-requirements" 20 | kind: ConfigMap 21 | metadata: 22 | labels: 23 | name: deployment-validation-operator 24 | name: deployment-validation-operator-config 25 | -------------------------------------------------------------------------------- /konflux-ci/bundle/manifests/deployment-validation-operator-config_v1_configmap.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | data: 3 | deployment-validation-operator-config.yaml: |- 4 | checks: 5 | doNotAutoAddDefaults: true 6 | addAllBuiltIn: false 7 | include: 8 | - "host-ipc" 9 | - "host-network" 10 | - "host-pid" 11 | - "non-isolated-pod" 12 | - "pdb-max-unavailable" 13 | - "pdb-min-available" 14 | - "privilege-escalation-container" 15 | - "privileged-container" 16 | - "run-as-non-root" 17 | - "unsafe-sysctls" 18 | - "unset-cpu-requirements" 19 | - "unset-memory-requirements" 20 | kind: ConfigMap 21 | metadata: 22 | labels: 23 | name: deployment-validation-operator 24 | name: deployment-validation-operator-config 25 | -------------------------------------------------------------------------------- /konflux-ci/cli-manifests/prod-release.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: appstudio.redhat.com/v1alpha1 3 | kind: Release 4 | metadata: 5 | name: prod-release-snapshot-0515-rc3 6 | namespace: dvo-obsint-tenant 7 | spec: 8 | releasePlan: release-plan-prod 9 | snapshot: prod-snapshot-0515-rc3 10 | data: 11 | releaseNotes: 12 | topic: Deployment Validation Operator 0.7.10 13 | synopsis: Maintenance update 14 | description: | 15 | This release resolves issues with outdated libraries and vulnerabilities present in the 0.7.9 release. 16 | solution: | 17 | Before applying this update, make sure all previously released errata relevant to your system have been applied. For details on how to apply this update, refer to: https://access.redhat.com/articles/11258 18 | references: [] 19 | -------------------------------------------------------------------------------- /pkg/validations/test-resources/config-with-custom-check.yaml: -------------------------------------------------------------------------------- 1 | customChecks: 2 | - name: test-minimum-replicas 3 | description: "some description" 4 | remediation: "some remediation" 5 | template: minimum-replicas 6 | params: 7 | minReplicas: 3 8 | scope: 9 | objectKinds: 10 | - DeploymentLike 11 | checks: 12 | doNotAutoAddDefaults: false 13 | addAllBuiltIn: true 14 | include: 15 | - "host-ipc" 16 | - "host-network" 17 | - "host-pid" 18 | - "non-isolated-pod" 19 | - "pdb-max-unavailable" 20 | - "pdb-min-available" 21 | - "privilege-escalation-container" 22 | - "privileged-container" 23 | - "run-as-non-root" 24 | - "unsafe-sysctls" 25 | - "unset-cpu-requirements" 26 | - "unset-memory-requirements" -------------------------------------------------------------------------------- /konflux-ci/cli-manifests/release-payloads/prod-release-0.7.11-operator.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: appstudio.redhat.com/v1alpha1 3 | kind: Release 4 | metadata: 5 | name: prod-release-operator-20250708-rc2 6 | namespace: dvo-obsint-tenant 7 | spec: 8 | releasePlan: release-plan-prod 9 | snapshot: deployment-validation-operator-nc5rt 10 | data: 11 | releaseNotes: 12 | topic: Deployment Validation Operator 0.7.11 13 | synopsis: Maintenance update 14 | description: | 15 | This release resolves issues with outdated libraries and vulnerabilities present in the 0.7.9 release. 16 | solution: | 17 | Before applying this update, make sure all previously released errata relevant to your system have been applied. For details on how to apply this update, refer to: https://access.redhat.com/articles/11258 18 | references: [] 19 | -------------------------------------------------------------------------------- /konflux-ci/cli-manifests/release-payloads/prod-release-0.7.12-operator.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: appstudio.redhat.com/v1alpha1 3 | kind: Release 4 | metadata: 5 | name: prod-release-operator-20250826-rc1 6 | namespace: dvo-obsint-tenant 7 | spec: 8 | releasePlan: release-plan-prod 9 | snapshot: deployment-validation-operator-klczl 10 | data: 11 | releaseNotes: 12 | topic: Deployment Validation Operator 0.7.12 13 | synopsis: Maintenance update 14 | description: | 15 | This release resolves issues with outdated libraries and vulnerabilities present in the 0.7.11 release. 16 | solution: | 17 | Before applying this update, make sure all previously released errata relevant to your system have been applied. For details on how to apply this update, refer to: https://access.redhat.com/articles/11258 18 | references: [] 19 | -------------------------------------------------------------------------------- /konflux-ci/cli-manifests/release-payloads/prod-release-0.7.13-operator.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: appstudio.redhat.com/v1alpha1 3 | kind: Release 4 | metadata: 5 | name: prod-release-operator-20251113-rc1 6 | namespace: dvo-obsint-tenant 7 | spec: 8 | releasePlan: release-plan-prod 9 | snapshot: deployment-validation-operator-4x9kk 10 | data: 11 | releaseNotes: 12 | topic: Deployment Validation Operator 0.7.13 13 | synopsis: Maintenance update 14 | description: | 15 | This release resolves issues with outdated libraries and vulnerabilities present in the 0.7.12 release. 16 | solution: | 17 | Before applying this update, make sure all previously released errata relevant to your system have been applied. For details on how to apply this update, refer to: https://access.redhat.com/articles/11258 18 | references: [] 19 | -------------------------------------------------------------------------------- /pkg/stringutils/consume.go: -------------------------------------------------------------------------------- 1 | package stringutils 2 | 3 | import "strings" 4 | 5 | // ConsumePrefix checks if *s has the given prefix, and if yes, modifies it 6 | // to remove the prefix. The return value indicates whether the original string 7 | // had the given prefix. 8 | func ConsumePrefix(s *string, prefix string) bool { 9 | orig := *s 10 | if !strings.HasPrefix(orig, prefix) { 11 | return false 12 | } 13 | *s = orig[len(prefix):] 14 | return true 15 | } 16 | 17 | // ConsumeSuffix checks if *s has the given suffix, and if yes, modifies it 18 | // to remove the suffix. The return value indicates whether the original string 19 | // had the given suffix. 20 | func ConsumeSuffix(s *string, suffix string) bool { 21 | orig := *s 22 | if !strings.HasSuffix(orig, suffix) { 23 | return false 24 | } 25 | *s = orig[:len(orig)-len(suffix)] 26 | return true 27 | } 28 | -------------------------------------------------------------------------------- /konflux-ci/fbc/catalog.Dockerfile: -------------------------------------------------------------------------------- 1 | ARG OCP_V=latest 2 | ARG CAT_TYPE=csv.metadata 3 | 4 | # The builder image is expected to contain 5 | # /bin/opm (with serve subcommand) 6 | FROM registry.redhat.io/openshift4/ose-operator-registry-rhel9:$OCP_V as builder 7 | 8 | # Copy FBC root into image at /configs and pre-populate serve cache 9 | ARG CAT_TYPE 10 | ADD $CAT_TYPE/catalog /configs 11 | RUN ["/bin/opm", "serve", "/configs", "--cache-dir=/tmp/cache", "--cache-only"] 12 | 13 | ARG OCP_V 14 | # The base image is expected to contain 15 | # /bin/opm (with serve subcommand) and /bin/grpc_health_probe 16 | FROM registry.redhat.io/openshift4/ose-operator-registry-rhel9:$OCP_V 17 | 18 | # Configure the entrypoint and command 19 | ENTRYPOINT ["/bin/opm"] 20 | CMD ["serve", "/configs", "--cache-dir=/tmp/cache"] 21 | 22 | COPY --from=builder /configs /configs 23 | COPY --from=builder /tmp/cache /tmp/cache 24 | 25 | # Set FBC-specific label for the location of the FBC root directory 26 | # in the image 27 | LABEL operators.operatorframework.io.index.configs.v1=/configs 28 | -------------------------------------------------------------------------------- /pkg/validations/base.go: -------------------------------------------------------------------------------- 1 | package validations 2 | 3 | import ( 4 | "sigs.k8s.io/controller-runtime/pkg/client" 5 | 6 | "github.com/prometheus/client_golang/prometheus" 7 | ) 8 | 9 | // NewRequestFromObject converts a client.Object into 10 | // a validation request. Note that the NamespaceUID of the 11 | // request cannot be derived from the object and should 12 | // be optionally be set after instantiation. 13 | func NewRequestFromObject(obj client.Object) Request { 14 | return Request{ 15 | Kind: obj.GetObjectKind().GroupVersionKind().Kind, 16 | Name: obj.GetName(), 17 | Namespace: obj.GetNamespace(), 18 | UID: string(obj.GetUID()), 19 | } 20 | } 21 | 22 | type Request struct { 23 | Kind string 24 | Name string 25 | Namespace string 26 | NamespaceUID string 27 | UID string 28 | } 29 | 30 | func (r *Request) ToPromLabels() prometheus.Labels { 31 | return prometheus.Labels{ 32 | "kind": r.Kind, 33 | "name": r.Name, 34 | "namespace": r.Namespace, 35 | "namespace_uid": r.NamespaceUID, 36 | "uid": r.UID, 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /pkg/controller/const.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | const ( 4 | 5 | // defaultKubeClientQPS defines the default Queries Per Second (QPS) of the kubeclient used by the operator 6 | DefaultKubeClientQPS = float32(0.5) 7 | 8 | // default number of resources retrieved from the api server per list request 9 | // the usage of list-continue mechanism ensures that the memory consumption 10 | // by this operator always stays under a desired threshold irrespective of the 11 | // number of resource instances for any kubernetes resource 12 | defaultListLimit = 5 13 | 14 | // EnvKubeClientQPS overrides defaultKubeClientQPS 15 | EnvKubeClientQPS string = "KUBECLIENT_QPS" 16 | 17 | // EnvResorucesPerListQuery overrides defaultListLimit 18 | EnvResorucesPerListQuery string = "RESOURCES_PER_LIST_QUERY" 19 | 20 | // EnvNamespaceIgnorePattern sets the pattern for ignoring namespaces from the list of namespaces 21 | // that are in the validate list of this operator 22 | EnvNamespaceIgnorePattern string = "NAMESPACE_IGNORE_PATTERN" 23 | 24 | // EnvValidationCheckInterval sets the frequency of the kube-linter check validations in minutes 25 | EnvValidationCheckInterval string = "VALIDATION_CHECK_INTERVAL" 26 | ) 27 | -------------------------------------------------------------------------------- /konflux-ci/cli-manifests/prod-snapshot.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: appstudio.redhat.com/v1alpha1 3 | kind: Snapshot 4 | metadata: 5 | name: prod-snapshot-0515-rc3 6 | namespace: dvo-obsint-tenant 7 | labels: 8 | test.appstudio.openshift.io/type: override 9 | appstudio.openshift.io/application: deployment-validation-operator 10 | spec: 11 | application: deployment-validation-operator 12 | components: 13 | - name: deployment-validation-operator 14 | containerImage: quay.io/redhat-user-workloads/dvo-obsint-tenant/deployment-validation-operator/deployment-validation-operator@sha256:fc0ac3e259047a8d5992bb57c3a8d104eb9c1cf0cdcee77de2e4405ac03ce24f 15 | source: 16 | git: 17 | url: https://github.com/app-sre/deployment-validation-operator 18 | revision: a62da3432830f7dadc2d014a38e1ca8177700bb5 19 | - name: deployment-validation-operator-bundle 20 | containerImage: quay.io/redhat-user-workloads/dvo-obsint-tenant/deployment-validation-operator-bundle@sha256:14be44a154bd0cb0ddceaa52737c0eadf9b9f56dbfe3eae73610dac439958ba0 21 | source: 22 | git: 23 | url: https://github.com/app-sre/deployment-validation-operator 24 | revision: a62da3432830f7dadc2d014a38e1ca8177700bb5 25 | -------------------------------------------------------------------------------- /bundle/Dockerfile.bundle: -------------------------------------------------------------------------------- 1 | FROM scratch 2 | 3 | COPY ./manifests /manifests 4 | COPY ./metadata /metadata 5 | 6 | LABEL com.redhat.component="deployment-validation-operator-bundle-container" \ 7 | name="app-sre/deployment-validation-operator-bundle" \ 8 | version="1" \ 9 | summary="Deployment Validation Operator for OpenShift" \ 10 | io.openshift.expose-services="" \ 11 | io.k8s.display-name="Deployment Validation Operator Bundle" \ 12 | maintainer="['dvo-owners@redhat.com']" \ 13 | description="Deployment Validation Operator for OpenShift" \ 14 | com.redhat.delivery.operator.bundle=true \ 15 | com.redhat.openshift.versions="v4.14-v4.18" \ 16 | operators.operatorframework.io.bundle.mediatype.v1=registry+v1 \ 17 | operators.operatorframework.io.bundle.manifests.v1=manifests/ \ 18 | operators.operatorframework.io.bundle.metadata.v1=metadata/ \ 19 | operators.operatorframework.io.bundle.package.v1=deployment-validation-operator \ 20 | operators.operatorframework.io.bundle.channels.v1=alpha \ 21 | operators.operatorframework.io.metrics.builder=operator-sdk-v1.31.0+git \ 22 | operators.operatorframework.io.metrics.mediatype.v1=metrics+v1 \ 23 | operators.operatorframework.io.metrics.project_layout=unknown 24 | -------------------------------------------------------------------------------- /pkg/prometheus/prometheus_test.go: -------------------------------------------------------------------------------- 1 | package prometheus 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | 8 | "github.com/prometheus/client_golang/prometheus" 9 | "github.com/prometheus/client_golang/prometheus/collectors" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | // TestGetRouter runs two unit tests with different scenarios: 14 | // - checks that path is available on the router 15 | // - checks that an error on collector registry is handled 16 | func TestGetRouter(t *testing.T) { 17 | 18 | t.Run("router path is available", func(t *testing.T) { 19 | // Given 20 | recorder := httptest.NewRecorder() 21 | mockPath := "/test" 22 | mockReq, _ := http.NewRequest("GET", mockPath, nil) 23 | mockRegistry := prometheus.NewRegistry() 24 | 25 | // When 26 | mux, err := getRouter(mockRegistry, mockPath) 27 | mux.ServeHTTP(recorder, mockReq) 28 | 29 | // Assert 30 | assert.NoError(t, err) 31 | assert.Equal(t, http.StatusOK, recorder.Code) 32 | }) 33 | 34 | t.Run("handle error on collector registry", func(t *testing.T) { 35 | // Given 36 | mockRegistry := prometheus.NewRegistry() 37 | mockCollector := collectors.NewGoCollector() 38 | errMock := mockRegistry.Register(mockCollector) 39 | if errMock != nil { 40 | t.Errorf("Unexpected error at registering mock : %s", errMock.Error()) 41 | } 42 | 43 | // When 44 | _, err := getRouter(mockRegistry, "/test") 45 | 46 | // Assert 47 | assert.Error(t, err) 48 | assert.IsType(t, registerCollectorError{}, err) 49 | }) 50 | } 51 | -------------------------------------------------------------------------------- /konflux-ci/bundle/bundle.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM scratch 2 | 3 | COPY ./manifests /manifests 4 | COPY ./metadata /metadata 5 | 6 | LABEL com.redhat.component="deployment-validation-operator-bundle-container" \ 7 | name="dvo/deployment-validation-operator-bundle" \ 8 | cpe="cpe:/a:redhat:deployment_validator_operator:0.7::el8" \ 9 | version="0.7" \ 10 | release="13" \ 11 | distribution-scope="private" \ 12 | vendor="Red Hat, Inc." \ 13 | url="github.com/app-sre/deployment-validation-operator/" \ 14 | summary="Deployment Validation Operator for OpenShift" \ 15 | io.openshift.expose-services="" \ 16 | io.k8s.display-name="Deployment Validation Operator Bundle" \ 17 | io.k8s.description="Deployment Validation Operator Bundle for OpenShift" \ 18 | maintainer="['dvo-owners@redhat.com']" \ 19 | description="Deployment Validation Operator for OpenShift" \ 20 | com.redhat.delivery.operator.bundle=true \ 21 | com.redhat.openshift.versions="v4.14-v4.18" \ 22 | operators.operatorframework.io.bundle.mediatype.v1=registry+v1 \ 23 | operators.operatorframework.io.bundle.manifests.v1=manifests/ \ 24 | operators.operatorframework.io.bundle.metadata.v1=metadata/ \ 25 | operators.operatorframework.io.bundle.package.v1=deployment-validation-operator \ 26 | operators.operatorframework.io.bundle.channels.v1=alpha \ 27 | operators.operatorframework.io.metrics.builder=operator-sdk-v1.31.0+git \ 28 | operators.operatorframework.io.metrics.mediatype.v1=metrics+v1 \ 29 | operators.operatorframework.io.metrics.project_layout=unknown 30 | -------------------------------------------------------------------------------- /internal/options/options_test.go: -------------------------------------------------------------------------------- 1 | package options 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | // TestOptionsStruct runs four tests on options struct methods: 11 | // - processEnv 12 | // - GetWatchNamespace 13 | // - GetWatchNamespace (flawed) 14 | // - MetricsEndpoint 15 | func TestOptionsStruct(t *testing.T) { 16 | 17 | t.Run("processEnv function", func(t *testing.T) { 18 | // Given 19 | opt := Options{} 20 | expectedValue := "test" 21 | os.Setenv(watchNamespaceEnvVar, expectedValue) 22 | 23 | // When 24 | opt.processEnv() 25 | 26 | // Assert 27 | assert.Equal(t, expectedValue, *opt.watchNamespace) 28 | }) 29 | 30 | t.Run("GetWatchNamespace function (no result)", func(t *testing.T) { 31 | // Given 32 | opt := Options{} 33 | 34 | // When 35 | _, isSet := opt.GetWatchNamespace() 36 | 37 | // Assert 38 | assert.Equal(t, false, isSet) 39 | }) 40 | 41 | t.Run("GetWatchNamespace function", func(t *testing.T) { 42 | // Given 43 | expectedValue := "test" 44 | opt := Options{ 45 | watchNamespace: &expectedValue, 46 | } 47 | 48 | // When 49 | namespace, isSet := opt.GetWatchNamespace() 50 | 51 | // Assert 52 | assert.Equal(t, true, isSet) 53 | assert.Equal(t, expectedValue, namespace) 54 | }) 55 | 56 | t.Run("MetricsEndpoint", func(t *testing.T) { 57 | // Given 58 | mockPort := int32(80) 59 | mockPath := "path/" 60 | opt := Options{ 61 | MetricsPort: mockPort, 62 | MetricsPath: mockPath, 63 | } 64 | 65 | // When 66 | endpoint := opt.MetricsEndpoint() 67 | 68 | // Assert 69 | assert.Equal(t, "http://0.0.0.0:80/path/", endpoint) 70 | }) 71 | } 72 | -------------------------------------------------------------------------------- /konflux-ci/cli-manifests/staging-snapshot.yaml: -------------------------------------------------------------------------------- 1 | # Applying this manifest will create a snapshot in Konflux with specific images for the operator and the bundle 2 | # Since images could be built in different PRs there could be some misalignment between the bundle and the operator images 3 | # Retrieve the specific images SHA and last revision: 4 | # * from the Konflux UI: 5 | # in the Application > Components > [component] 6 | # Latest build > Build container image 7 | # Latest image > image label 8 | # * from the CLI: 9 | # $ oc get component deployment-validation-operator -o yaml | yq .status.lastPromotedImage 10 | # $ oc get component deployment-validation-operator -o yaml | yq .status.lastBuiltCommit 11 | --- 12 | apiVersion: appstudio.redhat.com/v1alpha1 13 | kind: Snapshot 14 | metadata: 15 | name: staging-snapshot-0425-01 16 | namespace: dvo-obsint-tenant 17 | labels: 18 | test.appstudio.openshift.io/type: override 19 | appstudio.openshift.io/application: deployment-validation-operator 20 | spec: 21 | application: deployment-validation-operator 22 | components: 23 | - name: deployment-validation-operator 24 | containerImage: quay.io/redhat-user-workloads/dvo-obsint-tenant/deployment-validation-operator/deployment-validation-operator@sha256:63883be503162382f850c20dd3f034b69ea6ab6eb04c497e4254e17a09e99587 25 | source: 26 | git: 27 | url: https://github.com/app-sre/deployment-validation-operator 28 | revision: ea6bc934725a0653d6460cee294e98d66ed146cb 29 | - name: deployment-validation-operator-bundle 30 | containerImage: quay.io/redhat-user-workloads/dvo-obsint-tenant/deployment-validation-operator-bundle@sha256:05fcd25a14821f3259f7940874e5a78e62404603ea7910a5c379a726fbed91ea 31 | source: 32 | git: 33 | url: https://github.com/app-sre/deployment-validation-operator 34 | revision: ea6bc934725a0653d6460cee294e98d66ed146cb 35 | -------------------------------------------------------------------------------- /pkg/validations/base_test.go: -------------------------------------------------------------------------------- 1 | package validations 2 | 3 | import ( 4 | "testing" 5 | 6 | "sigs.k8s.io/controller-runtime/pkg/client" 7 | 8 | "github.com/prometheus/client_golang/prometheus" 9 | 10 | appsv1 "k8s.io/api/apps/v1" 11 | 12 | "github.com/stretchr/testify/assert" 13 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 14 | ) 15 | 16 | func TestRequest(t *testing.T) { 17 | t.Parallel() 18 | 19 | for name, tc := range map[string]struct { 20 | Object client.Object 21 | NamespaceUID string 22 | ExpectedLabels prometheus.Labels 23 | }{ 24 | "without namespace UID": { 25 | Object: &appsv1.Deployment{ 26 | TypeMeta: metav1.TypeMeta{ 27 | Kind: "Deployment", 28 | }, 29 | ObjectMeta: metav1.ObjectMeta{ 30 | Name: "test", 31 | Namespace: "test-namespace", 32 | UID: "abcdefgh", 33 | }, 34 | }, 35 | ExpectedLabels: prometheus.Labels{ 36 | "kind": "Deployment", 37 | "name": "test", 38 | "namespace": "test-namespace", 39 | "namespace_uid": "", 40 | "uid": "abcdefgh", 41 | }, 42 | }, 43 | "with namespace UID": { 44 | Object: &appsv1.Deployment{ 45 | TypeMeta: metav1.TypeMeta{ 46 | Kind: "Deployment", 47 | }, 48 | ObjectMeta: metav1.ObjectMeta{ 49 | Name: "test", 50 | Namespace: "test-namespace", 51 | UID: "abcdefgh", 52 | }, 53 | }, 54 | NamespaceUID: "12345678", 55 | ExpectedLabels: prometheus.Labels{ 56 | "kind": "Deployment", 57 | "name": "test", 58 | "namespace": "test-namespace", 59 | "namespace_uid": "12345678", 60 | "uid": "abcdefgh", 61 | }, 62 | }, 63 | } { 64 | tc := tc 65 | 66 | t.Run(name, func(t *testing.T) { 67 | t.Parallel() 68 | 69 | req := NewRequestFromObject(tc.Object) 70 | req.NamespaceUID = tc.NamespaceUID 71 | 72 | assert.Equal(t, tc.ExpectedLabels, req.ToPromLabels()) 73 | }) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /internal/options/options.go: -------------------------------------------------------------------------------- 1 | package options 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/spf13/pflag" 9 | "go.uber.org/zap/zapcore" 10 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 11 | ) 12 | 13 | type Options struct { 14 | MetricsPort int32 15 | MetricsPath string 16 | ProbeAddr string 17 | ConfigFile string 18 | watchNamespace *string 19 | Zap zap.Options 20 | } 21 | 22 | func (o *Options) MetricsEndpoint() string { 23 | return fmt.Sprintf("http://0.0.0.0:%d/%s", o.MetricsPort, o.MetricsPath) 24 | } 25 | 26 | func (o *Options) GetWatchNamespace() (string, bool) { 27 | if o.watchNamespace == nil { 28 | return "", false 29 | } 30 | 31 | return *o.watchNamespace, true 32 | } 33 | 34 | func (o *Options) Process() { 35 | o.processFlags() 36 | o.processEnv() 37 | } 38 | 39 | func (o *Options) processFlags() { 40 | // Add the zap logger flag set to the CLI. The flag set must 41 | // be added before calling pflag.Parse(). 42 | o.Zap.BindFlags(flag.CommandLine) 43 | o.Zap.Level = zapcore.Level(-1) // Set to debug to not alter current behavior. TODO: Remove when configurable. 44 | 45 | // Add flags registered by imported packages (e.g. glog and 46 | // controller-runtime) 47 | pflag.CommandLine.AddGoFlagSet(flag.CommandLine) 48 | 49 | // Add app specific flags 50 | flags := pflag.NewFlagSet("dvo", pflag.ExitOnError) 51 | flags.StringVar( 52 | &o.ConfigFile, 53 | "config", o.ConfigFile, 54 | "Path to config file", 55 | ) 56 | flags.StringVar( 57 | &o.ProbeAddr, 58 | "health-probe-bind-address", o.ProbeAddr, 59 | "The address the probe endpoint binds to.", 60 | ) 61 | 62 | pflag.CommandLine.AddFlagSet(flags) 63 | 64 | pflag.Parse() 65 | } 66 | 67 | // WatchNamespaceEnvVar is the constant for env variable WATCH_NAMESPACE 68 | // which specifies the Namespace to watch. 69 | // An empty value means the operator is running with cluster scope. 70 | const watchNamespaceEnvVar = "WATCH_NAMESPACE" 71 | 72 | func (o *Options) processEnv() { 73 | if val, ok := os.LookupEnv(watchNamespaceEnvVar); ok { 74 | o.watchNamespace = &val 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /OWNERS_ALIASES: -------------------------------------------------------------------------------- 1 | # ================================ DO NOT EDIT ================================ 2 | # This file is managed in https://github.com/openshift/boilerplate 3 | # See the OWNERS_ALIASES docs: https://git.k8s.io/community/contributors/guide/owners.md#OWNERS_ALIASES 4 | # ============================================================================= 5 | aliases: 6 | srep-functional-team-aurora: 7 | - abyrne55 8 | - AlexVulaj 9 | - dakotalongRH 10 | - lnguyen1401 11 | - luis-falcon 12 | - rafael-azevedo 13 | - reedcort 14 | srep-functional-team-fedramp: 15 | - tonytheleg 16 | - theautoroboto 17 | - rhdedgar 18 | - katherinelc321 19 | - robotmaxtron 20 | - rojasreinold 21 | - hbhushan3 22 | - fsferraz-rh 23 | srep-functional-team-hulk: 24 | - a7vicky 25 | - rendhalver 26 | - ravitri 27 | - shitaljante 28 | - weherdh 29 | - devppratik 30 | srep-functional-team-orange: 31 | - bergmannf 32 | - bng0y 33 | - typeid 34 | - Makdaam 35 | - mrWinston 36 | - Nikokolas3270 37 | - ninabauer 38 | - RaphaelBut 39 | - Tessg22 40 | srep-functional-team-rocket: 41 | - aliceh 42 | - anispate 43 | - clcollins 44 | - iamkirkbater 45 | - Mhodesty 46 | - nephomaniac 47 | - tnierman 48 | srep-functional-team-security: 49 | - gsleeman 50 | - jaybeeunix 51 | - sam-nguyen7 52 | - wshearn 53 | - dem4gus 54 | - npecka 55 | srep-functional-team-thor: 56 | - bmeng 57 | - MitaliBhalla 58 | - hectorakemp 59 | - feichashao 60 | - Tafhim 61 | - samanthajayasinghe 62 | srep-functional-leads: 63 | - rafael-azevedo 64 | - iamkirkbater 65 | - Nikokolas3270 66 | - theautoroboto 67 | - bmeng 68 | - mjlshen 69 | - sam-nguyen7 70 | - ravitri 71 | srep-team-leads: 72 | - NautiluX 73 | - rogbas 74 | - fahlmant 75 | - dustman9000 76 | - wanghaoran1988 77 | - bng0y 78 | sre-group-leads: 79 | - apahim 80 | - maorfr 81 | - rogbas 82 | srep-architects: 83 | - jewzaam 84 | - jharrington22 85 | - cblecker 86 | -------------------------------------------------------------------------------- /deploy/bundle/template/deployment-validation-operator.clusterserviceversion.yaml: -------------------------------------------------------------------------------- 1 | ## Commented fields in this template will be filled by 2 | ## hack/generate-operator-bundle-contents.py 3 | --- 4 | apiVersion: operators.coreos.com/v1alpha1 5 | kind: ClusterServiceVersion 6 | metadata: 7 | annotations: 8 | capabilities: Basic Install 9 | categories: Application Runtime, Monitoring, Security 10 | certified: "false" 11 | # containerImage: ${IMAGE}:${IMAGE_TAG} 12 | createdAt: 08/25/2020 13 | support: Best Effort 14 | repository: https://github.com/app-sre/deployment-validation-operator 15 | description: |- 16 | The deployment validation operator 17 | # name: deployment-validation-operator.v${VERSION} 18 | spec: 19 | description: 'The Deployment Validation Operator (DVO) checks deployments and other resources against a curated collection of best practices. These best practices focus mainly on ensuring that the applications are fault-tolerant. DVO reports failed validations via Prometheus metrics. If the best-practice check has failed, the metrics will report `1`.' 20 | displayName: Deployment Validation Operator 21 | install: 22 | spec: 23 | deployments: 24 | - name: deployment-validation-operator 25 | ### content of deploy/openshift/deployments.yaml 26 | clusterPermissions: 27 | ### content of deploy/openshift/cluster-role.yaml 28 | permissions: 29 | ### content of deploy/openshift/role.yaml 30 | strategy: deployment 31 | installModes: 32 | - supported: true 33 | type: OwnNamespace 34 | - supported: true 35 | type: SingleNamespace 36 | - supported: true 37 | type: AllNamespaces 38 | - supported: false 39 | type: MultiNamespace 40 | keywords: 41 | - dvo 42 | labels: 43 | alm-owner-dvo: deployment-validation-operator 44 | operated-by: deployment-validation-operator 45 | links: 46 | - name: repository 47 | url: https://github.com/app-sre/deployment-validation-operator 48 | - name: containerImage 49 | # url: https://${IMAGE}:${IMAGE_TAG} 50 | maturity: alpha 51 | provider: 52 | name: Red Hat 53 | selector: 54 | matchLabels: 55 | alm-owner-dvo: deployment-validation-operator 56 | operated-by: deployment-validation-operator 57 | # version: ${VERSION} 58 | -------------------------------------------------------------------------------- /pkg/utils/object.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "golang.stackrox.io/kube-linter/pkg/objectkinds" 5 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 6 | "k8s.io/apimachinery/pkg/runtime/schema" 7 | "k8s.io/client-go/discovery" 8 | "k8s.io/client-go/rest" 9 | 10 | logf "sigs.k8s.io/controller-runtime/pkg/log" 11 | 12 | "sigs.k8s.io/controller-runtime/pkg/client" 13 | ) 14 | 15 | var log = logf.Log.WithName("DeploymentValidation") 16 | 17 | var deploymentLikeMatcher, _ = objectkinds.ConstructMatcher(objectkinds.DeploymentLike) 18 | 19 | // IsController returns true if the object passed in does 20 | // not have a controller associated with it 21 | func IsController(obj metav1.Object) bool { 22 | controller := metav1.GetControllerOf(obj) 23 | return controller == nil 24 | } 25 | 26 | // IsOwner returns false if the object is deployment-like resource and owned by a deployment-like resource 27 | func IsOwner(obj client.Object) bool { 28 | gvk := obj.GetObjectKind().GroupVersionKind() 29 | // no need to check owner for none deployment-like resource 30 | if deploymentLikeMatcher.Matches(gvk) { 31 | for _, ref := range obj.GetOwnerReferences() { 32 | refGvk := schema.FromAPIVersionAndKind(ref.APIVersion, ref.Kind) 33 | // get a deployment-like owner 34 | if deploymentLikeMatcher.Matches(refGvk) { 35 | return false 36 | } 37 | } 38 | } 39 | return true 40 | } 41 | 42 | // IsOpenshift identify environment and returns true if its openshift else false 43 | func IsOpenshift(osKind map[string]bool) (bool, error) { 44 | log.Info("Checking User Environment in IsOpenshift.") 45 | config, err := rest.InClusterConfig() 46 | if err != nil { 47 | return false, err 48 | } 49 | discoveryclient, err := discovery.NewDiscoveryClientForConfig(config) 50 | if err != nil { 51 | return false, err 52 | } 53 | 54 | lists, err := discoveryclient.ServerPreferredResources() 55 | if err != nil { 56 | return false, err 57 | } 58 | 59 | for _, list := range lists { 60 | if len(list.APIResources) == 0 { 61 | continue 62 | } 63 | // Check for acceptable kinds in current Env 64 | for _, resource := range list.APIResources { 65 | if len(resource.Verbs) == 0 { 66 | continue 67 | } 68 | if osKind[resource.Kind] { 69 | return true, nil 70 | } 71 | } 72 | } 73 | 74 | return false, err 75 | } 76 | -------------------------------------------------------------------------------- /pkg/testutils/testutils.go: -------------------------------------------------------------------------------- 1 | package testutils 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "path" 8 | "reflect" 9 | "runtime" 10 | "strings" 11 | "text/template" 12 | 13 | "github.com/ghodss/yaml" 14 | "github.com/mcuadros/go-defaults" 15 | apps_v1 "k8s.io/api/apps/v1" 16 | ) 17 | 18 | type TemplateArgs struct { 19 | Replicas int `default:"3"` 20 | ResourceLimits bool `default:"true"` 21 | ResourceRequests bool `default:"true"` 22 | } 23 | 24 | func NewTemplateArgs() *TemplateArgs { 25 | args := new(TemplateArgs) 26 | defaults.SetDefaults(args) 27 | return args 28 | } 29 | 30 | func CreateReplicaSetFromTemplate(args *TemplateArgs) (apps_v1.ReplicaSet, error) { 31 | var replicaSet apps_v1.ReplicaSet 32 | 33 | yamlManifest, err := createYamlManifest("ReplicaSet", args) 34 | if err != nil { 35 | return replicaSet, err 36 | } 37 | 38 | // deserialise from YAML by using the json struct tags that are defined in the k8s API object structs 39 | err = yaml.Unmarshal(yamlManifest, &replicaSet) 40 | if err != nil { 41 | return replicaSet, err 42 | } 43 | 44 | return replicaSet, nil 45 | } 46 | 47 | func CreateDeploymentFromTemplate(args *TemplateArgs) (apps_v1.Deployment, error) { 48 | var deployment apps_v1.Deployment 49 | 50 | yamlManifest, err := createYamlManifest("Deployment", args) 51 | if err != nil { 52 | return deployment, err 53 | } 54 | 55 | // deserialise from YAML by using the json struct tags that are defined in the k8s API object structs 56 | err = yaml.Unmarshal(yamlManifest, &deployment) 57 | if err != nil { 58 | return deployment, err 59 | } 60 | 61 | return deployment, nil 62 | } 63 | 64 | func createYamlManifest(objectType string, args *TemplateArgs) ([]byte, error) { 65 | tpl, err := template.ParseFiles(templatePath(objectType)) 66 | if err != nil { 67 | return nil, err 68 | } 69 | 70 | templateContent := &bytes.Buffer{} 71 | if err := tpl.Execute(templateContent, args); err != nil { 72 | return nil, err 73 | } 74 | 75 | manifest, err := io.ReadAll(templateContent) 76 | if err != nil { 77 | return nil, err 78 | } 79 | 80 | return manifest, nil 81 | } 82 | 83 | func templatePath(objectName string) string { 84 | _, thisFile, _, _ := runtime.Caller(0) 85 | return path.Join(path.Dir(thisFile), fmt.Sprintf("templates/%s.yaml.tpl", objectName)) 86 | } 87 | 88 | func ObjectKind(obj interface{}) string { 89 | kind := reflect.TypeOf(obj).String() 90 | return strings.SplitN(kind, ".", 2)[1] 91 | } 92 | -------------------------------------------------------------------------------- /ci/codecov.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -o errexit 4 | set -o nounset 5 | set -o pipefail 6 | 7 | REPO_ROOT=$(git rev-parse --show-toplevel) 8 | CI_SERVER_URL=https://prow.svc.ci.openshift.org/view/gcs/origin-ci-test 9 | COVER_PROFILE=${COVER_PROFILE:-coverage.out} 10 | JOB_TYPE=${JOB_TYPE:-"local"} 11 | 12 | # Default concurrency to four threads. By default it's the number of procs, 13 | # which seems to be 16 in the CI env. Some consumers' coverage jobs were 14 | # regularly getting OOM-killed; so do this rather than boost the pod resources 15 | # unreasonably. 16 | COV_THREAD_COUNT=${COV_THREAD_COUNT:-4} 17 | make -C "${REPO_ROOT}" test-coverage TESTOPTS="-coverprofile=${COVER_PROFILE}.tmp -covermode=atomic -coverpkg=./... -p ${COV_THREAD_COUNT}" 18 | 19 | # Remove generated files from coverage profile 20 | grep -v "zz_generated" "${COVER_PROFILE}.tmp" > "${COVER_PROFILE}" 21 | rm -f "${COVER_PROFILE}.tmp" 22 | 23 | # Configure the git refs and job link based on how the job was triggered via prow 24 | if [[ "${JOB_TYPE}" == "presubmit" ]]; then 25 | echo "detected PR code coverage job for #${PULL_NUMBER}" 26 | REF_FLAGS="-P ${PULL_NUMBER} -C ${PULL_PULL_SHA}" 27 | JOB_LINK="${CI_SERVER_URL}/pr-logs/pull/${REPO_OWNER}_${REPO_NAME}/${PULL_NUMBER}/${JOB_NAME}/${BUILD_ID}" 28 | elif [[ "${JOB_TYPE}" == "postsubmit" ]]; then 29 | echo "detected branch code coverage job for ${PULL_BASE_REF}" 30 | REF_FLAGS="-B ${PULL_BASE_REF} -C ${PULL_BASE_SHA}" 31 | JOB_LINK="${CI_SERVER_URL}/logs/${JOB_NAME}/${BUILD_ID}" 32 | elif [[ "${JOB_TYPE}" == "local" ]]; then 33 | echo "coverage report available at ${COVER_PROFILE}" 34 | exit 0 35 | else 36 | echo "${JOB_TYPE} jobs not supported" >&2 37 | exit 1 38 | fi 39 | 40 | # Configure certain internal codecov variables with values from prow. 41 | export CI_BUILD_URL="${JOB_LINK}" 42 | export CI_BUILD_ID="${JOB_NAME}" 43 | export CI_JOB_ID="${BUILD_ID}" 44 | 45 | if [[ "${JOB_TYPE}" != "local" ]]; then 46 | if [[ -z "${ARTIFACT_DIR:-}" ]] || [[ ! -d "${ARTIFACT_DIR}" ]] || [[ ! -w "${ARTIFACT_DIR}" ]]; then 47 | echo '${ARTIFACT_DIR} must be set for non-local jobs, and must point to a writable directory' >&2 48 | exit 1 49 | fi 50 | curl -sS https://codecov.io/bash -o "${ARTIFACT_DIR}/codecov.sh" 51 | bash <(cat "${ARTIFACT_DIR}/codecov.sh") -Z -K -f "${COVER_PROFILE}" -r "${REPO_OWNER}/${REPO_NAME}" ${REF_FLAGS} 52 | else 53 | bash <(curl -s https://codecov.io/bash) -Z -K -f "${COVER_PROFILE}" -r "${REPO_OWNER}/${REPO_NAME}" ${REF_FLAGS} 54 | fi 55 | -------------------------------------------------------------------------------- /docs/architecture.md: -------------------------------------------------------------------------------- 1 | ![image info](./images/DVO_Human_Developer_Workflow.drawio.png "DVO Human-Developer Workflow") 2 | 3 | Code Base: A developer interacts with the code base to add features, create new releases, create unit tests, modify files 4 | 5 | GitHub: Submits PRs to DVO Repository, approve/reject PRs 6 | 7 | Jenkins: Verifies the DVO build job has kicked off during new release process 8 | 9 | Quay.io: Verifies the newly created DVO image exists in the corresponding repository 10 | 11 | OLM: Submits PR during DVO release process so versioning matches with new DVO image 12 | 13 | Cluster: Utilizes/Verifies changes work by deploying DVO to an OSD cluster and executing appropriate actions to test the change in functionality 14 | 15 | 16 | ![image info](./images/DVO_Human_Consumer_Workflow.drawio.png "DVO Human-Consumer Workflow") 17 | 18 | Code Base: A consumer interacts with the code base perform a manual/OLM installaion of DVO 19 | 20 | GitHub: A consumer will view the README to understand how they deploy DVO successfully either manually or via OLM 21 | 22 | Cluster: A consumer will need a cluster to deploy DVO to and interact with (future tense will be able to deploy via catalog) 23 | 24 | 25 | 26 | ![image info](./images/DVO_Operator_Managed.drawio.png "DVO Operator Managed Deployment") 27 | 28 | This diagram represents the resources deployed in a DVO Operator Managed Deployment. When a user deploys DVO via OLM this diagram shows the components that are created when connecting to the OLM catalog and deploying the operator. This configuration comes up with a subscription to operaterhub to check for upgrades (default 24h). 29 | 30 | 31 | 32 | ![image info](./images/DVO_Observability.drawio.png "DVO Observability Deployment") 33 | 34 | This diagram represents the resources deployed in a DVO Observability Deployment. All of the components listed here make up what is needed to setup the Prometheus/Grafana parts of DVO. This setup allows for monitoring DVO for validation checks on resources and reporting when a validation check fails in an easily consumed format. 35 | 36 | 37 | ![image info](./images/DVO_Internal_Codebase.drawio.png "DVO Internal Codebase") 38 | 39 | K8s API - allows for DVO to interact with Kube/Openshift objects 40 | 41 | K8s Controller Runtime - Enables DVO to utilzie the reconciliation loop to constantly be checking for new K8s resources to validate 42 | 43 | Kube-linter - provides standard templates for upstream validation checks 44 | 45 | Prometheus - Provides metrics if a validation check has failed 46 | 47 | OSDE2E - Provides end-to-end test framework and automated testing 48 | -------------------------------------------------------------------------------- /pkg/controller/watchnamespaces_test.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "os" 5 | "regexp" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | corev1 "k8s.io/api/core/v1" 10 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 11 | "k8s.io/apimachinery/pkg/types" 12 | ) 13 | 14 | // TestWatchNamespacesCache runs five tests on watchNamespacesCache struct methods: 15 | // - checks that instantiation takes care of env variable conf 16 | // - checks getNamespaceUID returns valid uid or empty string depending on argument 17 | // - checks getFormattedNamespaces returns formatted 18 | // - checks getFormattedNamespaces filters unwanted namespaces 19 | func TestWatchNamespacesCache(t *testing.T) { 20 | 21 | t.Run("instantiation with ignore pattern Env variable set", func(t *testing.T) { 22 | // Given 23 | os.Setenv(EnvNamespaceIgnorePattern, "mock") 24 | 25 | // When 26 | wnc := newWatchNamespacesCache() 27 | 28 | // Assert 29 | assert.Equal(t, "mock", wnc.ignorePattern.String()) 30 | }) 31 | 32 | t.Run("getNamespaceUID returns an existing uid", func(t *testing.T) { 33 | // Given 34 | expected := "mock" 35 | wnc := watchNamespacesCache{ 36 | namespaces: &[]namespace{ 37 | {name: "test", uid: expected}, 38 | }, 39 | } 40 | 41 | // When 42 | test := wnc.getNamespaceUID("test") 43 | 44 | // Assert 45 | assert.Equal(t, expected, test) 46 | }) 47 | 48 | t.Run("getNamespaceUID returns void string on non-existing uid", func(t *testing.T) { 49 | // Given 50 | wnc := watchNamespacesCache{ 51 | namespaces: &[]namespace{ 52 | {name: "test", uid: "mock"}, 53 | }, 54 | } 55 | 56 | // When 57 | test := wnc.getNamespaceUID("fail") 58 | 59 | // Assert 60 | assert.Equal(t, "", test) 61 | }) 62 | 63 | t.Run("getFormattedNamespaces returns object data formatted", func(t *testing.T) { 64 | // Given 65 | expectedName := "mock" 66 | expectedUID := "1234" 67 | mockNamespaceList := corev1.NamespaceList{ 68 | Items: []corev1.Namespace{ 69 | { 70 | ObjectMeta: v1.ObjectMeta{ 71 | Name: expectedName, UID: types.UID(expectedUID), 72 | }, 73 | }, 74 | }, 75 | } 76 | 77 | // When 78 | ns := getFormattedNamespaces(mockNamespaceList, nil) 79 | 80 | // Assert 81 | assert.Len(t, ns, 1) 82 | assert.Equal(t, expectedName, ns[0].name) 83 | assert.Equal(t, expectedUID, ns[0].uid) 84 | }) 85 | 86 | t.Run("getFormattedNamespaces ignores namespace with given pattern", func(t *testing.T) { 87 | // Given 88 | mockNamespaceList := corev1.NamespaceList{ 89 | Items: []corev1.Namespace{ 90 | { 91 | ObjectMeta: v1.ObjectMeta{Name: "mock"}, 92 | }, 93 | }, 94 | } 95 | 96 | // When 97 | ns := getFormattedNamespaces(mockNamespaceList, regexp.MustCompile("mock")) 98 | 99 | // Assert 100 | assert.Len(t, ns, 0) 101 | }) 102 | } 103 | -------------------------------------------------------------------------------- /pkg/controller/reconcileobjects_test.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "k8s.io/apimachinery/pkg/runtime" 8 | "k8s.io/apimachinery/pkg/runtime/schema" 9 | ) 10 | 11 | // TestGetPriorityVersion runs four tests on this method of resourceSet 12 | // - checks existingVersion is returned if no groups are returned by the scheme 13 | // - checks existingVersion is returned if there is no match on given group 14 | // - checks existingVersion is returned if group version match existing version 15 | // - checks currentVersion is returned if group version match current version 16 | func TestGetPriorityVersion(t *testing.T) { 17 | 18 | setSchemeVPErrorCatching := func(s *runtime.Scheme, g string, v string) { 19 | err := s.SetVersionPriority([]schema.GroupVersion{{Group: g, Version: v}}...) 20 | if err != nil { 21 | t.Fail() 22 | } 23 | } 24 | 25 | t.Run("Empty PrioritizedVersionsAllGroups return", func(t *testing.T) { 26 | // Given 27 | mock := resourceSet{scheme: runtime.NewScheme()} 28 | 29 | // When 30 | test := mock.getPriorityVersion("", "existingVersion", "") 31 | 32 | // Assert 33 | assert.Equal(t, "existingVersion", test) 34 | }) 35 | 36 | t.Run("No group match", func(t *testing.T) { 37 | // Given 38 | scheme := runtime.NewScheme() 39 | setSchemeVPErrorCatching(scheme, "group", "version") 40 | mock := resourceSet{scheme: scheme} 41 | 42 | // When 43 | test := mock.getPriorityVersion("no-match", "existingVersion", "") 44 | 45 | // Assert 46 | assert.Equal(t, "existingVersion", test) 47 | }) 48 | 49 | t.Run("Group match with existing version", func(t *testing.T) { 50 | // Given 51 | scheme := runtime.NewScheme() 52 | setSchemeVPErrorCatching(scheme, "group", "existingVersion") 53 | setSchemeVPErrorCatching(scheme, "group2", "currentVersion") 54 | mock := resourceSet{scheme: scheme} 55 | 56 | // When 57 | test := mock.getPriorityVersion("group", "existingVersion", "currentVersion") 58 | 59 | // Assert 60 | assert.Equal(t, "existingVersion", test) 61 | }) 62 | 63 | t.Run("Group match with current version", func(t *testing.T) { 64 | // Given 65 | scheme := runtime.NewScheme() 66 | setSchemeVPErrorCatching(scheme, "group", "existingVersion") 67 | setSchemeVPErrorCatching(scheme, "group2", "currentVersion") 68 | mock := resourceSet{scheme: scheme} 69 | 70 | // When 71 | test := mock.getPriorityVersion("group2", "existingVersion", "currentVersion") 72 | 73 | // Assert 74 | assert.Equal(t, "currentVersion", test) 75 | }) 76 | 77 | t.Run("getPriorityVersion : group match with no match version", func(t *testing.T) { 78 | // Given 79 | scheme := runtime.NewScheme() 80 | setSchemeVPErrorCatching(scheme, "group", "version") 81 | setSchemeVPErrorCatching(scheme, "group2", "version2") 82 | mock := resourceSet{scheme: scheme} 83 | 84 | // When 85 | test := mock.getPriorityVersion("group2", "existingVersion", "") 86 | 87 | // Assert 88 | assert.Equal(t, "existingVersion", test) 89 | }) 90 | } 91 | -------------------------------------------------------------------------------- /pkg/controller/watchnamespaces.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "regexp" 8 | 9 | corev1 "k8s.io/api/core/v1" 10 | "sigs.k8s.io/controller-runtime/pkg/client" 11 | ) 12 | 13 | // TODO : Evaluate changing uid type: string -> types.UID 14 | // This data is populated with types.UID from corev1/ObjectMeta 15 | // No access to this property out of the method getNamespaceUID 16 | type namespace struct { 17 | uid, name string 18 | } 19 | 20 | type watchNamespacesCache struct { 21 | namespaces *[]namespace 22 | ignorePattern *regexp.Regexp 23 | } 24 | 25 | // newWatchNamespacesCache returns a new watchNamespacesCache instance 26 | // if EnvNamespaceIgnorePattern Env variable is set, ignorePattern field 27 | // is populated with the information in the variable 28 | // sidenote: regexp will throw a Panic in case data cannot be parsed 29 | func newWatchNamespacesCache() *watchNamespacesCache { 30 | ignorePatternStr := os.Getenv(EnvNamespaceIgnorePattern) 31 | var nsIgnoreRegex *regexp.Regexp 32 | if ignorePatternStr != "" { 33 | nsIgnoreRegex = regexp.MustCompile(ignorePatternStr) 34 | } 35 | return &watchNamespacesCache{ 36 | ignorePattern: nsIgnoreRegex, 37 | } 38 | } 39 | 40 | // getFormattedNamespaces returns a list of namespaces filtering through given list 41 | // and formatting them as namespace structs 42 | func getFormattedNamespaces(list corev1.NamespaceList, ignorePattern *regexp.Regexp) (fns []namespace) { 43 | for _, ns := range list.Items { 44 | name := ns.GetName() 45 | if ignorePattern == nil || !ignorePattern.Match([]byte(name)) { 46 | fns = append(fns, namespace{uid: string(ns.GetUID()), name: name}) 47 | } 48 | } 49 | return 50 | } 51 | 52 | // setCache is a setter for the namespaces field 53 | func (nsc *watchNamespacesCache) setCache(namespaces *[]namespace) { 54 | nsc.namespaces = namespaces 55 | } 56 | 57 | // getNamespaceUID returns the namespace uid given a valid namespace as argument 58 | func (nsc *watchNamespacesCache) getNamespaceUID(namespace string) string { 59 | for _, ns := range *nsc.namespaces { 60 | if ns.name == namespace { 61 | return ns.uid 62 | } 63 | } 64 | return "" 65 | } 66 | 67 | // resetCache empties the namespaces field 68 | func (nsc *watchNamespacesCache) resetCache() { 69 | nsc.setCache(nil) 70 | } 71 | 72 | // getWatchNamespaces returns the namespaces field with a list of namespaces structs 73 | // If the field was not set, it will populate it with objects from given client 74 | // If the ignorePattern field is set, it will filter the matches 75 | func (nsc *watchNamespacesCache) getWatchNamespaces(ctx context.Context, c client.Client) (*[]namespace, error) { 76 | if nsc.namespaces == nil { 77 | namespaceList := corev1.NamespaceList{} 78 | if err := c.List(ctx, &namespaceList); err != nil { 79 | return nil, fmt.Errorf("listing %s: %w", namespaceList.GroupVersionKind().String(), err) 80 | } 81 | 82 | watchNamespaces := getFormattedNamespaces(namespaceList, nsc.ignorePattern) 83 | nsc.setCache(&watchNamespaces) 84 | } 85 | 86 | return nsc.namespaces, nil 87 | } 88 | -------------------------------------------------------------------------------- /docs/new-releases.md: -------------------------------------------------------------------------------- 1 | ### New Release 2 | 3 | **Before Proceeding:** 4 | * Assure desired changes for new release have been submitted and merged successfully 5 | * Check with team to verify if this is a MAJOR, MINOR, or a PATCH release 6 | 7 | **Release Process:** 8 | 1. Create a new DVO release in GitHub 9 | 10 | - Create new release on GitHub page from the right column 11 | - Follow the model of Major/Minor/Patch (x/y/z, 0.1.2) 12 | - Provide a description of the release (Auto-generate or Manually) 13 | 14 | 2. Publish the new DVO release to Quay.io (no action required) 15 | 16 | - Generating a new tagged release in GitHub will trigger a jenkins job that will build a new image of DVO with the new release tag 17 | - Verify Jenkins Job was successful - [DVO Jenkins](https://ci.int.devshift.net/view/deployment-validation-operator/job/app-sre-deployment-validation-operator-gh-build-tag/) 18 | 19 | 3. Publish new DVO release to Operator-Hub 20 | 21 | - OperatorHub Repository for DVO - [DVO OLM](https://github.com/k8s-operatorhub/community-operators/tree/main/operators/deployment-validation-operator) 22 | - Copy and Paste the pre-existing DVO version directory (ex. 0.2.0, 0.2.1) and change the name of the directory to reflect the new release version 23 | - Modify the clusterserviceversion file's name within the directory to reflect the new release version 24 | 25 | ```yaml 26 | # Edit the clusterserviceversion file within the directory and modify the following lines to reflect the new release 27 | # RELEASE VERSION == 0.2.0, 0.2.1, etc. 28 | 29 | * metadata.annotations.containerImage: quay.io/deployment-validation-operator/dv-operator: 30 | * metadata.name: deployment-validation-operator.v 31 | * spec.install.spec.deployments.spec.template.spec.containers.image: quay.io/deployment-validation-operator/dv-operator: 32 | * spec.links.url: https://quay.io/deployment-validation-operator/dv-operator: 33 | * spec.version: 34 | * spec.skipRange: '>=0.0.10 0.2.2, then the previous release was 0.2.1) 38 | 39 | * spec.replaces: deployment-validation-operator.v 40 | ``` 41 | 42 | - If changes need to be made to add/subtract reviewers, this can be changed within `ci.yaml` 43 | * This file allows for authorized users to review the PRs pushed to the DVO OLM project 44 | 45 | - If need-be for the nature of what the changes in the new DVO release, update the rest of these files accordingly 46 | 47 | - Submit a PR 48 | 49 | 4. OLM updates DVO version across DVO-consuming kubernetes clusters (no action required) 50 | 51 | - (Right now DVO is in an alpha-state, and so clusters running an OLM that is configured to ignore alpha releases in Operator-Hub may have unreliable success with the following): 52 | 53 | - Once the merge request to the `k8s-operatorhub/community-operators` GitHub repo is merged, the latest version of DVO available through the Operator-Hub ecosystem should automatically update. You can check the latest version available [here](https://operatorhub.io/operator/deployment-validation-operator). -------------------------------------------------------------------------------- /pkg/validations/utils.go: -------------------------------------------------------------------------------- 1 | package validations 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "golang.stackrox.io/kube-linter/pkg/builtinchecks" 8 | "golang.stackrox.io/kube-linter/pkg/checkregistry" 9 | klConfig "golang.stackrox.io/kube-linter/pkg/config" 10 | "golang.stackrox.io/kube-linter/pkg/configresolver" 11 | 12 | "github.com/app-sre/deployment-validation-operator/config" 13 | "github.com/prometheus/client_golang/prometheus" 14 | ) 15 | 16 | // GetKubeLinterRegistry returns a CheckRegistry containing kube-linter built-in validations. 17 | // It initializes a new CheckRegistry, loads the built-in validations into the registry, 18 | // and returns the resulting registry if successful. 19 | // 20 | // Returns: 21 | // - A CheckRegistry containing kube-linter built-in validations if successful. 22 | // - An error if the built-in validations fail to load into the registry. 23 | func GetKubeLinterRegistry() (checkregistry.CheckRegistry, error) { 24 | registry := checkregistry.New() 25 | if err := builtinchecks.LoadInto(registry); err != nil { 26 | return nil, fmt.Errorf("failed to load kube-linter built-in validations: %w", err) 27 | } 28 | 29 | return registry, nil 30 | } 31 | 32 | // GetAllNamesFromRegistry retrieves the names of all enabled checks from the provided CheckRegistry. 33 | // It fetches the names of checks that are enabled based on a specified configuration, excluding incompatible ones. 34 | // 35 | // Parameters: 36 | // - reg: A CheckRegistry containing predefined checks and their specifications. 37 | // 38 | // Returns: 39 | // - A slice of strings containing the names of all enabled checks if successful. 40 | // - An error if there's an issue while fetching the enabled check names or validating the configuration. 41 | func GetAllNamesFromRegistry(reg checkregistry.CheckRegistry) ([]string, error) { 42 | // Get all checks except for incompatible ones 43 | cfg := klConfig.Config{ 44 | Checks: klConfig.ChecksConfig{ 45 | AddAllBuiltIn: true, 46 | }, 47 | } 48 | 49 | checks, err := configresolver.GetEnabledChecksAndValidate(&cfg, reg) 50 | if err != nil { 51 | return nil, fmt.Errorf("error getting enabled validations: %w", err) 52 | } 53 | 54 | return checks, nil 55 | } 56 | 57 | func newGaugeVecMetric(check klConfig.Check) *prometheus.GaugeVec { 58 | metricName := strings.ReplaceAll(fmt.Sprintf("%s_%s", config.OperatorName, check.Name), "-", "_") 59 | 60 | return prometheus.NewGaugeVec( 61 | prometheus.GaugeOpts{ 62 | Name: metricName, 63 | Help: fmt.Sprintf( 64 | "Description: %s ; Remediation: %s", check.Description, check.Remediation, 65 | ), 66 | ConstLabels: prometheus.Labels{ 67 | "check_description": check.Description, 68 | "check_remediation": check.Remediation, 69 | }, 70 | }, []string{"namespace_uid", "namespace", "uid", "name", "kind"}) 71 | } 72 | 73 | // GetDefaultChecks provides a default set of checks usable in case there is no custom ConfigMap 74 | func GetDefaultChecks() klConfig.ChecksConfig { 75 | return klConfig.ChecksConfig{ 76 | DoNotAutoAddDefaults: true, 77 | Include: []string{ 78 | "host-ipc", 79 | "host-network", 80 | "host-pid", 81 | "non-isolated-pod", 82 | "pdb-max-unavailable", 83 | "pdb-min-available", 84 | "privilege-escalation-container", 85 | "privileged-container", 86 | "run-as-non-root", 87 | "unsafe-sysctls", 88 | "unset-cpu-requirements", 89 | "unset-memory-requirements", 90 | }, 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /konflux-ci/cli-manifests/release-payloads/prod-release-0.7.9-fbc.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: appstudio.redhat.com/v1alpha1 3 | kind: Release 4 | metadata: 5 | name: prod-release-fbc-4-15-20250702-01 6 | namespace: dvo-obsint-tenant 7 | spec: 8 | releasePlan: release-plan-fbc-prod-4-15 9 | snapshot: fbc-ocp4-15-np827 10 | data: 11 | releaseNotes: 12 | topic: Deployment Validation Operator FBC for Openshift v4.15 13 | synopsis: FBC update 14 | description: | 15 | This release includes catalog fragments until DVO 0.7.9 16 | solution: | 17 | Before applying this update, make sure all previously released errata relevant to your system have been applied. For details on how to apply this update, refer to: https://access.redhat.com/articles/11258 18 | references: [] 19 | --- 20 | apiVersion: appstudio.redhat.com/v1alpha1 21 | kind: Release 22 | metadata: 23 | name: prod-release-fbc-4-16-20250703-01 24 | namespace: dvo-obsint-tenant 25 | spec: 26 | releasePlan: release-plan-fbc-prod-4-16 27 | snapshot: fbc-ocp4-16-5xwd2 28 | data: 29 | releaseNotes: 30 | topic: Deployment Validation Operator FBC for Openshift v4.16 31 | synopsis: FBC update 32 | description: | 33 | This release includes catalog fragments until DVO 0.7.9 34 | solution: | 35 | Before applying this update, make sure all previously released errata relevant to your system have been applied. For details on how to apply this update, refer to: https://access.redhat.com/articles/11258 36 | references: [] 37 | --- 38 | apiVersion: appstudio.redhat.com/v1alpha1 39 | kind: Release 40 | metadata: 41 | name: prod-release-fbc-4-17-20250707-02 42 | namespace: dvo-obsint-tenant 43 | spec: 44 | releasePlan: release-plan-fbc-prod-4-17 45 | snapshot: fbc-ocp4-17-7bc8b 46 | data: 47 | releaseNotes: 48 | topic: Deployment Validation Operator FBC for Openshift v4.17 49 | synopsis: FBC update 50 | description: | 51 | This release includes catalog fragments until DVO 0.7.9 52 | solution: | 53 | Before applying this update, make sure all previously released errata relevant to your system have been applied. For details on how to apply this update, refer to: https://access.redhat.com/articles/11258 54 | references: [] 55 | --- 56 | apiVersion: appstudio.redhat.com/v1alpha1 57 | kind: Release 58 | metadata: 59 | name: prod-release-fbc-4-18-20250707-01 60 | namespace: dvo-obsint-tenant 61 | spec: 62 | releasePlan: release-plan-fbc-prod-4-18 63 | snapshot: fbc-ocp4-18-bnmbv 64 | data: 65 | releaseNotes: 66 | topic: Deployment Validation Operator FBC for Openshift v4.18 67 | synopsis: FBC update 68 | description: | 69 | This release includes catalog fragments until DVO 0.7.9 70 | solution: | 71 | Before applying this update, make sure all previously released errata relevant to your system have been applied. For details on how to apply this update, refer to: https://access.redhat.com/articles/11258 72 | references: [] 73 | --- 74 | apiVersion: appstudio.redhat.com/v1alpha1 75 | kind: Release 76 | metadata: 77 | name: prod-release-fbc-4-19-20250707-02 78 | namespace: dvo-obsint-tenant 79 | spec: 80 | releasePlan: release-plan-fbc-prod-4-19 81 | snapshot: fbc-ocp4-19-nwfrh 82 | data: 83 | releaseNotes: 84 | topic: Deployment Validation Operator FBC for Openshift v4.19 85 | synopsis: FBC update 86 | description: | 87 | This release includes catalog fragments until DVO 0.7.9 88 | solution: | 89 | Before applying this update, make sure all previously released errata relevant to your system have been applied. For details on how to apply this update, refer to: https://access.redhat.com/articles/11258 90 | references: [] 91 | -------------------------------------------------------------------------------- /konflux-ci/cli-manifests/release-payloads/prod-release-0.7.11-fbc.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: appstudio.redhat.com/v1alpha1 3 | kind: Release 4 | metadata: 5 | name: prod-release-fbc-4-15-20250709-02 6 | namespace: dvo-obsint-tenant 7 | spec: 8 | releasePlan: release-plan-fbc-prod-4-15 9 | snapshot: fbc-ocp4-15-hs4wz 10 | data: 11 | releaseNotes: 12 | topic: Deployment Validation Operator FBC for Openshift v4.15 13 | synopsis: FBC update 14 | description: | 15 | This release includes catalog fragments until DVO 0.7.11 16 | solution: | 17 | Before applying this update, make sure all previously released errata relevant to your system have been applied. For details on how to apply this update, refer to: https://access.redhat.com/articles/11258 18 | references: [] 19 | --- 20 | apiVersion: appstudio.redhat.com/v1alpha1 21 | kind: Release 22 | metadata: 23 | name: prod-release-fbc-4-16-20250709-02 24 | namespace: dvo-obsint-tenant 25 | spec: 26 | releasePlan: release-plan-fbc-prod-4-16 27 | snapshot: fbc-ocp4-16-qrm9j 28 | data: 29 | releaseNotes: 30 | topic: Deployment Validation Operator FBC for Openshift v4.16 31 | synopsis: FBC update 32 | description: | 33 | This release includes catalog fragments until DVO 0.7.11 34 | solution: | 35 | Before applying this update, make sure all previously released errata relevant to your system have been applied. For details on how to apply this update, refer to: https://access.redhat.com/articles/11258 36 | references: [] 37 | --- 38 | apiVersion: appstudio.redhat.com/v1alpha1 39 | kind: Release 40 | metadata: 41 | name: prod-release-fbc-4-17-20250709-03 42 | namespace: dvo-obsint-tenant 43 | spec: 44 | releasePlan: release-plan-fbc-prod-4-17 45 | snapshot: fbc-ocp4-17-pjgsj 46 | data: 47 | releaseNotes: 48 | topic: Deployment Validation Operator FBC for Openshift v4.17 49 | synopsis: FBC update 50 | description: | 51 | This release includes catalog fragments until DVO 0.7.11 52 | solution: | 53 | Before applying this update, make sure all previously released errata relevant to your system have been applied. For details on how to apply this update, refer to: https://access.redhat.com/articles/11258 54 | references: [] 55 | --- 56 | apiVersion: appstudio.redhat.com/v1alpha1 57 | kind: Release 58 | metadata: 59 | name: prod-release-fbc-4-18-20250709-03 60 | namespace: dvo-obsint-tenant 61 | spec: 62 | releasePlan: release-plan-fbc-prod-4-18 63 | snapshot: fbc-ocp4-18-mgsq7 64 | data: 65 | releaseNotes: 66 | topic: Deployment Validation Operator FBC for Openshift v4.18 67 | synopsis: FBC update 68 | description: | 69 | This release includes catalog fragments until DVO 0.7.11 70 | solution: | 71 | Before applying this update, make sure all previously released errata relevant to your system have been applied. For details on how to apply this update, refer to: https://access.redhat.com/articles/11258 72 | references: [] 73 | --- 74 | apiVersion: appstudio.redhat.com/v1alpha1 75 | kind: Release 76 | metadata: 77 | name: prod-release-fbc-4-19-20250709-03 78 | namespace: dvo-obsint-tenant 79 | spec: 80 | releasePlan: release-plan-fbc-prod-4-19 81 | snapshot: fbc-ocp4-19-9rwv8 82 | data: 83 | releaseNotes: 84 | topic: Deployment Validation Operator FBC for Openshift v4.19 85 | synopsis: FBC update 86 | description: | 87 | This release includes catalog fragments until DVO 0.7.11 88 | solution: | 89 | Before applying this update, make sure all previously released errata relevant to your system have been applied. For details on how to apply this update, refer to: https://access.redhat.com/articles/11258 90 | references: [] 91 | -------------------------------------------------------------------------------- /konflux-ci/cli-manifests/release-payloads/prod-release-0.7.12-fbc.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: appstudio.redhat.com/v1alpha1 3 | kind: Release 4 | metadata: 5 | name: prod-release-fbc-4-15-20250910-01 6 | namespace: dvo-obsint-tenant 7 | spec: 8 | releasePlan: release-plan-fbc-prod-4-15 9 | snapshot: fbc-ocp4-15-r68d8 10 | data: 11 | releaseNotes: 12 | topic: Deployment Validation Operator FBC for Openshift v4.15 13 | synopsis: FBC update 14 | description: | 15 | This release includes catalog fragments until DVO 0.7.12 16 | solution: | 17 | Before applying this update, make sure all previously released errata relevant to your system have been applied. For details on how to apply this update, refer to: https://access.redhat.com/articles/11258 18 | references: [] 19 | --- 20 | apiVersion: appstudio.redhat.com/v1alpha1 21 | kind: Release 22 | metadata: 23 | name: prod-release-fbc-4-16-20250910-01 24 | namespace: dvo-obsint-tenant 25 | spec: 26 | releasePlan: release-plan-fbc-prod-4-16 27 | snapshot: fbc-ocp4-16-mvhwn 28 | data: 29 | releaseNotes: 30 | topic: Deployment Validation Operator FBC for Openshift v4.16 31 | synopsis: FBC update 32 | description: | 33 | This release includes catalog fragments until DVO 0.7.12 34 | solution: | 35 | Before applying this update, make sure all previously released errata relevant to your system have been applied. For details on how to apply this update, refer to: https://access.redhat.com/articles/11258 36 | references: [] 37 | --- 38 | apiVersion: appstudio.redhat.com/v1alpha1 39 | kind: Release 40 | metadata: 41 | name: prod-release-fbc-4-17-20250910-01 42 | namespace: dvo-obsint-tenant 43 | spec: 44 | releasePlan: release-plan-fbc-prod-4-17 45 | snapshot: fbc-ocp4-17-6ct7c 46 | data: 47 | releaseNotes: 48 | topic: Deployment Validation Operator FBC for Openshift v4.17 49 | synopsis: FBC update 50 | description: | 51 | This release includes catalog fragments until DVO 0.7.12 52 | solution: | 53 | Before applying this update, make sure all previously released errata relevant to your system have been applied. For details on how to apply this update, refer to: https://access.redhat.com/articles/11258 54 | references: [] 55 | --- 56 | apiVersion: appstudio.redhat.com/v1alpha1 57 | kind: Release 58 | metadata: 59 | name: prod-release-fbc-4-18-20250910-01 60 | namespace: dvo-obsint-tenant 61 | spec: 62 | releasePlan: release-plan-fbc-prod-4-18 63 | snapshot: fbc-ocp4-18-f92p6 64 | data: 65 | releaseNotes: 66 | topic: Deployment Validation Operator FBC for Openshift v4.18 67 | synopsis: FBC update 68 | description: | 69 | This release includes catalog fragments until DVO 0.7.12 70 | solution: | 71 | Before applying this update, make sure all previously released errata relevant to your system have been applied. For details on how to apply this update, refer to: https://access.redhat.com/articles/11258 72 | references: [] 73 | --- 74 | apiVersion: appstudio.redhat.com/v1alpha1 75 | kind: Release 76 | metadata: 77 | name: prod-release-fbc-4-19-20250910-01 78 | namespace: dvo-obsint-tenant 79 | spec: 80 | releasePlan: release-plan-fbc-prod-4-19 81 | snapshot: fbc-ocp4-19-4p7b4 82 | data: 83 | releaseNotes: 84 | topic: Deployment Validation Operator FBC for Openshift v4.19 85 | synopsis: FBC update 86 | description: | 87 | This release includes catalog fragments until DVO 0.7.12 88 | solution: | 89 | Before applying this update, make sure all previously released errata relevant to your system have been applied. For details on how to apply this update, refer to: https://access.redhat.com/articles/11258 90 | references: [] 91 | -------------------------------------------------------------------------------- /konflux-ci/cli-manifests/release-payloads/prod-release-0.7.13-fbc.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: appstudio.redhat.com/v1alpha1 3 | kind: Release 4 | metadata: 5 | name: prod-release-fbc-4-15-20251117-02 6 | namespace: dvo-obsint-tenant 7 | spec: 8 | releasePlan: release-plan-fbc-prod-4-15 9 | snapshot: fbc-ocp4-15-9x8z8 10 | data: 11 | releaseNotes: 12 | topic: Deployment Validation Operator FBC for Openshift v4.15 13 | synopsis: FBC update 14 | description: | 15 | This release includes catalog fragments until DVO 0.7.13 16 | solution: | 17 | Before applying this update, make sure all previously released errata relevant to your system have been applied. For details on how to apply this update, refer to: https://access.redhat.com/articles/11258 18 | references: [] 19 | --- 20 | apiVersion: appstudio.redhat.com/v1alpha1 21 | kind: Release 22 | metadata: 23 | name: prod-release-fbc-4-16-20251117-02 24 | namespace: dvo-obsint-tenant 25 | spec: 26 | releasePlan: release-plan-fbc-prod-4-16 27 | snapshot: fbc-ocp4-16-gw85j 28 | data: 29 | releaseNotes: 30 | topic: Deployment Validation Operator FBC for Openshift v4.16 31 | synopsis: FBC update 32 | description: | 33 | This release includes catalog fragments until DVO 0.7.13 34 | solution: | 35 | Before applying this update, make sure all previously released errata relevant to your system have been applied. For details on how to apply this update, refer to: https://access.redhat.com/articles/11258 36 | references: [] 37 | --- 38 | apiVersion: appstudio.redhat.com/v1alpha1 39 | kind: Release 40 | metadata: 41 | name: prod-release-fbc-4-17-20251117-02 42 | namespace: dvo-obsint-tenant 43 | spec: 44 | releasePlan: release-plan-fbc-prod-4-17 45 | snapshot: fbc-ocp4-17-nwnd9 46 | data: 47 | releaseNotes: 48 | topic: Deployment Validation Operator FBC for Openshift v4.17 49 | synopsis: FBC update 50 | description: | 51 | This release includes catalog fragments until DVO 0.7.13 52 | solution: | 53 | Before applying this update, make sure all previously released errata relevant to your system have been applied. For details on how to apply this update, refer to: https://access.redhat.com/articles/11258 54 | references: [] 55 | --- 56 | apiVersion: appstudio.redhat.com/v1alpha1 57 | kind: Release 58 | metadata: 59 | name: prod-release-fbc-4-18-20251117-02 60 | namespace: dvo-obsint-tenant 61 | spec: 62 | releasePlan: release-plan-fbc-prod-4-18 63 | snapshot: fbc-ocp4-18-bttxh 64 | data: 65 | releaseNotes: 66 | topic: Deployment Validation Operator FBC for Openshift v4.18 67 | synopsis: FBC update 68 | description: | 69 | This release includes catalog fragments until DVO 0.7.13 70 | solution: | 71 | Before applying this update, make sure all previously released errata relevant to your system have been applied. For details on how to apply this update, refer to: https://access.redhat.com/articles/11258 72 | references: [] 73 | --- 74 | apiVersion: appstudio.redhat.com/v1alpha1 75 | kind: Release 76 | metadata: 77 | name: prod-release-fbc-4-19-20251117-02 78 | namespace: dvo-obsint-tenant 79 | spec: 80 | releasePlan: release-plan-fbc-prod-4-19 81 | snapshot: fbc-ocp4-19-n6c88 82 | data: 83 | releaseNotes: 84 | topic: Deployment Validation Operator FBC for Openshift v4.19 85 | synopsis: FBC update 86 | description: | 87 | This release includes catalog fragments until DVO 0.7.13 88 | solution: | 89 | Before applying this update, make sure all previously released errata relevant to your system have been applied. For details on how to apply this update, refer to: https://access.redhat.com/articles/11258 90 | references: [] 91 | -------------------------------------------------------------------------------- /deploy/openshift/operator.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | app: deployment-validation-operator 6 | name: deployment-validation-operator 7 | annotations: 8 | ignore-check.kube-linter.io/minimum-three-replicas: "This deployment uses 1 pod as currently replicating does not replicate metric data causing installation issues" 9 | spec: 10 | replicas: 1 11 | selector: 12 | matchLabels: 13 | app: deployment-validation-operator 14 | strategy: 15 | rollingUpdate: 16 | maxSurge: 1 17 | maxUnavailable: 0 18 | type: RollingUpdate 19 | template: 20 | metadata: 21 | labels: 22 | name: deployment-validation-operator 23 | app: deployment-validation-operator 24 | spec: 25 | tolerations: 26 | - key: node-role.kubernetes.io/infra 27 | operator: Exists 28 | effect: NoSchedule 29 | affinity: 30 | nodeAffinity: 31 | requiredDuringSchedulingIgnoredDuringExecution: 32 | nodeSelectorTerms: 33 | - matchExpressions: 34 | - key: node-role.kubernetes.io/infra 35 | operator: Exists 36 | podAntiAffinity: 37 | preferredDuringSchedulingIgnoredDuringExecution: 38 | - podAffinityTerm: 39 | labelSelector: 40 | matchExpressions: 41 | - key: app 42 | operator: In 43 | values: 44 | - deployment-validation-operator 45 | topologyKey: kubernetes.io/hostname 46 | weight: 100 47 | containers: 48 | - image: quay.io/deployment-validation-operator/dv-operator:latest 49 | imagePullPolicy: Always 50 | name: deployment-validation-operator 51 | args: 52 | - --config /config/deployment-validation-operator-config.yaml 53 | resources: 54 | requests: 55 | memory: "400Mi" 56 | cpu: "50m" 57 | limits: 58 | memory: "400Mi" 59 | livenessProbe: 60 | httpGet: 61 | path: /healthz 62 | port: 8081 63 | initialDelaySeconds: 30 64 | periodSeconds: 10 65 | timeoutSeconds: 3 66 | readinessProbe: 67 | httpGet: 68 | path: /readyz 69 | port: 8081 70 | initialDelaySeconds: 5 71 | periodSeconds: 10 72 | timeoutSeconds: 3 73 | env: 74 | - name: WATCH_NAMESPACE 75 | value: "" 76 | - name: OPERATOR_NAME 77 | value: "deployment-validation-operator" 78 | - name: NAMESPACE_IGNORE_PATTERN 79 | value: "openshift.*|kube-.+|open-cluster-management-.*|default|dedicated-admin" 80 | - name: RESOURCES_PER_LIST_QUERY 81 | value: "5" 82 | - name: VALIDATION_CHECK_INTERVAL 83 | value: "2m" 84 | - name: POD_NAME 85 | valueFrom: 86 | fieldRef: 87 | fieldPath: metadata.name 88 | - name: POD_NAMESPACE 89 | valueFrom: 90 | fieldRef: 91 | fieldPath: metadata.namespace 92 | volumeMounts: 93 | - name: dvo-config 94 | mountPath: /config 95 | securityContext: 96 | readOnlyRootFilesystem: true 97 | volumes: 98 | - name: dvo-config 99 | configMap: 100 | optional: true 101 | name: deployment-validation-operator-config 102 | restartPolicy: Always 103 | serviceAccountName: deployment-validation-operator 104 | terminationGracePeriodSeconds: 30 -------------------------------------------------------------------------------- /pkg/configmap/configmap_watcher_test.go: -------------------------------------------------------------------------------- 1 | package configmap 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "golang.stackrox.io/kube-linter/pkg/config" 9 | ) 10 | 11 | func TestReadConfig(t *testing.T) { 12 | tests := []struct { 13 | name string 14 | configData string 15 | expectedConfig config.Config 16 | expectedError error 17 | }{ 18 | { 19 | name: "Basic valid config", 20 | configData: ` 21 | checks: 22 | doNotAutoAddDefaults: false 23 | addAllBuiltIn: true 24 | include: 25 | - "unset-memory-requirements" 26 | - "unset-cpu-requirements"`, 27 | expectedConfig: config.Config{ 28 | Checks: config.ChecksConfig{ 29 | AddAllBuiltIn: true, 30 | DoNotAutoAddDefaults: false, 31 | Include: []string{"unset-memory-requirements", "unset-cpu-requirements"}, // nolint: lll 32 | }, 33 | }, 34 | expectedError: nil, 35 | }, 36 | { 37 | name: "Invalid config field \"doNotAutoAddDefaultsAAA\"", 38 | configData: ` 39 | checks: 40 | doNotAutoAddDefaultsAAA: false 41 | addAllBuiltIn: true 42 | include: 43 | - "unset-memory-requirements" 44 | - "unset-cpu-requirements"`, 45 | expectedError: fmt.Errorf("unmarshalling configmap data: error unmarshaling JSON: while decoding JSON: json: unknown field \"doNotAutoAddDefaultsAAA\""), // nolint: lll 46 | expectedConfig: config.Config{ 47 | Checks: config.ChecksConfig{ 48 | AddAllBuiltIn: true, 49 | DoNotAutoAddDefaults: false, 50 | Include: []string{"unset-memory-requirements", "unset-cpu-requirements"}, // nolint: lll 51 | }, 52 | }, 53 | }, 54 | { 55 | name: "Invalid config field \"include\"", 56 | configData: ` 57 | checks: 58 | doNotAutoAddDefaults: false 59 | addAllBuiltIn: true 60 | includeaa: 61 | - "unset-memory-requirements" 62 | - "unset-cpu-requirements"`, 63 | expectedError: fmt.Errorf("unmarshalling configmap data: error unmarshaling JSON: while decoding JSON: json: unknown field \"includeaa\""), // nolint: lll 64 | expectedConfig: config.Config{ 65 | Checks: config.ChecksConfig{ 66 | AddAllBuiltIn: true, 67 | DoNotAutoAddDefaults: false, 68 | }, 69 | }, 70 | }, 71 | { 72 | name: "Valid config with custom check", 73 | configData: ` 74 | checks: 75 | doNotAutoAddDefaults: false 76 | addAllBuiltIn: true 77 | include: 78 | - "unset-memory-requirements" 79 | customChecks: 80 | - name: test-minimum-replicas 81 | description: "some description" 82 | remediation: "some remediation" 83 | template: minimum-replicas 84 | params: 85 | minReplicas: 3 86 | scope: 87 | objectKinds: 88 | - DeploymentLike`, 89 | expectedError: nil, 90 | expectedConfig: config.Config{ 91 | Checks: config.ChecksConfig{ 92 | AddAllBuiltIn: true, 93 | DoNotAutoAddDefaults: false, 94 | Include: []string{"unset-memory-requirements"}, 95 | }, 96 | CustomChecks: []config.Check{ 97 | { 98 | Name: "test-minimum-replicas", 99 | Description: "some description", 100 | Remediation: "some remediation", 101 | Template: "minimum-replicas", 102 | Params: map[string]interface{}{ 103 | "minReplicas": float64(3), 104 | }, 105 | Scope: &config.ObjectKindsDesc{ 106 | ObjectKinds: []string{"DeploymentLike"}, 107 | }, 108 | }, 109 | }, 110 | }, 111 | }, 112 | } 113 | for _, tt := range tests { 114 | t.Run(tt.name, func(t *testing.T) { 115 | 116 | cfg, err := readConfig(tt.configData) 117 | if tt.expectedError != nil { 118 | assert.Equal(t, tt.expectedError.Error(), err.Error()) 119 | } else { 120 | assert.NoError(t, err) 121 | } 122 | assert.Equal(t, tt.expectedConfig, cfg) 123 | }) 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /hack/generate-operator-bundle-contents.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # vim: ts=4:sw=4:cc=99 4 | 5 | import datetime 6 | import os 7 | import sys 8 | import yaml 9 | import pathlib 10 | import argparse 11 | 12 | parser = argparse.ArgumentParser(add_help=False) 13 | required = parser.add_argument_group('required arguments') 14 | 15 | required.add_argument('-n', '--name', help='operator name', type=str, required=True) 16 | required.add_argument('-c', '--current-version', help='operator version', type=str, required=True) 17 | required.add_argument('-i', '--image', help='operator image', type=str, required=True) 18 | required.add_argument('-t', '--image-tag', help='operator image tag', type=str, required=True) 19 | required.add_argument('-o', '--output-dir', help='output directory', type=str, required=True) 20 | 21 | optional = parser.add_argument_group('optional arguments') 22 | optional.add_argument( '-h', '--help', action='help', default=argparse.SUPPRESS, 23 | help='show this help message and exit') 24 | optional.add_argument('-r', '--replaces', help='Replaces version', type=str) 25 | optional.add_argument('-s','--skip', action='append', 26 | help='Skips version (can be specified multiple times)') 27 | optional.add_argument('--no-nodeAffinity', '--no-nodeAffinity', 28 | help='Removes node affinity. Used for CSV generation for OCP clusters', 29 | action='store_true') 30 | 31 | args = parser.parse_args() 32 | 33 | root = pathlib.Path(__file__).parent.absolute() / '..' 34 | manifest_dir = root / 'deploy/openshift' 35 | csv_template_dir = root / 'deploy/bundle/template' 36 | 37 | with open(csv_template_dir / f'{args.name}.clusterserviceversion.yaml', 38 | 'r') as stream: 39 | csv = yaml.safe_load(stream) 40 | 41 | csv['spec']['install']['spec']['clusterPermissions'] = [] 42 | with open(manifest_dir / 'cluster-role.yaml', 'r') as stream: 43 | operator_role = yaml.safe_load(stream) 44 | csv['spec']['install']['spec']['clusterPermissions'].append( 45 | { 46 | 'rules': operator_role['rules'], 47 | 'serviceAccountName': args.name, 48 | }) 49 | 50 | csv['spec']['install']['spec']['permissions'] = [] 51 | with open(manifest_dir / 'role.yaml', 'r') as stream: 52 | operator_role = yaml.safe_load(stream) 53 | csv['spec']['install']['spec']['permissions'].append( 54 | { 55 | 'rules': operator_role['rules'], 56 | 'serviceAccountName': args.name, 57 | }) 58 | 59 | with open(manifest_dir / 'operator.yaml', 'r') as stream: 60 | operator_components = [] 61 | operator = yaml.safe_load_all(stream) 62 | for doc in operator: 63 | operator_components.append(doc) 64 | # There is only one yaml document in the operator deployment 65 | operator_deployment = operator_components[0] 66 | if args.no_nodeAffinity: 67 | operator_deployment['spec']['template']['spec']['affinity'].pop("nodeAffinity") 68 | csv['spec']['install']['spec']['deployments'][0]['spec'] = \ 69 | operator_deployment['spec'] 70 | 71 | csv['spec']['install']['spec']['deployments'][0]['spec']['template']['spec']['containers'][0]['image'] = \ 72 | csv['metadata']['annotations']['containerImage'] = f'{args.image}:{args.image_tag}' 73 | csv['metadata']['name'] = f'{args.name}.v{args.current_version}' 74 | csv['spec']['version'] = args.current_version 75 | csv['spec']['links'][1]['url'] = f'https://{args.image}:{args.image_tag}' 76 | 77 | if args.replaces: 78 | csv['spec']['replaces'] = f'{args.name}.v{args.replaces}' 79 | 80 | if args.skip: 81 | csv['metadata']['annotations']['olm.skipRange'] = ' || '.join( 82 | sorted(args.skip, key=lambda v: int(v.split('.')[2].split('-')[0]))) 83 | 84 | now = datetime.datetime.now() 85 | csv['metadata']['annotations']['createdAt'] = \ 86 | now.strftime('%Y-%m-%dT%H:%M:%SZ') 87 | 88 | csv_filename = pathlib.Path(args.output_dir) / \ 89 | f'{args.name}.v{args.current_version}.clusterserviceversion.yaml' 90 | os.makedirs(os.path.dirname(csv_filename), exist_ok=True) 91 | with open(csv_filename, 'w') as output_file: 92 | yaml.dump(csv, output_file, default_flow_style=False) 93 | -------------------------------------------------------------------------------- /pkg/controller/validationscache.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "github.com/app-sre/deployment-validation-operator/pkg/validations" 5 | "k8s.io/apimachinery/pkg/types" 6 | "sigs.k8s.io/controller-runtime/pkg/client" 7 | ) 8 | 9 | type validationKey struct { 10 | group, version, kind string 11 | name, namespace, nsID string 12 | uid types.UID 13 | } 14 | 15 | type resourceVersion string 16 | 17 | func newResourceversionVal(str string) resourceVersion { 18 | return resourceVersion(str) 19 | } 20 | 21 | // newValidationKey returns a unique identifier for the given 22 | // object suitable for hashing. 23 | func newValidationKey(obj client.Object, nsID string) validationKey { 24 | gvk := obj.GetObjectKind().GroupVersionKind() 25 | return validationKey{ 26 | group: gvk.Group, 27 | version: gvk.Version, 28 | kind: gvk.Kind, 29 | name: obj.GetName(), 30 | namespace: obj.GetNamespace(), 31 | nsID: nsID, 32 | uid: obj.GetUID(), 33 | } 34 | } 35 | 36 | type validationResource struct { 37 | version resourceVersion 38 | uid string 39 | outcome validations.ValidationOutcome 40 | } 41 | 42 | // newValidationResource returns a 'validationResource' populated 43 | // with the given 'resourceVersion', 'uid', and 'ValidationOutcome'. 44 | func newValidationResource( 45 | rscVer resourceVersion, 46 | uid string, 47 | outcome validations.ValidationOutcome, 48 | ) *validationResource { 49 | return &validationResource{ 50 | uid: uid, 51 | version: rscVer, 52 | outcome: outcome, 53 | } 54 | } 55 | 56 | type validationCache map[validationKey]*validationResource 57 | 58 | // newValidationCache returns a new empty instance of validationCache struct 59 | func newValidationCache() *validationCache { 60 | return &validationCache{} 61 | } 62 | 63 | // has returns 'true' if the given key exist in the instance; 'false' otherwise. 64 | func (vc *validationCache) has(key validationKey) bool { 65 | _, exists := (*vc)[key] 66 | return exists 67 | } 68 | 69 | // store caches a 'ValidationOutcome' for the given 'Object'. 70 | // constraint: cached outcomes will be updated in-place for a given object and 71 | // consecutive updates will not preserve previous state. 72 | func (vc *validationCache) store(obj client.Object, nsID string, outcome validations.ValidationOutcome) { 73 | key := newValidationKey(obj, nsID) 74 | (*vc)[key] = newValidationResource( 75 | newResourceversionVal(obj.GetResourceVersion()), 76 | string(obj.GetUID()), 77 | outcome, 78 | ) 79 | } 80 | 81 | // drain frees the cache of any used resources 82 | // resulting in all cached 'ValidationOutcome's being lost. 83 | func (vc *validationCache) drain() { 84 | *vc = validationCache{} 85 | } 86 | 87 | // remove uncaches the 'ValidationOutcome' for the 88 | // given object if it exists and performs a noop 89 | // if it does not. 90 | func (vc *validationCache) remove(obj client.Object, nsID string) { 91 | key := newValidationKey(obj, nsID) 92 | vc.removeKey(key) 93 | } 94 | 95 | // removeKey deletes a key, and its value, from the instance 96 | func (vc *validationCache) removeKey(key validationKey) { 97 | delete(*vc, key) 98 | } 99 | 100 | // retrieve returns a tuple of 'validationResource' (if present) 101 | // and 'ok' which returns 'true' if a 'validationResource' exists 102 | // for the given 'Object' and 'false' otherwise. 103 | func (vc *validationCache) retrieve(obj client.Object, nsID string) (*validationResource, bool) { 104 | key := newValidationKey(obj, nsID) 105 | val, exists := (*vc)[key] 106 | return val, exists 107 | } 108 | 109 | // objectAlreadyValidated returns 'true' if the given 'Object' 110 | // has a cached 'ValidationOutcome' with the same 'ResourceVersion' 111 | // (Kubernetes representation of iteration count for a persisted resource). 112 | // If the 'ResourceVersion' of an existing 'Object' is stale the cached 113 | // 'ValidationOutcome' is removed and 'false' is returned. In all other 114 | // cases 'false' is returned. 115 | func (vc *validationCache) objectAlreadyValidated(obj client.Object, nsID string) bool { 116 | validationOutcome, ok := vc.retrieve(obj, nsID) 117 | if !ok { 118 | return false 119 | } 120 | storedResourceVersion := validationOutcome.version 121 | currentResourceVersion := obj.GetResourceVersion() 122 | if string(storedResourceVersion) != currentResourceVersion { 123 | vc.remove(obj, nsID) 124 | return false 125 | } 126 | return true 127 | } 128 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | OPERATOR_NAME = deployment-validation-operator 2 | # Image repository vars 3 | REGISTRY_USER ?= ${QUAY_USER} 4 | REGISTRY_TOKEN ?= ${QUAY_TOKEN} 5 | IMAGE_REGISTRY ?= quay.io 6 | IMAGE_REPOSITORY ?= app-sre 7 | IMAGE_NAME ?= ${OPERATOR_NAME} 8 | OPERATOR_IMAGE = ${IMAGE_REGISTRY}/${IMAGE_REPOSITORY}/${IMAGE_NAME} 9 | 10 | OLM_CHANNEL ?= alpha 11 | OLM_BUNDLE_IMAGE = ${OPERATOR_IMAGE}-bundle 12 | OLM_CATALOG_IMAGE = ${OPERATOR_IMAGE}-catalog 13 | 14 | VERSION_MAJOR ?= 0 15 | VERSION_MINOR ?= 1 16 | COMMIT_COUNT=$(shell git rev-list --count HEAD) 17 | CURRENT_COMMIT=$(shell git rev-parse --short=7 HEAD) 18 | OPERATOR_VERSION=${VERSION_MAJOR}.${VERSION_MINOR}.${COMMIT_COUNT}-g${CURRENT_COMMIT} 19 | OPERATOR_IMAGE_TAG ?= ${OPERATOR_VERSION} 20 | 21 | CONTAINER_ENGINE_CONFIG_DIR = .docker 22 | CONTAINER_ENGINE = $(shell command -v podman 2>/dev/null || echo docker --config=$(CONTAINER_ENGINE_CONFIG_DIR)) 23 | 24 | ifdef FIPS_ENABLED 25 | FIPSENV=GOEXPERIMENT=strictfipsruntime GOFLAGS="-tags=strictfipsruntime" 26 | endif 27 | 28 | .PHONY: go-mod-update 29 | go-mod-update: 30 | go mod vendor 31 | 32 | .PHONY: ensure-golangci 33 | ensure-golangci: 34 | ./ci/golangci-lint.sh 35 | 36 | GOOS ?= linux 37 | GOENV=GOOS=${GOOS} GOARCH=${GOARCH} CGO_ENABLED=1 ${FIPSENV} 38 | GOBUILDFLAGS=-gcflags="all=-trimpath=${GOPATH}" -asmflags="all=-trimpath=${GOPATH}" 39 | .PHONY: go-build 40 | go-build: go-mod-update 41 | @echo "## Building the binary..." 42 | ${GOENV} go build ${GOBUILDFLAGS} -o build/_output/bin/$(OPERATOR_NAME) . 43 | 44 | ## Used by CI pipeline ci/prow/lint 45 | GOLANGCI_OPTIONAL_CONFIG = .golangci.yml 46 | GOLANGCI_LINT_CACHE =/tmp/golangci-cache 47 | .PHONY: lint 48 | lint: go-mod-update ensure-golangci 49 | @echo "## Running the golangci-lint tool..." 50 | GOLANGCI_LINT_CACHE=${GOLANGCI_LINT_CACHE} golangci-lint run -c ${GOLANGCI_OPTIONAL_CONFIG} ./... 51 | 52 | ## Used by CI pipeline ci/prow/test 53 | TEST_TARGETS = $(shell ${GOENV} go list -e ./... | grep -E -v "/(vendor)/") 54 | .PHONY: test 55 | test: go-mod-update 56 | @echo "## Running the code unit tests..." 57 | ${GOENV} go test ${TEST_TARGETS} 58 | 59 | ## These targets: coverage and test-coverage; are used by the CI pipeline ci/prow/coverage 60 | .PHONY: coverage 61 | coverage: 62 | @echo "## Running code coverage..." 63 | ci/codecov.sh 64 | 65 | TESTOPTS := 66 | .PHONY: test-coverage 67 | test-coverage: go-mod-update 68 | @echo "## Running the code unit tests with coverage..." 69 | ${GOENV} go test ${TESTOPTS} ${TEST_TARGETS} 70 | 71 | ## Used by CI pipeline ci/prow/validate 72 | .PHONY: validate 73 | validate: 74 | @echo "## Perform validation that the folder does not contain extra artifacts..." 75 | test 0 -eq $$(git status --porcelain | wc -l) || (echo "Base folder contains unknown artifacts" >&2 && git --no-pager diff && exit 1) 76 | 77 | .PHONY: quay-login 78 | quay-login: 79 | @echo "## Login to quay.io..." 80 | mkdir -p ${CONTAINER_ENGINE_CONFIG_DIR} 81 | export REGISTRY_AUTH_FILE=${CONTAINER_ENGINE_CONFIG_DIR}/config.json 82 | @${CONTAINER_ENGINE} login -u="${REGISTRY_USER}" -p="${REGISTRY_TOKEN}" quay.io 83 | 84 | .PHONY: docker-build 85 | docker-build: 86 | @echo "## Building the container image..." 87 | ${CONTAINER_ENGINE} build --pull -f build/Dockerfile -t ${OPERATOR_IMAGE}:${OPERATOR_IMAGE_TAG} . 88 | ${CONTAINER_ENGINE} tag ${OPERATOR_IMAGE}:${OPERATOR_IMAGE_TAG} ${OPERATOR_IMAGE}:latest 89 | 90 | .PHONY: docker-push 91 | docker-push: 92 | @echo "## Pushing the container image..." 93 | ${CONTAINER_ENGINE} push ${OPERATOR_IMAGE}:${OPERATOR_IMAGE_TAG} 94 | ${CONTAINER_ENGINE} push ${OPERATOR_IMAGE}:latest 95 | 96 | ## This target is run by build_tag.sh script, triggered by a Jenkins job 97 | .PHONY: docker-publish 98 | docker-publish: quay-login docker-build docker-push 99 | 100 | ## This target is run by the master branch Jenkins Job 101 | .PHONY: build-push 102 | build-push: docker-publish 103 | @echo "## Building bundle and catalog images..." 104 | @(CONTAINER_ENGINE="${CONTAINER_ENGINE}" \ 105 | CONTAINER_ENGINE_CONFIG_DIR="${CONTAINER_ENGINE_CONFIG_DIR}" \ 106 | CURRENT_COMMIT="${CURRENT_COMMIT}" \ 107 | OLM_BUNDLE_IMAGE="${OLM_BUNDLE_IMAGE}" \ 108 | OLM_CATALOG_IMAGE="${OLM_CATALOG_IMAGE}" \ 109 | OLM_CHANNEL="${OLM_CHANNEL}" \ 110 | OPERATOR_NAME="${OPERATOR_NAME}" \ 111 | OPERATOR_VERSION="${OPERATOR_VERSION}" \ 112 | OPERATOR_IMAGE="${OPERATOR_IMAGE}" \ 113 | OPERATOR_IMAGE_TAG="${OPERATOR_IMAGE_TAG}" \ 114 | IMAGE_REGISTRY=${IMAGE_REGISTRY} \ 115 | REGISTRY_USER="${REGISTRY_USER}" \ 116 | REGISTRY_TOKEN="${REGISTRY_TOKEN}" \ 117 | build/build_opm_catalog.sh) -------------------------------------------------------------------------------- /pkg/controller/reconcileobjects.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "golang.stackrox.io/kube-linter/pkg/objectkinds" 8 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 9 | "k8s.io/apimachinery/pkg/runtime" 10 | "k8s.io/apimachinery/pkg/runtime/schema" 11 | "k8s.io/client-go/discovery" 12 | ) 13 | 14 | type resourceSet struct { 15 | scheme *runtime.Scheme 16 | apiResources map[schema.GroupKind]metav1.APIResource 17 | } 18 | 19 | // newResourceSet returns an empty set of resources related to the given scheme 20 | // this object will expose methods to allow adding new resources to the set 21 | // and to return a slice of 'APIResource' entities ("k8s.io/apimachinery/pkg/apis/meta/v1") 22 | func newResourceSet(scheme *runtime.Scheme) *resourceSet { 23 | return &resourceSet{ 24 | scheme: scheme, 25 | apiResources: make(map[schema.GroupKind]metav1.APIResource), 26 | } 27 | } 28 | 29 | // Add will add a new resource to the current instance's set 30 | // If the resource is not valid it will return an error 31 | func (s *resourceSet) Add(key schema.GroupKind, val metav1.APIResource) error { 32 | if isSubResource(val) { 33 | return nil 34 | } 35 | 36 | if ok, err := isRegisteredKubeLinterKind(val); err != nil { 37 | return fmt.Errorf("checking if resource %s, is registered KubeLinter kind: %w", val.String(), err) 38 | } else if !ok { 39 | return nil 40 | } 41 | 42 | if !s.scheme.Recognizes(gvkFromMetav1APIResource(val)) { 43 | return nil 44 | } 45 | 46 | if existing, ok := s.apiResources[key]; ok { 47 | existing.Version = s.getPriorityVersion(existing.Group, existing.Version, val.Version) 48 | 49 | s.apiResources[key] = existing 50 | } else { 51 | s.apiResources[key] = val 52 | } 53 | return nil 54 | } 55 | 56 | // getPriorityVersion returns fixed group version if needed 57 | func (s *resourceSet) getPriorityVersion(group, existingVer, currentVer string) string { 58 | gv := s.scheme.PrioritizedVersionsAllGroups() 59 | for _, v := range gv { 60 | if v.Group != group { 61 | continue 62 | } 63 | if v.Version == existingVer { 64 | return existingVer 65 | } 66 | if v.Version == currentVer { 67 | return currentVer 68 | } 69 | } 70 | return existingVer 71 | } 72 | 73 | // ToSlice returns resources from the set as a slice 74 | // of 'APIResource' entities ("k8s.io/apimachinery/pkg/apis/meta/v1") 75 | func (s *resourceSet) ToSlice() []metav1.APIResource { 76 | res := make([]metav1.APIResource, 0, len(s.apiResources)) 77 | 78 | for _, v := range s.apiResources { 79 | res = append(res, v) 80 | } 81 | 82 | return res 83 | } 84 | 85 | func reconcileResourceList(client discovery.DiscoveryInterface, 86 | scheme *runtime.Scheme) ([]metav1.APIResource, error) { 87 | set := newResourceSet(scheme) 88 | 89 | _, apiResourceLists, err := client.ServerGroupsAndResources() 90 | if err != nil { 91 | return nil, err 92 | } 93 | 94 | for _, apiResourceList := range apiResourceLists { 95 | gv, err := schema.ParseGroupVersion(apiResourceList.GroupVersion) 96 | if err != nil { 97 | return nil, err 98 | } 99 | for _, rsc := range apiResourceList.APIResources { 100 | rsc.Group, rsc.Version = gv.Group, gv.Version 101 | key := schema.GroupKind{ 102 | Group: gv.Group, 103 | Kind: rsc.Kind, 104 | } 105 | if err := set.Add(key, rsc); err != nil { 106 | return nil, fmt.Errorf("adding resource %s to set: %w", rsc.String(), err) 107 | } 108 | } 109 | } 110 | return set.ToSlice(), nil 111 | } 112 | 113 | // isSubResource returns true if the apiResource.Name has a "/" in it eg: pod/status 114 | func isSubResource(apiResource metav1.APIResource) bool { 115 | return strings.Contains(apiResource.Name, "/") 116 | } 117 | 118 | func isRegisteredKubeLinterKind(rsrc metav1.APIResource) (bool, error) { 119 | // Construct the gvks for objects to watch. Remove the Any 120 | // kind or else all objects kinds will be watched. 121 | kubeLinterKinds := getKubeLinterKinds() 122 | kubeLinterMatcher, err := objectkinds.ConstructMatcher(kubeLinterKinds...) 123 | if err != nil { 124 | return false, err 125 | } 126 | 127 | gvk := gvkFromMetav1APIResource(rsrc) 128 | if kubeLinterMatcher.Matches(gvk) { 129 | return true, nil 130 | } 131 | return false, nil 132 | } 133 | 134 | func getKubeLinterKinds() []string { 135 | kubeLinterKinds := objectkinds.AllObjectKinds() 136 | for i := range kubeLinterKinds { 137 | if kubeLinterKinds[i] == objectkinds.Any { 138 | kubeLinterKinds = append(kubeLinterKinds[:i], kubeLinterKinds[i+1:]...) 139 | break 140 | } 141 | } 142 | return kubeLinterKinds 143 | } 144 | 145 | func gvkFromMetav1APIResource(rsc metav1.APIResource) schema.GroupVersionKind { 146 | return schema.GroupVersionKind{ 147 | Group: rsc.Group, 148 | Version: rsc.Version, 149 | Kind: rsc.Kind, 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /pkg/testutils/templates/ReplicaSet.yaml.tpl: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: ReplicaSet 3 | metadata: 4 | annotations: 5 | deployment.kubernetes.io/desired-replicas: "{{ .Replicas }}" 6 | deployment.kubernetes.io/max-replicas: "6" 7 | deployment.kubernetes.io/revision: "17" 8 | qontract.caller_name: saas-github-mirror 9 | qontract.integration: openshift-saas-deploy 10 | qontract.integration_version: 0.1.0 11 | qontract.sha256sum: 4af0efb326680a464e1f2c8f2780e36f749493e8f91cba1a7343e2508c30c08f 12 | qontract.update: 2020-09-02T12:36:35 13 | creationTimestamp: "2020-09-02T12:36:36Z" 14 | generation: 5 15 | labels: 16 | app: github-mirror 17 | pod-template-hash: 5797889b8c 18 | name: github-mirror-5797889b8c 19 | namespace: github-mirror-production 20 | ownerReferences: 21 | - apiVersion: apps/v1 22 | blockOwnerDeletion: true 23 | controller: true 24 | kind: Deployment 25 | name: github-mirror 26 | uid: 3b4d6091-4c16-11ea-bf75-023e213e25c3 27 | resourceVersion: "267502438" 28 | selfLink: /apis/apps/v1/namespaces/github-mirror-production/replicasets/github-mirror-5797889b8c 29 | uid: 2faee3f3-c63a-4a92-9638-5b7c3861269c 30 | spec: 31 | replicas: {{ .Replicas }} 32 | selector: 33 | matchLabels: 34 | app: github-mirror 35 | pod-template-hash: 5797889b8c 36 | template: 37 | metadata: 38 | creationTimestamp: null 39 | labels: 40 | app: github-mirror 41 | pod-template-hash: 5797889b8c 42 | spec: 43 | affinity: 44 | podAntiAffinity: 45 | preferredDuringSchedulingIgnoredDuringExecution: 46 | - podAffinityTerm: 47 | labelSelector: 48 | matchExpressions: 49 | - key: app 50 | operator: In 51 | values: 52 | - github-mirror 53 | topologyKey: kubernetes.io/hostname 54 | weight: 100 55 | - podAffinityTerm: 56 | labelSelector: 57 | matchExpressions: 58 | - key: app 59 | operator: In 60 | values: 61 | - github-mirror 62 | topologyKey: failure-domain.beta.kubernetes.io/zone 63 | weight: 100 64 | containers: 65 | - env: 66 | - name: GITHUB_USERS 67 | value: app-sre-bot:cs-sre-bot 68 | - name: GITHUB_MIRROR_URL 69 | value: https://github-mirror.devshift.net 70 | - name: CACHE_TYPE 71 | value: redis 72 | - name: PRIMARY_ENDPOINT 73 | valueFrom: 74 | secretKeyRef: 75 | key: db.endpoint 76 | name: ghmirror-elasticache-production 77 | - name: READER_ENDPOINT 78 | value: replica.ghmirror-redis-production.huo5rn.use1.cache.amazonaws.com 79 | - name: REDIS_PORT 80 | valueFrom: 81 | secretKeyRef: 82 | key: db.port 83 | name: ghmirror-elasticache-production 84 | - name: REDIS_TOKEN 85 | valueFrom: 86 | secretKeyRef: 87 | key: db.auth_token 88 | name: ghmirror-elasticache-production 89 | - name: REDIS_SSL 90 | value: "True" 91 | image: quay.io/app-sre/github-mirror:5344cbb 92 | imagePullPolicy: Always 93 | livenessProbe: 94 | failureThreshold: 3 95 | httpGet: 96 | path: /healthz 97 | port: 8080 98 | scheme: HTTP 99 | initialDelaySeconds: 30 100 | periodSeconds: 10 101 | successThreshold: 1 102 | timeoutSeconds: 3 103 | name: github-mirror 104 | ports: 105 | - containerPort: 8080 106 | name: github-mirror 107 | protocol: TCP 108 | readinessProbe: 109 | failureThreshold: 3 110 | httpGet: 111 | path: /healthz 112 | port: 8080 113 | scheme: HTTP 114 | initialDelaySeconds: 3 115 | periodSeconds: 10 116 | successThreshold: 1 117 | timeoutSeconds: 3 118 | resources: 119 | {{- if .ResourceLimits }} 120 | limits: 121 | cpu: "1" 122 | memory: 1Gi 123 | {{- end }} 124 | {{- if .ResourceRequests }} 125 | requests: 126 | cpu: 500m 127 | memory: 800Mi 128 | {{- end }} 129 | terminationMessagePath: /dev/termination-log 130 | terminationMessagePolicy: File 131 | dnsPolicy: ClusterFirst 132 | restartPolicy: Always 133 | schedulerName: default-scheduler 134 | securityContext: {} 135 | terminationGracePeriodSeconds: 30 136 | status: 137 | availableReplicas: {{ .Replicas }} 138 | fullyLabeledReplicas: {{ .Replicas }} 139 | observedGeneration: 5 140 | readyReplicas: {{ .Replicas }} 141 | replicas: {{ .Replicas }} 142 | -------------------------------------------------------------------------------- /pkg/controller/validationscache_test.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "fmt" 5 | "runtime" 6 | "testing" 7 | 8 | "github.com/app-sre/deployment-validation-operator/pkg/validations" 9 | "github.com/stretchr/testify/assert" 10 | appsv1 "k8s.io/api/apps/v1" 11 | corev1 "k8s.io/api/core/v1" 12 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 13 | ) 14 | 15 | // TestValidationCache runs four tests on validationscache file's functions 16 | // checks that store adds key-value pair properly 17 | // checks objectAlreadyValidated different scenarios 18 | // - key does not exist 19 | // - resource version does not match 20 | // - everything runs properly 21 | func TestValidationsCache(t *testing.T) { 22 | 23 | t.Run("store adds new key and value to current instance", func(t *testing.T) { 24 | // Given 25 | mock := newValidationCache() 26 | mockClientObject := corev1.Namespace{ObjectMeta: metav1.ObjectMeta{ 27 | ResourceVersion: "mock_version", 28 | UID: "mock_uid", 29 | }} 30 | 31 | // When 32 | mock.store(&mockClientObject, "", "mock_outcome") 33 | 34 | // Assert 35 | expected := newValidationResource(newResourceversionVal("mock_version"), "mock_uid", "mock_outcome") 36 | assert.Equal(t, expected, (*mock)[newValidationKey(&mockClientObject, "")]) 37 | }) 38 | 39 | t.Run("objectAlreadyValidated : key does not exist", func(t *testing.T) { 40 | // Given 41 | mock := newValidationCache() 42 | mockClientObject := corev1.Namespace{ObjectMeta: metav1.ObjectMeta{ 43 | ResourceVersion: "mock_version", 44 | UID: "mock_uid", 45 | }} 46 | 47 | // When 48 | test := mock.objectAlreadyValidated(&mockClientObject, "") 49 | 50 | // Assert 51 | assert.False(t, test) 52 | }) 53 | 54 | t.Run("objectAlreadyValidated : resource versions do not match", func(t *testing.T) { 55 | // Given 56 | mock := newValidationCache() 57 | mockClientObject := corev1.Namespace{ObjectMeta: metav1.ObjectMeta{ 58 | ResourceVersion: "mock_version", 59 | UID: "mock_uid", 60 | }} 61 | mock.store(&mockClientObject, "", "mock_outcome") 62 | toBeRemovedKey := newValidationKey(&mockClientObject, "") 63 | 64 | // When 65 | mockClientObject.ResourceVersion = "mock_new_version" 66 | test := mock.objectAlreadyValidated(&mockClientObject, "") 67 | 68 | // Assert 69 | assert.False(t, test) 70 | assert.False(t, mock.has(toBeRemovedKey)) 71 | }) 72 | 73 | t.Run("objectAlreadyValidated : OK", func(t *testing.T) { 74 | // Given 75 | mock := newValidationCache() 76 | mockClientObject := corev1.Namespace{ObjectMeta: metav1.ObjectMeta{ 77 | ResourceVersion: "mock_version", 78 | UID: "mock_uid", 79 | }} 80 | mock.store(&mockClientObject, "", "mock_outcome") 81 | 82 | // When 83 | test := mock.objectAlreadyValidated(&mockClientObject, "") 84 | 85 | // Assert 86 | assert.True(t, test) 87 | }) 88 | 89 | t.Run("storing two different objects with the same name and namespace", func(t *testing.T) { 90 | // Given 91 | testCache := newValidationCache() 92 | dep1 := appsv1.Deployment{ 93 | ObjectMeta: metav1.ObjectMeta{ 94 | Name: "test-deployment", 95 | Namespace: "test-app", 96 | UID: "foo123", 97 | }, 98 | } 99 | dep2 := appsv1.Deployment{ 100 | ObjectMeta: metav1.ObjectMeta{ 101 | Name: "test-deployment", 102 | Namespace: "test-app", 103 | UID: "bar345", 104 | }, 105 | } 106 | testCache.store(&dep1, "", validations.ObjectNeedsImprovement) 107 | testCache.store(&dep2, "", validations.ObjectValid) 108 | 109 | resource1, exists := testCache.retrieve(&dep1, "") 110 | assert.True(t, exists) 111 | assert.Equal(t, validations.ObjectNeedsImprovement, resource1.outcome) 112 | 113 | resource2, exists := testCache.retrieve(&dep2, "") 114 | assert.True(t, exists) 115 | assert.Equal(t, validations.ObjectValid, resource2.outcome) 116 | }) 117 | } 118 | 119 | func printMemoryInfo(s string) { 120 | var m runtime.MemStats 121 | runtime.ReadMemStats(&m) 122 | fmt.Printf("%s: %d KB\n", s, m.Alloc/1024) 123 | } 124 | 125 | func Benchmark_ValidationCache(b *testing.B) { 126 | vc := newValidationCache() 127 | printMemoryInfo("Memory consumption after empty cache creation") 128 | 129 | for i := 0; i < b.N; i++ { 130 | name := fmt.Sprintf("test-%d", i) 131 | vc.store(&appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Name: name}}, "", validations.ObjectValid) 132 | } 133 | printMemoryInfo(fmt.Sprintf("Memory consumption after storing %d items in the cache", b.N)) 134 | for i := 0; i < b.N; i++ { 135 | name := fmt.Sprintf("test-%d", i) 136 | vc.remove(&appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Name: name}}, "") 137 | } 138 | runtime.GC() 139 | printMemoryInfo("Memory consumption after removing the items ") 140 | 141 | for i := 0; i < b.N; i++ { 142 | name := fmt.Sprintf("test-%d", i) 143 | vc.store(&appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Name: name}}, "", validations.ObjectValid) 144 | } 145 | printMemoryInfo(fmt.Sprintf("Memory consumption after storing %d items again", b.N)) 146 | } 147 | -------------------------------------------------------------------------------- /pkg/configmap/configmap_watcher.go: -------------------------------------------------------------------------------- 1 | package configmap 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "reflect" 8 | "time" 9 | 10 | "golang.stackrox.io/kube-linter/pkg/config" 11 | 12 | "github.com/app-sre/deployment-validation-operator/pkg/validations" 13 | "github.com/ghodss/yaml" 14 | "github.com/go-logr/logr" 15 | apicorev1 "k8s.io/api/core/v1" 16 | "k8s.io/client-go/informers" 17 | "k8s.io/client-go/kubernetes" 18 | "k8s.io/client-go/rest" 19 | "k8s.io/client-go/tools/cache" 20 | "sigs.k8s.io/controller-runtime/pkg/log" 21 | ) 22 | 23 | type Watcher struct { 24 | clientset kubernetes.Interface 25 | cfg config.Config 26 | ch chan struct{} 27 | logger logr.Logger 28 | namespace string 29 | } 30 | 31 | var configMapName = "deployment-validation-operator-config" 32 | var configMapDataAccess = "deployment-validation-operator-config.yaml" 33 | 34 | // NewWatcher creates a new Watcher instance for observing changes to a ConfigMap. 35 | // 36 | // Parameters: 37 | // - cfg: A pointer to a rest.Config representing the Kubernetes client configuration. 38 | // 39 | // Returns: 40 | // - A pointer to a Watcher instance for monitoring changes to DVO ConfigMap resource. 41 | // - An error if there's an issue while initializing the Kubernetes clientset. 42 | func NewWatcher(cfg *rest.Config) (*Watcher, error) { 43 | clientset, err := kubernetes.NewForConfig(cfg) 44 | if err != nil { 45 | return nil, fmt.Errorf("initializing clientset: %w", err) 46 | } 47 | 48 | // the Informer will use this to monitor the namespace for the ConfigMap. 49 | namespace, err := getPodNamespace() 50 | if err != nil { 51 | return nil, fmt.Errorf("getting namespace: %w", err) 52 | } 53 | 54 | return &Watcher{ 55 | clientset: clientset, 56 | logger: log.Log.WithName("ConfigMapWatcher"), 57 | ch: make(chan struct{}), 58 | namespace: namespace, 59 | }, nil 60 | } 61 | 62 | // Start will update the channel structure with new configuration data from ConfigMap update event 63 | func (cmw *Watcher) Start(ctx context.Context) error { 64 | factory := informers.NewSharedInformerFactoryWithOptions( 65 | cmw.clientset, time.Second*30, informers.WithNamespace(cmw.namespace), 66 | ) 67 | informer := factory.Core().V1().ConfigMaps().Informer() 68 | 69 | informer.AddEventHandler(cache.ResourceEventHandlerFuncs{ // nolint:errcheck 70 | AddFunc: func(obj interface{}) { 71 | newCm := obj.(*apicorev1.ConfigMap) 72 | 73 | if configMapName != newCm.GetName() { 74 | return 75 | } 76 | 77 | cmw.logger.Info( 78 | "a ConfigMap has been created under watched namespace", 79 | "name", newCm.GetName(), 80 | "namespace", newCm.GetNamespace(), 81 | ) 82 | 83 | cfg, err := readConfig(newCm.Data[configMapDataAccess]) 84 | if err != nil { 85 | cmw.logger.Error(err, "ConfigMap data format") 86 | return 87 | } 88 | 89 | cmw.cfg = cfg 90 | 91 | cmw.ch <- struct{}{} 92 | }, 93 | UpdateFunc: func(oldObj, newObj interface{}) { 94 | newCm := newObj.(*apicorev1.ConfigMap) 95 | 96 | // This is sometimes triggered even if no change was due to the ConfigMap 97 | if configMapName != newCm.GetName() || reflect.DeepEqual(oldObj, newObj) { 98 | return 99 | } 100 | 101 | cmw.logger.Info( 102 | "a ConfigMap has been updated under watched namespace", 103 | "name", newCm.GetName(), 104 | "namespace", newCm.GetNamespace(), 105 | ) 106 | 107 | cfg, err := readConfig(newCm.Data[configMapDataAccess]) 108 | if err != nil { 109 | cmw.logger.Error(err, "ConfigMap data format") 110 | return 111 | } 112 | 113 | cmw.cfg = cfg 114 | 115 | cmw.ch <- struct{}{} 116 | }, 117 | DeleteFunc: func(oldObj interface{}) { 118 | cm := oldObj.(*apicorev1.ConfigMap) 119 | 120 | cmw.logger.Info( 121 | "a ConfigMap has been deleted under watched namespace", 122 | "name", cm.GetName(), 123 | "namespace", cm.GetNamespace(), 124 | ) 125 | 126 | cmw.cfg = config.Config{ 127 | Checks: validations.GetDefaultChecks(), 128 | } 129 | 130 | cmw.ch <- struct{}{} 131 | }, 132 | }) 133 | 134 | factory.Start(ctx.Done()) 135 | 136 | return nil 137 | } 138 | 139 | // ConfigChanged receives push notifications when the configuration is updated 140 | func (cmw *Watcher) ConfigChanged() <-chan struct{} { 141 | return cmw.ch 142 | } 143 | 144 | // GetConfig returns a previously saved kube-linter Config structure 145 | func (cmw *Watcher) GetConfig() config.Config { 146 | return cmw.cfg 147 | } 148 | 149 | // readConfig returns a valid Kube-linter Config structure 150 | // based on the checks received by the string 151 | func readConfig(data string) (config.Config, error) { 152 | var cfg config.Config 153 | 154 | err := yaml.Unmarshal([]byte(data), &cfg, yaml.DisallowUnknownFields) 155 | if err != nil { 156 | return cfg, fmt.Errorf("unmarshalling configmap data: %w", err) 157 | } 158 | 159 | return cfg, nil 160 | } 161 | 162 | func getPodNamespace() (string, error) { 163 | namespace, exists := os.LookupEnv("POD_NAMESPACE") 164 | if !exists { 165 | return "", fmt.Errorf("could not find DVO pod") 166 | } 167 | 168 | return namespace, nil 169 | } 170 | -------------------------------------------------------------------------------- /pkg/utils/selector.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 5 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 6 | ) 7 | 8 | type manifestPath []string 9 | 10 | var ( 11 | // known manifest/resource paths for selector requirements 12 | selectorMatchLabels manifestPath = []string{"spec", "selector", "matchLabels"} 13 | podSelectorMatchLabels manifestPath = []string{"spec", "podSelector", "matchLabels"} 14 | // v1.Service, v1.ReplicationController defines the selector as map[string]string 15 | // (and not as metav1.LabelSelector) in the ".spec.selector" path 16 | selectorMap manifestPath = []string{"spec", "selector"} 17 | selectorMatchExpressions manifestPath = []string{"spec", "selector", "matchExpressions"} 18 | podSelectorMatchExpressions manifestPath = []string{"spec", "podSelector", "matchExpressions"} 19 | ) 20 | 21 | // GetLabelSelector parses the unstructured object and checks following: 22 | // 1. Is there some "spec.selector" or "spec.podSelector". If not then return nil. 23 | // 2. Is there some empty "spec.selector" or "spec.podSelector". If yes then return empty LabelSelector struct. 24 | // 3. Otherwise try to parse "matchLabels" and "matchExpressions" requirements and create LabelSelector with 25 | // these values. 26 | func GetLabelSelector(object *unstructured.Unstructured) *metav1.LabelSelector { 27 | if !hasSelector(object) { 28 | return nil 29 | } 30 | 31 | if hasEmptySelector(object) { 32 | return &metav1.LabelSelector{} 33 | } 34 | 35 | matchLabels := readMatchLabels(object) 36 | matchExpressions := readMatchExpressions(object) 37 | return &metav1.LabelSelector{ 38 | MatchExpressions: matchExpressions, 39 | MatchLabels: matchLabels, 40 | } 41 | } 42 | 43 | // readMatchLabels tries to parse known paths for the "matchLabels" attribute in the 44 | // unstructured object input 45 | func readMatchLabels(object *unstructured.Unstructured) map[string]string { 46 | for _, path := range []manifestPath{selectorMatchLabels, podSelectorMatchLabels, selectorMap} { 47 | pathExists := pathExistsAsMap(object, path) 48 | if pathExists { 49 | matchLabels, _, _ := unstructured.NestedStringMap(object.Object, path...) 50 | return matchLabels 51 | } 52 | } 53 | return nil 54 | } 55 | 56 | // readMatchExpressions tries to parse known paths for the "matchExpressions" attribute in the 57 | // unstructured object input 58 | func readMatchExpressions(object *unstructured.Unstructured) []metav1.LabelSelectorRequirement { 59 | matchExpressionsRequirements := []metav1.LabelSelectorRequirement{} 60 | for _, path := range []manifestPath{selectorMatchExpressions, podSelectorMatchExpressions} { 61 | pathExists := pathExistsAsSlice(object, path) 62 | if !pathExists { 63 | continue 64 | } 65 | matchExpressions, _, _ := unstructured.NestedSlice(object.Object, path...) 66 | for _, matchExpression := range matchExpressions { 67 | meMap := matchExpression.(map[string]interface{}) 68 | labelSelectorReq := metav1.LabelSelectorRequirement{} 69 | for k, v := range meMap { 70 | switch { 71 | case k == "key": 72 | labelSelectorReq.Key = v.(string) 73 | case k == "operator": 74 | labelSelectorReq.Operator = metav1.LabelSelectorOperator(v.(string)) 75 | case k == "values": 76 | stringValues := []string{} 77 | for _, value := range v.([]interface{}) { 78 | stringValues = append(stringValues, value.(string)) 79 | } 80 | labelSelectorReq.Values = stringValues 81 | } 82 | } 83 | matchExpressionsRequirements = append(matchExpressionsRequirements, labelSelectorReq) 84 | } 85 | } 86 | 87 | if len(matchExpressionsRequirements) == 0 { 88 | return nil 89 | } 90 | return matchExpressionsRequirements 91 | } 92 | 93 | // hasEmptySelector checks whether of the "spec.selector" and "spec.podSelector" is defined, but empty. 94 | func hasEmptySelector(object *unstructured.Unstructured) bool { 95 | selector, found, _ := unstructured.NestedMap(object.Object, "spec", "selector") 96 | if found && len(selector) == 0 { 97 | return true 98 | } 99 | podSelector, found, _ := unstructured.NestedMap(object.Object, "spec", "podSelector") 100 | if found && len(podSelector) == 0 { 101 | return true 102 | } 103 | return false 104 | } 105 | 106 | // hasSelector checks whether there is some "spec.selector" or "spec.podSelector" value defined. 107 | func hasSelector(object *unstructured.Unstructured) bool { 108 | _, selectorFound, _ := unstructured.NestedMap(object.Object, "spec", "selector") 109 | _, podSelectorfound, _ := unstructured.NestedMap(object.Object, "spec", "podSelector") 110 | return selectorFound || podSelectorfound 111 | } 112 | 113 | // pathExistsAsMap checks whether provided manifest path exists as map[string]interface{} 114 | func pathExistsAsMap(object *unstructured.Unstructured, path manifestPath) bool { 115 | _, found, _ := unstructured.NestedMap(object.Object, path...) 116 | return found 117 | } 118 | 119 | // pathExistsAsSlice checks whether provided manifest path exists as []interface{} 120 | func pathExistsAsSlice(object *unstructured.Unstructured, path manifestPath) bool { 121 | _, found, _ := unstructured.NestedSlice(object.Object, path...) 122 | return found 123 | } 124 | -------------------------------------------------------------------------------- /bundle/manifests/deployment-validation-operator.clusterserviceversion.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: operators.coreos.com/v1alpha1 2 | kind: ClusterServiceVersion 3 | metadata: 4 | annotations: 5 | alm-examples: '[]' 6 | capabilities: Basic Install 7 | createdAt: "2023-09-12T14:13:09Z" 8 | operators.operatorframework.io/builder: operator-sdk-v1.31.0+git 9 | operators.operatorframework.io/project_layout: unknown 10 | name: deployment-validation-operator.v0.0.0 11 | namespace: placeholder 12 | spec: 13 | apiservicedefinitions: {} 14 | customresourcedefinitions: {} 15 | description: Deployment Validation Operator description. TODO. 16 | displayName: Deployment Validation Operator 17 | icon: 18 | - base64data: "" 19 | mediatype: "" 20 | install: 21 | spec: 22 | clusterPermissions: 23 | - rules: 24 | - apiGroups: 25 | - '*' 26 | resources: 27 | - '*' 28 | verbs: 29 | - get 30 | - list 31 | - watch 32 | serviceAccountName: deployment-validation-operator 33 | deployments: 34 | - label: 35 | app: deployment-validation-operator 36 | name: deployment-validation-operator 37 | spec: 38 | replicas: 1 39 | selector: 40 | matchLabels: 41 | app: deployment-validation-operator 42 | strategy: 43 | rollingUpdate: 44 | maxSurge: 1 45 | maxUnavailable: 0 46 | type: RollingUpdate 47 | template: 48 | metadata: 49 | labels: 50 | app: deployment-validation-operator 51 | name: deployment-validation-operator 52 | spec: 53 | affinity: 54 | podAntiAffinity: 55 | preferredDuringSchedulingIgnoredDuringExecution: 56 | - podAffinityTerm: 57 | labelSelector: 58 | matchExpressions: 59 | - key: app 60 | operator: In 61 | values: 62 | - deployment-validation-operator 63 | topologyKey: kubernetes.io/hostname 64 | weight: 100 65 | containers: 66 | - args: 67 | - --config /config/deployment-validation-operator-config.yaml 68 | env: 69 | - name: WATCH_NAMESPACE 70 | valueFrom: 71 | fieldRef: 72 | fieldPath: metadata.annotations['olm.targetNamespaces'] 73 | - name: OPERATOR_NAME 74 | value: deployment-validation-operator 75 | - name: NAMESPACE_IGNORE_PATTERN 76 | value: openshift.*|kube-.+|open-cluster-management-.*|default|dedicated-admin 77 | - name: RESOURCES_PER_LIST_QUERY 78 | value: "5" 79 | - name: VALIDATION_CHECK_INTERVAL 80 | value: "2m" 81 | - name: POD_NAME 82 | valueFrom: 83 | fieldRef: 84 | fieldPath: metadata.name 85 | - name: POD_NAMESPACE 86 | valueFrom: 87 | fieldRef: 88 | fieldPath: metadata.namespace 89 | image: quay.io/deployment-validation-operator/dv-operator:latest 90 | imagePullPolicy: Always 91 | livenessProbe: 92 | httpGet: 93 | path: /healthz 94 | port: 8081 95 | initialDelaySeconds: 30 96 | periodSeconds: 10 97 | timeoutSeconds: 3 98 | name: deployment-validation-operator 99 | readinessProbe: 100 | httpGet: 101 | path: /readyz 102 | port: 8081 103 | initialDelaySeconds: 5 104 | periodSeconds: 10 105 | timeoutSeconds: 3 106 | resources: 107 | limits: 108 | memory: 400Mi 109 | requests: 110 | cpu: 50m 111 | memory: 200Mi 112 | securityContext: 113 | readOnlyRootFilesystem: true 114 | volumeMounts: 115 | - mountPath: /config 116 | name: dvo-config 117 | restartPolicy: Always 118 | serviceAccountName: deployment-validation-operator 119 | terminationGracePeriodSeconds: 30 120 | tolerations: 121 | - effect: NoSchedule 122 | key: node-role.kubernetes.io/infra 123 | operator: Exists 124 | volumes: 125 | - configMap: 126 | name: deployment-validation-operator-config 127 | optional: true 128 | name: dvo-config 129 | permissions: 130 | - rules: 131 | - apiGroups: 132 | - "" 133 | resources: 134 | - configmaps 135 | - services 136 | verbs: 137 | - get 138 | - create 139 | - list 140 | - delete 141 | - update 142 | - watch 143 | - patch 144 | - apiGroups: 145 | - monitoring.coreos.com 146 | resources: 147 | - servicemonitors 148 | verbs: 149 | - '*' 150 | serviceAccountName: deployment-validation-operator 151 | strategy: deployment 152 | installModes: 153 | - supported: false 154 | type: OwnNamespace 155 | - supported: false 156 | type: SingleNamespace 157 | - supported: false 158 | type: MultiNamespace 159 | - supported: true 160 | type: AllNamespaces 161 | keywords: 162 | - deployment-validation-operator 163 | links: 164 | - name: Deployment Validation Operator 165 | url: https://deployment-validation-operator.domain 166 | maintainers: 167 | - email: your@email.com 168 | name: Maintainer Name 169 | maturity: alpha 170 | provider: 171 | name: Provider Name 172 | url: https://your.domain 173 | version: 0.0.0 174 | -------------------------------------------------------------------------------- /pkg/prometheus/prometheus.go: -------------------------------------------------------------------------------- 1 | package prometheus 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "net/http/pprof" 8 | "strings" 9 | "time" 10 | 11 | "github.com/app-sre/deployment-validation-operator/config" 12 | "github.com/app-sre/deployment-validation-operator/pkg/validations" 13 | "github.com/prometheus/client_golang/prometheus" 14 | "github.com/prometheus/client_golang/prometheus/collectors" 15 | "github.com/prometheus/client_golang/prometheus/promhttp" 16 | dto "github.com/prometheus/client_model/go" 17 | "golang.stackrox.io/kube-linter/pkg/checkregistry" 18 | ) 19 | 20 | type Registry interface { 21 | Register(prometheus.Collector) error 22 | Gather() ([]*dto.MetricFamily, error) 23 | } 24 | 25 | // registerCollectorError is returned by the NewServer method if the Registry 26 | // causes any error on registering a Collector 27 | type registerCollectorError struct { 28 | collector string 29 | trace error 30 | } 31 | 32 | func (err registerCollectorError) Error() string { 33 | return fmt.Sprintf("registering %s collector: %s", err.collector, err.trace.Error()) 34 | } 35 | 36 | // NewServer returns a server configured to work on path and address given 37 | // It registers a collector which exports the current state of process metrics 38 | // and a collector that exports metrics about the current process 39 | // It may return registerCollectorError if the collectors are already registered 40 | func NewServer(registry Registry, path, addr string) (*Server, error) { 41 | if !strings.HasPrefix(path, "/") { 42 | path = "/" + path 43 | } 44 | 45 | mux, err := getRouter(registry, path) 46 | if err != nil { 47 | return nil, err 48 | } 49 | 50 | return &Server{ 51 | s: &http.Server{ 52 | Addr: addr, 53 | Handler: mux, 54 | ReadHeaderTimeout: 2 * time.Second, 55 | }, 56 | }, nil 57 | } 58 | 59 | // newPprofServeMux creates a new HTTP ServeMux with pprof handlers registered. 60 | // It is intended for exposing pprof endpoints such as CPU and memory profiling. 61 | func newPprofServeMux() *http.ServeMux { 62 | mux := http.NewServeMux() 63 | 64 | // Register pprof handlers on the ServeMux with specific URL paths. 65 | mux.HandleFunc("/debug/pprof/", pprof.Index) 66 | mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) 67 | mux.HandleFunc("/debug/pprof/profile", pprof.Profile) 68 | mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol) 69 | mux.HandleFunc("/debug/pprof/trace", pprof.Trace) 70 | 71 | return mux 72 | } 73 | 74 | // getRouter registers two collectors to deliver metrics on a given path 75 | // It may return registerCollectorError if the collectors are already registered 76 | func getRouter(registry Registry, path string) (*http.ServeMux, error) { 77 | var ( 78 | processCollector = collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}) 79 | goCollector = collectors.NewGoCollector() 80 | ) 81 | 82 | if err := registry.Register(processCollector); err != nil { 83 | return nil, registerCollectorError{ 84 | collector: "process", 85 | trace: err, 86 | } 87 | } 88 | 89 | if err := registry.Register(goCollector); err != nil { 90 | return nil, registerCollectorError{ 91 | collector: "go", 92 | trace: err, 93 | } 94 | } 95 | 96 | mux := newPprofServeMux() 97 | mux.Handle(path, promhttp.HandlerFor(registry, promhttp.HandlerOpts{})) 98 | 99 | return mux, nil 100 | } 101 | 102 | // PreloadMetrics preloads metrics related to predefined checks into the provided Prometheus registry. 103 | // It retrieves predefined checks from the linter registry, sets up corresponding GaugeVec metrics, 104 | // and registers them in the Prometheus registry. 105 | // 106 | // Parameters: 107 | // - pr: A pointer to a Prometheus registry where the metrics will be registered. 108 | // 109 | // Returns: 110 | // - A map of check names to corresponding GaugeVec metrics. 111 | // - An error if any error occurs during metric setup or registration. 112 | func PreloadMetrics(pr *prometheus.Registry) (map[string]*prometheus.GaugeVec, error) { 113 | preloadedMetrics := make(map[string]*prometheus.GaugeVec) 114 | 115 | klr, err := validations.GetKubeLinterRegistry() 116 | if err != nil { 117 | return nil, err 118 | } 119 | 120 | checks, err := validations.GetAllNamesFromRegistry(klr) 121 | if err != nil { 122 | return nil, err 123 | } 124 | 125 | for _, checkName := range checks { 126 | metric, err := setupMetric(klr, checkName) 127 | if err != nil { 128 | return nil, fmt.Errorf("unable to create metric for check %s", checkName) 129 | } 130 | 131 | if err := pr.Register(metric); err != nil { 132 | return nil, fmt.Errorf("registering metric for check %q: %w", checkName, err) 133 | } 134 | 135 | preloadedMetrics[checkName] = metric 136 | } 137 | 138 | return preloadedMetrics, nil 139 | } 140 | 141 | // setupMetric sets up a Prometheus metric based on the provided checkname and information from a CheckRegistry. 142 | // The metric is created with the formatted name, description, and remediation information from the check specification. 143 | func setupMetric(reg checkregistry.CheckRegistry, name string) (*prometheus.GaugeVec, error) { 144 | check := reg.Load(name) 145 | if check == nil { 146 | return nil, fmt.Errorf("unable to create metric for check %s", name) 147 | } 148 | 149 | return prometheus.NewGaugeVec( 150 | prometheus.GaugeOpts{ 151 | Name: strings.ReplaceAll( 152 | fmt.Sprintf("%s_%s", config.OperatorName, check.Spec.Name), 153 | "-", "_"), 154 | Help: fmt.Sprintf( 155 | "Description: %s ; Remediation: %s", 156 | check.Spec.Description, 157 | check.Spec.Remediation, 158 | ), 159 | ConstLabels: prometheus.Labels{ 160 | "check_description": check.Spec.Description, 161 | "check_remediation": check.Spec.Remediation, 162 | }, 163 | }, []string{"namespace_uid", "namespace", "uid", "name", "kind"}), nil 164 | } 165 | 166 | type Server struct { 167 | s *http.Server 168 | } 169 | 170 | func (s *Server) Start(ctx context.Context) error { 171 | errCh := make(chan error) 172 | drain := func() { 173 | for range errCh { 174 | continue 175 | } 176 | } 177 | 178 | defer drain() 179 | 180 | go func() { 181 | defer close(errCh) 182 | 183 | errCh <- s.s.ListenAndServe() 184 | }() 185 | 186 | select { 187 | case err := <-errCh: 188 | return err 189 | case <-ctx.Done(): 190 | return s.s.Close() 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /pkg/testutils/templates/Deployment.yaml.tpl: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | annotations: 5 | deployment.kubernetes.io/revision: "17" 6 | kubectl.kubernetes.io/last-applied-configuration: | 7 | {"apiVersion":"apps/v1","kind":"Deployment","metadata":{"annotations":{"qontract.caller_name":"saas-github-mirror","qontract.integration":"openshift-saas-deploy","qontract.integration_version":"0.1.0","qontract.sha256sum":"4af0efb326680a464e1f2c8f2780e36f749493e8f91cba1a7343e2508c30c08f","qontract.update":"2020-09-02T12:36:35"},"labels":{"app":"github-mirror"},"name":"github-mirror","namespace":"github-mirror-production"},"spec":{"replicas":5,"selector":{"matchLabels":{"app":"github-mirror"}},"strategy":{"rollingUpdate":{"maxSurge":1,"maxUnavailable":0},"type":"RollingUpdate"},"template":{"metadata":{"labels":{"app":"github-mirror"}},"spec":{"affinity":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app","operator":"In","values":["github-mirror"]}]},"topologyKey":"kubernetes.io/hostname"},"weight":100},{"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app","operator":"In","values":["github-mirror"]}]},"topologyKey":"failure-domain.beta.kubernetes.io/zone"},"weight":100}]}},"containers":[{"env":[{"name":"GITHUB_USERS","value":"app-sre-bot:cs-sre-bot"},{"name":"GITHUB_MIRROR_URL","value":"https://github-mirror.devshift.net"},{"name":"CACHE_TYPE","value":"redis"},{"name":"PRIMARY_ENDPOINT","valueFrom":{"secretKeyRef":{"key":"db.endpoint","name":"ghmirror-elasticache-production"}}},{"name":"READER_ENDPOINT","value":"replica.ghmirror-redis-production.huo5rn.use1.cache.amazonaws.com"},{"name":"REDIS_PORT","valueFrom":{"secretKeyRef":{"key":"db.port","name":"ghmirror-elasticache-production"}}},{"name":"REDIS_TOKEN","valueFrom":{"secretKeyRef":{"key":"db.auth_token","name":"ghmirror-elasticache-production"}}},{"name":"REDIS_SSL","value":"True"}],"image":"quay.io/app-sre/github-mirror:5344cbb","imagePullPolicy":"Always","livenessProbe":{"httpGet":{"path":"/healthz","port":8080},"initialDelaySeconds":30,"periodSeconds":10,"timeoutSeconds":3},"name":"github-mirror","ports":[{"containerPort":8080,"name":"github-mirror"}],"readinessProbe":{"httpGet":{"path":"/healthz","port":8080},"initialDelaySeconds":3,"periodSeconds":10,"timeoutSeconds":3},"resources":{"limits":{"cpu":"1000m","memory":"1Gi"},"requests":{"cpu":"500m","memory":"800Mi"}}}]}}}} 8 | qontract.caller_name: saas-github-mirror 9 | qontract.integration: openshift-saas-deploy 10 | qontract.integration_version: 0.1.0 11 | qontract.sha256sum: 4af0efb326680a464e1f2c8f2780e36f749493e8f91cba1a7343e2508c30c08f 12 | qontract.update: 2020-09-02T12:36:35 13 | creationTimestamp: "2020-02-10T15:01:35Z" 14 | generation: 8482 15 | labels: 16 | app: github-mirror 17 | name: github-mirror 18 | namespace: github-mirror-production 19 | resourceVersion: "267502440" 20 | selfLink: /apis/apps/v1/namespaces/github-mirror-production/deployments/github-mirror 21 | uid: 3b4d6091-4c16-11ea-bf75-023e213e25c3 22 | spec: 23 | progressDeadlineSeconds: 2147483647 24 | replicas: {{ .Replicas }} 25 | revisionHistoryLimit: 2147483647 26 | selector: 27 | matchLabels: 28 | app: github-mirror 29 | strategy: 30 | rollingUpdate: 31 | maxSurge: 1 32 | maxUnavailable: 0 33 | type: RollingUpdate 34 | template: 35 | metadata: 36 | creationTimestamp: null 37 | labels: 38 | app: github-mirror 39 | spec: 40 | affinity: 41 | podAntiAffinity: 42 | preferredDuringSchedulingIgnoredDuringExecution: 43 | - podAffinityTerm: 44 | labelSelector: 45 | matchExpressions: 46 | - key: app 47 | operator: In 48 | values: 49 | - github-mirror 50 | topologyKey: kubernetes.io/hostname 51 | weight: 100 52 | - podAffinityTerm: 53 | labelSelector: 54 | matchExpressions: 55 | - key: app 56 | operator: In 57 | values: 58 | - github-mirror 59 | topologyKey: failure-domain.beta.kubernetes.io/zone 60 | weight: 100 61 | containers: 62 | - env: 63 | - name: GITHUB_USERS 64 | value: app-sre-bot:cs-sre-bot 65 | - name: GITHUB_MIRROR_URL 66 | value: https://github-mirror.devshift.net 67 | - name: CACHE_TYPE 68 | value: redis 69 | - name: PRIMARY_ENDPOINT 70 | valueFrom: 71 | secretKeyRef: 72 | key: db.endpoint 73 | name: ghmirror-elasticache-production 74 | - name: READER_ENDPOINT 75 | value: replica.ghmirror-redis-production.huo5rn.use1.cache.amazonaws.com 76 | - name: REDIS_PORT 77 | valueFrom: 78 | secretKeyRef: 79 | key: db.port 80 | name: ghmirror-elasticache-production 81 | - name: REDIS_TOKEN 82 | valueFrom: 83 | secretKeyRef: 84 | key: db.auth_token 85 | name: ghmirror-elasticache-production 86 | - name: REDIS_SSL 87 | value: "True" 88 | image: quay.io/app-sre/github-mirror:5344cbb 89 | imagePullPolicy: Always 90 | livenessProbe: 91 | failureThreshold: 3 92 | httpGet: 93 | path: /healthz 94 | port: 8080 95 | scheme: HTTP 96 | initialDelaySeconds: 30 97 | periodSeconds: 10 98 | successThreshold: 1 99 | timeoutSeconds: 3 100 | name: github-mirror 101 | ports: 102 | - containerPort: 8080 103 | name: github-mirror 104 | protocol: TCP 105 | readinessProbe: 106 | failureThreshold: 3 107 | httpGet: 108 | path: /healthz 109 | port: 8080 110 | scheme: HTTP 111 | initialDelaySeconds: 3 112 | periodSeconds: 10 113 | successThreshold: 1 114 | timeoutSeconds: 3 115 | resources: 116 | {{- if .ResourceLimits }} 117 | limits: 118 | cpu: "1" 119 | memory: 1Gi 120 | {{- end }} 121 | {{- if .ResourceRequests }} 122 | requests: 123 | cpu: 500m 124 | memory: 800Mi 125 | {{- end }} 126 | terminationMessagePath: /dev/termination-log 127 | terminationMessagePolicy: File 128 | dnsPolicy: ClusterFirst 129 | restartPolicy: Always 130 | schedulerName: default-scheduler 131 | securityContext: {} 132 | terminationGracePeriodSeconds: 30 133 | status: 134 | availableReplicas: {{ .Replicas }} 135 | conditions: 136 | - lastTransitionTime: "2020-09-08T07:59:45Z" 137 | lastUpdateTime: "2020-09-08T07:59:45Z" 138 | message: Deployment has minimum availability. 139 | reason: MinimumReplicasAvailable 140 | status: "True" 141 | type: Available 142 | observedGeneration: 8482 143 | readyReplicas: {{ .Replicas }} 144 | replicas: {{ .Replicas }} 145 | updatedReplicas: {{ .Replicas }} 146 | -------------------------------------------------------------------------------- /pkg/utils/selector_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 8 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 9 | ) 10 | 11 | func TestGetLabelSelector(t *testing.T) { 12 | tests := []struct { 13 | name string 14 | unstructuredResource *unstructured.Unstructured 15 | expectedLabelSelector *metav1.LabelSelector 16 | }{ 17 | { 18 | name: "When no selector is defined then LabelSelector is nil", 19 | unstructuredResource: &unstructured.Unstructured{ 20 | Object: map[string]interface{}{ 21 | "kind": "Pod", 22 | "apiVersion": "v1", 23 | "metadata": map[string]string{ 24 | "name": "test-pod", 25 | "namespace": "test", 26 | }, 27 | }, 28 | }, 29 | expectedLabelSelector: nil, 30 | }, 31 | { 32 | name: "When there's an empty selector then empty LabelSelector is obtained", 33 | unstructuredResource: &unstructured.Unstructured{ 34 | Object: map[string]interface{}{ 35 | "kind": "Pod", 36 | "apiVersion": "v1", 37 | "metadata": map[string]string{ 38 | "name": "test-pod", 39 | "namespace": "test", 40 | }, 41 | "spec": map[string]interface{}{ 42 | "selector": map[string]interface{}{}, 43 | }, 44 | }, 45 | }, 46 | expectedLabelSelector: &metav1.LabelSelector{}, 47 | }, 48 | { 49 | name: "When there's an empty podSelector then empty LabelSelector is obtained", 50 | unstructuredResource: &unstructured.Unstructured{ 51 | Object: map[string]interface{}{ 52 | "kind": "NetworkPolicy", 53 | "apiVersion": "networking.k8s.io/v1", 54 | "metadata": map[string]string{ 55 | "name": "test-pod", 56 | "namespace": "test", 57 | }, 58 | "spec": map[string]interface{}{ 59 | "podSelector": map[string]interface{}{}, 60 | }, 61 | }, 62 | }, 63 | expectedLabelSelector: &metav1.LabelSelector{}, 64 | }, 65 | { 66 | name: "Non empty podSelector with matchExpressions requirements", 67 | unstructuredResource: &unstructured.Unstructured{ 68 | Object: map[string]interface{}{ 69 | "kind": "NetworkPolicy", 70 | "apiVersion": "networking.k8s.io/v1", 71 | "metadata": map[string]string{ 72 | "name": "test-pod", 73 | "namespace": "test", 74 | }, 75 | "spec": map[string]interface{}{ 76 | "podSelector": map[string]interface{}{ 77 | "matchExpressions": []interface{}{ 78 | map[string]interface{}{ 79 | "key": "app", 80 | "operator": "In", 81 | "values": []interface{}{ 82 | "A", 83 | "B", 84 | }, 85 | }, 86 | map[string]interface{}{ 87 | "key": "environment", 88 | "operator": "NotIn", 89 | "values": []interface{}{ 90 | "testing", 91 | "staging", 92 | }, 93 | }, 94 | }, 95 | }, 96 | }, 97 | }, 98 | }, 99 | expectedLabelSelector: &metav1.LabelSelector{ 100 | MatchExpressions: []metav1.LabelSelectorRequirement{ 101 | { 102 | Key: "app", 103 | Operator: metav1.LabelSelectorOpIn, 104 | Values: []string{"A", "B"}, 105 | }, 106 | { 107 | Key: "environment", 108 | Operator: metav1.LabelSelectorOpNotIn, 109 | Values: []string{"testing", "staging"}, 110 | }, 111 | }, 112 | }, 113 | }, 114 | { 115 | name: "Non empty selector with matchLabels requirements", 116 | unstructuredResource: &unstructured.Unstructured{ 117 | Object: map[string]interface{}{ 118 | "kind": "Pod", 119 | "apiVersion": "v1", 120 | "metadata": map[string]string{ 121 | "name": "test-pod", 122 | "namespace": "test", 123 | }, 124 | "spec": map[string]interface{}{ 125 | "selector": map[string]interface{}{ 126 | "matchLabels": map[string]interface{}{ 127 | "app": "A", 128 | "environment": "production", 129 | }, 130 | }, 131 | }, 132 | }, 133 | }, 134 | expectedLabelSelector: &metav1.LabelSelector{ 135 | MatchLabels: map[string]string{ 136 | "app": "A", 137 | "environment": "production", 138 | }, 139 | }, 140 | }, 141 | { 142 | name: "Selector with multiple requirements", 143 | unstructuredResource: &unstructured.Unstructured{ 144 | Object: map[string]interface{}{ 145 | "kind": "Pod", 146 | "apiVersion": "v1", 147 | "metadata": map[string]string{ 148 | "name": "test-pod", 149 | "namespace": "test", 150 | }, 151 | "spec": map[string]interface{}{ 152 | "selector": map[string]interface{}{ 153 | "matchLabels": map[string]interface{}{ 154 | "app": "A", 155 | "environment": "production", 156 | }, 157 | "matchExpressions": []interface{}{ 158 | map[string]interface{}{ 159 | "key": "app", 160 | "operator": "Exists", 161 | }, 162 | map[string]interface{}{ 163 | "key": "environment", 164 | "operator": "DoesNotExist", 165 | }, 166 | map[string]interface{}{ 167 | "key": "test-key", 168 | "operator": "In", 169 | "values": []interface{}{ 170 | "FOO", 171 | "BAR", 172 | }, 173 | }, 174 | }, 175 | }, 176 | }, 177 | }, 178 | }, 179 | expectedLabelSelector: &metav1.LabelSelector{ 180 | MatchLabels: map[string]string{ 181 | "app": "A", 182 | "environment": "production", 183 | }, 184 | MatchExpressions: []metav1.LabelSelectorRequirement{ 185 | { 186 | Key: "app", 187 | Operator: metav1.LabelSelectorOpExists, 188 | }, 189 | { 190 | Key: "environment", 191 | Operator: metav1.LabelSelectorOpDoesNotExist, 192 | }, 193 | { 194 | Key: "test-key", 195 | Operator: metav1.LabelSelectorOpIn, 196 | Values: []string{ 197 | "FOO", 198 | "BAR", 199 | }, 200 | }, 201 | }, 202 | }, 203 | }, 204 | { 205 | name: "selector defined as map[string]string with no metav1.LabelSelector", 206 | unstructuredResource: &unstructured.Unstructured{ 207 | Object: map[string]interface{}{ 208 | "kind": "Service", 209 | "apiVersion": "v1", 210 | "metadata": map[string]string{ 211 | "name": "test-service", 212 | "namespace": "test", 213 | }, 214 | "spec": map[string]interface{}{ 215 | "selector": map[string]interface{}{ 216 | "app": "A", 217 | "environment": "production", 218 | }, 219 | }, 220 | }, 221 | }, 222 | expectedLabelSelector: &metav1.LabelSelector{ 223 | MatchLabels: map[string]string{ 224 | "app": "A", 225 | "environment": "production", 226 | }, 227 | }, 228 | }, 229 | } 230 | 231 | for _, tt := range tests { 232 | t.Run(tt.name, func(t *testing.T) { 233 | ls := GetLabelSelector(tt.unstructuredResource) 234 | assert.Equal(t, tt.expectedLabelSelector, ls) 235 | }) 236 | 237 | } 238 | 239 | } 240 | -------------------------------------------------------------------------------- /hack/olm-registry/olm-artifacts-template.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: template.openshift.io/v1 2 | kind: Template 3 | metadata: 4 | name: dvo-selectorsyncset-template 5 | parameters: 6 | - name: REGISTRY_IMG 7 | required: true 8 | - name: IMAGE_TAG 9 | required: true 10 | - name: IMAGE_DIGEST 11 | required: true 12 | - name: REPO_NAME 13 | value: deployment-validation-operator 14 | required: true 15 | - name: DISPLAY_NAME 16 | value: Deployment Validation Operator 17 | required: true 18 | - name: CHANNEL 19 | value: "alpha" 20 | displayName: OLM subscription channel 21 | description: OLM subscription channel 22 | required: true 23 | - name: TARGET_ORG 24 | displayName: Target organization 25 | description: Target api.openshift.com organization to deploy to 26 | required: true 27 | objects: 28 | - apiVersion: hive.openshift.io/v1 29 | kind: SelectorSyncSet 30 | metadata: 31 | annotations: 32 | component-display-name: ${DISPLAY_NAME} 33 | component-name: ${REPO_NAME} 34 | labels: 35 | managed.openshift.io/gitHash: ${IMAGE_TAG} 36 | managed.openshift.io/gitRepoName: ${REPO_NAME} 37 | managed.openshift.io/osd: "true" 38 | name: deployment-validation-operator 39 | spec: 40 | clusterDeploymentSelector: 41 | matchLabels: 42 | api.openshift.com/managed: "true" 43 | matchExpressions: 44 | - key: hive.openshift.io/version-major-minor 45 | operator: NotIn 46 | values: ["4.1", "4.2", "4.3", "4.4", "4.5", "4.6", "4.7", "4.8", "4.9", "4.10"] 47 | - key: hive.openshift.io/version-major-minor-patch 48 | operator: NotIn 49 | values: ["4.11.0", "4.11.1", "4.11.2", "4.11.3", "4.11.4", "4.11.5", "4.11.6", "4.11.7", "4.11.8", "4.11.9"] 50 | resourceApplyMode: Sync 51 | resources: 52 | - apiVersion: v1 53 | kind: Namespace 54 | metadata: 55 | name: openshift-deployment-validation-operator 56 | labels: 57 | openshift.io/cluster-monitoring: "true" 58 | - apiVersion: operators.coreos.com/v1alpha1 59 | kind: CatalogSource 60 | metadata: 61 | name: deployment-validation-operator-catalog 62 | namespace: openshift-deployment-validation-operator 63 | spec: 64 | sourceType: grpc 65 | grpcPodConfig: 66 | securityContextConfig: restricted 67 | nodeSelector: 68 | node-role.kubernetes.io: infra 69 | tolerations: 70 | - effect: NoSchedule 71 | key: node-role.kubernetes.io/infra 72 | operator: Exists 73 | image: ${REGISTRY_IMG}@${IMAGE_DIGEST} 74 | displayName: Deployment Validation Operator 75 | publisher: Red Hat 76 | - apiVersion: operators.coreos.com/v1 77 | kind: OperatorGroup 78 | metadata: 79 | name: deployment-validation-operator-og 80 | namespace: openshift-deployment-validation-operator 81 | annotations: 82 | olm.operatorframework.io/exclude-global-namespace-resolution: "true" 83 | spec: 84 | upgradeStrategy: TechPreviewUnsafeFailForward 85 | targetNamespaces: 86 | - openshift-deployment-validation-operator 87 | - apiVersion: operators.coreos.com/v1alpha1 88 | kind: Subscription 89 | metadata: 90 | name: deployment-validation-operator 91 | namespace: openshift-deployment-validation-operator 92 | spec: 93 | channel: ${CHANNEL} 94 | config: 95 | env: 96 | - name: "NAMESPACE_IGNORE_PATTERN" 97 | value: "^(openshift.*|kube-.*|open-cluster-management-.*|default|dedicated-admin|redhat-.*|acm|addon-dba-operator|codeready-.*|prow)$" 98 | name: deployment-validation-operator 99 | source: deployment-validation-operator-catalog 100 | sourceNamespace: openshift-deployment-validation-operator 101 | - apiVersion: networking.k8s.io/v1 102 | kind: NetworkPolicy 103 | metadata: 104 | name: allow-from-openshift-insights 105 | namespace: openshift-deployment-validation-operator 106 | spec: 107 | podSelector: {} 108 | ingress: 109 | - from: 110 | - namespaceSelector: 111 | matchLabels: 112 | name: openshift-insights 113 | policyTypes: 114 | - Ingress 115 | - apiVersion: networking.k8s.io/v1 116 | kind: NetworkPolicy 117 | metadata: 118 | name: allow-from-openshift-olm 119 | namespace: openshift-deployment-validation-operator 120 | spec: 121 | podSelector: {} 122 | ingress: 123 | - from: 124 | - namespaceSelector: 125 | matchLabels: 126 | kubernetes.io/metadata.name: openshift-operator-lifecycle-manager 127 | policyTypes: 128 | - Ingress 129 | - apiVersion: v1 130 | kind: Service 131 | metadata: 132 | name: deployment-validation-operator-metrics 133 | namespace: openshift-deployment-validation-operator 134 | labels: 135 | name: deployment-validation-operator 136 | spec: 137 | ports: 138 | - name: http-metrics 139 | port: 8383 140 | protocol: TCP 141 | targetPort: 8383 142 | selector: 143 | name: deployment-validation-operator 144 | - apiVersion: v1 145 | kind: ConfigMap 146 | metadata: 147 | name: deployment-validation-operator-config 148 | namespace: openshift-deployment-validation-operator 149 | labels: 150 | name: deployment-validation-operator 151 | data: 152 | deployment-validation-operator-config.yaml: |- 153 | checks: 154 | doNotAutoAddDefaults: false 155 | addAllBuiltIn: true 156 | 157 | exclude: 158 | - "access-to-create-pods" 159 | - "access-to-secrets" 160 | - "cluster-admin-role-binding" 161 | - "default-service-account" 162 | - "deprecated-service-account-field" 163 | - "docker-sock" 164 | - "drop-net-raw-capability" 165 | - "env-var-secret" 166 | - "exposed-services" 167 | - "latest-tag" 168 | - "mismatching-selector" 169 | - "no-extensions-v1beta" 170 | - "no-liveness-probe" 171 | - "no-read-only-root-fs" 172 | - "no-readiness-probe" 173 | - "no-rolling-update-strategy" 174 | - "privileged-ports" 175 | - "read-secret-from-env-var" 176 | - "required-annotation-email" 177 | - "required-label-owner" 178 | - "sensitive-host-mounts" 179 | - "ssh-port" 180 | - "unsafe-proc-mount" 181 | - "use-namespace" 182 | - "wildcard-in-rules" 183 | - "writable-host-mount" 184 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/app-sre/deployment-validation-operator 2 | 3 | go 1.24.0 4 | 5 | toolchain go1.24.8 6 | 7 | require ( 8 | github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 9 | github.com/go-logr/logr v1.4.2 10 | github.com/mcuadros/go-defaults v1.2.0 11 | github.com/openshift/api v0.0.0-20230406152840-ce21e3fe5da2 12 | github.com/prometheus/client_golang v1.22.0 13 | github.com/prometheus/client_model v0.6.1 14 | github.com/spf13/pflag v1.0.7 15 | github.com/spf13/viper v1.18.2 16 | github.com/stretchr/testify v1.10.0 17 | go.uber.org/zap v1.27.0 18 | golang.stackrox.io/kube-linter v0.6.8 19 | k8s.io/api v0.33.3 20 | k8s.io/apimachinery v0.33.3 21 | k8s.io/client-go v0.33.3 22 | sigs.k8s.io/controller-runtime v0.19.7 23 | ) 24 | 25 | require ( 26 | dario.cat/mergo v1.0.1 // indirect 27 | github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect 28 | github.com/BurntSushi/toml v1.5.0 // indirect 29 | github.com/MakeNowJust/heredoc v1.0.0 // indirect 30 | github.com/Masterminds/goutils v1.1.1 // indirect 31 | github.com/Masterminds/semver/v3 v3.3.0 // indirect 32 | github.com/Masterminds/sprig/v3 v3.3.0 // indirect 33 | github.com/beorn7/perks v1.0.1 // indirect 34 | github.com/blang/semver/v4 v4.0.0 // indirect 35 | github.com/bmatcuk/doublestar/v4 v4.6.1 // indirect 36 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 37 | github.com/chai2010/gettext-go v1.0.2 // indirect 38 | github.com/containerd/containerd v1.7.29 // indirect 39 | github.com/containerd/errdefs v0.3.0 // indirect 40 | github.com/containerd/log v0.1.0 // indirect 41 | github.com/containerd/platforms v0.2.1 // indirect 42 | github.com/cyphar/filepath-securejoin v0.4.1 // indirect 43 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 44 | github.com/emicklei/go-restful/v3 v3.11.2 // indirect 45 | github.com/evanphx/json-patch v5.9.11+incompatible // indirect 46 | github.com/evanphx/json-patch/v5 v5.9.0 // indirect 47 | github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect 48 | github.com/expr-lang/expr v1.17.7 // indirect 49 | github.com/fsnotify/fsnotify v1.7.0 // indirect 50 | github.com/fxamacker/cbor/v2 v2.7.0 // indirect 51 | github.com/go-errors/errors v1.5.1 // indirect 52 | github.com/go-logr/zapr v1.3.0 // indirect 53 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 54 | github.com/go-openapi/jsonreference v0.20.4 // indirect 55 | github.com/go-openapi/swag v0.23.0 // indirect 56 | github.com/gobwas/glob v0.2.3 // indirect 57 | github.com/gogo/protobuf v1.3.2 // indirect 58 | github.com/google/btree v1.1.3 // indirect 59 | github.com/google/gnostic-models v0.6.9 // indirect 60 | github.com/google/go-cmp v0.7.0 // indirect 61 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect 62 | github.com/google/uuid v1.6.0 // indirect 63 | github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect 64 | github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect 65 | github.com/hashicorp/errwrap v1.1.0 // indirect 66 | github.com/hashicorp/go-multierror v1.1.1 // indirect 67 | github.com/hashicorp/hcl v1.0.1-vault-5 // indirect 68 | github.com/huandu/xstrings v1.5.0 // indirect 69 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 70 | github.com/josharian/intern v1.0.0 // indirect 71 | github.com/json-iterator/go v1.1.12 // indirect 72 | github.com/kedacore/keda/v2 v2.13.0 // indirect 73 | github.com/klauspost/compress v1.18.0 // indirect 74 | github.com/kylelemons/godebug v1.1.0 // indirect 75 | github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect 76 | github.com/magiconair/properties v1.8.7 // indirect 77 | github.com/mailru/easyjson v0.7.7 // indirect 78 | github.com/mitchellh/copystructure v1.2.0 // indirect 79 | github.com/mitchellh/go-homedir v1.1.0 // indirect 80 | github.com/mitchellh/go-wordwrap v1.0.1 // indirect 81 | github.com/mitchellh/mapstructure v1.5.0 // indirect 82 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 83 | github.com/moby/spdystream v0.5.0 // indirect 84 | github.com/moby/term v0.5.2 // indirect 85 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 86 | github.com/modern-go/reflect2 v1.0.2 // indirect 87 | github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect 88 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 89 | github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect 90 | github.com/opencontainers/go-digest v1.0.0 // indirect 91 | github.com/opencontainers/image-spec v1.1.1 // indirect 92 | github.com/pelletier/go-toml/v2 v2.1.0 // indirect 93 | github.com/peterbourgon/diskv v2.0.1+incompatible // indirect 94 | github.com/pkg/errors v0.9.1 // indirect 95 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 96 | github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.71.2 // indirect 97 | github.com/prometheus/common v0.62.0 // indirect 98 | github.com/prometheus/procfs v0.15.1 // indirect 99 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 100 | github.com/sagikazarmark/locafero v0.4.0 // indirect 101 | github.com/sagikazarmark/slog-shim v0.1.0 // indirect 102 | github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect 103 | github.com/shopspring/decimal v1.4.0 // indirect 104 | github.com/sirupsen/logrus v1.9.3 // indirect 105 | github.com/sourcegraph/conc v0.3.0 // indirect 106 | github.com/spf13/afero v1.11.0 // indirect 107 | github.com/spf13/cast v1.7.0 // indirect 108 | github.com/spf13/cobra v1.9.1 // indirect 109 | github.com/subosito/gotenv v1.6.0 // indirect 110 | github.com/x448/float16 v0.8.4 // indirect 111 | github.com/xlab/treeprint v1.2.0 // indirect 112 | go.uber.org/multierr v1.11.0 // indirect 113 | go.yaml.in/yaml/v2 v2.4.2 // indirect 114 | go.yaml.in/yaml/v3 v3.0.3 // indirect 115 | golang.org/x/crypto v0.45.0 // indirect 116 | golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect 117 | golang.org/x/net v0.47.0 // indirect 118 | golang.org/x/oauth2 v0.30.0 // indirect 119 | golang.org/x/sync v0.18.0 // indirect 120 | golang.org/x/sys v0.38.0 // indirect 121 | golang.org/x/term v0.37.0 // indirect 122 | golang.org/x/text v0.31.0 // indirect 123 | golang.org/x/time v0.12.0 // indirect 124 | gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect 125 | google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 // indirect 126 | google.golang.org/grpc v1.68.1 // indirect 127 | google.golang.org/protobuf v1.36.5 // indirect 128 | gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect 129 | gopkg.in/inf.v0 v0.9.1 // indirect 130 | gopkg.in/ini.v1 v1.67.0 // indirect 131 | gopkg.in/yaml.v2 v2.4.0 // indirect 132 | gopkg.in/yaml.v3 v3.0.1 // indirect 133 | helm.sh/helm/v3 v3.18.5 // indirect 134 | k8s.io/apiextensions-apiserver v0.33.3 // indirect 135 | k8s.io/cli-runtime v0.33.3 // indirect 136 | k8s.io/component-base v0.33.3 // indirect 137 | k8s.io/gengo v0.0.0-20230829151522-9cce18d56c01 // indirect 138 | k8s.io/klog/v2 v2.130.1 // indirect 139 | k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect 140 | k8s.io/kubectl v0.33.3 // indirect 141 | k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect 142 | knative.dev/pkg v0.0.0-20240116073220-b488e7be5902 // indirect 143 | oras.land/oras-go/v2 v2.6.0 // indirect 144 | sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect 145 | sigs.k8s.io/kustomize/api v0.19.0 // indirect 146 | sigs.k8s.io/kustomize/kyaml v0.19.0 // indirect 147 | sigs.k8s.io/randfill v1.0.0 // indirect 148 | sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect 149 | sigs.k8s.io/yaml v1.5.0 // indirect 150 | ) 151 | -------------------------------------------------------------------------------- /konflux-ci/bundle/manifests/deployment-validation-operator.clusterserviceversion.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: operators.coreos.com/v1alpha1 2 | kind: ClusterServiceVersion 3 | metadata: 4 | annotations: 5 | alm-examples: '[]' 6 | capabilities: Basic Install 7 | categories: Application Runtime, Monitoring, Security 8 | certified: "false" 9 | containerImage: registry.redhat.io/dvo/deployment-validation-rhel8-operator@sha256:74728420bb74a5a9a8040b48413da48f506e42b333df53c2067f53f614a2342d 10 | createdAt: 2024-11-27T00:00:00Z 11 | description: The deployment validation operator 12 | operators.openshift.io/valid-subscription: "[\"OpenShift Container Platform\", \"OpenShift Platform Plus\"]" 13 | operators.operatorframework.io/builder: operator-sdk-v1.31.0+git 14 | operators.operatorframework.io/project_layout: unknown 15 | repository: https://github.com/app-sre/deployment-validation-operator 16 | support: Best Effort 17 | ignore-check.kube-linter.io/minimum-three-replicas: "This deployment uses 1 pod as currently replicating does not replicate metric data causing installation issues" 18 | features.operators.openshift.io/cnf: "false" 19 | features.operators.openshift.io/cni: "false" 20 | features.operators.openshift.io/csi: "false" 21 | features.operators.openshift.io/disconnected: "true" 22 | features.operators.openshift.io/fips-compliant: "true" 23 | features.operators.openshift.io/proxy-aware: "true" 24 | features.operators.openshift.io/tls-profiles: "false" 25 | features.operators.openshift.io/token-auth-aws: "false" 26 | features.operators.openshift.io/token-auth-azure: "false" 27 | features.operators.openshift.io/token-auth-gcp: "false" 28 | name: deployment-validation-operator.v0.7.13 29 | spec: 30 | description: | 31 | The Deployment Validation Operator (DVO) checks deployments and other resources against a curated collection of best practices. 32 | These best practices focus mainly on ensuring that the applications are fault-tolerant. DVO reports failed validations via Prometheus metrics. 33 | If the best-practice check has failed, the metrics will report `1`. 34 | displayName: Deployment Validation Operator 35 | icon: 36 | - base64data: "PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTguNTEgMjU4LjUxIj48ZGVmcz48c3R5bGU+LmNscy0xe2ZpbGw6I2QxZDFkMTt9LmNscy0ye2ZpbGw6IzhkOGQ4Zjt9PC9zdHlsZT48L2RlZnM+PHRpdGxlPkFzc2V0IDQ8L3RpdGxlPjxnIGlkPSJMYXllcl8yIiBkYXRhLW5hbWU9IkxheWVyIDIiPjxnIGlkPSJMYXllcl8xLTIiIGRhdGEtbmFtZT0iTGF5ZXIgMSI+PHBhdGggY2xhc3M9ImNscy0xIiBkPSJNMTI5LjI1LDIwQTEwOS4xLDEwOS4xLDAsMCwxLDIwNi40LDIwNi40LDEwOS4xLDEwOS4xLDAsMSwxLDUyLjExLDUyLjExLDEwOC40NSwxMDguNDUsMCwwLDEsMTI5LjI1LDIwbTAtMjBoMEM1OC4xNiwwLDAsNTguMTYsMCwxMjkuMjVIMGMwLDcxLjA5LDU4LjE2LDEyOS4yNiwxMjkuMjUsMTI5LjI2aDBjNzEuMDksMCwxMjkuMjYtNTguMTcsMTI5LjI2LTEyOS4yNmgwQzI1OC41MSw1OC4xNiwyMDAuMzQsMCwxMjkuMjUsMFoiLz48cGF0aCBjbGFzcz0iY2xzLTIiIGQ9Ik0xNzcuNTQsMTAzLjQxSDE0MS42NkwxNTQuOSw2NS43NmMxLjI1LTQuNC0yLjMzLTguNzYtNy4yMS04Ljc2SDEwMi45M2E3LjMyLDcuMzIsMCwwLDAtNy40LDZsLTEwLDY5LjYxYy0uNTksNC4xNywyLjg5LDcuODksNy40LDcuODloMzYuOUwxMTUuNTUsMTk3Yy0xLjEyLDQuNDEsMi40OCw4LjU1LDcuMjQsOC41NWE3LjU4LDcuNTgsMCwwLDAsNi40Ny0zLjQ4TDE4NCwxMTMuODVDMTg2Ljg2LDEwOS4yNCwxODMuMjksMTAzLjQxLDE3Ny41NCwxMDMuNDFaIi8+PC9nPjwvZz48L3N2Zz4=" 37 | mediatype: "image/svg+xml" 38 | install: 39 | spec: 40 | clusterPermissions: 41 | - rules: 42 | - apiGroups: 43 | - '*' 44 | resources: 45 | - '*' 46 | verbs: 47 | - get 48 | - list 49 | - watch 50 | serviceAccountName: deployment-validation-operator 51 | deployments: 52 | - label: 53 | app: deployment-validation-operator 54 | name: deployment-validation-operator 55 | spec: 56 | replicas: 1 57 | selector: 58 | matchLabels: 59 | app: deployment-validation-operator 60 | strategy: 61 | rollingUpdate: 62 | maxSurge: 1 63 | maxUnavailable: 0 64 | type: RollingUpdate 65 | template: 66 | metadata: 67 | labels: 68 | app: deployment-validation-operator 69 | name: deployment-validation-operator 70 | spec: 71 | affinity: 72 | podAntiAffinity: 73 | preferredDuringSchedulingIgnoredDuringExecution: 74 | - podAffinityTerm: 75 | labelSelector: 76 | matchExpressions: 77 | - key: app 78 | operator: In 79 | values: 80 | - deployment-validation-operator 81 | topologyKey: kubernetes.io/hostname 82 | weight: 100 83 | containers: 84 | - args: 85 | - --config /config/deployment-validation-operator-config.yaml 86 | env: 87 | - name: WATCH_NAMESPACE 88 | valueFrom: 89 | fieldRef: 90 | fieldPath: metadata.annotations['olm.targetNamespaces'] 91 | - name: OPERATOR_NAME 92 | value: deployment-validation-operator 93 | - name: NAMESPACE_IGNORE_PATTERN 94 | value: "^(openshift.*|kube-.*|open-cluster-management-.*|default|dedicated-admin|redhat-.*|acm|addon-dba-operator|codeready-.*|prow)$" 95 | - name: RESOURCES_PER_LIST_QUERY 96 | value: "5" 97 | - name: VALIDATION_CHECK_INTERVAL 98 | value: "2m" 99 | - name: POD_NAME 100 | valueFrom: 101 | fieldRef: 102 | fieldPath: metadata.name 103 | - name: POD_NAMESPACE 104 | valueFrom: 105 | fieldRef: 106 | fieldPath: metadata.namespace 107 | image: registry.redhat.io/dvo/deployment-validation-rhel8-operator@sha256:74728420bb74a5a9a8040b48413da48f506e42b333df53c2067f53f614a2342d 108 | imagePullPolicy: Always 109 | name: deployment-validation-operator 110 | ports: 111 | - containerPort: 8383 112 | name: http-metrics 113 | protocol: TCP 114 | resources: 115 | limits: 116 | memory: 400Mi 117 | requests: 118 | cpu: 50m 119 | memory: 200Mi 120 | securityContext: 121 | readOnlyRootFilesystem: true 122 | volumeMounts: 123 | - name: dvo-config 124 | mountPath: /config 125 | restartPolicy: Always 126 | serviceAccountName: deployment-validation-operator 127 | terminationGracePeriodSeconds: 30 128 | volumes: 129 | - name: dvo-config 130 | configMap: 131 | name: deployment-validation-operator-config 132 | optional: true 133 | permissions: 134 | - rules: 135 | - apiGroups: 136 | - "" 137 | resources: 138 | - configmaps 139 | - services 140 | verbs: 141 | - get 142 | - create 143 | - list 144 | - delete 145 | - update 146 | - watch 147 | - patch 148 | - apiGroups: 149 | - monitoring.coreos.com 150 | resources: 151 | - servicemonitors 152 | verbs: 153 | - '*' 154 | serviceAccountName: deployment-validation-operator 155 | strategy: deployment 156 | installModes: 157 | - supported: true 158 | type: OwnNamespace 159 | - supported: true 160 | type: SingleNamespace 161 | - supported: true 162 | type: AllNamespaces 163 | - supported: false 164 | type: MultiNamespace 165 | keywords: 166 | - dvo 167 | labels: 168 | alm-owner-dvo: deployment-validation-operator 169 | operated-by: deployment-validation-operator 170 | operatorframework.io/arch.amd64: supported 171 | operatorframework.io/arch.arm64: supported 172 | links: 173 | - name: repository 174 | url: https://github.com/app-sre/deployment-validation-operator 175 | - name: containerImage 176 | url: registry.redhat.io/dvo/deployment-validation-rhel8-operator@sha256:74728420bb74a5a9a8040b48413da48f506e42b333df53c2067f53f614a2342d 177 | maintainers: 178 | - name: Red Hat 179 | email: dvo-owners@redhat.com 180 | maturity: alpha 181 | provider: 182 | name: Red Hat 183 | selector: 184 | matchLabels: 185 | alm-owner-dvo: deployment-validation-operator 186 | operated-by: deployment-validation-operator 187 | replaces: deployment-validation-operator.v0.7.12 188 | skipRange: ">=0.0.10 <0.7.5" 189 | version: 0.7.13 190 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "runtime" 8 | "strconv" 9 | "strings" 10 | 11 | // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) 12 | _ "k8s.io/client-go/plugin/pkg/client/auth" 13 | "k8s.io/client-go/rest" 14 | 15 | apis "github.com/app-sre/deployment-validation-operator/api" 16 | dvconfig "github.com/app-sre/deployment-validation-operator/config" 17 | "github.com/app-sre/deployment-validation-operator/internal/options" 18 | "github.com/app-sre/deployment-validation-operator/pkg/configmap" 19 | "github.com/app-sre/deployment-validation-operator/pkg/controller" 20 | dvoProm "github.com/app-sre/deployment-validation-operator/pkg/prometheus" 21 | "github.com/app-sre/deployment-validation-operator/pkg/validations" 22 | "github.com/app-sre/deployment-validation-operator/version" 23 | "github.com/prometheus/client_golang/prometheus" 24 | 25 | "github.com/go-logr/logr" 26 | osappsv1 "github.com/openshift/api/apps/v1" 27 | k8sruntime "k8s.io/apimachinery/pkg/runtime" 28 | "k8s.io/client-go/discovery" 29 | clientgoscheme "k8s.io/client-go/kubernetes/scheme" 30 | "sigs.k8s.io/controller-runtime/pkg/cache" 31 | "sigs.k8s.io/controller-runtime/pkg/client" 32 | "sigs.k8s.io/controller-runtime/pkg/client/config" 33 | "sigs.k8s.io/controller-runtime/pkg/healthz" 34 | logf "sigs.k8s.io/controller-runtime/pkg/log" 35 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 36 | "sigs.k8s.io/controller-runtime/pkg/manager" 37 | "sigs.k8s.io/controller-runtime/pkg/manager/signals" 38 | ) 39 | 40 | const operatorNameEnvVar = "OPERATOR_NAME" 41 | 42 | func main() { 43 | // Make sure the operator name is what we want 44 | os.Setenv(operatorNameEnvVar, dvconfig.OperatorName) 45 | 46 | opts := options.Options{ 47 | MetricsPort: 8383, 48 | MetricsPath: "metrics", 49 | ProbeAddr: ":8081", 50 | ConfigFile: "config/deployment-validation-operator-config.yaml", 51 | } 52 | 53 | opts.Process() 54 | 55 | // Use a zap logr.Logger implementation. If none of the zap 56 | // flags are configured (or if the zap flag set is not being 57 | // used), this defaults to a production zap logger. 58 | // 59 | // The logger instantiated here can be changed to any logger 60 | // implementing the logr.Logger interface. This logger will 61 | // be propagated through the whole operator, generating 62 | // uniform and structured logs. 63 | logf.SetLogger(zap.New(zap.UseFlagOptions(&opts.Zap))) 64 | 65 | log := logf.Log.WithName("DeploymentValidation") 66 | logVersion(log) 67 | 68 | log.Info("Setting Up Manager") 69 | 70 | mgr, err := setupManager(log.V(1), opts) 71 | if err != nil { 72 | fail(log, err, "Unexpected error occurred while setting up manager") 73 | } 74 | 75 | log.Info("Starting Manager") 76 | 77 | if err := mgr.Start(signals.SetupSignalHandler()); err != nil { 78 | fail(log, err, "Unexpected error occurred while running manager") 79 | } 80 | } 81 | 82 | func setupManager(logger logr.Logger, opts options.Options) (manager.Manager, error) { 83 | 84 | logger.Info("Load KubeConfig") 85 | 86 | cfg, err := config.GetConfig() 87 | if err != nil { 88 | return nil, fmt.Errorf("getting config: %w", err) 89 | } 90 | 91 | logger.Info("Initialize Manager") 92 | 93 | mgr, err := initManager(logger, opts, cfg) 94 | if err != nil { 95 | return nil, fmt.Errorf("initializing manager: %w", err) 96 | } 97 | 98 | logger.Info("Registering Components") 99 | 100 | logger.Info("Initialize Prometheus Registry") 101 | 102 | reg := prometheus.NewRegistry() 103 | metrics, err := dvoProm.PreloadMetrics(reg) 104 | if err != nil { 105 | return nil, fmt.Errorf("preloading kube-linter metrics: %w", err) 106 | } 107 | 108 | logger.Info("Initialize Prometheus metrics endpoint", "endpoint", opts.MetricsEndpoint()) 109 | 110 | srv, err := dvoProm.NewServer(reg, opts.MetricsPath, fmt.Sprintf(":%d", opts.MetricsPort)) 111 | if err != nil { 112 | return nil, fmt.Errorf("initializing metrics server: %w", err) 113 | } 114 | 115 | if err := mgr.Add(srv); err != nil { 116 | return nil, fmt.Errorf("adding metrics server to manager: %w", err) 117 | } 118 | 119 | logger.Info("Initialize ConfigMap watcher") 120 | 121 | cmWatcher, err := configmap.NewWatcher(cfg) 122 | if err != nil { 123 | return nil, fmt.Errorf("initializing configmap watcher: %w", err) 124 | } 125 | 126 | if err := mgr.Add(cmWatcher); err != nil { 127 | return nil, fmt.Errorf("adding configmap watcher to manager: %w", err) 128 | } 129 | 130 | logger.Info("Initialize Validation Engine") 131 | 132 | validationEngine, err := validations.NewValidationEngine(opts.ConfigFile, metrics) 133 | if err != nil { 134 | return nil, fmt.Errorf("initializing validation engine: %w", err) 135 | } 136 | 137 | logger.Info("Initialize Reconciler") 138 | 139 | discoveryClient, err := discovery.NewDiscoveryClientForConfig(mgr.GetConfig()) 140 | if err != nil { 141 | return nil, fmt.Errorf("initializing discovery client: %w", err) 142 | } 143 | 144 | gr, err := controller.NewGenericReconciler(mgr.GetClient(), discoveryClient, cmWatcher, validationEngine) 145 | if err != nil { 146 | return nil, fmt.Errorf("initializing generic reconciler: %w", err) 147 | } 148 | 149 | if err = gr.AddToManager(mgr); err != nil { 150 | return nil, fmt.Errorf("adding generic reconciler to manager: %w", err) 151 | } 152 | 153 | return mgr, nil 154 | } 155 | 156 | func fail(logger logr.Logger, err error, msg string) { 157 | logger.Error(err, msg) 158 | 159 | os.Exit(1) 160 | } 161 | 162 | func logVersion(logger logr.Logger) { 163 | logger.Info(fmt.Sprintf("Operator Version: %s", version.Version)) 164 | logger.Info(fmt.Sprintf("Go Version: %s", runtime.Version())) 165 | logger.Info(fmt.Sprintf("Go OS/Arch: %s/%s", runtime.GOOS, runtime.GOARCH)) 166 | } 167 | 168 | func initializeScheme() (*k8sruntime.Scheme, error) { 169 | scheme := k8sruntime.NewScheme() 170 | 171 | if err := clientgoscheme.AddToScheme(scheme); err != nil { 172 | return nil, fmt.Errorf("adding client-go APIs to scheme: %w", err) 173 | } 174 | 175 | if err := osappsv1.AddToScheme(scheme); err != nil { 176 | return nil, fmt.Errorf("adding OpenShift Apps V1 API to scheme: %w", err) 177 | } 178 | 179 | if err := apis.AddToScheme(scheme); err != nil { 180 | return nil, fmt.Errorf("adding DVO APIs to scheme: %w", err) 181 | } 182 | 183 | return scheme, nil 184 | } 185 | 186 | var errWatchNamespaceNotSet = errors.New("'WatchNamespace' not set") 187 | 188 | func getManagerOptions(scheme *k8sruntime.Scheme, opts options.Options, logger logr.Logger) (manager.Options, error) { 189 | ns, ok := opts.GetWatchNamespace() 190 | if !ok { 191 | return manager.Options{}, errWatchNamespaceNotSet 192 | } 193 | 194 | mgrOpts := manager.Options{ 195 | HealthProbeBindAddress: opts.ProbeAddr, 196 | // disable caching of everything 197 | NewClient: newClient, 198 | Scheme: scheme, 199 | Logger: logger, 200 | } 201 | 202 | // disable controller-runtime managed prometheus endpoint 203 | mgrOpts.Metrics.BindAddress = "0" 204 | 205 | // Add support for MultiNamespace set in WATCH_NAMESPACE (e.g ns1,ns2) 206 | // Note that this is not intended to be used for excluding namespaces, this is better done via a Predicate 207 | // Also note that you may face performance issues when using this with a high number of namespaces. 208 | // More: https://godoc.org/github.com/kubernetes-sigs/controller-runtime/pkg/cache#MultiNamespacedCacheBuilder 209 | if ns != "" { 210 | defaultNamespaces := make(map[string]cache.Config) 211 | for _, namespace := range strings.Split(ns, ",") { 212 | defaultNamespaces[namespace] = cache.Config{} 213 | } 214 | mgrOpts.Cache.DefaultNamespaces = defaultNamespaces 215 | } 216 | 217 | return mgrOpts, nil 218 | } 219 | 220 | func newClient(cfg *rest.Config, opts client.Options) (client.Client, error) { 221 | qps, err := kubeClientQPS() 222 | if err != nil { 223 | return nil, err 224 | } 225 | 226 | cfg.QPS = qps 227 | 228 | return client.New(cfg, opts) 229 | } 230 | 231 | func kubeClientQPS() (float32, error) { 232 | qps := controller.DefaultKubeClientQPS 233 | envVal, ok := os.LookupEnv(controller.EnvKubeClientQPS) 234 | if !ok { 235 | return qps, nil 236 | } 237 | val, err := strconv.ParseFloat(envVal, 32) 238 | if err != nil { 239 | return 0.0, err 240 | } 241 | qps = float32(val) 242 | return qps, err 243 | } 244 | 245 | func initManager(logger logr.Logger, opts options.Options, cfg *rest.Config) (manager.Manager, error) { 246 | logger.Info("Initialize Scheme") 247 | scheme, err := initializeScheme() 248 | if err != nil { 249 | return nil, fmt.Errorf("initializing scheme: %w", err) 250 | } 251 | 252 | logger.Info("Getting Manager Options") 253 | mgrOpts, err := getManagerOptions(scheme, opts, logger) 254 | if err != nil { 255 | return nil, fmt.Errorf("getting manager options: %w", err) 256 | } 257 | 258 | mgr, err := manager.New(cfg, mgrOpts) 259 | if err != nil { 260 | return nil, fmt.Errorf("getting new manager: %w", err) 261 | } 262 | 263 | logger.Info("Adding Healthz and Readyz checks") 264 | if err := mgr.AddHealthzCheck("health", healthz.Ping); err != nil { 265 | return nil, fmt.Errorf("adding healthz check: %w", err) 266 | } 267 | 268 | if err := mgr.AddReadyzCheck("check", healthz.Ping); err != nil { 269 | return nil, fmt.Errorf("adding readyz check: %w", err) 270 | } 271 | 272 | return mgr, nil 273 | } 274 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deployment Validation Operator 2 | 3 | ## Description 4 | 5 | The Deployment Validation Operator (DVO) checks deployments and other resources against a curated collection of best practices. 6 | 7 | These best practices focus mainly on ensuring that the applications are fault-tolerant. 8 | 9 | DVO will only monitor Kubernetes resources and will not modify them in any way. As an operator it is a continuously running version of the static analysis tool Kube-linter [https://github.com/stackrox/kube-linter]. It will report failed validations via Prometheus, which will allow users of this operator to create alerts based on its results. All the metrics are gauges that will report `1` if the best-practice has failed. The metric will always have three parameters: `name`, `namespace` and `kind`. 10 | 11 | This operator doesn't define any CRDs at the moment. It has been bootstrapped with `operator-sdk` making it possible to add a CRD in the future if required. 12 | 13 | ## Architecture Diagrams 14 | 15 | [Architecure Diagrams](./docs/architecture.md) 16 | 17 | ## Running the operator locally 18 | 19 | To build the operator binary, you can run the following make target: 20 | 21 | ``` 22 | make go-build 23 | ``` 24 | 25 | The binary is created in the `build/_output/bin/` directory and can be run using: 26 | 27 | ``` 28 | POD_NAMESPACE="deployment-validation-operator" WATCH_NAMESPACE="" NAMESPACE_IGNORE_PATTERN='^(openshift.*|kube-.*)$' build/_output/bin/deployment-validation-operator --kubeconfig=$HOME/.kube/config --zap-devel 29 | ``` 30 | 31 | Finally you can check metrics exposed by the operator with: 32 | 33 | ``` 34 | curl localhost:8383/metrics 35 | ``` 36 | 37 | ## Deployment 38 | 39 | The manifests to deploy DVO take a permissive approach to permissions. This is done to make it easier to support monitoring new object kinds without having to change rbac rules. This means that elevated permissions will be required in order to deploy DVO through standard manifests. There is a manifest to deploy DVO though OLM from opereatorhub which does alleviate this need to have elevated permissions. 40 | 41 | * DVO deployment should only deploy 1 pod as currently metrics are not replicated across a standard 3 causing installation issues (will be fixed in a later version) 42 | 43 | ### Manual installation 44 | 45 | There are manifests to install the operator under the [`deploy/openshift`](deploy/openshift) directory. A typical installation would go as follows: 46 | 47 | * Create the `deployment-validation-operator` namespace/project 48 | * If deploying to a namespace other than `deployment-validation-operator`, there are commented lines you must change in `deploy/openshift/cluster-role-binding.yaml` and `deploy/openshift/role-binding.yaml` first 49 | * Create the service, service account, configmap, roles and role bindings 50 | * Create the operator deployment 51 | * **Note that the `nodeAffinity` attribute by default requires a node with the `node-role.kubernetes.io/infra` selector. In common (self-managed) clusters there is usually no such node, so you can remove the `nodeAffinity` attribute when deploying to those environments.** 52 | 53 | ``` 54 | oc new-project deployment-validation-operator 55 | for manifest in service-account.yaml \ 56 | service.yaml \ 57 | role.yaml \ 58 | cluster-role.yaml \ 59 | role-binding.yaml \ 60 | cluster-role-binding.yaml \ 61 | configmap.yaml \ 62 | operator.yaml 63 | do 64 | oc create -f deploy/openshift/$manifest 65 | done 66 | ``` 67 | ## Install Grafana dashboard 68 | 69 | There are manifests to install a simple grafana dashboard under the [`deploy/observability`](deploy/observability) directory. 70 | 71 | A typical installation to the default namespace `deployment-validation-operator` goes as follows: 72 | `oc process -f deploy/observability/template.yaml | oc create -f -` 73 | 74 | Or, if you want to deploy deployment-validation-operator components to a custom namespace: 75 | `oc process --local NAMESPACE="custom-dvo-namespace" -f deploy/observability/template.yaml | oc create -f -` 76 | 77 | ## Allow scraping from outside DVO namespace 78 | 79 | The metrics generated by DVO can be scraped by anything that understands prometheus metrics. A network policy may be needed to allow the DVO metrics to be collected from a service running in a namespace other than the one where DVO is deployed. For example, if a service in `some-namespace` wants to scrape the metrics from DVO then a network policy would need to be created like this: 80 | 81 | ``` 82 | oc process --local NAMESPACE='some-namespace' -f deploy/openshift/network-policies.yaml | oc create -f - 83 | ``` 84 | ## Excluding resources from operator validation 85 | 86 | There are two options to exclude the cluster resources from operator validation: 87 | 88 | * exclude the whole namespace by creating (or updating) the `NAMESPACE_IGNORE_PATTERN` environment variable 89 | * exclude a resource by using corresponding kube-linter annotation - see [Ignore specific resources](#ignore-specific-resources) 90 | 91 | ## Configuring Checks 92 | 93 | DVO performs validation checks using kube-linter. The checks configuration is mirrored to the one for the kube-linter project. More information on configuration options can be found [here](https://github.com/stackrox/kube-linter/blob/main/docs/configuring-kubelinter.md), and a list of available checks can be found [here](https://github.com/stackrox/kube-linter/blob/main/docs/generated/checks.md). 94 | 95 | To configure DVO with a different set of checks, create a ConfigMap in the cluster with the new checks configuration. An example of a configuration ConfigMap can be found [here](./deploy/openshift/configmap.yaml). 96 | 97 | If no custom configuration is found (the ConfigMap does not exist or does not contain a check declaration), the operator enables the following checks by default: 98 | * "host-ipc" 99 | * "host-network" 100 | * "host-pid" 101 | * "non-isolated-pod" 102 | * "pdb-max-unavailable" 103 | * "pdb-min-available" 104 | * "privilege-escalation-container" 105 | * "privileged-container" 106 | * "run-as-non-root" 107 | * "unsafe-sysctls" 108 | * "unset-cpu-requirements" 109 | * "unset-memory-requirements" 110 | 111 | ### Enabling checks 112 | 113 | To enable all checks, set the `addAllBuiltIn` property to `true`. If you only want to enable individual checks, include them as a collection in the `include` property and leave `addAllBuiltIn` with a value of `false`. 114 | 115 | The `include` property can work together with `doNotAutoAddDefaults` set to `true` in a whitelisting way. Only the checks collection passed in `include` will be executed. 116 | 117 | ### Disabling checks 118 | 119 | To disable all checks, set the `doNotAutoAddDefaults` property to `true`. If you only want to disable individual checks, include them as a collection in the `exclude` property and leave `doNotAutoAddDefaults` with a value of `false` 120 | 121 | The `exclude` property takes precedence over the `include` property. If a particular check is in both collections, it will be excluded by default. 122 | 123 | The `exclude` property can work in conjunction with `addAllBuiltIn` set to `true` in a blacklisting fashion. All checks will be triggered and only the checks passed in `exclude` will be ignored. 124 | 125 | #### Ignore specific resources 126 | 127 | It is possible to exclude certain resources from any or all validations. This is achieved by adding annotations to the resources we want DVO to ignore. 128 | 129 | To ignore a specific check, the annotation will have a key like `ignore-check.kube-linter.io/check-name`. Where `check-name` can be any supported or custom check. It is recommended that the value for this annotation is a clear explanation of why the resource should be ignored. 130 | 131 | To ignore all checks, the annotation key is `kube-linter.io/ignore-all`. Again, it is recommended to include a meaningful explanation in the value of the annotation. 132 | 133 | e.g. ignoring **run-as-non-root** check 134 | ```yaml 135 | metadata: 136 | annotations: 137 | ignore-check.kube-linter.io/run-as-non-root: "This image must be run as a privileged user for it to work." 138 | ``` 139 | e.g. ignoring all checks 140 | ```yaml 141 | metadata: 142 | annotations: 143 | kube-linter.io/ignore-all: "This deployment is managed by an OLM subscription" 144 | ``` 145 | 146 | This feature is maintained by kube-linter, [more info](https://docs.kubelinter.io/#/configuring-kubelinter?id=ignoring-violations-for-specific-cases) 147 | 148 | ## Tests 149 | 150 | You can run the unit tests via 151 | 152 | ``` 153 | make test 154 | ``` 155 | 156 | The end-to-end tests depend on [`ginkgo`](https://onsi.github.io/ginkgo/#installing-ginkgo). After exporting a `KUBECONFIG` variable, it can be run via 157 | 158 | ``` 159 | make e2e-test 160 | ``` 161 | 162 | The OCP e2e PR checks exist in the [deployment-validation-operator-tests](https://gitlab.cee.redhat.com/ccx/deployment-validation-operator-tests) repository. 163 | Tests are developed there and once a new build is done, the image is pushed onto [quay.io](https://quay.io/repository/redhatqe/deployment-validation-operator-tests). 164 | This image is then mirrored by the mirroring job in openshift release with this [config](https://github.com/openshift/release/blob/master/core-services/image-mirroring/supplemental-ci-images/mapping_supplemental_ci_images_ci#L22). 165 | The config file for the e2e tests job is then found [here](https://github.com/openshift/release/blob/master/ci-operator/config/app-sre/deployment-validation-operator/app-sre-deployment-validation-operator-master.yaml). 166 | 167 | Since these tests depend on the content of the deploy/openshift folder, if any changes are done there, please run the following command: 168 | ``` 169 | operator-sdk generate bundle --package=deployment-validation-operator --input-dir=deploy/openshift 170 | ``` 171 | It is then necessary to head to bundle/manifests/clusterserviceversion.yaml and search for and remove the NodeAffinity section. 172 | 173 | ## Releases 174 | 175 | To create a new DVO release follow this [New DVO Release](./docs/new-releases.md) 176 | 177 | ## Roadmap 178 | 179 | - e2e tests 180 | -------------------------------------------------------------------------------- /hack/olm-registry/hypershift-template.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: template.openshift.io/v1 2 | kind: Template 3 | metadata: 4 | name: hypershift-dvo-selectorsyncset-template 5 | parameters: 6 | - name: REGISTRY_IMG 7 | required: true 8 | - name: IMAGE_TAG 9 | required: true 10 | - name: IMAGE_DIGEST 11 | required: true 12 | - name: REPO_NAME 13 | value: deployment-validation-operator 14 | required: true 15 | - name: DISPLAY_NAME 16 | value: Deployment Validation Operator 17 | required: true 18 | - name: CHANNEL 19 | value: "alpha" 20 | displayName: OLM subscription channel 21 | description: OLM subscription channel 22 | required: true 23 | - name: TARGET_ORG 24 | displayName: Target organization 25 | description: Target api.openshift.com organization to deploy to 26 | required: true 27 | objects: 28 | - apiVersion: hive.openshift.io/v1 29 | kind: SelectorSyncSet 30 | metadata: 31 | annotations: 32 | component-display-name: ${DISPLAY_NAME} 33 | component-name: ${REPO_NAME} 34 | labels: 35 | managed.openshift.io/gitHash: ${IMAGE_TAG} 36 | managed.openshift.io/gitRepoName: ${REPO_NAME} 37 | managed.openshift.io/osd: "true" 38 | name: deployment-validation-operator-hypershift 39 | spec: 40 | clusterDeploymentSelector: 41 | matchLabels: 42 | ext-hypershift.openshift.io/cluster-type: service-cluster 43 | resourceApplyMode: Sync 44 | resources: 45 | - apiVersion: policy.open-cluster-management.io/v1 46 | kind: Policy 47 | metadata: 48 | annotations: 49 | policy.open-cluster-management.io/categories: CM Configuration Management 50 | policy.open-cluster-management.io/controls: CM-2 Baseline Configuration 51 | policy.open-cluster-management.io/standards: NIST SP 800-53 52 | name: deployment-validation-operator 53 | namespace: openshift-acm-policies 54 | spec: 55 | disabled: false 56 | remediationAction: enforce 57 | policy-templates: 58 | - objectDefinition: 59 | apiVersion: policy.open-cluster-management.io/v1 60 | kind: ConfigurationPolicy 61 | metadata: 62 | name: deployment-validation-operator 63 | spec: 64 | evaluationInterval: 65 | compliant: 2h 66 | noncompliant: 45s 67 | object-templates: 68 | - complianceType: MustHave 69 | objectDefinition: 70 | apiVersion: v1 71 | kind: Namespace 72 | metadata: 73 | name: openshift-deployment-validation-operator 74 | labels: 75 | openshift.io/cluster-monitoring: "true" 76 | - complianceType: MustHave 77 | objectDefinition: 78 | apiVersion: operators.coreos.com/v1alpha1 79 | kind: CatalogSource 80 | metadata: 81 | name: deployment-validation-operator-catalog 82 | namespace: openshift-deployment-validation-operator 83 | spec: 84 | sourceType: grpc 85 | image: ${REGISTRY_IMG}@${IMAGE_DIGEST} 86 | displayName: Deployment Validation Operator 87 | publisher: Red Hat 88 | grpcPodConfig: 89 | securityContextConfig: restricted 90 | - complianceType: MustHave 91 | objectDefinition: 92 | apiVersion: operators.coreos.com/v1 93 | kind: OperatorGroup 94 | metadata: 95 | name: deployment-validation-operator-og 96 | namespace: openshift-deployment-validation-operator 97 | annotations: 98 | olm.operatorframework.io/exclude-global-namespace-resolution: "true" 99 | spec: 100 | upgradeStrategy: TechPreviewUnsafeFailForward 101 | targetNamespaces: 102 | - openshift-deployment-validation-operator 103 | - complianceType: MustHave 104 | objectDefinition: 105 | apiVersion: operators.coreos.com/v1alpha1 106 | kind: Subscription 107 | metadata: 108 | name: deployment-validation-operator 109 | namespace: openshift-deployment-validation-operator 110 | spec: 111 | channel: ${CHANNEL} 112 | config: 113 | affinity: 114 | nodeAffinity: {} 115 | env: 116 | - name: "NAMESPACE_IGNORE_PATTERN" 117 | value: "^(openshift.*|kube-.*|default|dedicated-admin|redhat-.*|acm|addon-dba-operator|codeready-.*|prow)$" 118 | name: deployment-validation-operator 119 | source: deployment-validation-operator-catalog 120 | sourceNamespace: openshift-deployment-validation-operator 121 | - complianceType: MustHave 122 | objectDefinition: 123 | apiVersion: networking.k8s.io/v1 124 | kind: NetworkPolicy 125 | metadata: 126 | name: allow-from-openshift-insights 127 | namespace: openshift-deployment-validation-operator 128 | spec: 129 | ingress: 130 | - from: 131 | - namespaceSelector: 132 | matchLabels: 133 | name: openshift-insights 134 | policyTypes: 135 | - Ingress 136 | - complianceType: MustHave 137 | objectDefinition: 138 | apiVersion: networking.k8s.io/v1 139 | kind: NetworkPolicy 140 | metadata: 141 | name: allow-from-openshift-olm 142 | namespace: openshift-deployment-validation-operator 143 | spec: 144 | ingress: 145 | - {} 146 | podSelector: 147 | matchLabels: 148 | olm.catalogSource: deployment-validation-operator-catalog 149 | policyTypes: 150 | - Ingress 151 | - complianceType: MustHave 152 | objectDefinition: 153 | apiVersion: v1 154 | kind: Service 155 | metadata: 156 | name: deployment-validation-operator-metrics 157 | namespace: openshift-deployment-validation-operator 158 | labels: 159 | name: deployment-validation-operator 160 | spec: 161 | ports: 162 | - name: http-metrics 163 | port: 8383 164 | protocol: TCP 165 | targetPort: 8383 166 | selector: 167 | name: deployment-validation-operator 168 | - complianceType: MustHave 169 | objectDefinition: 170 | apiVersion: v1 171 | kind: ConfigMap 172 | metadata: 173 | name: deployment-validation-operator-config 174 | namespace: openshift-deployment-validation-operator 175 | labels: 176 | name: deployment-validation-operator 177 | data: 178 | deployment-validation-operator-config.yaml: |- 179 | checks: 180 | doNotAutoAddDefaults: false 181 | addAllBuiltIn: true 182 | 183 | exclude: 184 | - "access-to-create-pods" 185 | - "access-to-secrets" 186 | - "cluster-admin-role-binding" 187 | - "default-service-account" 188 | - "deprecated-service-account-field" 189 | - "docker-sock" 190 | - "drop-net-raw-capability" 191 | - "env-var-secret" 192 | - "exposed-services" 193 | - "latest-tag" 194 | - "mismatching-selector" 195 | - "no-extensions-v1beta" 196 | - "no-liveness-probe" 197 | - "no-read-only-root-fs" 198 | - "no-readiness-probe" 199 | - "no-rolling-update-strategy" 200 | - "privileged-ports" 201 | - "read-secret-from-env-var" 202 | - "required-annotation-email" 203 | - "required-label-owner" 204 | - "sensitive-host-mounts" 205 | - "ssh-port" 206 | - "unsafe-proc-mount" 207 | - "use-namespace" 208 | - "wildcard-in-rules" 209 | - "writable-host-mount" 210 | - apiVersion: apps.open-cluster-management.io/v1 211 | kind: PlacementRule 212 | metadata: 213 | name: placement-deployment-validation-operator 214 | namespace: openshift-acm-policies 215 | spec: 216 | clusterSelector: 217 | matchExpressions: 218 | - key: hypershift.open-cluster-management.io/hosted-cluster 219 | operator: In 220 | values: 221 | - "true" 222 | - apiVersion: policy.open-cluster-management.io/v1 223 | kind: PlacementBinding 224 | metadata: 225 | name: binding-deployment-validation-operator 226 | namespace: openshift-acm-policies 227 | placementRef: 228 | apiGroup: apps.open-cluster-management.io 229 | kind: PlacementRule 230 | name: placement-deployment-validation-operator 231 | subjects: 232 | - apiGroup: policy.open-cluster-management.io 233 | kind: Policy 234 | name: deployment-validation-operator 235 | --------------------------------------------------------------------------------