Biffs`em and Buffs`em!
\n\n", + "mode": "html" + }, + "pluginVersion": "10.2.2", + "transparent": true, + "type": "text" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 6 + }, + "id": 21, + "panels": [], + "title": "Grades", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMDASHB}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "percentage", + "steps": [ + { + "color": "semi-dark-red", + "value": null + }, + { + "color": "dark-orange", + "value": 50 + }, + { + "color": "green", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 24, + "x": 0, + "y": 7 + }, + "id": 9, + "options": { + "minVizHeight": 75, + "minVizWidth": 75, + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true + }, + "pluginVersion": "10.2.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMDASHB}" + }, + "editorMode": "code", + "expr": "popeye_cluster_score{scan!=\"\", cluster=~\"$cluster\", namespace=~\"$namespace\"}", + "instant": false, + "legendFormat": "{{cluster}}-{{namespace}} ({{grade}})", + "range": true, + "refId": "A" + } + ], + "type": "gauge" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 12 + }, + "id": 14, + "panels": [], + "title": "ScanCodes", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMDASHB}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "red", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 0, + "y": 13 + }, + "id": 11, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "10.2.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMDASHB}" + }, + "editorMode": "code", + "expr": "topk($topk, popeye_code_total{severity=\"error\", cluster=~\"$cluster\", namespace=~\"$namespace\", linter=~\"$linter\"}) by (namespace, linter)", + "format": "time_series", + "instant": false, + "legendFormat": "[POP-{{code}}] {{linter}}", + "range": true, + "refId": "A" + } + ], + "title": "Errors", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMDASHB}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "red", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 8, + "y": 13 + }, + "id": 17, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "10.2.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMDASHB}" + }, + "editorMode": "code", + "expr": "topk($topk, popeye_code_total{severity=\"warn\", cluster=~\"$cluster\", namespace=~\"$namespace\", linter=~\"$linter\"}) by (namespace, linter)", + "format": "time_series", + "instant": false, + "legendFormat": "[POP-{{code}}] {{linter}} ({{namespace}})", + "range": true, + "refId": "A" + } + ], + "title": "Warnings", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMDASHB}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "red", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 16, + "y": 13 + }, + "id": 12, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "10.2.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMDASHB}" + }, + "editorMode": "code", + "expr": "topk($topk, popeye_code_total{severity=\"info\", cluster=~\"$cluster\", namespace=~\"$namespace\", linter=~\"$linter\"}) by (namespace, linter)", + "format": "time_series", + "instant": false, + "legendFormat": "[POP-{{code}}] {{linter}}", + "range": true, + "refId": "A" + } + ], + "title": "Infos", + "type": "timeseries" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 20 + }, + "id": 13, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMDASHB}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "red", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 15, + "w": 8, + "x": 0, + "y": 21 + }, + "id": 7, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "10.2.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMDASHB}" + }, + "editorMode": "code", + "expr": "popeye_severity_total{severity=\"error\", cluster=~\"$cluster\", namespace=~\"$namespace\"}", + "format": "time_series", + "instant": false, + "legendFormat": "{{cluster}} ({{namespace}})", + "range": true, + "refId": "A" + } + ], + "title": "Errors", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMDASHB}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "orange", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 15, + "w": 8, + "x": 8, + "y": 21 + }, + "id": 6, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "10.2.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMDASHB}" + }, + "editorMode": "code", + "expr": "popeye_severity_total{severity=\"warn\", cluster=~\"$cluster\", namespace=~\"$namespace\"}", + "format": "time_series", + "instant": false, + "legendFormat": "{{cluster}} ({{namespace}})", + "range": true, + "refId": "A" + } + ], + "title": "Warnings", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMDASHB}" + }, + "description": "Popeye severity total scores.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 15, + "w": 8, + "x": 16, + "y": 21 + }, + "id": 5, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "value_and_name", + "wideLayout": true + }, + "pluginVersion": "10.2.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMDASHB}" + }, + "editorMode": "code", + "expr": "topk($topk, popeye_severity_total{severity=\"info\", cluster=~\"$cluster\", namespace=~\"$namespace\"})", + "instant": false, + "legendFormat": "{{namespace}}", + "range": true, + "refId": "A" + } + ], + "title": "Infos", + "type": "stat" + } + ], + "title": "Severities", + "type": "row" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 21 + }, + "id": 16, + "panels": [], + "title": "Linters", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMDASHB}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "fillOpacity": 70, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineWidth": 1, + "spanNulls": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "transparent", + "value": null + }, + { + "color": "super-light-red", + "value": 1 + }, + { + "color": "light-red", + "value": 5 + }, + { + "color": "red", + "value": 10 + }, + { + "color": "semi-dark-red", + "value": 20 + }, + { + "color": "dark-red", + "value": 30 + }, + { + "color": "#f9051b", + "value": 50 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 12, + "w": 8, + "x": 0, + "y": 22 + }, + "id": 10, + "options": { + "alignValue": "center", + "legend": { + "displayMode": "table", + "placement": "bottom", + "showLegend": false + }, + "mergeValues": true, + "rowHeight": 0.64, + "showValue": "auto", + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "10.2.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMDASHB}" + }, + "editorMode": "code", + "expr": "topk($topk, sum by (linter) (popeye_linter_tally_total{severity=\"error\", cluster=~\"$cluster\", linter=~\"$linter\"})) by (cluster, linter)", + "format": "time_series", + "instant": false, + "legendFormat": "{{linter}}", + "range": true, + "refId": "A" + } + ], + "title": "Errors", + "type": "state-timeline" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMDASHB}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "fillOpacity": 70, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineWidth": 0, + "spanNulls": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "transparent", + "value": null + }, + { + "color": "light-yellow", + "value": 1 + }, + { + "color": "dark-yellow", + "value": 5 + }, + { + "color": "light-orange", + "value": 10 + }, + { + "color": "semi-dark-orange", + "value": 30 + }, + { + "color": "dark-orange", + "value": 50 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 12, + "w": 8, + "x": 8, + "y": 22 + }, + "id": 18, + "options": { + "alignValue": "left", + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "mergeValues": true, + "rowHeight": 0.9, + "showValue": "auto", + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "10.2.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMDASHB}" + }, + "editorMode": "code", + "expr": "topk($topk, sum by (linter) (popeye_linter_tally_total{severity=\"warn\", cluster=~\"$cluster\", linter=~\"$linter\"})) by (cluster, linter)", + "format": "time_series", + "instant": false, + "legendFormat": "{{linter}}", + "range": true, + "refId": "A" + } + ], + "title": "Warnings", + "type": "state-timeline" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMDASHB}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "fillOpacity": 70, + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineWidth": 0, + "spanNulls": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "transparent", + "value": null + }, + { + "color": "super-light-blue", + "value": 1 + }, + { + "color": "light-blue", + "value": 5 + }, + { + "color": "blue", + "value": 10 + }, + { + "color": "semi-dark-blue", + "value": 20 + }, + { + "color": "dark-blue", + "value": 30 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 12, + "w": 8, + "x": 16, + "y": 22 + }, + "id": 19, + "options": { + "alignValue": "left", + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "mergeValues": true, + "rowHeight": 0.9, + "showValue": "auto", + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "10.2.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMDASHB}" + }, + "editorMode": "code", + "expr": "topk($topk, sum by (linter) (popeye_linter_tally_total{severity=\"info\", cluster=~\"$cluster\", linter=~\"$linter\"})) by (cluster, linter)", + "format": "time_series", + "instant": false, + "legendFormat": "{{linter}}", + "range": true, + "refId": "A" + } + ], + "title": "Info", + "type": "state-timeline" + } + ], + "refresh": "", + "schemaVersion": 38, + "tags": [ + "popeye" + ], + "templating": { + "list": [ + { + "current": { + "selected": false, + "text": "10", + "value": "10" + }, + "description": "Top k values", + "hide": 0, + "includeAll": false, + "label": "TopK", + "multi": false, + "name": "topk", + "options": [ + { + "selected": true, + "text": "10", + "value": "10" + }, + { + "selected": false, + "text": "20", + "value": "20" + }, + { + "selected": false, + "text": "50", + "value": "50" + } + ], + "query": "10,20,50", + "queryValue": "", + "skipUrlSync": false, + "type": "custom" + }, + { + "allValue": "", + "current": {}, + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMDASHB}" + }, + "definition": "label_values(popeye_code_total,cluster)", + "hide": 0, + "includeAll": true, + "label": "Cluster", + "multi": false, + "name": "cluster", + "options": [], + "query": { + "qryType": 1, + "query": "label_values(popeye_code_total,cluster)", + "refId": "PrometheusVariableQueryEditor-VariableQuery" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 1, + "type": "query" + }, + { + "current": {}, + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMDASHB}" + }, + "definition": "label_values(popeye_code_total,namespace)", + "hide": 0, + "includeAll": true, + "label": "Namespace", + "multi": false, + "name": "namespace", + "options": [], + "query": { + "qryType": 1, + "query": "label_values(popeye_code_total,namespace)", + "refId": "PrometheusVariableQueryEditor-VariableQuery" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "type": "query" + }, + { + "current": {}, + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMDASHB}" + }, + "definition": "label_values(popeye_code_total,linter)", + "hide": 0, + "includeAll": true, + "label": "Linter", + "multi": false, + "name": "linter", + "options": [], + "query": { + "qryType": 1, + "query": "label_values(popeye_code_total,linter)", + "refId": "PrometheusVariableQueryEditor-VariableQuery" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "type": "query" + } + ] + }, + "time": { + "from": "now-3h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "PopDash", + "uid": "ca2c5173-a010-4a3d-aab2-cb2871e6f3dd", + "version": 60, + "weekStart": "" +} \ No newline at end of file diff --git a/internal/alias.go b/internal/alias.go index f65cea1a..ee647c52 100644 --- a/internal/alias.go +++ b/internal/alias.go @@ -5,62 +5,110 @@ package internal import ( "fmt" + "slices" "strings" - "github.com/derailed/popeye/internal/client" "github.com/derailed/popeye/types" "github.com/rs/zerolog/log" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +var ( + ClusterGVR = types.NewGVR("cluster") ) // ResourceMetas represents a collection of resource metadata. -type ResourceMetas map[client.GVR]metav1.APIResource +type ResourceMetas map[types.GVR]metav1.APIResource // Aliases represents a collection of resource aliases. type Aliases struct { - aliases map[string]client.GVR + aliases map[string]types.GVR metas ResourceMetas } // NewAliases returns a new instance. func NewAliases() *Aliases { a := Aliases{ - aliases: make(map[string]client.GVR), + aliases: make(map[string]types.GVR), metas: make(ResourceMetas), } return &a } +func (a *Aliases) Dump() { + log.Debug().Msgf("\nAliases...") + kk := make([]string, 0, len(a.aliases)) + for k := range a.aliases { + kk = append(kk, k) + } + slices.Sort(kk) + for _, k := range kk { + log.Debug().Msgf("%-25s: %s", k, a.aliases[k]) + } +} + +var customShortNames = map[string][]string{ + "cluster": {"cl"}, + "secrets": {"sec"}, + "deployments": {"dp"}, + "clusterroles": {"cr"}, + "clusterrolebindings": {"crb"}, + "roles": {"ro"}, + "rolebindings": {"rb"}, + "networkpolicies": {"np"}, + "httproutes": {"gwr"}, + "gatewayclassess": {"gwc"}, + "gateways": {"gw"}, +} + // Init loads the aliases glossary. -func (a *Aliases) Init(f types.Factory, gvrs GVRs) error { - if err := a.loadPreferred(f); err != nil { +func (a *Aliases) Init(c types.Connection) error { + if err := a.loadPreferred(c); err != nil { return err } - for _, k := range gvrs { - gvr := client.NewGVR(k) - res, ok := a.metas[gvr] - if !ok { - panic(fmt.Sprintf("No resource meta found for %s", gvr)) - } + for gvr, res := range a.metas { a.aliases[res.Name] = gvr - a.aliases[res.SingularName] = gvr + if res.SingularName != "" { + a.aliases[res.SingularName] = gvr + } for _, n := range res.ShortNames { a.aliases[n] = gvr } + if kk, ok := customShortNames[res.Name]; ok { + for _, k := range kk { + a.aliases[k] = gvr + } + } + if lgvr, ok := Glossary[R(res.SingularName)]; ok { + if greaterV(gvr.V(), lgvr.V()) { + Glossary[R(res.SingularName)] = gvr + } + } else if lgvr, ok := Glossary[R(res.Name)]; ok { + if greaterV(gvr.V(), lgvr.V()) { + Glossary[R(res.Name)] = gvr + } + } } - a.aliases["cl"] = client.NewGVR("cluster") - a.aliases["sec"] = client.NewGVR("v1/secrets") - a.aliases["dp"] = client.NewGVR("apps/v1/deployments") - a.aliases["cr"] = client.NewGVR("rbac.authorization.k8s.io/v1/clusterroles") - a.aliases["crb"] = client.NewGVR("rbac.authorization.k8s.io/v1/clusterrolebindings") - a.aliases["ro"] = client.NewGVR("rbac.authorization.k8s.io/v1/roles") - a.aliases["rb"] = client.NewGVR("rbac.authorization.k8s.io/v1/rolebindings") - a.aliases["np"] = client.NewGVR("networking.k8s.io/v1/networkpolicies") return nil } +func greaterV(v1, v2 string) bool { + if v1 == "" && v2 == "" { + return true + } + if v2 == "" { + return true + } + if v1 == "v1" || v1 == "v2" { + return true + } + + return false +} + // TitleFor produces a section title from an alias. func (a *Aliases) TitleFor(s string, plural bool) string { gvr, ok := a.aliases[s] @@ -77,28 +125,31 @@ func (a *Aliases) TitleFor(s string, plural bool) string { return m.SingularName } -func (a *Aliases) loadPreferred(f types.Factory) error { - dial, err := f.Client().CachedDiscovery() +func (a *Aliases) loadPreferred(c types.Connection) error { + dial, err := c.CachedDiscovery() if err != nil { return err } - rr, err := dial.ServerPreferredResources() + ll, err := dial.ServerPreferredResources() if err != nil { return err } - for _, r := range rr { - for _, res := range r.APIResources { - gvr := client.FromGVAndR(r.GroupVersion, res.Name) - res.Group, res.Version = gvr.G(), gvr.V() - if res.SingularName == "" { - res.SingularName = strings.ToLower(res.Kind) + for _, l := range ll { + gv, err := schema.ParseGroupVersion(l.GroupVersion) + if err != nil { + continue + } + for _, r := range l.APIResources { + gvr := types.NewGVRFromAPIRes(gv, r) + r.Group, r.Version = gvr.G(), gvr.V() + if r.SingularName == "" { + r.SingularName = strings.ToLower(r.Kind) } - a.metas[gvr] = res + a.metas[gvr] = r } } - - a.metas[client.NewGVR("cluster")] = metav1.APIResource{ - Name: "cluster", + a.metas[ClusterGVR] = metav1.APIResource{ + Name: ClusterGVR.String(), } return nil @@ -118,17 +169,18 @@ func (a *Aliases) ToResources(nn []string) []string { } // Singular returns a singular resource name. -func (a *Aliases) Singular(gvr client.GVR) string { +func (a *Aliases) Singular(gvr types.GVR) string { m, ok := a.metas[gvr] if !ok { log.Error().Msgf("Missing meta for gvr %q", gvr) return gvr.R() } + return m.SingularName } // Exclude checks if section should be excluded from the report. -func (a *Aliases) Exclude(gvr client.GVR, sections []string) bool { +func (a *Aliases) Exclude(gvr types.GVR, sections []string) bool { if len(sections) == 0 { return false } diff --git a/internal/cache/cluster.go b/internal/cache/cluster.go index b21e1c11..751a92b5 100644 --- a/internal/cache/cluster.go +++ b/internal/cache/cluster.go @@ -3,20 +3,22 @@ package cache +import "github.com/Masterminds/semver" + // ClusterKey tracks Cluster resource references const ClusterKey = "cl" // Cluster represents Cluster cache. type Cluster struct { - major, minor string + rev *semver.Version } // NewCluster returns a new Cluster cache. -func NewCluster(major, minor string) *Cluster { - return &Cluster{major: major, minor: minor} +func NewCluster(v *semver.Version) *Cluster { + return &Cluster{rev: v} } // ListVersion returns cluster server version. -func (c *Cluster) ListVersion() (string, string) { - return c.major, c.minor +func (c *Cluster) ListVersion() *semver.Version { + return c.rev } diff --git a/internal/cache/cluster_test.go b/internal/cache/cluster_test.go index d5ffa4d9..459f70fc 100644 --- a/internal/cache/cluster_test.go +++ b/internal/cache/cluster_test.go @@ -6,14 +6,21 @@ package cache_test import ( "testing" + "github.com/Masterminds/semver" "github.com/derailed/popeye/internal/cache" + "github.com/rs/zerolog" "github.com/stretchr/testify/assert" ) +func init() { + zerolog.SetGlobalLevel(zerolog.FatalLevel) +} + func TestCluster(t *testing.T) { - c := cache.NewCluster("1", "9") + v, err := semver.NewVersion("1.9") + assert.NoError(t, err) + c := cache.NewCluster(v) - ma, mi := c.ListVersion() - assert.Equal(t, "1", ma) - assert.Equal(t, "9", mi) + v1 := c.ListVersion() + assert.Equal(t, v, v1) } diff --git a/internal/cache/cm.go b/internal/cache/cm.go deleted file mode 100644 index 2aa81b0e..00000000 --- a/internal/cache/cm.go +++ /dev/null @@ -1,26 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package cache - -import ( - v1 "k8s.io/api/core/v1" -) - -// ConfigMapKey tracks ConfigMap resource references -const ConfigMapKey = "cm" - -// ConfigMap represents ConfigMap cache. -type ConfigMap struct { - cms map[string]*v1.ConfigMap -} - -// NewConfigMap returns a new ConfigMap cache. -func NewConfigMap(cms map[string]*v1.ConfigMap) *ConfigMap { - return &ConfigMap{cms: cms} -} - -// ListConfigMaps returns all available ConfigMaps on the cluster. -func (c *ConfigMap) ListConfigMaps() map[string]*v1.ConfigMap { - return c.cms -} diff --git a/internal/cache/cr.go b/internal/cache/cr.go deleted file mode 100644 index 400098a6..00000000 --- a/internal/cache/cr.go +++ /dev/null @@ -1,26 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package cache - -import ( - rbacv1 "k8s.io/api/rbac/v1" -) - -// ClusterRoleKey tracks ClusterRole resource references -const ClusterRoleKey = "clusterrole" - -// ClusterRole represents ClusterRole cache. -type ClusterRole struct { - crs map[string]*rbacv1.ClusterRole -} - -// NewClusterRole returns a new ClusterRole cache. -func NewClusterRole(crs map[string]*rbacv1.ClusterRole) *ClusterRole { - return &ClusterRole{crs: crs} -} - -// ListClusterRoles returns all available ClusterRoles on the cluster. -func (c *ClusterRole) ListClusterRoles() map[string]*rbacv1.ClusterRole { - return c.crs -} diff --git a/internal/cache/crb.go b/internal/cache/crb.go index 110f8a40..a503a602 100644 --- a/internal/cache/crb.go +++ b/internal/cache/crb.go @@ -8,27 +8,28 @@ import ( "sync" "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/client" + "github.com/derailed/popeye/internal/db" rbacv1 "k8s.io/api/rbac/v1" ) // ClusterRoleBinding represents ClusterRoleBinding cache. type ClusterRoleBinding struct { - crbs map[string]*rbacv1.ClusterRoleBinding + db *db.DB } // NewClusterRoleBinding returns a new ClusterRoleBinding cache. -func NewClusterRoleBinding(crbs map[string]*rbacv1.ClusterRoleBinding) *ClusterRoleBinding { - return &ClusterRoleBinding{crbs: crbs} -} - -// ListClusterRoleBindings returns all available ClusterRoleBindings on the cluster. -func (c *ClusterRoleBinding) ListClusterRoleBindings() map[string]*rbacv1.ClusterRoleBinding { - return c.crbs +func NewClusterRoleBinding(db *db.DB) *ClusterRoleBinding { + return &ClusterRoleBinding{db: db} } // ClusterRoleRefs computes all clusterrole external references. func (c *ClusterRoleBinding) ClusterRoleRefs(refs *sync.Map) { - for fqn, crb := range c.crbs { + txn, it := c.db.MustITFor(internal.Glossary[internal.CRB]) + defer txn.Abort() + for o := it.Next(); o != nil; o = it.Next() { + crb := o.(*rbacv1.ClusterRoleBinding) + fqn := client.FQN(crb.Namespace, crb.Name) key := ResFqn(strings.ToLower(crb.RoleRef.Kind), FQN(crb.Namespace, crb.RoleRef.Name)) if c, ok := refs.Load(key); ok { c.(internal.StringSet).Add(fqn) diff --git a/internal/cache/crb_test.go b/internal/cache/crb_test.go index a3629bb9..82a06df3 100644 --- a/internal/cache/crb_test.go +++ b/internal/cache/crb_test.go @@ -9,13 +9,21 @@ import ( "github.com/derailed/popeye/internal" "github.com/derailed/popeye/internal/cache" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/test" "github.com/stretchr/testify/assert" rbacv1 "k8s.io/api/rbac/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) func TestClusterRoleRef(t *testing.T) { - cr := cache.NewClusterRoleBinding(makeCRBMap()) + dba, err := test.NewTestDB() + assert.NoError(t, err) + l := db.NewLoader(dba) + + ctx := test.MakeCtx(t) + assert.NoError(t, test.LoadDB[*rbacv1.ClusterRoleBinding](ctx, l.DB, "auth/crb/1.yaml", internal.Glossary[internal.CRB])) + + cr := cache.NewClusterRoleBinding(dba) var refs sync.Map cr.ClusterRoleRefs(&refs) @@ -24,36 +32,9 @@ func TestClusterRoleRef(t *testing.T) { _, ok = m.(internal.StringSet)["crb1"] assert.True(t, ok) - m, ok = refs.Load("role:blee/r1") - assert.True(t, ok) - _, ok = m.(internal.StringSet)["crb2"] + m, ok = refs.Load("role:r1") assert.True(t, ok) -} - -// Helpers... -func makeCRBMap() map[string]*rbacv1.ClusterRoleBinding { - return map[string]*rbacv1.ClusterRoleBinding{ - "crb1": makeCRB("", "crb1", "ClusterRole", "cr1"), - "crb2": makeCRB("blee", "crb2", "Role", "r1"), - } -} - -func makeCRB(ns, name, kind, refName string) *rbacv1.ClusterRoleBinding { - return &rbacv1.ClusterRoleBinding{ - ObjectMeta: makeObjMeta(ns, name), - RoleRef: rbacv1.RoleRef{ - Kind: kind, - Name: refName, - }, - } -} - -func makeObjMeta(ns, n string) metav1.ObjectMeta { - m := metav1.ObjectMeta{Name: n} - if ns != "" { - m.Namespace = ns - } - - return m + _, ok = m.(internal.StringSet)["crb3"] + assert.True(t, ok) } diff --git a/internal/cache/dp.go b/internal/cache/dp.go deleted file mode 100644 index f7a47e6b..00000000 --- a/internal/cache/dp.go +++ /dev/null @@ -1,26 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package cache - -import ( - appsv1 "k8s.io/api/apps/v1" -) - -// DeploymentKey tracks Deployment resource references -const DeploymentKey = "dp" - -// Deployment represents Deployment cache. -type Deployment struct { - dps map[string]*appsv1.Deployment -} - -// NewDeployment returns a new Deployment cache. -func NewDeployment(dps map[string]*appsv1.Deployment) *Deployment { - return &Deployment{dps: dps} -} - -// ListDeployments returns all available Deployments on the cluster. -func (d *Deployment) ListDeployments() map[string]*appsv1.Deployment { - return d.dps -} diff --git a/internal/cache/ds.go b/internal/cache/ds.go deleted file mode 100644 index ee8e53e9..00000000 --- a/internal/cache/ds.go +++ /dev/null @@ -1,26 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package cache - -import ( - appsv1 "k8s.io/api/apps/v1" -) - -// DaemonSetKey tracks DaemonSet resource references -const DaemonSetKey = "ds" - -// DaemonSet represents DaemonSet cache. -type DaemonSet struct { - ds map[string]*appsv1.DaemonSet -} - -// NewDaemonSet returns a new DaemonSet cache. -func NewDaemonSet(ds map[string]*appsv1.DaemonSet) *DaemonSet { - return &DaemonSet{ds: ds} -} - -// ListDaemonSets returns all available DaemonSets on the cluster. -func (d *DaemonSet) ListDaemonSets() map[string]*appsv1.DaemonSet { - return d.ds -} diff --git a/internal/cache/ep.go b/internal/cache/ep.go deleted file mode 100644 index 21b47ff5..00000000 --- a/internal/cache/ep.go +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package cache - -import ( - v1 "k8s.io/api/core/v1" -) - -// Endpoints represents Endpoints cache. -type Endpoints struct { - eps map[string]*v1.Endpoints -} - -// NewEndpoints returns a new Endpoints cache. -func NewEndpoints(eps map[string]*v1.Endpoints) *Endpoints { - return &Endpoints{eps: eps} -} - -// GetEndpoints returns all available Endpoints on the cluster. -func (e *Endpoints) GetEndpoints(fqn string) *v1.Endpoints { - return e.eps[fqn] -} diff --git a/internal/cache/helper.go b/internal/cache/helper.go index e42383dd..3c0b479d 100644 --- a/internal/cache/helper.go +++ b/internal/cache/helper.go @@ -37,7 +37,7 @@ func namespaced(fqn string) (string, string) { } // MatchLabels check if pod labels match a selector. -func matchLabels(labels, sel map[string]string) bool { +func MatchLabels(labels, sel map[string]string) bool { if len(sel) == 0 { return false } diff --git a/internal/cache/helper_test.go b/internal/cache/helper_test.go index d7d7ee4d..71e2c30f 100644 --- a/internal/cache/helper_test.go +++ b/internal/cache/helper_test.go @@ -78,7 +78,7 @@ func TestMatchLabels(t *testing.T) { for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, matchLabels(u.labels, u.selector)) + assert.Equal(t, u.e, MatchLabels(u.labels, u.selector)) }) } } diff --git a/internal/cache/hpa.go b/internal/cache/hpa.go deleted file mode 100644 index 7af3340d..00000000 --- a/internal/cache/hpa.go +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package cache - -import ( - autoscalingv1 "k8s.io/api/autoscaling/v1" -) - -// HorizontalPodAutoscaler represents a collection of HorizontalPodAutoScalers available on a cluster. -type HorizontalPodAutoscaler struct { - hpas map[string]*autoscalingv1.HorizontalPodAutoscaler -} - -// NewHorizontalPodAutoscaler returns a new HorizontalPodAutoScaler. -func NewHorizontalPodAutoscaler(svcs map[string]*autoscalingv1.HorizontalPodAutoscaler) *HorizontalPodAutoscaler { - return &HorizontalPodAutoscaler{svcs} -} - -// ListHorizontalPodAutoscalers returns all available HorizontalPodAutoScalers on the cluster. -func (h *HorizontalPodAutoscaler) ListHorizontalPodAutoscalers() map[string]*autoscalingv1.HorizontalPodAutoscaler { - return h.hpas -} diff --git a/internal/cache/ing.go b/internal/cache/ing.go index fc118d55..b67980ca 100644 --- a/internal/cache/ing.go +++ b/internal/cache/ing.go @@ -4,11 +4,12 @@ package cache import ( + "errors" "sync" - netv1 "k8s.io/api/networking/v1" - "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" + netv1 "k8s.io/api/networking/v1" ) // IngressKey tracks Ingress resource references @@ -16,26 +17,29 @@ const IngressKey = "ing" // Ingress represents Ingress cache. type Ingress struct { - ings map[string]*netv1.Ingress + db *db.DB } // NewIngress returns a new Ingress cache. -func NewIngress(ings map[string]*netv1.Ingress) *Ingress { - return &Ingress{ings: ings} -} - -// ListIngresses returns all available Ingresss on the cluster. -func (d *Ingress) ListIngresses() map[string]*netv1.Ingress { - return d.ings +func NewIngress(db *db.DB) *Ingress { + return &Ingress{db: db} } // IngressRefs computes all ingress external references. -func (d *Ingress) IngressRefs(refs *sync.Map) { - for _, ing := range d.ings { +func (d *Ingress) IngressRefs(refs *sync.Map) error { + txn, it := d.db.MustITFor(internal.Glossary[internal.ING]) + defer txn.Abort() + for o := it.Next(); o != nil; o = it.Next() { + ing, ok := o.(*netv1.Ingress) + if !ok { + return errors.New("expected ing") + } for _, tls := range ing.Spec.TLS { d.trackReference(refs, ResFqn(SecretKey, FQN(ing.Namespace, tls.SecretName))) } } + + return nil } func (d *Ingress) trackReference(refs *sync.Map, key string) { diff --git a/internal/cache/ing_test.go b/internal/cache/ing_test.go index b393c953..0a66d9d9 100644 --- a/internal/cache/ing_test.go +++ b/internal/cache/ing_test.go @@ -7,29 +7,24 @@ import ( "sync" "testing" - "github.com/magiconair/properties/assert" + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/test" + "github.com/stretchr/testify/assert" netv1 "k8s.io/api/networking/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) func TestIngressRefs(t *testing.T) { - ing := NewIngress(map[string]*netv1.Ingress{ - "default/ing1": { - ObjectMeta: metav1.ObjectMeta{ - Namespace: "default", - }, - Spec: netv1.IngressSpec{ - TLS: []netv1.IngressTLS{ - { - SecretName: "foo", - }, - }, - }, - }, - }) + dba, err := test.NewTestDB() + assert.NoError(t, err) + l := db.NewLoader(dba) + + ctx := test.MakeCtx(t) + assert.NoError(t, test.LoadDB[*netv1.Ingress](ctx, l.DB, "net/ingress/1.yaml", internal.Glossary[internal.ING])) var refs sync.Map - ing.IngressRefs(&refs) + ing := NewIngress(dba) + assert.NoError(t, ing.IngressRefs(&refs)) _, ok := refs.Load("sec:default/foo") assert.Equal(t, ok, true) diff --git a/internal/cache/limit_range.go b/internal/cache/limit_range.go deleted file mode 100644 index 68a9f41f..00000000 --- a/internal/cache/limit_range.go +++ /dev/null @@ -1,26 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package cache - -import ( - v1 "k8s.io/api/core/v1" -) - -// LimitRangeKey tracks LimitRange resource references -const LimitRangeKey = "lr" - -// LimitRange represents LimitRange cache. -type LimitRange struct { - lrs map[string]*v1.LimitRange -} - -// NewLimitRange returns a new LimitRange cache. -func NewLimitRange(lrs map[string]*v1.LimitRange) *LimitRange { - return &LimitRange{lrs: lrs} -} - -// ListLimitRanges returns all available LimitRanges on the cluster. -func (c *LimitRange) ListLimitRanges() map[string]*v1.LimitRange { - return c.lrs -} diff --git a/internal/cache/no.go b/internal/cache/no.go deleted file mode 100644 index 6b27818c..00000000 --- a/internal/cache/no.go +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package cache - -import ( - v1 "k8s.io/api/core/v1" -) - -// Node represents a collection of Nodes available on a cluster. -type Node struct { - nodes map[string]*v1.Node -} - -// NewNode returns a new Node. -func NewNode(svcs map[string]*v1.Node) *Node { - return &Node{svcs} -} - -// ListNodes returns all available Nodes on the cluster. -func (n *Node) ListNodes() map[string]*v1.Node { - return n.nodes -} diff --git a/internal/cache/no_mx.go b/internal/cache/no_mx.go index 89073422..2d83f753 100644 --- a/internal/cache/no_mx.go +++ b/internal/cache/no_mx.go @@ -4,53 +4,46 @@ package cache import ( + "github.com/derailed/popeye/internal/db" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" - mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" ) -// NodesMetrics represents a Node metrics cache. -type NodesMetrics struct { - nmx map[string]*mv1beta1.NodeMetrics -} - -// NewNodesMetrics returns new Node metrics cache. -func NewNodesMetrics(mx map[string]*mv1beta1.NodeMetrics) *NodesMetrics { - return &NodesMetrics{nmx: mx} -} - -// ListNodesMetrics returns all available NodeMetrics on the cluster. -func (n *NodesMetrics) ListNodesMetrics() map[string]*mv1beta1.NodeMetrics { - return n.nmx -} - // ListAllocatedMetrics collects total used cpu and mem on the cluster. -func (n *NodesMetrics) ListAllocatedMetrics() v1.ResourceList { +func listAllocatedMetrics(db *db.DB) (v1.ResourceList, error) { cpu, mem := new(resource.Quantity), new(resource.Quantity) - for _, mx := range n.nmx { + mm, err := db.ListNMX() + if err != nil { + return nil, err + } + for _, mx := range mm { cpu.Add(*mx.Usage.Cpu()) mem.Add(*mx.Usage.Memory()) } - return v1.ResourceList{ - v1.ResourceCPU: *cpu, - v1.ResourceMemory: *mem, - } + return v1.ResourceList{v1.ResourceCPU: *cpu, v1.ResourceMemory: *mem}, nil } // ListAvailableMetrics return the total cluster available cpu/mem. -func (n *NodesMetrics) ListAvailableMetrics(nn map[string]*v1.Node) v1.ResourceList { +func ListAvailableMetrics(db *db.DB) (v1.ResourceList, error) { cpu, mem := new(resource.Quantity), new(resource.Quantity) + nn, err := db.ListNodes() + if err != nil { + return nil, err + } for _, n := range nn { cpu.Add(*n.Status.Allocatable.Cpu()) mem.Add(*n.Status.Allocatable.Memory()) } - used := n.ListAllocatedMetrics() + used, err := listAllocatedMetrics(db) + if err != nil { + return nil, err + } cpu.Sub(*used.Cpu()) mem.Sub(*used.Memory()) return v1.ResourceList{ v1.ResourceCPU: *cpu, v1.ResourceMemory: *mem, - } + }, nil } diff --git a/internal/cache/no_mx_test.go b/internal/cache/no_mx_test.go index 19c00f30..739d904f 100644 --- a/internal/cache/no_mx_test.go +++ b/internal/cache/no_mx_test.go @@ -6,9 +6,11 @@ package cache import ( "testing" + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/test" "github.com/stretchr/testify/assert" v1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" ) @@ -24,17 +26,26 @@ func TestClusterAllocatableMetrics(t *testing.T) { "n2": makeNodeMx("n2", "300m", "200Mi"), }, e: v1.ResourceList{ - v1.ResourceCPU: toQty("400m"), - v1.ResourceMemory: toQty("300Mi"), + v1.ResourceCPU: test.ToQty("2"), + v1.ResourceMemory: test.ToQty("200Mi"), }, }, } + dba, err := test.NewTestDB() + assert.NoError(t, err) + l := db.NewLoader(dba) + + ctx := test.MakeCtx(t) + assert.NoError(t, test.LoadDB[*mv1beta1.NodeMetrics](ctx, l.DB, "mx/node/1.yaml", internal.Glossary[internal.NMX])) + assert.NoError(t, test.LoadDB[*v1.Node](ctx, l.DB, "core/node/1.yaml", internal.Glossary[internal.NO])) + for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { - n := NewNodesMetrics(map[string]*mv1beta1.NodeMetrics{}) - res := n.ListAvailableMetrics(u.nn) + res, err := ListAvailableMetrics(dba) + assert.NoError(t, err) + assert.Equal(t, u.e.Cpu().Value(), res.Cpu().Value()) assert.Equal(t, u.e.Memory().Value(), res.Memory().Value()) }) @@ -44,19 +55,13 @@ func TestClusterAllocatableMetrics(t *testing.T) { // ---------------------------------------------------------------------------- // Helpers... -func toQty(s string) resource.Quantity { - q, _ := resource.ParseQuantity(s) - - return q -} - func makeNodeMx(n, cpu, mem string) *v1.Node { return &v1.Node{ ObjectMeta: metav1.ObjectMeta{Name: n}, Status: v1.NodeStatus{ Allocatable: v1.ResourceList{ - v1.ResourceCPU: toQty(cpu), - v1.ResourceMemory: toQty(mem), + v1.ResourceCPU: test.ToQty(cpu), + v1.ResourceMemory: test.ToQty(mem), }, }, } diff --git a/internal/cache/np.go b/internal/cache/np.go deleted file mode 100644 index 85038b2b..00000000 --- a/internal/cache/np.go +++ /dev/null @@ -1,26 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package cache - -import ( - nv1 "k8s.io/api/networking/v1" -) - -// NetworkPolicyKey tracks NetworkPolicy resource references -const NetworkPolicyKey = "np" - -// NetworkPolicy represents NetworkPolicy cache. -type NetworkPolicy struct { - nps map[string]*nv1.NetworkPolicy -} - -// NewNetworkPolicy returns a new NetworkPolicy cache. -func NewNetworkPolicy(nps map[string]*nv1.NetworkPolicy) *NetworkPolicy { - return &NetworkPolicy{nps: nps} -} - -// ListNetworkPolicies returns all available NetworkPolicys on the cluster. -func (d *NetworkPolicy) ListNetworkPolicies() map[string]*nv1.NetworkPolicy { - return d.nps -} diff --git a/internal/cache/ns.go b/internal/cache/ns.go deleted file mode 100644 index 1def995e..00000000 --- a/internal/cache/ns.go +++ /dev/null @@ -1,39 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package cache - -import ( - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -// Namespace represents a collection of Namespaces available on a cluster. -type Namespace struct { - nss map[string]*v1.Namespace -} - -// NewNamespace returns a new Namespace. -func NewNamespace(nss map[string]*v1.Namespace) *Namespace { - return &Namespace{nss} -} - -// ListNamespaces returns all available Namespaces on the cluster. -func (n *Namespace) ListNamespaces() map[string]*v1.Namespace { - return n.nss -} - -// ListNamespacesBySelector list all pods matching the given selector. -func (n *Namespace) ListNamespacesBySelector(sel *metav1.LabelSelector) map[string]*v1.Namespace { - res := map[string]*v1.Namespace{} - if sel == nil { - return res - } - for fqn, ns := range n.nss { - if matchLabels(ns.ObjectMeta.Labels, sel.MatchLabels) { - res[fqn] = ns - } - } - - return res -} diff --git a/internal/cache/pdb.go b/internal/cache/pdb.go deleted file mode 100644 index 4bb0435a..00000000 --- a/internal/cache/pdb.go +++ /dev/null @@ -1,41 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package cache - -import ( - policyv1 "k8s.io/api/policy/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -// PodDisruptionBudgetKey tracks PodDisruptionBudget resource references -const PodDisruptionBudgetKey = "pdb" - -// PodDisruptionBudget represents PodDisruptionBudget cache. -type PodDisruptionBudget struct { - cms map[string]*policyv1.PodDisruptionBudget -} - -// NewPodDisruptionBudget returns a new PodDisruptionBudget cache. -func NewPodDisruptionBudget(cms map[string]*policyv1.PodDisruptionBudget) *PodDisruptionBudget { - return &PodDisruptionBudget{cms: cms} -} - -// ListPodDisruptionBudgets returns all available PodDisruptionBudgets on the cluster. -func (c *PodDisruptionBudget) ListPodDisruptionBudgets() map[string]*policyv1.PodDisruptionBudget { - return c.cms -} - -// ForLabels returns a pdb whose selector match the given labels. Returns nil if no match. -func (c *PodDisruptionBudget) ForLabels(labels map[string]string) *policyv1.PodDisruptionBudget { - for _, pdb := range c.ListPodDisruptionBudgets() { - m, err := metav1.LabelSelectorAsMap(pdb.Spec.Selector) - if err != nil { - continue - } - if matchLabels(labels, m) { - return pdb - } - } - return nil -} diff --git a/internal/cache/pod.go b/internal/cache/pod.go index 47987d2b..c77a78e4 100644 --- a/internal/cache/pod.go +++ b/internal/cache/pod.go @@ -4,62 +4,37 @@ package cache import ( + "errors" "sync" "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/client" + "github.com/derailed/popeye/internal/db" v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/labels" ) // Pod represents a Pod cache. type Pod struct { - pods map[string]*v1.Pod + db *db.DB } // NewPod returns a Pod cache. -func NewPod(pods map[string]*v1.Pod) *Pod { - return &Pod{pods: pods} -} - -// ListPods return available pods. -func (p *Pod) ListPods() map[string]*v1.Pod { - return p.pods -} - -// ListPodsBySelector list all pods matching the given selector. -func (p *Pod) ListPodsBySelector(ns string, sel *metav1.LabelSelector) map[string]*v1.Pod { - res := map[string]*v1.Pod{} - if sel == nil || sel.Size() == 0 { - return res - } - for fqn, po := range p.pods { - if po.Namespace != ns { - continue - } - if s, err := metav1.LabelSelectorAsSelector(sel); err == nil && s.Matches(labels.Set(po.Labels)) { - res[fqn] = po - } - } - - return res -} - -// GetPod returns a pod via a label query. -func (p *Pod) GetPod(ns string, sel map[string]string) *v1.Pod { - res := p.ListPodsBySelector(ns, &metav1.LabelSelector{MatchLabels: sel}) - for _, v := range res { - return v - } - - return nil +func NewPod(dba *db.DB) *Pod { + return &Pod{db: dba} } // PodRefs computes all pods external references. -func (p *Pod) PodRefs(refs *sync.Map) { - for fqn, po := range p.pods { +func (p *Pod) PodRefs(refs *sync.Map) error { + txn, it := p.db.MustITFor(internal.Glossary[internal.PO]) + defer txn.Abort() + for o := it.Next(); o != nil; o = it.Next() { + po, ok := o.(*v1.Pod) + if !ok { + return errors.New("expected a pod") + } + fqn := client.FQN(po.Namespace, po.Name) p.imagePullSecRefs(po.Namespace, po.Spec.ImagePullSecrets, refs) - p.namespaceRefs(po.Namespace, refs) + namespaceRefs(po.Namespace, refs) for _, co := range po.Spec.InitContainers { p.containerRefs(fqn, co, refs) } @@ -68,19 +43,8 @@ func (p *Pod) PodRefs(refs *sync.Map) { } p.volumeRefs(po.Namespace, po.Spec.Volumes, refs) } -} -func (p *Pod) imagePullSecRefs(ns string, sRefs []v1.LocalObjectReference, refs *sync.Map) { - for _, s := range sRefs { - key := ResFqn(SecretKey, FQN(ns, s.Name)) - refs.Store(key, internal.AllKeys) - } -} - -func (p *Pod) namespaceRefs(ns string, refs *sync.Map) { - if set, ok := refs.LoadOrStore("ns", internal.StringSet{ns: internal.Blank}); ok { - set.(internal.StringSet).Add(ns) - } + return nil } func (p *Pod) containerRefs(pfqn string, co v1.Container, refs *sync.Map) { @@ -104,6 +68,34 @@ func (p *Pod) containerRefs(pfqn string, co v1.Container, refs *sync.Map) { } } +func (*Pod) volumeRefs(ns string, vv []v1.Volume, refs *sync.Map) { + for _, v := range vv { + sv := v.VolumeSource.Secret + if sv != nil { + addKeys(SecretKey, FQN(ns, sv.SecretName), sv.Items, refs) + continue + } + + cmv := v.VolumeSource.ConfigMap + if cmv != nil { + addKeys(ConfigMapKey, FQN(ns, cmv.LocalObjectReference.Name), cmv.Items, refs) + } + } +} + +func (p *Pod) imagePullSecRefs(ns string, sRefs []v1.LocalObjectReference, refs *sync.Map) { + for _, s := range sRefs { + key := ResFqn(SecretKey, FQN(ns, s.Name)) + refs.Store(key, internal.AllKeys) + } +} + +func namespaceRefs(ns string, refs *sync.Map) { + if set, ok := refs.LoadOrStore("ns", internal.StringSet{ns: internal.Blank}); ok { + set.(internal.StringSet).Add(ns) + } +} + func (p *Pod) secretRefs(ns string, ref *v1.SecretKeySelector, refs *sync.Map) { if ref == nil { return @@ -132,21 +124,6 @@ func (p *Pod) configMapRefs(ns string, ref *v1.ConfigMapKeySelector, refs *sync. } } -func (*Pod) volumeRefs(ns string, vv []v1.Volume, refs *sync.Map) { - for _, v := range vv { - sv := v.VolumeSource.Secret - if sv != nil { - addKeys(SecretKey, FQN(ns, sv.SecretName), sv.Items, refs) - continue - } - - cmv := v.VolumeSource.ConfigMap - if cmv != nil { - addKeys(ConfigMapKey, FQN(ns, cmv.LocalObjectReference.Name), cmv.Items, refs) - } - } -} - // ---------------------------------------------------------------------------- // Helpers... diff --git a/internal/cache/pod_mx.go b/internal/cache/pod_mx.go deleted file mode 100644 index 3a26036a..00000000 --- a/internal/cache/pod_mx.go +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package cache - -import ( - mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" -) - -// PodsMetrics represents a Pod metrics cache. -type PodsMetrics struct { - mx map[string]*mv1beta1.PodMetrics -} - -// NewPodsMetrics returns new Pod metrics cache. -func NewPodsMetrics(mx map[string]*mv1beta1.PodMetrics) *PodsMetrics { - return &PodsMetrics{mx: mx} -} - -// ListPodsMetrics returns all available PodMetrics on the cluster. -func (p *PodsMetrics) ListPodsMetrics() map[string]*mv1beta1.PodMetrics { - return p.mx -} diff --git a/internal/cache/pod_test.go b/internal/cache/pod_test.go index 1221d291..f1dde35a 100644 --- a/internal/cache/pod_test.go +++ b/internal/cache/pod_test.go @@ -1,385 +1,75 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright Authors of Popeye -package cache +package cache_test import ( - "sort" "sync" "testing" "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/cache" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/test" "github.com/stretchr/testify/assert" v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -func TestGetPod(t *testing.T) { - pods := map[string]*v1.Pod{ - "default/p1": makePodLabels("p1", map[string]string{"a": "a", "b": "b", "c": "c"}), - "default/p2": makePodLabels("p2", map[string]string{"a": "a", "b": "b"}), - "default/p3": makePodLabels("p3", map[string]string{"a": "c"}), - } +func TestPodRef(t *testing.T) { + dba, err := test.NewTestDB() + assert.NoError(t, err) + l := db.NewLoader(dba) + + ctx := test.MakeCtx(t) + assert.NoError(t, test.LoadDB[*v1.Pod](ctx, l.DB, "core/pod/1.yaml", internal.Glossary[internal.PO])) + + cr := cache.NewPod(dba) + var refs sync.Map + assert.NoError(t, cr.PodRefs(&refs)) uu := map[string]struct { - sel map[string]string - e string + k string + vv []string }{ - "noSelector": { - sel: map[string]string{}, + "ns": { + k: "ns", }, - "p1": { - sel: map[string]string{"a": "a", "b": "b", "c": "c"}, - e: "default/p1", + "cm1-env": { + k: "cm:default/cm1", + vv: []string{"blee", "ns"}, }, - "p3": { - sel: map[string]string{"a": "c"}, - e: "default/p3", + "cm3-vol": { + k: "cm:default/cm3", + vv: []string{"k1", "k2", "k3", "k4"}, }, - "none": { - sel: map[string]string{"a": "x"}, + "cm4-env-from": { + k: "cm:default/cm4", }, - } - - p := NewPod(pods) - for k := range uu { - u := uu[k] - t.Run(k, func(t *testing.T) { - po := p.GetPod("default", u.sel) - if po == nil { - assert.Equal(t, u.e, "") - } else { - assert.Equal(t, u.e, MetaFQN(po.ObjectMeta)) - } - }) - } -} - -func TestListPodsBySelector(t *testing.T) { - pods := map[string]*v1.Pod{ - "default/p1": makePodLabels("p1", map[string]string{"a": "a", "b": "b"}), - "default/p2": makePodLabels("p2", map[string]string{"a": "a", "b": "b"}), - "default/p3": makePodLabels("p3", map[string]string{"a": "c"}), - } - - uu := map[string]struct { - sel *metav1.LabelSelector - e []string - }{ - "noSelector": { - nil, - []string{}, + "sec1-vol": { + k: "sec:default/sec1", + vv: []string{"k1"}, }, - "p1p2": { - &metav1.LabelSelector{MatchLabels: map[string]string{"a": "a"}}, - []string{"default/p1", "default/p2"}, + "sec2-env": { + k: "sec:default/sec2", + vv: []string{"ca.crt", "fred", "k1", "namespace"}, }, - "p3": { - &metav1.LabelSelector{MatchLabels: map[string]string{"a": "c"}}, - []string{"default/p3"}, + "sec3-img-pull": { + k: "sec:default/sec3", }, - "none": { - &metav1.LabelSelector{MatchLabels: map[string]string{"a": "x"}}, - []string{}, + "sec4-env-from": { + k: "sec:default/sec4", }, } - p := NewPod(pods) for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { - res := p.ListPodsBySelector("default", u.sel) - keys := []string{} - for k := range res { - keys = append(keys, k) + m, ok := refs.Load(u.k) + assert.True(t, ok) + for _, k := range u.vv { + _, ok = m.(internal.StringSet)[k] + assert.True(t, ok) } - sort.Strings(keys) - assert.Equal(t, u.e, keys) }) } } - -func TestPodRefsVolume(t *testing.T) { - pods := map[string]*v1.Pod{ - "default/p1": makePodVolume("p1", "cm1", "s1", false), - "default/p2": makePodVolume("p2", "cm2", "s2", true), - "default/p3": makePodVolume("p3", "cm2", "s2", false), - } - - p := NewPod(pods) - - var refs sync.Map - p.PodRefs(&refs) - - ii, ok := refs.Load("cm:default/cm1") - assert.True(t, ok) - assert.Equal(t, 2, len(ii.(internal.StringSet))) - - ii, ok = refs.Load("cm:default/cm2") - assert.True(t, ok) - assert.Equal(t, 2, len(ii.(internal.StringSet))) - - ii, ok = refs.Load("sec:default/s1") - assert.True(t, ok) - assert.Equal(t, 1, len(ii.(internal.StringSet))) - - ii, ok = refs.Load("sec:default/s2") - assert.True(t, ok) - assert.Equal(t, 1, len(ii.(internal.StringSet))) - - ii, ok = refs.Load("ns") - assert.True(t, ok) - assert.Equal(t, 1, len(ii.(internal.StringSet))) -} - -func TestPodRefsEnvFrom(t *testing.T) { - pods := map[string]*v1.Pod{ - "default/p1": makePodEnvFrom("p1", "r1", false), - "default/p2": makePodEnvFrom("p2", "r2", true), - "default/p3": makePodEnvFrom("p3", "r1", false), - } - - p := NewPod(pods) - - var refs sync.Map - p.PodRefs(&refs) - - ii, ok := refs.Load("cm:default/r1") - assert.True(t, ok) - assert.Equal(t, 1, len(ii.(internal.StringSet))) - - ii, ok = refs.Load("cm:default/r2") - assert.True(t, ok) - assert.Equal(t, 1, len(ii.(internal.StringSet))) - - ii, ok = refs.Load("sec:default/r1") - assert.True(t, ok) - assert.Equal(t, 1, len(ii.(internal.StringSet))) - - ii, ok = refs.Load("sec:default/r2") - assert.True(t, ok) - assert.Equal(t, 1, len(ii.(internal.StringSet))) -} - -func TestPodRefsEnv(t *testing.T) { - pods := map[string]*v1.Pod{ - "default/p1": makePodEnv("p1", "r1", false), - "default/p2": makePodEnv("p2", "r2", true), - } - p := NewPod(pods) - var refs sync.Map - p.PodRefs(&refs) - - ii, ok := refs.Load("cm:default/r1") - assert.True(t, ok) - assert.Equal(t, 2, len(ii.(internal.StringSet))) - - ii, ok = refs.Load("cm:default/r2") - assert.True(t, ok) - assert.Equal(t, 2, len(ii.(internal.StringSet))) - - ii, ok = refs.Load("sec:default/r1") - assert.True(t, ok) - assert.Equal(t, 1, len(ii.(internal.StringSet))) - - ii, ok = refs.Load("sec:default/r2") - assert.True(t, ok) - assert.Equal(t, 1, len(ii.(internal.StringSet))) -} - -func TestPodPullImageSecrets(t *testing.T) { - pods := map[string]*v1.Pod{ - "default/p1": makePodPull("p1", "r1", false), - "default/p2": makePodPull("p2", "r2", true), - } - - p := NewPod(pods) - var refs sync.Map - p.PodRefs(&refs) - - ii, ok := refs.Load("cm:default/r1") - assert.True(t, ok) - assert.Equal(t, 2, len(ii.(internal.StringSet))) - - ii, ok = refs.Load("cm:default/r2") - assert.True(t, ok) - assert.Equal(t, 2, len(ii.(internal.StringSet))) - - ii, ok = refs.Load("sec:default/s1") - assert.True(t, ok) - assert.Equal(t, 1, len(ii.(internal.StringSet))) - - ii, ok = refs.Load("sec:default/s2") - assert.True(t, ok) - assert.Equal(t, 1, len(ii.(internal.StringSet))) -} - -func TestNamespaced(t *testing.T) { - uu := []struct { - s, ens, en string - }{ - {"fred/blee", "fred", "blee"}, - {"blee", "", "blee"}, - } - - for _, u := range uu { - ns, n := namespaced(u.s) - assert.Equal(t, u.ens, ns) - assert.Equal(t, u.en, n) - } -} - -// ---------------------------------------------------------------------------- -// Helpers... - -func makePodVolume(n, cm, sec string, optional bool) *v1.Pod { - po := makePod(n) - po.Spec.Volumes = []v1.Volume{ - { - Name: "v1", - VolumeSource: v1.VolumeSource{ - ConfigMap: &v1.ConfigMapVolumeSource{ - LocalObjectReference: v1.LocalObjectReference{ - Name: cm, - }, - Items: []v1.KeyToPath{ - {Key: "k1"}, - {Key: "k2"}, - }, - Optional: &optional, - }, - }, - }, - { - Name: "v2", - VolumeSource: v1.VolumeSource{ - Secret: &v1.SecretVolumeSource{ - SecretName: sec, - Optional: &optional, - }, - }, - }, - } - - return po -} - -func makePodPull(n, ref string, optional bool) *v1.Pod { - po := makePodEnv(n, ref, optional) - - po.Spec.ImagePullSecrets = []v1.LocalObjectReference{ - {Name: "s1"}, - {Name: "s2"}, - } - - return po -} - -func makePodEnv(n, ref string, optional bool) *v1.Pod { - po := makePod(n) - po.Spec.Containers = []v1.Container{ - { - Name: "c1", - Env: []v1.EnvVar{ - { - Name: "e1", - ValueFrom: &v1.EnvVarSource{ - ConfigMapKeyRef: &v1.ConfigMapKeySelector{ - LocalObjectReference: v1.LocalObjectReference{ - Name: ref, - }, - Key: "k1", - Optional: &optional, - }, - }, - }, - }, - }, - { - Name: "c2", - Env: []v1.EnvVar{ - { - Name: "e2", - ValueFrom: &v1.EnvVarSource{ - ConfigMapKeyRef: &v1.ConfigMapKeySelector{ - LocalObjectReference: v1.LocalObjectReference{ - Name: ref, - }, - Key: "k2", - Optional: &optional, - }, - }, - }, - }, - }, - } - po.Spec.InitContainers = []v1.Container{ - { - Name: "ic1", - Env: []v1.EnvVar{ - { - Name: "e1", - ValueFrom: &v1.EnvVarSource{ - SecretKeyRef: &v1.SecretKeySelector{ - LocalObjectReference: v1.LocalObjectReference{Name: ref}, - Key: "k2", - Optional: &optional, - }, - }, - }, - }, - }, - } - - return po -} - -func makePodEnvFrom(n, cm string, optional bool) *v1.Pod { - po := makePod(n) - po.Spec.Containers = []v1.Container{ - { - Name: "c1", - EnvFrom: []v1.EnvFromSource{ - { - ConfigMapRef: &v1.ConfigMapEnvSource{ - LocalObjectReference: v1.LocalObjectReference{Name: cm}, - Optional: &optional, - }, - }, - }, - }, - } - po.Spec.InitContainers = []v1.Container{ - { - Name: "ic1", - EnvFrom: []v1.EnvFromSource{ - { - SecretRef: &v1.SecretEnvSource{ - LocalObjectReference: v1.LocalObjectReference{Name: cm}, - Optional: &optional, - }, - }, - }, - }, - } - - return po -} - -func makePod(n string) *v1.Pod { - po := v1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Name: n, - Namespace: "default", - }, - } - - return &po -} - -func makePodLabels(n string, labels map[string]string) *v1.Pod { - po := makePod(n) - po.ObjectMeta.Labels = labels - - return po -} diff --git a/internal/cache/pv.go b/internal/cache/pv.go deleted file mode 100644 index 24303d33..00000000 --- a/internal/cache/pv.go +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package cache - -import ( - v1 "k8s.io/api/core/v1" -) - -// PersistentVolume represents a collection of PersistentVolumes available on a cluster. -type PersistentVolume struct { - pvs map[string]*v1.PersistentVolume -} - -// NewPersistentVolume returns a new PersistentVolume. -func NewPersistentVolume(pvs map[string]*v1.PersistentVolume) *PersistentVolume { - return &PersistentVolume{pvs} -} - -// ListPersistentVolumes returns all available PersistentVolumes on the cluster. -func (p *PersistentVolume) ListPersistentVolumes() map[string]*v1.PersistentVolume { - return p.pvs -} diff --git a/internal/cache/pvc.go b/internal/cache/pvc.go deleted file mode 100644 index d64dc7f8..00000000 --- a/internal/cache/pvc.go +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package cache - -import ( - v1 "k8s.io/api/core/v1" -) - -// PersistentVolumeClaim represents a collection of PersistentVolumeClaims available on a cluster. -type PersistentVolumeClaim struct { - pvcs map[string]*v1.PersistentVolumeClaim -} - -// NewPersistentVolumeClaim returns a new PersistentVolumeClaim. -func NewPersistentVolumeClaim(pvcs map[string]*v1.PersistentVolumeClaim) *PersistentVolumeClaim { - return &PersistentVolumeClaim{pvcs} -} - -// ListPersistentVolumeClaims returns all available PersistentVolumeClaims on the cluster. -func (p *PersistentVolumeClaim) ListPersistentVolumeClaims() map[string]*v1.PersistentVolumeClaim { - return p.pvcs -} diff --git a/internal/cache/rb.go b/internal/cache/rb.go index f5f89194..b9b4b426 100644 --- a/internal/cache/rb.go +++ b/internal/cache/rb.go @@ -8,6 +8,8 @@ import ( "sync" "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/client" + "github.com/derailed/popeye/internal/db" rbacv1 "k8s.io/api/rbac/v1" ) @@ -16,23 +18,26 @@ const RoleKey = "role" // RoleBinding represents RoleBinding cache. type RoleBinding struct { - rbs map[string]*rbacv1.RoleBinding + db *db.DB } // NewRoleBinding returns a new RoleBinding cache. -func NewRoleBinding(rbs map[string]*rbacv1.RoleBinding) *RoleBinding { - return &RoleBinding{rbs: rbs} -} - -// ListRoleBindings returns all available RoleBindings on the cluster. -func (r *RoleBinding) ListRoleBindings() map[string]*rbacv1.RoleBinding { - return r.rbs +func NewRoleBinding(db *db.DB) *RoleBinding { + return &RoleBinding{db: db} } // RoleRefs computes all role external references. func (r *RoleBinding) RoleRefs(refs *sync.Map) { - for fqn, rb := range r.rbs { - key := ResFqn(strings.ToLower(rb.RoleRef.Kind), FQN(rb.Namespace, rb.RoleRef.Name)) + txn, it := r.db.MustITFor(internal.Glossary[internal.ROB]) + defer txn.Abort() + for o := it.Next(); o != nil; o = it.Next() { + rb := o.(*rbacv1.RoleBinding) + fqn := client.FQN(rb.Namespace, rb.Name) + cfqn := FQN(rb.Namespace, rb.RoleRef.Name) + if rb.RoleRef.Kind == "ClusterRole" { + cfqn = client.FQN("", rb.RoleRef.Name) + } + key := ResFqn(strings.ToLower(rb.RoleRef.Kind), cfqn) if c, ok := refs.LoadOrStore(key, internal.StringSet{fqn: internal.Blank}); ok { c.(internal.StringSet).Add(fqn) } diff --git a/internal/cache/rb_test.go b/internal/cache/rb_test.go index fbd9b433..2596f79e 100644 --- a/internal/cache/rb_test.go +++ b/internal/cache/rb_test.go @@ -9,41 +9,31 @@ import ( "github.com/derailed/popeye/internal" "github.com/derailed/popeye/internal/cache" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/test" "github.com/stretchr/testify/assert" rbacv1 "k8s.io/api/rbac/v1" ) func TestRoleRef(t *testing.T) { - cr := cache.NewRoleBinding(makeRBMap()) + dba, err := test.NewTestDB() + assert.NoError(t, err) + l := db.NewLoader(dba) + + ctx := test.MakeCtx(t) + assert.NoError(t, test.LoadDB[*rbacv1.RoleBinding](ctx, l.DB, "auth/rob/1.yaml", internal.Glossary[internal.ROB])) + + cr := cache.NewRoleBinding(dba) var refs sync.Map cr.RoleRefs(&refs) - m, ok := refs.Load("clusterrole:cr1") + m, ok := refs.Load("clusterrole:cr-bozo") assert.True(t, ok) - _, ok = m.(internal.StringSet)["rb1"] + _, ok = m.(internal.StringSet)["default/rb3"] assert.True(t, ok) - m, ok = refs.Load("role:blee/r1") + m, ok = refs.Load("role:default/r1") assert.True(t, ok) - _, ok = m.(internal.StringSet)["rb2"] + _, ok = m.(internal.StringSet)["default/rb1"] assert.True(t, ok) } - -// Helpers... - -func makeRBMap() map[string]*rbacv1.RoleBinding { - return map[string]*rbacv1.RoleBinding{ - "rb1": makeRB("", "r1", "ClusterRole", "cr1"), - "rb2": makeRB("blee", "r2", "Role", "r1"), - } -} - -func makeRB(ns, name, kind, refName string) *rbacv1.RoleBinding { - return &rbacv1.RoleBinding{ - ObjectMeta: makeObjMeta(ns, name), - RoleRef: rbacv1.RoleRef{ - Kind: kind, - Name: refName, - }, - } -} diff --git a/internal/cache/role.go b/internal/cache/role.go deleted file mode 100644 index f10f5070..00000000 --- a/internal/cache/role.go +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package cache - -import ( - rbacv1 "k8s.io/api/rbac/v1" -) - -// Role represents Role cache. -type Role struct { - ros map[string]*rbacv1.Role -} - -// NewRole returns a new Role cache. -func NewRole(ros map[string]*rbacv1.Role) *Role { - return &Role{ros: ros} -} - -// ListRoles returns all available Roles on the cluster. -func (r *Role) ListRoles() map[string]*rbacv1.Role { - return r.ros -} diff --git a/internal/cache/rs.go b/internal/cache/rs.go deleted file mode 100644 index 071ef4a5..00000000 --- a/internal/cache/rs.go +++ /dev/null @@ -1,26 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package cache - -import ( - appsv1 "k8s.io/api/apps/v1" -) - -// ReplicaSetKey tracks ReplicaSet resource references -const ReplicaSetKey = "ds" - -// ReplicaSet represents ReplicaSet cache. -type ReplicaSet struct { - rss map[string]*appsv1.ReplicaSet -} - -// NewReplicaSet returns a new ReplicaSet cache. -func NewReplicaSet(rss map[string]*appsv1.ReplicaSet) *ReplicaSet { - return &ReplicaSet{rss: rss} -} - -// ListReplicaSets returns all available ReplicaSets on the cluster. -func (d *ReplicaSet) ListReplicaSets() map[string]*appsv1.ReplicaSet { - return d.rss -} diff --git a/internal/cache/sa.go b/internal/cache/sa.go index bd5c4ccf..258e4e73 100644 --- a/internal/cache/sa.go +++ b/internal/cache/sa.go @@ -4,38 +4,44 @@ package cache import ( + "errors" "sync" "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" v1 "k8s.io/api/core/v1" ) // ServiceAccount tracks serviceaccounts. type ServiceAccount struct { - sas map[string]*v1.ServiceAccount + db *db.DB } // NewServiceAccount returns a new serviceaccount loader. -func NewServiceAccount(sas map[string]*v1.ServiceAccount) *ServiceAccount { - return &ServiceAccount{sas: sas} -} - -// ListServiceAccounts list available ServiceAccounts. -func (s *ServiceAccount) ListServiceAccounts() map[string]*v1.ServiceAccount { - return s.sas +func NewServiceAccount(db *db.DB) *ServiceAccount { + return &ServiceAccount{db: db} } // ServiceAccountRefs computes all serviceaccount external references. -func (s *ServiceAccount) ServiceAccountRefs(refs *sync.Map) { - for _, sa := range s.sas { +func (s *ServiceAccount) ServiceAccountRefs(refs *sync.Map) error { + txn, it := s.db.MustITFor(internal.Glossary[internal.SA]) + defer txn.Abort() + for o := it.Next(); o != nil; o = it.Next() { + sa, ok := o.(*v1.ServiceAccount) + if !ok { + return errors.New("expected sa") + } + namespaceRefs(sa.Namespace, refs) for _, s := range sa.Secrets { key := ResFqn(SecretKey, FQN(s.Namespace, s.Name)) refs.Store(key, internal.AllKeys) } - for _, s := range sa.ImagePullSecrets { key := ResFqn(SecretKey, FQN(sa.Namespace, s.Name)) refs.Store(key, internal.AllKeys) } + } + + return nil } diff --git a/internal/cache/sa_test.go b/internal/cache/sa_test.go index 67ae8ead..b0c613d5 100644 --- a/internal/cache/sa_test.go +++ b/internal/cache/sa_test.go @@ -8,24 +8,35 @@ import ( "testing" "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/test" "github.com/stretchr/testify/assert" v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) func TestServiceAccountRefs(t *testing.T) { + dba, err := test.NewTestDB() + assert.NoError(t, err) + l := db.NewLoader(dba) + + ctx := test.MakeCtx(t) + assert.NoError(t, test.LoadDB[*v1.ServiceAccount](ctx, l.DB, "core/sa/1.yaml", internal.Glossary[internal.SA])) + uu := []struct { keys []string }{ - {[]string{"sec:default/s1", "sec:default/is1"}}, + { + []string{ + "sec:default/s1", + "sec:default/bozo", + }, + }, } - sa := NewServiceAccount(map[string]*v1.ServiceAccount{ - "default/sa1": makeSASecrets("sa1"), - }) + var refs sync.Map + sa := NewServiceAccount(dba) + assert.NoError(t, sa.ServiceAccountRefs(&refs)) for _, u := range uu { - var refs sync.Map - sa.ServiceAccountRefs(&refs) for _, k := range u.keys { v, ok := refs.Load(k) assert.True(t, ok) @@ -33,33 +44,3 @@ func TestServiceAccountRefs(t *testing.T) { } } } - -// ---------------------------------------------------------------------------- -// Helpers... - -func makeSASecrets(n string) *v1.ServiceAccount { - sa := makeSA(n) - sa.Secrets = []v1.ObjectReference{ - { - Kind: "Secret", - Name: "s1", - Namespace: "default", - }, - } - sa.ImagePullSecrets = []v1.LocalObjectReference{ - { - Name: "is1", - }, - } - - return sa -} - -func makeSA(n string) *v1.ServiceAccount { - return &v1.ServiceAccount{ - ObjectMeta: metav1.ObjectMeta{ - Name: n, - Namespace: "default", - }, - } -} diff --git a/internal/cache/sec.go b/internal/cache/sec.go deleted file mode 100644 index 90b89dcb..00000000 --- a/internal/cache/sec.go +++ /dev/null @@ -1,26 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package cache - -import ( - v1 "k8s.io/api/core/v1" -) - -// SecretKey tracks Secret resource references -const SecretKey = "sec" - -// Secret represents a collection of Secrets available on a cluster. -type Secret struct { - secrets map[string]*v1.Secret -} - -// NewSecret returns a new Secret cache. -func NewSecret(ss map[string]*v1.Secret) *Secret { - return &Secret{ss} -} - -// ListSecrets returns all available Secrets on the cluster. -func (s *Secret) ListSecrets() map[string]*v1.Secret { - return s.secrets -} diff --git a/internal/cache/sts.go b/internal/cache/sts.go deleted file mode 100644 index 353d48cc..00000000 --- a/internal/cache/sts.go +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package cache - -import ( - appsv1 "k8s.io/api/apps/v1" -) - -// StatefulSet represents a collection of StatefulSets available on a cluster. -type StatefulSet struct { - sts map[string]*appsv1.StatefulSet -} - -// NewStatefulSet returns a new StatefulSet. -func NewStatefulSet(sts map[string]*appsv1.StatefulSet) *StatefulSet { - return &StatefulSet{sts} -} - -// ListStatefulSets returns all available StatefulSets on the cluster. -func (s *StatefulSet) ListStatefulSets() map[string]*appsv1.StatefulSet { - return s.sts -} diff --git a/internal/cache/svc.go b/internal/cache/svc.go deleted file mode 100644 index 20c762bd..00000000 --- a/internal/cache/svc.go +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package cache - -import ( - v1 "k8s.io/api/core/v1" -) - -// Service represents a collection of Services available on a cluster. -type Service struct { - svcs map[string]*v1.Service -} - -// NewService returns a new Service. -func NewService(svcs map[string]*v1.Service) *Service { - return &Service{svcs} -} - -// ListServices returns all available Services on the cluster. -func (s *Service) ListServices() map[string]*v1.Service { - return s.svcs -} diff --git a/internal/cache/testdata/auth/crb/1.yaml b/internal/cache/testdata/auth/crb/1.yaml new file mode 100644 index 00000000..0193807b --- /dev/null +++ b/internal/cache/testdata/auth/crb/1.yaml @@ -0,0 +1,43 @@ +--- +apiVersion: v1 +kind: List +items: + - apiVersion: rbac.authorization.k8s.io/v1 + kind: ClusterRoleBinding + metadata: + name: crb1 + subjects: + - kind: User + name: fred + apiGroup: rbac.authorization.k8s.io + roleRef: + kind: ClusterRole + name: cr1 + apiGroup: rbac.authorization.k8s.io + - apiVersion: rbac.authorization.k8s.io/v1 + kind: ClusterRoleBinding + metadata: + name: crb2 + subjects: + - kind: ServiceAccount + name: sa2 + namespace: default + apiGroup: rbac.authorization.k8s.io + roleRef: + kind: ClusterRole + name: cr-bozo + apiGroup: rbac.authorization.k8s.io + - apiVersion: rbac.authorization.k8s.io/v1 + kind: ClusterRoleBinding + metadata: + name: crb3 + subjects: + - kind: ServiceAccount + name: sa-bozo + namespace: default + apiGroup: rbac.authorization.k8s.io + roleRef: + kind: Role + name: r1 + namespace: blee + apiGroup: rbac.authorization.k8s.io \ No newline at end of file diff --git a/internal/cache/testdata/auth/rob/1.yaml b/internal/cache/testdata/auth/rob/1.yaml new file mode 100644 index 00000000..f338fe2c --- /dev/null +++ b/internal/cache/testdata/auth/rob/1.yaml @@ -0,0 +1,43 @@ +--- +apiVersion: v1 +kind: List +items: + - apiVersion: rbac.authorization.k8s.io/v1 + kind: RoleBinding + metadata: + name: rb1 + namespace: default + subjects: + - kind: User + name: fred + apiGroup: rbac.authorization.k8s.io + roleRef: + kind: Role + name: r1 + apiGroup: rbac.authorization.k8s.io + - apiVersion: rbac.authorization.k8s.io/v1 + kind: RoleBinding + metadata: + name: rb2 + namespace: default + subjects: + - kind: ServiceAccount + name: sa-bozo + apiGroup: rbac.authorization.k8s.io + roleRef: + kind: Role + name: r-bozo + apiGroup: rbac.authorization.k8s.io + - apiVersion: rbac.authorization.k8s.io/v1 + kind: RoleBinding + metadata: + name: rb3 + namespace: default + subjects: + - kind: ServiceAccount + name: sa-bozo + apiGroup: rbac.authorization.k8s.io + roleRef: + kind: ClusterRole + name: cr-bozo + apiGroup: rbac.authorization.k8s.io \ No newline at end of file diff --git a/internal/cache/testdata/core/node/1.yaml b/internal/cache/testdata/core/node/1.yaml new file mode 100644 index 00000000..cd0384cc --- /dev/null +++ b/internal/cache/testdata/core/node/1.yaml @@ -0,0 +1,71 @@ +apiVersion: v1 +kind: List +items: +- apiVersion: v1 + kind: Node + metadata: + labels: + node-role.kubernetes.io/control-plane: "" + node-role.kubernetes.io/master: "" + node.kubernetes.io/exclude-from-external-load-balancers: "" + name: n1 + spec: + podCIDR: 10.244.0.0/24 + podCIDRs: + - 10.244.0.0/24 + status: + addresses: + - address: 192.168.228.3 + type: InternalIP + - address: dashb-control-plane + type: Hostname + allocatable: + cpu: 4 + ephemeral-storage: 816748224Ki + memory: 400Mi + pods: "110" + capacity: + cpu: "10" + ephemeral-storage: 816748224Ki + memory: 8124744Ki + pods: "110" + conditions: + - lastHeartbeatTime: "2024-01-27T15:31:39Z" + lastTransitionTime: "2024-01-03T20:35:11Z" + message: kubelet has sufficient memory available + reason: KubeletHasSufficientMemory + status: "False" + type: MemoryPressure + - lastHeartbeatTime: "2024-01-27T15:31:39Z" + lastTransitionTime: "2024-01-03T20:35:11Z" + message: kubelet has no disk pressure + reason: KubeletHasNoDiskPressure + status: "False" + type: DiskPressure + - lastHeartbeatTime: "2024-01-27T15:31:39Z" + lastTransitionTime: "2024-01-03T20:35:11Z" + message: kubelet has sufficient PID available + reason: KubeletHasSufficientPID + status: "False" + type: PIDPressure + - lastHeartbeatTime: "2024-01-27T15:31:39Z" + lastTransitionTime: "2024-01-03T20:35:38Z" + message: kubelet is posting ready status + reason: KubeletReady + status: "True" + type: Ready + daemonEndpoints: + kubeletEndpoint: + Port: 10250 + images: + nodeInfo: + architecture: arm64 + bootID: 0836e65d-3091-4cb5-8ad4-8f65425f87e3 + containerRuntimeVersion: containerd://1.5.1 + kernelVersion: 6.5.10-orbstack-00110-gbcfe04c86d2f + kubeProxyVersion: v1.21.1 + kubeletVersion: v1.21.1 + machineID: 6bbc44bb821d48b995092021d706d8e6 + operatingSystem: linux + osImage: Ubuntu 20.10 + systemUUID: 6bbc44bb821d48b995092021d706d8e6 diff --git a/internal/cache/testdata/core/pod/1.yaml b/internal/cache/testdata/core/pod/1.yaml new file mode 100644 index 00000000..0e7f8910 --- /dev/null +++ b/internal/cache/testdata/core/pod/1.yaml @@ -0,0 +1,138 @@ +--- +apiVersion: v1 +kind: List +items: +- apiVersion: v1 + kind: Pod + metadata: + name: p1 + namespace: default + labels: + app: p1 + spec: + serviceAccountName: default + tolerations: + - key: t1 + operator: Exists + effect: NoSchedule + containers: + - name: c1 + image: alpine + resources: + requests: + cpu: 1 + memory: 1Mi + limits: + cpu: 1 + memory: 1Mi + ports: + - containerPort: 9090 + name: http + protocol: TCP + env: + - name: env1 + valueFrom: + configMapKeyRef: + name: cm1 + key: ns + - name: env2 + valueFrom: + secretKeyRef: + name: sec1 + key: k1 + volumeMounts: + - name: config + mountPath: "/config" + readOnly: true + volumes: + - name: mypd + persistentVolumeClaim: + claimName: pvc1 + - name: config + configMap: + name: cm3 + items: + - key: k1 + path: "game.properties" + - key: k2 + path: "user-interface.properties" + - name: secret + secret: + secretName: sec2 + optional: false + items: + - key: fred + path: blee +- apiVersion: v1 + kind: Pod + metadata: + name: p2 + namespace: default + labels: + app: p2 + spec: + serviceAccountName: default + imagePullSecrets: + - name: sec3 + tolerations: + - key: t1 + operator: Exists + effect: NoSchedule + initContainers: + - name: ic1 + image: fred + containers: + - name: c1 + image: alpine + resources: + requests: + cpu: 1 + memory: 1Mi + limits: + cpu: 1 + memory: 1Mi + ports: + - containerPort: 9090 + name: http + protocol: TCP + envFrom: + - configMapRef: + name: cm4 + - secretRef: + name: sec4 + env: + - name: env1 + valueFrom: + configMapKeyRef: + name: cm1 + key: blee + - name: env2 + valueFrom: + secretKeyRef: + name: sec2 + key: k1 + volumeMounts: + - name: config + mountPath: "/config" + readOnly: true + volumes: + - name: mypd + persistentVolumeClaim: + claimName: pvc1 + - name: config + configMap: + name: cm3 + items: + - key: k3 + path: blee + - key: k4 + path: zorg + - name: secret + secret: + secretName: sec2 + optional: false + items: + - key: ca.crt + path: "game.properties" + - key: namespace + path: "user-interface.properties" diff --git a/internal/cache/testdata/core/pod/2.yaml b/internal/cache/testdata/core/pod/2.yaml new file mode 100644 index 00000000..933ac556 --- /dev/null +++ b/internal/cache/testdata/core/pod/2.yaml @@ -0,0 +1,178 @@ +--- +apiVersion: v1 +kind: List +items: + - apiVersion: v1 + kind: Pod + metadata: + name: p1 + namespace: default + labels: + app: p1 + ownerReferences: + - apiVersion: apps/v1 + controller: true + kind: ReplicaSet + name: rs1 + spec: + serviceAccountName: sa1 + automountServiceAccountToken: false + status: + conditions: + phase: Running + - apiVersion: v1 + kind: Pod + metadata: + name: p2 + namespace: default + labels: + app: test2 + spec: + serviceAccountName: sa2 + - apiVersion: v1 + kind: Pod + metadata: + name: p3 + namespace: default + labels: + app: p3 + ownerReferences: + - apiVersion: apps/v1 + controller: true + kind: DaemonSet + name: rs3 + spec: + serviceAccountName: sa3 + containers: + - image: dorker.io/blee:1.0.1 + name: c1 + resources: + limits: + cpu: 1 + mem: 1Mi + livenessProbe: + httpGet: + path: /healthz + port: 8080 + initialDelaySeconds: 3 + periodSeconds: 3 + readinessProbe: + httpGet: + path: /healthz + port: 8080 + initialDelaySeconds: 3 + periodSeconds: 3 + status: + conditions: + - status: "False" + type: Initialized + - status: "False" + type: Ready + - status: "False" + type: ContainersReady + - status: "False" + type: PodScheduled + phase: Running + - apiVersion: v1 + kind: Pod + metadata: + name: p4 + namespace: default + labels: + app: test4 + ownerReferences: + - apiVersion: apps/v1 + controller: false + kind: Job + name: j4 + spec: + serviceAccountName: default + automountServiceAccountToken: true + initContainers: + - image: zorg + imagePullPolicy: IfNotPresent + name: ic1 + containers: + - image: blee + imagePullPolicy: IfNotPresent + name: c1 + resources: + limits: + cpu: 1 + mem: 1Mi + livenessProbe: + httpGet: + path: /healthz + port: 8080 + initialDelaySeconds: 3 + periodSeconds: 3 + volumeMounts: + - mountPath: /etc/config + name: config-volume + readOnly: true + - mountPath: /var/run/secrets/kubernetes.io/serviceaccount + name: kube-api-access-jgtlv + readOnly: true + - image: zorg:latest + imagePullPolicy: IfNotPresent + name: c2 + resources: + requests: + mem: 1Mi + readinessProbe: + httpGet: + path: /healthz + port: p1 + initialDelaySeconds: 3 + periodSeconds: 3 + volumeMounts: + - mountPath: /etc/config + name: config-volume + readOnly: true + - mountPath: /var/run/secrets/kubernetes.io/serviceaccount + name: kube-api-access-jgtlv + readOnly: true + status: + phase: Running + conditions: + initContainerStatuses: + - containerID: ic1 + image: blee + name: ic1 + ready: false + restartCount: 1000 + started: false + containerStatuses: + - containerID: c1 + image: blee + name: c1 + ready: false + restartCount: 1000 + started: false + - containerID: c2 + name: c2 + ready: true + restartCount: 0 + started: true + - apiVersion: v1 + kind: Pod + metadata: + name: p5 + namespace: default + labels: + app: test5 + ownerReferences: + - apiVersion: apps/v1 + controller: true + kind: ReplicaSet + name: rs5 + spec: + serviceAccountName: sa5 + automountServiceAccountToken: true + containers: + - image: blee:v1.2 + imagePullPolicy: IfNotPresent + name: c1 + status: + conditions: + phase: Running diff --git a/internal/cache/testdata/core/sa/1.yaml b/internal/cache/testdata/core/sa/1.yaml new file mode 100644 index 00000000..a6ff6fad --- /dev/null +++ b/internal/cache/testdata/core/sa/1.yaml @@ -0,0 +1,54 @@ +--- +apiVersion: v1 +kind: List +items: + - apiVersion: v1 + kind: ServiceAccount + metadata: + name: default + namespace: default + - apiVersion: v1 + kind: ServiceAccount + metadata: + name: sa1 + namespace: default + automountServiceAccountToken: false + - apiVersion: v1 + kind: ServiceAccount + metadata: + name: sa2 + namespace: default + automountServiceAccountToken: true + - apiVersion: v1 + kind: ServiceAccount + metadata: + name: sa3 + namespace: default + automountServiceAccountToken: true + - apiVersion: v1 + kind: ServiceAccount + metadata: + name: sa4 + namespace: default + automountServiceAccountToken: false + secrets: + - kind: Secret + namespace: default + name: bozo + apiVersion: v1 + imagePullSecrets: + - name: s1 + namespace: fred + - apiVersion: v1 + kind: ServiceAccount + metadata: + name: sa5 + namespace: default + automountServiceAccountToken: false + secrets: + - kind: Secret + namespace: default + name: s1 + apiVersion: v1 + imagePullSecrets: + - name: bozo diff --git a/internal/cache/testdata/mx/node/1.yaml b/internal/cache/testdata/mx/node/1.yaml new file mode 100644 index 00000000..bcf0eab5 --- /dev/null +++ b/internal/cache/testdata/mx/node/1.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: List +items: +- apiVersion: metrics.k8s.io/v1beta1 + kind: NodeMetrics + metadata: + name: n1 + usage: + cpu: 2 + memory: 200Mi diff --git a/internal/cache/testdata/net/ingress/1.yaml b/internal/cache/testdata/net/ingress/1.yaml new file mode 100644 index 00000000..48b5892f --- /dev/null +++ b/internal/cache/testdata/net/ingress/1.yaml @@ -0,0 +1,111 @@ +apiVersion: v1 +kind: List +items: +- apiVersion: networking.k8s.io/v1 + kind: Ingress + metadata: + name: ing1 + namespace: default + spec: + ingressClassName: nginx + rules: + - http: + paths: + - path: /testpath + pathType: Prefix + backend: + service: + name: svc1 + port: + name: http +- apiVersion: networking.k8s.io/v1 + kind: Ingress + metadata: + name: ing2 + namespace: default + spec: + ingressClassName: nginx + tls: + - secretName: foo + rules: + - http: + paths: + - path: /testpath + pathType: Prefix + backend: + service: + name: svc1 + port: + number: 9090 +- apiVersion: networking.k8s.io/v1 + kind: Ingress + metadata: + name: ing3 + namespace: default + spec: + ingressClassName: nginx + rules: + - http: + paths: + - path: /testpath + pathType: Prefix + backend: + service: + name: s2 + port: + number: 80 +- apiVersion: networking.k8s.io/v1 + kind: Ingress + metadata: + name: ing4 + namespace: default + spec: + ingressClassName: nginx + rules: + - http: + paths: + - path: /testpath + pathType: Prefix + backend: + service: + name: svc2 +- apiVersion: networking.k8s.io/v1 + kind: Ingress + metadata: + name: ing5 + namespace: default + annotations: + spec: + ingressClassName: nginx + rules: + - http: + paths: + - path: /testpath + pathType: Prefix + backend: + resource: + apiGroup: fred.com + kind: Zorg + name: zorg + status: + loadBalancer: + ingress: + - ports: + - error: boom +- apiVersion: networking.k8s.io/v1 + kind: Ingress + metadata: + name: ing6 + namespace: default + spec: + ingressClassName: nginx + rules: + - http: + paths: + - path: /testpath + pathType: Prefix + backend: + service: + name: svc1 + port: + number: 9091 diff --git a/internal/cache/types.go b/internal/cache/types.go new file mode 100644 index 00000000..822bfa4c --- /dev/null +++ b/internal/cache/types.go @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package cache + +const ( + // SecretKey tracks Secret resource references + SecretKey = "sec" + + // ClusterRoleKey tracks ClusterRole resource references + ClusterRoleKey = "clusterrole" + + // ConfigMapKey tracks ConfigMap resource references + ConfigMapKey = "cm" +) diff --git a/internal/client/client.go b/internal/client/client.go index 13ba0382..a8f61d7d 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -45,7 +45,7 @@ type APIClient struct { mxsClient *versioned.Clientset cachedClient *disk.CachedDiscoveryClient config types.Config - mx sync.Mutex + mx sync.RWMutex cache *cache.LRUExpireCache } @@ -71,19 +71,18 @@ func InitConnectionOrDie(config types.Config) (*APIClient, error) { return &a, nil } -func makeSAR(ns, gvr string) *authorizationv1.SelfSubjectAccessReview { +func makeSAR(ns string, gvr types.GVR) *authorizationv1.SelfSubjectAccessReview { if ns == "-" { ns = "" } - spec := NewGVR(gvr) - res := spec.GVR() + res := gvr.GVR() return &authorizationv1.SelfSubjectAccessReview{ Spec: authorizationv1.SelfSubjectAccessReviewSpec{ ResourceAttributes: &authorizationv1.ResourceAttributes{ Namespace: ns, Group: res.Group, Resource: res.Resource, - Subresource: spec.SubResource(), + Subresource: gvr.SubResource(), }, }, } @@ -100,9 +99,21 @@ func (a *APIClient) ActiveContext() string { log.Error().Msgf("Unable to located active context") return "" } + return c } +// ActiveCluster returns the current cluster name. +func (a *APIClient) ActiveCluster() string { + cl, err := a.config.CurrentClusterName() + if err != nil { + log.Error().Msgf("Unable to located active cluster") + return "" + } + + return cl +} + // IsActiveNamespace returns true if namespaces matches. func (a *APIClient) IsActiveNamespace(ns string) bool { if a.ActiveNamespace() == AllNamespaces { @@ -115,8 +126,9 @@ func (a *APIClient) IsActiveNamespace(ns string) bool { func (a *APIClient) ActiveNamespace() string { ns, err := a.CurrentNamespaceName() if err != nil { - return AllNamespaces + return DefaultNamespace } + return ns } @@ -126,12 +138,19 @@ func (a *APIClient) clearCache() { } } +// ConnectionOK checks api server connection status. +func (a *APIClient) ConnectionOK() bool { + _, err := a.Dial() + + return err == nil +} + // CanI checks if user has access to a certain resource. -func (a *APIClient) CanI(ns, gvr string, verbs []string) (auth bool, err error) { +func (a *APIClient) CanI(ns string, gvr types.GVR, verbs ...string) (auth bool, err error) { if IsClusterWide(ns) { ns = AllNamespaces } - key := makeCacheKey(ns, gvr, verbs) + key := makeCacheKey(ns, gvr.String(), verbs) if v, ok := a.cache.Get(key); ok { if auth, ok = v.(bool); ok { return auth, nil @@ -291,10 +310,15 @@ func (a *APIClient) CachedDiscovery() (*disk.CachedDiscoveryClient, error) { // DynDial returns a handle to a dynamic interface. func (a *APIClient) DynDial() (dynamic.Interface, error) { + a.mx.RLock() if a.dClient != nil { + a.mx.RUnlock() return a.dClient, nil } + a.mx.RUnlock() + a.mx.Lock() + defer a.mx.Unlock() rc, err := a.RestConfig() if err != nil { return nil, err diff --git a/internal/client/config.go b/internal/client/config.go index b849a258..85cd1f3f 100644 --- a/internal/client/config.go +++ b/internal/client/config.go @@ -157,8 +157,8 @@ func (c *Config) CurrentClusterName() (string, error) { current = *c.flags.Context } - if ctx, ok := cfg.Contexts[current]; ok { - return ctx.Cluster, nil + if ct, ok := cfg.Contexts[current]; ok { + return ct.Cluster, nil } return "", errors.New("unable to locate current cluster") @@ -240,19 +240,16 @@ func (c *Config) CurrentNamespaceName() (string, error) { cfg, err := c.RawConfig() if err != nil { - return "", err + return DefaultNamespace, err } - ctx, err := c.CurrentContextName() - if err != nil { - return "", err - } - if ctx, ok := cfg.Contexts[ctx]; ok { - if isSet(&ctx.Namespace) { - return ctx.Namespace, nil + if ct, ok := cfg.Contexts[cfg.CurrentContext]; ok { + if ct.Namespace == BlankNamespace { + return DefaultNamespace, nil } + return ct.Namespace, nil } - return "", fmt.Errorf("No active namespace specified") + return DefaultNamespace, nil } // NamespaceNames fetch all available namespaces on current cluster. diff --git a/internal/client/config_test.go b/internal/client/config_test.go index 01d1a715..d00a12bb 100644 --- a/internal/client/config_test.go +++ b/internal/client/config_test.go @@ -5,21 +5,15 @@ package client_test import ( "errors" - "fmt" "testing" "github.com/derailed/popeye/internal/client" - "github.com/rs/zerolog" "github.com/stretchr/testify/assert" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/cli-runtime/pkg/genericclioptions" ) -func init() { - zerolog.SetGlobalLevel(zerolog.FatalLevel) -} - func TestConfigCurrentContext(t *testing.T) { name, kubeConfig := "blee", "./testdata/config" uu := []struct { @@ -75,21 +69,35 @@ func TestConfigCurrentUser(t *testing.T) { } func TestConfigCurrentNamespace(t *testing.T) { - name, kubeConfig := "blee", "./testdata/config" - uu := []struct { - flags *genericclioptions.ConfigFlags - namespace string - err error + ns, kubeConfig := "ns1", "./testdata/config" + uu := map[string]struct { + flags *genericclioptions.ConfigFlags + ns string + err error }{ - {&genericclioptions.ConfigFlags{KubeConfig: &kubeConfig}, "", fmt.Errorf("No active namespace specified")}, - {&genericclioptions.ConfigFlags{KubeConfig: &kubeConfig, Namespace: &name}, "blee", nil}, - } - - for _, u := range uu { - cfg := client.NewConfig(u.flags) - ns, err := cfg.CurrentNamespaceName() - assert.Equal(t, u.err, err) - assert.Equal(t, u.namespace, ns) + "open": { + flags: &genericclioptions.ConfigFlags{ + KubeConfig: &kubeConfig, + }, + ns: client.DefaultNamespace, + }, + "manual": { + flags: &genericclioptions.ConfigFlags{ + KubeConfig: &kubeConfig, + Namespace: &ns, + }, + ns: ns, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + cfg := client.NewConfig(u.flags) + ns, err := cfg.CurrentNamespaceName() + assert.Equal(t, u.err, err) + assert.Equal(t, u.ns, ns) + }) } } diff --git a/internal/client/factory.go b/internal/client/factory.go index 5c2cfc20..99d40804 100644 --- a/internal/client/factory.go +++ b/internal/client/factory.go @@ -65,8 +65,8 @@ func (f *Factory) Terminate() { } // List returns a resource collection. -func (f *Factory) List(gvr, ns string, wait bool, labels labels.Selector) ([]runtime.Object, error) { - inf, err := f.CanForResource(ns, gvr, types.MonitorAccess) +func (f *Factory) List(gvr types.GVR, ns string, wait bool, labels labels.Selector) ([]runtime.Object, error) { + inf, err := f.CanForResource(ns, gvr, types.MonitorAccess...) if err != nil { return nil, err } @@ -84,9 +84,9 @@ func (f *Factory) List(gvr, ns string, wait bool, labels labels.Selector) ([]run } // Get retrieves a given resource. -func (f *Factory) Get(gvr, path string, wait bool, sel labels.Selector) (runtime.Object, error) { +func (f *Factory) Get(gvr types.GVR, path string, wait bool, sel labels.Selector) (runtime.Object, error) { ns, n := Namespaced(path) - inf, err := f.CanForResource(ns, gvr, []string{types.GetVerb}) + inf, err := f.CanForResource(ns, gvr, types.GetVerb) if err != nil { return nil, err } @@ -166,15 +166,15 @@ func (f *Factory) isClusterWide() bool { } // CanForResource return an informer is user has access. -func (f *Factory) CanForResource(ns, gvr string, verbs []string) (informers.GenericInformer, error) { +func (f *Factory) CanForResource(ns string, gvr types.GVR, verbs ...string) (informers.GenericInformer, error) { // If user can access resource cluster wide, prefer cluster wide factory. if !IsClusterWide(ns) { - auth, err := f.Client().CanI(AllNamespaces, gvr, verbs) + auth, err := f.Client().CanI(AllNamespaces, gvr, verbs...) if auth && err == nil { return f.ForResource(AllNamespaces, gvr) } } - auth, err := f.Client().CanI(ns, gvr, verbs) + auth, err := f.Client().CanI(ns, gvr, verbs...) if err != nil { return nil, err } @@ -186,12 +186,12 @@ func (f *Factory) CanForResource(ns, gvr string, verbs []string) (informers.Gene } // ForResource returns an informer for a given resource. -func (f *Factory) ForResource(ns, gvr string) (informers.GenericInformer, error) { +func (f *Factory) ForResource(ns string, gvr types.GVR) (informers.GenericInformer, error) { fact, err := f.ensureFactory(ns) if err != nil { return nil, err } - inf := fact.ForResource(NewGVR(gvr).GVR()) + inf := fact.ForResource(gvr.GVR()) if inf == nil { log.Error().Err(fmt.Errorf("MEOW! No informer for %q:%q", ns, gvr)) return inf, nil diff --git a/internal/client/meta.go b/internal/client/meta.go deleted file mode 100644 index fd9bf1a8..00000000 --- a/internal/client/meta.go +++ /dev/null @@ -1,46 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package client - -import ( - "github.com/derailed/popeye/types" -) - -// Schema tracks resource schema. -type Schema struct { - GVR GVR - Preferred bool -} - -// Meta tracks a collection of resources. -type Meta map[string][]Schema - -func newMeta() Meta { - return make(map[string][]Schema) -} - -// Resources tracks dictionary of resources. -var Resources = newMeta() - -// Load loads resource meta from server. -func Load(f types.Factory) error { - dial, err := f.Client().CachedDiscovery() - if err != nil { - return err - } - rr, err := dial.ServerPreferredResources() - if err != nil { - return err - } - - for _, r := range rr { - for _, res := range r.APIResources { - gvr := FromGVAndR(r.GroupVersion, res.Name) - res.Group, res.Version = gvr.G(), gvr.V() - Resources[gvr.R()] = []Schema{{GVR: gvr, Preferred: true}} - } - } - - return nil -} diff --git a/internal/client/metrics_test.go b/internal/client/metrics_test.go index 4aa6f007..695b3a61 100644 --- a/internal/client/metrics_test.go +++ b/internal/client/metrics_test.go @@ -38,7 +38,6 @@ func TestMetricsEmpty(t *testing.T) { } } -// ---------------------------------------------------------------------------- // Helpers... func toQty(s string) resource.Quantity { diff --git a/internal/client/types.go b/internal/client/types.go index c2eccbea..cec3ce8e 100644 --- a/internal/client/types.go +++ b/internal/client/types.go @@ -18,4 +18,10 @@ const ( // NotNamespaced designates a non resource namespace. NotNamespaced = "*" + + // BlankNamespace tracks an unspecified namespace. + BlankNamespace = "" + + // DefaultNamespace tracks the default namespace. + DefaultNamespace = "default" ) diff --git a/internal/context.go b/internal/context.go index dcbb62b3..4fb3a43a 100644 --- a/internal/context.go +++ b/internal/context.go @@ -6,42 +6,46 @@ package internal import ( "context" - "github.com/derailed/popeye/internal/client" + "github.com/derailed/popeye/internal/rules" + "github.com/derailed/popeye/types" ) -// RunInfo describes a sanitizer run. +// RunInfo describes a scan run. type RunInfo struct { Section string - SectionGVR client.GVR - FQN string + SectionGVR types.GVR Group string - GroupGVR client.GVR + GroupGVR types.GVR + Spec rules.Spec + Total int +} + +func NewRunInfo(gvr types.GVR) RunInfo { + return RunInfo{ + Section: gvr.R(), + SectionGVR: gvr, + } } // WithGroup adds a group to the context. -func WithGroup(ctx context.Context, gvr client.GVR, grp string) context.Context { +func WithGroup(ctx context.Context, gvr types.GVR, grp string) context.Context { r := MustExtractRunInfo(ctx) r.Group, r.GroupGVR = grp, gvr - return context.WithValue(ctx, KeyRunInfo, r) -} -// WithFQN adds a fqn to the context. -func WithFQN(ctx context.Context, fqn string) context.Context { - r := MustExtractRunInfo(ctx) - r.FQN = fqn return context.WithValue(ctx, KeyRunInfo, r) } -// MustExtractFQN extract fqn from context or die. -func MustExtractFQN(ctx context.Context) string { +func WithSpec(ctx context.Context, spec rules.Spec) context.Context { r := MustExtractRunInfo(ctx) - return r.FQN + r.Spec = spec + + return context.WithValue(ctx, KeyRunInfo, r) } // MustExtractSectionGVR extract section gvr from context or die. -func MustExtractSectionGVR(ctx context.Context) string { +func MustExtractSectionGVR(ctx context.Context) types.GVR { r := MustExtractRunInfo(ctx) - return r.SectionGVR.String() + return r.SectionGVR } // MustExtractRunInfo extracts runinfo from context or die. @@ -52,3 +56,11 @@ func MustExtractRunInfo(ctx context.Context) RunInfo { } return r } + +func MustExtractFactory(ctx context.Context) types.Factory { + f, ok := ctx.Value(KeyFactory).(types.Factory) + if !ok { + panic("Doh! No factory in context") + } + return f +} diff --git a/internal/dag/cluster.go b/internal/dag/cluster.go index 572726f1..dfc8b07b 100644 --- a/internal/dag/cluster.go +++ b/internal/dag/cluster.go @@ -3,19 +3,28 @@ package dag -import "context" +import ( + "context" + + "github.com/Masterminds/semver" +) // ListVersion return server api version. -func ListVersion(ctx context.Context) (string, string, error) { +func ListVersion(ctx context.Context) (*semver.Version, error) { f := mustExtractFactory(ctx) dial, err := f.Client().Dial() if err != nil { - return "", "", err + return nil, err + } + info, err := dial.Discovery().ServerVersion() + if err != nil { + return nil, err } - v, err := dial.Discovery().ServerVersion() + + rev, err := semver.NewVersion(info.Major + "." + info.Minor) if err != nil { - return "", "", err + return nil, err } - return v.Major, v.Minor, nil + return rev, nil } diff --git a/internal/dag/cm.go b/internal/dag/cm.go deleted file mode 100644 index 4687d0b0..00000000 --- a/internal/dag/cm.go +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package dag - -import ( - "context" - "errors" - - "github.com/derailed/popeye/internal/client" - "github.com/derailed/popeye/internal/dao" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" -) - -// ctx := context.WithValue(context.Background(), internal.KeyFactory, f) - -// ListConfigMaps list all included ConfigMaps. -func ListConfigMaps(ctx context.Context) (map[string]*v1.ConfigMap, error) { - return listAllConfigMaps(ctx) -} - -// ListAllConfigMaps fetch all ConfigMaps on the cluster. -func listAllConfigMaps(ctx context.Context) (map[string]*v1.ConfigMap, error) { - ll, err := fetchConfigMaps(ctx) - if err != nil { - return nil, err - } - cms := make(map[string]*v1.ConfigMap, len(ll.Items)) - for i := range ll.Items { - cms[metaFQN(ll.Items[i].ObjectMeta)] = &ll.Items[i] - } - - return cms, nil -} - -// FetchConfigMaps retrieves all ConfigMaps on the cluster. -func fetchConfigMaps(ctx context.Context) (*v1.ConfigMapList, error) { - f, cfg := mustExtractFactory(ctx), mustExtractConfig(ctx) - if cfg.Flags.StandAlone { - dial, err := f.Client().Dial() - if err != nil { - return nil, err - } - return dial.CoreV1().ConfigMaps(f.Client().ActiveNamespace()).List(ctx, metav1.ListOptions{}) - } - - var res dao.Resource - res.Init(f, client.NewGVR("v1/configmaps")) - oo, err := res.List(ctx) - if err != nil { - return nil, err - } - var ll v1.ConfigMapList - for _, o := range oo { - var cm v1.ConfigMap - err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &cm) - if err != nil { - return nil, errors.New("expecting configmap resource") - } - ll.Items = append(ll.Items, cm) - } - - return &ll, nil -} diff --git a/internal/dag/cr.go b/internal/dag/cr.go deleted file mode 100644 index 7c212613..00000000 --- a/internal/dag/cr.go +++ /dev/null @@ -1,65 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package dag - -import ( - "context" - "errors" - - "github.com/derailed/popeye/internal/client" - "github.com/derailed/popeye/internal/dao" - rbacv1 "k8s.io/api/rbac/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" -) - -// ListClusterRoles list included ClusterRoles. -func ListClusterRoles(ctx context.Context) (map[string]*rbacv1.ClusterRole, error) { - return listAllClusterRoles(ctx) -} - -// ListAllClusterRoles fetch all ClusterRoles on the cluster. -func listAllClusterRoles(ctx context.Context) (map[string]*rbacv1.ClusterRole, error) { - ll, err := fetchClusterRoles(ctx) - if err != nil { - return nil, err - } - crs := make(map[string]*rbacv1.ClusterRole, len(ll.Items)) - for i := range ll.Items { - crs[metaFQN(ll.Items[i].ObjectMeta)] = &ll.Items[i] - } - - return crs, nil -} - -// FetchClusterRoles retrieves all ClusterRoles on the cluster. -func fetchClusterRoles(ctx context.Context) (*rbacv1.ClusterRoleList, error) { - f, cfg := mustExtractFactory(ctx), mustExtractConfig(ctx) - if cfg.Flags.StandAlone { - dial, err := f.Client().Dial() - if err != nil { - return nil, err - } - return dial.RbacV1().ClusterRoles().List(ctx, metav1.ListOptions{}) - } - - var res dao.Resource - res.Init(f, client.NewGVR("rbac.authorization.k8s.io/v1/clusterroles")) - oo, err := res.List(ctx) - if err != nil { - return nil, err - } - var ll rbacv1.ClusterRoleList - for _, o := range oo { - var cr rbacv1.ClusterRole - err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &cr) - if err != nil { - return nil, errors.New("expecting clusterrole resource") - } - ll.Items = append(ll.Items, cr) - } - - return &ll, nil -} diff --git a/internal/dag/crb.go b/internal/dag/crb.go deleted file mode 100644 index d06155ce..00000000 --- a/internal/dag/crb.go +++ /dev/null @@ -1,66 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package dag - -import ( - "context" - "errors" - - "github.com/derailed/popeye/internal/client" - "github.com/derailed/popeye/internal/dao" - rbacv1 "k8s.io/api/rbac/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" -) - -// ListClusterRoleBindings list included ClusterRoleBindings. -func ListClusterRoleBindings(ctx context.Context) (map[string]*rbacv1.ClusterRoleBinding, error) { - return listAllClusterRoleBindings(ctx) -} - -// ListAllClusterRoleBindings fetch all ClusterRoleBindings on the cluster. -func listAllClusterRoleBindings(ctx context.Context) (map[string]*rbacv1.ClusterRoleBinding, error) { - ll, err := fetchClusterRoleBindings(ctx) - if err != nil { - return nil, err - } - - crbs := make(map[string]*rbacv1.ClusterRoleBinding, len(ll.Items)) - for i := range ll.Items { - crbs[metaFQN(ll.Items[i].ObjectMeta)] = &ll.Items[i] - } - - return crbs, nil -} - -// FetchClusterRoleBindings retrieves all ClusterRoleBindings on the cluster. -func fetchClusterRoleBindings(ctx context.Context) (*rbacv1.ClusterRoleBindingList, error) { - f, cfg := mustExtractFactory(ctx), mustExtractConfig(ctx) - if cfg.Flags.StandAlone { - dial, err := f.Client().Dial() - if err != nil { - return nil, err - } - return dial.RbacV1().ClusterRoleBindings().List(ctx, metav1.ListOptions{}) - } - - var res dao.Resource - res.Init(f, client.NewGVR("rbac.authorization.k8s.io/v1/clusterrolebindings")) - oo, err := res.List(ctx) - if err != nil { - return nil, err - } - var ll rbacv1.ClusterRoleBindingList - for _, o := range oo { - var crb rbacv1.ClusterRoleBinding - err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &crb) - if err != nil { - return nil, errors.New("expecting clusterrolebinding resource") - } - ll.Items = append(ll.Items, crb) - } - - return &ll, nil -} diff --git a/internal/dag/dp.go b/internal/dag/dp.go deleted file mode 100644 index d7972388..00000000 --- a/internal/dag/dp.go +++ /dev/null @@ -1,65 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package dag - -import ( - "context" - "errors" - - "github.com/derailed/popeye/internal/client" - "github.com/derailed/popeye/internal/dao" - appsv1 "k8s.io/api/apps/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" -) - -// ListDeployments list all included Deployments. -func ListDeployments(ctx context.Context) (map[string]*appsv1.Deployment, error) { - return listAllDeployments(ctx) -} - -// ListAllDeployments fetch all Deployments on the cluster. -func listAllDeployments(ctx context.Context) (map[string]*appsv1.Deployment, error) { - ll, err := fetchDeployments(ctx) - if err != nil { - return nil, err - } - dps := make(map[string]*appsv1.Deployment, len(ll.Items)) - for i := range ll.Items { - dps[metaFQN(ll.Items[i].ObjectMeta)] = &ll.Items[i] - } - - return dps, nil -} - -// FetchDeployments retrieves all Deployments on the cluster. -func fetchDeployments(ctx context.Context) (*appsv1.DeploymentList, error) { - f, cfg := mustExtractFactory(ctx), mustExtractConfig(ctx) - if cfg.Flags.StandAlone { - dial, err := f.Client().Dial() - if err != nil { - return nil, err - } - return dial.AppsV1().Deployments(f.Client().ActiveNamespace()).List(ctx, metav1.ListOptions{}) - } - - var res dao.Resource - res.Init(f, client.NewGVR("apps/v1/deployments")) - oo, err := res.List(ctx) - if err != nil { - return nil, err - } - var ll appsv1.DeploymentList - for _, o := range oo { - var dp appsv1.Deployment - err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &dp) - if err != nil { - return nil, errors.New("expecting deployment resource") - } - ll.Items = append(ll.Items, dp) - } - - return &ll, nil -} diff --git a/internal/dag/ds.go b/internal/dag/ds.go deleted file mode 100644 index 6cc11dd3..00000000 --- a/internal/dag/ds.go +++ /dev/null @@ -1,65 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package dag - -import ( - "context" - "errors" - - "github.com/derailed/popeye/internal/client" - "github.com/derailed/popeye/internal/dao" - appsv1 "k8s.io/api/apps/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" -) - -// ListDaemonSets list all included DaemonSets. -func ListDaemonSets(ctx context.Context) (map[string]*appsv1.DaemonSet, error) { - return listAllDaemonSets(ctx) -} - -// ListAllDaemonSets fetch all DaemonSets on the cluster. -func listAllDaemonSets(ctx context.Context) (map[string]*appsv1.DaemonSet, error) { - ll, err := fetchDaemonSets(ctx) - if err != nil { - return nil, err - } - dps := make(map[string]*appsv1.DaemonSet, len(ll.Items)) - for i := range ll.Items { - dps[metaFQN(ll.Items[i].ObjectMeta)] = &ll.Items[i] - } - - return dps, nil -} - -// FetchDaemonSets retrieves all DaemonSets on the cluster. -func fetchDaemonSets(ctx context.Context) (*appsv1.DaemonSetList, error) { - f, cfg := mustExtractFactory(ctx), mustExtractConfig(ctx) - if cfg.Flags.StandAlone { - dial, err := f.Client().Dial() - if err != nil { - return nil, err - } - return dial.AppsV1().DaemonSets(f.Client().ActiveNamespace()).List(ctx, metav1.ListOptions{}) - } - - var res dao.Resource - res.Init(f, client.NewGVR("apps/v1/daemonsets")) - oo, err := res.List(ctx) - if err != nil { - return nil, err - } - var ll appsv1.DaemonSetList - for _, o := range oo { - var ds appsv1.DaemonSet - err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &ds) - if err != nil { - return nil, errors.New("expecting daemonset resource") - } - ll.Items = append(ll.Items, ds) - } - - return &ll, nil -} diff --git a/internal/dag/ep.go b/internal/dag/ep.go deleted file mode 100644 index 0e053bac..00000000 --- a/internal/dag/ep.go +++ /dev/null @@ -1,66 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package dag - -import ( - "context" - "errors" - - "github.com/derailed/popeye/internal/client" - "github.com/derailed/popeye/internal/dao" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" -) - -// ListEndpoints list all included Endpoints. -func ListEndpoints(ctx context.Context) (map[string]*v1.Endpoints, error) { - return listAllEndpoints(ctx) -} - -// ListAllEndpoints fetch all Endpoints on the cluster. -func listAllEndpoints(ctx context.Context) (map[string]*v1.Endpoints, error) { - ll, err := fetchEndpoints(ctx) - if err != nil { - return nil, err - } - eps := make(map[string]*v1.Endpoints, len(ll.Items)) - for i := range ll.Items { - eps[metaFQN(ll.Items[i].ObjectMeta)] = &ll.Items[i] - } - - return eps, nil -} - -// FetchEndpoints retrieves all Endpoints on the cluster. -func fetchEndpoints(ctx context.Context) (*v1.EndpointsList, error) { - f, cfg := mustExtractFactory(ctx), mustExtractConfig(ctx) - if cfg.Flags.StandAlone { - dial, err := f.Client().Dial() - if err != nil { - return nil, err - } - return dial.CoreV1().Endpoints(f.Client().ActiveNamespace()).List(ctx, metav1.ListOptions{}) - } - - var res dao.Resource - res.Init(f, client.NewGVR("v1/endpoints")) - oo, err := res.List(ctx) - if err != nil { - return nil, err - } - var ll v1.EndpointsList - for _, o := range oo { - var ep v1.Endpoints - err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &ep) - if err != nil { - return nil, errors.New("expecting endpoints resource") - } - ll.Items = append(ll.Items, ep) - } - - return &ll, nil - -} diff --git a/internal/dag/helpers.go b/internal/dag/helpers.go index 7d03abf3..2a802493 100644 --- a/internal/dag/helpers.go +++ b/internal/dag/helpers.go @@ -7,7 +7,6 @@ import ( "context" "github.com/derailed/popeye/internal" - "github.com/derailed/popeye/pkg/config" "github.com/derailed/popeye/types" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -20,14 +19,6 @@ func mustExtractFactory(ctx context.Context) types.Factory { return f } -func mustExtractConfig(ctx context.Context) *config.Config { - cfg, ok := ctx.Value(internal.KeyConfig).(*config.Config) - if !ok { - panic("expecting config in context") - } - return cfg -} - // MetaFQN returns a full qualified ns/name string. func metaFQN(m metav1.ObjectMeta) string { if m.Namespace == "" { diff --git a/internal/dag/hpa.go b/internal/dag/hpa.go deleted file mode 100644 index 30fca7ad..00000000 --- a/internal/dag/hpa.go +++ /dev/null @@ -1,65 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package dag - -import ( - "context" - "errors" - - "github.com/derailed/popeye/internal/client" - "github.com/derailed/popeye/internal/dao" - autoscalingv1 "k8s.io/api/autoscaling/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" -) - -// ListHorizontalPodAutoscalers list all included HorizontalPodAutoscalers. -func ListHorizontalPodAutoscalers(ctx context.Context) (map[string]*autoscalingv1.HorizontalPodAutoscaler, error) { - return listAllHorizontalPodAutoscalers(ctx) -} - -// ListAllHorizontalPodAutoscalers fetch all HorizontalPodAutoscalers on the cluster. -func listAllHorizontalPodAutoscalers(ctx context.Context) (map[string]*autoscalingv1.HorizontalPodAutoscaler, error) { - ll, err := fetchHorizontalPodAutoscalers(ctx) - if err != nil { - return nil, err - } - hpas := make(map[string]*autoscalingv1.HorizontalPodAutoscaler, len(ll.Items)) - for i := range ll.Items { - hpas[metaFQN(ll.Items[i].ObjectMeta)] = &ll.Items[i] - } - - return hpas, nil -} - -// FetchHorizontalPodAutoscalers retrieves all HorizontalPodAutoscalers on the cluster. -func fetchHorizontalPodAutoscalers(ctx context.Context) (*autoscalingv1.HorizontalPodAutoscalerList, error) { - f, cfg := mustExtractFactory(ctx), mustExtractConfig(ctx) - if cfg.Flags.StandAlone { - dial, err := f.Client().Dial() - if err != nil { - return nil, err - } - return dial.AutoscalingV1().HorizontalPodAutoscalers(f.Client().ActiveNamespace()).List(ctx, metav1.ListOptions{}) - } - - var res dao.Resource - res.Init(f, client.NewGVR("autoscaling/v1/horizontalpodautoscalers")) - oo, err := res.List(ctx) - if err != nil { - return nil, err - } - var ll autoscalingv1.HorizontalPodAutoscalerList - for _, o := range oo { - var hpa autoscalingv1.HorizontalPodAutoscaler - err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &hpa) - if err != nil { - return nil, errors.New("expecting hpa resource") - } - ll.Items = append(ll.Items, hpa) - } - - return &ll, nil -} diff --git a/internal/dag/ing.go b/internal/dag/ing.go deleted file mode 100644 index 4f60aca8..00000000 --- a/internal/dag/ing.go +++ /dev/null @@ -1,70 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package dag - -import ( - "context" - "errors" - - netv1 "k8s.io/api/networking/v1" - - "github.com/derailed/popeye/internal/client" - "github.com/derailed/popeye/internal/dao" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" -) - -// IngressGVR tracks ingress specification -var IngressGVR = client.NewGVR("networking.k8s.io/v1/ingresses") - -// ListIngresses list all included Ingresses. -func ListIngresses(ctx context.Context) (map[string]*netv1.Ingress, error) { - return listAllIngresses(ctx) -} - -// ListAllIngresses fetch all Ingresses on the cluster. -func listAllIngresses(ctx context.Context) (map[string]*netv1.Ingress, error) { - ll, err := fetchIngresses(ctx) - if err != nil { - return nil, err - } - ings := make(map[string]*netv1.Ingress, len(ll.Items)) - for i := range ll.Items { - ings[metaFQN(ll.Items[i].ObjectMeta)] = &ll.Items[i] - } - - return ings, nil -} - -// FetchIngresses retrieves all Ingresses on the cluster. -func fetchIngresses(ctx context.Context) (*netv1.IngressList, error) { - f, cfg := mustExtractFactory(ctx), mustExtractConfig(ctx) - if cfg.Flags.StandAlone { - dial, err := f.Client().Dial() - if err != nil { - return nil, err - } - - return dial.NetworkingV1().Ingresses(f.Client().ActiveNamespace()).List(ctx, metav1.ListOptions{}) - } - - var res dao.Resource - res.Init(f, IngressGVR) - oo, err := res.List(ctx) - if err != nil { - return nil, err - } - var ll netv1.IngressList - for _, o := range oo { - var ing netv1.Ingress - err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &ing) - if err != nil { - return nil, errors.New("expecting ingress resource") - } - ll.Items = append(ll.Items, ing) - } - - return &ll, nil -} diff --git a/internal/dag/limit_range.go b/internal/dag/limit_range.go deleted file mode 100644 index 5ce9819c..00000000 --- a/internal/dag/limit_range.go +++ /dev/null @@ -1,65 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package dag - -import ( - "context" - "errors" - - "github.com/derailed/popeye/internal/client" - "github.com/derailed/popeye/internal/dao" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" -) - -// ListLimitRanges list all included LimitRanges. -func ListLimitRanges(ctx context.Context) (map[string]*v1.LimitRange, error) { - return listAllLimitRanges(ctx) -} - -// ListAllLimitRanges fetch all LimitRanges on the cluster. -func listAllLimitRanges(ctx context.Context) (map[string]*v1.LimitRange, error) { - ll, err := fetchLimitRanges(ctx) - if err != nil { - return nil, err - } - lrs := make(map[string]*v1.LimitRange, len(ll.Items)) - for i := range ll.Items { - lrs[metaFQN(ll.Items[i].ObjectMeta)] = &ll.Items[i] - } - - return lrs, nil -} - -// fetchLimitRanges retrieves all LimitRanges on the cluster. -func fetchLimitRanges(ctx context.Context) (*v1.LimitRangeList, error) { - f, cfg := mustExtractFactory(ctx), mustExtractConfig(ctx) - if cfg.Flags.StandAlone { - dial, err := f.Client().Dial() - if err != nil { - return nil, err - } - return dial.CoreV1().LimitRanges(f.Client().ActiveNamespace()).List(ctx, metav1.ListOptions{}) - } - - var res dao.Resource - res.Init(f, client.NewGVR("v1/limitranges")) - oo, err := res.List(ctx) - if err != nil { - return nil, err - } - var ll v1.LimitRangeList - for _, o := range oo { - var lr v1.LimitRange - err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &lr) - if err != nil { - return nil, errors.New("expecting limitrange resource") - } - ll.Items = append(ll.Items, lr) - } - - return &ll, nil -} diff --git a/internal/dag/no.go b/internal/dag/no.go deleted file mode 100644 index 3d4740b6..00000000 --- a/internal/dag/no.go +++ /dev/null @@ -1,65 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package dag - -import ( - "context" - "errors" - - "github.com/derailed/popeye/internal/client" - "github.com/derailed/popeye/internal/dao" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" -) - -// ListNodes list all included Nodes. -func ListNodes(ctx context.Context) (map[string]*v1.Node, error) { - return listAllNodes(ctx) -} - -// ListAllNodes fetch all Nodes on the cluster. -func listAllNodes(ctx context.Context) (map[string]*v1.Node, error) { - ll, err := fetchNodes(ctx) - if err != nil { - return nil, err - } - nos := make(map[string]*v1.Node, len(ll.Items)) - for i := range ll.Items { - nos[metaFQN(ll.Items[i].ObjectMeta)] = &ll.Items[i] - } - - return nos, nil -} - -// FetchNodes retrieves all Nodes on the cluster. -func fetchNodes(ctx context.Context) (*v1.NodeList, error) { - f, cfg := mustExtractFactory(ctx), mustExtractConfig(ctx) - if cfg.Flags.StandAlone { - dial, err := f.Client().Dial() - if err != nil { - return nil, err - } - return dial.CoreV1().Nodes().List(ctx, metav1.ListOptions{}) - } - - var res dao.Resource - res.Init(f, client.NewGVR("v1/nodes")) - oo, err := res.List(ctx) - if err != nil { - return nil, err - } - var ll v1.NodeList - for _, o := range oo { - var no v1.Node - err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &no) - if err != nil { - return nil, errors.New("expecting node resource") - } - ll.Items = append(ll.Items, no) - } - - return &ll, nil -} diff --git a/internal/dag/no_mx.go b/internal/dag/no_mx.go deleted file mode 100644 index 720344b4..00000000 --- a/internal/dag/no_mx.go +++ /dev/null @@ -1,40 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package dag - -import ( - "context" - - "github.com/derailed/popeye/internal/client" - "github.com/derailed/popeye/types" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" -) - -// ListNodesMetrics fetch all available Node metrics on the cluster. -func ListNodesMetrics(c types.Connection) (map[string]*mv1beta1.NodeMetrics, error) { - ll, err := fetchNodesMetrics(c) - if err != nil { - return map[string]*mv1beta1.NodeMetrics{}, err - } - - pmx := make(map[string]*mv1beta1.NodeMetrics, len(ll.Items)) - for i := range ll.Items { - pmx[metaFQN(ll.Items[i].ObjectMeta)] = &ll.Items[i] - } - - return pmx, nil -} - -// FetchNodesMetrics retrieves all Node metrics on the cluster. -func fetchNodesMetrics(c types.Connection) (*mv1beta1.NodeMetricsList, error) { - vc, err := c.MXDial() - if err != nil { - return nil, err - } - - ctx, cancel := context.WithTimeout(context.Background(), client.CallTimeout) - defer cancel() - return vc.MetricsV1beta1().NodeMetricses().List(ctx, metav1.ListOptions{}) -} diff --git a/internal/dag/np.go b/internal/dag/np.go deleted file mode 100644 index e2b046c1..00000000 --- a/internal/dag/np.go +++ /dev/null @@ -1,65 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package dag - -import ( - "context" - "errors" - - "github.com/derailed/popeye/internal/client" - "github.com/derailed/popeye/internal/dao" - nv1 "k8s.io/api/networking/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" -) - -// ListNetworkPolicies list all included NetworkPolicies. -func ListNetworkPolicies(ctx context.Context) (map[string]*nv1.NetworkPolicy, error) { - return listAllNetworkPolicies(ctx) -} - -// ListAllNetworkPolicies fetch all NetworkPolicies on the cluster. -func listAllNetworkPolicies(ctx context.Context) (map[string]*nv1.NetworkPolicy, error) { - ll, err := fetchNetworkPolicies(ctx) - if err != nil { - return nil, err - } - dps := make(map[string]*nv1.NetworkPolicy, len(ll.Items)) - for i := range ll.Items { - dps[metaFQN(ll.Items[i].ObjectMeta)] = &ll.Items[i] - } - - return dps, nil -} - -// FetchNetworkPolicies retrieves all NetworkPolicies on the cluster. -func fetchNetworkPolicies(ctx context.Context) (*nv1.NetworkPolicyList, error) { - f, cfg := mustExtractFactory(ctx), mustExtractConfig(ctx) - if cfg.Flags.StandAlone { - dial, err := f.Client().Dial() - if err != nil { - return nil, err - } - return dial.NetworkingV1().NetworkPolicies(f.Client().ActiveNamespace()).List(ctx, metav1.ListOptions{}) - } - - var res dao.Resource - res.Init(f, client.NewGVR("networking.k8s.io/v1/networkpolicies")) - oo, err := res.List(ctx) - if err != nil { - return nil, err - } - var ll nv1.NetworkPolicyList - for _, o := range oo { - var np nv1.NetworkPolicy - err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &np) - if err != nil { - return nil, errors.New("expecting networkpolicy resource") - } - ll.Items = append(ll.Items, np) - } - - return &ll, nil -} diff --git a/internal/dag/ns.go b/internal/dag/ns.go deleted file mode 100644 index 10b2740f..00000000 --- a/internal/dag/ns.go +++ /dev/null @@ -1,65 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package dag - -import ( - "context" - "errors" - - "github.com/derailed/popeye/internal/client" - "github.com/derailed/popeye/internal/dao" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" -) - -// ListNamespaces list all included Namespaces. -func ListNamespaces(ctx context.Context) (map[string]*v1.Namespace, error) { - return listAllNamespaces(ctx) -} - -// ListAllNamespaces fetch all Namespaces on the cluster. -func listAllNamespaces(ctx context.Context) (map[string]*v1.Namespace, error) { - ll, err := fetchNamespaces(ctx) - if err != nil { - return nil, err - } - nss := make(map[string]*v1.Namespace, len(ll.Items)) - for i := range ll.Items { - nss[metaFQN(ll.Items[i].ObjectMeta)] = &ll.Items[i] - } - - return nss, nil -} - -// FetchNamespaces retrieves all Namespaces on the cluster. -func fetchNamespaces(ctx context.Context) (*v1.NamespaceList, error) { - f, cfg := mustExtractFactory(ctx), mustExtractConfig(ctx) - if cfg.Flags.StandAlone { - dial, err := f.Client().Dial() - if err != nil { - return nil, err - } - return dial.CoreV1().Namespaces().List(ctx, metav1.ListOptions{}) - } - - var res dao.Resource - res.Init(f, client.NewGVR("v1/namespaces")) - oo, err := res.List(ctx) - if err != nil { - return nil, err - } - var ll v1.NamespaceList - for _, o := range oo { - var ns v1.Namespace - err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &ns) - if err != nil { - return nil, errors.New("expecting namespace resource") - } - ll.Items = append(ll.Items, ns) - } - - return &ll, nil -} diff --git a/internal/dag/pdb.go b/internal/dag/pdb.go deleted file mode 100644 index ea8c77e6..00000000 --- a/internal/dag/pdb.go +++ /dev/null @@ -1,65 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package dag - -import ( - "context" - "errors" - - "github.com/derailed/popeye/internal/client" - "github.com/derailed/popeye/internal/dao" - policyv1 "k8s.io/api/policy/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" -) - -// ListPodDisruptionBudgets list all included PodDisruptionBudgets. -func ListPodDisruptionBudgets(ctx context.Context) (map[string]*policyv1.PodDisruptionBudget, error) { - return listAllPodDisruptionBudgets(ctx) -} - -// ListAllPodDisruptionBudgets fetch all PodDisruptionBudgets on the cluster. -func listAllPodDisruptionBudgets(ctx context.Context) (map[string]*policyv1.PodDisruptionBudget, error) { - ll, err := fetchPodDisruptionBudgets(ctx) - if err != nil { - return nil, err - } - pdbs := make(map[string]*policyv1.PodDisruptionBudget, len(ll.Items)) - for i := range ll.Items { - pdbs[metaFQN(ll.Items[i].ObjectMeta)] = &ll.Items[i] - } - - return pdbs, nil -} - -// fetchPodDisruptionBudgets retrieves all PodDisruptionBudgets on the cluster. -func fetchPodDisruptionBudgets(ctx context.Context) (*policyv1.PodDisruptionBudgetList, error) { - f, cfg := mustExtractFactory(ctx), mustExtractConfig(ctx) - if cfg.Flags.StandAlone { - dial, err := f.Client().Dial() - if err != nil { - return nil, err - } - return dial.PolicyV1().PodDisruptionBudgets(f.Client().ActiveNamespace()).List(ctx, metav1.ListOptions{}) - } - - var res dao.Resource - res.Init(f, client.NewGVR("policy/v1/poddisruptionbudgets")) - oo, err := res.List(ctx) - if err != nil { - return nil, err - } - var ll policyv1.PodDisruptionBudgetList - for _, o := range oo { - var pdb policyv1.PodDisruptionBudget - err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &pdb) - if err != nil { - return nil, errors.New("expecting pdb resource") - } - ll.Items = append(ll.Items, pdb) - } - - return &ll, nil -} diff --git a/internal/dag/pod.go b/internal/dag/pod.go deleted file mode 100644 index 7d0592bf..00000000 --- a/internal/dag/pod.go +++ /dev/null @@ -1,65 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package dag - -import ( - "context" - "errors" - - "github.com/derailed/popeye/internal/client" - "github.com/derailed/popeye/internal/dao" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" -) - -// ListPods list all filtered pods. -func ListPods(ctx context.Context) (map[string]*v1.Pod, error) { - return listAllPods(ctx) -} - -// ListAllPods fetch all Pods on the cluster. -func listAllPods(ctx context.Context) (map[string]*v1.Pod, error) { - ll, err := fetchPods(ctx) - if err != nil { - return nil, err - } - pods := make(map[string]*v1.Pod, len(ll.Items)) - for i := range ll.Items { - pods[metaFQN(ll.Items[i].ObjectMeta)] = &ll.Items[i] - } - - return pods, nil -} - -// FetchPods retrieves all Pods on the cluster. -func fetchPods(ctx context.Context) (*v1.PodList, error) { - f, cfg := mustExtractFactory(ctx), mustExtractConfig(ctx) - if cfg.Flags.StandAlone { - dial, err := f.Client().Dial() - if err != nil { - return nil, err - } - return dial.CoreV1().Pods(f.Client().ActiveNamespace()).List(ctx, metav1.ListOptions{}) - } - - var res dao.Resource - res.Init(f, client.NewGVR("v1/pods")) - oo, err := res.List(ctx) - if err != nil { - return nil, err - } - var ll v1.PodList - for _, o := range oo { - var po v1.Pod - err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &po) - if err != nil { - return nil, errors.New("expecting pod resource") - } - ll.Items = append(ll.Items, po) - } - - return &ll, nil -} diff --git a/internal/dag/pod_mx.go b/internal/dag/pod_mx.go deleted file mode 100644 index 12996338..00000000 --- a/internal/dag/pod_mx.go +++ /dev/null @@ -1,39 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package dag - -import ( - "context" - - "github.com/derailed/popeye/internal/client" - "github.com/derailed/popeye/types" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" -) - -// ListPodsMetrics fetch all available Pod metrics on the cluster. -func ListPodsMetrics(c types.Connection) (map[string]*mv1beta1.PodMetrics, error) { - ll, err := fetchPodsMetrics(c) - if err != nil { - return map[string]*mv1beta1.PodMetrics{}, err - } - - pmx := make(map[string]*mv1beta1.PodMetrics, len(ll.Items)) - for i := range ll.Items { - pmx[metaFQN(ll.Items[i].ObjectMeta)] = &ll.Items[i] - } - - return pmx, nil -} - -// FetchPodsMetrics retrieves all Pod metrics on the cluster. -func fetchPodsMetrics(c types.Connection) (*mv1beta1.PodMetricsList, error) { - vc, err := c.MXDial() - if err != nil { - return nil, err - } - ctx, cancel := context.WithTimeout(context.Background(), client.CallTimeout) - defer cancel() - return vc.MetricsV1beta1().PodMetricses(c.ActiveNamespace()).List(ctx, metav1.ListOptions{}) -} diff --git a/internal/dag/pv.go b/internal/dag/pv.go deleted file mode 100644 index ba611ca6..00000000 --- a/internal/dag/pv.go +++ /dev/null @@ -1,65 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package dag - -import ( - "context" - "errors" - - "github.com/derailed/popeye/internal/client" - "github.com/derailed/popeye/internal/dao" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" -) - -// ListPersistentVolumes list all included PersistentVolumes. -func ListPersistentVolumes(ctx context.Context) (map[string]*v1.PersistentVolume, error) { - return listAllPersistentVolumes(ctx) -} - -// ListAllPersistentVolumes fetch all PersistentVolumes on the cluster. -func listAllPersistentVolumes(ctx context.Context) (map[string]*v1.PersistentVolume, error) { - ll, err := fetchPersistentVolumes(ctx) - if err != nil { - return nil, err - } - pvs := make(map[string]*v1.PersistentVolume, len(ll.Items)) - for i := range ll.Items { - pvs[metaFQN(ll.Items[i].ObjectMeta)] = &ll.Items[i] - } - - return pvs, nil -} - -// FetchPersistentVolumes retrieves all PersistentVolumes on the cluster. -func fetchPersistentVolumes(ctx context.Context) (*v1.PersistentVolumeList, error) { - f, cfg := mustExtractFactory(ctx), mustExtractConfig(ctx) - if cfg.Flags.StandAlone { - dial, err := f.Client().Dial() - if err != nil { - return nil, err - } - return dial.CoreV1().PersistentVolumes().List(ctx, metav1.ListOptions{}) - } - - var res dao.Resource - res.Init(f, client.NewGVR("v1/persistentvolumes")) - oo, err := res.List(ctx) - if err != nil { - return nil, err - } - var ll v1.PersistentVolumeList - for _, o := range oo { - var pv v1.PersistentVolume - err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &pv) - if err != nil { - return nil, errors.New("expecting persistentvolume resource") - } - ll.Items = append(ll.Items, pv) - } - - return &ll, nil -} diff --git a/internal/dag/pvc.go b/internal/dag/pvc.go deleted file mode 100644 index d8833d2c..00000000 --- a/internal/dag/pvc.go +++ /dev/null @@ -1,65 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package dag - -import ( - "context" - "errors" - - "github.com/derailed/popeye/internal/client" - "github.com/derailed/popeye/internal/dao" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" -) - -// ListPersistentVolumeClaims list all included PersistentVolumeClaims. -func ListPersistentVolumeClaims(ctx context.Context) (map[string]*v1.PersistentVolumeClaim, error) { - return listAllPersistentVolumeClaims(ctx) -} - -// ListAllPersistentVolumeClaims fetch all PersistentVolumeClaims on the cluster. -func listAllPersistentVolumeClaims(ctx context.Context) (map[string]*v1.PersistentVolumeClaim, error) { - ll, err := fetchPersistentVolumeClaims(ctx) - if err != nil { - return nil, err - } - pvcs := make(map[string]*v1.PersistentVolumeClaim, len(ll.Items)) - for i := range ll.Items { - pvcs[metaFQN(ll.Items[i].ObjectMeta)] = &ll.Items[i] - } - - return pvcs, nil -} - -// FetchPersistentVolumeClaims retrieves all PersistentVolumeClaims on the cluster. -func fetchPersistentVolumeClaims(ctx context.Context) (*v1.PersistentVolumeClaimList, error) { - f, cfg := mustExtractFactory(ctx), mustExtractConfig(ctx) - if cfg.Flags.StandAlone { - dial, err := f.Client().Dial() - if err != nil { - return nil, err - } - return dial.CoreV1().PersistentVolumeClaims(f.Client().ActiveNamespace()).List(ctx, metav1.ListOptions{}) - } - - var res dao.Resource - res.Init(f, client.NewGVR("v1/persistentvolumeclaims")) - oo, err := res.List(ctx) - if err != nil { - return nil, err - } - var ll v1.PersistentVolumeClaimList - for _, o := range oo { - var pvc v1.PersistentVolumeClaim - err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &pvc) - if err != nil { - return nil, errors.New("expecting persistentvolumeclaim resource") - } - ll.Items = append(ll.Items, pvc) - } - - return &ll, nil -} diff --git a/internal/dag/rb.go b/internal/dag/rb.go deleted file mode 100644 index 39b6e510..00000000 --- a/internal/dag/rb.go +++ /dev/null @@ -1,65 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package dag - -import ( - "context" - "errors" - - "github.com/derailed/popeye/internal/client" - "github.com/derailed/popeye/internal/dao" - rbacv1 "k8s.io/api/rbac/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" -) - -// ListRoleBindings list included RoleBindings. -func ListRoleBindings(ctx context.Context) (map[string]*rbacv1.RoleBinding, error) { - return listAllRoleBindings(ctx) -} - -// ListAllRoleBindings fetch all RoleBindings on the cluster. -func listAllRoleBindings(ctx context.Context) (map[string]*rbacv1.RoleBinding, error) { - ll, err := fetchRoleBindings(ctx) - if err != nil { - return nil, err - } - rbs := make(map[string]*rbacv1.RoleBinding, len(ll.Items)) - for i := range ll.Items { - rbs[metaFQN(ll.Items[i].ObjectMeta)] = &ll.Items[i] - } - - return rbs, nil -} - -// FetchRoleBindings retrieves all RoleBindings on the cluster. -func fetchRoleBindings(ctx context.Context) (*rbacv1.RoleBindingList, error) { - f, cfg := mustExtractFactory(ctx), mustExtractConfig(ctx) - if cfg.Flags.StandAlone { - dial, err := f.Client().Dial() - if err != nil { - return nil, err - } - return dial.RbacV1().RoleBindings(f.Client().ActiveNamespace()).List(ctx, metav1.ListOptions{}) - } - - var res dao.Resource - res.Init(f, client.NewGVR("rbac.authorization.k8s.io/v1/rolebindings")) - oo, err := res.List(ctx) - if err != nil { - return nil, err - } - var ll rbacv1.RoleBindingList - for _, o := range oo { - var rb rbacv1.RoleBinding - err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &rb) - if err != nil { - return nil, errors.New("expecting rolebinding resource") - } - ll.Items = append(ll.Items, rb) - } - - return &ll, nil -} diff --git a/internal/dag/role.go b/internal/dag/role.go deleted file mode 100644 index d9ca9b3e..00000000 --- a/internal/dag/role.go +++ /dev/null @@ -1,65 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package dag - -import ( - "context" - "errors" - - "github.com/derailed/popeye/internal/client" - "github.com/derailed/popeye/internal/dao" - rbacv1 "k8s.io/api/rbac/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" -) - -// ListRoles list included Roles. -func ListRoles(ctx context.Context) (map[string]*rbacv1.Role, error) { - return listAllRoles(ctx) -} - -// ListAllRoles fetch all Roles on the cluster. -func listAllRoles(ctx context.Context) (map[string]*rbacv1.Role, error) { - ll, err := fetchRoles(ctx) - if err != nil { - return nil, err - } - ros := make(map[string]*rbacv1.Role, len(ll.Items)) - for i := range ll.Items { - ros[metaFQN(ll.Items[i].ObjectMeta)] = &ll.Items[i] - } - - return ros, nil -} - -// FetchRoleBindings retrieves all RoleBindings on the cluster. -func fetchRoles(ctx context.Context) (*rbacv1.RoleList, error) { - f, cfg := mustExtractFactory(ctx), mustExtractConfig(ctx) - if cfg.Flags.StandAlone { - dial, err := f.Client().Dial() - if err != nil { - return nil, err - } - return dial.RbacV1().Roles(f.Client().ActiveNamespace()).List(ctx, metav1.ListOptions{}) - } - - var res dao.Resource - res.Init(f, client.NewGVR("rbac.authorization.k8s.io/v1/roles")) - oo, err := res.List(ctx) - if err != nil { - return nil, err - } - var ll rbacv1.RoleList - for _, o := range oo { - var ro rbacv1.Role - err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &ro) - if err != nil { - return nil, errors.New("expecting role resource") - } - ll.Items = append(ll.Items, ro) - } - - return &ll, nil -} diff --git a/internal/dag/rs.go b/internal/dag/rs.go deleted file mode 100644 index 477ff2e0..00000000 --- a/internal/dag/rs.go +++ /dev/null @@ -1,65 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package dag - -import ( - "context" - "errors" - - "github.com/derailed/popeye/internal/client" - "github.com/derailed/popeye/internal/dao" - appsv1 "k8s.io/api/apps/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" -) - -// ListReplicaSets list all included ReplicaSets. -func ListReplicaSets(ctx context.Context) (map[string]*appsv1.ReplicaSet, error) { - return listAllReplicaSets(ctx) -} - -// ListAllReplicaSets fetch all ReplicaSets on the cluster. -func listAllReplicaSets(ctx context.Context) (map[string]*appsv1.ReplicaSet, error) { - ll, err := fetchReplicaSets(ctx) - if err != nil { - return nil, err - } - rss := make(map[string]*appsv1.ReplicaSet, len(ll.Items)) - for i := range ll.Items { - rss[metaFQN(ll.Items[i].ObjectMeta)] = &ll.Items[i] - } - - return rss, nil -} - -// FetchReplicaSets retrieves all ReplicaSets on the cluster. -func fetchReplicaSets(ctx context.Context) (*appsv1.ReplicaSetList, error) { - f, cfg := mustExtractFactory(ctx), mustExtractConfig(ctx) - if cfg.Flags.StandAlone { - dial, err := f.Client().Dial() - if err != nil { - return nil, err - } - return dial.AppsV1().ReplicaSets(f.Client().ActiveNamespace()).List(ctx, metav1.ListOptions{}) - } - - var res dao.Resource - res.Init(f, client.NewGVR("apps/v1/replicasets")) - oo, err := res.List(ctx) - if err != nil { - return nil, err - } - var ll appsv1.ReplicaSetList - for _, o := range oo { - var rs appsv1.ReplicaSet - err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &rs) - if err != nil { - return nil, errors.New("expecting replicaset resource") - } - ll.Items = append(ll.Items, rs) - } - - return &ll, nil -} diff --git a/internal/dag/sa.go b/internal/dag/sa.go deleted file mode 100644 index 5965d941..00000000 --- a/internal/dag/sa.go +++ /dev/null @@ -1,66 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package dag - -import ( - "context" - "errors" - - "github.com/derailed/popeye/internal/client" - "github.com/derailed/popeye/internal/dao" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" -) - -// ListServiceAccounts list included ServiceAccounts. -func ListServiceAccounts(ctx context.Context) (map[string]*v1.ServiceAccount, error) { - return listAllServiceAccounts(ctx) -} - -// ListAllServiceAccounts fetch all ServiceAccounts on the cluster. -func listAllServiceAccounts(ctx context.Context) (map[string]*v1.ServiceAccount, error) { - ll, err := fetchServiceAccounts(ctx) - if err != nil { - return nil, err - } - sas := make(map[string]*v1.ServiceAccount, len(ll.Items)) - for i := range ll.Items { - sas[metaFQN(ll.Items[i].ObjectMeta)] = &ll.Items[i] - } - - return sas, nil -} - -// FetchServiceAccounts retrieves all ServiceAccounts on the cluster. -func fetchServiceAccounts(ctx context.Context) (*v1.ServiceAccountList, error) { - f, cfg := mustExtractFactory(ctx), mustExtractConfig(ctx) - if cfg.Flags.StandAlone { - dial, err := f.Client().Dial() - if err != nil { - return nil, err - } - return dial.CoreV1().ServiceAccounts(f.Client().ActiveNamespace()).List(ctx, metav1.ListOptions{}) - } - - var res dao.Resource - res.Init(f, client.NewGVR("v1/serviceaccounts")) - oo, err := res.List(ctx) - if err != nil { - return nil, err - } - var ll v1.ServiceAccountList - for _, o := range oo { - var sa v1.ServiceAccount - err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &sa) - if err != nil { - return nil, errors.New("expecting serviceaccount resource") - } - ll.Items = append(ll.Items, sa) - } - - return &ll, nil - -} diff --git a/internal/dag/sec.go b/internal/dag/sec.go deleted file mode 100644 index 58643a42..00000000 --- a/internal/dag/sec.go +++ /dev/null @@ -1,65 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package dag - -import ( - "context" - "errors" - - "github.com/derailed/popeye/internal/client" - "github.com/derailed/popeye/internal/dao" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" -) - -// ListSecrets list all included Secrets. -func ListSecrets(ctx context.Context) (map[string]*v1.Secret, error) { - return listAllSecrets(ctx) -} - -// ListAllSecrets fetch all Secrets on the cluster. -func listAllSecrets(ctx context.Context) (map[string]*v1.Secret, error) { - ll, err := fetchSecrets(ctx) - if err != nil { - return nil, err - } - secs := make(map[string]*v1.Secret, len(ll.Items)) - for i := range ll.Items { - secs[metaFQN(ll.Items[i].ObjectMeta)] = &ll.Items[i] - } - - return secs, nil -} - -// FetchSecrets retrieves all Secrets on the cluster. -func fetchSecrets(ctx context.Context) (*v1.SecretList, error) { - f, cfg := mustExtractFactory(ctx), mustExtractConfig(ctx) - if cfg.Flags.StandAlone { - dial, err := f.Client().Dial() - if err != nil { - return nil, err - } - return dial.CoreV1().Secrets(f.Client().ActiveNamespace()).List(ctx, metav1.ListOptions{}) - } - - var res dao.Resource - res.Init(f, client.NewGVR("v1/secrets")) - oo, err := res.List(ctx) - if err != nil { - return nil, err - } - var ll v1.SecretList - for _, o := range oo { - var sec v1.Secret - err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &sec) - if err != nil { - return nil, errors.New("expecting secret resource") - } - ll.Items = append(ll.Items, sec) - } - - return &ll, nil -} diff --git a/internal/dag/sts.go b/internal/dag/sts.go deleted file mode 100644 index 21a811e7..00000000 --- a/internal/dag/sts.go +++ /dev/null @@ -1,65 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package dag - -import ( - "context" - "errors" - - "github.com/derailed/popeye/internal/client" - "github.com/derailed/popeye/internal/dao" - appsv1 "k8s.io/api/apps/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" -) - -// ListStatefulSets list available StatefulSets. -func ListStatefulSets(ctx context.Context) (map[string]*appsv1.StatefulSet, error) { - return listAllStatefulSets(ctx) -} - -// ListAllStatefulSets fetch all StatefulSets on the cluster. -func listAllStatefulSets(ctx context.Context) (map[string]*appsv1.StatefulSet, error) { - ll, err := fetchStatefulSets(ctx) - if err != nil { - return nil, err - } - sts := make(map[string]*appsv1.StatefulSet, len(ll.Items)) - for i := range ll.Items { - sts[metaFQN(ll.Items[i].ObjectMeta)] = &ll.Items[i] - } - - return sts, nil -} - -// FetchStatefulSets retrieves all StatefulSets on the cluster. -func fetchStatefulSets(ctx context.Context) (*appsv1.StatefulSetList, error) { - f, cfg := mustExtractFactory(ctx), mustExtractConfig(ctx) - if cfg.Flags.StandAlone { - dial, err := f.Client().Dial() - if err != nil { - return nil, err - } - return dial.AppsV1().StatefulSets(f.Client().ActiveNamespace()).List(ctx, metav1.ListOptions{}) - } - - var res dao.Resource - res.Init(f, client.NewGVR("apps/v1/statefulsets")) - oo, err := res.List(ctx) - if err != nil { - return nil, err - } - var ll appsv1.StatefulSetList - for _, o := range oo { - var sts appsv1.StatefulSet - err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &sts) - if err != nil { - return nil, errors.New("expecting sts resource") - } - ll.Items = append(ll.Items, sts) - } - - return &ll, nil -} diff --git a/internal/dag/svc.go b/internal/dag/svc.go deleted file mode 100644 index aa8d1f3d..00000000 --- a/internal/dag/svc.go +++ /dev/null @@ -1,65 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package dag - -import ( - "context" - "errors" - - "github.com/derailed/popeye/internal/client" - "github.com/derailed/popeye/internal/dao" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" -) - -// ListServices list all included Services. -func ListServices(ctx context.Context) (map[string]*v1.Service, error) { - return listAllServices(ctx) -} - -// ListAllServices fetch all Services on the cluster. -func listAllServices(ctx context.Context) (map[string]*v1.Service, error) { - ll, err := fetchServices(ctx) - if err != nil { - return nil, err - } - svcs := make(map[string]*v1.Service, len(ll.Items)) - for i := range ll.Items { - svcs[metaFQN(ll.Items[i].ObjectMeta)] = &ll.Items[i] - } - - return svcs, nil -} - -// FetchServices retrieves all Services on the cluster. -func fetchServices(ctx context.Context) (*v1.ServiceList, error) { - f, cfg := mustExtractFactory(ctx), mustExtractConfig(ctx) - if cfg.Flags.StandAlone { - dial, err := f.Client().Dial() - if err != nil { - return nil, err - } - return dial.CoreV1().Services(f.Client().ActiveNamespace()).List(ctx, metav1.ListOptions{}) - } - - var res dao.Resource - res.Init(f, client.NewGVR("v1/services")) - oo, err := res.List(ctx) - if err != nil { - return nil, err - } - var ll v1.ServiceList - for _, o := range oo { - var svc v1.Service - err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &svc) - if err != nil { - return nil, errors.New("expecting service resource") - } - ll.Items = append(ll.Items, svc) - } - - return &ll, nil -} diff --git a/internal/dao/ev.go b/internal/dao/ev.go new file mode 100644 index 00000000..79b38b45 --- /dev/null +++ b/internal/dao/ev.go @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package dao + +import ( + "context" + "fmt" + "strings" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/client" + "github.com/derailed/popeye/types" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + WarnEvt = "Warning" +) + +type EventInfo struct { + Kind string + Reason string + Message string + Count int64 +} + +func (e EventInfo) IsIssue() bool { + return e.Kind == WarnEvt || !strings.Contains(e.Reason, "Success") +} + +type EventInfos []EventInfo + +func (ee EventInfos) Issues() []string { + if len(ee) == 0 { + return nil + } + ss := make([]string, 0, len(ee)) + for _, e := range ee { + if e.IsIssue() { + ss = append(ss, e.Message) + } + } + + return ss +} + +type Event struct { + Table +} + +func EventsFor(ctx context.Context, gvr types.GVR, level, kind, fqn string) (EventInfos, error) { + ns, n := client.Namespaced(fqn) + f, ok := ctx.Value(internal.KeyFactory).(types.Factory) + if !ok { + return nil, nil + } + + ss := make([]string, 0, 2) + if level != "" { + ss = append(ss, fmt.Sprintf("type=%s", level)) + } + if kind != "" { + ss = append(ss, fmt.Sprintf("involvedObject.name=%s,involvedObject.kind=%s", n, kind)) + } + ctx = context.WithValue(ctx, internal.KeyFields, strings.Join(ss, ",")) + + var t Table + t.Init(f, types.NewGVR("v1/events")) + + oo, err := t.List(ctx, ns) + if err != nil { + return nil, err + } + if len(oo) == 0 { + return nil, fmt.Errorf("No events found %s", fqn) + } + + tt := oo[0].(*metav1.Table) + ee := make(EventInfos, 0, len(tt.Rows)) + for _, r := range tt.Rows { + ee = append(ee, EventInfo{ + Kind: r.Cells[1].(string), + Reason: r.Cells[2].(string), + Message: r.Cells[6].(string), + Count: r.Cells[8].(int64), + }) + } + + return ee, nil +} diff --git a/internal/dao/generic.go b/internal/dao/generic.go index 79ed94be..e450065c 100644 --- a/internal/dao/generic.go +++ b/internal/dao/generic.go @@ -42,6 +42,7 @@ func (g *Generic) List(ctx context.Context) ([]runtime.Object, error) { if err != nil { return nil, err } + if client.IsClusterScoped(ns) { ll, err = dial.List(ctx, metav1.ListOptions{LabelSelector: labelSel}) } else { @@ -80,5 +81,6 @@ func (g *Generic) dynClient() (dynamic.NamespaceableResourceInterface, error) { if err != nil { return nil, err } + return dial.Resource(g.gvr.GVR()), nil } diff --git a/internal/dao/non_resource.go b/internal/dao/non_resource.go index b8b302f3..7ba1d7ae 100644 --- a/internal/dao/non_resource.go +++ b/internal/dao/non_resource.go @@ -7,7 +7,6 @@ import ( "context" "fmt" - "github.com/derailed/popeye/internal/client" "github.com/derailed/popeye/types" "k8s.io/apimachinery/pkg/runtime" ) @@ -16,11 +15,11 @@ import ( type NonResource struct { types.Factory - gvr client.GVR + gvr types.GVR } // Init initializes the resource. -func (n *NonResource) Init(f types.Factory, gvr client.GVR) { +func (n *NonResource) Init(f types.Factory, gvr types.GVR) { n.Factory, n.gvr = f, gvr } diff --git a/internal/dao/resource.go b/internal/dao/resource.go index ed3fa295..9e6394da 100644 --- a/internal/dao/resource.go +++ b/internal/dao/resource.go @@ -8,6 +8,7 @@ import ( "fmt" "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/client" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" ) @@ -30,11 +31,14 @@ func (r *Resource) List(ctx context.Context) ([]runtime.Object, error) { if !ok { panic(fmt.Sprintf("BOOM no namespace in context %s", r.gvr)) } + if r.gvr == internal.Glossary[internal.NS] { + ns = client.AllNamespaces + } - return r.Factory.List(r.gvr.String(), ns, true, lsel) + return r.Factory.List(r.gvr, ns, true, lsel) } // Get returns a resource instance if found, else an error. func (r *Resource) Get(_ context.Context, path string) (runtime.Object, error) { - return r.Factory.Get(r.gvr.String(), path, true, labels.Everything()) + return r.Factory.Get(r.gvr, path, true, labels.Everything()) } diff --git a/internal/dao/table.go b/internal/dao/table.go new file mode 100644 index 00000000..e4b8dbe9 --- /dev/null +++ b/internal/dao/table.go @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package dao + +import ( + "context" + "fmt" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/client" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/serializer" + "k8s.io/client-go/rest" +) + +// Table retrieves K8s resources as tabular data. +type Table struct { + Generic +} + +// Get returns a given resource. +func (t *Table) Get(ctx context.Context, path string) (runtime.Object, error) { + a := fmt.Sprintf(gvFmt, metav1.SchemeGroupVersion.Version, metav1.GroupName) + _, codec := t.codec() + + c, err := t.getClient() + if err != nil { + return nil, err + } + ns, n := client.Namespaced(path) + req := c.Get(). + SetHeader("Accept", a). + Name(n). + Resource(t.gvr.R()). + VersionedParams(&metav1.TableOptions{}, codec) + if ns != client.ClusterScope { + req = req.Namespace(ns) + } + + return req.Do(ctx).Get() +} + +// List all Resources in a given namespace. +func (t *Table) List(ctx context.Context, ns string) ([]runtime.Object, error) { + labelSel, _ := ctx.Value(internal.KeyLabels).(string) + fieldSel, _ := ctx.Value(internal.KeyFields).(string) + + a := fmt.Sprintf(gvFmt, metav1.SchemeGroupVersion.Version, metav1.GroupName) + _, codec := t.codec() + + c, err := t.getClient() + if err != nil { + return nil, err + } + o, err := c.Get(). + SetHeader("Accept", a). + Namespace(ns). + Resource(t.gvr.R()). + VersionedParams(&metav1.ListOptions{FieldSelector: fieldSel, LabelSelector: labelSel}, codec). + Do(ctx).Get() + if err != nil { + return nil, err + } + + return []runtime.Object{o}, nil +} + +// ---------------------------------------------------------------------------- +// Helpers... + +const gvFmt = "application/json;as=Table;v=%s;g=%s, application/json" + +func (t *Table) getClient() (*rest.RESTClient, error) { + cfg, err := t.Client().RestConfig() + if err != nil { + return nil, err + } + gv := t.gvr.GV() + cfg.GroupVersion = &gv + cfg.APIPath = "/apis" + if t.gvr.G() == "" { + cfg.APIPath = "/api" + } + codec, _ := t.codec() + cfg.NegotiatedSerializer = codec.WithoutConversion() + + crRestClient, err := rest.RESTClientFor(cfg) + if err != nil { + return nil, err + } + + return crRestClient, nil +} + +func (t *Table) codec() (serializer.CodecFactory, runtime.ParameterCodec) { + scheme := runtime.NewScheme() + gv := t.gvr.GV() + metav1.AddToGroupVersion(scheme, gv) + scheme.AddKnownTypes(gv, &metav1.Table{}, &metav1.TableOptions{IncludeObject: v1.IncludeObject}) + scheme.AddKnownTypes(metav1.SchemeGroupVersion, &metav1.Table{}, &metav1.TableOptions{IncludeObject: v1.IncludeObject}) + + return serializer.NewCodecFactory(scheme), runtime.NewParameterCodec(scheme) +} diff --git a/internal/dao/types.go b/internal/dao/types.go index 2872eef0..059d30e6 100644 --- a/internal/dao/types.go +++ b/internal/dao/types.go @@ -6,14 +6,13 @@ package dao import ( "context" - "github.com/derailed/popeye/internal/client" "github.com/derailed/popeye/types" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" ) // ResourceMetas represents a collection of resource metadata. -type ResourceMetas map[client.GVR]metav1.APIResource +type ResourceMetas map[types.GVR]metav1.APIResource // Getter represents a resource getter. type Getter interface { @@ -33,7 +32,7 @@ type Accessor interface { Getter // Init the resource with a factory object. - Init(types.Factory, client.GVR) + Init(types.Factory, types.GVR) // GVR returns a gvr a string. GVR() string diff --git a/internal/db/db.go b/internal/db/db.go new file mode 100644 index 00000000..03bdef59 --- /dev/null +++ b/internal/db/db.go @@ -0,0 +1,404 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package db + +import ( + "fmt" + + "github.com/rs/zerolog/log" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/client" + "github.com/derailed/popeye/types" + "github.com/hashicorp/go-memdb" + batchv1 "k8s.io/api/batch/v1" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" +) + +type DB struct { + *memdb.MemDB +} + +func NewDB(db *memdb.MemDB) *DB { + return &DB{ + MemDB: db, + } +} + +func (db *DB) ITFor(gvr types.GVR) (*memdb.Txn, memdb.ResultIterator, error) { + if gvr == types.BlankGVR { + panic(fmt.Errorf("invalid table")) + } + txn := db.Txn(false) + it, err := txn.Get(gvr.String(), "id") + if err != nil { + return nil, nil, err + } + + return txn, it, nil +} + +func (db *DB) MustITForNS(gvr types.GVR, ns string) (*memdb.Txn, memdb.ResultIterator) { + txn := db.Txn(false) + it, err := txn.Get(gvr.String(), "ns", ns) + if err != nil { + panic(fmt.Errorf("Db get failed for %q: %w", gvr, err)) + } + + return txn, it +} + +func (db *DB) MustITFor(gvr types.GVR) (*memdb.Txn, memdb.ResultIterator) { + txn := db.Txn(false) + it, err := txn.Get(gvr.String(), "id") + if err != nil { + panic(fmt.Errorf("Db get failed for %q: %w", gvr, err)) + } + + return txn, it +} + +func (db *DB) ListNodes() (map[string]*v1.Node, error) { + txn, it := db.MustITFor(internal.Glossary[internal.NO]) + defer txn.Abort() + + mm := make(map[string]*v1.Node) + for o := it.Next(); o != nil; o = it.Next() { + no, ok := o.(*v1.Node) + if !ok { + return nil, fmt.Errorf("expecting node but got %T", o) + } + mm[no.Name] = no + } + + return mm, nil +} + +func (db *DB) FindPMX(fqn string) (*mv1beta1.PodMetrics, error) { + gvr := internal.Glossary[internal.PMX] + if gvr == types.BlankGVR { + return nil, nil + } + txn := db.Txn(false) + defer txn.Abort() + o, err := txn.First(gvr.String(), "id", fqn) + if err != nil || o == nil { + return nil, fmt.Errorf("object not found: %q", fqn) + } + + pmx, ok := o.(*mv1beta1.PodMetrics) + if !ok { + return nil, fmt.Errorf("expecting PodMetrics but got %T", o) + } + + return pmx, nil +} + +func (db *DB) FindNMX(fqn string) (*mv1beta1.NodeMetrics, error) { + gvr := internal.Glossary[internal.NMX] + if gvr == types.BlankGVR { + return nil, nil + } + txn := db.Txn(false) + defer txn.Abort() + o, err := txn.First(gvr.String(), "id", fqn) + if err != nil || o == nil { + return nil, fmt.Errorf("object not found: %q", fqn) + } + + nmx, ok := o.(*mv1beta1.NodeMetrics) + if !ok { + return nil, fmt.Errorf("expecting NodeMetrics but got %T", o) + } + + return nmx, nil +} + +func (db *DB) ListNMX() ([]*mv1beta1.NodeMetrics, error) { + gvr := internal.Glossary[internal.NMX] + if gvr == types.BlankGVR { + return nil, nil + } + txn, it, err := db.ITFor(gvr) + if err != nil { + return nil, err + } + defer txn.Abort() + + mm := make([]*mv1beta1.NodeMetrics, 0, 10) + for o := it.Next(); o != nil; o = it.Next() { + nmx, ok := o.(*mv1beta1.NodeMetrics) + if !ok { + return nil, fmt.Errorf("expecting NodeMetrics but got %T", o) + } + mm = append(mm, nmx) + } + + return mm, nil +} + +func (db *DB) Find(kind types.GVR, fqn string) (any, error) { + txn := db.Txn(false) + defer txn.Abort() + o, err := txn.First(kind.String(), "id", fqn) + if err != nil || o == nil { + return nil, fmt.Errorf("object not found: %q", fqn) + } + + return o, nil +} + +func (db *DB) FindPod(ns string, sel map[string]string) (*v1.Pod, error) { + txn := db.Txn(false) + defer txn.Abort() + txn, it := db.MustITFor(internal.Glossary[internal.PO]) + defer txn.Abort() + for o := it.Next(); o != nil; o = it.Next() { + po, ok := o.(*v1.Pod) + if !ok { + return nil, fmt.Errorf("expecting pod") + } + if po.Namespace != ns { + continue + } + if MatchLabels(po.Labels, sel) { + return po, nil + } + } + + return nil, fmt.Errorf("no pods match selector: %v", sel) +} + +func (db *DB) FindJobs(fqn string) ([]*batchv1.Job, error) { + txn := db.Txn(false) + defer txn.Abort() + txn, it := db.MustITFor(internal.Glossary[internal.JOB]) + defer txn.Abort() + + cns, cn := client.Namespaced(fqn) + jj := make([]*batchv1.Job, 0, 10) + for o := it.Next(); o != nil; o = it.Next() { + jo, ok := o.(*batchv1.Job) + if !ok { + return nil, fmt.Errorf("expecting job") + } + if jo.Namespace != cns { + continue + } + for _, o := range jo.OwnerReferences { + if o.Controller == nil && !*o.Controller { + continue + } + if o.Name == cn { + jj = append(jj, jo) + } + } + } + + return jj, nil +} + +func (db *DB) FindPods(ns string, sel map[string]string) ([]*v1.Pod, error) { + txn := db.Txn(false) + defer txn.Abort() + txn, it := db.MustITFor(internal.Glossary[internal.PO]) + defer txn.Abort() + pp := make([]*v1.Pod, 0, 10) + for o := it.Next(); o != nil; o = it.Next() { + po, ok := o.(*v1.Pod) + if !ok { + return nil, fmt.Errorf("expecting pod but got %T", o) + } + if po.Namespace != ns { + continue + } + if MatchLabels(po.Labels, sel) { + pp = append(pp, po) + } + } + + return pp, nil +} + +func (db *DB) FindPodsBySel(ns string, sel *metav1.LabelSelector) ([]*v1.Pod, error) { + if sel == nil || sel.Size() == 0 { + return nil, fmt.Errorf("no pod selector given") + } + + txn := db.Txn(false) + defer txn.Abort() + txn, it := db.MustITFor(internal.Glossary[internal.PO]) + defer txn.Abort() + pp := make([]*v1.Pod, 0, 10) + for o := it.Next(); o != nil; o = it.Next() { + po, ok := o.(*v1.Pod) + if !ok { + return nil, fmt.Errorf("expecting pod") + } + if po.Namespace != ns { + continue + } + if MatchSelector(po.Labels, sel) { + pp = append(pp, po) + } + } + + return pp, nil +} + +func (db *DB) FindNSBySel(sel *metav1.LabelSelector) ([]*v1.Namespace, error) { + if sel == nil || sel.Size() == 0 { + return nil, nil + } + + txn := db.Txn(false) + defer txn.Abort() + txn, it := db.MustITFor(internal.Glossary[internal.NS]) + defer txn.Abort() + nss := make([]*v1.Namespace, 0, 10) + for o := it.Next(); o != nil; o = it.Next() { + ns, ok := o.(*v1.Namespace) + if !ok { + return nil, fmt.Errorf("expecting namespace") + } + if MatchSelector(ns.Labels, sel) { + nss = append(nss, ns) + } + } + + return nss, nil +} + +func (db *DB) DumpNS() error { + txn := db.Txn(false) + defer txn.Abort() + it, err := txn.Get(internal.Glossary[internal.NS].String(), "id") + if err != nil { + return err + } + for o := it.Next(); o != nil; o = it.Next() { + ns, _ := o.(*v1.Namespace) + log.Debug().Msgf("NS %q", ns.Name) + } + + return nil +} + +func (db *DB) FindNS(ns string) (*v1.Namespace, error) { + txn := db.Txn(false) + defer txn.Abort() + o, err := txn.First(internal.Glossary[internal.NS].String(), "ns", ns) + if err != nil { + return nil, err + } + nss, ok := o.(*v1.Namespace) + if !ok { + return nil, fmt.Errorf("expecting namespace but got %s", o) + } + + return nss, nil +} + +func (db *DB) FindNSNameBySel(sel *metav1.LabelSelector) ([]string, error) { + if sel == nil || sel.Size() == 0 { + return nil, nil + } + + txn := db.Txn(false) + defer txn.Abort() + txn, it := db.MustITFor(internal.Glossary[internal.NS]) + defer txn.Abort() + nss := make([]string, 0, 10) + for o := it.Next(); o != nil; o = it.Next() { + ns, ok := o.(*v1.Namespace) + if !ok { + return nil, fmt.Errorf("expecting namespace but got %s", o) + } + if MatchSelector(ns.Labels, sel) { + nss = append(nss, ns.Name) + } + } + + return nss, nil +} + +// Helpers... + +// MatchSelector check if pod labels match a selector. +func MatchSelector(labels map[string]string, sel *metav1.LabelSelector) bool { + if len(labels) == 0 || sel.Size() == 0 { + return false + } + if MatchLabels(labels, sel.MatchLabels) { + return true + } + + return matchExp(labels, sel.MatchExpressions) +} + +func matchExp(labels map[string]string, ee []metav1.LabelSelectorRequirement) bool { + for _, e := range ee { + if matchSel(labels, e) { + return true + } + } + + return false +} + +func matchSel(labels map[string]string, e metav1.LabelSelectorRequirement) bool { + _, ok := labels[e.Key] + if e.Operator == metav1.LabelSelectorOpDoesNotExist && !ok { + return true + } + if !ok { + return false + } + + switch e.Operator { + case metav1.LabelSelectorOpNotIn: + for _, v := range e.Values { + if v1, ok := labels[e.Key]; ok && v1 == v { + return false + } + } + return true + case metav1.LabelSelectorOpIn: + for _, v := range e.Values { + if v == labels[e.Key] { + return true + } + } + return false + case metav1.LabelSelectorOpExists: + return true + } + + return false +} + +// MatchLabels check if pod labels match a selector. +func MatchLabels(labels, sel map[string]string) bool { + if len(sel) == 0 { + return false + } + + for k, v := range sel { + if v1, ok := labels[k]; !ok || v1 != v { + return false + } + } + + return true +} + +func (db *DB) Exists(kind types.GVR, fqn string) bool { + txn := db.Txn(false) + defer txn.Abort() + o, err := txn.First(kind.String(), "id", fqn) + + return err == nil && o != nil +} diff --git a/internal/db/loader.go b/internal/db/loader.go new file mode 100644 index 00000000..7aa47124 --- /dev/null +++ b/internal/db/loader.go @@ -0,0 +1,230 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package db + +import ( + "context" + "fmt" + "strings" + "sync" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/client" + "github.com/derailed/popeye/internal/dao" + "github.com/derailed/popeye/types" + "github.com/rs/zerolog/log" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" +) + +type CastFn[T any] func(o runtime.Object) (*T, error) + +type Loader struct { + DB *DB + loaded map[types.GVR]struct{} + mx sync.RWMutex +} + +func NewLoader(db *DB) *Loader { + l := Loader{ + DB: db, + loaded: make(map[types.GVR]struct{}), + } + + return &l +} + +func (l *Loader) isLoaded(gvr types.GVR) bool { + l.mx.RLock() + defer l.mx.RUnlock() + + _, ok := l.loaded[gvr] + + return ok +} + +func (l *Loader) setLoaded(gvr types.GVR) { + l.mx.Lock() + defer l.mx.Unlock() + + l.loaded[gvr] = struct{}{} +} + +// LoadResource loads resource and save to db. +func LoadResource[T metav1.ObjectMetaAccessor](ctx context.Context, l *Loader, gvr types.GVR) error { + if l.isLoaded(gvr) || gvr == types.BlankGVR { + return nil + } + + oo, err := loadResource(ctx, gvr) + if err != nil { + return err + } + if err = Save[T](ctx, l.DB, gvr, oo); err != nil { + return err + } + l.setLoaded(gvr) + + return nil +} + +func Cast[T any](o runtime.Object) (T, error) { + var r T + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &r); err != nil { + return r, fmt.Errorf("expecting %T resource but got %T: %w", r, o, err) + } + + return r, nil +} + +func Save[T metav1.ObjectMetaAccessor](ctx context.Context, dba *DB, gvr types.GVR, oo []runtime.Object) error { + txn := dba.Txn(true) + defer txn.Commit() + + for _, o := range oo { + u, err := Cast[T](o) + if err != nil { + return err + } + if err := txn.Insert(gvr.String(), u); err != nil { + return err + } + } + + return nil +} + +func (l *Loader) LoadPodMX(ctx context.Context) error { + pmxGVR := internal.Glossary[internal.PMX] + if l.isLoaded(pmxGVR) { + return nil + } + + c := mustExtractFactory(ctx).Client() + + log.Debug().Msg("PRELOAD PMX") + ll, err := l.fetchPodsMetrics(c) + if err != nil { + return err + } + txn := l.DB.Txn(true) + defer txn.Commit() + for _, l := range ll.Items { + if err := txn.Insert(pmxGVR.String(), &l); err != nil { + return err + } + } + l.setLoaded(pmxGVR) + + return nil +} + +func (l *Loader) LoadNodeMX(ctx context.Context) error { + c := mustExtractFactory(ctx).Client() + if !c.HasMetrics() { + return nil + } + + nmxGVR := internal.Glossary[internal.NMX] + if l.isLoaded(nmxGVR) { + return nil + } + log.Debug().Msg("PRELOAD NMX") + ll, err := l.fetchNodesMetrics(c) + if err != nil { + return err + } + txn := l.DB.Txn(true) + defer txn.Commit() + for _, l := range ll.Items { + if err := txn.Insert(nmxGVR.String(), &l); err != nil { + return err + } + } + l.setLoaded(nmxGVR) + + return nil +} + +func (l *Loader) fetchPodsMetrics(c types.Connection) (*mv1beta1.PodMetricsList, error) { + vc, err := c.MXDial() + if err != nil { + return nil, err + } + ctx, cancel := context.WithTimeout(context.Background(), client.CallTimeout) + defer cancel() + return vc.MetricsV1beta1().PodMetricses(c.ActiveNamespace()).List(ctx, metav1.ListOptions{}) +} + +func (l *Loader) fetchNodesMetrics(c types.Connection) (*mv1beta1.NodeMetricsList, error) { + vc, err := c.MXDial() + if err != nil { + return nil, err + } + + ctx, cancel := context.WithTimeout(context.Background(), client.CallTimeout) + defer cancel() + return vc.MetricsV1beta1().NodeMetricses().List(ctx, metav1.ListOptions{}) +} + +func loadResource(ctx context.Context, gvr types.GVR) ([]runtime.Object, error) { + f := mustExtractFactory(ctx) + if strings.Contains(gvr.String(), "metrics") { + if !f.Client().HasMetrics() { + return nil, nil + } + } + if gvr.IsMetricsRes() { + var res dao.Generic + res.Init(f, gvr) + return res.List(ctx) + } + + var res dao.Resource + res.Init(f, gvr) + + return res.List(ctx) +} + +func (l *Loader) LoadGeneric(ctx context.Context, gvr types.GVR) error { + if l.isLoaded(gvr) { + return nil + } + + oo, err := l.fetchGeneric(ctx, gvr) + if err != nil { + return err + } + txn := l.DB.Txn(true) + defer txn.Commit() + for _, o := range oo { + if err := txn.Insert(gvr.String(), o); err != nil { + return err + } + } + l.setLoaded(gvr) + + return nil +} + +func (l *Loader) fetchGeneric(ctx context.Context, gvr types.GVR) ([]runtime.Object, error) { + f := mustExtractFactory(ctx) + var res dao.Resource + res.Init(f, types.NewGVR(gvr.String())) + + return res.List(ctx) +} + +// Helpers... + +func mustExtractFactory(ctx context.Context) types.Factory { + f, ok := ctx.Value(internal.KeyFactory).(types.Factory) + if !ok { + panic("expecting factory in context") + } + + return f +} diff --git a/internal/db/schema/indexer.go b/internal/db/schema/indexer.go new file mode 100644 index 00000000..bb3d8cde --- /dev/null +++ b/internal/db/schema/indexer.go @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package schema + +import ( + "fmt" + + "github.com/derailed/popeye/internal/client" + "github.com/hashicorp/go-memdb" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +type MetaAccessor interface { + GetNamespace() string + GetName() string +} + +var _ MetaAccessor = (*unstructured.Unstructured)(nil) + +type fqnIndexer struct{} + +func (fqnIndexer) FromArgs(args ...any) ([]byte, error) { + if len(args) != 1 { + return nil, fmt.Errorf("must provide only a single argument") + } + + return []byte(args[0].(string)), nil +} + +func (fqnIndexer) FromObject(o any) (bool, []byte, error) { + m, ok := o.(MetaAccessor) + if !ok { + return ok, nil, fmt.Errorf("indexer expected MetaAccessor but got %T", o) + } + + return true, []byte(client.FQN(m.GetNamespace(), m.GetName())), nil +} + +type nsIndexer struct{} + +func (nsIndexer) FromArgs(args ...any) ([]byte, error) { + if len(args) != 1 { + return nil, fmt.Errorf("must provide only a single argument") + } + + return []byte(args[0].(string)), nil +} + +func (nsIndexer) FromObject(o any) (bool, []byte, error) { + m, ok := o.(MetaAccessor) + if !ok { + return ok, nil, fmt.Errorf("indexer expected MetaAccessor but got %T", o) + } + + return true, []byte(m.GetNamespace()), nil +} + +func indexFor(table string) *memdb.TableSchema { + return &memdb.TableSchema{ + Name: table, + Indexes: map[string]*memdb.IndexSchema{ + "id": { + Name: "id", + Unique: true, + Indexer: &fqnIndexer{}, + }, + "ns": { + Name: "ns", + Indexer: &nsIndexer{}, + }, + }, + } +} diff --git a/internal/db/schema/k8s.go b/internal/db/schema/k8s.go new file mode 100644 index 00000000..ea2bd510 --- /dev/null +++ b/internal/db/schema/k8s.go @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package schema + +import ( + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/types" + "github.com/hashicorp/go-memdb" +) + +// Init initializes db tables. +func Init() *memdb.DBSchema { + var sc memdb.DBSchema + sc.Tables = make(map[string]*memdb.TableSchema) + for _, gvr := range internal.Glossary { + if gvr == types.BlankGVR { + continue + } + sc.Tables[gvr.String()] = indexFor(gvr.String()) + } + + return &sc +} diff --git a/internal/db/types.go b/internal/db/types.go new file mode 100644 index 00000000..b81bee6b --- /dev/null +++ b/internal/db/types.go @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package db + +import "k8s.io/apimachinery/pkg/runtime" + +type ConvertFn func(o runtime.Object) (any, error) diff --git a/internal/glossary.go b/internal/glossary.go new file mode 100644 index 00000000..c7f33741 --- /dev/null +++ b/internal/glossary.go @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package internal + +import ( + "slices" + + "github.com/derailed/popeye/types" + "github.com/rs/zerolog/log" +) + +type R string + +var Glossary = make(Linters) + +func init() { + for _, r := range Rs { + Glossary[r] = types.BlankGVR + } +} + +const ( + CM R = "configmaps" + CL R = "cluster" + EP R = "endpoints" + NS R = "namespaces" + NO R = "nodes" + PV R = "persistentvolumes" + PVC R = "persistentvolumeclaims" + PO R = "pods" + SEC R = "secrets" + SA R = "serviceaccounts" + SVC R = "services" + DP R = "deployments" + DS R = "daemonsets" + RS R = "replicasets" + STS R = "statefulsets" + CR R = "clusterroles" + CRB R = "clusterrolebindings" + RO R = "roles" + ROB R = "rolebindings" + ING R = "ingresses" + NP R = "networkpolicies" + PDB R = "poddisruptionbudgets" + HPA R = "horizontalpodautoscalers" + PMX R = "podmetrics" + NMX R = "nodemetrics" + CJOB R = "cronjobs" + JOB R = "jobs" + GW R = "gateways" + GWC R = "gatewayclasses" + GWR R = "httproutes" +) + +var Rs = []R{ + CL, CM, EP, NS, NO, PV, PVC, PO, SEC, SA, SVC, DP, DS, RS, STS, CR, + CRB, RO, ROB, ING, NP, PDB, HPA, PMX, NMX, CJOB, JOB, GW, GWC, GWR, +} + +type Linters map[R]types.GVR + +func (ll Linters) Dump() { + log.Debug().Msg("\nLinters...") + kk := make([]R, 0, len(ll)) + for k := range ll { + kk = append(kk, k) + } + slices.Sort(kk) + for _, k := range kk { + log.Debug().Msgf("%-25s: %s", k, ll[k]) + } +} + +func (ll Linters) Include(gvr types.GVR) (R, bool) { + for k, v := range ll { + g, r := v.G(), v.R() + if g == gvr.G() && r == gvr.R() { + return k, true + } + } + + return "", false +} diff --git a/internal/gvr/types.go b/internal/gvr/types.go new file mode 100644 index 00000000..e4a4ba6d --- /dev/null +++ b/internal/gvr/types.go @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package gvr + +// type GVR1 string + +// func (g GVR) String() string { +// return string(g) +// } + +// var ( +// CM GVR = "v1/configmaps" +// Pod GVR = "v1/pods" +// NS GVR = "v1/namespaces" +// Node GVR = "v1/nodes" +// SA GVR = "v1/serviceaccounts" +// PDB GVR = "policy/v1/poddisruptionbudgets" +// PV GVR = "v1/persistentvolumes" +// PVC GVR = "v1/persistentvolumeclaims" +// SEC GVR = "v1/secrets" +// ING GVR = "networking.k8s.io/v1/ingresses" +// NP GVR = "networking.k8s.io/v1/networkpolicies" +// SVC GVR = "v1/services" +// EP GVR = "v1/endpoints" +// RO GVR = "rbac.authorization.k8s.io/v1/roles" +// RB GVR = "rbac.authorization.k8s.io/v1/rolebindings" +// CR GVR = "rbac.authorization.k8s.io/v1/clusterroles" +// CRB GVR = "rbac.authorization.k8s.io/v1/clusterrolebindings" +// DP GVR = "apps/v1/deployments" +// DS GVR = "apps/v1/daemonsets" +// RS GVR = "apps/v1/replicasets" +// STS GVR = "apps/v1/statefulsets" +// HPA GVR = "autoscaling/v1/horizontalpodautoscalers" +// PMX GVR = "metrics.k8s.io/v1beta1/podmetrics" +// NMX GVR = "metrics.k8s.io/v1beta1/podmetrics" +// ) diff --git a/internal/issues/assets/codes.yml b/internal/issues/assets/codes.yaml similarity index 74% rename from internal/issues/assets/codes.yml rename to internal/issues/assets/codes.yaml index 371c60c3..189b3842 100644 --- a/internal/issues/assets/codes.yml +++ b/internal/issues/assets/codes.yaml @@ -16,7 +16,7 @@ codes: message: No readiness probe severity: 2 105: - message: '%s probe uses a port#, prefer a named port' + message: '%s uses a port#, prefer a named port' severity: 1 106: message: No resources requests/limits defined @@ -40,7 +40,7 @@ codes: message: Memory Current/Limit (%s/%s) reached user %d%% threshold (%d%%) severity: 3 113: - message: Container image %s is not hosted on an allowed docker registry + message: Container image %q is not hosted on an allowed docker registry severity: 3 # Pod @@ -63,7 +63,7 @@ codes: message: Pod was restarted (%d) %s severity: 2 206: - message: No PodDisruptionBudget defined + message: Pod has no associated PodDisruptionBudget severity: 1 207: message: Pod is in an unhappy phase (%s) @@ -77,7 +77,7 @@ codes: # Security 300: - message: Using "default" ServiceAccount + message: Uses "default" ServiceAccount severity: 2 301: message: Connects to API Server? ServiceAccount token is mounted @@ -92,11 +92,14 @@ codes: message: References a secret "%s" which does not exist severity: 3 305: - message: References a docker-image "%s" pull secret which does not exist + message: "References a pull secret which does not exist: %s" severity: 3 306: message: Container could be running as root user. Check SecurityContext/Image severity: 2 + 307: + message: "%s references a non existing ServiceAccount: %q" + severity: 2 # General 400: @@ -120,8 +123,11 @@ codes: 406: message: K8s version OK severity: 0 + 407: + message: "%s references %s %q which does not exist" + severity: 3 - # Deployment + StatefulSet + # Pod controllers 500: message: Zero scale detected severity: 2 @@ -143,13 +149,13 @@ codes: 507: message: Deployment references ServiceAccount %q which does not exist severity: 3 + 508: + message: "No pods match controller selector: %s" + severity: 3 # HPA 600: - message: HPA %s references a Deployment %s which does not exist - severity: 3 - 601: - message: HPA %s references a StatefulSet %s which does not exist + message: "HPA %s references a %s which does not exist: %s" severity: 3 602: message: Replicas (%d/%d) at burst will match/exceed cluster CPU(%s) capacity by %s @@ -169,8 +175,8 @@ codes: message: Found taint "%s" but no pod can tolerate severity: 2 701: - message: Node is in an unknown condition - severity: 3 + message: Node has an unknown condition + severity: 2 702: message: Node is not in ready state severity: 3 @@ -212,7 +218,7 @@ codes: # PodDisruptionBudget 900: - message: Used? No pods match selector + message: "No pods match pdb selector: %s" severity: 2 901: message: MinAvailable (%d) is greater than the number of pods(%d) currently running @@ -220,11 +226,11 @@ codes: # PV/PVC 1000: - message: Available + message: Available volume detected severity: 1 1001: message: Pending volume detected - severity: 3 + severity: 2 1002: message: Lost volume detected severity: 3 @@ -266,6 +272,9 @@ codes: 1109: message: Only one Pod associated with this endpoint severity: 2 + 1110: + message: Match EP has no subsets + severity: 2 # ReplicaSet 1120: @@ -274,10 +283,31 @@ codes: # NetworkPolicies 1200: - message: No pods match %s pod selector + message: "No pods match pod selector: %s" severity: 2 1201: - message: No namespaces match %s namespace selector + message: "No namespaces match %s namespace selector: %s" + severity: 2 + 1202: + message: "No pods match %s pod selector: %s" + severity: 2 + 1203: + message: "%s %s policy in effect" + severity: 1 + 1204: + message: "Pod %s is not secured by a network policy" + severity: 2 + 1205: + message: "Pod ingress and egress are not secured by a network policy" + severity: 2 + 1206: + message: "No pods matched %s IPBlock %s" + severity: 2 + 1207: + message: "No pods matched except %s IPBlock %s" + severity: 2 + 1208: + message: "No pods match %s pod selector: %s in namespace: %s" severity: 2 # RBAC @@ -285,3 +315,32 @@ codes: 1300: message: References a %s (%s) which does not exist severity: 2 + + + # Ingress + 1400: + message: "Ingress LoadBalancer port reported an error: %s" + severity: 3 + 1401: + message: "Ingress references a service backend which does not exist: %s" + severity: 3 + 1402: + message: "Ingress references a service port which is not defined: %s" + severity: 3 + 1403: + message: 'Ingress backend uses a port#, prefer a named port: %d' + severity: 1 + 1404: + message: 'Invalid Ingress backend spec. Must use port name or number' + severity: 3 + + # Cronjob + 1500: + message: "%s is suspended" + severity: 2 + 1501: + message: No active jobs detected + severity: 1 + 1502: + message: CronJob has not been ran yet or is failing + severity: 2 \ No newline at end of file diff --git a/internal/issues/codes.go b/internal/issues/codes.go index 8fc57f7a..0ea672e2 100644 --- a/internal/issues/codes.go +++ b/internal/issues/codes.go @@ -4,21 +4,21 @@ package issues import ( - // Pull in asset codes. _ "embed" - "github.com/derailed/popeye/pkg/config" + "github.com/derailed/popeye/internal/rules" "gopkg.in/yaml.v2" ) -type ( - // Codes represents a collection of sanitizer codes. - Codes struct { - Glossary config.Glossary `yaml:"codes"` - } -) +//go:embed assets/codes.yaml +var codes string + +// Codes represents a collection of linter codes. +type Codes struct { + Glossary rules.Glossary `yaml:"codes"` +} -// LoadCodes retrieves sanitizers codes from yaml file. +// LoadCodes retrieves linters codes from yaml file. func LoadCodes() (*Codes, error) { var cc Codes if err := yaml.Unmarshal([]byte(codes), &cc); err != nil { @@ -29,23 +29,23 @@ func LoadCodes() (*Codes, error) { } // Refine overrides code severity based on user input. -func (c *Codes) Refine(gloss config.Glossary) { - for k, v := range gloss { - c, ok := c.Glossary[k] +func (c *Codes) Refine(oo rules.Overrides) { + for _, ov := range oo { + c, ok := c.Glossary[ov.ID] if !ok { continue } - if validSeverity(v.Severity) { - c.Severity = v.Severity + if validSeverity(ov.Severity) { + c.Severity = ov.Severity + } + if ov.Message != "" { + c.Message = ov.Message } } } // Helpers... -func validSeverity(l config.Level) bool { +func validSeverity(l rules.Level) bool { return l > 0 && l < 4 } - -//go:embed assets/codes.yml -var codes string diff --git a/internal/issues/codes_test.go b/internal/issues/codes_test.go index 3c8bf10e..17942685 100644 --- a/internal/issues/codes_test.go +++ b/internal/issues/codes_test.go @@ -1,10 +1,13 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + package issues_test import ( "testing" "github.com/derailed/popeye/internal/issues" - "github.com/derailed/popeye/pkg/config" + "github.com/derailed/popeye/internal/rules" "github.com/stretchr/testify/assert" ) @@ -12,33 +15,36 @@ func TestCodesLoad(t *testing.T) { cc, err := issues.LoadCodes() assert.Nil(t, err) - assert.Equal(t, 86, len(cc.Glossary)) + assert.Equal(t, 104, len(cc.Glossary)) assert.Equal(t, "No liveness probe", cc.Glossary[103].Message) - assert.Equal(t, config.WarnLevel, cc.Glossary[103].Severity) + assert.Equal(t, rules.WarnLevel, cc.Glossary[103].Severity) } func TestRefine(t *testing.T) { cc, err := issues.LoadCodes() assert.Nil(t, err) - id1, id2 := config.ID(100), config.ID(101) - gloss := config.Glossary{ - 0: &config.Code{ + ov := rules.Overrides{ + rules.CodeOverride{ + ID: 0, Message: "blah", - Severity: config.InfoLevel, + Severity: rules.InfoLevel, }, - id1: &config.Code{ + rules.CodeOverride{ + ID: 100, Message: "blah", - Severity: config.InfoLevel, + Severity: rules.InfoLevel, }, - id2: &config.Code{ + + rules.CodeOverride{ + ID: 101, Message: "blah", Severity: 1000, }, } - cc.Refine(gloss) + cc.Refine(ov) - assert.Equal(t, config.InfoLevel, cc.Glossary[id1].Severity) - assert.Equal(t, config.WarnLevel, cc.Glossary[id2].Severity) + assert.Equal(t, rules.InfoLevel, cc.Glossary[100].Severity) + assert.Equal(t, rules.WarnLevel, cc.Glossary[101].Severity) } diff --git a/internal/issues/collector.go b/internal/issues/collector.go index e785b087..b5d3325d 100644 --- a/internal/issues/collector.go +++ b/internal/issues/collector.go @@ -8,11 +8,12 @@ import ( "fmt" "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/rules" "github.com/derailed/popeye/pkg/config" "github.com/rs/zerolog/log" ) -// Collector represents a sanitizer issue container. +// Collector tracks linter issues and codes. type Collector struct { *config.Config @@ -35,6 +36,12 @@ func (c *Collector) InitOutcome(fqn string) { c.outcomes[fqn] = Issues{} } +func (c *Collector) CloseOutcome(ctx context.Context, fqn string, cos []string) { + if c.NoConcerns(fqn) && c.Config.ExcludeFQN(internal.MustExtractSectionGVR(ctx), fqn, cos) { + c.ClearOutcome(fqn) + } +} + // ClearOutcome delete all fqn related issues. func (c *Collector) ClearOutcome(fqn string) { delete(c.outcomes, fqn) @@ -46,39 +53,42 @@ func (c *Collector) NoConcerns(fqn string) bool { } // MaxSeverity return the highest severity level for the given section. -func (c *Collector) MaxSeverity(fqn string) config.Level { +func (c *Collector) MaxSeverity(fqn string) rules.Level { return c.outcomes.MaxSeverity(fqn) } // AddSubCode add a sub error code. -func (c *Collector) AddSubCode(ctx context.Context, code config.ID, args ...interface{}) { +func (c *Collector) AddSubCode(ctx context.Context, code rules.ID, args ...interface{}) { run := internal.MustExtractRunInfo(ctx) co, ok := c.codes.Glossary[code] if !ok { log.Error().Err(fmt.Errorf("No code with ID %d", code)).Msg("AddSubCode failed") } - if co.Severity < config.Level(c.Config.LintLevel) { + if co.Severity < rules.Level(c.Config.LintLevel) { return } - if !c.ShouldExclude(run.SectionGVR.String(), run.FQN, code) { - c.addIssue(run.FQN, New(run.GroupGVR, run.Group, co.Severity, co.Format(code, args...))) + run.Spec.GVR, run.Spec.Code = run.SectionGVR, code + if !c.Match(run.Spec) { + c.addIssue(run.Spec.FQN, New(run.GroupGVR, run.Group, co.Severity, co.Format(code, args...))) } } // AddCode add an error code. -func (c *Collector) AddCode(ctx context.Context, code config.ID, args ...interface{}) { +func (c *Collector) AddCode(ctx context.Context, code rules.ID, args ...interface{}) { run := internal.MustExtractRunInfo(ctx) co, ok := c.codes.Glossary[code] if !ok { // BOZO!! refact once codes are in!! panic(fmt.Errorf("no codes found with id %d", code)) } - if co.Severity < config.Level(c.Config.LintLevel) { + if co.Severity < rules.Level(c.Config.LintLevel) { return } - if !c.ShouldExclude(run.SectionGVR.String(), run.FQN, code) { - c.addIssue(run.FQN, New(run.SectionGVR, Root, co.Severity, co.Format(code, args...))) + + run.Spec.GVR, run.Spec.Code = run.SectionGVR, code + if !c.Match(run.Spec) { + c.addIssue(run.Spec.FQN, New(run.SectionGVR, Root, co.Severity, co.Format(code, args...))) } } @@ -86,7 +96,7 @@ func (c *Collector) AddCode(ctx context.Context, code config.ID, args ...interfa func (c *Collector) AddErr(ctx context.Context, errs ...error) { run := internal.MustExtractRunInfo(ctx) for _, e := range errs { - c.addIssue(run.FQN, New(run.SectionGVR, Root, config.ErrorLevel, e.Error())) + c.addIssue(run.Spec.FQN, New(run.SectionGVR, Root, rules.ErrorLevel, e.Error())) } } diff --git a/internal/issues/collector_test.go b/internal/issues/collector_test.go index fafca39b..5f930061 100644 --- a/internal/issues/collector_test.go +++ b/internal/issues/collector_test.go @@ -9,8 +9,9 @@ import ( "testing" "github.com/derailed/popeye/internal" - "github.com/derailed/popeye/internal/client" + "github.com/derailed/popeye/internal/rules" "github.com/derailed/popeye/pkg/config" + "github.com/derailed/popeye/types" "github.com/stretchr/testify/assert" ) @@ -24,8 +25,8 @@ func TestNoConcerns(t *testing.T) { }, "issues": { issues: []Issue{ - New(client.NewGVR("blee"), Root, config.InfoLevel, "blee"), - New(client.NewGVR("blee"), Root, config.WarnLevel, "blee"), + New(types.NewGVR("blee"), Root, rules.InfoLevel, "blee"), + New(types.NewGVR("blee"), Root, rules.WarnLevel, "blee"), }, }, } @@ -45,40 +46,40 @@ func TestMaxSeverity(t *testing.T) { uu := map[string]struct { issues []Issue section string - severity config.Level + severity rules.Level count int }{ "noIssue": { section: Root, - severity: config.OkLevel, + severity: rules.OkLevel, count: 0, }, "mix": { issues: []Issue{ - New(client.NewGVR("fred"), Root, config.InfoLevel, "blee"), - New(client.NewGVR("fred"), Root, config.WarnLevel, "blee"), + New(types.NewGVR("fred"), Root, rules.InfoLevel, "blee"), + New(types.NewGVR("fred"), Root, rules.WarnLevel, "blee"), }, section: Root, - severity: config.WarnLevel, + severity: rules.WarnLevel, count: 2, }, "same": { issues: []Issue{ - New(client.NewGVR("fred"), Root, config.InfoLevel, "blee"), - New(client.NewGVR("fred"), Root, config.InfoLevel, "blee"), + New(types.NewGVR("fred"), Root, rules.InfoLevel, "blee"), + New(types.NewGVR("fred"), Root, rules.InfoLevel, "blee"), }, section: Root, - severity: config.InfoLevel, + severity: rules.InfoLevel, count: 2, }, "error": { issues: []Issue{ - New(client.NewGVR("fred"), Root, config.ErrorLevel, "blee"), - New(client.NewGVR("fred"), Root, config.InfoLevel, "blee"), - New(client.NewGVR("fred"), Root, config.InfoLevel, "blee"), + New(types.NewGVR("fred"), Root, rules.ErrorLevel, "blee"), + New(types.NewGVR("fred"), Root, rules.InfoLevel, "blee"), + New(types.NewGVR("fred"), Root, rules.InfoLevel, "blee"), }, section: Root, - severity: config.ErrorLevel, + severity: rules.ErrorLevel, count: 3, }, } @@ -126,43 +127,43 @@ func TestAddErr(t *testing.T) { c.AddErr(ctx, u.errors...) assert.Equal(t, u.count, len(c.outcomes[u.fqn])) - assert.Equal(t, config.ErrorLevel, c.MaxSeverity(u.fqn)) + assert.Equal(t, rules.ErrorLevel, c.MaxSeverity(u.fqn)) }) } } func TestAddCode(t *testing.T) { uu := map[string]struct { - code config.ID + code rules.ID fqn string args []interface{} - level config.Level + level rules.Level e string }{ "No params": { code: 100, fqn: Root, - level: config.ErrorLevel, + level: rules.ErrorLevel, e: "[POP-100] Untagged docker image in use", }, "Params": { code: 108, fqn: Root, - level: config.InfoLevel, + level: rules.InfoLevel, args: []interface{}{80}, e: "[POP-108] Unnamed port 80", }, "Dud!": { code: 0, fqn: Root, - level: config.InfoLevel, + level: rules.InfoLevel, args: []interface{}{80}, e: "[POP-108] Unnamed port 80", }, "Issue 169": { code: 1102, fqn: Root, - level: config.InfoLevel, + level: rules.InfoLevel, args: []interface{}{"123", "test-port"}, e: "[POP-1102] Use of target port #123 for service port test-port. Prefer named port", }, @@ -180,7 +181,6 @@ func TestAddCode(t *testing.T) { assert.Panics(t, subCode, "blee") } else { c.AddCode(ctx, u.code, u.args...) - assert.Equal(t, u.e, c.outcomes[u.fqn][0].Message) assert.Equal(t, u.level, c.outcomes[u.fqn][0].Level) } @@ -190,24 +190,24 @@ func TestAddCode(t *testing.T) { func TestAddSubCode(t *testing.T) { uu := map[string]struct { - code config.ID + code rules.ID section, group string args []interface{} - level config.Level + level rules.Level e string }{ "No params": { code: 100, section: Root, group: "blee", - level: config.ErrorLevel, + level: rules.ErrorLevel, e: "[POP-100] Untagged docker image in use", }, "Params": { code: 108, section: Root, group: "blee", - level: config.InfoLevel, + level: rules.InfoLevel, args: []interface{}{80}, e: "[POP-108] Unnamed port 80", }, @@ -215,7 +215,7 @@ func TestAddSubCode(t *testing.T) { code: 0, section: Root, group: "blee", - level: config.InfoLevel, + level: rules.InfoLevel, args: []interface{}{80}, e: "[POP-108] Unnamed port 80", }, @@ -245,11 +245,12 @@ func TestAddSubCode(t *testing.T) { // Helpers... -func loadCodes(t *testing.T) *Codes { - codes, err := LoadCodes() - assert.Nil(t, err) - - return codes +func makeContext(section, fqn, group string) context.Context { + return context.WithValue(context.Background(), internal.KeyRunInfo, internal.RunInfo{ + Section: section, + Group: group, + Spec: rules.Spec{FQN: fqn}, + }) } func makeConfig(t *testing.T) *config.Config { @@ -258,10 +259,9 @@ func makeConfig(t *testing.T) *config.Config { return c } -func makeContext(section, fqn, group string) context.Context { - return context.WithValue(context.Background(), internal.KeyRunInfo, internal.RunInfo{ - Section: section, - Group: group, - FQN: fqn, - }) +func loadCodes(t *testing.T) *Codes { + codes, err := LoadCodes() + assert.Nil(t, err) + + return codes } diff --git a/internal/issues/issue.go b/internal/issues/issue.go index c90553aa..932f4eec 100644 --- a/internal/issues/issue.go +++ b/internal/issues/issue.go @@ -5,34 +5,49 @@ package issues import ( "fmt" + "regexp" - "github.com/derailed/popeye/internal/client" - "github.com/derailed/popeye/pkg/config" + "github.com/derailed/popeye/internal/rules" + "github.com/derailed/popeye/types" ) +var codeRX = regexp.MustCompile(`\A\[POP-(\d+)\]`) + // Blank issue var Blank = Issue{} -type ( - // Issue tracks a sanitizer issue. - Issue struct { - Group string `yaml:"group" json:"group"` - GVR string `yaml:"gvr" json:"gvr"` - Level config.Level `yaml:"level" json:"level"` - Message string `yaml:"message" json:"message"` - } -) +// Issue tracks a linter issue. +type Issue struct { + Group string `yaml:"group" json:"group"` + GVR string `yaml:"gvr" json:"gvr"` + Level rules.Level `yaml:"level" json:"level"` + Message string `yaml:"message" json:"message"` +} // New returns a new lint issue. -func New(gvr client.GVR, group string, level config.Level, description string) Issue { +func New(gvr types.GVR, group string, level rules.Level, description string) Issue { return Issue{GVR: gvr.String(), Group: group, Level: level, Message: description} } // Newf returns a new lint issue using a formatter. -func Newf(gvr client.GVR, group string, level config.Level, format string, args ...interface{}) Issue { +func Newf(gvr types.GVR, group string, level rules.Level, format string, args ...interface{}) Issue { return New(gvr, group, level, fmt.Sprintf(format, args...)) } +func (i Issue) Code() (string, bool) { + mm := codeRX.FindStringSubmatch(i.Message) + if len(mm) < 2 { + return "", false + } + + return mm[1], true +} + +// Dump for debugging. +func (i Issue) Dump() { + fmt.Printf(" %s (%d) %s\n", i.GVR, i.Level, i.Message) +} + // Blank checks if an issue is blank. func (i Issue) Blank() bool { return i == Blank @@ -44,14 +59,14 @@ func (i Issue) IsSubIssue() bool { } // LevelToStr returns a severity level as a string. -func LevelToStr(l config.Level) string { +func LevelToStr(l rules.Level) string { // nolint:exhaustive switch l { - case config.ErrorLevel: + case rules.ErrorLevel: return "error" - case config.WarnLevel: + case rules.WarnLevel: return "warn" - case config.InfoLevel: + case rules.InfoLevel: return "info" default: return "ok" diff --git a/internal/issues/issue_test.go b/internal/issues/issue_test.go index cdf43132..7354e976 100644 --- a/internal/issues/issue_test.go +++ b/internal/issues/issue_test.go @@ -6,8 +6,8 @@ package issues import ( "testing" - "github.com/derailed/popeye/internal/client" - "github.com/derailed/popeye/pkg/config" + "github.com/derailed/popeye/internal/rules" + "github.com/derailed/popeye/types" "github.com/stretchr/testify/assert" ) @@ -16,10 +16,10 @@ func TestIsSubIssues(t *testing.T) { i Issue e bool }{ - "root": {New(client.NewGVR("fred"), Root, config.WarnLevel, "blah"), false}, - "rootf": {Newf(client.NewGVR("fred"), Root, config.WarnLevel, "blah %s", "blee"), false}, - "sub": {New(client.NewGVR("fred"), "sub1", config.WarnLevel, "blah"), true}, - "subf": {Newf(client.NewGVR("fred"), "sub1", config.WarnLevel, "blah %s", "blee"), true}, + "root": {New(types.NewGVR("fred"), Root, rules.WarnLevel, "blah"), false}, + "rootf": {Newf(types.NewGVR("fred"), Root, rules.WarnLevel, "blah %s", "blee"), false}, + "sub": {New(types.NewGVR("fred"), "sub1", rules.WarnLevel, "blah"), true}, + "subf": {Newf(types.NewGVR("fred"), "sub1", rules.WarnLevel, "blah %s", "blee"), true}, } for k := range uu { @@ -36,7 +36,7 @@ func TestBlank(t *testing.T) { e bool }{ "blank": {Issue{}, true}, - "notBlank": {New(client.NewGVR("fred"), Root, config.WarnLevel, "blah"), false}, + "notBlank": {New(types.NewGVR("fred"), Root, rules.WarnLevel, "blah"), false}, } for k := range uu { diff --git a/internal/issues/issues.go b/internal/issues/issues.go index 06204ea2..050fdc19 100644 --- a/internal/issues/issues.go +++ b/internal/issues/issues.go @@ -4,9 +4,15 @@ package issues import ( - "sort" - - "github.com/derailed/popeye/pkg/config" + "encoding/json" + "fmt" + "slices" + "strconv" + "strings" + + "github.com/derailed/popeye/internal/client" + "github.com/derailed/popeye/internal/issues/tally" + "github.com/derailed/popeye/internal/rules" ) // Root denotes a root issue group. @@ -20,9 +26,24 @@ type ( Outcome map[string]Issues ) +func (i Issues) CodeTally() tally.Code { + ss := make(tally.Code) + for _, issue := range i { + if c, ok := issue.Code(); ok { + if v, ok := ss[c]; ok { + ss[c] = v + 1 + } else { + ss[c] = 1 + } + } + } + + return ss +} + // MaxSeverity gather the max severity in a collection of issues. -func (i Issues) MaxSeverity() config.Level { - max := config.OkLevel +func (i Issues) MaxSeverity() rules.Level { + max := rules.OkLevel for _, is := range i { if is.Level > max { max = is.Level @@ -32,30 +53,39 @@ func (i Issues) MaxSeverity() config.Level { return max } +func (i Issues) HasIssues() bool { + return len(i) > 0 +} + +func SortKeys(k1, k2 string) int { + v1, err := strconv.Atoi(k1) + if err == nil { + v2, _ := strconv.Atoi(k2) + switch { + case v1 == v2: + return 0 + case v1 < v2: + return -1 + default: + return 1 + } + } + + return strings.Compare(k1, k2) +} + // Sort sorts issues. -func (i Issues) Sort(l config.Level) Issues { +func (i Issues) Sort(l rules.Level) Issues { ii := make(Issues, 0, len(i)) gg := i.Group() - keys := make(sort.StringSlice, 0, len(gg)) + kk := make([]string, 0, len(gg)) for k := range gg { - keys = append(keys, k) + kk = append(kk, k) } - keys.Sort() - for _, group := range keys { - sev := gg[group].MaxSeverity() - if sev < l { - continue - } - for _, i := range gg[group] { - if i.Level < l { - continue - } - if i.Group == Root { - ii = append(ii, i) - continue - } - ii = append(ii, i) - } + slices.SortFunc(kk, SortKeys) + + for _, k := range kk { + ii = append(ii, gg[k]...) } return ii } @@ -70,13 +100,69 @@ func (i Issues) Group() map[string]Issues { return res } +// NSTally collects Namespace code tally for a given linter. +func (o Outcome) NSTally() tally.Namespace { + nn := make(tally.Namespace, len(o)) + for fqn, v := range o { + ns, _ := client.Namespaced(fqn) + if ns == "" { + ns = "-" + } + tt := v.CodeTally() + if v1, ok := nn[ns]; ok { + v1.Merge(tt) + } else { + nn[ns] = tt + } + } + + return nn +} + // MaxSeverity scans the issues and reports the highest severity. -func (o Outcome) MaxSeverity(section string) config.Level { +func (o Outcome) MaxSeverity(section string) rules.Level { return o[section].MaxSeverity() } +func (o Outcome) MarshalJSON() ([]byte, error) { + out := make([]string, 0, len(o)) + for k, v := range o { + if len(v) == 0 { + continue + } + raw, err := json.Marshal(v) + if err != nil { + return nil, err + } + out = append(out, fmt.Sprintf("%q: %s", k, raw)) + } + + return []byte("{" + strings.Join(out, ",") + "}"), nil +} + +func (o Outcome) MarshalYAML() (interface{}, error) { + out := make(Outcome, len(o)) + for k, v := range o { + if len(v) == 0 { + continue + } + out[k] = v + } + + return out, nil +} + +func (o Outcome) HasIssues() bool { + var count int + for _, ii := range o { + count += len(ii) + } + + return count > 0 +} + // MaxGroupSeverity scans the issues and reports the highest severity. -func (o Outcome) MaxGroupSeverity(section, group string) config.Level { +func (o Outcome) MaxGroupSeverity(section, group string) rules.Level { return o.For(section, group).MaxSeverity() } @@ -93,8 +179,8 @@ func (o Outcome) For(section, group string) Issues { return ii } -// Filter filters outcomes based on sanitizer level. -func (o Outcome) Filter(level config.Level) Outcome { +// Filter filters outcomes based on lint level. +func (o Outcome) Filter(level rules.Level) Outcome { for k, issues := range o { vv := make(Issues, 0, len(issues)) for _, issue := range issues { @@ -106,3 +192,15 @@ func (o Outcome) Filter(level config.Level) Outcome { } return o } + +func (o Outcome) Dump() { + if len(o) == 0 { + fmt.Println("No ISSUES!") + } + for k, ii := range o { + fmt.Println(k) + for _, i := range ii { + i.Dump() + } + } +} diff --git a/internal/issues/issues_test.go b/internal/issues/issues_test.go index 1f5ddabf..26d5f3a7 100644 --- a/internal/issues/issues_test.go +++ b/internal/issues/issues_test.go @@ -6,37 +6,37 @@ package issues import ( "testing" - "github.com/derailed/popeye/internal/client" - "github.com/derailed/popeye/pkg/config" + "github.com/derailed/popeye/internal/rules" + "github.com/derailed/popeye/types" "github.com/stretchr/testify/assert" ) func TestMaxGroupSeverity(t *testing.T) { o := Outcome{ "s1": Issues{ - New(client.NewGVR("fred"), Root, config.OkLevel, "i1"), + New(types.NewGVR("fred"), Root, rules.OkLevel, "i1"), }, "s2": Issues{ - New(client.NewGVR("fred"), Root, config.OkLevel, "i1"), - New(client.NewGVR("fred"), Root, config.WarnLevel, "i2"), - New(client.NewGVR("fred"), "g1", config.WarnLevel, "i2"), + New(types.NewGVR("fred"), Root, rules.OkLevel, "i1"), + New(types.NewGVR("fred"), Root, rules.WarnLevel, "i2"), + New(types.NewGVR("fred"), "g1", rules.WarnLevel, "i2"), }, } - assert.Equal(t, config.OkLevel, o.MaxGroupSeverity("s1", Root)) - assert.Equal(t, config.WarnLevel, o.MaxGroupSeverity("s2", Root)) + assert.Equal(t, rules.OkLevel, o.MaxGroupSeverity("s1", Root)) + assert.Equal(t, rules.WarnLevel, o.MaxGroupSeverity("s2", Root)) } func TestIssuesForGroup(t *testing.T) { o := Outcome{ "s1": Issues{ - New(client.NewGVR("fred"), Root, config.OkLevel, "i1"), + New(types.NewGVR("fred"), Root, rules.OkLevel, "i1"), }, "s2": Issues{ - New(client.NewGVR("fred"), Root, config.OkLevel, "i1"), - New(client.NewGVR("fred"), Root, config.WarnLevel, "i2"), - New(client.NewGVR("fred"), "g1", config.WarnLevel, "i3"), - New(client.NewGVR("fred"), "g1", config.WarnLevel, "i4"), + New(types.NewGVR("fred"), Root, rules.OkLevel, "i1"), + New(types.NewGVR("fred"), Root, rules.WarnLevel, "i2"), + New(types.NewGVR("fred"), "g1", rules.WarnLevel, "i3"), + New(types.NewGVR("fred"), "g1", rules.WarnLevel, "i4"), }, } @@ -47,13 +47,13 @@ func TestIssuesForGroup(t *testing.T) { func TestGroup(t *testing.T) { o := Outcome{ "s2": Issues{ - New(client.NewGVR("fred"), Root, config.OkLevel, "i1"), - New(client.NewGVR("fred"), Root, config.WarnLevel, "i2"), - New(client.NewGVR("fred"), "g1", config.ErrorLevel, "i2"), + New(types.NewGVR("fred"), Root, rules.OkLevel, "i1"), + New(types.NewGVR("fred"), Root, rules.WarnLevel, "i2"), + New(types.NewGVR("fred"), "g1", rules.ErrorLevel, "i2"), }, } grp := o["s2"].Group() - assert.Equal(t, config.ErrorLevel, o["s2"].MaxSeverity()) + assert.Equal(t, rules.ErrorLevel, o["s2"].MaxSeverity()) assert.Equal(t, 2, len(grp)) } diff --git a/internal/issues/tally/code.go b/internal/issues/tally/code.go new file mode 100644 index 00000000..e1a70493 --- /dev/null +++ b/internal/issues/tally/code.go @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package tally + +import ( + "slices" + "strconv" + + "github.com/derailed/popeye/internal/rules" + "github.com/rs/zerolog/log" +) + +// SevScore tracks per level total score. +type SevScore map[rules.Level]int + +// Code tracks code issue counts. +type Code map[string]int + +// Compact removes zero entries. +func (cc Code) Compact() { + for c, v := range cc { + if v == 0 { + delete(cc, c) + } + } +} + +// Rollup rollups code scores per severity. +func (cc Code) Rollup(gg rules.Glossary) SevScore { + if len(cc) == 0 { + return nil + } + ss := make(SevScore, len(cc)) + for sid, count := range cc { + id, _ := strconv.Atoi(sid) + c := gg[rules.ID(id)] + ss[c.Severity] += count + } + + return ss +} + +// Merge merges two sets. +func (cc Code) Merge(cc1 Code) { + for code, count := range cc1 { + cc[code] += count + } +} + +// Dump for debugging. +func (cc Code) Dump(indent string) { + kk := make([]string, 0, len(cc)) + for k := range cc { + kk = append(kk, k) + } + slices.Sort(kk) + for _, k := range kk { + log.Debug().Msgf("%s%s: %d", indent, k, cc[k]) + } +} diff --git a/internal/issues/tally/code_test.go b/internal/issues/tally/code_test.go new file mode 100644 index 00000000..0f887acd --- /dev/null +++ b/internal/issues/tally/code_test.go @@ -0,0 +1,219 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package tally_test + +import ( + "testing" + + "github.com/derailed/popeye/internal/issues/tally" + "github.com/derailed/popeye/internal/rules" + "github.com/stretchr/testify/assert" +) + +func TestCodeMerge(t *testing.T) { + uu := map[string]struct { + c1, c2, e tally.Code + }{ + "empty": {}, + "empty-1": { + c1: tally.Code{}, + c2: tally.Code{ + "100": 1, + "101": 2, + "102": 5, + "103": 6, + }, + e: tally.Code{ + "100": 1, + "101": 2, + "102": 5, + "103": 6, + }, + }, + "empty-2": { + c1: tally.Code{ + "100": 1, + "101": 2, + "102": 5, + "103": 6, + }, + c2: tally.Code{}, + e: tally.Code{ + "100": 1, + "101": 2, + "102": 5, + "103": 6, + }, + }, + + "same": { + c1: tally.Code{ + "100": 1, + "101": 2, + "102": 5, + "103": 6, + }, + c2: tally.Code{ + "100": 1, + "101": 2, + "102": 5, + "103": 6, + }, + e: tally.Code{ + "100": 2, + "101": 4, + "102": 10, + "103": 12, + }, + }, + "delta": { + c1: tally.Code{ + "100": 1, + "101": 2, + "102": 5, + "103": 6, + }, + c2: tally.Code{ + "102": 5, + "200": 1, + "201": 2, + "203": 6, + }, + e: tally.Code{ + "100": 1, + "101": 2, + "102": 10, + "103": 6, + "200": 1, + "201": 2, + "203": 6, + }, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + u.c1.Merge(u.c2) + assert.Equal(t, u.e, u.c1) + }) + } +} + +func TestCodeCompact(t *testing.T) { + uu := map[string]struct { + c, e tally.Code + }{ + "empty": {}, + "none": { + c: tally.Code{ + "100": 1, + "101": 2, + "102": 5, + "103": 6, + }, + e: tally.Code{ + "100": 1, + "101": 2, + "102": 5, + "103": 6, + }, + }, + "happy": { + c: tally.Code{ + "100": 1, + "101": 2, + "200": 0, + "201": 6, + "202": 0, + "203": 0, + }, + e: tally.Code{ + "100": 1, + "101": 2, + "201": 6, + }, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + u.c.Compact() + assert.Equal(t, u.e, u.c) + }) + } +} + +func TestCodeRollup(t *testing.T) { + uu := map[string]struct { + c tally.Code + e tally.SevScore + }{ + "empty": {}, + "plain": { + c: tally.Code{ + "100": 1, + "101": 2, + "102": 5, + "103": 6, + }, + e: tally.SevScore{ + 0: 6, + 1: 5, + 2: 2, + 3: 1, + }, + }, + "singles": { + c: tally.Code{ + "100": 1, + "101": 2, + "200": 5, + "201": 6, + "202": 20, + "203": 10, + }, + e: tally.SevScore{ + 0: 10, + 1: 20, + 2: 8, + 3: 6, + }, + }, + } + + g := rules.Glossary{ + 100: { + Severity: rules.ErrorLevel, + }, + 101: { + Severity: rules.WarnLevel, + }, + 102: { + Severity: rules.InfoLevel, + }, + 103: { + Severity: rules.OkLevel, + }, + 200: { + Severity: rules.ErrorLevel, + }, + 201: { + Severity: rules.WarnLevel, + }, + 202: { + Severity: rules.InfoLevel, + }, + 203: { + Severity: rules.OkLevel, + }, + } + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, u.c.Rollup(g)) + }) + } +} diff --git a/internal/issues/tally/linter.go b/internal/issues/tally/linter.go new file mode 100644 index 00000000..4f263614 --- /dev/null +++ b/internal/issues/tally/linter.go @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package tally + +import ( + "slices" + + "github.com/rs/zerolog/log" +) + +// Linter tracks linters namespace tallies. +type Linter map[string]Namespace + +func (l Linter) Compact() { + for linter, v := range l { + v.Compact() + if len(v) == 0 { + delete(l, linter) + } + } +} + +func (s Linter) Dump() { + kk := make([]string, 0, len(s)) + for k := range s { + kk = append(kk, k) + } + slices.Sort(kk) + for _, k := range kk { + log.Debug().Msgf("%s", k) + s[k].Dump(" ") + } +} diff --git a/internal/issues/tally/linter_test.go b/internal/issues/tally/linter_test.go new file mode 100644 index 00000000..0d673bf3 --- /dev/null +++ b/internal/issues/tally/linter_test.go @@ -0,0 +1,173 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package tally_test + +import ( + "testing" + + "github.com/derailed/popeye/internal/issues/tally" + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" +) + +func init() { + zerolog.SetGlobalLevel(zerolog.FatalLevel) +} + +func TestLinterCompact(t *testing.T) { + uu := map[string]struct { + lt, e tally.Linter + }{ + "empty": {}, + "multi": { + lt: tally.Linter{ + "a": tally.Namespace{ + "ns1": tally.Code{ + "100": 0, + "101": 2, + "102": 5, + "103": 0, + }, + "ns2": tally.Code{ + "100": 1, + "101": 0, + "102": 0, + "103": 6, + }, + }, + "b": tally.Namespace{ + "ns1": tally.Code{ + "100": 0, + "101": 2, + "102": 5, + "103": 0, + }, + "ns3": tally.Code{ + "100": 1, + "101": 0, + "102": 0, + "103": 6, + }, + }, + }, + e: tally.Linter{ + "a": tally.Namespace{ + "ns1": tally.Code{ + "101": 2, + "102": 5, + }, + "ns2": tally.Code{ + "100": 1, + "103": 6, + }, + }, + "b": tally.Namespace{ + "ns1": tally.Code{ + "101": 2, + "102": 5, + }, + "ns3": tally.Code{ + "100": 1, + "103": 6, + }, + }, + }, + }, + "delete-ns": { + lt: tally.Linter{ + "a": tally.Namespace{ + "ns1": tally.Code{ + "100": 0, + "101": 0, + "102": 0, + "103": 0, + }, + "ns2": tally.Code{ + "100": 1, + "101": 0, + "102": 0, + "103": 6, + }, + }, + "b": tally.Namespace{ + "ns1": tally.Code{ + "100": 0, + "101": 0, + "102": 0, + "103": 0, + }, + "ns3": tally.Code{ + "100": 1, + "101": 0, + "102": 0, + "103": 6, + }, + }, + }, + e: tally.Linter{ + "a": tally.Namespace{ + "ns2": tally.Code{ + "100": 1, + "103": 6, + }, + }, + "b": tally.Namespace{ + "ns3": tally.Code{ + "100": 1, + "103": 6, + }, + }, + }, + }, + "delete-linter": { + lt: tally.Linter{ + "a": tally.Namespace{ + "ns1": tally.Code{ + "100": 0, + "101": 0, + "102": 0, + "103": 0, + }, + "ns2": tally.Code{ + "100": 0, + "101": 0, + "102": 0, + "103": 0, + }, + }, + "b": tally.Namespace{ + "ns1": tally.Code{ + "100": 0, + "101": 0, + "102": 0, + "103": 0, + }, + "ns3": tally.Code{ + "100": 1, + "101": 0, + "102": 0, + "103": 6, + }, + }, + }, + e: tally.Linter{ + "b": tally.Namespace{ + "ns3": tally.Code{ + "100": 1, + "103": 6, + }, + }, + }, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + u.lt.Compact() + u.lt.Dump() + assert.Equal(t, u.e, u.lt) + }) + } +} diff --git a/internal/issues/tally/ns.go b/internal/issues/tally/ns.go new file mode 100644 index 00000000..3f5c9f74 --- /dev/null +++ b/internal/issues/tally/ns.go @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package tally + +import ( + "slices" + "strings" + + "github.com/rs/zerolog/log" +) + +// Namespace tracks each namespace code tally. +type Namespace map[string]Code + +// Compact compacts set by removing zero entries. +func (nn Namespace) Compact() { + for ns, v := range nn { + v.Compact() + if len(v) == 0 { + delete(nn, ns) + } + } +} + +// Merge merges 2 sets. +func (nn Namespace) Merge(t Namespace) { + for k, v := range t { + if v1, ok := nn[k]; ok { + nn[k].Merge(v1) + } else { + nn[k] = v + } + } +} + +// Dump for debugging. +func (s Namespace) Dump(indent string) { + kk := make([]string, 0, len(s)) + for k := range s { + kk = append(kk, k) + } + slices.Sort(kk) + for _, k := range kk { + log.Debug().Msgf("%s%s", indent, k) + s[k].Dump(strings.Repeat(indent, 2)) + } +} diff --git a/internal/issues/tally/ns_test.go b/internal/issues/tally/ns_test.go new file mode 100644 index 00000000..6f8bcfc9 --- /dev/null +++ b/internal/issues/tally/ns_test.go @@ -0,0 +1,175 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package tally_test + +import ( + "testing" + + "github.com/derailed/popeye/internal/issues/tally" + "github.com/stretchr/testify/assert" +) + +func TestNSMerge(t *testing.T) { + uu := map[string]struct { + ns1, ns2, e tally.Namespace + }{ + "empty": {}, + "one-way": { + ns1: tally.Namespace{ + "ns1": tally.Code{ + "100": 1, + "101": 2, + "102": 5, + "103": 6, + }, + }, + ns2: tally.Namespace{}, + e: tally.Namespace{ + "ns1": tally.Code{ + "100": 1, + "101": 2, + "102": 5, + "103": 6, + }, + }, + }, + "union": { + ns1: tally.Namespace{ + "ns1": tally.Code{ + "100": 1, + "101": 2, + "102": 5, + "103": 6, + }, + }, + ns2: tally.Namespace{ + "ns2": tally.Code{ + "100": 1, + "101": 2, + "102": 5, + "103": 6, + }, + }, + e: tally.Namespace{ + "ns1": tally.Code{ + "100": 1, + "101": 2, + "102": 5, + "103": 6, + }, + "ns2": tally.Code{ + "100": 1, + "101": 2, + "102": 5, + "103": 6, + }, + }, + }, + "intersect": { + ns1: tally.Namespace{ + "ns1": tally.Code{ + "100": 1, + "101": 2, + "102": 5, + "103": 6, + }, + }, + ns2: tally.Namespace{ + "ns1": tally.Code{ + "100": 1, + "101": 2, + "102": 5, + "103": 6, + }, + "ns2": tally.Code{ + "100": 1, + "101": 2, + "102": 5, + "103": 6, + }, + }, + e: tally.Namespace{ + "ns1": tally.Code{ + "100": 2, + "101": 4, + "102": 10, + "103": 12, + }, + "ns2": tally.Code{ + "100": 1, + "101": 2, + "102": 5, + "103": 6, + }, + }, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + u.ns1.Merge(u.ns2) + assert.Equal(t, u.e, u.ns1) + }) + } +} + +func TestNSCompact(t *testing.T) { + uu := map[string]struct { + ns1, e tally.Namespace + }{ + "empty": {}, + "multi": { + ns1: tally.Namespace{ + "ns1": tally.Code{ + "100": 0, + "101": 2, + "102": 5, + "103": 0, + }, + "ns2": tally.Code{ + "100": 1, + "101": 0, + "102": 0, + "103": 6, + }, + }, + e: tally.Namespace{ + "ns1": tally.Code{ + "101": 2, + "102": 5, + }, + "ns2": tally.Code{ + "100": 1, + "103": 6, + }, + }, + }, + "single": { + ns1: tally.Namespace{ + "ns1": tally.Code{ + "100": 1, + "101": 0, + "102": 5, + "103": 6, + }, + }, + e: tally.Namespace{ + "ns1": tally.Code{ + "100": 1, + "102": 5, + "103": 6, + }, + }, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + u.ns1.Compact() + assert.Equal(t, u.e, u.ns1) + }) + } +} diff --git a/internal/keys.go b/internal/keys.go index 03a8aa09..ba1a57b4 100644 --- a/internal/keys.go +++ b/internal/keys.go @@ -16,4 +16,5 @@ const ( KeyConfig ContextKey = "config" KeyNamespace ContextKey = "namespace" KeyVersion ContextKey = "version" + KeyDB ContextKey = "db" ) diff --git a/internal/lint/cluster.go b/internal/lint/cluster.go new file mode 100644 index 00000000..cf35546a --- /dev/null +++ b/internal/lint/cluster.go @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "context" + + "github.com/Masterminds/semver" + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/issues" +) + +const ( + tolerableMajor = 1 + tolerableMinor = 21 +) + +type ( + // Cluster tracks cluster sanitization. + Cluster struct { + *issues.Collector + ClusterLister + } + + // ClusterLister list available Clusters on a cluster. + ClusterLister interface { + ListVersion() *semver.Version + HasMetrics() bool + } +) + +// NewCluster returns a new instance. +func NewCluster(co *issues.Collector, lister ClusterLister) *Cluster { + return &Cluster{ + Collector: co, + ClusterLister: lister, + } +} + +// Lint cleanse the resource. +func (c *Cluster) Lint(ctx context.Context) error { + return c.checkVersion(ctx) +} + +func (c *Cluster) checkVersion(ctx context.Context) error { + rev := c.ListVersion() + + ctx = internal.WithSpec(ctx, specFor("Version", nil)) + if rev.Major() != tolerableMajor || rev.Minor() < tolerableMinor { + c.AddCode(ctx, 405) + } else { + c.AddCode(ctx, 406) + } + + return nil +} diff --git a/internal/lint/cluster_test.go b/internal/lint/cluster_test.go new file mode 100644 index 00000000..c073388c --- /dev/null +++ b/internal/lint/cluster_test.go @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "testing" + + "github.com/Masterminds/semver" + "github.com/stretchr/testify/assert" + + "github.com/derailed/popeye/internal/issues" + "github.com/derailed/popeye/internal/rules" + "github.com/derailed/popeye/internal/test" +) + +func TestClusterLint(t *testing.T) { + uu := map[string]struct { + major, minor string + metrics bool + e issues.Outcome + }{ + "good": { + major: "1", minor: "29", + metrics: true, + e: map[string]issues.Issues{ + "Version": { + { + GVR: "clusters", + Group: issues.Root, + Message: "[POP-406] K8s version OK", + Level: rules.OkLevel, + }, + }, + }, + }, + "gizzard": { + major: "1", minor: "11", + metrics: false, + e: map[string]issues.Issues{ + "Version": { + { + GVR: "clusters", + Group: issues.Root, + Message: "[POP-405] Is this a jurassic cluster? Might want to upgrade K8s a bit", + Level: rules.WarnLevel, + }, + }, + }, + }, + } + + ctx := test.MakeContext("clusters", "cluster") + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + cl := NewCluster( + test.MakeCollector(t), + newMockCluster(u.major, u.minor, u.metrics), + ) + + assert.Nil(t, cl.Lint(ctx)) + assert.Equal(t, u.e, cl.Outcome()) + }) + } +} + +// Helpers... + +type mockCluster struct { + major, minor string + metrics bool +} + +func newMockCluster(major, minor string, metrics bool) mockCluster { + return mockCluster{major: major, minor: minor, metrics: metrics} +} + +func (c mockCluster) ListVersion() *semver.Version { + v, _ := semver.NewVersion(c.major + "." + c.minor) + return v +} + +func (c mockCluster) HasMetrics() bool { + return c.metrics +} diff --git a/internal/lint/cm.go b/internal/lint/cm.go new file mode 100644 index 00000000..84c5bc94 --- /dev/null +++ b/internal/lint/cm.go @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "context" + "sync" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/cache" + "github.com/derailed/popeye/internal/client" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/issues" + v1 "k8s.io/api/core/v1" +) + +// ConfigMap tracks ConfigMap sanitization. +type ConfigMap struct { + *issues.Collector + db *db.DB + system excludedFQN +} + +// NewConfigMap returns a new instance. +func NewConfigMap(c *issues.Collector, db *db.DB) *ConfigMap { + return &ConfigMap{ + Collector: c, + db: db, + system: excludedFQN{ + "rx:^kube-public": {}, + "rx:kube-root-ca.crt": {}, + }, + } +} + +// Lint lints the resource. +func (s *ConfigMap) Lint(ctx context.Context) error { + var cmRefs sync.Map + if err := cache.NewPod(s.db).PodRefs(&cmRefs); err != nil { + return err + } + + return s.checkStale(ctx, &cmRefs) +} + +func (s *ConfigMap) checkStale(ctx context.Context, refs *sync.Map) error { + txn, it := s.db.MustITFor(internal.Glossary[internal.CM]) + defer txn.Abort() + for o := it.Next(); o != nil; o = it.Next() { + cm := o.(*v1.ConfigMap) + fqn := client.FQN(cm.Namespace, cm.Name) + s.InitOutcome(fqn) + ctx = internal.WithSpec(ctx, specFor(fqn, cm)) + if s.system.skip(fqn) { + continue + } + + keys, ok := refs.Load(cache.ResFqn(cache.ConfigMapKey, fqn)) + if !ok { + s.AddCode(ctx, 400) + continue + } + if keys.(internal.StringSet).Has(internal.All) { + continue + } + kk := make(internal.StringSet, len(cm.Data)) + for k := range cm.Data { + kk.Add(k) + } + deltas := keys.(internal.StringSet).Diff(kk) + for k := range deltas { + s.AddCode(ctx, 401, k) + } + } + + return nil +} diff --git a/internal/lint/cm_test.go b/internal/lint/cm_test.go new file mode 100644 index 00000000..606226af --- /dev/null +++ b/internal/lint/cm_test.go @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "testing" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/rules" + "github.com/derailed/popeye/internal/test" + "github.com/stretchr/testify/assert" + v1 "k8s.io/api/core/v1" +) + +func TestConfigMapLint(t *testing.T) { + dba, err := test.NewTestDB() + assert.NoError(t, err) + l := db.NewLoader(dba) + + ctx := test.MakeCtx(t) + assert.NoError(t, test.LoadDB[*v1.ConfigMap](ctx, l.DB, "core/cm/1.yaml", internal.Glossary[internal.CM])) + assert.NoError(t, test.LoadDB[*v1.Pod](ctx, l.DB, "core/pod/1.yaml", internal.Glossary[internal.PO])) + + cm := NewConfigMap(test.MakeCollector(t), dba) + assert.Nil(t, cm.Lint(test.MakeContext("v1/configmaps", "configmaps"))) + assert.Equal(t, 4, len(cm.Outcome())) + + ii := cm.Outcome()["default/cm1"] + assert.Equal(t, 1, len(ii)) + assert.Equal(t, "[POP-401] Key \"fred.yaml\" used? Unable to locate key reference", ii[0].Message) + assert.Equal(t, rules.InfoLevel, ii[0].Level) + + ii = cm.Outcome()["default/cm2"] + assert.Equal(t, 1, len(ii)) + assert.Equal(t, "[POP-400] Used? Unable to locate resource reference", ii[0].Message) + assert.Equal(t, rules.InfoLevel, ii[0].Level) + + ii = cm.Outcome()["default/cm3"] + assert.Equal(t, 0, len(ii)) + + ii = cm.Outcome()["default/cm4"] + assert.Equal(t, 1, len(ii)) + assert.Equal(t, `[POP-400] Used? Unable to locate resource reference`, ii[0].Message) + assert.Equal(t, rules.InfoLevel, ii[0].Level) +} + +// ---------------------------------------------------------------------------- +// Helpers... + +// type mockConfigMap struct{} + +// func newMockConfigMap() mockConfigMap { +// return mockConfigMap{} +// } + +// func (c mockConfigMap) PodRefs(refs *sync.Map) { +// refs.Store("cm:default/cm1", internal.StringSet{ +// "k1": internal.Blank, +// "k2": internal.Blank, +// }) +// refs.Store("cm:default/cm2", internal.AllKeys) +// refs.Store("cm:default/cm4", internal.StringSet{ +// "k1": internal.Blank, +// }) +// } + +// func (c mockConfigMap) ListConfigMaps() map[string]*v1.ConfigMap { +// return map[string]*v1.ConfigMap{ +// "default/cm1": makeMockConfigMap("cm1"), +// "default/cm2": makeMockConfigMap("cm2"), +// "default/cm3": makeMockConfigMap("cm3"), +// "default/cm4": makeMockConfigMap("cm4"), +// } +// } + +// func makeMockConfigMap(n string) *v1.ConfigMap { +// return &v1.ConfigMap{ +// ObjectMeta: metav1.ObjectMeta{ +// Name: n, +// Namespace: "default", +// }, +// Data: map[string]string{ +// "k1": "", +// "k2": "", +// }, +// } +// } diff --git a/internal/sanitize/container.go b/internal/lint/container.go similarity index 93% rename from internal/sanitize/container.go rename to internal/lint/container.go index 60cdc925..b3be6aa9 100644 --- a/internal/sanitize/container.go +++ b/internal/lint/container.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright Authors of Popeye -package sanitize +package lint import ( "context" @@ -9,6 +9,7 @@ import ( "github.com/derailed/popeye/internal" "github.com/derailed/popeye/internal/client" + "github.com/derailed/popeye/types" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/util/intstr" ) @@ -33,14 +34,13 @@ type ( } ) -// NewContainer returns a new sanitizer. +// NewContainer returns a new instance. func NewContainer(fqn string, c LimitCollector) *Container { return &Container{fqn: fqn, LimitCollector: c} } func (c *Container) sanitize(ctx context.Context, co v1.Container, checkProbes bool) { - ctx = internal.WithFQN(ctx, c.fqn) - ctx = internal.WithGroup(ctx, client.NewGVR("containers"), co.Name) + ctx = internal.WithGroup(ctx, types.NewGVR("containers"), co.Name) c.checkImageTags(ctx, co.Image) if c.allowedRegistryListExists() { c.checkImageRegistry(ctx, co.Image) @@ -86,16 +86,16 @@ func (c *Container) checkProbes(ctx context.Context, co v1.Container) { c.AddSubCode(ctx, 102) return } - if co.LivenessProbe == nil { c.AddSubCode(ctx, 103) + } else { + c.checkNamedProbe(ctx, co.LivenessProbe, true) } - c.checkNamedProbe(ctx, co.LivenessProbe, true) - if co.ReadinessProbe == nil { c.AddSubCode(ctx, 104) + } else { + c.checkNamedProbe(ctx, co.ReadinessProbe, false) } - c.checkNamedProbe(ctx, co.ReadinessProbe, false) } func (c *Container) checkNamedProbe(ctx context.Context, p *v1.Probe, liveness bool) { diff --git a/internal/sanitize/container_bench_test.go b/internal/lint/container_bench_test.go similarity index 95% rename from internal/sanitize/container_bench_test.go rename to internal/lint/container_bench_test.go index c1c41a87..150dcc8d 100644 --- a/internal/sanitize/container_bench_test.go +++ b/internal/lint/container_bench_test.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright Authors of Popeye -package sanitize +package lint import ( "context" diff --git a/internal/sanitize/container_status.go b/internal/lint/container_status.go similarity index 84% rename from internal/sanitize/container_status.go rename to internal/lint/container_status.go index 3e93542e..4fde185a 100644 --- a/internal/sanitize/container_status.go +++ b/internal/lint/container_status.go @@ -1,14 +1,14 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright Authors of Popeye -package sanitize +package lint import ( "context" "github.com/derailed/popeye/internal" - "github.com/derailed/popeye/internal/client" - "github.com/derailed/popeye/pkg/config" + "github.com/derailed/popeye/internal/rules" + "github.com/derailed/popeye/types" v1 "k8s.io/api/core/v1" ) @@ -37,8 +37,8 @@ func newContainerStatus(c Collector, fqn string, count int, isInit bool, restart } func (c *containerStatus) sanitize(ctx context.Context, s v1.ContainerStatus) { - ctx = internal.WithFQN(ctx, c.fqn) - ctx = internal.WithGroup(ctx, client.NewGVR("containers"), s.Name) + ctx = internal.WithGroup(ctx, types.NewGVR("containers"), s.Name) + c.rollup(s) if c.terminated > 0 && c.ready == 0 { return @@ -75,10 +75,10 @@ func (c *containerStatus) rollup(s v1.ContainerStatus) { c.restarts += int(s.RestartCount) } -func (c *containerStatus) checkReason(ctx context.Context, code config.ID, reason string) { +func (c *containerStatus) checkReason(ctx context.Context, code rules.ID, reason string) { if reason == "" { c.collector.AddSubCode(ctx, code, c.ready, c.count) return } - c.collector.AddSubCode(ctx, config.ID(code+1), c.ready, c.count, c.reason) + c.collector.AddSubCode(ctx, rules.ID(code+1), c.ready, c.count, c.reason) } diff --git a/internal/sanitize/container_status_test.go b/internal/lint/container_status_test.go similarity index 67% rename from internal/sanitize/container_status_test.go rename to internal/lint/container_status_test.go index 66a9aeca..c928a467 100644 --- a/internal/sanitize/container_status_test.go +++ b/internal/lint/container_status_test.go @@ -1,19 +1,20 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright Authors of Popeye -package sanitize +package lint import ( "testing" - "github.com/derailed/popeye/internal/client" "github.com/derailed/popeye/internal/issues" - "github.com/derailed/popeye/pkg/config" + "github.com/derailed/popeye/internal/rules" + "github.com/derailed/popeye/internal/test" + "github.com/derailed/popeye/types" "github.com/stretchr/testify/assert" v1 "k8s.io/api/core/v1" ) -func TestContainerStatusSanitize(t *testing.T) { +func TestContainerStatusLint(t *testing.T) { uu := map[string]struct { cs v1.ContainerStatus issues int @@ -37,7 +38,7 @@ func TestContainerStatusSanitize(t *testing.T) { State: v1.ContainerState{}, }, 1, - issues.New(client.NewGVR("containers"), "c1", config.ErrorLevel, "[POP-204] Pod is not ready [0/1]"), + issues.New(types.NewGVR("containers"), "c1", rules.ErrorLevel, "[POP-204] Pod is not ready [0/1]"), }, "waitingNoReason": { v1.ContainerStatus{ @@ -49,7 +50,7 @@ func TestContainerStatusSanitize(t *testing.T) { }, }, 1, - issues.New(client.NewGVR("containers"), "c1", config.ErrorLevel, "[POP-203] Pod is waiting [0/1] blah"), + issues.New(types.NewGVR("containers"), "c1", rules.ErrorLevel, "[POP-203] Pod is waiting [0/1] blah"), }, "waiting": { v1.ContainerStatus{ @@ -61,7 +62,7 @@ func TestContainerStatusSanitize(t *testing.T) { }, }, 1, - issues.New(client.NewGVR("containers"), "c1", config.ErrorLevel, "[POP-202] Pod is waiting [0/1]"), + issues.New(types.NewGVR("containers"), "c1", rules.ErrorLevel, "[POP-202] Pod is waiting [0/1]"), }, "terminatedReason": { v1.ContainerStatus{ @@ -73,7 +74,7 @@ func TestContainerStatusSanitize(t *testing.T) { }, }, 1, - issues.New(client.NewGVR("containers"), "c1", config.WarnLevel, "[POP-201] Pod is terminating [1/1] blah"), + issues.New(types.NewGVR("containers"), "c1", rules.WarnLevel, "[POP-201] Pod is terminating [1/1] blah"), }, "terminated": { v1.ContainerStatus{ @@ -85,7 +86,7 @@ func TestContainerStatusSanitize(t *testing.T) { }, }, 1, - issues.New(client.NewGVR("containers"), "c1", config.WarnLevel, "[POP-200] Pod is terminating [1/1]"), + issues.New(types.NewGVR("containers"), "c1", rules.WarnLevel, "[POP-200] Pod is terminating [1/1]"), }, "terminatedNotReady": { v1.ContainerStatus{ @@ -106,21 +107,21 @@ func TestContainerStatusSanitize(t *testing.T) { RestartCount: 11, }, 1, - issues.New(client.NewGVR("containers"), "c1", config.WarnLevel, "[POP-205] Pod was restarted (11) times"), + issues.New(types.NewGVR("containers"), "c1", rules.WarnLevel, "[POP-205] Pod was restarted (11) times"), }, } - ctx := makeContext("containers", "containers") + ctx := test.MakeContext("containers", "containers") for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { - c := issues.NewCollector(loadCodes(t), makeConfig(t)) + c := test.MakeCollector(t) cs := newContainerStatus(c, "default/p1", 1, false, 10) cs.sanitize(ctx, u.cs) - assert.Equal(t, u.issues, len(c.Outcome()["default/p1"])) + assert.Equal(t, u.issues, len(c.Outcome()[""])) if u.issues != 0 { - assert.Equal(t, u.issue, c.Outcome()["default/p1"][0]) + assert.Equal(t, u.issue, c.Outcome()[""][0]) } }) } diff --git a/internal/sanitize/container_test.go b/internal/lint/container_test.go similarity index 69% rename from internal/sanitize/container_test.go rename to internal/lint/container_test.go index d8122b77..6e78fff5 100644 --- a/internal/sanitize/container_test.go +++ b/internal/lint/container_test.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright Authors of Popeye -package sanitize +package lint import ( "testing" @@ -9,10 +9,11 @@ import ( "github.com/derailed/popeye/internal" "github.com/derailed/popeye/internal/client" "github.com/derailed/popeye/internal/issues" - "github.com/derailed/popeye/pkg/config" + "github.com/derailed/popeye/internal/rules" + "github.com/derailed/popeye/internal/test" + "github.com/derailed/popeye/types" "github.com/stretchr/testify/assert" v1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/resource" "k8s.io/apimachinery/pkg/util/intstr" ) @@ -29,7 +30,7 @@ func TestContainerCheckUtilization(t *testing.T) { lcpu: "10m", lmem: "10Mi", }), - mx: client.Metrics{CurrentCPU: toQty("1m"), CurrentMEM: toQty("1Mi")}, + mx: client.Metrics{CurrentCPU: test.ToQty("1m"), CurrentMEM: test.ToQty("1Mi")}, }, "cpuOver": { co: makeContainer("c1", coOpts{ @@ -38,7 +39,7 @@ func TestContainerCheckUtilization(t *testing.T) { lcpu: "100m", lmem: "10Mi", }), - mx: client.Metrics{CurrentCPU: toQty("200m"), CurrentMEM: toQty("2Mi")}, + mx: client.Metrics{CurrentCPU: test.ToQty("200m"), CurrentMEM: test.ToQty("2Mi")}, issues: 1, }, "memOver": { @@ -48,7 +49,7 @@ func TestContainerCheckUtilization(t *testing.T) { lcpu: "100m", lmem: "10Mi", }), - mx: client.Metrics{CurrentCPU: toQty("10m"), CurrentMEM: toQty("20Mi")}, + mx: client.Metrics{CurrentCPU: test.ToQty("10m"), CurrentMEM: test.ToQty("20Mi")}, issues: 1, }, "bothOver": { @@ -58,7 +59,7 @@ func TestContainerCheckUtilization(t *testing.T) { lcpu: "100m", lmem: "10Mi", }), - mx: client.Metrics{CurrentCPU: toQty("5"), CurrentMEM: toQty("20Mi")}, + mx: client.Metrics{CurrentCPU: test.ToQty("5"), CurrentMEM: test.ToQty("20Mi")}, issues: 2, }, "LimOver": { @@ -68,18 +69,18 @@ func TestContainerCheckUtilization(t *testing.T) { lcpu: "100m", lmem: "10Mi", }), - mx: client.Metrics{CurrentCPU: toQty("5"), CurrentMEM: toQty("20Mi")}, + mx: client.Metrics{CurrentCPU: test.ToQty("5"), CurrentMEM: test.ToQty("20Mi")}, issues: 2, }, } - ctx := makeContext("containers", "container") - ctx = internal.WithFQN(ctx, "default/p1") + ctx := test.MakeContext("containers", "container") + ctx = internal.WithSpec(ctx, specFor("default/p1", nil)) for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { c := NewContainer("default/p1", newRangeCollector(t)) - ctx = internal.WithGroup(ctx, client.NewGVR("containers"), u.co.Name) + ctx = internal.WithGroup(ctx, types.NewGVR("containers"), u.co.Name) c.checkUtilization(ctx, u.co, u.mx) assert.Equal(t, u.issues, len(c.Outcome().For("default/p1", "c1"))) @@ -92,15 +93,15 @@ func TestContainerCheckResources(t *testing.T) { request bool limit bool issues int - severity config.Level + severity rules.Level }{ "cool": {request: true, limit: true, issues: 0}, - "noLim": {request: true, issues: 1, severity: config.WarnLevel}, + "noLim": {request: true, issues: 1, severity: rules.WarnLevel}, "noReq": {limit: true, issues: 0}, - "none": {issues: 1, severity: config.WarnLevel}, + "none": {issues: 1, severity: rules.WarnLevel}, } - ctx := makeContext("containers", "container") + ctx := test.MakeContext("containers", "container") for k := range uu { u := uu[k] opts := coOpts{} @@ -116,8 +117,8 @@ func TestContainerCheckResources(t *testing.T) { l := NewContainer("default/p1", newRangeCollector(t)) t.Run(k, func(t *testing.T) { - ctx = internal.WithFQN(ctx, "default/p1") - ctx = internal.WithGroup(ctx, client.NewGVR("containers"), co.Name) + ctx = internal.WithSpec(ctx, specFor("default/p1", nil)) + ctx = internal.WithGroup(ctx, types.NewGVR("containers"), co.Name) l.checkResources(ctx, co) assert.Equal(t, u.issues, len(l.Outcome()["default/p1"])) @@ -134,16 +135,16 @@ func TestContainerCheckProbes(t *testing.T) { readiness bool namedPort bool issues int - severity config.Level + severity rules.Level }{ "cool": {liveness: true, readiness: true}, - "noReady": {liveness: true, issues: 1, severity: config.WarnLevel}, - "noLive": {readiness: true, issues: 1, severity: config.WarnLevel}, - "noneProbes": {issues: 1, severity: config.WarnLevel}, - "Unnamed": {liveness: true, readiness: true, namedPort: true, issues: 2, severity: config.InfoLevel}, + "noReady": {liveness: true, issues: 1, severity: rules.WarnLevel}, + "noLive": {readiness: true, issues: 1, severity: rules.WarnLevel}, + "noneProbes": {issues: 1, severity: rules.WarnLevel}, + "Unnamed": {liveness: true, readiness: true, namedPort: true, issues: 2, severity: rules.InfoLevel}, } - ctx := makeContext("containers", "container") + ctx := test.MakeContext("containers", "container") for k := range uu { u := uu[k] co := makeContainer("c1", coOpts{}) @@ -175,16 +176,16 @@ func TestContainerCheckImageTags(t *testing.T) { image string pissues int issues int - severity config.Level + severity rules.Level }{ "cool": {image: "cool:1.2.3", issues: 0}, - "noRev": {pissues: 1, image: "fred", issues: 1, severity: config.ErrorLevel}, - "latest": {pissues: 1, image: "fred:latest", issues: 1, severity: config.WarnLevel}, + "noRev": {pissues: 1, image: "fred", issues: 1, severity: rules.ErrorLevel}, + "latest": {pissues: 1, image: "fred:latest", issues: 1, severity: rules.WarnLevel}, } - ctx := makeContext("containers", "container") - ctx = internal.WithFQN(ctx, "default/p1") - ctx = internal.WithGroup(ctx, client.NewGVR("containers"), "c1") + ctx := test.MakeContext("containers", "container") + ctx = internal.WithSpec(ctx, specFor("default/p1", nil)) + ctx = internal.WithGroup(ctx, types.NewGVR("containers"), "c1") for k := range uu { u := uu[k] co := makeContainer("c1", coOpts{}) @@ -208,16 +209,16 @@ func TestContainerCheckImageRegistry(t *testing.T) { image string pissues int issues int - severity config.Level + severity rules.Level }{ "dockerDefault": {image: "dockerhub:1.2.3", issues: 0}, "cool": {image: "docker.io/cool:1.2.3", issues: 0}, - "wrongRegistry": {pissues: 1, image: "wrong-registry.io/fred", issues: 1, severity: config.ErrorLevel}, + "wrongRegistry": {pissues: 1, image: "wrong-registry.io/fred", issues: 1, severity: rules.ErrorLevel}, } - ctx := makeContext("containers", "container") - ctx = internal.WithFQN(ctx, "default/p1") - ctx = internal.WithGroup(ctx, client.NewGVR("containers"), "c1") + ctx := test.MakeContext("containers", "container") + ctx = internal.WithSpec(ctx, specFor("default/p1", nil)) + ctx = internal.WithGroup(ctx, types.NewGVR("containers"), "c1") for k := range uu { u := uu[k] co := makeContainer("c1", coOpts{}) @@ -240,15 +241,15 @@ func TestContainerCheckNamedPorts(t *testing.T) { uu := map[string]struct { port string issues int - severity config.Level + severity rules.Level }{ "named": {port: "cool", issues: 0}, - "unamed": {port: "", issues: 1, severity: config.WarnLevel}, + "unamed": {port: "", issues: 1, severity: rules.WarnLevel}, } - ctx := makeContext("containers", "container") - ctx = internal.WithFQN(ctx, "p1") - ctx = internal.WithGroup(ctx, client.NewGVR("v1/pods"), "p1") + ctx := test.MakeContext("containers", "container") + ctx = internal.WithSpec(ctx, specFor("p1", nil)) + ctx = internal.WithGroup(ctx, types.NewGVR("v1/pods"), "p1") for k := range uu { u := uu[k] co := makeContainer("c1", coOpts{}) @@ -266,7 +267,7 @@ func TestContainerCheckNamedPorts(t *testing.T) { } } -func TestContainerSanitize(t *testing.T) { +func TestContainerLint(t *testing.T) { uu := map[string]struct { co v1.Container issues int @@ -274,15 +275,15 @@ func TestContainerSanitize(t *testing.T) { "NoImgNoProbs": {makeContainer("c1", coOpts{}), 3}, } - ctx := makeContext("containers", "container") + ctx := test.MakeContext("containers", "container") for k := range uu { u := uu[k] c := NewContainer("default/p1", newRangeCollector(t)) t.Run(k, func(t *testing.T) { c.sanitize(ctx, u.co, true) - assert.Equal(t, 3, len(c.Outcome()["default/p1"])) - assert.Equal(t, u.issues, len(c.Outcome().For("default/p1", "c1"))) + assert.Equal(t, 3, len(c.Outcome()[""])) + assert.Equal(t, u.issues, len(c.Outcome().For("", "c1"))) }) } } @@ -295,13 +296,14 @@ type rangeCollector struct { } func newRangeCollector(t *testing.T) *rangeCollector { - return &rangeCollector{issues.NewCollector(loadCodes(t), makeConfig(t))} + return &rangeCollector{test.MakeCollector(t)} } func newRangeCollectorWithRegistry(t *testing.T) *rangeCollector { - cfg := makeConfig(t) - cfg.Registries = append(cfg.Registries, "docker.io") - return &rangeCollector{issues.NewCollector(loadCodes(t), cfg)} + c := rangeCollector{test.MakeCollector(t)} + c.Config.Registries = []string{"docker.io"} + + return &c } func (*rangeCollector) RestartsLimit() int { @@ -331,10 +333,10 @@ func makeContainer(n string, opts coOpts) v1.Container { } if opts.rcpu != "" { - co.Resources.Requests = makeRes(opts.rcpu, opts.rmem) + co.Resources.Requests = test.MakeRes(opts.rcpu, opts.rmem) } if opts.lcpu != "" { - co.Resources.Limits = makeRes(opts.lcpu, opts.lmem) + co.Resources.Limits = test.MakeRes(opts.lcpu, opts.lmem) } if opts.lprob { co.LivenessProbe = &v1.Probe{} @@ -345,19 +347,3 @@ func makeContainer(n string, opts coOpts) v1.Container { return co } - -func makeRes(c, m string) v1.ResourceList { - return v1.ResourceList{ - v1.ResourceCPU: *makeQty(c), - v1.ResourceMemory: *makeQty(m), - } -} - -func makeQty(s string) *resource.Quantity { - if s == "" { - return nil - } - - qty, _ := resource.ParseQuantity(s) - return &qty -} diff --git a/internal/lint/cr.go b/internal/lint/cr.go new file mode 100644 index 00000000..f801892e --- /dev/null +++ b/internal/lint/cr.go @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "context" + "sync" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/cache" + "github.com/derailed/popeye/internal/client" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/issues" + "github.com/derailed/popeye/internal/rules" + rbacv1 "k8s.io/api/rbac/v1" +) + +type excludedFQN map[rules.Expression]struct{} + +func (e excludedFQN) skip(fqn string) bool { + if _, ok := e[rules.Expression(fqn)]; ok { + return true + } + for k := range e { + if k.IsRX() && k.MatchRX(fqn) { + return true + } + } + + return false +} + +// ClusterRole tracks ClusterRole sanitization. +type ClusterRole struct { + *issues.Collector + + db *db.DB + system excludedFQN +} + +// NewClusterRole returns a new instance. +func NewClusterRole(c *issues.Collector, db *db.DB) *ClusterRole { + return &ClusterRole{ + Collector: c, + db: db, + system: excludedFQN{ + "admin": {}, + "edit": {}, + "view": {}, + "rx:^system:": {}, + }, + } +} + +// Lint sanitizes the resource. +func (s *ClusterRole) Lint(ctx context.Context) error { + var crRefs sync.Map + crb := cache.NewClusterRoleBinding(s.db) + crb.ClusterRoleRefs(&crRefs) + rb := cache.NewRoleBinding(s.db) + rb.RoleRefs(&crRefs) + s.checkStale(ctx, &crRefs) + + return nil +} + +func (s *ClusterRole) checkStale(ctx context.Context, refs *sync.Map) { + txn, it := s.db.MustITFor(internal.Glossary[internal.CR]) + defer txn.Abort() + for o := it.Next(); o != nil; o = it.Next() { + cr := o.(*rbacv1.ClusterRole) + fqn := client.FQN(cr.Namespace, cr.Name) + s.InitOutcome(fqn) + ctx = internal.WithSpec(ctx, specFor(fqn, cr)) + if s.system.skip(fqn) { + continue + } + if _, ok := refs.Load(cache.ResFqn(cache.ClusterRoleKey, fqn)); !ok { + s.AddCode(ctx, 400) + } + } +} diff --git a/internal/lint/cr_test.go b/internal/lint/cr_test.go new file mode 100644 index 00000000..96a0782c --- /dev/null +++ b/internal/lint/cr_test.go @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "testing" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/rules" + "github.com/derailed/popeye/internal/test" + "github.com/stretchr/testify/assert" + v1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" +) + +func TestCRLint(t *testing.T) { + dba, err := test.NewTestDB() + assert.NoError(t, err) + l := db.NewLoader(dba) + + ctx := test.MakeCtx(t) + assert.NoError(t, test.LoadDB[*rbacv1.ClusterRole](ctx, l.DB, "auth/cr/1.yaml", internal.Glossary[internal.CR])) + assert.NoError(t, test.LoadDB[*rbacv1.ClusterRoleBinding](ctx, l.DB, "auth/crb/1.yaml", internal.Glossary[internal.CRB])) + assert.NoError(t, test.LoadDB[*rbacv1.RoleBinding](ctx, l.DB, "auth/rob/1.yaml", internal.Glossary[internal.ROB])) + assert.NoError(t, test.LoadDB[*v1.ServiceAccount](ctx, l.DB, "core/sa/1.yaml", internal.Glossary[internal.SA])) + + cr := NewClusterRole(test.MakeCollector(t), dba) + assert.Nil(t, cr.Lint(test.MakeContext("rbac.authorization.k8s.io/v1/clusterroles", "clusterroles"))) + assert.Equal(t, 3, len(cr.Outcome())) + + ii := cr.Outcome()["cr1"] + assert.Equal(t, 0, len(ii)) + + ii = cr.Outcome()["cr2"] + assert.Equal(t, 1, len(ii)) + assert.Equal(t, `[POP-400] Used? Unable to locate resource reference`, ii[0].Message) + assert.Equal(t, rules.InfoLevel, ii[0].Level) + + ii = cr.Outcome()["cr3"] + assert.Equal(t, 1, len(ii)) + assert.Equal(t, `[POP-400] Used? Unable to locate resource reference`, ii[0].Message) + assert.Equal(t, rules.InfoLevel, ii[0].Level) +} diff --git a/internal/lint/crb.go b/internal/lint/crb.go new file mode 100644 index 00000000..6b4e837d --- /dev/null +++ b/internal/lint/crb.go @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "context" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/cache" + "github.com/derailed/popeye/internal/client" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/issues" + rbacv1 "k8s.io/api/rbac/v1" +) + +type ( + // ClusterRoleBinding tracks ClusterRoleBinding sanitization. + ClusterRoleBinding struct { + *issues.Collector + + db *db.DB + } +) + +// NewClusterRoleBinding returns a new instance. +func NewClusterRoleBinding(c *issues.Collector, db *db.DB) *ClusterRoleBinding { + return &ClusterRoleBinding{ + Collector: c, + db: db, + } +} + +// Lint sanitizes the resource. +func (c *ClusterRoleBinding) Lint(ctx context.Context) error { + c.checkInUse(ctx) + + return nil +} + +func (c *ClusterRoleBinding) checkInUse(ctx context.Context) { + txn, it := c.db.MustITFor(internal.Glossary[internal.CRB]) + defer txn.Abort() + for o := it.Next(); o != nil; o = it.Next() { + crb := o.(*rbacv1.ClusterRoleBinding) + fqn := client.FQN(crb.Namespace, crb.Name) + + c.InitOutcome(fqn) + ctx = internal.WithSpec(ctx, specFor(fqn, crb)) + + switch crb.RoleRef.Kind { + case "ClusterRole": + if !c.db.Exists(internal.Glossary[internal.CR], crb.RoleRef.Name) { + c.AddCode(ctx, 1300, crb.RoleRef.Kind, crb.RoleRef.Name) + } + case "Role": + rFQN := cache.FQN(crb.Namespace, crb.RoleRef.Name) + if !c.db.Exists(internal.Glossary[internal.RO], rFQN) { + c.AddCode(ctx, 1300, crb.RoleRef.Kind, rFQN) + } + } + for _, s := range crb.Subjects { + if s.Kind == "ServiceAccount" { + safqn := cache.FQN(s.Namespace, s.Name) + if !c.db.Exists(internal.Glossary[internal.SA], safqn) { + c.AddCode(ctx, 1300, s.Kind, safqn) + } + } + } + } +} diff --git a/internal/lint/crb_test.go b/internal/lint/crb_test.go new file mode 100644 index 00000000..a3235914 --- /dev/null +++ b/internal/lint/crb_test.go @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "testing" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/rules" + "github.com/derailed/popeye/internal/test" + "github.com/stretchr/testify/assert" + v1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" +) + +func TestCRBLint(t *testing.T) { + dba, err := test.NewTestDB() + assert.NoError(t, err) + l := db.NewLoader(dba) + + ctx := test.MakeCtx(t) + assert.NoError(t, test.LoadDB[*rbacv1.ClusterRoleBinding](ctx, l.DB, "auth/crb/1.yaml", internal.Glossary[internal.CRB])) + assert.NoError(t, test.LoadDB[*rbacv1.ClusterRole](ctx, l.DB, "auth/cr/1.yaml", internal.Glossary[internal.CR])) + assert.NoError(t, test.LoadDB[*rbacv1.Role](ctx, l.DB, "auth/ro/1.yaml", internal.Glossary[internal.RO])) + assert.NoError(t, test.LoadDB[*v1.ServiceAccount](ctx, l.DB, "core/sa/1.yaml", internal.Glossary[internal.SA])) + + crb := NewClusterRoleBinding(test.MakeCollector(t), dba) + assert.Nil(t, crb.Lint(test.MakeContext("rbac.authorization.k8s.io/v1/clusterrolebindings", "clusterrolebindings"))) + assert.Equal(t, 3, len(crb.Outcome())) + + ii := crb.Outcome()["crb1"] + assert.Equal(t, 0, len(ii)) + + ii = crb.Outcome()["crb2"] + assert.Equal(t, 1, len(ii)) + assert.Equal(t, `[POP-1300] References a ClusterRole (cr-bozo) which does not exist`, ii[0].Message) + assert.Equal(t, rules.WarnLevel, ii[0].Level) + + ii = crb.Outcome()["crb3"] + assert.Equal(t, 2, len(ii)) + assert.Equal(t, `[POP-1300] References a Role (r-bozo) which does not exist`, ii[0].Message) + assert.Equal(t, rules.WarnLevel, ii[0].Level) + assert.Equal(t, `[POP-1300] References a ServiceAccount (default/sa-bozo) which does not exist`, ii[1].Message) + assert.Equal(t, rules.WarnLevel, ii[1].Level) +} diff --git a/internal/lint/cronjob.go b/internal/lint/cronjob.go new file mode 100644 index 00000000..c893407f --- /dev/null +++ b/internal/lint/cronjob.go @@ -0,0 +1,138 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "context" + "errors" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/cache" + "github.com/derailed/popeye/internal/client" + "github.com/derailed/popeye/internal/dao" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/issues" + batchv1 "k8s.io/api/batch/v1" + v1 "k8s.io/api/core/v1" +) + +// CronJob tracks CronJob linting. +type CronJob struct { + *issues.Collector + + db *db.DB +} + +// NewCronJob returns a new instance. +func NewCronJob(co *issues.Collector, db *db.DB) *CronJob { + return &CronJob{ + Collector: co, + db: db, + } +} + +// Lint cleanse the resource. +func (s *CronJob) Lint(ctx context.Context) error { + over := pullOverAllocs(ctx) + txn, it := s.db.MustITFor(internal.Glossary[internal.CJOB]) + defer txn.Abort() + for o := it.Next(); o != nil; o = it.Next() { + cj := o.(*batchv1.CronJob) + fqn := client.FQN(cj.Namespace, cj.Name) + s.InitOutcome(fqn) + ctx = internal.WithSpec(ctx, specFor(fqn, cj)) + s.checkCronJob(ctx, fqn, cj) + s.checkContainers(ctx, fqn, cj.Spec.JobTemplate.Spec.Template.Spec) + s.checkUtilization(ctx, over, fqn) + } + + return nil +} + +// CheckCronJob checks if CronJob contract is currently happy or not. +func (s *CronJob) checkCronJob(ctx context.Context, fqn string, cj *batchv1.CronJob) { + checkEvents(ctx, s.Collector, internal.CJOB, "", "CronJob", fqn) + + if cj.Spec.Suspend != nil && *cj.Spec.Suspend { + s.AddCode(ctx, 1500, cj.Kind) + } + + if len(cj.Status.Active) == 0 { + s.AddCode(ctx, 1501) + } + if cj.Status.LastSuccessfulTime == nil { + s.AddCode(ctx, 1502) + } + + if sa := cj.Spec.JobTemplate.Spec.Template.Spec.ServiceAccountName; sa != "" { + saFQN := client.FQN(cj.Namespace, sa) + if !s.db.Exists(internal.Glossary[internal.SA], saFQN) { + s.AddCode(ctx, 307, cj.Kind, sa) + } + } +} + +// CheckContainers runs thru CronJob template and checks pod configuration. +func (s *CronJob) checkContainers(ctx context.Context, fqn string, spec v1.PodSpec) { + c := NewContainer(fqn, s) + for _, co := range spec.InitContainers { + c.sanitize(ctx, co, false) + } + for _, co := range spec.Containers { + c.sanitize(ctx, co, false) + } +} + +// CheckUtilization checks CronJobs requested resources vs current utilization. +func (s *CronJob) checkUtilization(ctx context.Context, over bool, fqn string) { + jj, err := s.db.FindJobs(fqn) + if err != nil { + s.AddErr(ctx, err) + return + } + mx := jobResourceUsage(ctx, s.db, s, jj) + if mx.RequestCPU.IsZero() && mx.RequestMEM.IsZero() { + return + } + checkCPU(ctx, s, over, mx) + checkMEM(ctx, s, over, mx) +} + +// Helpers... + +func checkEvents(ctx context.Context, ii *issues.Collector, r internal.R, kind, object, fqn string) { + ee, err := dao.EventsFor(ctx, internal.Glossary[r], kind, object, fqn) + if err != nil { + ii.AddErr(ctx, err) + } + for _, e := range ee.Issues() { + ii.AddErr(ctx, errors.New(e)) + } +} + +func jobResourceUsage(ctx context.Context, dba *db.DB, c Collector, jobs []*batchv1.Job) ConsumptionMetrics { + var mx ConsumptionMetrics + + if len(jobs) == 0 { + return mx + } + + for _, job := range jobs { + fqn := cache.FQN(job.Namespace, job.Name) + cpu, mem := computePodResources(job.Spec.Template.Spec) + mx.RequestCPU.Add(cpu) + mx.RequestMEM.Add(mem) + + pmx, err := dba.FindPMX(fqn) + if err != nil { + continue + } + for _, cx := range pmx.Containers { + mx.CurrentCPU.Add(*cx.Usage.Cpu()) + mx.CurrentMEM.Add(*cx.Usage.Memory()) + } + } + + return mx +} diff --git a/internal/lint/cronjob_test.go b/internal/lint/cronjob_test.go new file mode 100644 index 00000000..91ab0c23 --- /dev/null +++ b/internal/lint/cronjob_test.go @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "testing" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/rules" + "github.com/derailed/popeye/internal/test" + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" + batchv1 "k8s.io/api/batch/v1" + v1 "k8s.io/api/core/v1" + mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" +) + +func init() { + zerolog.SetGlobalLevel(zerolog.FatalLevel) +} + +func TestCronJobLint(t *testing.T) { + dba, err := test.NewTestDB() + assert.NoError(t, err) + l := db.NewLoader(dba) + + ctx := test.MakeCtx(t) + assert.NoError(t, test.LoadDB[*batchv1.CronJob](ctx, l.DB, "batch/cjob/1.yaml", internal.Glossary[internal.CJOB])) + assert.NoError(t, test.LoadDB[*batchv1.Job](ctx, l.DB, "batch/job/1.yaml", internal.Glossary[internal.JOB])) + assert.NoError(t, test.LoadDB[*v1.ServiceAccount](ctx, l.DB, "core/sa/1.yaml", internal.Glossary[internal.SA])) + assert.NoError(t, test.LoadDB[*v1.Pod](ctx, l.DB, "core/pod/1.yaml", internal.Glossary[internal.PO])) + assert.NoError(t, test.LoadDB[*mv1beta1.PodMetrics](ctx, l.DB, "mx/pod/1.yaml", internal.Glossary[internal.PMX])) + + cj := NewCronJob(test.MakeCollector(t), dba) + assert.Nil(t, cj.Lint(test.MakeContext("batch/v1/cronjobs", "cronjobs"))) + assert.Equal(t, 2, len(cj.Outcome())) + + ii := cj.Outcome()["default/cj1"] + assert.Equal(t, 2, len(ii)) + assert.Equal(t, `[POP-503] At current load, CPU under allocated. Current:2000m vs Requested:1m (200000.00%)`, ii[0].Message) + assert.Equal(t, rules.WarnLevel, ii[0].Level) + assert.Equal(t, `[POP-505] At current load, Memory under allocated. Current:20Mi vs Requested:1Mi (2000.00%)`, ii[1].Message) + assert.Equal(t, rules.WarnLevel, ii[1].Level) + + ii = cj.Outcome()["default/cj2"] + assert.Equal(t, 6, len(ii)) + assert.Equal(t, `[POP-1500] CronJob is suspended`, ii[0].Message) + assert.Equal(t, rules.WarnLevel, ii[0].Level) + assert.Equal(t, `[POP-1501] No active jobs detected`, ii[1].Message) + assert.Equal(t, rules.InfoLevel, ii[1].Level) + assert.Equal(t, `[POP-1502] CronJob has not been ran yet or is failing`, ii[2].Message) + assert.Equal(t, rules.WarnLevel, ii[2].Level) + assert.Equal(t, `[POP-307] CronJob references a non existing ServiceAccount: "sa-bozo"`, ii[3].Message) + assert.Equal(t, rules.WarnLevel, ii[3].Level) + assert.Equal(t, `[POP-100] Untagged docker image in use`, ii[4].Message) + assert.Equal(t, rules.ErrorLevel, ii[4].Level) + assert.Equal(t, `[POP-106] No resources requests/limits defined`, ii[5].Message) + assert.Equal(t, rules.WarnLevel, ii[5].Level) +} diff --git a/internal/lint/dp.go b/internal/lint/dp.go new file mode 100644 index 00000000..7c021496 --- /dev/null +++ b/internal/lint/dp.go @@ -0,0 +1,126 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "context" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/client" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/issues" + appsv1 "k8s.io/api/apps/v1" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" +) + +// Deployment tracks Deployment sanitization. +type Deployment struct { + *issues.Collector + + db *db.DB +} + +// NewDeployment returns a new instance. +func NewDeployment(co *issues.Collector, db *db.DB) *Deployment { + return &Deployment{ + Collector: co, + db: db, + } +} + +// Lint cleanse the resource. +func (s *Deployment) Lint(ctx context.Context) error { + over := pullOverAllocs(ctx) + txn, it := s.db.MustITFor(internal.Glossary[internal.DP]) + defer txn.Abort() + for o := it.Next(); o != nil; o = it.Next() { + dp := o.(*appsv1.Deployment) + fqn := client.FQN(dp.Namespace, dp.Name) + s.InitOutcome(fqn) + ctx = internal.WithSpec(ctx, specFor(fqn, dp)) + s.checkDeployment(ctx, dp) + s.checkContainers(ctx, fqn, dp.Spec.Template.Spec) + s.checkUtilization(ctx, over, dp) + } + + return nil +} + +// CheckDeployment checks if deployment contract is currently happy or not. +func (s *Deployment) checkDeployment(ctx context.Context, dp *appsv1.Deployment) { + if dp.Spec.Replicas == nil || (dp.Spec.Replicas != nil && *dp.Spec.Replicas == 0) { + s.AddCode(ctx, 500) + return + } + + if dp.Spec.Replicas != nil && *dp.Spec.Replicas != dp.Status.AvailableReplicas { + s.AddCode(ctx, 501, *dp.Spec.Replicas, dp.Status.AvailableReplicas) + } + + if dp.Spec.Template.Spec.ServiceAccountName == "" { + return + } + + saFQN := client.FQN(dp.Namespace, dp.Spec.Template.Spec.ServiceAccountName) + if !s.db.Exists(internal.Glossary[internal.SA], saFQN) { + s.AddCode(ctx, 507, dp.Spec.Template.Spec.ServiceAccountName) + } +} + +// CheckContainers runs thru deployment template and checks pod configuration. +func (s *Deployment) checkContainers(ctx context.Context, fqn string, spec v1.PodSpec) { + c := NewContainer(fqn, s) + for _, co := range spec.InitContainers { + c.sanitize(ctx, co, false) + } + for _, co := range spec.Containers { + c.sanitize(ctx, co, false) + } +} + +// CheckUtilization checks deployments requested resources vs current utilization. +func (s *Deployment) checkUtilization(ctx context.Context, over bool, dp *appsv1.Deployment) { + mx := resourceUsage(ctx, s.db, s, dp.Namespace, dp.Spec.Selector) + if mx.RequestCPU.IsZero() && mx.RequestMEM.IsZero() { + return + } + checkCPU(ctx, s, over, mx) + checkMEM(ctx, s, over, mx) +} + +// Helpers... + +// PullOverAllocs check for over allocation setting in context. +func pullOverAllocs(ctx context.Context) bool { + over := ctx.Value(internal.KeyOverAllocs) + if over == nil { + return false + } + return over.(bool) +} + +func computePodResources(spec v1.PodSpec) (cpu, mem resource.Quantity) { + for _, co := range spec.InitContainers { + c, m, _ := containerResources(co) + if c != nil { + cpu.Add(*c) + } + if m != nil { + mem.Add(*m) + } + } + + for _, co := range spec.Containers { + c, m, _ := containerResources(co) + if c != nil { + cpu.Add(*c) + } + if m != nil { + mem.Add(*m) + } + } + + return +} diff --git a/internal/lint/dp_test.go b/internal/lint/dp_test.go new file mode 100644 index 00000000..2f575db5 --- /dev/null +++ b/internal/lint/dp_test.go @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "testing" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/rules" + "github.com/derailed/popeye/internal/test" + "github.com/stretchr/testify/assert" + appsv1 "k8s.io/api/apps/v1" + v1 "k8s.io/api/core/v1" + mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" +) + +func TestDPLint(t *testing.T) { + dba, err := test.NewTestDB() + assert.NoError(t, err) + l := db.NewLoader(dba) + + ctx := test.MakeCtx(t) + assert.NoError(t, test.LoadDB[*appsv1.Deployment](ctx, l.DB, "apps/dp/1.yaml", internal.Glossary[internal.DP])) + assert.NoError(t, test.LoadDB[*v1.ServiceAccount](ctx, l.DB, "core/sa/1.yaml", internal.Glossary[internal.SA])) + assert.NoError(t, test.LoadDB[*v1.Pod](ctx, l.DB, "core/pod/1.yaml", internal.Glossary[internal.PO])) + assert.NoError(t, test.LoadDB[*mv1beta1.PodMetrics](ctx, l.DB, "mx/pod/1.yaml", internal.Glossary[internal.PMX])) + + dp := NewDeployment(test.MakeCollector(t), dba) + assert.Nil(t, dp.Lint(test.MakeContext("apps/v1/deployments", "deployments"))) + assert.Equal(t, 3, len(dp.Outcome())) + + ii := dp.Outcome()["default/dp1"] + assert.Equal(t, 2, len(ii)) + assert.Equal(t, `[POP-503] At current load, CPU under allocated. Current:20000m vs Requested:1000m (2000.00%)`, ii[0].Message) + assert.Equal(t, `[POP-505] At current load, Memory under allocated. Current:20Mi vs Requested:1Mi (2000.00%)`, ii[1].Message) + + ii = dp.Outcome()["default/dp2"] + assert.Equal(t, 5, len(ii)) + assert.Equal(t, `[POP-501] Unhealthy 1 desired but have 0 available`, ii[0].Message) + assert.Equal(t, rules.ErrorLevel, ii[0].Level) + assert.Equal(t, `[POP-507] Deployment references ServiceAccount "sa-bozo" which does not exist`, ii[1].Message) + assert.Equal(t, rules.ErrorLevel, ii[1].Level) + assert.Equal(t, `[POP-106] No resources requests/limits defined`, ii[2].Message) + assert.Equal(t, rules.WarnLevel, ii[2].Level) + assert.Equal(t, `[POP-108] Unnamed port 3000`, ii[3].Message) + assert.Equal(t, rules.InfoLevel, ii[3].Level) + assert.Equal(t, `[POP-508] No pods match controller selector: app=pod-bozo`, ii[4].Message) + assert.Equal(t, rules.ErrorLevel, ii[4].Level) + + ii = dp.Outcome()["default/dp3"] + assert.Equal(t, 2, len(ii)) + assert.Equal(t, `[POP-500] Zero scale detected`, ii[0].Message) + assert.Equal(t, rules.WarnLevel, ii[0].Level) + assert.Equal(t, `no pod selector given`, ii[1].Message) + assert.Equal(t, rules.ErrorLevel, ii[1].Level) +} diff --git a/internal/lint/ds.go b/internal/lint/ds.go new file mode 100644 index 00000000..7f5491ae --- /dev/null +++ b/internal/lint/ds.go @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "context" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/client" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/issues" + appsv1 "k8s.io/api/apps/v1" + v1 "k8s.io/api/core/v1" +) + +// DaemonSet tracks DaemonSet sanitization. +type DaemonSet struct { + *issues.Collector + + db *db.DB +} + +// NewDaemonSet returns a new instance. +func NewDaemonSet(co *issues.Collector, db *db.DB) *DaemonSet { + return &DaemonSet{ + Collector: co, + db: db, + } +} + +// Lint cleanse the resource. +func (s *DaemonSet) Lint(ctx context.Context) error { + over := pullOverAllocs(ctx) + txn, it := s.db.MustITFor(internal.Glossary[internal.DS]) + defer txn.Abort() + for o := it.Next(); o != nil; o = it.Next() { + ds := o.(*appsv1.DaemonSet) + fqn := client.FQN(ds.Namespace, ds.Name) + s.InitOutcome(fqn) + ctx = internal.WithSpec(ctx, specFor(fqn, ds)) + + s.checkDaemonSet(ctx, ds) + s.checkContainers(ctx, fqn, ds.Spec.Template.Spec) + s.checkUtilization(ctx, over, ds) + } + + return nil +} + +func (s *DaemonSet) checkDaemonSet(ctx context.Context, ds *appsv1.DaemonSet) { + if ds.Spec.Template.Spec.ServiceAccountName == "" { + return + } + _, err := s.db.Find(internal.Glossary[internal.SA], client.FQN(ds.Namespace, ds.Spec.Template.Spec.ServiceAccountName)) + if err != nil { + s.AddCode(ctx, 507, ds.Spec.Template.Spec.ServiceAccountName) + } +} + +// CheckContainers runs thru deployment template and checks pod configuration. +func (s *DaemonSet) checkContainers(ctx context.Context, fqn string, spec v1.PodSpec) { + c := NewContainer(fqn, s) + for _, co := range spec.InitContainers { + c.sanitize(ctx, co, false) + } + for _, co := range spec.Containers { + c.sanitize(ctx, co, false) + } +} + +// CheckUtilization checks deployments requested resources vs current utilization. +func (s *DaemonSet) checkUtilization(ctx context.Context, over bool, ds *appsv1.DaemonSet) { + mx := resourceUsage(ctx, s.db, s, ds.Namespace, ds.Spec.Selector) + if mx.RequestCPU.IsZero() && mx.RequestMEM.IsZero() { + return + } + + checkCPU(ctx, s, over, mx) + checkMEM(ctx, s, over, mx) +} diff --git a/internal/lint/ds_test.go b/internal/lint/ds_test.go new file mode 100644 index 00000000..66a0e9ce --- /dev/null +++ b/internal/lint/ds_test.go @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "testing" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/rules" + "github.com/derailed/popeye/internal/test" + "github.com/stretchr/testify/assert" + appsv1 "k8s.io/api/apps/v1" + v1 "k8s.io/api/core/v1" + mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" +) + +func TestDSLint(t *testing.T) { + dba, err := test.NewTestDB() + assert.NoError(t, err) + l := db.NewLoader(dba) + + ctx := test.MakeCtx(t) + assert.NoError(t, test.LoadDB[*appsv1.DaemonSet](ctx, l.DB, "apps/ds/1.yaml", internal.Glossary[internal.DS])) + assert.NoError(t, test.LoadDB[*v1.ServiceAccount](ctx, l.DB, "core/sa/1.yaml", internal.Glossary[internal.SA])) + assert.NoError(t, test.LoadDB[*v1.Pod](ctx, l.DB, "core/pod/1.yaml", internal.Glossary[internal.PO])) + assert.NoError(t, test.LoadDB[*mv1beta1.PodMetrics](ctx, l.DB, "mx/pod/1.yaml", internal.Glossary[internal.PMX])) + + ds := NewDaemonSet(test.MakeCollector(t), dba) + assert.Nil(t, ds.Lint(test.MakeContext("apps/v1/daemonsets", "daemonsets"))) + assert.Equal(t, 2, len(ds.Outcome())) + + ii := ds.Outcome()["default/ds1"] + assert.Equal(t, 2, len(ii)) + assert.Equal(t, `[POP-503] At current load, CPU under allocated. Current:20000m vs Requested:1000m (2000.00%)`, ii[0].Message) + assert.Equal(t, `[POP-505] At current load, Memory under allocated. Current:20Mi vs Requested:1Mi (2000.00%)`, ii[1].Message) + + ii = ds.Outcome()["default/ds2"] + assert.Equal(t, 6, len(ii)) + assert.Equal(t, `[POP-507] Deployment references ServiceAccount "sa-bozo" which does not exist`, ii[0].Message) + assert.Equal(t, rules.ErrorLevel, ii[0].Level) + assert.Equal(t, `[POP-100] Untagged docker image in use`, ii[1].Message) + assert.Equal(t, rules.ErrorLevel, ii[1].Level) + assert.Equal(t, `[POP-106] No resources requests/limits defined`, ii[2].Message) + assert.Equal(t, rules.WarnLevel, ii[2].Level) + assert.Equal(t, `[POP-100] Untagged docker image in use`, ii[3].Message) + assert.Equal(t, rules.ErrorLevel, ii[3].Level) + assert.Equal(t, `[POP-106] No resources requests/limits defined`, ii[4].Message) + assert.Equal(t, rules.WarnLevel, ii[4].Level) + assert.Equal(t, `[POP-508] No pods match controller selector: app=p10`, ii[5].Message) + assert.Equal(t, rules.ErrorLevel, ii[5].Level) +} diff --git a/internal/lint/gw.go b/internal/lint/gw.go new file mode 100644 index 00000000..f5b37539 --- /dev/null +++ b/internal/lint/gw.go @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "context" + "fmt" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/client" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/issues" + gwv1 "sigs.k8s.io/gateway-api/apis/v1" +) + +type ( + // Gateway tracks Gateway sanitization. + Gateway struct { + *issues.Collector + + db *db.DB + } +) + +// NewGateway returns a new instance. +func NewGateway(co *issues.Collector, db *db.DB) *Gateway { + return &Gateway{ + Collector: co, + db: db, + } +} + +// Lint cleanse the resource. +func (s *Gateway) Lint(ctx context.Context) error { + txn, it := s.db.MustITFor(internal.Glossary[internal.GW]) + defer txn.Abort() + for o := it.Next(); o != nil; o = it.Next() { + gw := o.(*gwv1.Gateway) + fqn := client.FQN(gw.Namespace, gw.Name) + s.InitOutcome(fqn) + ctx = internal.WithSpec(ctx, specFor(fqn, gw)) + s.checkRefs(ctx, gw) + } + + return nil +} + +func (s *Gateway) checkRefs(ctx context.Context, gw *gwv1.Gateway) { + txn := s.db.Txn(false) + defer txn.Abort() + txn, it := s.db.MustITFor(internal.Glossary[internal.GWC]) + defer txn.Abort() + + for o := it.Next(); o != nil; o = it.Next() { + gwc, ok := o.(*gwv1.GatewayClass) + if !ok { + s.AddErr(ctx, fmt.Errorf("expecting gatewayclass but got %T", o)) + continue + } + if gwc.Name == string(gw.Spec.GatewayClassName) { + return + } + } + + s.AddCode(ctx, 407, gw.Kind, "GatewayClass", gw.Spec.GatewayClassName) +} diff --git a/internal/lint/gw_test.go b/internal/lint/gw_test.go new file mode 100644 index 00000000..72558682 --- /dev/null +++ b/internal/lint/gw_test.go @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "testing" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/rules" + "github.com/derailed/popeye/internal/test" + "github.com/stretchr/testify/assert" + gwv1 "sigs.k8s.io/gateway-api/apis/v1" +) + +func TestGatewayLint(t *testing.T) { + dba, err := test.NewTestDB() + assert.NoError(t, err) + l := db.NewLoader(dba) + + ctx := test.MakeCtx(t) + assert.NoError(t, test.LoadDB[*gwv1.GatewayClass](ctx, l.DB, "net/gwc/1.yaml", internal.Glossary[internal.GWC])) + assert.NoError(t, test.LoadDB[*gwv1.Gateway](ctx, l.DB, "net/gw/1.yaml", internal.Glossary[internal.GW])) + + gw := NewGateway(test.MakeCollector(t), dba) + assert.Nil(t, gw.Lint(test.MakeContext("gateway.networking.k8s.io/v1/gateways", "gateways"))) + assert.Equal(t, 2, len(gw.Outcome())) + + ii := gw.Outcome()["default/gw1"] + assert.Equal(t, 0, len(ii)) + + ii = gw.Outcome()["default/gw2"] + assert.Equal(t, 1, len(ii)) + assert.Equal(t, `[POP-407] Gateway references GatewayClass "gwc-bozo" which does not exist`, ii[0].Message) + assert.Equal(t, rules.ErrorLevel, ii[0].Level) + // assert.Equal(t, `[POP-407] Gateway references GatewayClass "gwc-bozo" which does not exist`, ii[0].Message) + // assert.Equal(t, rules.ErrorLevel, ii[0].Level) +} diff --git a/internal/lint/gwc.go b/internal/lint/gwc.go new file mode 100644 index 00000000..1476b151 --- /dev/null +++ b/internal/lint/gwc.go @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "context" + "fmt" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/client" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/issues" + gwv1 "sigs.k8s.io/gateway-api/apis/v1" +) + +type ( + // GatewayClass tracks GatewayClass sanitization. + GatewayClass struct { + *issues.Collector + + db *db.DB + } +) + +// NewGatewayClass returns a new instance. +func NewGatewayClass(co *issues.Collector, db *db.DB) *GatewayClass { + return &GatewayClass{ + Collector: co, + db: db, + } +} + +// Lint cleanse the resource. +func (s *GatewayClass) Lint(ctx context.Context) error { + txn, it := s.db.MustITFor(internal.Glossary[internal.GWC]) + defer txn.Abort() + for o := it.Next(); o != nil; o = it.Next() { + gwc := o.(*gwv1.GatewayClass) + fqn := client.FQN(gwc.Namespace, gwc.Name) + s.InitOutcome(fqn) + ctx = internal.WithSpec(ctx, specFor(fqn, gwc)) + s.checkRefs(ctx, gwc.Name) + } + + return nil +} + +func (s *GatewayClass) checkRefs(ctx context.Context, n string) { + txn := s.db.Txn(false) + defer txn.Abort() + txn, it := s.db.MustITFor(internal.Glossary[internal.GW]) + defer txn.Abort() + + for o := it.Next(); o != nil; o = it.Next() { + gw, ok := o.(*gwv1.Gateway) + if !ok { + s.AddErr(ctx, fmt.Errorf("expecting gateway but got %T", o)) + continue + } + if n == string(gw.Spec.GatewayClassName) { + return + } + } + + s.AddCode(ctx, 400) +} diff --git a/internal/lint/gwc_test.go b/internal/lint/gwc_test.go new file mode 100644 index 00000000..36059f2b --- /dev/null +++ b/internal/lint/gwc_test.go @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "testing" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/rules" + "github.com/derailed/popeye/internal/test" + "github.com/stretchr/testify/assert" + gwv1 "sigs.k8s.io/gateway-api/apis/v1" +) + +func TestGatewayClassLint(t *testing.T) { + dba, err := test.NewTestDB() + assert.NoError(t, err) + l := db.NewLoader(dba) + + ctx := test.MakeCtx(t) + assert.NoError(t, test.LoadDB[*gwv1.GatewayClass](ctx, l.DB, "net/gwc/1.yaml", internal.Glossary[internal.GWC])) + assert.NoError(t, test.LoadDB[*gwv1.Gateway](ctx, l.DB, "net/gw/1.yaml", internal.Glossary[internal.GW])) + + gwc := NewGatewayClass(test.MakeCollector(t), dba) + assert.Nil(t, gwc.Lint(test.MakeContext("gateway.networking.k8s.io/v1/gatewayclasses", "gatewayclasses"))) + assert.Equal(t, 2, len(gwc.Outcome())) + + ii := gwc.Outcome()["gwc1"] + assert.Equal(t, 0, len(ii)) + + ii = gwc.Outcome()["gwc2"] + assert.Equal(t, 1, len(ii)) + assert.Equal(t, `[POP-400] Used? Unable to locate resource reference`, ii[0].Message) + assert.Equal(t, rules.InfoLevel, ii[0].Level) +} diff --git a/internal/lint/gwr.go b/internal/lint/gwr.go new file mode 100644 index 00000000..f241d891 --- /dev/null +++ b/internal/lint/gwr.go @@ -0,0 +1,136 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "context" + "fmt" + "strconv" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/client" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/issues" + v1 "k8s.io/api/core/v1" + gwv1 "sigs.k8s.io/gateway-api/apis/v1" +) + +type ( + // HTTPRoute tracks HTTPRoute sanitization. + HTTPRoute struct { + *issues.Collector + + db *db.DB + } +) + +// NewHTTPRoute returns a new instance. +func NewHTTPRoute(co *issues.Collector, db *db.DB) *HTTPRoute { + return &HTTPRoute{ + Collector: co, + db: db, + } +} + +// Lint cleanse the resource. +func (s *HTTPRoute) Lint(ctx context.Context) error { + txn, it := s.db.MustITFor(internal.Glossary[internal.GWR]) + defer txn.Abort() + for o := it.Next(); o != nil; o = it.Next() { + gwr := o.(*gwv1.HTTPRoute) + fqn := client.FQN(gwr.Namespace, gwr.Name) + s.InitOutcome(fqn) + ctx = internal.WithSpec(ctx, specFor(fqn, gwr)) + s.checkRoute(ctx, fqn, gwr) + } + + return nil +} + +// Check service ref +func (s *HTTPRoute) checkRoute(ctx context.Context, fqn string, gwr *gwv1.HTTPRoute) { + for _, r := range gwr.Spec.ParentRefs { + switch { + case r.Group == nil: + var kind string + if r.Kind == nil { + kind = "Gateway" + } else { + kind = string(*r.Kind) + } + switch kind { + case "Gateway": + s.checkGWRef(ctx, gwr.Namespace, &r) + case "Service": + s.checkSvcRef(ctx, gwr.Namespace, &r) + default: + s.AddErr(ctx, fmt.Errorf("unhandled parent kind: %s", kind)) + } + case *r.Group == "", *r.Group == "Service": + s.checkSvcRef(ctx, gwr.Namespace, &r) + } + } + + for _, r := range gwr.Spec.Rules { + for _, be := range r.BackendRefs { + switch { + case be.Kind == nil, *be.Kind == "Service": + s.checkSvcBE(ctx, gwr.Namespace, &be.BackendRef) + } + } + } +} + +func (s *HTTPRoute) checkSvcBE(ctx context.Context, ns string, be *gwv1.BackendRef) { + if be.BackendObjectReference.Kind == nil || *be.BackendObjectReference.Kind == "Service" { + txn := s.db.Txn(false) + defer txn.Abort() + + if be.Namespace != nil { + ns = string(*be.Namespace) + } + fqn := client.FQN(ns, string(be.Name)) + o, err := s.db.Find(internal.Glossary[internal.SVC], fqn) + if err != nil { + s.AddCode(ctx, 407, "Route", "Service", fqn) + return + } + svc, ok := o.(*v1.Service) + if !ok { + s.AddErr(ctx, fmt.Errorf("expecting service but got %T", o)) + return + } + if be.Port == nil { + return + } + for _, p := range svc.Spec.Ports { + if p.Port == int32(*be.Port) { + return + } + } + s.AddCode(ctx, 1106, strconv.Itoa(int(*be.Port))) + } +} + +func (s *HTTPRoute) checkGWRef(ctx context.Context, ns string, ref *gwv1.ParentReference) { + if ref.Namespace != nil { + ns = string(*ref.Namespace) + } + fqn := client.FQN(ns, string(ref.Name)) + _, err := s.db.Find(internal.Glossary[internal.GW], fqn) + if err != nil { + s.AddCode(ctx, 407, "HTTPRoute", "Gateway", fqn) + } +} + +func (s *HTTPRoute) checkSvcRef(ctx context.Context, ns string, ref *gwv1.ParentReference) { + if ref.Namespace != nil { + ns = string(*ref.Namespace) + } + fqn := client.FQN(ns, string(ref.Name)) + _, err := s.db.Find(internal.Glossary[internal.SVC], fqn) + if err != nil { + s.AddCode(ctx, 407, "HTTPRoute", "Service", fqn) + } +} diff --git a/internal/lint/gwr_test.go b/internal/lint/gwr_test.go new file mode 100644 index 00000000..3c7fb96b --- /dev/null +++ b/internal/lint/gwr_test.go @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "testing" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/rules" + "github.com/derailed/popeye/internal/test" + "github.com/stretchr/testify/assert" + v1 "k8s.io/api/core/v1" + gwv1 "sigs.k8s.io/gateway-api/apis/v1" +) + +func TestHttpRouteTestLint(t *testing.T) { + dba, err := test.NewTestDB() + assert.NoError(t, err) + l := db.NewLoader(dba) + + ctx := test.MakeCtx(t) + assert.NoError(t, test.LoadDB[*gwv1.HTTPRoute](ctx, l.DB, "net/gwr/1.yaml", internal.Glossary[internal.GWR])) + assert.NoError(t, test.LoadDB[*gwv1.GatewayClass](ctx, l.DB, "net/gwc/1.yaml", internal.Glossary[internal.GWC])) + assert.NoError(t, test.LoadDB[*gwv1.Gateway](ctx, l.DB, "net/gw/1.yaml", internal.Glossary[internal.GW])) + assert.NoError(t, test.LoadDB[*v1.Service](ctx, l.DB, "core/svc/1.yaml", internal.Glossary[internal.SVC])) + + hr := NewHTTPRoute(test.MakeCollector(t), dba) + assert.Nil(t, hr.Lint(test.MakeContext("gateway.networking.k8s.io/v1/httproutes", "httproutes"))) + assert.Equal(t, 3, len(hr.Outcome())) + + ii := hr.Outcome()["default/r1"] + assert.Equal(t, 0, len(ii)) + + ii = hr.Outcome()["default/r2"] + assert.Equal(t, 2, len(ii)) + assert.Equal(t, `[POP-407] HTTPRoute references Gateway "default/gw-bozo" which does not exist`, ii[0].Message) + assert.Equal(t, rules.ErrorLevel, ii[0].Level) + assert.Equal(t, `[POP-1106] No target ports match service port 8080`, ii[1].Message) + assert.Equal(t, rules.ErrorLevel, ii[1].Level) + + ii = hr.Outcome()["default/r3"] + assert.Equal(t, 2, len(ii)) + assert.Equal(t, `[POP-407] HTTPRoute references Service "default/svc-bozo" which does not exist`, ii[0].Message) + assert.Equal(t, rules.ErrorLevel, ii[0].Level) + assert.Equal(t, `[POP-1106] No target ports match service port 9090`, ii[1].Message) + assert.Equal(t, rules.ErrorLevel, ii[1].Level) + +} diff --git a/internal/sanitize/helper.go b/internal/lint/helper.go similarity index 72% rename from internal/sanitize/helper.go rename to internal/lint/helper.go index ca8a5ea4..7e6239f9 100644 --- a/internal/sanitize/helper.go +++ b/internal/lint/helper.go @@ -1,15 +1,20 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright Authors of Popeye -package sanitize +package lint import ( + "context" "fmt" "strconv" "strings" + "github.com/derailed/popeye/internal/cache" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/rules" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) const ( @@ -23,6 +28,53 @@ const ( type qos = int +func specFor(fqn string, o metav1.ObjectMetaAccessor) rules.Spec { + spec := rules.Spec{ + FQN: fqn, + } + if o == nil { + return spec + } + + m := o.GetObjectMeta() + spec.Labels, spec.Annotations = m.GetLabels(), m.GetAnnotations() + + return spec +} + +func resourceUsage(ctx context.Context, dba *db.DB, c Collector, ns string, sel *metav1.LabelSelector) ConsumptionMetrics { + var mx ConsumptionMetrics + + pp, err := dba.FindPodsBySel(ns, sel) + if err != nil { + c.AddErr(ctx, err) + return mx + } + if len(pp) == 0 { + c.AddCode(ctx, 508, dumpSel(sel)) + return mx + } + + for _, pod := range pp { + fqn := cache.FQN(pod.Namespace, pod.Name) + cpu, mem := computePodResources(pod.Spec) + mx.QOS = pod.Status.QOSClass + mx.RequestCPU.Add(cpu) + mx.RequestMEM.Add(mem) + + pmx, err := dba.FindPMX(fqn) + if err != nil || pmx == nil { + continue + } + for _, cx := range pmx.Containers { + mx.CurrentCPU.Add(*cx.Usage.Cpu()) + mx.CurrentMEM.Add(*cx.Usage.Memory()) + } + } + + return mx +} + // Poor man plural... func pluralOf(s string, count int) string { if count > 1 { @@ -49,17 +101,6 @@ func ToPerc(v1, v2 int64) int64 { return int64((float64(v1) / float64(v2)) * 100) } -// In checks if a string is in a list of strings. -func in(ll []string, s string) bool { - for _, l := range ll { - if l == s { - return true - } - } - - return false -} - // ToMC converts quantity to millicores. func toMC(q resource.Quantity) int64 { return q.MilliValue() diff --git a/internal/lint/helper_test.go b/internal/lint/helper_test.go new file mode 100644 index 00000000..c30d4a97 --- /dev/null +++ b/internal/lint/helper_test.go @@ -0,0 +1,186 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +// import ( +// "testing" + +// "github.com/stretchr/testify/assert" +// v1 "k8s.io/api/core/v1" +// "k8s.io/apimachinery/pkg/api/resource" +// ) + +// func TestNamepaced(t *testing.T) { +// uu := []struct { +// s string +// ns, n string +// }{ +// {"fred/blee", "fred", "blee"}, +// {"fred", "", "fred"}, +// } + +// for _, u := range uu { +// ns, n := namespaced(u.s) +// assert.Equal(t, u.ns, ns) +// assert.Equal(t, u.n, n) +// } +// } + +// func TestPluralOf(t *testing.T) { +// uu := []struct { +// n string +// count int +// e string +// }{ +// {"fred", 0, "fred"}, +// {"fred", 1, "fred"}, +// {"fred", 2, "freds"}, +// } + +// for _, u := range uu { +// assert.Equal(t, u.e, pluralOf(u.n, u.count)) +// } +// } + +// func TestToPerc(t *testing.T) { +// uu := []struct { +// v1, v2, e int64 +// }{ +// {50, 100, 50}, +// {100, 0, 0}, +// {100, 50, 200}, +// } + +// for _, u := range uu { +// assert.Equal(t, u.e, ToPerc(u.v1, u.v2)) +// } +// } + +// func TestIn(t *testing.T) { +// uu := []struct { +// l []string +// s string +// e bool +// }{ +// {[]string{"a", "b", "c"}, "a", true}, +// {[]string{"a", "b", "c"}, "z", false}, +// } + +// for _, u := range uu { +// assert.Equal(t, u.e, in(u.l, u.s)) +// } +// } + +// func TestToMCRatio(t *testing.T) { +// uu := []struct { +// q1, q2 resource.Quantity +// r float64 +// }{ +// {test.ToQty("100m"), test.ToQty("200m"), 50}, +// {test.ToQty("100m"), test.ToQty("50m"), 200}, +// {test.ToQty("0m"), test.ToQty("5m"), 0}, +// {test.ToQty("10m"), test.ToQty("0m"), 0}, +// } + +// for _, u := range uu { +// assert.Equal(t, u.r, toMCRatio(u.q1, u.q2)) +// } +// } + +// func TestToMEMRatio(t *testing.T) { +// uu := []struct { +// q1, q2 resource.Quantity +// r float64 +// }{ +// {test.ToQty("10Mi"), test.ToQty("20Mi"), 50}, +// {test.ToQty("20Mi"), test.ToQty("10Mi"), 200}, +// {test.ToQty("0Mi"), test.ToQty("5Mi"), 0}, +// {test.ToQty("10Mi"), test.ToQty("0Mi"), 0}, +// } + +// for _, u := range uu { +// assert.Equal(t, u.r, toMEMRatio(u.q1, u.q2)) +// } +// } + +// func TestContainerResources(t *testing.T) { +// uu := map[string]struct { +// co v1.Container +// cpu, mem *resource.Quantity +// qos qos +// }{ +// "none": { +// co: makeContainer("c1", coOpts{ +// image: "fred:1.0.1", +// }), +// qos: qosBestEffort, +// }, +// "guaranteed": { +// co: makeContainer("c1", coOpts{ +// image: "fred:1.0.1", +// rcpu: "100m", +// rmem: "10Mi", +// lcpu: "100m", +// lmem: "10Mi", +// }), +// cpu: makeQty("100m"), +// mem: makeQty("10Mi"), +// qos: qosGuaranteed, +// }, +// "bustableLimit": { +// co: makeContainer("c1", coOpts{ +// image: "fred:1.0.1", +// lcpu: "100m", +// lmem: "10Mi", +// }), +// cpu: makeQty("100m"), +// mem: makeQty("10Mi"), +// qos: qosBurstable, +// }, +// "burstableRequest": { +// co: makeContainer("c1", coOpts{ +// image: "fred:1.0.1", +// rcpu: "100m", +// rmem: "10Mi", +// }), +// cpu: makeQty("100m"), +// mem: makeQty("10Mi"), +// qos: qosBurstable, +// }, +// } + +// for k := range uu { +// u := uu[k] +// t.Run(k, func(t *testing.T) { +// cpu, mem, qos := containerResources(u.co) + +// assert.Equal(t, cpu, u.cpu) +// assert.Equal(t, mem, u.mem) +// assert.Equal(t, u.qos, qos) +// }) +// } +// } + +// func TestPortAsString(t *testing.T) { +// uu := []struct { +// port v1.ServicePort +// e string +// }{ +// {v1.ServicePort{Protocol: v1.ProtocolTCP, Name: "p1", Port: 80}, "TCP:p1:80"}, +// {v1.ServicePort{Protocol: v1.ProtocolUDP, Name: "", Port: 80}, "UDP::80"}, +// } + +// for _, u := range uu { +// assert.Equal(t, u.e, portAsStr(u.port)) +// } +// } + +// // ---------------------------------------------------------------------------- +// // Helpers... + +// func test.ToQty(s string) resource.Quantity { +// q, _ := resource.ParseQuantity(s) + +// return q +// } diff --git a/internal/sanitize/hpa.go b/internal/lint/hpa.go similarity index 53% rename from internal/sanitize/hpa.go rename to internal/lint/hpa.go index 7f6f07ee..b7f96364 100644 --- a/internal/sanitize/hpa.go +++ b/internal/lint/hpa.go @@ -1,84 +1,88 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright Authors of Popeye -package sanitize +package lint import ( "context" + "strings" "github.com/derailed/popeye/internal" "github.com/derailed/popeye/internal/cache" + "github.com/derailed/popeye/internal/client" + "github.com/derailed/popeye/internal/db" "github.com/derailed/popeye/internal/issues" + appsv1 "k8s.io/api/apps/v1" autoscalingv1 "k8s.io/api/autoscaling/v1" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" - mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" ) -type ( - // PodMetricsLister handles pods metrics. - PodMetricsLister interface { - ListPodsMetrics() map[string]*mv1beta1.PodMetrics - } - - // ClusterMetricsLister handles cluster metrics. - ClusterMetricsLister interface { - ListAvailableMetrics(map[string]*v1.Node) v1.ResourceList - } - - // HorizontalPodAutoscaler represents a HorizontalPodAutoscaler linter. - HorizontalPodAutoscaler struct { - *issues.Collector - HpaLister - } +// HorizontalPodAutoscaler represents a HorizontalPodAutoscaler linter. +type HorizontalPodAutoscaler struct { + *issues.Collector - // HpaLister list available hpas on a cluster. - HpaLister interface { - NodeLister - DeploymentLister - StatefulSetLister - ClusterMetricsLister - ListHorizontalPodAutoscalers() map[string]*autoscalingv1.HorizontalPodAutoscaler - } -) + db *db.DB +} -// NewHorizontalPodAutoscaler returns a new ServiceAccount linter. -func NewHorizontalPodAutoscaler(co *issues.Collector, lister HpaLister) *HorizontalPodAutoscaler { +// NewHorizontalPodAutoscaler returns a new instance. +func NewHorizontalPodAutoscaler(co *issues.Collector, db *db.DB) *HorizontalPodAutoscaler { return &HorizontalPodAutoscaler{ Collector: co, - HpaLister: lister, + db: db, } } -// Sanitize an horizontalpodautoscaler. -func (h *HorizontalPodAutoscaler) Sanitize(ctx context.Context) error { +// Lint sanitizes an hpa. +func (h *HorizontalPodAutoscaler) Lint(ctx context.Context) error { var ( tcpu, tmem resource.Quantity current int32 ) - res := h.ListAvailableMetrics(h.ListNodes()) - for fqn, hpa := range h.ListHorizontalPodAutoscalers() { + res, err := cache.ListAvailableMetrics(h.db) + if err != nil { + return err + } + txn, it := h.db.MustITFor(internal.Glossary[internal.HPA]) + defer txn.Abort() + for o := it.Next(); o != nil; o = it.Next() { + hpa := o.(*autoscalingv1.HorizontalPodAutoscaler) + fqn := client.FQN(hpa.Namespace, hpa.Name) h.InitOutcome(fqn) - ctx = internal.WithFQN(ctx, fqn) + ctx = internal.WithSpec(ctx, specFor(fqn, hpa)) var rcpu, rmem resource.Quantity ns, _ := namespaced(fqn) switch hpa.Spec.ScaleTargetRef.Kind { case "Deployment": - dpFqn, dps := cache.FQN(ns, hpa.Spec.ScaleTargetRef.Name), h.ListDeployments() - if dp, ok := dps[dpFqn]; ok { + rfqn := cache.FQN(ns, hpa.Spec.ScaleTargetRef.Name) + if o, err := h.db.Find(internal.Glossary[internal.DP], rfqn); err == nil { + dp := o.(*appsv1.Deployment) rcpu, rmem = podResources(dp.Spec.Template.Spec) current = dp.Status.AvailableReplicas } else { - h.AddCode(ctx, 600, fqn, dpFqn) + h.AddCode(ctx, 600, fqn, strings.ToLower(hpa.Spec.ScaleTargetRef.Kind), rfqn) + continue + } + + case "ReplicaSet": + rfqn := cache.FQN(ns, hpa.Spec.ScaleTargetRef.Name) + if o, err := h.db.Find(internal.Glossary[internal.RS], rfqn); err == nil { + rs := o.(*appsv1.ReplicaSet) + rcpu, rmem = podResources(rs.Spec.Template.Spec) + current = rs.Status.AvailableReplicas + } else { + h.AddCode(ctx, 600, fqn, strings.ToLower(hpa.Spec.ScaleTargetRef.Kind), rfqn) continue } + case "StatefulSet": - stsFqn, sts := cache.FQN(ns, hpa.Spec.ScaleTargetRef.Name), h.ListStatefulSets() - if st, ok := sts[stsFqn]; ok { - rcpu, rmem = podResources(st.Spec.Template.Spec) - current = st.Status.CurrentReplicas + rfqn := cache.FQN(ns, hpa.Spec.ScaleTargetRef.Name) + if o, err := h.db.Find(internal.Glossary[internal.STS], rfqn); err == nil { + sts := o.(*appsv1.StatefulSet) + rcpu, rmem = podResources(sts.Spec.Template.Spec) + current = sts.Status.CurrentReplicas } else { - h.AddCode(ctx, 601, fqn, stsFqn) + h.AddCode(ctx, 600, fqn, strings.ToLower(hpa.Spec.ScaleTargetRef.Kind), rfqn) continue } } @@ -87,10 +91,6 @@ func (h *HorizontalPodAutoscaler) Sanitize(ctx context.Context) error { list := h.checkResources(ctx, hpa.Spec.MaxReplicas, current, rList, res) tcpu.Add(*list.Cpu()) tmem.Add(*list.Memory()) - - if h.NoConcerns(fqn) && h.Config.ExcludeFQN(internal.MustExtractSectionGVR(ctx), fqn) { - h.ClearOutcome(fqn) - } } h.checkUtilization(ctx, tcpu, tmem, res) @@ -121,7 +121,7 @@ func (h *HorizontalPodAutoscaler) checkResources(ctx context.Context, max, curre func (h *HorizontalPodAutoscaler) checkUtilization(ctx context.Context, tcpu, tmem resource.Quantity, res v1.ResourceList) { acpu, amem := *res.Cpu(), *res.Memory() - ctx = internal.WithFQN(ctx, "HPA") + ctx = internal.WithSpec(ctx, specFor("HPA", nil)) if toMC(tcpu) > toMC(acpu) { cpu := tcpu.DeepCopy() cpu.Sub(acpu) diff --git a/internal/lint/hpa_test.go b/internal/lint/hpa_test.go new file mode 100644 index 00000000..03010ab1 --- /dev/null +++ b/internal/lint/hpa_test.go @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "testing" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/rules" + "github.com/derailed/popeye/internal/test" + "github.com/stretchr/testify/assert" + appsv1 "k8s.io/api/apps/v1" + autoscalingv1 "k8s.io/api/autoscaling/v1" + v1 "k8s.io/api/core/v1" + mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" +) + +func TestHPALint(t *testing.T) { + dba, err := test.NewTestDB() + assert.NoError(t, err) + l := db.NewLoader(dba) + + ctx := test.MakeCtx(t) + assert.NoError(t, test.LoadDB[*autoscalingv1.HorizontalPodAutoscaler](ctx, l.DB, "autoscaling/hpa/1.yaml", internal.Glossary[internal.HPA])) + assert.NoError(t, test.LoadDB[*appsv1.Deployment](ctx, l.DB, "apps/dp/1.yaml", internal.Glossary[internal.DP])) + assert.NoError(t, test.LoadDB[*appsv1.ReplicaSet](ctx, l.DB, "apps/rs/1.yaml", internal.Glossary[internal.RS])) + assert.NoError(t, test.LoadDB[*appsv1.StatefulSet](ctx, l.DB, "apps/sts/1.yaml", internal.Glossary[internal.STS])) + assert.NoError(t, test.LoadDB[*v1.Node](ctx, l.DB, "core/node/1.yaml", internal.Glossary[internal.NO])) + assert.NoError(t, test.LoadDB[*v1.Pod](ctx, l.DB, "core/pod/2.yaml", internal.Glossary[internal.PO])) + assert.NoError(t, test.LoadDB[*v1.ServiceAccount](ctx, l.DB, "core/sa/1.yaml", internal.Glossary[internal.SA])) + assert.NoError(t, test.LoadDB[*mv1beta1.PodMetrics](ctx, l.DB, "mx/pod/1.yaml", internal.Glossary[internal.PMX])) + assert.NoError(t, test.LoadDB[*mv1beta1.NodeMetrics](ctx, l.DB, "mx/node/1.yaml", internal.Glossary[internal.NMX])) + + hpa := NewHorizontalPodAutoscaler(test.MakeCollector(t), dba) + assert.Nil(t, hpa.Lint(test.MakeContext("autoscaling/v1/horizontalpodautoscalers", "horizontalpodautoscalers"))) + assert.Equal(t, 7, len(hpa.Outcome())) + + ii := hpa.Outcome()["default/hpa1"] + assert.Equal(t, 1, len(ii)) + + ii = hpa.Outcome()["default/hpa2"] + assert.Equal(t, 1, len(ii)) + assert.Equal(t, `[POP-600] HPA default/hpa2 references a deployment which does not exist: default/dp-toast`, ii[0].Message) + assert.Equal(t, rules.ErrorLevel, ii[0].Level) + + ii = hpa.Outcome()["default/hpa3"] + assert.Equal(t, 1, len(ii)) + assert.Equal(t, `[POP-600] HPA default/hpa3 references a replicaset which does not exist: default/rs-toast`, ii[0].Message) + assert.Equal(t, rules.ErrorLevel, ii[0].Level) + + ii = hpa.Outcome()["default/hpa4"] + assert.Equal(t, 1, len(ii)) + assert.Equal(t, `[POP-600] HPA default/hpa4 references a statefulset which does not exist: default/sts-toast`, ii[0].Message) + assert.Equal(t, rules.ErrorLevel, ii[0].Level) + + ii = hpa.Outcome()["default/hpa5"] + assert.Equal(t, 1, len(ii)) + + ii = hpa.Outcome()["default/hpa6"] + assert.Equal(t, 1, len(ii)) + +} diff --git a/internal/lint/ing.go b/internal/lint/ing.go new file mode 100644 index 00000000..4e18fdb9 --- /dev/null +++ b/internal/lint/ing.go @@ -0,0 +1,113 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "context" + "errors" + "fmt" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/cache" + "github.com/derailed/popeye/internal/client" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/issues" + v1 "k8s.io/api/core/v1" + netv1 "k8s.io/api/networking/v1" +) + +type ( + // Ingress tracks Ingress sanitization. + Ingress struct { + *issues.Collector + + db *db.DB + } +) + +// NewIngress returns a new instance. +func NewIngress(co *issues.Collector, db *db.DB) *Ingress { + return &Ingress{ + Collector: co, + db: db, + } +} + +// Lint cleanse the resource. +func (s *Ingress) Lint(ctx context.Context) error { + txn, it := s.db.MustITFor(internal.Glossary[internal.ING]) + defer txn.Abort() + for o := it.Next(); o != nil; o = it.Next() { + ing := o.(*netv1.Ingress) + fqn := client.FQN(ing.Namespace, ing.Name) + s.InitOutcome(fqn) + ctx = internal.WithSpec(ctx, specFor(fqn, ing)) + + for _, ing := range ing.Status.LoadBalancer.Ingress { + for _, p := range ing.Ports { + if p.Error != nil { + s.AddCode(ctx, 1400, *p.Error) + } + } + } + for _, r := range ing.Spec.Rules { + http := r.IngressRuleValue.HTTP + if http == nil { + continue + } + for _, h := range http.Paths { + s.checkBackendSvc(ctx, ing.Namespace, h.Backend.Service) + s.checkBackendRef(ctx, ing.Namespace, h.Backend.Resource) + } + } + } + + return nil +} + +func (s *Ingress) checkBackendRef(ctx context.Context, ns string, be *v1.TypedLocalObjectReference) { + if be == nil { + return + } + s.AddErr(ctx, errors.New("Ingress local obj refs not supported")) +} + +func (s *Ingress) checkBackendSvc(ctx context.Context, ns string, be *netv1.IngressServiceBackend) { + if be == nil { + return + } + o, err := s.db.Find(internal.Glossary[internal.SVC], cache.FQN(ns, be.Name)) + if err != nil { + s.AddCode(ctx, 1401, be.Name) + return + } + isvc, ok := o.(*v1.Service) + if !ok { + s.AddErr(ctx, fmt.Errorf("expecting service but got %T", o)) + return + } + if !s.findPortByNumberOrName(ctx, isvc.Spec.Ports, be.Port) { + s.AddCode(ctx, 1402, fmt.Sprintf("%s:%d", be.Port.Name, be.Port.Number)) + } + if be.Port.Name == "" { + if be.Port.Number == 0 { + s.AddCode(ctx, 1404) + return + } + s.AddCode(ctx, 1403, be.Port.Number) + } +} + +func (s *Ingress) findPortByNumberOrName(ctx context.Context, pp []v1.ServicePort, port netv1.ServiceBackendPort) bool { + for _, p := range pp { + if p.Name == port.Name { + return true + } + if p.Port == port.Number { + return true + } + } + + return false +} diff --git a/internal/lint/ing_test.go b/internal/lint/ing_test.go new file mode 100644 index 00000000..c9f4ebef --- /dev/null +++ b/internal/lint/ing_test.go @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "testing" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/rules" + "github.com/derailed/popeye/internal/test" + "github.com/stretchr/testify/assert" + v1 "k8s.io/api/core/v1" + netv1 "k8s.io/api/networking/v1" +) + +func TestIngLint(t *testing.T) { + dba, err := test.NewTestDB() + assert.NoError(t, err) + l := db.NewLoader(dba) + + ctx := test.MakeCtx(t) + assert.NoError(t, test.LoadDB[*netv1.Ingress](ctx, l.DB, "net/ingress/1.yaml", internal.Glossary[internal.ING])) + assert.NoError(t, test.LoadDB[*v1.Service](ctx, l.DB, "core/svc/1.yaml", internal.Glossary[internal.SVC])) + + ing := NewIngress(test.MakeCollector(t), dba) + assert.Nil(t, ing.Lint(test.MakeContext("networking.k8s.io/v1/ingresses", "ingresses"))) + assert.Equal(t, 6, len(ing.Outcome())) + + ii := ing.Outcome()["default/ing1"] + assert.Equal(t, 0, len(ii)) + + ii = ing.Outcome()["default/ing2"] + assert.Equal(t, 1, len(ii)) + assert.Equal(t, `[POP-1403] Ingress backend uses a port#, prefer a named port: 9090`, ii[0].Message) + assert.Equal(t, rules.InfoLevel, ii[0].Level) + + ii = ing.Outcome()["default/ing3"] + assert.Equal(t, 1, len(ii)) + assert.Equal(t, `[POP-1401] Ingress references a service backend which does not exist: s2`, ii[0].Message) + assert.Equal(t, rules.ErrorLevel, ii[0].Level) + + ii = ing.Outcome()["default/ing4"] + assert.Equal(t, 2, len(ii)) + assert.Equal(t, `[POP-1402] Ingress references a service port which is not defined: :0`, ii[0].Message) + assert.Equal(t, rules.ErrorLevel, ii[0].Level) + assert.Equal(t, `[POP-1404] Invalid Ingress backend spec. Must use port name or number`, ii[1].Message) + assert.Equal(t, rules.ErrorLevel, ii[1].Level) + + ii = ing.Outcome()["default/ing5"] + assert.Equal(t, 2, len(ii)) + assert.Equal(t, `[POP-1400] Ingress LoadBalancer port reported an error: boom`, ii[0].Message) + assert.Equal(t, rules.ErrorLevel, ii[0].Level) + assert.Equal(t, `Ingress local obj refs not supported`, ii[1].Message) + assert.Equal(t, rules.ErrorLevel, ii[1].Level) + + ii = ing.Outcome()["default/ing6"] + assert.Equal(t, 2, len(ii)) + assert.Equal(t, `[POP-1402] Ingress references a service port which is not defined: :9091`, ii[0].Message) + assert.Equal(t, rules.ErrorLevel, ii[0].Level) + assert.Equal(t, `[POP-1403] Ingress backend uses a port#, prefer a named port: 9091`, ii[1].Message) + assert.Equal(t, rules.InfoLevel, ii[1].Level) +} diff --git a/internal/lint/job.go b/internal/lint/job.go new file mode 100644 index 00000000..d028ee13 --- /dev/null +++ b/internal/lint/job.go @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "context" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/client" + "github.com/derailed/popeye/internal/dao" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/issues" + batchv1 "k8s.io/api/batch/v1" + v1 "k8s.io/api/core/v1" +) + +// Job tracks Job linting. +type Job struct { + *issues.Collector + + db *db.DB +} + +// NewJob returns a new instance. +func NewJob(co *issues.Collector, db *db.DB) *Job { + return &Job{ + Collector: co, + db: db, + } +} + +// Lint cleanse the resource. +func (s *Job) Lint(ctx context.Context) error { + over := pullOverAllocs(ctx) + txn, it := s.db.MustITFor(internal.Glossary[internal.JOB]) + defer txn.Abort() + for o := it.Next(); o != nil; o = it.Next() { + j := o.(*batchv1.Job) + fqn := client.FQN(j.Namespace, j.Name) + s.InitOutcome(fqn) + ctx = internal.WithSpec(ctx, specFor(fqn, j)) + s.checkJob(ctx, fqn, j) + s.checkContainers(ctx, fqn, j.Spec.Template.Spec) + s.checkUtilization(ctx, over, fqn) + } + + return nil +} + +// CheckJob checks if Job contract is currently happy or not. +func (s *Job) checkJob(ctx context.Context, fqn string, j *batchv1.Job) { + checkEvents(ctx, s.Collector, internal.JOB, dao.WarnEvt, "Job", fqn) + + if j.Spec.Suspend != nil && *j.Spec.Suspend { + s.AddCode(ctx, 1500, j.Kind) + } + + if sa := j.Spec.Template.Spec.ServiceAccountName; sa != "" { + saFQN := client.FQN(j.Namespace, sa) + if !s.db.Exists(internal.Glossary[internal.SA], saFQN) { + s.AddCode(ctx, 307, j.Kind, sa) + } + } +} + +// CheckContainers runs thru Job template and checks pod configuration. +func (s *Job) checkContainers(ctx context.Context, fqn string, spec v1.PodSpec) { + c := NewContainer(fqn, s) + for _, co := range spec.InitContainers { + c.sanitize(ctx, co, false) + } + for _, co := range spec.Containers { + c.sanitize(ctx, co, false) + } +} + +// CheckUtilization checks Jobs requested resources vs current utilization. +func (s *Job) checkUtilization(ctx context.Context, over bool, fqn string) { + jj, err := s.db.FindJobs(fqn) + if err != nil { + s.AddErr(ctx, err) + return + } + mx := jobResourceUsage(ctx, s.db, s, jj) + if mx.RequestCPU.IsZero() && mx.RequestMEM.IsZero() { + return + } + checkCPU(ctx, s, over, mx) + checkMEM(ctx, s, over, mx) +} diff --git a/internal/lint/job_test.go b/internal/lint/job_test.go new file mode 100644 index 00000000..e335d3c5 --- /dev/null +++ b/internal/lint/job_test.go @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "testing" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/rules" + "github.com/derailed/popeye/internal/test" + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" + batchv1 "k8s.io/api/batch/v1" + v1 "k8s.io/api/core/v1" + mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" +) + +func init() { + zerolog.SetGlobalLevel(zerolog.FatalLevel) +} + +func TestJobLint(t *testing.T) { + dba, err := test.NewTestDB() + assert.NoError(t, err) + l := db.NewLoader(dba) + + ctx := test.MakeCtx(t) + assert.NoError(t, test.LoadDB[*batchv1.Job](ctx, l.DB, "batch/job/1.yaml", internal.Glossary[internal.JOB])) + assert.NoError(t, test.LoadDB[*v1.ServiceAccount](ctx, l.DB, "core/sa/1.yaml", internal.Glossary[internal.SA])) + assert.NoError(t, test.LoadDB[*v1.Pod](ctx, l.DB, "core/pod/1.yaml", internal.Glossary[internal.PO])) + assert.NoError(t, test.LoadDB[*mv1beta1.PodMetrics](ctx, l.DB, "mx/pod/1.yaml", internal.Glossary[internal.PMX])) + + j := NewJob(test.MakeCollector(t), dba) + assert.Nil(t, j.Lint(test.MakeContext("batch/v1/jobs", "jobs"))) + assert.Equal(t, 3, len(j.Outcome())) + + ii := j.Outcome()["default/j1"] + assert.Equal(t, 0, len(ii)) + + ii = j.Outcome()["default/j2"] + assert.Equal(t, 2, len(ii)) + assert.Equal(t, `[POP-100] Untagged docker image in use`, ii[0].Message) + assert.Equal(t, `[POP-106] No resources requests/limits defined`, ii[1].Message) + assert.Equal(t, rules.WarnLevel, ii[1].Level) +} diff --git a/internal/sanitize/metrics.go b/internal/lint/metrics_helpers.go similarity index 91% rename from internal/sanitize/metrics.go rename to internal/lint/metrics_helpers.go index 8809c003..d2ed6355 100644 --- a/internal/sanitize/metrics.go +++ b/internal/lint/metrics_helpers.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright Authors of Popeye -package sanitize +package lint import ( v1 "k8s.io/api/core/v1" @@ -17,9 +17,9 @@ type ConsumptionMetrics struct { RequestedStorage resource.Quantity } -// ReqAbsCPURatio returns abasolute cpu ratio. +// ReqAbsCPURatio returns absolute cpu ratio. func (d *ConsumptionMetrics) ReqAbsCPURatio() float64 { - if d.CurrentCPU.Cmp(d.RequestCPU) > 1 { + if d.CurrentCPU.Cmp(d.RequestCPU) == 1 { return toMCRatio(d.CurrentCPU, d.RequestCPU) } return toMCRatio(d.RequestCPU, d.CurrentCPU) @@ -35,7 +35,7 @@ func (d *ConsumptionMetrics) ReqCPURatio() float64 { // ReqAbsMEMRatio returns absolute mem ratio. func (d *ConsumptionMetrics) ReqAbsMEMRatio() float64 { - if d.CurrentMEM.Cmp(d.RequestMEM) > 1 { + if d.CurrentMEM.Cmp(d.RequestMEM) == 1 { return toMEMRatio(d.CurrentMEM, d.RequestMEM) } return toMEMRatio(d.RequestMEM, d.CurrentMEM) diff --git a/internal/lint/metrics_helpers_test.go b/internal/lint/metrics_helpers_test.go new file mode 100644 index 00000000..ae04f41b --- /dev/null +++ b/internal/lint/metrics_helpers_test.go @@ -0,0 +1,198 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "k8s.io/apimachinery/pkg/api/resource" +) + +func TestLimitCPURatio(t *testing.T) { + uu := map[string]struct { + mx ConsumptionMetrics + e float64 + }{ + "empty": {}, + "same": { + mx: ConsumptionMetrics{ + CurrentCPU: *resource.NewQuantity(10, resource.DecimalExponent), + LimitCPU: *resource.NewQuantity(10, resource.DecimalExponent), + }, + e: 100, + }, + "delta": { + mx: ConsumptionMetrics{ + CurrentCPU: *resource.NewQuantity(100, resource.DecimalExponent), + LimitCPU: *resource.NewQuantity(10, resource.DecimalExponent), + }, + e: 1_000, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, u.mx.LimitCPURatio()) + }) + } +} + +func TestReqAbsCPURatio(t *testing.T) { + uu := map[string]struct { + mx ConsumptionMetrics + e float64 + }{ + "empty": {}, + "same": { + mx: ConsumptionMetrics{ + CurrentCPU: *resource.NewQuantity(10, resource.DecimalExponent), + RequestCPU: *resource.NewQuantity(10, resource.DecimalExponent), + }, + e: 100, + }, + "higher": { + mx: ConsumptionMetrics{ + CurrentCPU: *resource.NewQuantity(2, resource.DecimalExponent), + RequestCPU: *resource.NewQuantity(10, resource.DecimalExponent), + }, + e: 500, + }, + "lower": { + mx: ConsumptionMetrics{ + CurrentCPU: *resource.NewQuantity(10, resource.DecimalExponent), + RequestCPU: *resource.NewQuantity(100, resource.DecimalExponent), + }, + e: 1_000, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, u.mx.ReqAbsCPURatio()) + }) + } +} + +func TestReqCPURatio(t *testing.T) { + uu := map[string]struct { + mx ConsumptionMetrics + e float64 + }{ + "empty": {}, + "same": { + mx: ConsumptionMetrics{ + CurrentCPU: *resource.NewQuantity(10, resource.DecimalExponent), + RequestCPU: *resource.NewQuantity(10, resource.DecimalExponent), + }, + e: 100, + }, + "higher": { + mx: ConsumptionMetrics{ + CurrentCPU: *resource.NewQuantity(100, resource.DecimalExponent), + RequestCPU: *resource.NewQuantity(10, resource.DecimalExponent), + }, + e: 1000, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, u.mx.ReqCPURatio()) + }) + } +} + +func TestReqAbsMEMRatio(t *testing.T) { + uu := map[string]struct { + mx ConsumptionMetrics + e float64 + }{ + "empty": {}, + "same": { + mx: ConsumptionMetrics{ + CurrentMEM: *resource.NewQuantity(10*megaByte, resource.DecimalExponent), + RequestMEM: *resource.NewQuantity(10*megaByte, resource.DecimalExponent), + }, + e: 100, + }, + "higher": { + mx: ConsumptionMetrics{ + CurrentMEM: *resource.NewQuantity(100*megaByte, resource.DecimalExponent), + RequestMEM: *resource.NewQuantity(10*megaByte, resource.DecimalExponent), + }, + e: 1_000, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, u.mx.ReqAbsMEMRatio()) + }) + } +} + +func TestReqMEMRatio(t *testing.T) { + uu := map[string]struct { + mx ConsumptionMetrics + e float64 + }{ + "empty": {}, + "same": { + mx: ConsumptionMetrics{ + CurrentMEM: *resource.NewQuantity(10*megaByte, resource.DecimalExponent), + RequestMEM: *resource.NewQuantity(10*megaByte, resource.DecimalExponent), + }, + e: 100, + }, + "delta": { + mx: ConsumptionMetrics{ + CurrentMEM: *resource.NewQuantity(100*megaByte, resource.DecimalExponent), + RequestMEM: *resource.NewQuantity(10*megaByte, resource.DecimalExponent), + }, + e: 1_000, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, u.mx.ReqMEMRatio()) + }) + } +} + +func TestLimitMEMRatio(t *testing.T) { + uu := map[string]struct { + mx ConsumptionMetrics + e float64 + }{ + "empty": {}, + "same": { + mx: ConsumptionMetrics{ + CurrentMEM: *resource.NewQuantity(10*megaByte, resource.DecimalExponent), + LimitMEM: *resource.NewQuantity(10*megaByte, resource.DecimalExponent), + }, + e: 100, + }, + "delta": { + mx: ConsumptionMetrics{ + CurrentMEM: *resource.NewQuantity(100*megaByte, resource.DecimalExponent), + LimitMEM: *resource.NewQuantity(10*megaByte, resource.DecimalExponent), + }, + e: 1_000, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, u.mx.LimitMEMRatio()) + }) + } +} diff --git a/internal/lint/node.go b/internal/lint/node.go new file mode 100644 index 00000000..c1d38020 --- /dev/null +++ b/internal/lint/node.go @@ -0,0 +1,174 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "context" + "errors" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/client" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/issues" + v1 "k8s.io/api/core/v1" +) + +type ( + tolerations map[string]struct{} + + // Node represents a Node linter. + Node struct { + *issues.Collector + + db *db.DB + } +) + +// NewNode returns a new instance. +func NewNode(co *issues.Collector, db *db.DB) *Node { + return &Node{ + Collector: co, + db: db, + } +} + +// Lint cleanse the resource. +func (n *Node) Lint(ctx context.Context) error { + nmx := make(client.NodesMetrics) + n.nodesMetrics(nmx) + + tt, err := n.fetchPodTolerations() + if err != nil { + return err + } + txn, it := n.db.MustITFor(internal.Glossary[internal.NO]) + defer txn.Abort() + for o := it.Next(); o != nil; o = it.Next() { + no := o.(*v1.Node) + fqn := no.Name + n.InitOutcome(fqn) + ctx = internal.WithSpec(ctx, specFor(fqn, no)) + + n.checkConditions(ctx, no) + if err := n.checkTaints(ctx, no.Spec.Taints, tt); err != nil { + n.AddErr(ctx, err) + } + n.checkUtilization(ctx, nmx[fqn]) + } + + return nil +} + +func (n *Node) checkTaints(ctx context.Context, taints []v1.Taint, tt tolerations) error { + for _, ta := range taints { + if _, ok := tt[mkKey(ta.Key, ta.Value)]; !ok { + n.AddCode(ctx, 700, ta.Key) + } + } + + return nil +} + +func (n *Node) fetchPodTolerations() (tolerations, error) { + tt := make(tolerations) + txn, it := n.db.MustITFor(internal.Glossary[internal.PO]) + defer txn.Abort() + + for o := it.Next(); o != nil; o = it.Next() { + po, ok := o.(*v1.Pod) + if !ok { + return nil, errors.New("po conversion failed") + } + for _, t := range po.Spec.Tolerations { + tt[mkKey(t.Key, t.Value)] = struct{}{} + } + } + + return tt, nil +} + +func mkKey(k, v string) string { + return k + ":" + v +} + +func (n *Node) checkConditions(ctx context.Context, no *v1.Node) { + if no.Spec.Unschedulable { + n.AddCode(ctx, 711) + } + for _, c := range no.Status.Conditions { + if c.Status == v1.ConditionUnknown { + n.AddCode(ctx, 701) + } + if c.Type == v1.NodeReady && c.Status == v1.ConditionFalse { + n.AddCode(ctx, 702) + } + n.statusReport(ctx, c.Type, c.Status) + } +} + +func (n *Node) statusReport(ctx context.Context, cond v1.NodeConditionType, status v1.ConditionStatus) { + if status == v1.ConditionFalse { + return + } + + switch cond { + case v1.NodeMemoryPressure: + n.AddCode(ctx, 704) + case v1.NodeDiskPressure: + n.AddCode(ctx, 705) + case v1.NodePIDPressure: + n.AddCode(ctx, 706) + case v1.NodeNetworkUnavailable: + n.AddCode(ctx, 707) + } +} + +func (n *Node) checkUtilization(ctx context.Context, mx client.NodeMetrics) { + if mx.Empty() { + n.AddCode(ctx, 708) + return + } + + percCPU := ToPerc(toMC(mx.CurrentCPU), toMC(mx.AvailableCPU)) + cpuLimit := int64(n.NodeCPULimit()) + if percCPU > cpuLimit { + n.AddCode(ctx, 709, cpuLimit, percCPU) + } + + percMEM := ToPerc(toMB(mx.CurrentMEM), toMB(mx.AvailableMEM)) + memLimit := int64(n.NodeMEMLimit()) + if percMEM > memLimit { + n.AddCode(ctx, 710, memLimit, percMEM) + } +} + +func (n *Node) nodesMetrics(nmx client.NodesMetrics) { + mm, err := n.db.ListNMX() + if err != nil || len(mm) == 0 { + return + } + + txn, it := n.db.MustITFor(internal.Glossary[internal.NO]) + defer txn.Abort() + for o := it.Next(); o != nil; o = it.Next() { + no := o.(*v1.Node) + if len(no.Status.Allocatable) == 0 && len(no.Status.Capacity) == 0 { + continue + } + nmx[no.Name] = client.NodeMetrics{ + AvailableCPU: *no.Status.Allocatable.Cpu(), + AvailableMEM: *no.Status.Allocatable.Memory(), + TotalCPU: *no.Status.Capacity.Cpu(), + TotalMEM: *no.Status.Capacity.Memory(), + } + } + + for _, m := range mm { + if mx, ok := nmx[m.Name]; ok { + mx.CurrentCPU = *m.Usage.Cpu() + mx.CurrentMEM = *m.Usage.Memory() + nmx[m.Name] = mx + } + } +} diff --git a/internal/sanitize/node_bench_test.go b/internal/lint/node_bench_test.go similarity index 97% rename from internal/sanitize/node_bench_test.go rename to internal/lint/node_bench_test.go index b026e435..17f09c25 100644 --- a/internal/sanitize/node_bench_test.go +++ b/internal/lint/node_bench_test.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright Authors of Popeye -package sanitize +package lint // import ( // "testing" diff --git a/internal/lint/node_test.go b/internal/lint/node_test.go new file mode 100644 index 00000000..84586db6 --- /dev/null +++ b/internal/lint/node_test.go @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "testing" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/rules" + "github.com/derailed/popeye/internal/test" + "github.com/stretchr/testify/assert" + v1 "k8s.io/api/core/v1" + mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" +) + +func TestNodeSanitizer(t *testing.T) { + dba, err := test.NewTestDB() + assert.NoError(t, err) + l := db.NewLoader(dba) + + ctx := test.MakeCtx(t) + assert.NoError(t, test.LoadDB[*v1.Node](ctx, l.DB, "core/node/1.yaml", internal.Glossary[internal.NO])) + assert.NoError(t, test.LoadDB[*v1.Pod](ctx, l.DB, "core/pod/1.yaml", internal.Glossary[internal.PO])) + assert.NoError(t, test.LoadDB[*mv1beta1.NodeMetrics](ctx, l.DB, "mx/node/1.yaml", internal.Glossary[internal.NMX])) + + no := NewNode(test.MakeCollector(t), dba) + assert.Nil(t, no.Lint(test.MakeContext("v1/nodes", "nodes"))) + assert.Equal(t, 5, len(no.Outcome())) + + ii := no.Outcome()["n1"] + assert.Equal(t, 0, len(ii)) + + ii = no.Outcome()["n2"] + assert.Equal(t, 2, len(ii)) + assert.Equal(t, `[POP-707] No network configured on node`, ii[0].Message) + assert.Equal(t, rules.ErrorLevel, ii[0].Level) + assert.Equal(t, `[POP-700] Found taint "t2" but no pod can tolerate`, ii[1].Message) + assert.Equal(t, rules.WarnLevel, ii[1].Level) + + ii = no.Outcome()["n3"] + assert.Equal(t, 5, len(ii)) + assert.Equal(t, `[POP-704] Insufficient memory`, ii[0].Message) + assert.Equal(t, rules.WarnLevel, ii[0].Level) + assert.Equal(t, `[POP-705] Insufficient disk space`, ii[1].Message) + assert.Equal(t, rules.WarnLevel, ii[1].Level) + assert.Equal(t, `[POP-706] Insufficient PIDs on Node`, ii[2].Message) + assert.Equal(t, rules.ErrorLevel, ii[2].Level) + assert.Equal(t, `[POP-707] No network configured on node`, ii[3].Message) + assert.Equal(t, rules.ErrorLevel, ii[3].Level) + assert.Equal(t, `[POP-708] No node metrics available`, ii[4].Message) + assert.Equal(t, rules.InfoLevel, ii[4].Level) + + ii = no.Outcome()["n4"] + assert.Equal(t, 4, len(ii)) + assert.Equal(t, `[POP-711] Scheduling disabled`, ii[0].Message) + assert.Equal(t, rules.WarnLevel, ii[0].Level) + assert.Equal(t, `[POP-701] Node has an unknown condition`, ii[1].Message) + assert.Equal(t, rules.WarnLevel, ii[1].Level) + assert.Equal(t, `[POP-702] Node is not in ready state`, ii[2].Message) + assert.Equal(t, rules.ErrorLevel, ii[2].Level) + assert.Equal(t, `[POP-708] No node metrics available`, ii[3].Message) + assert.Equal(t, rules.InfoLevel, ii[3].Level) + + ii = no.Outcome()["n5"] + assert.Equal(t, 2, len(ii)) + assert.Equal(t, `[POP-709] CPU threshold (80%) reached 20000%`, ii[0].Message) + assert.Equal(t, rules.WarnLevel, ii[0].Level) + assert.Equal(t, `[POP-710] Memory threshold (80%) reached 400%`, ii[1].Message) + assert.Equal(t, rules.WarnLevel, ii[1].Level) +} diff --git a/internal/lint/np.go b/internal/lint/np.go new file mode 100644 index 00000000..4526abb0 --- /dev/null +++ b/internal/lint/np.go @@ -0,0 +1,261 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "context" + "fmt" + "net" + "strings" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/client" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/issues" + v1 "k8s.io/api/core/v1" + netv1 "k8s.io/api/networking/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type direction string + +const ( + dirIn direction = "Ingress" + dirOut direction = "Egress" + bothPols = "All" + noPols = "" +) + +// NetworkPolicy tracks NetworkPolicy sanitizatios. +type NetworkPolicy struct { + *issues.Collector + + db *db.DB + ipCache map[string][]v1.PodIP +} + +// NewNetworkPolicy returns a new instance. +func NewNetworkPolicy(co *issues.Collector, db *db.DB) *NetworkPolicy { + return &NetworkPolicy{ + Collector: co, + db: db, + ipCache: make(map[string][]v1.PodIP), + } +} + +// Lint cleanse the resource. +func (s *NetworkPolicy) Lint(ctx context.Context) error { + txn, it := s.db.MustITFor(internal.Glossary[internal.NP]) + defer txn.Abort() + for o := it.Next(); o != nil; o = it.Next() { + np := o.(*netv1.NetworkPolicy) + fqn := client.FQN(np.Namespace, np.Name) + s.InitOutcome(fqn) + ctx = internal.WithSpec(ctx, specFor(fqn, np)) + + s.checkSelector(ctx, fqn, np.Spec.PodSelector) + s.checkIngresses(ctx, fqn, np.Spec.Ingress) + s.checkEgresses(ctx, fqn, np.Spec.Egress) + s.checkRuleType(ctx, fqn, &np.Spec) + } + + return nil +} + +func (s *NetworkPolicy) checkRuleType(ctx context.Context, fqn string, spec *netv1.NetworkPolicySpec) { + if spec.PodSelector.Size() > 0 { + return + } + + switch { + case isAllowAll(spec): + s.AddCode(ctx, 1203, "Allow", bothPols) + case isAllowAllIngress(spec): + s.AddCode(ctx, 1203, "Allow All", dirIn) + case isAllowAllEgress(spec): + s.AddCode(ctx, 1203, "Allow All", dirOut) + case isDenyAll(spec): + s.AddCode(ctx, 1203, "Deny", bothPols) + case isDenyAllIngress(spec): + s.AddCode(ctx, 1203, "Deny All", dirIn) + case isDenyAllEgress(spec): + s.AddCode(ctx, 1203, "Deny All", dirOut) + } +} + +func isDefaultDenyAll(np *netv1.NetworkPolicy) bool { + if len(np.Spec.Ingress) > 0 { + return false + } + if len(np.Spec.Egress) > 0 { + return false + } + if np.Spec.PodSelector.Size() > 0 { + return false + } + + return len(np.Spec.PolicyTypes) == 2 +} + +func (s *NetworkPolicy) checkSelector(ctx context.Context, fqn string, sel metav1.LabelSelector) { + ns, _ := client.Namespaced(fqn) + if sel.Size() > 0 { + pp, err := s.db.FindPodsBySel(ns, &sel) + if err != nil || len(pp) == 0 { + s.AddCode(ctx, 1200, dumpSel(&sel)) + return + } + } +} + +func (s *NetworkPolicy) checkIngresses(ctx context.Context, fqn string, rr []netv1.NetworkPolicyIngressRule) { + for _, r := range rr { + for _, from := range r.From { + s.checkSelectors(ctx, fqn, from.NamespaceSelector, from.PodSelector, dirIn) + s.checkIPBlocks(ctx, fqn, from.IPBlock, dirIn) + } + } +} + +func (s *NetworkPolicy) checkEgresses(ctx context.Context, fqn string, rr []netv1.NetworkPolicyEgressRule) { + for _, r := range rr { + for _, to := range r.To { + s.checkSelectors(ctx, fqn, to.NamespaceSelector, to.PodSelector, dirOut) + s.checkIPBlocks(ctx, fqn, to.IPBlock, dirOut) + } + } +} + +func (s *NetworkPolicy) checkSelectors(ctx context.Context, fqn string, nsSel, podSel *metav1.LabelSelector, d direction) { + ns, _ := client.Namespaced(fqn) + if nsSel != nil && nsSel.Size() > 0 { + nss, err := s.db.FindNSBySel(nsSel) + if err != nil { + s.AddErr(ctx, fmt.Errorf("unable to locate namespace using selector: %s", dumpSel(nsSel))) + return + } + s.checkNSSelector(ctx, nsSel, nss, d) + s.checkPodSelector(ctx, nss, podSel, d) + return + } + nss, err := s.db.FindNS(ns) + if err != nil { + s.AddErr(ctx, fmt.Errorf("unable to locate namespace: %q", ns)) + return + } + s.checkPodSelector(ctx, []*v1.Namespace{nss}, podSel, d) +} + +func (s *NetworkPolicy) checkIPBlocks(ctx context.Context, fqn string, b *netv1.IPBlock, d direction) { + if b == nil { + return + } + ns, _ := client.Namespaced(fqn) + _, ipnet, err := net.ParseCIDR(b.CIDR) + if err != nil { + s.AddErr(ctx, err) + } + if !s.matchPips(ns, ipnet) { + s.AddCode(ctx, 1206, d, b.CIDR) + } + for _, ex := range b.Except { + _, ipnet, err := net.ParseCIDR(ex) + if err != nil { + s.AddErr(ctx, err) + continue + } + if !s.matchPips(ns, ipnet) { + s.AddCode(ctx, 1207, d, ex) + } + } +} + +func (s *NetworkPolicy) matchPips(ns string, ipnet *net.IPNet) bool { + if ipnet == nil { + return false + } + txn, it := s.db.MustITForNS(internal.Glossary[internal.PO], ns) + defer txn.Abort() + for o := it.Next(); o != nil; o = it.Next() { + po := o.(*v1.Pod) + for _, ip := range po.Status.PodIPs { + if ipnet.Contains(net.ParseIP(ip.IP)) { + return true + } + } + } + + return false +} + +func (s *NetworkPolicy) checkPodSelector(ctx context.Context, nss []*v1.Namespace, sel *metav1.LabelSelector, d direction) { + if sel == nil || sel.Size() == 0 { + return + } + + var found bool + nn := make([]string, 0, len(nss)) + for _, ns := range nss { + pp, err := s.db.FindPodsBySel(ns.Name, sel) + if err != nil { + s.AddErr(ctx, fmt.Errorf("unable to locate pods by selector: %w", err)) + return + } + if len(pp) > 0 { + found = true + } else { + nn = append(nn, ns.Name) + } + } + if !found { + if len(nn) > 0 { + s.AddCode(ctx, 1208, d, dumpSel(sel), strings.Join(nn, ",")) + } else { + s.AddCode(ctx, 1202, d, dumpSel(sel)) + } + } +} + +func (s *NetworkPolicy) checkNSSelector(ctx context.Context, sel *metav1.LabelSelector, nss []*v1.Namespace, d direction) bool { + if len(nss) == 0 { + s.AddCode(ctx, 1201, d, dumpSel(sel)) + return false + } + + return true +} + +// Helpers... + +func dumpLabels(labels map[string]string) string { + if len(labels) == 0 { + return "" + } + ll := make([]string, 0, len(labels)) + for k, v := range labels { + ll = append(ll, fmt.Sprintf("%s=%s", k, v)) + } + + return strings.Join(ll, ",") +} + +func dumpSel(sel *metav1.LabelSelector) string { + if sel == nil { + return "n/a" + } + + var out string + out = dumpLabels(sel.MatchLabels) + + ll := make([]string, 0, len(sel.MatchExpressions)) + for _, v := range sel.MatchExpressions { + ll = append(ll, fmt.Sprintf("%s-%s-%s", v.Key, v.Operator, strings.Join(v.Values, ","))) + } + if out != "" && len(ll) > 0 { + out += "|" + } + out += strings.Join(ll, ",") + + return out +} diff --git a/internal/lint/np_helpers.go b/internal/lint/np_helpers.go new file mode 100644 index 00000000..193f823f --- /dev/null +++ b/internal/lint/np_helpers.go @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import netv1 "k8s.io/api/networking/v1" + +func noPodSel(spec *netv1.NetworkPolicySpec) bool { + return spec.PodSelector.Size() == 0 +} + +func isAllowAll(spec *netv1.NetworkPolicySpec) bool { + return noPodSel(spec) && + blankIngress(spec.Ingress) && blankEgress(spec.Egress) && + len(spec.PolicyTypes) == 2 +} + +func isAllowAllIngress(spec *netv1.NetworkPolicySpec) bool { + return noPodSel(spec) && + blankIngress(spec.Ingress) && polInclude(spec.PolicyTypes, dirIn) +} + +func isAllowAllEgress(spec *netv1.NetworkPolicySpec) bool { + return noPodSel(spec) && + blankEgress(spec.Egress) && polInclude(spec.PolicyTypes, dirOut) +} + +func isDeny(spec *netv1.NetworkPolicySpec) bool { + return noPodSel(spec) && spec.Egress == nil && spec.Ingress == nil +} + +func isDenyAll(spec *netv1.NetworkPolicySpec) bool { + return isDeny(spec) && len(spec.PolicyTypes) == 2 +} + +func isDenyAllIngress(spec *netv1.NetworkPolicySpec) bool { + return noPodSel(spec) && spec.Ingress == nil && polInclude(spec.PolicyTypes, dirIn) +} + +func isDenyAllEgress(spec *netv1.NetworkPolicySpec) bool { + return noPodSel(spec) && spec.Egress == nil && polInclude(spec.PolicyTypes, dirOut) +} + +func blankEgress(rr []netv1.NetworkPolicyEgressRule) bool { + return len(rr) == 1 && len(rr[0].Ports) == 0 && len(rr[0].To) == 0 +} + +func blankIngress(rr []netv1.NetworkPolicyIngressRule) bool { + return len(rr) == 1 && len(rr[0].Ports) == 0 && len(rr[0].From) == 0 +} + +func polInclude(pp []netv1.PolicyType, d direction) bool { + for _, p := range pp { + if p == netv1.PolicyType(d) { + return true + } + } + + return false +} diff --git a/internal/lint/np_test.go b/internal/lint/np_test.go new file mode 100644 index 00000000..67d99889 --- /dev/null +++ b/internal/lint/np_test.go @@ -0,0 +1,147 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "testing" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/rules" + "github.com/derailed/popeye/internal/test" + "github.com/stretchr/testify/assert" + v1 "k8s.io/api/core/v1" + netv1 "k8s.io/api/networking/v1" +) + +func TestNPLintDenyAll(t *testing.T) { + dba, err := test.NewTestDB() + assert.NoError(t, err) + l := db.NewLoader(dba) + + ctx := test.MakeCtx(t) + assert.NoError(t, test.LoadDB[*netv1.NetworkPolicy](ctx, l.DB, "net/np/2.yaml", internal.Glossary[internal.NP])) + assert.NoError(t, test.LoadDB[*v1.Namespace](ctx, l.DB, "core/ns/1.yaml", internal.Glossary[internal.NS])) + assert.NoError(t, test.LoadDB[*v1.Pod](ctx, l.DB, "core/pod/1.yaml", internal.Glossary[internal.PO])) + + np := NewNetworkPolicy(test.MakeCollector(t), dba) + assert.Nil(t, np.Lint(test.MakeContext("networking.k8s.io/v1/networkpolicies", "networkpolicies"))) + assert.Equal(t, 8, len(np.Outcome())) + + ii := np.Outcome()["default/deny-all"] + assert.Equal(t, 1, len(ii)) + assert.Equal(t, `[POP-1203] Deny All policy in effect`, ii[0].Message) + assert.Equal(t, rules.InfoLevel, ii[0].Level) + + ii = np.Outcome()["default/deny-all-ing"] + assert.Equal(t, 1, len(ii)) + assert.Equal(t, `[POP-1203] Deny All Ingress policy in effect`, ii[0].Message) + assert.Equal(t, rules.InfoLevel, ii[0].Level) + + ii = np.Outcome()["default/deny-all-eg"] + assert.Equal(t, 1, len(ii)) + assert.Equal(t, `[POP-1203] Deny All Egress policy in effect`, ii[0].Message) + assert.Equal(t, rules.InfoLevel, ii[0].Level) + + ii = np.Outcome()["default/allow-all"] + assert.Equal(t, 1, len(ii)) + assert.Equal(t, `[POP-1203] Allow All policy in effect`, ii[0].Message) + assert.Equal(t, rules.InfoLevel, ii[0].Level) + + ii = np.Outcome()["default/allow-all-ing"] + assert.Equal(t, 1, len(ii)) + assert.Equal(t, `[POP-1203] Allow All Ingress policy in effect`, ii[0].Message) + assert.Equal(t, rules.InfoLevel, ii[0].Level) + + ii = np.Outcome()["default/allow-all-eg"] + assert.Equal(t, 1, len(ii)) + assert.Equal(t, `[POP-1203] Allow All Egress policy in effect`, ii[0].Message) + assert.Equal(t, rules.InfoLevel, ii[0].Level) + + ii = np.Outcome()["default/ip-block-all-ing"] + assert.Equal(t, 2, len(ii)) + assert.Equal(t, `[POP-1206] No pods matched Egress IPBlock 172.2.0.0/24`, ii[0].Message) + assert.Equal(t, rules.WarnLevel, ii[0].Level) + assert.Equal(t, `[POP-1203] Deny All Ingress policy in effect`, ii[1].Message) + assert.Equal(t, rules.InfoLevel, ii[1].Level) + + ii = np.Outcome()["default/ip-block-all-eg"] + assert.Equal(t, 2, len(ii)) + assert.Equal(t, `[POP-1206] No pods matched Ingress IPBlock 172.2.0.0/24`, ii[0].Message) + assert.Equal(t, rules.WarnLevel, ii[0].Level) + assert.Equal(t, `[POP-1203] Deny All Egress policy in effect`, ii[1].Message) + assert.Equal(t, rules.InfoLevel, ii[1].Level) +} + +func TestNPLint(t *testing.T) { + dba, err := test.NewTestDB() + assert.NoError(t, err) + l := db.NewLoader(dba) + + ctx := test.MakeCtx(t) + assert.NoError(t, test.LoadDB[*netv1.NetworkPolicy](ctx, l.DB, "net/np/1.yaml", internal.Glossary[internal.NP])) + assert.NoError(t, test.LoadDB[*v1.Namespace](ctx, l.DB, "core/ns/1.yaml", internal.Glossary[internal.NS])) + assert.NoError(t, test.LoadDB[*v1.Pod](ctx, l.DB, "core/pod/1.yaml", internal.Glossary[internal.PO])) + + np := NewNetworkPolicy(test.MakeCollector(t), dba) + assert.Nil(t, np.Lint(test.MakeContext("networking.k8s.io/v1/networkpolicies", "networkpolicies"))) + assert.Equal(t, 3, len(np.Outcome())) + + ii := np.Outcome()["default/np1"] + assert.Equal(t, 0, len(ii)) + + ii = np.Outcome()["default/np2"] + assert.Equal(t, 3, len(ii)) + assert.Equal(t, `[POP-1207] No pods matched except Ingress IPBlock 172.1.1.0/24`, ii[0].Message) + assert.Equal(t, rules.WarnLevel, ii[0].Level) + assert.Equal(t, `[POP-1208] No pods match Ingress pod selector: app=p2 in namespace: ns2`, ii[1].Message) + assert.Equal(t, rules.WarnLevel, ii[1].Level) + assert.Equal(t, `[POP-1206] No pods matched Egress IPBlock 172.0.0.0/24`, ii[2].Message) + assert.Equal(t, rules.WarnLevel, ii[2].Level) + + ii = np.Outcome()["default/np3"] + assert.Equal(t, 6, len(ii)) + assert.Equal(t, `[POP-1200] No pods match pod selector: app=p-bozo`, ii[0].Message) + assert.Equal(t, rules.WarnLevel, ii[0].Level) + assert.Equal(t, `[POP-1206] No pods matched Ingress IPBlock 172.2.0.0/16`, ii[1].Message) + assert.Equal(t, rules.WarnLevel, ii[1].Level) + assert.Equal(t, `[POP-1207] No pods matched except Ingress IPBlock 172.2.1.0/24`, ii[2].Message) + assert.Equal(t, rules.WarnLevel, ii[2].Level) + assert.Equal(t, `[POP-1201] No namespaces match Ingress namespace selector: app-In-ns-bozo`, ii[3].Message) + assert.Equal(t, rules.WarnLevel, ii[3].Level) + assert.Equal(t, `[POP-1202] No pods match Ingress pod selector: app=pod-bozo`, ii[4].Message) + assert.Equal(t, rules.WarnLevel, ii[4].Level) + assert.Equal(t, `[POP-1208] No pods match Egress pod selector: app=p1-missing in namespace: default`, ii[5].Message) + assert.Equal(t, rules.WarnLevel, ii[5].Level) +} + +func Test_npDefaultDenyAll(t *testing.T) { + uu := map[string]struct { + path string + e bool + }{ + "open": { + path: "net/np/a.yaml", + }, + "deny-all": { + path: "net/np/deny-all.yaml", + e: true, + }, + "allow-all-ing": { + path: "net/np/allow-all-ing.yaml", + }, + "no-selector": { + path: "net/np/d.yaml", + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + np, err := test.LoadRes[netv1.NetworkPolicy](u.path) + assert.NoError(t, err) + assert.Equal(t, u.e, isDefaultDenyAll(np)) + }) + } +} diff --git a/internal/lint/ns.go b/internal/lint/ns.go new file mode 100644 index 00000000..2d123290 --- /dev/null +++ b/internal/lint/ns.go @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "context" + "errors" + "sync" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/cache" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/issues" + v1 "k8s.io/api/core/v1" +) + +// Namespace represents a Namespace linter. +type Namespace struct { + *issues.Collector + db *db.DB +} + +// NewNamespace returns a new instance. +func NewNamespace(co *issues.Collector, db *db.DB) *Namespace { + return &Namespace{ + Collector: co, + db: db, + } +} + +// ReferencedNamespaces fetch all namespaces referenced by pods and service accounts. +func (s *Namespace) ReferencedNamespaces(res map[string]struct{}) error { + var refs sync.Map + pod := cache.NewPod(s.db) + if err := pod.PodRefs(&refs); err != nil { + return err + } + sa := cache.NewServiceAccount(s.db) + if err := sa.ServiceAccountRefs(&refs); err != nil { + return err + } + if ss, ok := refs.Load("ns"); ok { + for ns := range ss.(internal.StringSet) { + res[ns] = struct{}{} + } + } + + return nil +} + +// Lint cleanse the resource. +func (s *Namespace) Lint(ctx context.Context) error { + used := make(map[string]struct{}) + if err := s.ReferencedNamespaces(used); err != nil { + s.AddErr(ctx, err) + } + txn, it := s.db.MustITFor(internal.Glossary[internal.NS]) + defer txn.Abort() + + for o := it.Next(); o != nil; o = it.Next() { + ns, ok := o.(*v1.Namespace) + if !ok { + return errors.New("expected ns") + } + fqn := ns.Name + s.InitOutcome(fqn) + ctx = internal.WithSpec(ctx, specFor(fqn, ns)) + + if s.checkActive(ctx, ns.Status.Phase) { + if _, ok := used[fqn]; !ok { + s.AddCode(ctx, 400) + } + } + } + + return nil +} + +func (s *Namespace) checkActive(ctx context.Context, p v1.NamespacePhase) bool { + if !isNSActive(p) { + s.AddCode(ctx, 800) + return false + } + + return true +} + +// ---------------------------------------------------------------------------- +// Helpers... + +func isNSActive(phase v1.NamespacePhase) bool { + return phase == v1.NamespaceActive +} diff --git a/internal/lint/ns_test.go b/internal/lint/ns_test.go new file mode 100644 index 00000000..ddeb3742 --- /dev/null +++ b/internal/lint/ns_test.go @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "testing" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/rules" + "github.com/derailed/popeye/internal/test" + "github.com/stretchr/testify/assert" + v1 "k8s.io/api/core/v1" +) + +func TestNSSanitizer(t *testing.T) { + dba, err := test.NewTestDB() + assert.NoError(t, err) + l := db.NewLoader(dba) + ctx := test.MakeCtx(t) + assert.NoError(t, test.LoadDB[*v1.Namespace](ctx, l.DB, "core/ns/1.yaml", internal.Glossary[internal.NS])) + assert.NoError(t, test.LoadDB[*v1.Pod](ctx, l.DB, "core/pod/1.yaml", internal.Glossary[internal.PO])) + + ns := NewNamespace(test.MakeCollector(t), dba) + assert.Nil(t, ns.Lint(test.MakeContext("v1/namespaces", "ns"))) + assert.Equal(t, 3, len(ns.Outcome())) + + ii := ns.Outcome()["default"] + assert.Equal(t, 0, len(ii)) + + ii = ns.Outcome()["ns1"] + assert.Equal(t, 1, len(ii)) + assert.Equal(t, "[POP-400] Used? Unable to locate resource reference", ii[0].Message) + assert.Equal(t, rules.InfoLevel, ii[0].Level) + + ii = ns.Outcome()["ns2"] + assert.Equal(t, 1, len(ii)) + assert.Equal(t, "[POP-800] Namespace is inactive", ii[0].Message) + assert.Equal(t, rules.ErrorLevel, ii[0].Level) +} diff --git a/internal/lint/pdb.go b/internal/lint/pdb.go new file mode 100644 index 00000000..a8774c37 --- /dev/null +++ b/internal/lint/pdb.go @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "context" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/client" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/issues" + polv1 "k8s.io/api/policy/v1" +) + +// PodDisruptionBudget tracks PodDisruptionBudget sanitization. +type PodDisruptionBudget struct { + *issues.Collector + + db *db.DB +} + +// NewPodDisruptionBudget returns a new PodDisruptionBudget linter. +func NewPodDisruptionBudget(c *issues.Collector, db *db.DB) *PodDisruptionBudget { + return &PodDisruptionBudget{ + Collector: c, + db: db, + } +} + +// Lint cleanse the resource. +func (p *PodDisruptionBudget) Lint(ctx context.Context) error { + txn, it := p.db.MustITFor(internal.Glossary[internal.PDB]) + defer txn.Abort() + for o := it.Next(); o != nil; o = it.Next() { + pdb := o.(*polv1.PodDisruptionBudget) + fqn := client.FQN(pdb.Namespace, pdb.Name) + p.InitOutcome(fqn) + ctx = internal.WithSpec(ctx, specFor(fqn, pdb)) + + p.checkInUse(ctx, pdb) + } + + return nil +} + +func (p *PodDisruptionBudget) checkInUse(ctx context.Context, pdb *polv1.PodDisruptionBudget) { + pp, err := p.db.FindPodsBySel(pdb.Namespace, pdb.Spec.Selector) + if err != nil || len(pp) == 0 { + p.AddCode(ctx, 900, dumpSel(pdb.Spec.Selector)) + return + } +} diff --git a/internal/lint/pdb_test.go b/internal/lint/pdb_test.go new file mode 100644 index 00000000..5a80ec01 --- /dev/null +++ b/internal/lint/pdb_test.go @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "testing" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/rules" + "github.com/derailed/popeye/internal/test" + "github.com/stretchr/testify/assert" + v1 "k8s.io/api/core/v1" + polv1 "k8s.io/api/policy/v1" +) + +func TestPDBLint(t *testing.T) { + dba, err := test.NewTestDB() + assert.NoError(t, err) + l := db.NewLoader(dba) + + ctx := test.MakeCtx(t) + assert.NoError(t, test.LoadDB[*polv1.PodDisruptionBudget](ctx, l.DB, "pol/pdb/1.yaml", internal.Glossary[internal.PDB])) + assert.NoError(t, test.LoadDB[*v1.Pod](ctx, l.DB, "core/pod/1.yaml", internal.Glossary[internal.PO])) + + pdb := NewPodDisruptionBudget(test.MakeCollector(t), dba) + assert.Nil(t, pdb.Lint(test.MakeContext("policy/v1/poddisruptionbudgets", "poddisruptionbudgets"))) + assert.Equal(t, 5, len(pdb.Outcome())) + + ii := pdb.Outcome()["default/pdb1"] + assert.Equal(t, 0, len(ii)) + + ii = pdb.Outcome()["default/pdb2"] + assert.Equal(t, 1, len(ii)) + assert.Equal(t, `[POP-900] No pods match pdb selector: app=p2`, ii[0].Message) + assert.Equal(t, rules.WarnLevel, ii[0].Level) + + ii = pdb.Outcome()["default/pdb3"] + assert.Equal(t, 1, len(ii)) + assert.Equal(t, `[POP-900] No pods match pdb selector: app=test4`, ii[0].Message) + assert.Equal(t, rules.WarnLevel, ii[0].Level) + + ii = pdb.Outcome()["default/pdb4"] + assert.Equal(t, 1, len(ii)) + assert.Equal(t, `[POP-900] No pods match pdb selector: app=test5`, ii[0].Message) + assert.Equal(t, rules.WarnLevel, ii[0].Level) + + ii = pdb.Outcome()["default/pdb4-1"] + assert.Equal(t, 1, len(ii)) + assert.Equal(t, `[POP-900] No pods match pdb selector: app=test5`, ii[0].Message) + assert.Equal(t, rules.WarnLevel, ii[0].Level) +} diff --git a/internal/lint/pod.go b/internal/lint/pod.go new file mode 100644 index 00000000..0bd2ad32 --- /dev/null +++ b/internal/lint/pod.go @@ -0,0 +1,468 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "context" + "fmt" + "net" + "sort" + "strings" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/cache" + "github.com/derailed/popeye/internal/client" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/issues" + "github.com/derailed/popeye/types" + "github.com/rs/zerolog/log" + v1 "k8s.io/api/core/v1" + netv1 "k8s.io/api/networking/v1" + policyv1 "k8s.io/api/policy/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" +) + +type ( + // Pod represents a Pod linter. + Pod struct { + *issues.Collector + + db *db.DB + } + + // PodMetric tracks pod metrics available and current range. + PodMetric interface { + CurrentCPU() int64 + CurrentMEM() int64 + Empty() bool + } +) + +// NewPod returns a new instance. +func NewPod(co *issues.Collector, db *db.DB) *Pod { + return &Pod{ + Collector: co, + db: db, + } +} + +// Lint cleanse the resource.. +func (s *Pod) Lint(ctx context.Context) error { + txn, it := s.db.MustITFor(internal.Glossary[internal.PO]) + defer txn.Abort() + for o := it.Next(); o != nil; o = it.Next() { + po := o.(*v1.Pod) + fqn := client.FQN(po.Namespace, po.Name) + s.InitOutcome(fqn) + defer s.CloseOutcome(ctx, fqn, nil) + + ctx = internal.WithSpec(ctx, specFor(fqn, po)) + s.checkStatus(ctx, po) + s.checkContainerStatus(ctx, fqn, po) + s.checkContainers(ctx, fqn, po) + s.checkOwnedByAnything(ctx, po.OwnerReferences) + s.checkNPs(ctx, po) + if !ownedByDaemonSet(po) { + s.checkPdb(ctx, po.ObjectMeta.Labels) + } + s.checkForMultiplePdbMatches(ctx, po.Namespace, po.ObjectMeta.Labels) + s.checkSecure(ctx, fqn, po.Spec) + + pmx, err := s.db.FindPMX(fqn) + if err != nil { + continue + } + cmx := make(client.ContainerMetrics) + containerMetrics(pmx, cmx) + s.checkUtilization(ctx, fqn, po, cmx) + } + + return nil +} + +func ownedByDaemonSet(po *v1.Pod) bool { + for _, o := range po.OwnerReferences { + if o.Kind == "DaemonSet" { + return true + } + } + return false +} + +func (s *Pod) checkNPs(ctx context.Context, pod *v1.Pod) { + txn, it := s.db.MustITForNS(internal.Glossary[internal.NP], pod.Namespace) + defer txn.Abort() + + matches := [2]int{} + for o := it.Next(); o != nil; o = it.Next() { + np := o.(*netv1.NetworkPolicy) + if isDenyAll(&np.Spec) || isAllowAll(&np.Spec) { + return + } + if isDenyAllIngress(&np.Spec) || isAllowAllIngress(&np.Spec) { + matches[0]++ + if s.checkEgresses(ctx, pod, np.Spec.Egress) { + matches[1]++ + } + continue + } + if isDenyAllEgress(&np.Spec) || isAllowAllEgress(&np.Spec) { + matches[1]++ + if s.checkIngresses(ctx, pod, np.Spec.Ingress) { + matches[0]++ + } + continue + } + if labelsMatch(&np.Spec.PodSelector, pod.Labels) { + if s.checkIngresses(ctx, pod, np.Spec.Ingress) { + matches[0]++ + } + if s.checkEgresses(ctx, pod, np.Spec.Egress) { + matches[1]++ + } + } + } + + if matches[0] == 0 { + s.AddCode(ctx, 1204, dirIn) + } + if matches[1] == 0 { + s.AddCode(ctx, 1204, dirOut) + } +} + +type Labels map[string]string + +func (s *Pod) isPodTargeted(pod *v1.Pod, nsSel, podSel *metav1.LabelSelector, b *netv1.IPBlock) (bool, error) { + nn, err := s.db.FindNSNameBySel(nsSel) + if err != nil { + return false, err + } + if len(nn) == 0 && b == nil { + if podSel == nil { + return false, nil + } + return labelsMatch(podSel, pod.Labels), nil + } + for _, sns := range nn { + if sns != pod.Namespace { + continue + } + if podSel != nil && podSel.Size() > 0 { + if labelsMatch(podSel, pod.Labels) { + return true, nil + } + } + } + + if b == nil { + return false, nil + } + _, ipnet, err := net.ParseCIDR(b.CIDR) + if err != nil { + return false, err + } + for _, ip := range pod.Status.PodIPs { + if ipnet.Contains(net.ParseIP(ip.IP)) { + return true, nil + } + } + + return false, nil +} + +func (s *Pod) checkIngresses(ctx context.Context, pod *v1.Pod, rr []netv1.NetworkPolicyIngressRule) bool { + var match int + if rr == nil { + return false + } + for _, r := range rr { + if r.From == nil { + return true + } + for _, from := range r.From { + ok, err := s.isPodTargeted(pod, from.NamespaceSelector, from.PodSelector, from.IPBlock) + if err != nil { + s.AddErr(ctx, err) + return true + } + if ok { + match++ + } + } + } + + return match > 0 +} + +func (s *Pod) checkEgresses(ctx context.Context, pod *v1.Pod, rr []netv1.NetworkPolicyEgressRule) bool { + if rr == nil { + return false + } + var match int + for _, r := range rr { + if r.To == nil { + return true + } + for _, to := range r.To { + ok, err := s.isPodTargeted(pod, to.NamespaceSelector, to.PodSelector, to.IPBlock) + if err != nil { + s.AddErr(ctx, err) + return true + } + if ok { + match++ + } + } + } + + return match > 0 +} + +func labelsMatch(sel *metav1.LabelSelector, ll map[string]string) bool { + if sel == nil || sel.Size() == 0 { + return true + } + + return db.MatchSelector(ll, sel) +} + +func (s *Pod) checkOwnedByAnything(ctx context.Context, ownerRefs []metav1.OwnerReference) { + if len(ownerRefs) == 0 { + s.AddCode(ctx, 208) + return + } + + controlled := false + for _, or := range ownerRefs { + if or.Controller != nil && *or.Controller { + controlled = true + break + } + } + + if !controlled { + s.AddCode(ctx, 208) + } +} + +func (s *Pod) checkPdb(ctx context.Context, labels map[string]string) { + if s.ForLabels(labels) == nil { + s.AddCode(ctx, 206) + } +} + +// ForLabels returns a pdb whose selector match the given labels. Returns nil if no match. +func (s *Pod) ForLabels(labels map[string]string) *policyv1.PodDisruptionBudget { + txn, it := s.db.MustITFor(internal.Glossary[internal.PDB]) + defer txn.Abort() + for o := it.Next(); o != nil; o = it.Next() { + pdb := o.(*policyv1.PodDisruptionBudget) + m, err := metav1.LabelSelectorAsMap(pdb.Spec.Selector) + if err != nil { + continue + } + if cache.MatchLabels(labels, m) { + return pdb + } + } + return nil +} + +func (s *Pod) checkUtilization(ctx context.Context, fqn string, po *v1.Pod, cmx client.ContainerMetrics) { + if len(cmx) == 0 { + return + } + for _, co := range po.Spec.Containers { + cmx, ok := cmx[co.Name] + if !ok { + continue + } + NewContainer(fqn, s).checkUtilization(ctx, co, cmx) + } +} + +func (s *Pod) checkSecure(ctx context.Context, fqn string, spec v1.PodSpec) { + if err := s.checkSA(ctx, fqn, spec); err != nil { + s.AddErr(ctx, err) + } + s.checkSecContext(ctx, fqn, spec) +} + +func (s *Pod) checkSA(ctx context.Context, fqn string, spec v1.PodSpec) error { + ns, _ := namespaced(fqn) + if spec.ServiceAccountName == "default" { + s.AddCode(ctx, 300) + } + + txn := s.db.Txn(false) + defer txn.Abort() + saFQN := cache.FQN(ns, spec.ServiceAccountName) + o, err := txn.First(internal.Glossary[internal.SA].String(), "id", saFQN) + if err != nil || o == nil { + s.AddCode(ctx, 307, "Pod", spec.ServiceAccountName) + if isBoolSet(spec.AutomountServiceAccountToken) { + s.AddCode(ctx, 301) + } + return nil + } + sa, ok := o.(*v1.ServiceAccount) + if !ok { + return fmt.Errorf("expecting SA %q but got %T", saFQN, o) + } + if spec.AutomountServiceAccountToken == nil { + if isBoolSet(sa.AutomountServiceAccountToken) { + s.AddCode(ctx, 301) + } + } else if isBoolSet(spec.AutomountServiceAccountToken) { + s.AddCode(ctx, 301) + } + + return nil +} + +func (s *Pod) checkSecContext(ctx context.Context, fqn string, spec v1.PodSpec) { + if spec.SecurityContext == nil { + return + } + + // If pod security ctx is present and we have + podSec := hasPodNonRootUser(spec.SecurityContext) + var victims int + for _, co := range spec.InitContainers { + if !checkCOSecurityContext(co) && !podSec { + victims++ + s.AddSubCode(internal.WithGroup(ctx, types.NewGVR("containers"), co.Name), 306) + } + } + for _, co := range spec.Containers { + if !checkCOSecurityContext(co) && !podSec { + victims++ + s.AddSubCode(internal.WithGroup(ctx, types.NewGVR("containers"), co.Name), 306) + } + } + if victims > 0 && !podSec { + s.AddCode(ctx, 302) + } +} + +func checkCOSecurityContext(co v1.Container) bool { + return hasCoNonRootUser(co.SecurityContext) +} + +func hasPodNonRootUser(sec *v1.PodSecurityContext) bool { + if sec == nil { + return false + } + if sec.RunAsNonRoot != nil { + return *sec.RunAsNonRoot + } + if sec.RunAsUser != nil { + return *sec.RunAsUser != 0 + } + return false +} + +func hasCoNonRootUser(sec *v1.SecurityContext) bool { + if sec == nil { + return false + } + if sec.RunAsNonRoot != nil { + return *sec.RunAsNonRoot + } + if sec.RunAsUser != nil { + return *sec.RunAsUser != 0 + } + return false +} + +func (s *Pod) checkContainers(ctx context.Context, fqn string, po *v1.Pod) { + co := NewContainer(fqn, s) + for _, c := range po.Spec.InitContainers { + co.sanitize(ctx, c, false) + } + for _, c := range po.Spec.Containers { + co.sanitize(ctx, c, !isPartOfJob(po)) + } +} + +func (s *Pod) checkContainerStatus(ctx context.Context, fqn string, po *v1.Pod) { + limit := s.RestartsLimit() + size := len(po.Status.InitContainerStatuses) + for _, cs := range po.Status.InitContainerStatuses { + newContainerStatus(s, fqn, size, true, limit).sanitize(ctx, cs) + } + + size = len(po.Status.ContainerStatuses) + for _, cs := range po.Status.ContainerStatuses { + newContainerStatus(s, fqn, size, false, limit).sanitize(ctx, cs) + } +} + +func (s *Pod) checkStatus(ctx context.Context, po *v1.Pod) { + switch po.Status.Phase { + case v1.PodRunning: + case v1.PodSucceeded: + default: + s.AddCode(ctx, 207, po.Status.Phase) + } +} + +// !!BOZO!! Check +func (s *Pod) checkForMultiplePdbMatches(ctx context.Context, podNamespace string, podLabels map[string]string) { + matchedPdbs := make([]string, 0, 10) + txn, it := s.db.MustITFor(internal.Glossary[internal.PDB]) + defer txn.Abort() + for o := it.Next(); o != nil; o = it.Next() { + pdb := o.(*policyv1.PodDisruptionBudget) + if podNamespace != pdb.Namespace { + continue + } + selector, err := metav1.LabelSelectorAsSelector(pdb.Spec.Selector) + if err != nil { + log.Error().Err(err).Msg("No selectors found") + return + } + if selector.Empty() || !selector.Matches(labels.Set(podLabels)) { + continue + } + matchedPdbs = append(matchedPdbs, pdb.Name) + } + if len(matchedPdbs) > 1 { + sort.Strings(matchedPdbs) + s.AddCode(ctx, 209, strings.Join(matchedPdbs, ", ")) + } +} + +// ---------------------------------------------------------------------------- +// Helpers... + +func containerMetrics(pmx *mv1beta1.PodMetrics, mx client.ContainerMetrics) { + if pmx == nil { + return + } + + for _, co := range pmx.Containers { + mx[co.Name] = client.Metrics{ + CurrentCPU: *co.Usage.Cpu(), + CurrentMEM: *co.Usage.Memory(), + } + } +} + +func isPartOfJob(po *v1.Pod) bool { + for _, o := range po.OwnerReferences { + if o.Kind == "Job" { + return true + } + } + + return false +} + +func isBoolSet(b *bool) bool { + return b != nil && *b +} diff --git a/internal/lint/pod_test.go b/internal/lint/pod_test.go new file mode 100644 index 00000000..2d70a2e8 --- /dev/null +++ b/internal/lint/pod_test.go @@ -0,0 +1,239 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "context" + "testing" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/test" + "github.com/stretchr/testify/assert" + v1 "k8s.io/api/core/v1" + netv1 "k8s.io/api/networking/v1" + polv1 "k8s.io/api/policy/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" +) + +func TestPodNPLint(t *testing.T) { + dba, err := test.NewTestDB() + assert.NoError(t, err) + l := db.NewLoader(dba) + + ctx := test.MakeCtx(t) + assert.NoError(t, test.LoadDB[*v1.Pod](ctx, l.DB, "core/pod/3.yaml", internal.Glossary[internal.PO])) + assert.NoError(t, test.LoadDB[*v1.ServiceAccount](ctx, l.DB, "core/sa/2.yaml", internal.Glossary[internal.SA])) + assert.NoError(t, test.LoadDB[*v1.Namespace](ctx, l.DB, "core/ns/1.yaml", internal.Glossary[internal.NS])) + assert.NoError(t, test.LoadDB[*polv1.PodDisruptionBudget](ctx, l.DB, "pol/pdb/1.yaml", internal.Glossary[internal.PDB])) + assert.NoError(t, test.LoadDB[*netv1.NetworkPolicy](ctx, l.DB, "net/np/3.yaml", internal.Glossary[internal.NP])) + assert.NoError(t, test.LoadDB[*mv1beta1.PodMetrics](ctx, l.DB, "mx/pod/1.yaml", internal.Glossary[internal.PMX])) + + po := NewPod(test.MakeCollector(t), dba) + assert.Nil(t, po.Lint(test.MakeContext("v1/pods", "pods"))) + assert.Equal(t, 2, len(po.Outcome())) + + ii := po.Outcome()["ns1/p1"] + assert.Equal(t, 1, len(ii)) + assert.Equal(t, `[POP-1204] Pod Egress is not secured by a network policy`, ii[0].Message) + + ii = po.Outcome()["ns2/p2"] + assert.Equal(t, 0, len(ii)) +} + +func TestPodCheckSecure(t *testing.T) { + uu := map[string]struct { + pod v1.Pod + issues int + }{ + "cool_1": { + pod: makeSecPod(secNonRootSet, secNonRootSet, secNonRootSet, secNonRootSet), + issues: 1, + }, + "cool_2": { + pod: makeSecPod(secNonRootSet, secNonRootUnset, secNonRootUnset, secNonRootUnset), + issues: 1, + }, + "cool_3": { + pod: makeSecPod(secNonRootUnset, secNonRootSet, secNonRootSet, secNonRootSet), + issues: 1, + }, + "cool_4": { + pod: makeSecPod(secNonRootUndefined, secNonRootSet, secNonRootSet, secNonRootSet), + issues: 1, + }, + "cool_5": { + pod: makeSecPod(secNonRootSet, secNonRootUndefined, secNonRootUndefined, secNonRootUndefined), + issues: 1, + }, + "hacked_1": { + pod: makeSecPod(secNonRootUndefined, secNonRootUndefined, secNonRootUndefined, secNonRootUndefined), + issues: 5, + }, + "hacked_2": { + pod: makeSecPod(secNonRootUndefined, secNonRootUnset, secNonRootUndefined, secNonRootUndefined), + issues: 5, + }, + "hacked_3": { + pod: makeSecPod(secNonRootUndefined, secNonRootSet, secNonRootUndefined, secNonRootUndefined), + issues: 4, + }, + "hacked_4": { + pod: makeSecPod(secNonRootUndefined, secNonRootUnset, secNonRootSet, secNonRootUndefined), + issues: 4, + }, + "toast": { + pod: makeSecPod(secNonRootUndefined, secNonRootUndefined, secNonRootUndefined, secNonRootUndefined), + issues: 5, + }, + } + + ctx := test.MakeContext("v1/pods", "po") + ctx = internal.WithSpec(ctx, specFor("default/p1", nil)) + ctx = context.WithValue(ctx, internal.KeyConfig, test.MakeConfig(t)) + dba, err := test.NewTestDB() + assert.NoError(t, err) + l := db.NewLoader(dba) + + assert.NoError(t, test.LoadDB[*v1.Pod](ctx, l.DB, "core/pod/2.yaml", internal.Glossary[internal.PO])) + assert.NoError(t, test.LoadDB[*v1.ServiceAccount](ctx, l.DB, "core/sa/1.yaml", internal.Glossary[internal.SA])) + assert.NoError(t, test.LoadDB[*polv1.PodDisruptionBudget](ctx, l.DB, "pol/pdb/1.yaml", internal.Glossary[internal.PDB])) + assert.NoError(t, test.LoadDB[*mv1beta1.PodMetrics](ctx, l.DB, "mx/pod/1.yaml", internal.Glossary[internal.PMX])) + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + p := NewPod(test.MakeCollector(t), dba) + p.checkSecure(ctx, "default/p1", u.pod.Spec) + assert.Equal(t, u.issues, len(p.Outcome()["default/p1"])) + }) + } +} + +func TestPodLint(t *testing.T) { + dba, err := test.NewTestDB() + assert.NoError(t, err) + l := db.NewLoader(dba) + + ctx := test.MakeCtx(t) + assert.NoError(t, test.LoadDB[*v1.Pod](ctx, l.DB, "core/pod/2.yaml", internal.Glossary[internal.PO])) + assert.NoError(t, test.LoadDB[*v1.ServiceAccount](ctx, l.DB, "core/sa/1.yaml", internal.Glossary[internal.SA])) + assert.NoError(t, test.LoadDB[*polv1.PodDisruptionBudget](ctx, l.DB, "pol/pdb/1.yaml", internal.Glossary[internal.PDB])) + assert.NoError(t, test.LoadDB[*netv1.NetworkPolicy](ctx, l.DB, "net/np/1.yaml", internal.Glossary[internal.NP])) + assert.NoError(t, test.LoadDB[*mv1beta1.PodMetrics](ctx, l.DB, "mx/pod/1.yaml", internal.Glossary[internal.PMX])) + + po := NewPod(test.MakeCollector(t), dba) + po.Collector.Config.Registries = []string{"dorker.io"} + assert.Nil(t, po.Lint(test.MakeContext("v1/pods", "pods"))) + assert.Equal(t, 5, len(po.Outcome())) + + ii := po.Outcome()["default/p1"] + assert.Equal(t, 0, len(ii)) + + ii = po.Outcome()["default/p2"] + assert.Equal(t, 6, len(ii)) + assert.Equal(t, `[POP-207] Pod is in an unhappy phase ()`, ii[0].Message) + assert.Equal(t, `[POP-208] Unmanaged pod detected. Best to use a controller`, ii[1].Message) + assert.Equal(t, `[POP-1204] Pod Ingress is not secured by a network policy`, ii[2].Message) + assert.Equal(t, `[POP-1204] Pod Egress is not secured by a network policy`, ii[3].Message) + assert.Equal(t, `[POP-206] Pod has no associated PodDisruptionBudget`, ii[4].Message) + assert.Equal(t, `[POP-301] Connects to API Server? ServiceAccount token is mounted`, ii[5].Message) + + ii = po.Outcome()["default/p3"] + assert.Equal(t, 6, len(ii)) + assert.Equal(t, `[POP-105] Liveness uses a port#, prefer a named port`, ii[0].Message) + assert.Equal(t, `[POP-105] Readiness uses a port#, prefer a named port`, ii[1].Message) + assert.Equal(t, `[POP-1204] Pod Ingress is not secured by a network policy`, ii[2].Message) + assert.Equal(t, `[POP-1204] Pod Egress is not secured by a network policy`, ii[3].Message) + assert.Equal(t, `[POP-301] Connects to API Server? ServiceAccount token is mounted`, ii[4].Message) + assert.Equal(t, `[POP-109] CPU Current/Request (2000m/1000m) reached user 80% threshold (200%)`, ii[5].Message) + + ii = po.Outcome()["default/p4"] + assert.Equal(t, 15, len(ii)) + assert.Equal(t, `[POP-204] Pod is not ready [0/1]`, ii[0].Message) + assert.Equal(t, `[POP-204] Pod is not ready [0/2]`, ii[1].Message) + assert.Equal(t, `[POP-100] Untagged docker image in use`, ii[2].Message) + assert.Equal(t, `[POP-113] Container image "zorg" is not hosted on an allowed docker registry`, ii[3].Message) + assert.Equal(t, `[POP-106] No resources requests/limits defined`, ii[4].Message) + assert.Equal(t, `[POP-100] Untagged docker image in use`, ii[5].Message) + assert.Equal(t, `[POP-113] Container image "blee" is not hosted on an allowed docker registry`, ii[6].Message) + assert.Equal(t, `[POP-101] Image tagged "latest" in use`, ii[7].Message) + assert.Equal(t, `[POP-113] Container image "zorg:latest" is not hosted on an allowed docker registry`, ii[8].Message) + assert.Equal(t, `[POP-107] No resource limits defined`, ii[9].Message) + assert.Equal(t, `[POP-208] Unmanaged pod detected. Best to use a controller`, ii[10].Message) + assert.Equal(t, `[POP-1204] Pod Ingress is not secured by a network policy`, ii[11].Message) + assert.Equal(t, `[POP-1204] Pod Egress is not secured by a network policy`, ii[12].Message) + assert.Equal(t, `[POP-300] Uses "default" ServiceAccount`, ii[13].Message) + assert.Equal(t, `[POP-301] Connects to API Server? ServiceAccount token is mounted`, ii[14].Message) + + ii = po.Outcome()["default/p5"] + assert.Equal(t, 7, len(ii)) + assert.Equal(t, `[POP-113] Container image "blee:v1.2" is not hosted on an allowed docker registry`, ii[0].Message) + assert.Equal(t, `[POP-106] No resources requests/limits defined`, ii[1].Message) + assert.Equal(t, `[POP-102] No probes defined`, ii[2].Message) + assert.Equal(t, `[POP-1204] Pod Ingress is not secured by a network policy`, ii[3].Message) + assert.Equal(t, `[POP-1204] Pod Egress is not secured by a network policy`, ii[4].Message) + assert.Equal(t, `[POP-209] Pod is managed by multiple PodDisruptionBudgets (pdb4, pdb4-1)`, ii[5].Message) + assert.Equal(t, `[POP-301] Connects to API Server? ServiceAccount token is mounted`, ii[6].Message) +} + +// ---------------------------------------------------------------------------- +// Helpers... + +type nonRootUser int + +const ( + secNonRootUndefined nonRootUser = iota - 1 + secNonRootUnset = 0 + secNonRootSet = 1 +) + +func makeSecCO(name string, level nonRootUser) v1.Container { + t, f := true, false + var secCtx v1.SecurityContext + switch level { + case secNonRootUnset: + secCtx.RunAsNonRoot = &f + case secNonRootSet: + secCtx.RunAsNonRoot = &t + default: + secCtx.RunAsNonRoot = nil + } + + return v1.Container{Name: name, SecurityContext: &secCtx} +} + +func makeSecPod(pod, init, co1, co2 nonRootUser) v1.Pod { + t, f := true, false + var zero int64 + var secCtx v1.PodSecurityContext + switch pod { + case secNonRootUnset: + secCtx.RunAsNonRoot = &f + case secNonRootSet: + secCtx.RunAsNonRoot = &t + default: + secCtx.RunAsNonRoot = nil + secCtx.RunAsUser = &zero + } + + return v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "p1", + }, + Spec: v1.PodSpec{ + ServiceAccountName: "default", + AutomountServiceAccountToken: &f, + InitContainers: []v1.Container{ + makeSecCO("ic1", init), + }, + Containers: []v1.Container{ + makeSecCO("c1", co1), + makeSecCO("c2", co2), + }, + SecurityContext: &secCtx, + }, + } +} diff --git a/internal/lint/pv.go b/internal/lint/pv.go new file mode 100644 index 00000000..e55d642f --- /dev/null +++ b/internal/lint/pv.go @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "context" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/client" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/issues" + v1 "k8s.io/api/core/v1" +) + +type ( + // PersistentVolume represents a PersistentVolume linter. + PersistentVolume struct { + *issues.Collector + db *db.DB + } +) + +// NewPersistentVolume returns a new instance. +func NewPersistentVolume(co *issues.Collector, db *db.DB) *PersistentVolume { + return &PersistentVolume{ + Collector: co, + db: db, + } +} + +// Lint cleanse the resource. +func (s *PersistentVolume) Lint(ctx context.Context) error { + txn, it := s.db.MustITFor(internal.Glossary[internal.PV]) + defer txn.Abort() + for o := it.Next(); o != nil; o = it.Next() { + pv := o.(*v1.PersistentVolume) + fqn := client.FQN(pv.Namespace, pv.Name) + s.InitOutcome(fqn) + ctx = internal.WithSpec(ctx, specFor(fqn, pv)) + + s.checkBound(ctx, pv.Status.Phase) + } + + return nil +} + +func (s *PersistentVolume) checkBound(ctx context.Context, phase v1.PersistentVolumePhase) { + switch phase { + case v1.VolumeAvailable: + s.AddCode(ctx, 1000) + case v1.VolumePending: + s.AddCode(ctx, 1001) + case v1.VolumeFailed: + s.AddCode(ctx, 1002) + } +} diff --git a/internal/lint/pv_test.go b/internal/lint/pv_test.go new file mode 100644 index 00000000..269eee4f --- /dev/null +++ b/internal/lint/pv_test.go @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "testing" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/rules" + "github.com/derailed/popeye/internal/test" + "github.com/stretchr/testify/assert" + v1 "k8s.io/api/core/v1" +) + +func TestPVLint(t *testing.T) { + dba, err := test.NewTestDB() + assert.NoError(t, err) + l := db.NewLoader(dba) + + ctx := test.MakeCtx(t) + assert.NoError(t, test.LoadDB[*v1.PersistentVolume](ctx, l.DB, "core/pv/1.yaml", internal.Glossary[internal.PV])) + assert.NoError(t, test.LoadDB[*v1.Pod](ctx, l.DB, "core/pod/1.yaml", internal.Glossary[internal.PO])) + + pv := NewPersistentVolume(test.MakeCollector(t), dba) + assert.Nil(t, pv.Lint(test.MakeContext("v1/persistentvolumes", "persistentvolumes"))) + assert.Equal(t, 4, len(pv.Outcome())) + + ii := pv.Outcome()["default/pv1"] + assert.Equal(t, 0, len(ii)) + + ii = pv.Outcome()["default/pv2"] + assert.Equal(t, 1, len(ii)) + assert.Equal(t, `[POP-1002] Lost volume detected`, ii[0].Message) + assert.Equal(t, rules.ErrorLevel, ii[0].Level) + + ii = pv.Outcome()["default/pv3"] + assert.Equal(t, 1, len(ii)) + assert.Equal(t, `[POP-1000] Available volume detected`, ii[0].Message) + assert.Equal(t, rules.InfoLevel, ii[0].Level) + + ii = pv.Outcome()["default/pv4"] + assert.Equal(t, 1, len(ii)) + assert.Equal(t, `[POP-1001] Pending volume detected`, ii[0].Message) + assert.Equal(t, rules.WarnLevel, ii[0].Level) +} diff --git a/internal/lint/pvc.go b/internal/lint/pvc.go new file mode 100644 index 00000000..3b37c790 --- /dev/null +++ b/internal/lint/pvc.go @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "context" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/cache" + "github.com/derailed/popeye/internal/client" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/issues" + v1 "k8s.io/api/core/v1" +) + +type ( + // PersistentVolumeClaim represents a PersistentVolumeClaim linter. + PersistentVolumeClaim struct { + *issues.Collector + db *db.DB + } +) + +// NewPersistentVolumeClaim returns a new instance. +func NewPersistentVolumeClaim(co *issues.Collector, db *db.DB) *PersistentVolumeClaim { + return &PersistentVolumeClaim{ + Collector: co, + db: db, + } +} + +// Lint cleanse the resource. +func (s *PersistentVolumeClaim) Lint(ctx context.Context) error { + refs := make(map[string]struct{}) + txn, it := s.db.MustITFor(internal.Glossary[internal.PO]) + defer txn.Abort() + for o := it.Next(); o != nil; o = it.Next() { + pod := o.(*v1.Pod) + for _, v := range pod.Spec.Volumes { + if v.VolumeSource.PersistentVolumeClaim == nil { + continue + } + refs[cache.FQN(pod.Namespace, v.VolumeSource.PersistentVolumeClaim.ClaimName)] = struct{}{} + } + } + + txn, it = s.db.MustITFor(internal.Glossary[internal.PVC]) + defer txn.Abort() + for o := it.Next(); o != nil; o = it.Next() { + pvc := o.(*v1.PersistentVolumeClaim) + fqn := client.FQN(pvc.Namespace, pvc.Name) + s.InitOutcome(fqn) + ctx = internal.WithSpec(ctx, specFor(fqn, pvc)) + + s.checkBound(ctx, pvc.Status.Phase) + if _, ok := refs[fqn]; !ok { + s.AddCode(ctx, 400) + } + } + + return nil +} + +func (s *PersistentVolumeClaim) checkBound(ctx context.Context, phase v1.PersistentVolumeClaimPhase) { + switch phase { + case v1.ClaimPending: + s.AddCode(ctx, 1003) + case v1.ClaimLost: + s.AddCode(ctx, 1004) + } +} diff --git a/internal/lint/pvc_test.go b/internal/lint/pvc_test.go new file mode 100644 index 00000000..fd63677d --- /dev/null +++ b/internal/lint/pvc_test.go @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "testing" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/rules" + "github.com/derailed/popeye/internal/test" + "github.com/stretchr/testify/assert" + v1 "k8s.io/api/core/v1" +) + +func TestPVCLint(t *testing.T) { + dba, err := test.NewTestDB() + assert.NoError(t, err) + l := db.NewLoader(dba) + + ctx := test.MakeCtx(t) + assert.NoError(t, test.LoadDB[*v1.PersistentVolumeClaim](ctx, l.DB, "core/pvc/1.yaml", internal.Glossary[internal.PVC])) + assert.NoError(t, test.LoadDB[*v1.Pod](ctx, l.DB, "core/pod/1.yaml", internal.Glossary[internal.PO])) + + pvc := NewPersistentVolumeClaim(test.MakeCollector(t), dba) + assert.Nil(t, pvc.Lint(test.MakeContext("v1/persistentvolumeclaims", "persistentvolumeclaims"))) + assert.Equal(t, 3, len(pvc.Outcome())) + + ii := pvc.Outcome()["default/pvc1"] + assert.Equal(t, 0, len(ii)) + + ii = pvc.Outcome()["default/pvc2"] + assert.Equal(t, 2, len(ii)) + assert.Equal(t, `[POP-1004] Lost claim detected`, ii[0].Message) + assert.Equal(t, rules.ErrorLevel, ii[0].Level) + assert.Equal(t, `[POP-400] Used? Unable to locate resource reference`, ii[1].Message) + assert.Equal(t, rules.InfoLevel, ii[1].Level) + + ii = pvc.Outcome()["default/pvc3"] + assert.Equal(t, 2, len(ii)) + assert.Equal(t, `[POP-1003] Pending claim detected`, ii[0].Message) + assert.Equal(t, rules.ErrorLevel, ii[0].Level) + assert.Equal(t, `[POP-400] Used? Unable to locate resource reference`, ii[1].Message) + assert.Equal(t, rules.InfoLevel, ii[1].Level) +} diff --git a/internal/lint/rb.go b/internal/lint/rb.go new file mode 100644 index 00000000..ed09f40f --- /dev/null +++ b/internal/lint/rb.go @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "context" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/cache" + "github.com/derailed/popeye/internal/client" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/issues" + rbacv1 "k8s.io/api/rbac/v1" +) + +type ( + // RoleBinding tracks RoleBinding sanitization. + RoleBinding struct { + *issues.Collector + + db *db.DB + } +) + +// NewRoleBinding returns a new instance. +func NewRoleBinding(c *issues.Collector, db *db.DB) *RoleBinding { + return &RoleBinding{ + Collector: c, + db: db, + } +} + +// Lint cleanse the resource.. +func (r *RoleBinding) Lint(ctx context.Context) error { + r.checkInUse(ctx) + + return nil +} + +func (r *RoleBinding) checkInUse(ctx context.Context) { + txn, it := r.db.MustITFor(internal.Glossary[internal.ROB]) + defer txn.Abort() + for o := it.Next(); o != nil; o = it.Next() { + rb := o.(*rbacv1.RoleBinding) + fqn := client.FQN(rb.Namespace, rb.Name) + r.InitOutcome(fqn) + ctx = internal.WithSpec(ctx, specFor(fqn, rb)) + + switch rb.RoleRef.Kind { + case "ClusterRole": + if !r.db.Exists(internal.Glossary[internal.CR], rb.RoleRef.Name) { + r.AddCode(ctx, 1300, rb.RoleRef.Kind, rb.RoleRef.Name) + } + case "Role": + rFQN := cache.FQN(rb.Namespace, rb.RoleRef.Name) + if !r.db.Exists(internal.Glossary[internal.RO], rFQN) { + r.AddCode(ctx, 1300, rb.RoleRef.Kind, rFQN) + } + } + } +} diff --git a/internal/lint/rb_test.go b/internal/lint/rb_test.go new file mode 100644 index 00000000..086a405b --- /dev/null +++ b/internal/lint/rb_test.go @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "testing" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/rules" + "github.com/derailed/popeye/internal/test" + "github.com/stretchr/testify/assert" + v1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" +) + +func TestRBLint(t *testing.T) { + dba, err := test.NewTestDB() + assert.NoError(t, err) + l := db.NewLoader(dba) + + ctx := test.MakeCtx(t) + assert.NoError(t, test.LoadDB[*rbacv1.RoleBinding](ctx, l.DB, "auth/rob/1.yaml", internal.Glossary[internal.ROB])) + assert.NoError(t, test.LoadDB[*rbacv1.Role](ctx, l.DB, "auth/ro/1.yaml", internal.Glossary[internal.RO])) + assert.NoError(t, test.LoadDB[*rbacv1.ClusterRole](ctx, l.DB, "auth/cr/1.yaml", internal.Glossary[internal.CR])) + assert.NoError(t, test.LoadDB[*rbacv1.ClusterRoleBinding](ctx, l.DB, "auth/crb/1.yaml", internal.Glossary[internal.CRB])) + assert.NoError(t, test.LoadDB[*v1.ServiceAccount](ctx, l.DB, "core/sa/1.yaml", internal.Glossary[internal.SA])) + + rb := NewRoleBinding(test.MakeCollector(t), dba) + assert.Nil(t, rb.Lint(test.MakeContext("rbac.authorization.k8s.io/v1/rolebindings", "rolebindings"))) + assert.Equal(t, 3, len(rb.Outcome())) + + ii := rb.Outcome()["default/rb1"] + assert.Equal(t, 0, len(ii)) + + ii = rb.Outcome()["default/rb2"] + assert.Equal(t, 1, len(ii)) + assert.Equal(t, `[POP-1300] References a Role (default/r-bozo) which does not exist`, ii[0].Message) + assert.Equal(t, rules.WarnLevel, ii[0].Level) + + ii = rb.Outcome()["default/rb3"] + assert.Equal(t, 1, len(ii)) + assert.Equal(t, `[POP-1300] References a ClusterRole (cr-bozo) which does not exist`, ii[0].Message) + assert.Equal(t, rules.WarnLevel, ii[0].Level) +} diff --git a/internal/lint/ro.go b/internal/lint/ro.go new file mode 100644 index 00000000..ec604497 --- /dev/null +++ b/internal/lint/ro.go @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "context" + "sync" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/cache" + "github.com/derailed/popeye/internal/client" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/issues" + rbacv1 "k8s.io/api/rbac/v1" +) + +type ( + // Role tracks Role sanitization. + Role struct { + *issues.Collector + + db *db.DB + } +) + +// NewRole returns a new instance. +func NewRole(c *issues.Collector, db *db.DB) *Role { + return &Role{ + Collector: c, + db: db, + } +} + +// Lint cleanse the resource. +func (s *Role) Lint(ctx context.Context) error { + var roRefs sync.Map + crb := cache.NewClusterRoleBinding(s.db) + crb.ClusterRoleRefs(&roRefs) + rb := cache.NewRoleBinding(s.db) + rb.RoleRefs(&roRefs) + s.checkInUse(ctx, &roRefs) + + return nil +} + +func (s *Role) checkInUse(ctx context.Context, refs *sync.Map) { + txn, it := s.db.MustITFor(internal.Glossary[internal.RO]) + defer txn.Abort() + for o := it.Next(); o != nil; o = it.Next() { + ro := o.(*rbacv1.Role) + fqn := client.FQN(ro.Namespace, ro.Name) + s.InitOutcome(fqn) + ctx = internal.WithSpec(ctx, specFor(fqn, ro)) + + _, ok := refs.Load(cache.ResFqn(cache.RoleKey, fqn)) + if !ok { + s.AddCode(ctx, 400) + } + } +} diff --git a/internal/lint/ro_test.go b/internal/lint/ro_test.go new file mode 100644 index 00000000..4d36ce76 --- /dev/null +++ b/internal/lint/ro_test.go @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "testing" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/rules" + "github.com/derailed/popeye/internal/test" + "github.com/stretchr/testify/assert" + rbacv1 "k8s.io/api/rbac/v1" +) + +func TestROLint(t *testing.T) { + dba, err := test.NewTestDB() + assert.NoError(t, err) + l := db.NewLoader(dba) + + ctx := test.MakeCtx(t) + assert.NoError(t, test.LoadDB[*rbacv1.Role](ctx, l.DB, "auth/ro/1.yaml", internal.Glossary[internal.RO])) + assert.NoError(t, test.LoadDB[*rbacv1.RoleBinding](ctx, l.DB, "auth/rob/1.yaml", internal.Glossary[internal.ROB])) + assert.NoError(t, test.LoadDB[*rbacv1.ClusterRoleBinding](ctx, l.DB, "auth/crb/1.yaml", internal.Glossary[internal.CRB])) + + ro := NewRole(test.MakeCollector(t), dba) + assert.Nil(t, ro.Lint(test.MakeContext("rbac.authorization.k8s.io/v1/roles", "roles"))) + assert.Equal(t, 3, len(ro.Outcome())) + + ii := ro.Outcome()["default/r1"] + assert.Equal(t, 0, len(ii)) + + ii = ro.Outcome()["default/r2"] + assert.Equal(t, 1, len(ii)) + assert.Equal(t, `[POP-400] Used? Unable to locate resource reference`, ii[0].Message) + assert.Equal(t, rules.InfoLevel, ii[0].Level) + + ii = ro.Outcome()["default/r3"] + assert.Equal(t, 1, len(ii)) + assert.Equal(t, `[POP-400] Used? Unable to locate resource reference`, ii[0].Message) + assert.Equal(t, rules.InfoLevel, ii[0].Level) +} diff --git a/internal/lint/rs.go b/internal/lint/rs.go new file mode 100644 index 00000000..5dbfebc3 --- /dev/null +++ b/internal/lint/rs.go @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "context" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/client" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/issues" + appsv1 "k8s.io/api/apps/v1" +) + +// ReplicaSet tracks ReplicaSet sanitization. +type ReplicaSet struct { + *issues.Collector + + db *db.DB +} + +// NewReplicaSet returns a new instance. +func NewReplicaSet(co *issues.Collector, db *db.DB) *ReplicaSet { + return &ReplicaSet{ + Collector: co, + db: db, + } +} + +// Lint cleanse the resource. +func (s *ReplicaSet) Lint(ctx context.Context) error { + txn, it := s.db.MustITFor(internal.Glossary[internal.RS]) + defer txn.Abort() + for o := it.Next(); o != nil; o = it.Next() { + rs := o.(*appsv1.ReplicaSet) + fqn := client.FQN(rs.Namespace, rs.Name) + s.InitOutcome(fqn) + ctx = internal.WithSpec(ctx, specFor(fqn, rs)) + + s.checkHealth(ctx, rs) + } + + return nil +} + +func (s *ReplicaSet) checkHealth(ctx context.Context, rs *appsv1.ReplicaSet) { + if rs.Spec.Replicas != nil && *rs.Spec.Replicas != rs.Status.ReadyReplicas { + s.AddCode(ctx, 1120, *rs.Spec.Replicas, rs.Status.ReadyReplicas) + } +} diff --git a/internal/lint/rs_test.go b/internal/lint/rs_test.go new file mode 100644 index 00000000..75e4d62c --- /dev/null +++ b/internal/lint/rs_test.go @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "testing" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/rules" + "github.com/derailed/popeye/internal/test" + "github.com/stretchr/testify/assert" + appsv1 "k8s.io/api/apps/v1" + v1 "k8s.io/api/core/v1" +) + +func TestRSLint(t *testing.T) { + dba, err := test.NewTestDB() + assert.NoError(t, err) + l := db.NewLoader(dba) + + ctx := test.MakeCtx(t) + assert.NoError(t, test.LoadDB[*appsv1.ReplicaSet](ctx, l.DB, "apps/rs/1.yaml", internal.Glossary[internal.RS])) + assert.NoError(t, test.LoadDB[*v1.Pod](ctx, l.DB, "core/pod/1.yaml", internal.Glossary[internal.PO])) + + rs := NewReplicaSet(test.MakeCollector(t), dba) + assert.Nil(t, rs.Lint(test.MakeContext("apps/v1/replicasets", "replicasets"))) + assert.Equal(t, 2, len(rs.Outcome())) + + ii := rs.Outcome()["default/rs1"] + assert.Equal(t, 0, len(ii)) + + ii = rs.Outcome()["default/rs2"] + assert.Equal(t, 1, len(ii)) + assert.Equal(t, `[POP-1120] Unhealthy ReplicaSet 2 desired but have 0 ready`, ii[0].Message) + assert.Equal(t, rules.ErrorLevel, ii[0].Level) +} diff --git a/internal/lint/sa.go b/internal/lint/sa.go new file mode 100644 index 00000000..d44440e0 --- /dev/null +++ b/internal/lint/sa.go @@ -0,0 +1,160 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "context" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/cache" + "github.com/derailed/popeye/internal/client" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/issues" + + v1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" +) + +const defaultSA = "default" + +// ServiceAccount tracks ServiceAccount linter. +type ServiceAccount struct { + *issues.Collector + db *db.DB +} + +// NewServiceAccount returns a new instance. +func NewServiceAccount(co *issues.Collector, db *db.DB) *ServiceAccount { + return &ServiceAccount{ + Collector: co, + db: db, + } +} + +// Lint cleanse the resource. +func (s *ServiceAccount) Lint(ctx context.Context) error { + refs := make(map[string]struct{}, 20) + if err := s.crbRefs(refs); err != nil { + return err + } + if err := s.rbRefs(refs); err != nil { + return err + } + err := s.podRefs(refs) + if err != nil { + return err + } + + txn, it := s.db.MustITFor(internal.Glossary[internal.SA]) + defer txn.Abort() + for o := it.Next(); o != nil; o = it.Next() { + sa := o.(*v1.ServiceAccount) + fqn := client.FQN(sa.Namespace, sa.Name) + s.InitOutcome(fqn) + ctx = internal.WithSpec(ctx, specFor(fqn, sa)) + + s.checkMounts(ctx, sa.AutomountServiceAccountToken) + s.checkSecretRefs(ctx, fqn, sa.Secrets) + s.checkPullSecretRefs(ctx, fqn, sa.ImagePullSecrets) + if _, ok := refs[fqn]; !ok && sa.Name != defaultSA { + s.AddCode(ctx, 400) + } + } + + return nil +} + +func (s *ServiceAccount) checkSecretRefs(ctx context.Context, fqn string, refs []v1.ObjectReference) { + ns, _ := namespaced(fqn) + for _, ref := range refs { + if ref.Namespace != "" { + ns = ref.Namespace + } + sfqn := cache.FQN(ns, ref.Name) + if !s.db.Exists(internal.Glossary[internal.SEC], sfqn) { + s.AddCode(ctx, 304, sfqn) + } + } +} + +func (s *ServiceAccount) checkPullSecretRefs(ctx context.Context, fqn string, refs []v1.LocalObjectReference) { + ns, _ := namespaced(fqn) + for _, ref := range refs { + sfqn := cache.FQN(ns, ref.Name) + if !s.db.Exists(internal.Glossary[internal.SEC], sfqn) { + s.AddCode(ctx, 305, sfqn) + } + } +} + +func (s *ServiceAccount) checkMounts(ctx context.Context, b *bool) { + if b != nil && *b { + s.AddCode(ctx, 303) + } +} + +func (s *ServiceAccount) crbRefs(refs map[string]struct{}) error { + txn := s.db.Txn(false) + defer txn.Abort() + it, err := txn.Get(internal.Glossary[internal.CRB].String(), "id") + if err != nil { + return err + } + for o := it.Next(); o != nil; o = it.Next() { + crb := o.(*rbacv1.ClusterRoleBinding) + pullSas(crb.Subjects, refs) + } + + return nil +} + +func (s *ServiceAccount) rbRefs(refs map[string]struct{}) error { + txn := s.db.Txn(false) + defer txn.Abort() + it, err := txn.Get(internal.Glossary[internal.ROB].String(), "id") + if err != nil { + return err + } + for o := it.Next(); o != nil; o = it.Next() { + rb := o.(*rbacv1.RoleBinding) + pullSas(rb.Subjects, refs) + } + + return nil +} + +func (s *ServiceAccount) podRefs(refs map[string]struct{}) error { + txn := s.db.Txn(false) + defer txn.Abort() + it, err := txn.Get(internal.Glossary[internal.PO].String(), "id") + if err != nil { + return err + } + for o := it.Next(); o != nil; o = it.Next() { + p := o.(*v1.Pod) + if p.Spec.ServiceAccountName != "" { + refs[cache.FQN(p.Namespace, p.Spec.ServiceAccountName)] = struct{}{} + } + } + + return nil +} + +// ---------------------------------------------------------------------------- +// Helpers... + +func pullSas(ss []rbacv1.Subject, res map[string]struct{}) { + for _, s := range ss { + if s.Kind == "ServiceAccount" { + fqn := fqnSubject(s) + if _, ok := res[fqn]; !ok { + res[fqn] = struct{}{} + } + } + } +} + +func fqnSubject(s rbacv1.Subject) string { + return cache.FQN(s.Namespace, s.Name) +} diff --git a/internal/lint/sa_test.go b/internal/lint/sa_test.go new file mode 100644 index 00000000..2237f21c --- /dev/null +++ b/internal/lint/sa_test.go @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "testing" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/rules" + "github.com/derailed/popeye/internal/test" + "github.com/stretchr/testify/assert" + v1 "k8s.io/api/core/v1" + netv1 "k8s.io/api/networking/v1" + rbacv1 "k8s.io/api/rbac/v1" +) + +func TestSALint(t *testing.T) { + dba, err := test.NewTestDB() + assert.NoError(t, err) + l := db.NewLoader(dba) + + ctx := test.MakeCtx(t) + assert.NoError(t, test.LoadDB[*v1.ServiceAccount](ctx, l.DB, "core/sa/1.yaml", internal.Glossary[internal.SA])) + assert.NoError(t, test.LoadDB[*v1.Pod](ctx, l.DB, "core/pod/2.yaml", internal.Glossary[internal.PO])) + assert.NoError(t, test.LoadDB[*rbacv1.RoleBinding](ctx, l.DB, "auth/rob/1.yaml", internal.Glossary[internal.ROB])) + assert.NoError(t, test.LoadDB[*rbacv1.ClusterRoleBinding](ctx, l.DB, "auth/crb/1.yaml", internal.Glossary[internal.CRB])) + assert.NoError(t, test.LoadDB[*v1.Secret](ctx, l.DB, "core/secret/1.yaml", internal.Glossary[internal.SEC])) + assert.NoError(t, test.LoadDB[*v1.Service](ctx, l.DB, "core/svc/1.yaml", internal.Glossary[internal.SVC])) + assert.NoError(t, test.LoadDB[*netv1.Ingress](ctx, l.DB, "net/ingress/1.yaml", internal.Glossary[internal.ING])) + + sa := NewServiceAccount(test.MakeCollector(t), dba) + assert.Nil(t, sa.Lint(test.MakeContext("v1/serviceaccounts", "serviceaccounts"))) + assert.Equal(t, 6, len(sa.Outcome())) + + ii := sa.Outcome()["default/default"] + assert.Equal(t, 0, len(ii)) + + ii = sa.Outcome()["default/sa1"] + assert.Equal(t, 0, len(ii)) + + ii = sa.Outcome()["default/sa2"] + assert.Equal(t, 1, len(ii)) + assert.Equal(t, `[POP-303] Do you mean it? ServiceAccount is automounting APIServer credentials`, ii[0].Message) + assert.Equal(t, rules.WarnLevel, ii[0].Level) + + ii = sa.Outcome()["default/sa3"] + assert.Equal(t, 1, len(ii)) + assert.Equal(t, `[POP-303] Do you mean it? ServiceAccount is automounting APIServer credentials`, ii[0].Message) + assert.Equal(t, rules.WarnLevel, ii[0].Level) + + ii = sa.Outcome()["default/sa4"] + assert.Equal(t, 3, len(ii)) + assert.Equal(t, `[POP-304] References a secret "default/bozo" which does not exist`, ii[0].Message) + assert.Equal(t, rules.ErrorLevel, ii[0].Level) + assert.Equal(t, `[POP-305] References a pull secret which does not exist: default/s1`, ii[1].Message) + assert.Equal(t, rules.ErrorLevel, ii[1].Level) + assert.Equal(t, `[POP-400] Used? Unable to locate resource reference`, ii[2].Message) + assert.Equal(t, rules.InfoLevel, ii[2].Level) + + ii = sa.Outcome()["default/sa5"] + assert.Equal(t, 2, len(ii)) + assert.Equal(t, `[POP-304] References a secret "default/s1" which does not exist`, ii[0].Message) + assert.Equal(t, rules.ErrorLevel, ii[0].Level) + assert.Equal(t, `[POP-305] References a pull secret which does not exist: default/bozo`, ii[1].Message) + assert.Equal(t, rules.ErrorLevel, ii[1].Level) + +} diff --git a/internal/lint/sec.go b/internal/lint/sec.go new file mode 100644 index 00000000..24f86103 --- /dev/null +++ b/internal/lint/sec.go @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "context" + "sync" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/cache" + "github.com/derailed/popeye/internal/client" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/issues" + v1 "k8s.io/api/core/v1" +) + +// Secret tracks Secret sanitization. +type Secret struct { + *issues.Collector + + db *db.DB + system excludedFQN +} + +// NewSecret returns a new instance. +func NewSecret(co *issues.Collector, db *db.DB) *Secret { + return &Secret{ + Collector: co, + db: db, + system: excludedFQN{ + "rx:default-token": {}, + "rx:^kube-.*/.*-token-": {}, + "rx:^local-path-storage/.*token-": {}, + }, + } +} + +// Lint cleanse the resource. +func (s *Secret) Lint(ctx context.Context) error { + var refs sync.Map + + if err := cache.NewPod(s.db).PodRefs(&refs); err != nil { + s.AddErr(ctx, err) + } + if err := cache.NewServiceAccount(s.db).ServiceAccountRefs(&refs); err != nil { + s.AddErr(ctx, err) + } + if err := cache.NewIngress(s.db).IngressRefs(&refs); err != nil { + s.AddErr(ctx, err) + } + s.checkStale(ctx, &refs) + + return nil +} + +func (s *Secret) checkStale(ctx context.Context, refs *sync.Map) { + txn, it := s.db.MustITFor(internal.Glossary[internal.SEC]) + defer txn.Abort() + for o := it.Next(); o != nil; o = it.Next() { + sec := o.(*v1.Secret) + fqn := client.FQN(sec.Namespace, sec.Name) + s.InitOutcome(fqn) + ctx = internal.WithSpec(ctx, specFor(fqn, sec)) + + if s.system.skip(fqn) { + continue + } + refs.Range(func(k, v interface{}) bool { + return true + }) + + keys, ok := refs.Load(cache.ResFqn(cache.SecretKey, fqn)) + if !ok { + s.AddCode(ctx, 400) + continue + } + if keys.(internal.StringSet).Has(internal.All) { + continue + } + + kk := make(internal.StringSet, len(sec.Data)) + for k := range sec.Data { + kk.Add(k) + } + deltas := keys.(internal.StringSet).Diff(kk) + for k := range deltas { + s.AddCode(ctx, 401, k) + } + } +} diff --git a/internal/lint/sec_test.go b/internal/lint/sec_test.go new file mode 100644 index 00000000..9c0556c9 --- /dev/null +++ b/internal/lint/sec_test.go @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "testing" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/rules" + "github.com/derailed/popeye/internal/test" + "github.com/stretchr/testify/assert" + v1 "k8s.io/api/core/v1" + netv1 "k8s.io/api/networking/v1" +) + +func TestSecretLint(t *testing.T) { + dba, err := test.NewTestDB() + assert.NoError(t, err) + l := db.NewLoader(dba) + + ctx := test.MakeCtx(t) + assert.NoError(t, test.LoadDB[*v1.Secret](ctx, l.DB, "core/secret/1.yaml", internal.Glossary[internal.SEC])) + assert.NoError(t, test.LoadDB[*v1.Pod](ctx, l.DB, "core/pod/1.yaml", internal.Glossary[internal.PO])) + assert.NoError(t, test.LoadDB[*v1.ServiceAccount](ctx, l.DB, "core/sa/1.yaml", internal.Glossary[internal.SA])) + assert.NoError(t, test.LoadDB[*netv1.Ingress](ctx, l.DB, "net/ingress/1.yaml", internal.Glossary[internal.ING])) + + sec := NewSecret(test.MakeCollector(t), dba) + assert.Nil(t, sec.Lint(test.MakeContext("v1/secrets", "secrets"))) + assert.Equal(t, 3, len(sec.Outcome())) + + ii := sec.Outcome()["default/sec1"] + assert.Equal(t, 1, len(ii)) + assert.Equal(t, `[POP-401] Key "ns" used? Unable to locate key reference`, ii[0].Message) + assert.Equal(t, rules.InfoLevel, ii[0].Level) + + ii = sec.Outcome()["default/sec2"] + assert.Equal(t, 1, len(ii)) + assert.Equal(t, `[POP-400] Used? Unable to locate resource reference`, ii[0].Message) + assert.Equal(t, rules.InfoLevel, ii[0].Level) + + ii = sec.Outcome()["default/sec3"] + assert.Equal(t, 1, len(ii)) + assert.Equal(t, `[POP-400] Used? Unable to locate resource reference`, ii[0].Message) + assert.Equal(t, rules.InfoLevel, ii[0].Level) + +} diff --git a/internal/sanitize/sts.go b/internal/lint/sts.go similarity index 51% rename from internal/sanitize/sts.go rename to internal/lint/sts.go index 85f53398..0dcfb413 100644 --- a/internal/sanitize/sts.go +++ b/internal/lint/sts.go @@ -1,16 +1,16 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright Authors of Popeye -package sanitize +package lint import ( "context" "github.com/derailed/popeye/internal" "github.com/derailed/popeye/internal/client" + "github.com/derailed/popeye/internal/db" "github.com/derailed/popeye/internal/issues" appsv1 "k8s.io/api/apps/v1" - v1 "k8s.io/api/core/v1" ) type ( @@ -23,70 +23,41 @@ type ( ConfigLister } - // StatefulSetLister handles statefulsets. - StatefulSetLister interface { - PodLimiter - ConfigLister - PodSelectorLister - PodsMetricsLister - - ListStatefulSets() map[string]*appsv1.StatefulSet - ListServiceAccounts() map[string]*v1.ServiceAccount - } - - // StatefulSet represents a StatefulSet sanitizer. + // StatefulSet represents a StatefulSet linter. StatefulSet struct { *issues.Collector - StatefulSetLister + + db *db.DB } ) -// NewStatefulSet returns a new sanitizer. -func NewStatefulSet(co *issues.Collector, lister StatefulSetLister) *StatefulSet { +// NewStatefulSet returns a new instance. +func NewStatefulSet(co *issues.Collector, db *db.DB) *StatefulSet { return &StatefulSet{ - Collector: co, - StatefulSetLister: lister, + Collector: co, + db: db, } } -// Sanitize cleanse the resource. -func (s *StatefulSet) Sanitize(ctx context.Context) error { - pmx := client.PodsMetrics{} - podsMetrics(s, pmx) - +// Lint cleanse the resource. +func (s *StatefulSet) Lint(ctx context.Context) error { over := pullOverAllocs(ctx) - for fqn, st := range s.ListStatefulSets() { + txn, it := s.db.MustITFor(internal.Glossary[internal.STS]) + defer txn.Abort() + for o := it.Next(); o != nil; o = it.Next() { + sts := o.(*appsv1.StatefulSet) + fqn := client.FQN(sts.Namespace, sts.Name) s.InitOutcome(fqn) - ctx = internal.WithFQN(ctx, fqn) - - s.checkDeprecation(ctx, st) - s.checkStatefulSet(ctx, st) - s.checkContainers(ctx, st) - s.checkUtilization(ctx, over, st, pmx) + ctx = internal.WithSpec(ctx, specFor(fqn, sts)) - if s.NoConcerns(fqn) && s.Config.ExcludeFQN(internal.MustExtractSectionGVR(ctx), fqn) { - s.ClearOutcome(fqn) - } + s.checkStatefulSet(ctx, sts) + s.checkContainers(ctx, fqn, sts) + s.checkUtilization(ctx, over, sts) } return nil } -func (s *StatefulSet) checkDeprecation(ctx context.Context, st *appsv1.StatefulSet) { - const current = "apps/v1" - - rev, err := resourceRev(internal.MustExtractFQN(ctx), "StatefulSet", st.Annotations) - if err != nil { - if rev = revFromLink(st.SelfLink); rev == "" { - return - } - } - - if rev != current { - s.AddCode(ctx, 403, "StatefulSet", rev, current) - } -} - func (s *StatefulSet) checkStatefulSet(ctx context.Context, sts *appsv1.StatefulSet) { if sts.Spec.Replicas == nil || (sts.Spec.Replicas != nil && *sts.Spec.Replicas == 0) { s.AddCode(ctx, 500) @@ -101,16 +72,16 @@ func (s *StatefulSet) checkStatefulSet(ctx context.Context, sts *appsv1.Stateful return } - if _, ok := s.ListServiceAccounts()[client.FQN(sts.Namespace, sts.Spec.Template.Spec.ServiceAccountName)]; !ok { + saFQN := client.FQN(sts.Namespace, sts.Spec.Template.Spec.ServiceAccountName) + if !s.db.Exists(internal.Glossary[internal.SA], saFQN) { s.AddCode(ctx, 507, sts.Spec.Template.Spec.ServiceAccountName) } - } -func (s *StatefulSet) checkContainers(ctx context.Context, st *appsv1.StatefulSet) { +func (s *StatefulSet) checkContainers(ctx context.Context, fqn string, st *appsv1.StatefulSet) { spec := st.Spec.Template.Spec - l := NewContainer(internal.MustExtractFQN(ctx), s) + l := NewContainer(fqn, s) for _, co := range spec.InitContainers { l.sanitize(ctx, co, false) } @@ -144,8 +115,8 @@ func checkMEM(ctx context.Context, c CollectorLimiter, over bool, mx Consumption } } -func (s *StatefulSet) checkUtilization(ctx context.Context, over bool, st *appsv1.StatefulSet, pmx client.PodsMetrics) { - mx := s.statefulsetUsage(st, pmx) +func (s *StatefulSet) checkUtilization(ctx context.Context, over bool, sts *appsv1.StatefulSet) { + mx := resourceUsage(ctx, s.db, s, sts.Namespace, sts.Spec.Selector) if mx.RequestCPU.IsZero() && mx.RequestMEM.IsZero() { return } @@ -153,24 +124,3 @@ func (s *StatefulSet) checkUtilization(ctx context.Context, over bool, st *appsv checkCPU(ctx, s, over, mx) checkMEM(ctx, s, over, mx) } - -func (s *StatefulSet) statefulsetUsage(st *appsv1.StatefulSet, pmx client.PodsMetrics) ConsumptionMetrics { - var mx ConsumptionMetrics - for pfqn, pod := range s.ListPodsBySelector(st.Namespace, st.Spec.Selector) { - cpu, mem := computePodResources(pod.Spec) - mx.QOS = pod.Status.QOSClass - mx.RequestCPU.Add(cpu) - mx.RequestMEM.Add(mem) - - ccx, ok := pmx[pfqn] - if !ok { - continue - } - for _, cx := range ccx { - mx.CurrentCPU.Add(cx.CurrentCPU) - mx.CurrentMEM.Add(cx.CurrentMEM) - } - } - - return mx -} diff --git a/internal/lint/sts_test.go b/internal/lint/sts_test.go new file mode 100644 index 00000000..0d76d6fe --- /dev/null +++ b/internal/lint/sts_test.go @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "testing" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/rules" + "github.com/derailed/popeye/internal/test" + "github.com/stretchr/testify/assert" + appsv1 "k8s.io/api/apps/v1" + v1 "k8s.io/api/core/v1" + mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" +) + +func TestSTSLint(t *testing.T) { + dba, err := test.NewTestDB() + assert.NoError(t, err) + l := db.NewLoader(dba) + + ctx := test.MakeCtx(t) + assert.NoError(t, test.LoadDB[*appsv1.StatefulSet](ctx, l.DB, "apps/sts/1.yaml", internal.Glossary[internal.STS])) + assert.NoError(t, test.LoadDB[*v1.ServiceAccount](ctx, l.DB, "core/sa/1.yaml", internal.Glossary[internal.SA])) + assert.NoError(t, test.LoadDB[*v1.Pod](ctx, l.DB, "core/pod/1.yaml", internal.Glossary[internal.PO])) + assert.NoError(t, test.LoadDB[*mv1beta1.PodMetrics](ctx, l.DB, "mx/pod/1.yaml", internal.Glossary[internal.PMX])) + + sts := NewStatefulSet(test.MakeCollector(t), dba) + assert.Nil(t, sts.Lint(test.MakeContext("apps/v1/statefulsets", "statefulsets"))) + assert.Equal(t, 3, len(sts.Outcome())) + + ii := sts.Outcome()["default/sts1"] + assert.Equal(t, 2, len(ii)) + assert.Equal(t, `[POP-503] At current load, CPU under allocated. Current:20000m vs Requested:1000m (2000.00%)`, ii[0].Message) + assert.Equal(t, `[POP-505] At current load, Memory under allocated. Current:20Mi vs Requested:1Mi (2000.00%)`, ii[1].Message) + + ii = sts.Outcome()["default/sts2"] + assert.Equal(t, 2, len(ii)) + assert.Equal(t, `[POP-100] Untagged docker image in use`, ii[0].Message) + assert.Equal(t, rules.ErrorLevel, ii[0].Level) + assert.Equal(t, `[POP-508] No pods match controller selector: app=p2`, ii[1].Message) + assert.Equal(t, rules.ErrorLevel, ii[1].Level) + + ii = sts.Outcome()["default/sts3"] + assert.Equal(t, 4, len(ii)) + assert.Equal(t, `[POP-501] Unhealthy 1 desired but have 0 available`, ii[0].Message) + assert.Equal(t, rules.ErrorLevel, ii[0].Level) + assert.Equal(t, `[POP-507] Deployment references ServiceAccount "sa-bozo" which does not exist`, ii[1].Message) + assert.Equal(t, rules.ErrorLevel, ii[1].Level) + assert.Equal(t, `[POP-106] No resources requests/limits defined`, ii[2].Message) + assert.Equal(t, rules.WarnLevel, ii[2].Level) + assert.Equal(t, `[POP-508] No pods match controller selector: app=p3`, ii[3].Message) + assert.Equal(t, rules.ErrorLevel, ii[3].Level) +} diff --git a/internal/sanitize/svc.go b/internal/lint/svc.go similarity index 68% rename from internal/sanitize/svc.go rename to internal/lint/svc.go index 8e9bcdbe..1712b8c3 100644 --- a/internal/sanitize/svc.go +++ b/internal/lint/svc.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright Authors of Popeye -package sanitize +package lint import ( "context" @@ -9,66 +9,51 @@ import ( "github.com/derailed/popeye/internal" "github.com/derailed/popeye/internal/cache" + "github.com/derailed/popeye/internal/client" + "github.com/derailed/popeye/internal/db" "github.com/derailed/popeye/internal/issues" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/util/intstr" ) -type ( - // ServiceLister list available Services on a cluster. - ServiceLister interface { - PodGetter - EndPointLister - ListServices() map[string]*v1.Service - } - - // PodGetter find a single pod matching service selector. - PodGetter interface { - GetPod(ns string, sel map[string]string) *v1.Pod - } - - // EndPointLister find all service endpoints. - EndPointLister interface { - GetEndpoints(string) *v1.Endpoints - } - - // Service represents a service sanitizer. - Service struct { - *issues.Collector - ServiceLister - } -) +// Service represents a service linter. +type Service struct { + *issues.Collector + db *db.DB +} -// NewService returns a new sanitizer. -func NewService(co *issues.Collector, lister ServiceLister) *Service { +// NewService returns a new instance. +func NewService(co *issues.Collector, db *db.DB) *Service { return &Service{ - Collector: co, - ServiceLister: lister, + Collector: co, + db: db, } } -// Sanitize cleanse the resource. -func (s *Service) Sanitize(ctx context.Context) error { - for fqn, svc := range s.ListServices() { +// Lint cleanse the resource. +func (s *Service) Lint(ctx context.Context) error { + txn, it := s.db.MustITFor(internal.Glossary[internal.SVC]) + defer txn.Abort() + for o := it.Next(); o != nil; o = it.Next() { + svc := o.(*v1.Service) + fqn := client.FQN(svc.Namespace, svc.Name) s.InitOutcome(fqn) - ctx = internal.WithFQN(ctx, fqn) + ctx = internal.WithSpec(ctx, specFor(fqn, svc)) - s.checkPorts(ctx, svc.Namespace, svc.Spec.Selector, svc.Spec.Ports) - s.checkEndpoints(ctx, svc.Spec.Selector, svc.Spec.Type) + if len(svc.Spec.Selector) > 0 { + s.checkPorts(ctx, svc.Namespace, svc.Spec.Selector, svc.Spec.Ports) + s.checkEndpoints(ctx, fqn, svc.Spec.Type) + } s.checkType(ctx, svc.Spec.Type) s.checkExternalTrafficPolicy(ctx, svc.Spec.Type, svc.Spec.ExternalTrafficPolicy) - - if s.NoConcerns(fqn) && s.Config.ExcludeFQN(internal.MustExtractSectionGVR(ctx), fqn) { - s.ClearOutcome(fqn) - } } return nil } func (s *Service) checkPorts(ctx context.Context, ns string, sel map[string]string, ports []v1.ServicePort) { - po := s.GetPod(ns, sel) - if po == nil { + po, err := s.db.FindPod(ns, sel) + if err != nil || po == nil { if len(sel) > 0 { s.AddCode(ctx, 1100) } @@ -114,20 +99,22 @@ func (s *Service) checkExternalTrafficPolicy(ctx context.Context, kind v1.Servic } // CheckEndpoints runs a sanity check on this service endpoints. -func (s *Service) checkEndpoints(ctx context.Context, sel map[string]string, kind v1.ServiceType) { - // Service may not have selectors. - if len(sel) == 0 { - return - } +func (s *Service) checkEndpoints(ctx context.Context, fqn string, kind v1.ServiceType) { // External service bail -> no EPs. if kind == v1.ServiceTypeExternalName { return } - ep := s.GetEndpoints(internal.MustExtractFQN(ctx)) - if ep == nil || len(ep.Subsets) == 0 { + + o, err := s.db.Find(internal.Glossary[internal.EP], fqn) + if err != nil || o == nil { s.AddCode(ctx, 1105) return } + ep := o.(*v1.Endpoints) + if len(ep.Subsets) == 0 { + s.AddCode(ctx, 1110) + return + } numEndpointAddresses := 0 for _, s := range ep.Subsets { numEndpointAddresses += len(s.Addresses) diff --git a/internal/lint/svc_test.go b/internal/lint/svc_test.go new file mode 100644 index 00000000..76e66cdb --- /dev/null +++ b/internal/lint/svc_test.go @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of Popeye + +package lint + +import ( + "context" + "testing" + + "github.com/derailed/popeye/internal" + "github.com/derailed/popeye/internal/db" + "github.com/derailed/popeye/internal/issues" + "github.com/derailed/popeye/internal/rules" + "github.com/derailed/popeye/internal/test" + "github.com/stretchr/testify/assert" + v1 "k8s.io/api/core/v1" +) + +func TestSVCLint(t *testing.T) { + dba, err := test.NewTestDB() + assert.NoError(t, err) + l := db.NewLoader(dba) + + ctx := test.MakeCtx(t) + assert.NoError(t, test.LoadDB[*v1.Service](ctx, l.DB, "core/svc/1.yaml", internal.Glossary[internal.SVC])) + assert.NoError(t, test.LoadDB[*v1.Pod](ctx, l.DB, "core/pod/1.yaml", internal.Glossary[internal.PO])) + assert.NoError(t, test.LoadDB[*v1.Endpoints](ctx, l.DB, "core/ep/1.yaml", internal.Glossary[internal.EP])) + + svc := NewService(test.MakeCollector(t), dba) + assert.Nil(t, svc.Lint(test.MakeContext("v1/pods", "pods"))) + assert.Equal(t, 5, len(svc.Outcome())) + + ii := svc.Outcome()["default/p1"] + assert.Equal(t, 0, len(ii)) + +} + +func Test_svcCheckEndpoints(t *testing.T) { + uu := map[string]struct { + kind v1.ServiceType + fqn, key string + issues issues.Issues + }{ + "empty": { + issues: issues.Issues{ + { + Group: "__root__", + GVR: "v1/services", + Level: rules.ErrorLevel, + Message: "[POP-1105] No associated endpoints", + }, + }, + }, + "external": { + kind: v1.ServiceTypeExternalName, + }, + "no-ep": { + kind: v1.ServiceTypeNodePort, + fqn: "default/svc3", + issues: issues.Issues{ + { + Group: "__root__", + GVR: "v1/services", + Level: rules.ErrorLevel, + Message: "[POP-1105] No associated endpoints", + }, + }, + }, + "nodeport": { + kind: v1.ServiceTypeNodePort, + fqn: "default/svc2", + issues: issues.Issues{ + { + Group: "__root__", + GVR: "v1/services", + Level: rules.WarnLevel, + Message: "[POP-1109] Only one Pod associated with this endpoint", + }, + }, + }, + "no-subset": { + kind: v1.ServiceTypeNodePort, + fqn: "default/svc4", + issues: issues.Issues{ + { + Group: "__root__", + GVR: "v1/services", + Level: rules.WarnLevel, + Message: "[POP-1110] Match EP has no subsets", + }, + }, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + dba, err := test.NewTestDB() + assert.NoError(t, err) + l := db.NewLoader(dba) + ctx := test.MakeContext("v1/services", "services") + ctx = context.WithValue(ctx, internal.KeyConfig, test.MakeConfig(t)) + + assert.NoError(t, test.LoadDB[*v1.Endpoints](ctx, l.DB, "core/ep/1.yaml", internal.Glossary[internal.EP])) + + s := NewService(test.MakeCollector(t), dba) + if u.fqn != "" { + ctx = internal.WithSpec(ctx, specFor(u.fqn, nil)) + } + s.checkEndpoints(ctx, u.fqn, u.kind) + + assert.Equal(t, u.issues, s.Outcome()[u.fqn]) + }) + } +} diff --git a/internal/lint/testdata/apps/dp/1.yaml b/internal/lint/testdata/apps/dp/1.yaml new file mode 100644 index 00000000..61e29645 --- /dev/null +++ b/internal/lint/testdata/apps/dp/1.yaml @@ -0,0 +1,198 @@ +apiVersion: v1 +kind: List +items: +- apiVersion: apps/v1 + kind: Deployment + metadata: + name: dp1 + namespace: default + spec: + progressDeadlineSeconds: 600 + replicas: 1 + revisionHistoryLimit: 10 + selector: + matchLabels: + app: p1 + strategy: + rollingUpdate: + maxSurge: 25% + maxUnavailable: 25% + type: RollingUpdate + template: + metadata: + labels: + app: p1 + spec: + initContainers: + - name: ic1 + image: fred:v1.0.0 + resources: + limits: + cpu: 200m + memory: 30Mi + requests: + cpu: 100m + memory: 10Mi + containers: + - name: c1 + image: fred:v1.0.0 + imagePullPolicy: Always + ports: + - containerPort: 4000 + name: http + protocol: TCP + resources: + limits: + cpu: 200m + memory: 30Mi + requests: + cpu: 100m + memory: 10Mi + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + volumeMounts: + - mountPath: /data + name: om + dnsPolicy: ClusterFirst + restartPolicy: Always + schedulerName: default-scheduler + securityContext: {} + terminationGracePeriodSeconds: 30 + volumes: + - emptyDir: {} + name: om + status: + availableReplicas: 1 + conditions: + - lastTransitionTime: "2024-01-18T19:49:21Z" + lastUpdateTime: "2024-01-18T19:49:21Z" + message: Deployment has minimum availability. + reason: MinimumReplicasAvailable + status: "True" + type: Available + - lastTransitionTime: "2024-01-03T20:38:15Z" + lastUpdateTime: "2024-01-18T19:51:27Z" + message: ReplicaSet "dashb-7c46847b9" has successfully progressed. + reason: NewReplicaSetAvailable + status: "True" + type: Progressing + observedGeneration: 9 + readyReplicas: 1 + replicas: 1 + updatedReplicas: 1 +- apiVersion: apps/v1 + kind: Deployment + metadata: + name: dp2 + namespace: default + spec: + progressDeadlineSeconds: 600 + replicas: 1 + revisionHistoryLimit: 10 + selector: + matchLabels: + app: pod-bozo + strategy: + rollingUpdate: + maxSurge: 25% + maxUnavailable: 25% + type: RollingUpdate + template: + metadata: + spec: + automountServiceAccountToken: true + containers: + - image: blee:0.1.0 + imagePullPolicy: IfNotPresent + livenessProbe: + failureThreshold: 10 + httpGet: + path: /api/health + port: 3000 + scheme: HTTP + initialDelaySeconds: 60 + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 30 + name: grafana + ports: + - containerPort: 3000 + protocol: TCP + readinessProbe: + failureThreshold: 3 + httpGet: + path: /api/health + port: 3000 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 + resources: {} + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + seccompProfile: + type: RuntimeDefault + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + dnsPolicy: ClusterFirst + enableServiceLinks: true + restartPolicy: Always + schedulerName: default-scheduler + securityContext: + fsGroup: 472 + runAsGroup: 472 + runAsNonRoot: true + runAsUser: 472 + serviceAccount: sa-bozo + serviceAccountName: sa-bozo + terminationGracePeriodSeconds: 30 + volumes: + - configMap: + defaultMode: 420 + name: fred + name: config + - emptyDir: {} + name: storage + status: + availableReplicas: 0 + conditions: + - lastTransitionTime: "2024-01-03T21:21:50Z" + lastUpdateTime: "2024-01-03T21:21:50Z" + message: Deployment has minimum availability. + reason: MinimumReplicasAvailable + status: "True" + type: Available + - lastTransitionTime: "2024-01-03T21:21:40Z" + lastUpdateTime: "2024-01-03T21:21:50Z" + message: zorg + reason: NewReplicaSetAvailable + status: "True" + type: Progressing + observedGeneration: 1 + readyReplicas: 1 + replicas: 1 + updatedReplicas: 1 +- apiVersion: apps/v1 + kind: Deployment + metadata: + name: dp3 + namespace: default + spec: + progressDeadlineSeconds: 600 + replicas: 0 + revisionHistoryLimit: 10 + selector: + template: + metadata: + spec: + automountServiceAccountToken: true + containers: + status: + availableReplicas: 0 + observedGeneration: 1 + readyReplicas: 1 + replicas: 1 + updatedReplicas: 1 diff --git a/internal/lint/testdata/apps/ds/1.yaml b/internal/lint/testdata/apps/ds/1.yaml new file mode 100644 index 00000000..5203e848 --- /dev/null +++ b/internal/lint/testdata/apps/ds/1.yaml @@ -0,0 +1,328 @@ +apiVersion: v1 +kind: List +items: +- apiVersion: apps/v1 + kind: DaemonSet + metadata: + name: ds1 + namespace: default + spec: + revisionHistoryLimit: 10 + selector: + matchLabels: + app: p1 + template: + metadata: + labels: + app: p1 + spec: + automountServiceAccountToken: false + containers: + - name: c1 + image: fred:v0.0.0 + imagePullPolicy: IfNotPresent + livenessProbe: + failureThreshold: 3 + httpGet: + path: / + port: 9100 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 + ports: + - containerPort: 9100 + hostPort: 9100 + name: metrics + protocol: TCP + readinessProbe: + failureThreshold: 3 + httpGet: + path: / + port: 9100 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 + resources: + requests: + cpu: 1m + memory: 10Mi + limits: + cpu: 1m + memory: 10Mi + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + volumeMounts: + - mountPath: /host/proc + name: proc + readOnly: true + - mountPath: /host/sys + name: sys + readOnly: true + - mountPath: /host/root + mountPropagation: HostToContainer + name: root + readOnly: true + dnsPolicy: ClusterFirst + hostNetwork: true + hostPID: true + nodeSelector: + kubernetes.io/os: linux + restartPolicy: Always + schedulerName: default-scheduler + securityContext: + fsGroup: 65534 + runAsGroup: 65534 + runAsNonRoot: true + runAsUser: 65534 + serviceAccount: sa1 + serviceAccountName: sa1 + terminationGracePeriodSeconds: 30 + tolerations: + - effect: NoSchedule + operator: Exists + volumes: + - hostPath: + path: /proc + type: "" + name: proc + - hostPath: + path: /sys + type: "" + name: sys + - hostPath: + path: / + type: "" + name: root + updateStrategy: + rollingUpdate: + maxSurge: 0 + maxUnavailable: 1 + type: RollingUpdate + status: + currentNumberScheduled: 2 + desiredNumberScheduled: 2 + numberAvailable: 2 + numberMisscheduled: 0 + numberReady: 2 + observedGeneration: 1 + updatedNumberScheduled: 2 +- apiVersion: apps/v1 + kind: DaemonSet + metadata: + name: ds2 + namespace: default + spec: + revisionHistoryLimit: 10 + selector: + matchLabels: + app: p10 + template: + metadata: + labels: + app: p10 + spec: + automountServiceAccountToken: false + initContainers: + - name: ic1 + image: fred + containers: + - name: c1 + image: fred + imagePullPolicy: IfNotPresent + livenessProbe: + failureThreshold: 3 + httpGet: + path: / + port: 9100 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 + ports: + - containerPort: 9100 + hostPort: 9100 + name: metrics + protocol: TCP + readinessProbe: + failureThreshold: 3 + httpGet: + path: / + port: 9100 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 + resources: + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + volumeMounts: + - mountPath: /host/proc + name: proc + readOnly: true + - mountPath: /host/sys + name: sys + readOnly: true + - mountPath: /host/root + mountPropagation: HostToContainer + name: root + readOnly: true + dnsPolicy: ClusterFirst + hostNetwork: true + hostPID: true + nodeSelector: + kubernetes.io/os: linux + restartPolicy: Always + schedulerName: default-scheduler + securityContext: + fsGroup: 65534 + runAsGroup: 65534 + runAsNonRoot: true + runAsUser: 65534 + serviceAccount: sa-bozo + serviceAccountName: sa-bozo + terminationGracePeriodSeconds: 30 + tolerations: + - effect: NoSchedule + operator: Exists + volumes: + - hostPath: + path: /proc + type: "" + name: proc + - hostPath: + path: /sys + type: "" + name: sys + - hostPath: + path: / + type: "" + name: root + updateStrategy: + rollingUpdate: + maxSurge: 0 + maxUnavailable: 1 + type: RollingUpdate + status: + currentNumberScheduled: 2 + desiredNumberScheduled: 2 + numberAvailable: 2 + numberMisscheduled: 0 + numberReady: 2 + observedGeneration: 1 + updatedNumberScheduled: 2 +- apiVersion: apps/v1 + kind: DaemonSet + metadata: + name: ds2 + namespace: default + spec: + revisionHistoryLimit: 10 + selector: + matchLabels: + app: p10 + template: + metadata: + labels: + app: p10 + spec: + automountServiceAccountToken: false + initContainers: + - name: ic1 + image: fred + containers: + - name: c1 + image: fred + imagePullPolicy: IfNotPresent + livenessProbe: + failureThreshold: 3 + httpGet: + path: / + port: 9100 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 + ports: + - containerPort: 9100 + hostPort: 9100 + name: metrics + protocol: TCP + readinessProbe: + failureThreshold: 3 + httpGet: + path: / + port: 9100 + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 + resources: + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + volumeMounts: + - mountPath: /host/proc + name: proc + readOnly: true + - mountPath: /host/sys + name: sys + readOnly: true + - mountPath: /host/root + mountPropagation: HostToContainer + name: root + readOnly: true + dnsPolicy: ClusterFirst + hostNetwork: true + hostPID: true + nodeSelector: + kubernetes.io/os: linux + restartPolicy: Always + schedulerName: default-scheduler + securityContext: + fsGroup: 65534 + runAsGroup: 65534 + runAsNonRoot: true + runAsUser: 65534 + serviceAccount: sa-bozo + serviceAccountName: sa-bozo + terminationGracePeriodSeconds: 30 + tolerations: + - effect: NoSchedule + operator: Exists + volumes: + - hostPath: + path: /proc + type: "" + name: proc + - hostPath: + path: /sys + type: "" + name: sys + - hostPath: + path: / + type: "" + name: root + updateStrategy: + rollingUpdate: + maxSurge: 0 + maxUnavailable: 1 + type: RollingUpdate + status: + currentNumberScheduled: 2 + desiredNumberScheduled: 2 + numberAvailable: 2 + numberMisscheduled: 0 + numberReady: 2 + observedGeneration: 1 + updatedNumberScheduled: 2 + diff --git a/internal/lint/testdata/apps/rs/1.yaml b/internal/lint/testdata/apps/rs/1.yaml new file mode 100644 index 00000000..ead707be --- /dev/null +++ b/internal/lint/testdata/apps/rs/1.yaml @@ -0,0 +1,103 @@ +apiVersion: v1 +kind: List +items: +- apiVersion: apps/v1 + kind: ReplicaSet + metadata: + name: rs1 + namespace: default + ownerReferences: + - apiVersion: apps/v1 + blockOwnerDeletion: true + controller: true + kind: Deployment + name: dp1 + spec: + replicas: 0 + selector: + matchLabels: + app: p1 + template: + metadata: + labels: + app: p1 + spec: + containers: + - image: fred + imagePullPolicy: Always + name: c1 + ports: + - containerPort: 4000 + name: http + protocol: TCP + resources: + limits: + cpu: 200m + memory: 30Mi + requests: + cpu: 100m + memory: 10Mi + volumeMounts: + - mountPath: /data + name: om + dnsPolicy: ClusterFirst + restartPolicy: Always + schedulerName: default-scheduler + securityContext: {} + terminationGracePeriodSeconds: 30 + volumes: + - emptyDir: {} + name: om + status: + observedGeneration: 2 + replicas: 0 +- apiVersion: apps/v1 + kind: ReplicaSet + metadata: + name: rs2 + namespace: default + ownerReferences: + - apiVersion: apps/v1 + blockOwnerDeletion: true + controller: true + kind: Deployment + name: dp2 + spec: + replicas: 2 + selector: + matchLabels: + app: p2 + template: + metadata: + labels: + app: p2 + spec: + containers: + - image: fred + imagePullPolicy: Always + name: c1 + ports: + - containerPort: 4000 + name: http + protocol: TCP + resources: + limits: + cpu: 200m + memory: 30Mi + requests: + cpu: 100m + memory: 10Mi + volumeMounts: + - mountPath: /data + name: om + dnsPolicy: ClusterFirst + restartPolicy: Always + schedulerName: default-scheduler + securityContext: {} + terminationGracePeriodSeconds: 30 + volumes: + - emptyDir: {} + name: om + status: + observedGeneration: 2 + replicas: 1 diff --git a/internal/lint/testdata/apps/sts/1.yaml b/internal/lint/testdata/apps/sts/1.yaml new file mode 100644 index 00000000..a3904a32 --- /dev/null +++ b/internal/lint/testdata/apps/sts/1.yaml @@ -0,0 +1,230 @@ +apiVersion: v1 +kind: List +items: +- apiVersion: apps/v1 + kind: StatefulSet + metadata: + name: sts1 + namespace: default + spec: + podManagementPolicy: OrderedReady + replicas: 1 + revisionHistoryLimit: 10 + selector: + matchLabels: + app: p1 + serviceName: svc1 + template: + metadata: + labels: + app: p1 + spec: + automountServiceAccountToken: true + containers: + - image: fred:v0.1.0 + imagePullPolicy: IfNotPresent + livenessProbe: + failureThreshold: 3 + httpGet: + path: / + port: http + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 + name: c1 + ports: + - containerPort: 9093 + name: http + protocol: TCP + readinessProbe: + failureThreshold: 3 + httpGet: + path: / + port: http + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 + resources: + limits: + cpu: 200m + memory: 30Mi + requests: + cpu: 100m + memory: 10Mi + securityContext: + runAsGroup: 65534 + runAsNonRoot: true + runAsUser: 65534 + dnsPolicy: ClusterFirst + restartPolicy: Always + schedulerName: default-scheduler + securityContext: + fsGroup: 65534 + runAsGroup: 65534 + runAsNonRoot: true + runAsUser: 65534 + serviceAccount: sa1 + serviceAccountName: sa1 + terminationGracePeriodSeconds: 30 + updateStrategy: + rollingUpdate: + partition: 0 + type: RollingUpdate + status: + collisionCount: 0 + currentReplicas: 1 + observedGeneration: 1 + readyReplicas: 1 + replicas: 1 + updatedReplicas: 1 +- apiVersion: apps/v1 + kind: StatefulSet + metadata: + name: sts2 + namespace: default + spec: + podManagementPolicy: OrderedReady + replicas: 1 + revisionHistoryLimit: 10 + selector: + matchLabels: + app: p2 + serviceName: svc2 + template: + metadata: + labels: + app: p2 + spec: + automountServiceAccountToken: true + containers: + - image: fred + imagePullPolicy: IfNotPresent + livenessProbe: + failureThreshold: 3 + httpGet: + path: / + port: http + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 + name: c1 + ports: + - containerPort: 9093 + name: http + protocol: TCP + readinessProbe: + failureThreshold: 3 + httpGet: + path: / + port: http + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 + resources: + requests: + cpu: 1 + memory: 1Mi + limits: + cpu: 1 + memory: 1Mi + securityContext: + runAsGroup: 65534 + runAsNonRoot: true + runAsUser: 65534 + dnsPolicy: ClusterFirst + restartPolicy: Always + schedulerName: default-scheduler + securityContext: + fsGroup: 65534 + runAsGroup: 65534 + runAsNonRoot: true + runAsUser: 65534 + serviceAccount: sa1 + serviceAccountName: sa1 + terminationGracePeriodSeconds: 30 + updateStrategy: + rollingUpdate: + partition: 0 + type: RollingUpdate + status: + collisionCount: 0 + currentReplicas: 1 + observedGeneration: 1 + readyReplicas: 1 + replicas: 1 + updatedReplicas: 1 +- apiVersion: apps/v1 + kind: StatefulSet + metadata: + name: sts3 + namespace: default + spec: + podManagementPolicy: OrderedReady + replicas: 1 + revisionHistoryLimit: 10 + selector: + matchLabels: + app: p3 + serviceName: svc3 + template: + metadata: + labels: + app: p3 + spec: + automountServiceAccountToken: true + containers: + - image: fred:0.0.1 + imagePullPolicy: IfNotPresent + livenessProbe: + failureThreshold: 3 + httpGet: + path: / + port: http + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 + name: c1 + ports: + - containerPort: 9093 + name: http + protocol: TCP + readinessProbe: + failureThreshold: 3 + httpGet: + path: / + port: http + scheme: HTTP + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 + resources: {} + securityContext: + runAsGroup: 65534 + runAsNonRoot: true + runAsUser: 65534 + dnsPolicy: ClusterFirst + restartPolicy: Always + schedulerName: default-scheduler + securityContext: + fsGroup: 65534 + runAsGroup: 65534 + runAsNonRoot: true + runAsUser: 65534 + serviceAccountName: sa-bozo + terminationGracePeriodSeconds: 30 + updateStrategy: + rollingUpdate: + partition: 0 + type: RollingUpdate + status: + collisionCount: 0 + currentReplicas: 1 + observedGeneration: 1 + readyReplicas: 0 + replicas: 1 + updatedReplicas: 1 \ No newline at end of file diff --git a/internal/lint/testdata/auth/cr/1.yaml b/internal/lint/testdata/auth/cr/1.yaml new file mode 100644 index 00000000..7be46916 --- /dev/null +++ b/internal/lint/testdata/auth/cr/1.yaml @@ -0,0 +1,28 @@ +--- +apiVersion: v1 +kind: List +items: + - apiVersion: rbac.authorization.k8s.io/v1 + kind: ClusterRole + metadata: + name: cr1 + rules: + - apiGroups: [""] + resources: ["pods"] + verbs: ["get", "watch", "list"] + - apiVersion: rbac.authorization.k8s.io/v1 + kind: ClusterRole + metadata: + name: cr2 + rules: + - apiGroups: [""] + resources: ["configmaps"] + verbs: ["get", "watch", "list"] + - apiVersion: rbac.authorization.k8s.io/v1 + kind: ClusterRole + metadata: + name: cr3 + rules: + - apiGroups: [""] + resources: ["secrets"] + verbs: ["get", "watch", "list"] \ No newline at end of file diff --git a/internal/lint/testdata/auth/crb/1.yaml b/internal/lint/testdata/auth/crb/1.yaml new file mode 100644 index 00000000..9c7112a3 --- /dev/null +++ b/internal/lint/testdata/auth/crb/1.yaml @@ -0,0 +1,42 @@ +--- +apiVersion: v1 +kind: List +items: + - apiVersion: rbac.authorization.k8s.io/v1 + kind: ClusterRoleBinding + metadata: + name: crb1 + subjects: + - kind: User + name: fred + apiGroup: rbac.authorization.k8s.io + roleRef: + kind: ClusterRole + name: cr1 + apiGroup: rbac.authorization.k8s.io + - apiVersion: rbac.authorization.k8s.io/v1 + kind: ClusterRoleBinding + metadata: + name: crb2 + subjects: + - kind: ServiceAccount + name: sa2 + namespace: default + apiGroup: rbac.authorization.k8s.io + roleRef: + kind: ClusterRole + name: cr-bozo + apiGroup: rbac.authorization.k8s.io + - apiVersion: rbac.authorization.k8s.io/v1 + kind: ClusterRoleBinding + metadata: + name: crb3 + subjects: + - kind: ServiceAccount + name: sa-bozo + namespace: default + apiGroup: rbac.authorization.k8s.io + roleRef: + kind: Role + name: r-bozo + apiGroup: rbac.authorization.k8s.io \ No newline at end of file diff --git a/internal/lint/testdata/auth/ro/1.yaml b/internal/lint/testdata/auth/ro/1.yaml new file mode 100644 index 00000000..dd5c06a8 --- /dev/null +++ b/internal/lint/testdata/auth/ro/1.yaml @@ -0,0 +1,31 @@ +--- +apiVersion: v1 +kind: List +items: + - apiVersion: rbac.authorization.k8s.io/v1 + kind: Role + metadata: + name: r1 + namespace: default + rules: + - apiGroups: [""] + resources: ["pods"] + verbs: ["get", "watch", "list"] + - apiVersion: rbac.authorization.k8s.io/v1 + kind: Role + metadata: + name: r2 + namespace: default + rules: + - apiGroups: [""] + resources: ["configmaps"] + verbs: ["get", "watch", "list"] + - apiVersion: rbac.authorization.k8s.io/v1 + kind: Role + metadata: + name: r3 + namespace: default + rules: + - apiGroups: [""] + resources: ["secrets"] + verbs: ["get", "watch", "list"] \ No newline at end of file diff --git a/internal/lint/testdata/auth/rob/1.yaml b/internal/lint/testdata/auth/rob/1.yaml new file mode 100644 index 00000000..f338fe2c --- /dev/null +++ b/internal/lint/testdata/auth/rob/1.yaml @@ -0,0 +1,43 @@ +--- +apiVersion: v1 +kind: List +items: + - apiVersion: rbac.authorization.k8s.io/v1 + kind: RoleBinding + metadata: + name: rb1 + namespace: default + subjects: + - kind: User + name: fred + apiGroup: rbac.authorization.k8s.io + roleRef: + kind: Role + name: r1 + apiGroup: rbac.authorization.k8s.io + - apiVersion: rbac.authorization.k8s.io/v1 + kind: RoleBinding + metadata: + name: rb2 + namespace: default + subjects: + - kind: ServiceAccount + name: sa-bozo + apiGroup: rbac.authorization.k8s.io + roleRef: + kind: Role + name: r-bozo + apiGroup: rbac.authorization.k8s.io + - apiVersion: rbac.authorization.k8s.io/v1 + kind: RoleBinding + metadata: + name: rb3 + namespace: default + subjects: + - kind: ServiceAccount + name: sa-bozo + apiGroup: rbac.authorization.k8s.io + roleRef: + kind: ClusterRole + name: cr-bozo + apiGroup: rbac.authorization.k8s.io \ No newline at end of file diff --git a/internal/lint/testdata/autoscaling/hpa/1.yaml b/internal/lint/testdata/autoscaling/hpa/1.yaml new file mode 100644 index 00000000..9afc38de --- /dev/null +++ b/internal/lint/testdata/autoscaling/hpa/1.yaml @@ -0,0 +1,183 @@ +apiVersion: v1 +kind: List +items: +- apiVersion: autoscaling/v2 + kind: HorizontalPodAutoscaler + metadata: + name: hpa1 + namespace: default + spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: dp1 + minReplicas: 1 + maxReplicas: 10 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 50 + status: + observedGeneration: 1 + currentReplicas: 1 + desiredReplicas: 1 + currentMetrics: + - type: Resource + resource: + name: cpu + current: + averageUtilization: 0 + averageValue: 0 +- apiVersion: autoscaling/v2 + kind: HorizontalPodAutoscaler + metadata: + name: hpa2 + namespace: default + spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: dp-toast + minReplicas: 1 + maxReplicas: 10 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 50 + status: + observedGeneration: 1 + currentReplicas: 1 + desiredReplicas: 1 + currentMetrics: + - type: Resource + resource: + name: cpu + current: + averageUtilization: 0 + averageValue: 0 +- apiVersion: autoscaling/v2 + kind: HorizontalPodAutoscaler + metadata: + name: hpa3 + namespace: default + spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: ReplicaSet + name: rs-toast + minReplicas: 1 + maxReplicas: 10 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 50 + status: + observedGeneration: 1 + currentReplicas: 1 + desiredReplicas: 1 + currentMetrics: + - type: Resource + resource: + name: cpu + current: + averageUtilization: 0 + averageValue: 0 +- apiVersion: autoscaling/v2 + kind: HorizontalPodAutoscaler + metadata: + name: hpa4 + namespace: default + spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: StatefulSet + name: sts-toast + minReplicas: 1 + maxReplicas: 10 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 50 + status: + observedGeneration: 1 + currentReplicas: 1 + desiredReplicas: 1 + currentMetrics: + - type: Resource + resource: + name: cpu + current: + averageUtilization: 0 + averageValue: 0 +- apiVersion: autoscaling/v2 + kind: HorizontalPodAutoscaler + metadata: + name: hpa5 + namespace: default + spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: StatefulSet + name: sts1 + minReplicas: 1 + maxReplicas: 10 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 50 + status: + observedGeneration: 1 + currentReplicas: 1 + desiredReplicas: 1 + currentMetrics: + - type: Resource + resource: + name: cpu + current: + averageUtilization: 0 + averageValue: 0 +- apiVersion: autoscaling/v2 + kind: HorizontalPodAutoscaler + metadata: + name: hpa6 + namespace: default + spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: ReplicaSet + name: rs1 + minReplicas: 1 + maxReplicas: 10 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 50 + status: + observedGeneration: 1 + currentReplicas: 1 + desiredReplicas: 1 + currentMetrics: + - type: Resource + resource: + name: cpu + current: + averageUtilization: 0 + averageValue: 0 \ No newline at end of file diff --git a/internal/lint/testdata/batch/cjob/1.yaml b/internal/lint/testdata/batch/cjob/1.yaml new file mode 100644 index 00000000..e760b3e6 --- /dev/null +++ b/internal/lint/testdata/batch/cjob/1.yaml @@ -0,0 +1,76 @@ +apiVersion: v1 +kind: List +items: +- apiVersion: batch/v1 + kind: CronJob + metadata: + name: cj1 + namespace: default + spec: + concurrencyPolicy: Forbid + failedJobsHistoryLimit: 1 + jobTemplate: + metadata: + creationTimestamp: null + spec: + selector: + matchLabels: + app: j1 + template: + metadata: + creationTimestamp: null + spec: + containers: + - image: fred:1.0 + imagePullPolicy: Always + name: c1 + resources: + limits: + cpu: 500m + memory: 1Mi + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + dnsPolicy: ClusterFirst + restartPolicy: OnFailure + schedulerName: default-scheduler + securityContext: {} + terminationGracePeriodSeconds: 30 + schedule: '* * * * *' + successfulJobsHistoryLimit: 3 + suspend: false + status: + active: + - apiVersion: batch/v1 + kind: Job + name: j1 + namespace: default + lastScheduleTime: "2023-02-06T15:49:00Z" + lastSuccessfulTime: "2023-02-06T15:49:38Z" +- apiVersion: batch/v1 + kind: CronJob + metadata: + name: cj2 + namespace: default + spec: + concurrencyPolicy: Forbid + failedJobsHistoryLimit: 1 + jobTemplate: + spec: + template: + spec: + serviceAccountName: sa-bozo + containers: + - image: blang/busybox-bash + imagePullPolicy: Always + name: c1 + resources: {} + dnsPolicy: ClusterFirst + restartPolicy: OnFailure + schedulerName: default-scheduler + securityContext: {} + terminationGracePeriodSeconds: 30 + schedule: '* * * * *' + successfulJobsHistoryLimit: 3 + suspend: true + status: + active: [] diff --git a/internal/lint/testdata/batch/job/1.yaml b/internal/lint/testdata/batch/job/1.yaml new file mode 100644 index 00000000..6cfde3e7 --- /dev/null +++ b/internal/lint/testdata/batch/job/1.yaml @@ -0,0 +1,160 @@ +apiVersion: v1 +kind: List +items: +- apiVersion: batch/v1 + kind: Job + metadata: + labels: + job-name: j1 + app: j1 + name: j1 + namespace: default + ownerReferences: + - apiVersion: batch/v1 + blockOwnerDeletion: true + controller: true + kind: CronJob + name: cj1 + spec: + backoffLimit: 6 + completionMode: NonIndexed + completions: 1 + parallelism: 1 + selector: + matchLabels: + batch.kubernetes.io/controller-uid: xxx + suspend: false + template: + metadata: + creationTimestamp: null + labels: + batch.kubernetes.io/controller-uid: xxx + batch.kubernetes.io/job-name: j1 + job-name: j1 + spec: + containers: + - image: fred:1.0 + imagePullPolicy: Always + name: c1 + resources: + limits: + cpu: 1m + memory: 1Mi + dnsPolicy: ClusterFirst + restartPolicy: OnFailure + schedulerName: default-scheduler + securityContext: {} + terminationGracePeriodSeconds: 30 + status: + conditions: + - lastProbeTime: "2023-02-05T23:21:13Z" + lastTransitionTime: "2023-02-05T23:21:13Z" + status: "True" + type: Complete + ready: 0 + startTime: "2023-02-05T23:21:00Z" + succeeded: 1 + uncountedTerminatedPods: {} +- apiVersion: batch/v1 + kind: Job + metadata: + labels: + job-name: j2 + name: j2 + namespace: default + ownerReferences: + - apiVersion: batch/v1 + blockOwnerDeletion: true + controller: true + kind: CronJob + name: cj2 + spec: + backoffLimit: 6 + completionMode: NonIndexed + completions: 1 + parallelism: 1 + selector: + matchLabels: + batch.kubernetes.io/controller-uid: xxx + suspend: false + template: + metadata: + creationTimestamp: null + labels: + batch.kubernetes.io/job-name: j2 + job-name: j2 + spec: + containers: + - image: bozo + imagePullPolicy: Always + name: c1 + resources: {} + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + dnsPolicy: ClusterFirst + restartPolicy: OnFailure + schedulerName: default-scheduler + securityContext: {} + terminationGracePeriodSeconds: 30 + status: + active: 1 + ready: 0 + startTime: "2023-02-06T15:49:38Z" + uncountedTerminatedPods: {} +- apiVersion: batch/v1 + kind: Job + metadata: + labels: + job-name: j3 + name: j3 + namespace: default + ownerReferences: + - apiVersion: batch/v1 + blockOwnerDeletion: true + controller: true + kind: CronJob + name: cj3 + spec: + backoffLimit: 6 + completionMode: NonIndexed + completions: 1 + parallelism: 1 + selector: + matchLabels: + batch.kubernetes.io/controller-uid: xxx + suspend: true + template: + metadata: + creationTimestamp: null + labels: + batch.kubernetes.io/job-name: j2 + job-name: j2 + spec: + initContainers: + - image: bozo:1.0.0 + imagePullPolicy: Always + name: ic1 + resources: + limits: + cpu: 1m + memory: 1Mi + containers: + - image: bozo:1.0.0 + imagePullPolicy: Always + name: c1 + resources: + limits: + cpu: 1m + memory: 1Mi + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + dnsPolicy: ClusterFirst + restartPolicy: OnFailure + schedulerName: default-scheduler + securityContext: {} + terminationGracePeriodSeconds: 30 + status: + active: 1 + ready: 0 + startTime: "2023-02-06T15:49:38Z" + uncountedTerminatedPods: {} \ No newline at end of file diff --git a/internal/lint/testdata/core/cm/1.yaml b/internal/lint/testdata/core/cm/1.yaml new file mode 100644 index 00000000..462383a3 --- /dev/null +++ b/internal/lint/testdata/core/cm/1.yaml @@ -0,0 +1,37 @@ +--- +apiVersion: v1 +kind: List +items: +- apiVersion: v1 + kind: ConfigMap + metadata: + name: cm1 + namespace: default + data: + fred.yaml: | + k1: 1 + k2: blee +- apiVersion: v1 + kind: ConfigMap + metadata: + name: cm2 + namespace: default + data: + k1: apple + k2: bee +- apiVersion: v1 + kind: ConfigMap + metadata: + name: cm3 + namespace: default + data: + k1: apple + k2: bee +- apiVersion: v1 + kind: ConfigMap + metadata: + name: cm4 + namespace: default + data: + k1: apple + k2: bee diff --git a/internal/lint/testdata/core/ep/1.yaml b/internal/lint/testdata/core/ep/1.yaml new file mode 100644 index 00000000..78f37ff9 --- /dev/null +++ b/internal/lint/testdata/core/ep/1.yaml @@ -0,0 +1,68 @@ +apiVersion: v1 +kind: List +items: +- apiVersion: v1 + kind: Endpoints + metadata: + name: svc1 + namespace: default + labels: + app: p1 + subsets: + - addresses: + - ip: 10.244.1.27 + nodeName: n1 + targetRef: + kind: Pod + name: p1 + namespace: default + ports: + - name: http + port: 4000 + protocol: TCP +- apiVersion: v1 + kind: Endpoints + metadata: + name: svc2 + namespace: default + subsets: + - addresses: + - ip: 10.244.1.19 + nodeName: n1 + targetRef: + kind: Pod + name: p2 + namespace: default + ports: + - name: service + port: 3000 + protocol: TCP +- apiVersion: v1 + kind: Endpoints + metadata: + name: svc-none + namespace: default + subsets: + - addresses: + - ip: 10.244.1.19 + nodeName: n1 + targetRef: + kind: Pod + name: p5 + namespace: default + - ip: 10.244.1.19 + nodeName: n1 + targetRef: + kind: Pod + name: p4 + namespace: default + ports: + - name: service + port: 3000 + protocol: TCP +- apiVersion: v1 + kind: Endpoints + metadata: + name: svc4 + namespace: default + subsets: \ No newline at end of file diff --git a/internal/lint/testdata/core/node/1.yaml b/internal/lint/testdata/core/node/1.yaml new file mode 100644 index 00000000..39262c28 --- /dev/null +++ b/internal/lint/testdata/core/node/1.yaml @@ -0,0 +1,234 @@ +apiVersion: v1 +kind: List +items: +- apiVersion: v1 + kind: Node + metadata: + labels: + node-role.kubernetes.io/control-plane: "" + node-role.kubernetes.io/master: "" + node.kubernetes.io/exclude-from-external-load-balancers: "" + name: n1 + spec: + podCIDR: 10.244.0.0/24 + podCIDRs: + - 10.244.0.0/24 + status: + addresses: + - address: 192.168.228.3 + type: InternalIP + - address: dashb-control-plane + type: Hostname + allocatable: + cpu: "10" + ephemeral-storage: 816748224Ki + memory: 8124744Ki + pods: "110" + capacity: + cpu: "10" + ephemeral-storage: 816748224Ki + memory: 8124744Ki + pods: "110" + conditions: + - lastHeartbeatTime: "2024-01-27T15:31:39Z" + lastTransitionTime: "2024-01-03T20:35:11Z" + message: kubelet has sufficient memory available + reason: KubeletHasSufficientMemory + status: "False" + type: MemoryPressure + - lastHeartbeatTime: "2024-01-27T15:31:39Z" + lastTransitionTime: "2024-01-03T20:35:11Z" + message: kubelet has no disk pressure + reason: KubeletHasNoDiskPressure + status: "False" + type: DiskPressure + - lastHeartbeatTime: "2024-01-27T15:31:39Z" + lastTransitionTime: "2024-01-03T20:35:11Z" + message: kubelet has sufficient PID available + reason: KubeletHasSufficientPID + status: "False" + type: PIDPressure + - lastHeartbeatTime: "2024-01-27T15:31:39Z" + lastTransitionTime: "2024-01-03T20:35:38Z" + message: kubelet is posting ready status + reason: KubeletReady + status: "True" + type: Ready + daemonEndpoints: + kubeletEndpoint: + Port: 10250 + images: + nodeInfo: + architecture: arm64 + bootID: 0836e65d-3091-4cb5-8ad4-8f65425f87e3 + containerRuntimeVersion: containerd://1.5.1 + kernelVersion: 6.5.10-orbstack-00110-gbcfe04c86d2f + kubeProxyVersion: v1.21.1 + kubeletVersion: v1.21.1 + machineID: 6bbc44bb821d48b995092021d706d8e6 + operatingSystem: linux + osImage: Ubuntu 20.10 + systemUUID: 6bbc44bb821d48b995092021d706d8e6 +- apiVersion: v1 + kind: Node + metadata: + annotations: + kubeadm.alpha.kubernetes.io/cri-socket: unix:///run/containerd/containerd.sock + node.alpha.kubernetes.io/ttl: "0" + volumes.kubernetes.io/controller-managed-attach-detach: "true" + labels: + beta.kubernetes.io/arch: arm64 + beta.kubernetes.io/os: linux + kubernetes.io/arch: arm64 + kubernetes.io/hostname: n2 + kubernetes.io/os: linux + name: n2 + spec: + podCIDR: 10.244.1.0/24 + podCIDRs: + - 10.244.1.0/24 + taints: + - effect: NoSchedule + key: t2 + status: + addresses: + - address: 192.168.228.2 + type: InternalIP + - address: dashb-worker + type: Hostname + allocatable: + cpu: "10" + ephemeral-storage: 816748224Ki + memory: 8124744Ki + pods: "110" + capacity: + cpu: "10" + ephemeral-storage: 816748224Ki + memory: 8124744Ki + pods: "110" + conditions: + - lastHeartbeatTime: "2024-01-27T15:30:29Z" + lastTransitionTime: "2024-01-03T20:35:48Z" + message: kubelet has sufficient memory available + reason: KubeletHasSufficientMemory + status: "False" + type: MemoryPressure + - lastHeartbeatTime: "2024-01-27T15:30:29Z" + lastTransitionTime: "2024-01-03T20:35:48Z" + message: kubelet has no disk pressure + reason: KubeletHasNoDiskPressure + status: "False" + type: DiskPressure + - lastHeartbeatTime: "2024-01-27T15:30:29Z" + lastTransitionTime: "2024-01-03T20:35:48Z" + message: kubelet has sufficient PID available + reason: KubeletHasSufficientPID + status: "False" + type: PIDPressure + - lastHeartbeatTime: "2024-01-27T15:30:29Z" + lastTransitionTime: "2024-01-03T20:35:58Z" + message: kubelet is posting ready status + reason: KubeletReady + status: "True" + type: Ready + - lastHeartbeatTime: "2024-01-27T15:30:29Z" + lastTransitionTime: "2024-01-03T20:35:58Z" + message: blee + reason: blah + status: "True" + type: NetworkUnavailable + daemonEndpoints: + kubeletEndpoint: + Port: 10250 + images: + nodeInfo: + architecture: arm64 + containerRuntimeVersion: containerd://1.5.1 + kernelVersion: 6.5.10-orbstack-00110-gbcfe04c86d2f + kubeProxyVersion: v1.21.1 + kubeletVersion: v1.21.1 + operatingSystem: linux + osImage: Ubuntu 20.10 +- apiVersion: v1 + kind: Node + metadata: + labels: + beta.kubernetes.io/arch: arm64 + beta.kubernetes.io/os: linux + kubernetes.io/arch: arm64 + kubernetes.io/hostname: n3 + kubernetes.io/os: linux + name: n3 + spec: + status: + conditions: + - message: kubelet has sufficient memory available + reason: KubeletHasSufficientMemory + status: "True" + type: MemoryPressure + - message: kubelet has no disk pressure + reason: KubeletHasNoDiskPressure + status: "True" + type: DiskPressure + - message: kubelet has sufficient PID available + reason: KubeletHasSufficientPID + status: "True" + type: PIDPressure + - message: kubelet is posting ready status + reason: KubeletReady + status: "True" + type: Ready + - message: blee + reason: blah + status: "True" + type: NetworkUnavailable + images: + nodeInfo: +- apiVersion: v1 + kind: Node + metadata: + labels: + beta.kubernetes.io/arch: arm64 + beta.kubernetes.io/os: linux + kubernetes.io/arch: arm64 + kubernetes.io/hostname: n4 + kubernetes.io/os: linux + name: n4 + spec: + unschedulable: true + status: + conditions: + - message: bla + reason: blee + status: Unknown + type: "" + - message: bla + reason: blee + status: "False" + type: Ready + images: + nodeInfo: +- apiVersion: v1 + kind: Node + metadata: + labels: + beta.kubernetes.io/arch: arm64 + beta.kubernetes.io/os: linux + kubernetes.io/arch: arm64 + kubernetes.io/hostname: n5 + kubernetes.io/os: linux + name: n5 + spec: + status: + images: + nodeInfo: + allocatable: + cpu: "100m" + ephemeral-storage: 816748224Ki + memory: 10Mi + pods: "110" + capacity: + cpu: "1" + ephemeral-storage: 816748224Ki + memory: 10Mi + pods: "110" diff --git a/internal/lint/testdata/core/ns/1.yaml b/internal/lint/testdata/core/ns/1.yaml new file mode 100644 index 00000000..cd63ee8e --- /dev/null +++ b/internal/lint/testdata/core/ns/1.yaml @@ -0,0 +1,34 @@ +apiVersion: v1 +kind: List +items: +- apiVersion: v1 + kind: Namespace + metadata: + name: default + labels: + ns: default + spec: + finalizers: + - kubernetes + status: + phase: Active +- apiVersion: v1 + kind: Namespace + metadata: + name: ns1 + labels: + app: ns1 + spec: + finalizers: + - kubernetes + status: + phase: Active +- apiVersion: v1 + kind: Namespace + metadata: + name: ns2 + labels: + app: ns2 + spec: + finalizers: + - kubernetes \ No newline at end of file diff --git a/internal/lint/testdata/core/pod/1.yaml b/internal/lint/testdata/core/pod/1.yaml new file mode 100644 index 00000000..fe35bd0a --- /dev/null +++ b/internal/lint/testdata/core/pod/1.yaml @@ -0,0 +1,71 @@ +--- +apiVersion: v1 +kind: List +items: +- apiVersion: v1 + kind: Pod + metadata: + name: p1 + namespace: default + labels: + app: p1 + spec: + serviceAccountName: default + tolerations: + - key: t1 + operator: Exists + effect: NoSchedule + containers: + - name: c1 + image: alpine + resources: + requests: + cpu: 1 + memory: 1Mi + limits: + cpu: 1 + memory: 1Mi + ports: + - containerPort: 9090 + name: http + protocol: TCP + env: + - name: env1 + valueFrom: + configMapKeyRef: + name: cm1 + key: ns + - name: env2 + valueFrom: + secretKeyRef: + name: sec1 + key: k1 + volumeMounts: + - name: config + mountPath: "/config" + readOnly: true + volumes: + - name: mypd + persistentVolumeClaim: + claimName: pvc1 + - name: config + configMap: + name: cm3 + items: + - key: k1 + path: "game.properties" + - key: k2 + path: "user-interface.properties" + - name: secret + secret: + secretName: sec1 + optional: false + items: + - key: ca.crt + path: "game.properties" + - key: namespace + path: "user-interface.properties" + status: + podIPs: + - ip: 172.1.0.3 + # - ip: 172.1.4.5 \ No newline at end of file diff --git a/internal/lint/testdata/core/pod/2.yaml b/internal/lint/testdata/core/pod/2.yaml new file mode 100644 index 00000000..933ac556 --- /dev/null +++ b/internal/lint/testdata/core/pod/2.yaml @@ -0,0 +1,178 @@ +--- +apiVersion: v1 +kind: List +items: + - apiVersion: v1 + kind: Pod + metadata: + name: p1 + namespace: default + labels: + app: p1 + ownerReferences: + - apiVersion: apps/v1 + controller: true + kind: ReplicaSet + name: rs1 + spec: + serviceAccountName: sa1 + automountServiceAccountToken: false + status: + conditions: + phase: Running + - apiVersion: v1 + kind: Pod + metadata: + name: p2 + namespace: default + labels: + app: test2 + spec: + serviceAccountName: sa2 + - apiVersion: v1 + kind: Pod + metadata: + name: p3 + namespace: default + labels: + app: p3 + ownerReferences: + - apiVersion: apps/v1 + controller: true + kind: DaemonSet + name: rs3 + spec: + serviceAccountName: sa3 + containers: + - image: dorker.io/blee:1.0.1 + name: c1 + resources: + limits: + cpu: 1 + mem: 1Mi + livenessProbe: + httpGet: + path: /healthz + port: 8080 + initialDelaySeconds: 3 + periodSeconds: 3 + readinessProbe: + httpGet: + path: /healthz + port: 8080 + initialDelaySeconds: 3 + periodSeconds: 3 + status: + conditions: + - status: "False" + type: Initialized + - status: "False" + type: Ready + - status: "False" + type: ContainersReady + - status: "False" + type: PodScheduled + phase: Running + - apiVersion: v1 + kind: Pod + metadata: + name: p4 + namespace: default + labels: + app: test4 + ownerReferences: + - apiVersion: apps/v1 + controller: false + kind: Job + name: j4 + spec: + serviceAccountName: default + automountServiceAccountToken: true + initContainers: + - image: zorg + imagePullPolicy: IfNotPresent + name: ic1 + containers: + - image: blee + imagePullPolicy: IfNotPresent + name: c1 + resources: + limits: + cpu: 1 + mem: 1Mi + livenessProbe: + httpGet: + path: /healthz + port: 8080 + initialDelaySeconds: 3 + periodSeconds: 3 + volumeMounts: + - mountPath: /etc/config + name: config-volume + readOnly: true + - mountPath: /var/run/secrets/kubernetes.io/serviceaccount + name: kube-api-access-jgtlv + readOnly: true + - image: zorg:latest + imagePullPolicy: IfNotPresent + name: c2 + resources: + requests: + mem: 1Mi + readinessProbe: + httpGet: + path: /healthz + port: p1 + initialDelaySeconds: 3 + periodSeconds: 3 + volumeMounts: + - mountPath: /etc/config + name: config-volume + readOnly: true + - mountPath: /var/run/secrets/kubernetes.io/serviceaccount + name: kube-api-access-jgtlv + readOnly: true + status: + phase: Running + conditions: + initContainerStatuses: + - containerID: ic1 + image: blee + name: ic1 + ready: false + restartCount: 1000 + started: false + containerStatuses: + - containerID: c1 + image: blee + name: c1 + ready: false + restartCount: 1000 + started: false + - containerID: c2 + name: c2 + ready: true + restartCount: 0 + started: true + - apiVersion: v1 + kind: Pod + metadata: + name: p5 + namespace: default + labels: + app: test5 + ownerReferences: + - apiVersion: apps/v1 + controller: true + kind: ReplicaSet + name: rs5 + spec: + serviceAccountName: sa5 + automountServiceAccountToken: true + containers: + - image: blee:v1.2 + imagePullPolicy: IfNotPresent + name: c1 + status: + conditions: + phase: Running diff --git a/internal/lint/testdata/core/pod/3.yaml b/internal/lint/testdata/core/pod/3.yaml new file mode 100644 index 00000000..49b1419f --- /dev/null +++ b/internal/lint/testdata/core/pod/3.yaml @@ -0,0 +1,124 @@ +--- +apiVersion: v1 +kind: List +items: +- apiVersion: v1 + kind: Pod + metadata: + name: p1 + namespace: ns1 + labels: + app: p1 + ownerReferences: + - apiVersion: apps/v1 + controller: true + kind: DaemonSet + name: rs3 + spec: + serviceAccountName: sa1 + tolerations: + - key: t1 + operator: Exists + effect: NoSchedule + containers: + - name: c1 + image: alpine:v1.0 + livenessProbe: + httpGet: + path: /healthz + port: http + initialDelaySeconds: 3 + periodSeconds: 3 + readinessProbe: + httpGet: + path: /healthz + port: http + initialDelaySeconds: 3 + periodSeconds: 3 + resources: + requests: + cpu: 1 + memory: 1Mi + limits: + cpu: 1 + memory: 1Mi + ports: + - containerPort: 9090 + name: http + protocol: TCP + env: + - name: env1 + valueFrom: + configMapKeyRef: + name: cm1 + key: ns + - name: env2 + valueFrom: + secretKeyRef: + name: sec1 + key: k1 + status: + conditions: + phase: Running + podIPs: + - ip: 172.1.0.3 +- apiVersion: v1 + kind: Pod + metadata: + name: p2 + namespace: ns2 + labels: + app: p2 + ownerReferences: + - apiVersion: apps/v1 + controller: true + kind: DaemonSet + name: rs3 + spec: + serviceAccountName: sa2 + tolerations: + - key: t1 + operator: Exists + effect: NoSchedule + containers: + - name: c1 + image: alpine:v1.0 + livenessProbe: + httpGet: + path: /healthz + port: http + initialDelaySeconds: 3 + periodSeconds: 3 + readinessProbe: + httpGet: + path: /healthz + port: http + initialDelaySeconds: 3 + periodSeconds: 3 + resources: + requests: + cpu: 1 + memory: 1Mi + limits: + cpu: 1 + memory: 1Mi + ports: + - containerPort: 9090 + name: http + protocol: TCP + env: + - name: env1 + valueFrom: + configMapKeyRef: + name: cm1 + key: ns + - name: env2 + valueFrom: + secretKeyRef: + name: sec1 + key: k1 + status: + conditions: + phase: Running + podIPs: + - ip: 172.1.0.3 \ No newline at end of file diff --git a/internal/lint/testdata/core/pv/1.yaml b/internal/lint/testdata/core/pv/1.yaml new file mode 100644 index 00000000..bf74972f --- /dev/null +++ b/internal/lint/testdata/core/pv/1.yaml @@ -0,0 +1,124 @@ +apiVersion: v1 +kind: List +items: +- apiVersion: v1 + kind: PersistentVolume + metadata: + name: pv1 + namespace: default + spec: + accessModes: + - ReadWriteOnce + capacity: + storage: 2Gi + claimRef: + apiVersion: v1 + kind: PersistentVolumeClaim + name: pvc1 + namespace: default + nodeAffinity: + required: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/hostname + operator: In + values: + - n1 + persistentVolumeReclaimPolicy: Delete + storageClassName: standard + volumeMode: Filesystem + status: + phase: Bound +- apiVersion: v1 + kind: PersistentVolume + metadata: + name: pv2 + namespace: default + spec: + accessModes: + - ReadWriteOnce + capacity: + storage: 8Gi + claimRef: + apiVersion: v1 + kind: PersistentVolumeClaim + name: pv2 + namespace: default + hostPath: + path: /var/blee + type: DirectoryOrCreate + nodeAffinity: + required: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/hostname + operator: In + values: + - n2 + persistentVolumeReclaimPolicy: Delete + storageClassName: standard + volumeMode: Filesystem + status: + phase: Failed +- apiVersion: v1 + kind: PersistentVolume + metadata: + name: pv3 + namespace: default + spec: + accessModes: + - ReadWriteOnce + capacity: + storage: 8Gi + claimRef: + apiVersion: v1 + kind: PersistentVolumeClaim + name: pv3 + namespace: default + hostPath: + path: /var/blee + type: DirectoryOrCreate + nodeAffinity: + required: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/hostname + operator: In + values: + - n3 + persistentVolumeReclaimPolicy: Delete + storageClassName: standard + volumeMode: Filesystem + status: + phase: Available +- apiVersion: v1 + kind: PersistentVolume + metadata: + name: pv4 + namespace: default + spec: + accessModes: + - ReadWriteOnce + capacity: + storage: 8Gi + claimRef: + apiVersion: v1 + kind: PersistentVolumeClaim + name: pv4 + namespace: default + hostPath: + path: /var/blee + type: DirectoryOrCreate + nodeAffinity: + required: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/hostname + operator: In + values: + - n4 + persistentVolumeReclaimPolicy: Delete + storageClassName: standard + volumeMode: Filesystem + status: + phase: Pending diff --git a/internal/lint/testdata/core/pvc/1.yaml b/internal/lint/testdata/core/pvc/1.yaml new file mode 100644 index 00000000..6cae0c8d --- /dev/null +++ b/internal/lint/testdata/core/pvc/1.yaml @@ -0,0 +1,91 @@ +apiVersion: v1 +kind: List +items: +- apiVersion: v1 + kind: PersistentVolumeClaim + metadata: + annotations: + pv.kubernetes.io/bind-completed: "yes" + pv.kubernetes.io/bound-by-controller: "yes" + volume.kubernetes.io/selected-node: n1 + finalizers: + - kubernetes.io/pvc-protection + name: pvc1 + namespace: default + spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 2Gi + storageClassName: standard + volumeMode: Filesystem + volumeName: pvc-5a8a78fd-cc3c-4838-ab0b-2b1a475f555d + status: + accessModes: + - ReadWriteOnce + capacity: + storage: 2Gi + phase: Bound +- apiVersion: v1 + kind: PersistentVolumeClaim + metadata: + annotations: + pv.kubernetes.io/bind-completed: "yes" + pv.kubernetes.io/bound-by-controller: "yes" + volume.kubernetes.io/selected-node: n2 + finalizers: + - kubernetes.io/pvc-protection + labels: + app.kubernetes.io/component: server + app.kubernetes.io/instance: prom + app.kubernetes.io/name: prometheus + name: pvc2 + namespace: default + resourceVersion: "861" + spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 8Gi + storageClassName: standard + volumeMode: Filesystem + volumeName: pvc-86489da2-08df-4e95-800c-b8537e3ff03b + status: + accessModes: + - ReadWriteOnce + capacity: + storage: 8Gi + phase: Lost +- apiVersion: v1 + kind: PersistentVolumeClaim + metadata: + annotations: + pv.kubernetes.io/bind-completed: "yes" + pv.kubernetes.io/bound-by-controller: "yes" + volume.kubernetes.io/selected-node: n3 + finalizers: + - kubernetes.io/pvc-protection + labels: + app.kubernetes.io/component: server + app.kubernetes.io/instance: prom + app.kubernetes.io/name: prometheus + name: pvc3 + namespace: default + resourceVersion: "861" + spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 8Gi + storageClassName: standard + volumeMode: Filesystem + volumeName: pvc-86489da2-08df-4e95-800c-b8537e3ff03b + status: + accessModes: + - ReadWriteOnce + capacity: + storage: 8Gi + phase: Pending diff --git a/internal/lint/testdata/core/sa/1.yaml b/internal/lint/testdata/core/sa/1.yaml new file mode 100644 index 00000000..6ce3ea21 --- /dev/null +++ b/internal/lint/testdata/core/sa/1.yaml @@ -0,0 +1,53 @@ +--- +apiVersion: v1 +kind: List +items: + - apiVersion: v1 + kind: ServiceAccount + metadata: + name: default + namespace: default + - apiVersion: v1 + kind: ServiceAccount + metadata: + name: sa1 + namespace: default + automountServiceAccountToken: false + - apiVersion: v1 + kind: ServiceAccount + metadata: + name: sa2 + namespace: default + automountServiceAccountToken: true + - apiVersion: v1 + kind: ServiceAccount + metadata: + name: sa3 + namespace: default + automountServiceAccountToken: true + - apiVersion: v1 + kind: ServiceAccount + metadata: + name: sa4 + namespace: default + automountServiceAccountToken: false + secrets: + - kind: Secret + namespace: default + name: bozo + apiVersion: v1 + imagePullSecrets: + - name: s1 + - apiVersion: v1 + kind: ServiceAccount + metadata: + name: sa5 + namespace: default + automountServiceAccountToken: false + secrets: + - kind: Secret + namespace: default + name: s1 + apiVersion: v1 + imagePullSecrets: + - name: bozo diff --git a/internal/lint/testdata/core/sa/2.yaml b/internal/lint/testdata/core/sa/2.yaml new file mode 100644 index 00000000..a71e07c1 --- /dev/null +++ b/internal/lint/testdata/core/sa/2.yaml @@ -0,0 +1,15 @@ +--- +apiVersion: v1 +kind: List +items: + - apiVersion: v1 + kind: ServiceAccount + metadata: + name: sa1 + namespace: ns1 + - apiVersion: v1 + kind: ServiceAccount + metadata: + name: sa2 + namespace: ns2 + automountServiceAccountToken: false \ No newline at end of file diff --git a/internal/lint/testdata/core/secret/1.yaml b/internal/lint/testdata/core/secret/1.yaml new file mode 100644 index 00000000..e5c2cce0 --- /dev/null +++ b/internal/lint/testdata/core/secret/1.yaml @@ -0,0 +1,34 @@ +apiVersion: v1 +kind: List +items: +- apiVersion: v1 + data: + ca.crt: blee + ns: zorg + kind: Secret + metadata: + annotations: + kubernetes.io/service-account.name: default + name: sec1 + namespace: default + type: kubernetes.io/service-account-token +- apiVersion: v1 + data: + admin-password: zorg + admin-user: blee + kind: Secret + metadata: + labels: + name: sec2 + namespace: default + type: Opaque +- apiVersion: v1 + data: + ca.crt: crap + namespace: zorg + kind: Secret + metadata: + annotations: + name: sec3 + namespace: default + type: kubernetes.io/service-account-token diff --git a/internal/lint/testdata/core/svc/1.yaml b/internal/lint/testdata/core/svc/1.yaml new file mode 100644 index 00000000..cc1ff25a --- /dev/null +++ b/internal/lint/testdata/core/svc/1.yaml @@ -0,0 +1,122 @@ +apiVersion: v1 +kind: List +items: +- apiVersion: v1 + kind: Service + metadata: + labels: + app: p1 + name: svc1 + namespace: default + spec: + clusterIP: 10.96.66.245 + clusterIPs: + - 10.96.66.245 + externalTrafficPolicy: Local + ipFamilies: + - IPv4 + ipFamilyPolicy: SingleStack + ports: + - name: http + nodePort: 30400 + port: 9090 + protocol: TCP + targetPort: 9090 + selector: + app: p1 + sessionAffinity: None + type: NodePort + status: + loadBalancer: {} +- apiVersion: v1 + kind: Service + metadata: + name: svc2 + namespace: default + spec: + clusterIP: 10.96.12.148 + clusterIPs: + - 10.96.12.148 + ipFamilies: + - IPv4 + ipFamilyPolicy: SingleStack + ports: + - name: service + port: 80 + protocol: TCP + targetPort: 3000 + selector: + app: p2 + sessionAffinity: None + type: ClusterIP + status: + loadBalancer: {} +- apiVersion: v1 + kind: Service + metadata: + name: svc3 + namespace: default + spec: + clusterIP: 10.96.12.148 + clusterIPs: + - 10.96.12.148 + ipFamilies: + - IPv4 + ipFamilyPolicy: SingleStack + ports: + - name: service + port: 80 + protocol: TCP + targetPort: 3000 + selector: + app: p2 + sessionAffinity: None + type: ExternalName + status: + loadBalancer: {} +- apiVersion: v1 + kind: Service + metadata: + name: svc4 + namespace: default + spec: + externalTrafficPolicy: Cluster + clusterIP: 10.96.12.148 + clusterIPs: + - 10.96.12.148 + ipFamilies: + - IPv4 + ipFamilyPolicy: SingleStack + ports: + - name: service + port: 80 + protocol: TCP + targetPort: 3000 + selector: + app: p4 + sessionAffinity: None + type: LoadBalancer + status: + loadBalancer: {} +- apiVersion: v1 + kind: Service + metadata: + name: svc5 + namespace: default + spec: + clusterIP: 10.96.12.148 + clusterIPs: + - 10.96.12.148 + ipFamilies: + - IPv4 + ipFamilyPolicy: SingleStack + ports: + - name: service + port: 80 + protocol: TCP + targetPort: 3000 + selector: + app: p5 + sessionAffinity: None + status: + loadBalancer: {} \ No newline at end of file diff --git a/internal/lint/testdata/mx/node/1.yaml b/internal/lint/testdata/mx/node/1.yaml new file mode 100644 index 00000000..2186d04c --- /dev/null +++ b/internal/lint/testdata/mx/node/1.yaml @@ -0,0 +1,18 @@ +apiVersion: v1 +kind: List +items: +- apiVersion: metrics.k8s.io/v1beta1 + kind: NodeMetrics + metadata: + name: n1 + usage: + cpu: 144965184n + memory: 770776Ki +- apiVersion: metrics.k8s.io/v1beta1 + kind: NodeMetrics + metadata: + name: n5 + usage: + cpu: 20 + memory: 40Mi + window: 20.101s diff --git a/internal/lint/testdata/mx/pod/1.yaml b/internal/lint/testdata/mx/pod/1.yaml new file mode 100644 index 00000000..d4112bfe --- /dev/null +++ b/internal/lint/testdata/mx/pod/1.yaml @@ -0,0 +1,39 @@ +apiVersion: v1 +kind: List +items: +- apiVersion: metrics.k8s.io/v1beta1 + kind: PodMetrics + metadata: + labels: + app: p1 + name: p1 + namespace: default + containers: + - name: c1 + usage: + cpu: 20 + memory: 20Mi +- apiVersion: metrics.k8s.io/v1beta1 + kind: PodMetrics + metadata: + labels: + app: p3 + name: p3 + namespace: default + containers: + - name: c1 + usage: + cpu: 2000m + memory: 20Mi +- apiVersion: metrics.k8s.io/v1beta1 + kind: PodMetrics + metadata: + labels: + app: j1 + name: j1 + namespace: default + containers: + - name: c1 + usage: + cpu: 2000m + memory: 20Mi \ No newline at end of file diff --git a/internal/lint/testdata/net/gw/1.yaml b/internal/lint/testdata/net/gw/1.yaml new file mode 100644 index 00000000..0ed8b602 --- /dev/null +++ b/internal/lint/testdata/net/gw/1.yaml @@ -0,0 +1,25 @@ +apiVersion: v1 +kind: List +items: +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + name: gw1 + namespace: default + spec: + gatewayClassName: gwc1 + listeners: + - name: http + protocol: HTTP + port: 80 +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + name: gw2 + namespace: default + spec: + gatewayClassName: gwc-bozo + listeners: + - name: http + protocol: HTTP + port: 80 \ No newline at end of file diff --git a/internal/lint/testdata/net/gwc/1.yaml b/internal/lint/testdata/net/gwc/1.yaml new file mode 100644 index 00000000..f292d46c --- /dev/null +++ b/internal/lint/testdata/net/gwc/1.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: List +items: +- apiVersion: gateway.networking.k8s.io/v1 + kind: GatewayClass + metadata: + name: gwc1 + spec: + controllerName: example.com/gateway-controller +- apiVersion: gateway.networking.k8s.io/v1 + kind: GatewayClass + metadata: + name: gwc2 + spec: + controllerName: example.com/gateway-controller \ No newline at end of file diff --git a/internal/lint/testdata/net/gwr/1.yaml b/internal/lint/testdata/net/gwr/1.yaml new file mode 100644 index 00000000..4c0c2143 --- /dev/null +++ b/internal/lint/testdata/net/gwr/1.yaml @@ -0,0 +1,58 @@ +apiVersion: v1 +kind: List +items: +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + name: r1 + namespace: default + spec: + parentRefs: + - name: gw1 + hostnames: + - fred + rules: + - matches: + - path: + type: PathPrefix + value: /blee + backendRefs: + - name: svc1 + port: 9090 +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + name: r2 + namespace: default + spec: + parentRefs: + - name: gw-bozo + hostnames: + - bozo + rules: + - matches: + - path: + type: PathPrefix + value: /zorg + backendRefs: + - name: svc2 + port: 8080 +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + name: r3 + namespace: default + spec: + parentRefs: + - kind: Service + name: svc-bozo + hostnames: + - bozo + rules: + - matches: + - path: + type: PathPrefix + value: /zorg + backendRefs: + - name: svc2 + port: 9090 \ No newline at end of file diff --git a/internal/lint/testdata/net/ingress/1.yaml b/internal/lint/testdata/net/ingress/1.yaml new file mode 100644 index 00000000..12160400 --- /dev/null +++ b/internal/lint/testdata/net/ingress/1.yaml @@ -0,0 +1,109 @@ +apiVersion: v1 +kind: List +items: +- apiVersion: networking.k8s.io/v1 + kind: Ingress + metadata: + name: ing1 + namespace: default + spec: + ingressClassName: nginx + rules: + - http: + paths: + - path: /testpath + pathType: Prefix + backend: + service: + name: svc1 + port: + name: http +- apiVersion: networking.k8s.io/v1 + kind: Ingress + metadata: + name: ing2 + namespace: default + spec: + ingressClassName: nginx + rules: + - http: + paths: + - path: /testpath + pathType: Prefix + backend: + service: + name: svc1 + port: + number: 9090 +- apiVersion: networking.k8s.io/v1 + kind: Ingress + metadata: + name: ing3 + namespace: default + spec: + ingressClassName: nginx + rules: + - http: + paths: + - path: /testpath + pathType: Prefix + backend: + service: + name: s2 + port: + number: 80 +- apiVersion: networking.k8s.io/v1 + kind: Ingress + metadata: + name: ing4 + namespace: default + spec: + ingressClassName: nginx + rules: + - http: + paths: + - path: /testpath + pathType: Prefix + backend: + service: + name: svc2 +- apiVersion: networking.k8s.io/v1 + kind: Ingress + metadata: + name: ing5 + namespace: default + annotations: + spec: + ingressClassName: nginx + rules: + - http: + paths: + - path: /testpath + pathType: Prefix + backend: + resource: + apiGroup: fred.com + kind: Zorg + name: zorg + status: + loadBalancer: + ingress: + - ports: + - error: boom +- apiVersion: networking.k8s.io/v1 + kind: Ingress + metadata: + name: ing6 + namespace: default + spec: + ingressClassName: nginx + rules: + - http: + paths: + - path: /testpath + pathType: Prefix + backend: + service: + name: svc1 + port: + number: 9091 diff --git a/internal/lint/testdata/net/np/1.yaml b/internal/lint/testdata/net/np/1.yaml new file mode 100644 index 00000000..e25a10c4 --- /dev/null +++ b/internal/lint/testdata/net/np/1.yaml @@ -0,0 +1,107 @@ +apiVersion: v1 +kind: List +items: +- apiVersion: networking.k8s.io/v1 + kind: NetworkPolicy + metadata: + name: np1 + namespace: default + spec: + podSelector: + matchLabels: + app: p1 + policyTypes: + - Ingress + - Egress + ingress: + - from: + - ipBlock: + cidr: 172.1.0.0/16 + except: + - 172.1.0.0/24 + - namespaceSelector: + matchLabels: + ns: default + podSelector: + matchLabels: + app: p1 + ports: + - protocol: TCP + port: 6379 + egress: + - to: + - ipBlock: + cidr: 172.1.0.0/16 + ports: + - protocol: TCP + port: 5978 +- apiVersion: networking.k8s.io/v1 + kind: NetworkPolicy + metadata: + name: np2 + namespace: default + spec: + ingress: + - from: + - ipBlock: + cidr: 172.1.0.0/16 + except: + - 172.1.1.0/24 + - namespaceSelector: + matchLabels: + app: ns2 + podSelector: + matchLabels: + app: p2 + ports: + - protocol: TCP + port: 6379 + egress: + - to: + - podSelector: + matchLabels: + app: p1 + - ipBlock: + cidr: 172.0.0.0/24 + ports: + - protocol: TCP + port: 5978 +- apiVersion: networking.k8s.io/v1 + kind: NetworkPolicy + metadata: + name: np3 + namespace: default + spec: + podSelector: + matchLabels: + app: p-bozo + ingress: + - from: + - ipBlock: + cidr: 172.2.0.0/16 + except: + - 172.2.1.0/24 + - namespaceSelector: + matchExpressions: + - key: app + operator: In + values: [ns-bozo] + podSelector: + matchLabels: + app: pod-bozo + ports: + - protocol: TCP + port: 6379 + egress: + - to: + - namespaceSelector: + matchLabels: + app: ns1 + - podSelector: + matchLabels: + app: p1-missing + - ipBlock: + cidr: 172.1.0.0/24 + ports: + - protocol: TCP + port: 5978 \ No newline at end of file diff --git a/internal/lint/testdata/net/np/2.yaml b/internal/lint/testdata/net/np/2.yaml new file mode 100644 index 00000000..0d088d36 --- /dev/null +++ b/internal/lint/testdata/net/np/2.yaml @@ -0,0 +1,101 @@ +apiVersion: v1 +kind: List +items: +- apiVersion: networking.k8s.io/v1 + kind: NetworkPolicy + metadata: + name: deny-all + namespace: default + spec: + podSelector: {} + policyTypes: + - Ingress + - Egress +- apiVersion: networking.k8s.io/v1 + kind: NetworkPolicy + metadata: + name: deny-all-ing + namespace: default + spec: + podSelector: {} + policyTypes: + - Ingress +- apiVersion: networking.k8s.io/v1 + kind: NetworkPolicy + metadata: + name: deny-all-eg + namespace: default + spec: + podSelector: {} + policyTypes: + - Egress +- apiVersion: networking.k8s.io/v1 + kind: NetworkPolicy + metadata: + name: allow-all + namespace: default + spec: + podSelector: {} + ingress: + - {} + egress: + - {} + policyTypes: + - Ingress + - Egress +- apiVersion: networking.k8s.io/v1 + kind: NetworkPolicy + metadata: + name: allow-all-ing + namespace: default + spec: + podSelector: {} + ingress: + - {} + policyTypes: + - Ingress +- apiVersion: networking.k8s.io/v1 + kind: NetworkPolicy + metadata: + name: allow-all-eg + namespace: default + spec: + podSelector: {} + egress: + - {} + policyTypes: + - Egress +- apiVersion: networking.k8s.io/v1 + kind: NetworkPolicy + metadata: + name: ip-block-all-ing + namespace: default + spec: + podSelector: {} + egress: + - to: + - ipBlock: + cidr: 172.2.0.0/24 + ports: + - protocol: TCP + port: 5978 + policyTypes: + - Ingress + - Egress +- apiVersion: networking.k8s.io/v1 + kind: NetworkPolicy + metadata: + name: ip-block-all-eg + namespace: default + spec: + podSelector: {} + ingress: + - from: + - ipBlock: + cidr: 172.2.0.0/24 + ports: + - protocol: TCP + port: 5978 + policyTypes: + - Ingress + - Egress \ No newline at end of file diff --git a/internal/lint/testdata/net/np/3.yaml b/internal/lint/testdata/net/np/3.yaml new file mode 100644 index 00000000..3ae4bad3 --- /dev/null +++ b/internal/lint/testdata/net/np/3.yaml @@ -0,0 +1,37 @@ +apiVersion: v1 +kind: List +items: +- apiVersion: networking.k8s.io/v1 + kind: NetworkPolicy + metadata: + name: deny-all-ing + namespace: ns1 + spec: + podSelector: {} + policyTypes: + - Ingress +- apiVersion: networking.k8s.io/v1 + kind: NetworkPolicy + metadata: + name: allow-all-egress + namespace: ns2 + spec: + # podSelector: + # matchLabels: + # app: p2 + ingress: + - from: + - namespaceSelector: + matchLabels: + app: ns2 + podSelector: + matchLabels: + app: p2 + - podSelector: + matchLabels: + app: p2 + egress: + - {} + policyTypes: + - Ingress + - Egress \ No newline at end of file diff --git a/internal/lint/testdata/net/np/a.yaml b/internal/lint/testdata/net/np/a.yaml new file mode 100644 index 00000000..f806268b --- /dev/null +++ b/internal/lint/testdata/net/np/a.yaml @@ -0,0 +1,34 @@ +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: test-network-policy + namespace: default +spec: + podSelector: + matchLabels: + role: db + policyTypes: + - Ingress + - Egress + ingress: + - from: + - ipBlock: + cidr: 172.17.0.0/16 + except: + - 172.17.1.0/24 + - namespaceSelector: + matchLabels: + project: myproject + - podSelector: + matchLabels: + role: frontend + ports: + - protocol: TCP + port: 6379 + egress: + - to: + - ipBlock: + cidr: 10.0.0.0/24 + ports: + - protocol: TCP + port: 5978 \ No newline at end of file diff --git a/internal/lint/testdata/net/np/allow-all-egr.yaml b/internal/lint/testdata/net/np/allow-all-egr.yaml new file mode 100644 index 00000000..80e532a4 --- /dev/null +++ b/internal/lint/testdata/net/np/allow-all-egr.yaml @@ -0,0 +1,10 @@ +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: allow-all-egress +spec: + podSelector: {} + egress: + - {} + policyTypes: + - Egress \ No newline at end of file diff --git a/internal/lint/testdata/net/np/allow-all-ing.yaml b/internal/lint/testdata/net/np/allow-all-ing.yaml new file mode 100644 index 00000000..1c2e7be1 --- /dev/null +++ b/internal/lint/testdata/net/np/allow-all-ing.yaml @@ -0,0 +1,10 @@ +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: allow-all-egress +spec: + podSelector: {} + egress: + - {} + policyTypes: + - Ingress \ No newline at end of file diff --git a/internal/lint/testdata/net/np/b.yaml b/internal/lint/testdata/net/np/b.yaml new file mode 100644 index 00000000..ddb47718 --- /dev/null +++ b/internal/lint/testdata/net/np/b.yaml @@ -0,0 +1,34 @@ +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: np-b + namespace: default +spec: + ingress: + - from: + - ipBlock: + cidr: 172.17.0.0/16 + except: + - 172.17.1.0/24 + - namespaceSelector: + matchLabels: + ns: ns1 + - podSelector: + matchLabels: + po: po1 + ports: + - protocol: TCP + port: 6379 + egress: + - to: + - namespaceSelector: + matchLabels: + ns: ns1 + - podSelector: + matchLabels: + po: po1 + - ipBlock: + cidr: 10.0.0.0/24 + ports: + - protocol: TCP + port: 5978 \ No newline at end of file diff --git a/internal/lint/testdata/net/np/c.yaml b/internal/lint/testdata/net/np/c.yaml new file mode 100644 index 00000000..5ef86a89 --- /dev/null +++ b/internal/lint/testdata/net/np/c.yaml @@ -0,0 +1,34 @@ +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: np-c + namespace: default +spec: + ingress: + - from: + - ipBlock: + cidr: 172.17.0.0/16 + except: + - 172.17.1.0/24 + - namespaceSelector: + matchLabels: + ns: ns1 + - podSelector: + matchLabels: + po: p1-missing + ports: + - protocol: TCP + port: 6379 + egress: + - to: + - namespaceSelector: + matchLabels: + ns: ns1 + - podSelector: + matchLabels: + po: p1-missing + - ipBlock: + cidr: 10.0.0.0/24 + ports: + - protocol: TCP + port: 5978 \ No newline at end of file diff --git a/internal/lint/testdata/net/np/d.yaml b/internal/lint/testdata/net/np/d.yaml new file mode 100644 index 00000000..4d03a99f --- /dev/null +++ b/internal/lint/testdata/net/np/d.yaml @@ -0,0 +1,13 @@ +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: np-d + namespace: default +spec: + podSelector: + podSelector: + matchLabels: + role: db + policyTypes: + - Ingress + - Egress \ No newline at end of file diff --git a/internal/lint/testdata/net/np/deny-all-egr.yaml b/internal/lint/testdata/net/np/deny-all-egr.yaml new file mode 100644 index 00000000..6be3087c --- /dev/null +++ b/internal/lint/testdata/net/np/deny-all-egr.yaml @@ -0,0 +1,8 @@ +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: deny-all-eg +spec: + podSelector: {} + policyTypes: + - Egress \ No newline at end of file diff --git a/internal/lint/testdata/net/np/deny-all-ing.yaml b/internal/lint/testdata/net/np/deny-all-ing.yaml new file mode 100644 index 00000000..ba04fc9a --- /dev/null +++ b/internal/lint/testdata/net/np/deny-all-ing.yaml @@ -0,0 +1,8 @@ +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: deny-all-ing +spec: + podSelector: {} + policyTypes: + - Ingress \ No newline at end of file diff --git a/internal/lint/testdata/net/np/deny-all.yaml b/internal/lint/testdata/net/np/deny-all.yaml new file mode 100644 index 00000000..a7983980 --- /dev/null +++ b/internal/lint/testdata/net/np/deny-all.yaml @@ -0,0 +1,9 @@ +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: deny-all +spec: + podSelector: {} + policyTypes: + - Ingress + - Egress \ No newline at end of file diff --git a/internal/lint/testdata/pol/pdb/1.yaml b/internal/lint/testdata/pol/pdb/1.yaml new file mode 100644 index 00000000..e9a49fa0 --- /dev/null +++ b/internal/lint/testdata/pol/pdb/1.yaml @@ -0,0 +1,54 @@ +--- +apiVersion: v1 +kind: List +items: + - apiVersion: policy/v1 + kind: PodDisruptionBudget + metadata: + name: pdb1 + namespace: default + spec: + minAvailable: 2 + selector: + matchLabels: + app: p1 + - apiVersion: policy/v1 + kind: PodDisruptionBudget + metadata: + name: pdb2 + namespace: default + spec: + minAvailable: 1 + selector: + matchLabels: + app: p2 + - apiVersion: policy/v1 + kind: PodDisruptionBudget + metadata: + name: pdb3 + namespace: default + spec: + minAvailable: 1 + selector: + matchLabels: + app: test4 + - apiVersion: policy/v1 + kind: PodDisruptionBudget + metadata: + name: pdb4 + namespace: default + spec: + minAvailable: 1 + selector: + matchLabels: + app: test5 + - apiVersion: policy/v1 + kind: PodDisruptionBudget + metadata: + name: pdb4-1 + namespace: default + spec: + minAvailable: 1 + selector: + matchLabels: + app: test5 diff --git a/internal/sanitize/types.go b/internal/lint/types.go similarity index 85% rename from internal/sanitize/types.go rename to internal/lint/types.go index 11b073d9..86fc6e2f 100644 --- a/internal/sanitize/types.go +++ b/internal/lint/types.go @@ -1,12 +1,13 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright Authors of Popeye -package sanitize +package lint import ( "context" "github.com/derailed/popeye/internal/issues" + "github.com/derailed/popeye/internal/rules" "github.com/derailed/popeye/pkg/config" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -19,10 +20,13 @@ type Collector interface { Outcome() issues.Outcome // AddSubCode records a sub issue. - AddSubCode(ctx context.Context, id config.ID, args ...interface{}) + AddSubCode(context.Context, rules.ID, ...interface{}) // AddCode records a new issue. - AddCode(ctx context.Context, id config.ID, args ...interface{}) + AddCode(context.Context, rules.ID, ...interface{}) + + // AddErr records errors. + AddErr(context.Context, ...error) } // PodsMetricsLister handles pods metrics. diff --git a/internal/report/html.go b/internal/report/assets/report.html similarity index 64% rename from internal/report/html.go rename to internal/report/assets/report.html index 08a52914..ea2df526 100644 --- a/internal/report/html.go +++ b/internal/report/assets/report.html @@ -1,12 +1,6 @@ -// SPDX-License-Identifier: Apache-2.0 -// Copyright Authors of Popeye - -package report - -var htmlTemplate = ` -