diff --git a/README.md b/README.md index 311d082..95813bb 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,201 @@ # Description An autodiscovery tool to help you know what and when to update. -Features: - + automatically discover current software version - + automatically discover newer versions for the softwares - + calculate the obsolescence score SLI + +![boat](doc/image/grafana-dashboard.png "dashboard") +*Grafana Dashboard displaying upgrade-manager's metrics, pointing out which apps need to be upgrading and their associated versions* + +Key Features: ++ **Service discovery**: finds softwares deployed (see software sources supported) ++ **Obsolescence Score SLI (Service Level Indicator) Calculation**: compute each software’s obsolescence score and expose it as a prometheus metric ++ **New eligible release detection**: automatically find the version corresponding to a tailor-made selection logic + + + +### Managing IT system obsolescence is like being in a boat full of holes +![boat](doc/image/boat.png "Boat") + +Let's pretend your IT systems are a boat: ++ Each software is a new hole in the boat. ++ Each hole gets wider and wider as new software versions are released. ++ In such a situation, you need to decide: + + When to patch a hole (meaning updating softwares) in order for the boat not to sink. + + It means you need to define the minimum size (diameter) of a hole in order for the crew to consider patching it + + When to row (do projects delivering business value) in order for the boat to make progress. + + Which hole to patch first, and it probably makes sense to patch the widest. + + It means you need a simple way to quickly get the list of holes that are the widest. + +### How upgrade-manager responds to this issue +In real life, a software's obsolescence (how big the hole is) is hard to measure, since we don't have a clear metric to quantify it. + +This is why upgrade-manager computes an **obsolescence score**, exposing it as a Prometheus metric: ++ It represents **how obsolete a system** is, based on different calculation strategies (see more about calculators). It is essentially an indicator to measure the obsolescence at an software-level ++ It can be summed / averaged across all apps to have a company-wide freshness/obsolescence metric. + +## Installation +You can use the default Helm chart to deploy upgrade-manager as follows: + +```bash +helm repo add qonto oci://public.ecr.aws/qonto +helm install upgrade-manager qonto/upgrade-manager-chart \ + -n upgrade-manager --create-namespace +``` + +## Configuration +upgrade-manager uses yaml config file (the default location is `/app/config/config.yaml`). + +For more information about specific sources' configuration block, see [sources](./doc/sources/README.md) + +```yaml +global: + interval: 10m # How often upgrade-manager should run the main loop (discovering all softwares, their new versions and compute scores) + aws: + region: us-east-1 # AWS region to look for resources in +sources: + deployments: + - + argocdHelm: + - + filesystemHelm: + - + aws: + eks: + rds: + msk: + elasticache: + lambda: +http: + host: 0.0.0.0 # local server address + port: 10000 # local port to listen on + write-timeout: 10 + read-timeout: 10 + read-header-timeout: 10 +``` + +## Required AWS Privileges when running AWS sources +To automatically discover AWS resources and their newer versions, upgrade-manager needs the following AWS privileges (represented in Terraform HCL): +```json +data "aws_iam_policy_document" "upgrade-manager" { + statement { + sid = "elasticache" + + actions = [ + "elasticache:DescribeCacheClusters", + "elasticache:DescribeCacheEngineVersions", + ] + + resources = [ + "*", + ] + } + statement { + sid = "ecr" + + actions = [ + "ecr:ListImages", + "ecr:GetAuthorizationToken", + "ecr:BatchGetImage", + "ecr:GetDownloadUrlForLayer", + ] + + resources = [ + "*", + ] + } + statement { + sid = "eks" + + actions = [ + "eks:DescribeAddon", + "eks:DescribeAddonVersions", + "eks:ListClusters", + "eks:DescribeCluster", + "eks:ListAddons", + ] + + resources = [ + "*", + ] + } + statement { + sid = "rds" + + actions = [ + "rds:DescribeDBInstances", + "rds:DescribeDBEngineVersions", + ] + + resources = [ + "*", + ] + } + statement { + sid = "lambda" + + actions = [ + "lambda:ListFunctions", + ] + + resources = [ + "*", + ] + } + statement { + sid = "kafka" + + actions = [ + "kafka:ListClustersV2", + "kafka:GetCompatibleKafkaVersions", + ] + + resources = [ + "*", + ] + } +} + +``` + + +## Problem Statement: why use upgrade-manager? + +As we scale, we deploy more and more softwares. We end up with hundreds of softwares to maintain across many different platforms. + +Each of this softwares need to be updated regularly to benefit from security fixes, new features etc. + + +## Dashboard +To visualize metrics, you can use the dashboard available in the `dashboard/` directory + +## Alerting Patterns: deciding when to update softwares +As SREs, we like to define SLOs/SLAs based on SLIs. Among other things, it helps us take actions based on specific thresholds. +Using our **obsolescence score** metric, we can now define thresholds to help us decide when we need to act and update softwares. + +Each company has its own way to define alerting rules, but a sane default could be: + +1. The obsolescence score for each individual software should be < 100 +```yaml +- alert: ObsolecenceScoreTooHigh + expr: upgrade_manager_software_obsolescence_score{isparent="1"} > 99 + for: 5m + labels: + team: foo + annotations: + summary: "The software {{ $labels.app }} is obsolete (>99) and needs to be updated" + runbook_url: https://letmegooglethat.com/?q=how+to+update+softwares +``` + +2. The average obsolescence score for all apps should be < 80 +```yaml +- alert: AverageObsolecenceScoreTooHigh + expr: avg(upgrade_manager_software_obsolescence_score{isparent="1"}) < 80 + for: 5m + labels: + team: foo + annotations: + summary: "The average obsolescence score across all softwares is too high, softwares need to be updated" + runbook_url: https://letmegooglethat.com/?q=how+to+update+softwares +``` +![boat](doc/image/score-based-alerting.png "score-based alerting") + +When an alert is triggered, an engineer should acknowledge it and upgrade the applications with the highest obsolescence scores. diff --git a/dashboards/default.json b/dashboards/default.json new file mode 100644 index 0000000..d5f06af --- /dev/null +++ b/dashboards/default.json @@ -0,0 +1,1282 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": 576, + "links": [], + "liveNow": false, + "panels": [ + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 14, + "panels": [], + "title": "Per-App Metrics (app score >0)", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "#EAB839", + "value": 75 + }, + { + "color": "red", + "value": 100 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 2, + "x": 0, + "y": 1 + }, + "id": 6, + "maxPerRow": 12, + "options": { + "colorMode": "background", + "graphMode": "none", + "justifyMode": "center", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "text": { + "titleSize": 14, + "valueSize": 25 + }, + "textMode": "value", + "wideLayout": true + }, + "pluginVersion": "10.2.2", + "repeat": "app", + "repeatDirection": "h", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "upgrade_manager_software_obsolescence_score{isparent=\"1\", app=~\"$app\", app_type=~\"$type\"}", + "format": "table", + "instant": true, + "interval": "", + "legendFormat": "{{app}}", + "range": false, + "refId": "A" + } + ], + "title": "$app", + "transformations": [ + { + "id": "organize", + "options": {} + } + ], + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "center", + "cellOptions": { + "type": "auto" + }, + "filterable": true, + "inspect": false + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "#EAB839", + "value": 75 + }, + { + "color": "red", + "value": 100 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "__name__" + }, + "properties": [ + { + "id": "custom.width", + "value": 381 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "app" + }, + "properties": [ + { + "id": "custom.width", + "value": 238 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "action" + }, + "properties": [ + { + "id": "custom.width", + "value": 163 + }, + { + "id": "custom.cellOptions", + "value": { + "type": "auto" + } + }, + { + "id": "mappings", + "value": [ + { + "options": { + "": { + "color": "semi-dark-green", + "index": 0, + "text": "nothing :)" + } + }, + "type": "value" + } + ] + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "parent app" + }, + "properties": [ + { + "id": "custom.width", + "value": 217 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "obsolescence score" + }, + "properties": [ + { + "id": "custom.width", + "value": 161 + }, + { + "id": "custom.cellOptions", + "value": { + "mode": "basic", + "type": "color-background" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "app to update" + }, + "properties": [ + { + "id": "custom.width", + "value": 225 + } + ] + } + ] + }, + "gridPos": { + "h": 9, + "w": 24, + "x": 0, + "y": 7 + }, + "id": 4, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "enablePagination": true, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "frameIndex": 1, + "showHeader": true, + "sortBy": [ + { + "desc": true, + "displayName": "obsolescence score" + } + ] + }, + "pluginVersion": "10.2.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "upgrade_manager_software_obsolescence_score{parent=~\"$app\", action=\"update\", app_type=~\"$type\"}", + "format": "table", + "instant": true, + "interval": "", + "legendFormat": "__auto", + "range": false, + "refId": "A" + } + ], + "title": "Upgrades to-do", + "transformations": [ + { + "id": "organize", + "options": { + "excludeByName": { + "Time": true, + "Value": false, + "__name__": true, + "action": false, + "app": false, + "app_type": false, + "container": true, + "current_version": false, + "endpoint": true, + "environment": false, + "error": true, + "instance": true, + "isparent": true, + "job": true, + "namespace": true, + "pod": true, + "prometheus": true, + "service": true + }, + "indexByName": { + "Time": 0, + "Value": 7, + "__name__": 1, + "action": 6, + "app": 3, + "app_type": 8, + "container": 9, + "current_version": 4, + "endpoint": 11, + "environment": 12, + "error": 13, + "instance": 14, + "job": 15, + "kubernetes_cluster": 16, + "namespace": 17, + "parent": 2, + "pod": 18, + "prometheus": 19, + "service": 20, + "target_version": 5 + }, + "renameByName": { + "Value": "obsolescence score", + "action": "to-do", + "app": "app to update", + "current_version": "", + "parent": "parent app" + } + } + } + ], + "type": "table" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 16 + }, + "id": 71, + "panels": [], + "title": "Global metrics (all apps)", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "purple", + "value": null + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 5, + "x": 0, + "y": 17 + }, + "id": 8, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "10.2.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "upgrade_manager_total_software_found", + "format": "table", + "instant": true, + "range": false, + "refId": "A" + } + ], + "title": "Tracked Applications", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "axisSoftMax": 120, + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 3, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "line" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 14, + "x": 5, + "y": 17 + }, + "id": 2, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum(upgrade_manager_software_obsolescence_score{app=~\"$app\", isparent=\"1\"})/count(upgrade_manager_software_obsolescence_score{isparent=\"1\", app=~\"$app\"})", + "legendFormat": "average score across selected apps", + "range": true, + "refId": "A" + } + ], + "title": "Average Obsolescence Score Over Time", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "blue", + "value": null + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 14, + "w": 5, + "x": 19, + "y": 17 + }, + "id": 128, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "10.2.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum(upgrade_manager_software_obsolescence_score{isparent=\"1\", app=~\"$app\"})", + "format": "table", + "range": true, + "refId": "A" + } + ], + "title": "Total obsolescence score", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "dark-orange", + "value": null + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 5, + "x": 0, + "y": 24 + }, + "id": 129, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "10.2.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "count(upgrade_manager_software_obsolescence_score{isparent=\"1\"}>=100)", + "format": "table", + "range": true, + "refId": "A" + } + ], + "title": "App over score limit (100)", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "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": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Software Count * 80" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "transparent", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "sum(upgrade_manager_software_obsolescence_score{isparent=\"1\"})" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "purple", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total Sum of Obsolescence Score" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "purple", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 7, + "w": 14, + "x": 5, + "y": 24 + }, + "id": 265, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum(upgrade_manager_software_obsolescence_score{isparent=\"1\"})", + "hide": false, + "legendFormat": "Total Sum of Obsolescence Score", + "range": true, + "refId": "A" + } + ], + "title": "Total Obsolescence Score over time", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 31 + }, + "id": 12, + "panels": [], + "title": "Health Metrics", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "dark-purple", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 5, + "x": 0, + "y": 32 + }, + "id": 127, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "10.2.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "upgrade_manager_total_software_found", + "format": "table", + "instant": true, + "range": false, + "refId": "A" + } + ], + "title": "Tracked Applications", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "The 3 steps of the processing for an application.\nThe three values should be equal otherwise it means there is an error.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-GrYlRd" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 10, + "x": 5, + "y": 32 + }, + "id": 10, + "options": { + "displayMode": "lcd", + "minVizHeight": 10, + "minVizWidth": 0, + "namePlacement": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showUnfilled": true, + "text": { + "titleSize": 12 + }, + "valueMode": "color" + }, + "pluginVersion": "10.2.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "upgrade_manager_total_software_load_success", + "instant": true, + "legendFormat": "New Versions Discovery", + "range": false, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "upgrade_manager_total_software_obsolescence_score_compute_success", + "hide": false, + "instant": true, + "legendFormat": "Score Computation", + "range": false, + "refId": "C" + } + ], + "title": "Processing Pipeline Success Milestones", + "type": "bargauge" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "filterable": true, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "app" + }, + "properties": [ + { + "id": "custom.width", + "value": 305 + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 9, + "x": 15, + "y": 32 + }, + "id": 229, + "options": { + "cellHeight": "sm", + "footer": { + "countRows": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": true + }, + "showHeader": true, + "sortBy": [] + }, + "pluginVersion": "10.2.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "upgrade_manager_software_obsolescence_score{isparent=\"1\"}", + "format": "table", + "instant": true, + "range": false, + "refId": "A" + } + ], + "title": "List of tracked softwares", + "transformations": [ + { + "id": "organize", + "options": { + "excludeByName": { + "Time": true, + "Value": false, + "__name__": true, + "action": true, + "app": false, + "app_type": true, + "container": true, + "current_version": true, + "endpoint": true, + "environment": true, + "error": true, + "instance": true, + "isparent": true, + "job": true, + "kubernetes_cluster": true, + "namespace": true, + "parent": true, + "pod": true, + "prometheus": true, + "service": true, + "target_version": true + }, + "indexByName": {}, + "renameByName": {} + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "noValue": "0", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 1 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 15, + "x": 0, + "y": 37 + }, + "id": 178, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "10.2.2", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "upgrade_manager_software_process_error", + "format": "table", + "instant": true, + "range": false, + "refId": "A" + } + ], + "title": "App Processing Errors", + "transformations": [ + { + "id": "organize", + "options": { + "excludeByName": { + "Time": true, + "Value": true, + "__name__": true, + "app_type": false, + "container": true, + "endpoint": true, + "environment": true, + "error": true, + "instance": true, + "isparent": true, + "job": true, + "kubernetes_cluster": true, + "pod": true, + "prometheus": true, + "service": true + }, + "indexByName": {}, + "renameByName": {} + } + } + ], + "type": "stat" + } + ], + "refresh": "", + "schemaVersion": 38, + "tags": [], + "templating": { + "list": [ + { + "current": { + "selected": true, + "text": "", + "value": "4siG5M8Mk" + }, + "hide": 0, + "includeAll": false, + "label": "Datasource", + "multi": false, + "name": "datasource", + "options": [], + "query": "prometheus", + "queryValue": "", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "type": "datasource" + }, + { + "allValue": ".*", + "current": { + "selected": false, + "text": "All", + "value": "$__all" + }, + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "definition": "label_values(upgrade_manager_software_obsolescence_score{isparent=\"1\", action !=\"\"}, app)", + "hide": 0, + "includeAll": true, + "label": "App", + "multi": false, + "name": "app", + "options": [], + "query": { + "query": "label_values(upgrade_manager_software_obsolescence_score{isparent=\"1\", action !=\"\"}, app)", + "refId": "StandardVariableQuery" + }, + "refresh": 2, + "regex": "", + "skipUrlSync": false, + "sort": 1, + "type": "query" + }, + { + "allValue": ".*", + "current": { + "selected": false, + "text": "All", + "value": "$__all" + }, + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "definition": "label_values(upgrade_manager_software_obsolescence_score, app_type)", + "hide": 0, + "includeAll": true, + "label": "App Type", + "multi": false, + "name": "type", + "options": [], + "query": { + "query": "label_values(upgrade_manager_software_obsolescence_score, app_type)", + "refId": "StandardVariableQuery" + }, + "refresh": 2, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "type": "query" + } + ] + }, + "time": { + "from": "now-5m", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "upgrade-manager", + "uid": "-Mnm3jvVk", + "version": 61, + "weekStart": "" + } \ No newline at end of file diff --git a/doc/calculators/README.md b/doc/calculators/README.md new file mode 100644 index 0000000..aa9d657 --- /dev/null +++ b/doc/calculators/README.md @@ -0,0 +1,10 @@ +# Calculators + +Calculators represent different strategies to compute an obsolescence score for a software. +When a [Source](../sources/README.md) finds a software, it is responsible for attaching `Calculator` to it via the `software.Calculator` struct field. + +Here is the existing list of supported calculators: + - [Semver Calculator / Augmented Semver Calculator](./semver_calculator.md) + - [Date Calculator](./date_calculator.md) + - [Candidate Count Calculator](./candidate_count_calculator.md) + - [Skip](./skip.md) diff --git a/doc/calculators/candidate_count_calculator.md b/doc/calculators/candidate_count_calculator.md new file mode 100644 index 0000000..4a13346 --- /dev/null +++ b/doc/calculators/candidate_count_calculator.md @@ -0,0 +1,13 @@ +# Candidate Count Calculator + +## Description +For each version candidate the software could be upgraded to, the software will receive an extra `30 obsolescence points`. + +Sources using this Calculator: +- [lambda](../sources/aws_lambda.md) + + +### Example +An AWS lambda function is running Python `3.10`. +Let's say the latest available version of Python on AWS Lambda is Python `3.12` +In that case, the lambda software will be given an obsolescence of `30*2=60 points` (because there are version 2 candidates, namely Python 3.11 and Python 3.12) \ No newline at end of file diff --git a/doc/calculators/date_calculator.md b/doc/calculators/date_calculator.md new file mode 100644 index 0000000..ddaf675 --- /dev/null +++ b/doc/calculators/date_calculator.md @@ -0,0 +1,13 @@ +# Date Calculator + +## Description +For each day since the day the latest eligible version candidate was released, the software will receive an extra `5` obsolescence points. + +Sources using this Calculator: +- [Deployments](../sources/deployments.md) + +### Example +A deployment is using an image `foo:1.1` +The latest eligible version is `foo:1.4`, it was released `30` days after `foo:1.1` + +In that case, the deployment software will be given an obsolescence of `30*5=60 points` \ No newline at end of file diff --git a/doc/calculators/semver_calculator.md b/doc/calculators/semver_calculator.md new file mode 100644 index 0000000..e747650 --- /dev/null +++ b/doc/calculators/semver_calculator.md @@ -0,0 +1,45 @@ +# Semver Calculator + +## Description +Using semantic versioning, this calculator compares the current and latest version, and compute the obsolescence score as follows: + ++ x major versions late = `x * 50 point` + ++ y minor versions late = `y * 5 points` + ++ z patch versions late = `z * 1 point` + +The calculator evaluates the version segments one by one in the following order: `major>minor>patch`. As soon as it finds a difference between the current and the latest version, it computes the score for the segment and skips the following ones. + +Sources using this Calculator: +- [aws_eks](../sources/aws_eks.md) (for eks addons versions) +- [aws_elasticache](../sources/aws_elasticache.md) +- [aws_rds](../sources/aws_rds.md) +- [aws_msk](../sources/aws_msk.md) +- [argoCDHelm](../sources/argoCDHelm.md) + +## Augmented Semver Calculator +Some softwares following semantic versioning have a major version that almost never changes. AWS EKS (kubernetes) is a good example as we don't know if we'll ever going to see a major version 2.0 in the future. In the case of Kubernetes, minor versions can actually be considered major versions as they introduce significant (sometimes breaking) changes and as most major cloud and software providers generally support only a few minor versions. From that fact, we decided to introduce the Augmented Semver Calculator. + +The exact score table could be made configurable per source in the future. + +The `AugmentedSemverCalculator` is working the same way as the Semver Calculator, but it adds more points: + ++ x major versions late = `x * 100 point` + ++ y minor versions late = `y * 50 points` + ++ z patch versions late = `z * 5 point` + +The `AugmentedSemverCalculator` is currently only used by the EKS source. + +Sources using this Calculator: +- [aws_eks](../sources/aws_eks.md) (for k8s engine versions) + +## Example +An ArgoHelm (ArgoCD Application target a Helm configuration) software has a current Chart version of `1.2.1` and the latest eligible version is `1.6.3`. + +1. The calculator compares the major versions: `1` and `1`. They are the same so it does not add any points +2. The calculator compares the minor versions: `2` and `6`. Since `6` is higher than `2`, the calculator adds `(6-2)*5 = 4*5 = 20 points` +3. The calcualtor does not compare the patch versions because it has already found a difference in a previous version segment. + diff --git a/doc/calculators/skip.md b/doc/calculators/skip.md new file mode 100644 index 0000000..11814ab --- /dev/null +++ b/doc/calculators/skip.md @@ -0,0 +1,10 @@ +# Skip Calculator + +## Description +The Skip Calculator arbitrarily gives an obsolescence score of `0` + +## Example +An AWS Lambda function is using a deprecated runtime (a runtime can be evaluated as deprecated because AWS marked it as deprecated or because the is has been added to the list of deprecated runtimes in `config.sources.aws.lambda.deprecate-runtimes`). +Let's say the runtime is Python 2.7. + +In that case, the lambda source will arbitrarily set the score to the value of `config.sources.aws.lambda.deprecated-runtimes-score` (defaults to `100`). \ No newline at end of file diff --git a/doc/image/boat.png b/doc/image/boat.png new file mode 100644 index 0000000..267cb0d Binary files /dev/null and b/doc/image/boat.png differ diff --git a/doc/image/grafana-dashboard.png b/doc/image/grafana-dashboard.png new file mode 100644 index 0000000..b338984 Binary files /dev/null and b/doc/image/grafana-dashboard.png differ diff --git a/doc/2022-10-25-kubernetes-upgrade-kpi-diagram.png b/doc/image/score-based-alerting.png similarity index 100% rename from doc/2022-10-25-kubernetes-upgrade-kpi-diagram.png rename to doc/image/score-based-alerting.png diff --git a/doc/metrics.md b/doc/metrics.md new file mode 100644 index 0000000..9e478e0 --- /dev/null +++ b/doc/metrics.md @@ -0,0 +1,10 @@ +## Prometheus Metrics exposed by upgrade-manager +upgrade-manager exposes different Prometheus metrics that can be used for alerting/monitoring. +To scrape these metrics, you can refer to the default ServiceMonitor deployed with the chart. + +- `upgrade_manager_software_obsolescence_score` (gaugeVec): obsolescence score for softwares discovered by upgrade-manager +- `upgrade_manager_software_process_error` (counterVec): count of errors while processing softwares +- `upgrade_manager_total_software_found` (gauge): total number of softwares found in the auto-discovery process +- `upgrade_manager_total_software_load_success` (gauge): total count of softwares with successfully loaded version candidates +- `upgrade_manager_total_software_obsolescence_score_compute_success` (gauge): total count of softwares with successfully computed obsolescence scores. +- `upgrade_manager_main_loop_execution_time` (gauge): Time taken by the last main loop execution (to find softwares, find new versions and compute scores)" diff --git a/doc/sources/README.md b/doc/sources/README.md new file mode 100644 index 0000000..261df47 --- /dev/null +++ b/doc/sources/README.md @@ -0,0 +1,16 @@ +# Sources + +A `Source` represent a type of software that can be automatically discovered by `upgrade-manager`. + +The `Source` is responsible for two tasks: +1. Automatically `discover instances of a given software type`. For example it could find all ArgoCD applications using helm in a kubernetes cluster, or all AWS Lambda functions deployed in an AWS Account. +2. Automatically `discover version candidates for the software`, meaning newer versions that are eligible as per the source's filtering logic (see more details in each source's documentation), + +Here is the existing list of currently supported sources: + - [ArgoCD Helm applications (ArgoCDHelm)](./argoCDHelm.md) + - [Local Helm directory (filesystemhelm)](./filesystemhelm.md) + - [AWS Lambda](./aws_lambda.md) + - [AWS MSK](./aws_msk.md) + - [AWS Elasticache](./aws_elasticache.md) + - [AWS EKS](./aws_eks.md) + - [AWS RDS](./aws_rds.md) diff --git a/doc/sources/argoCDHelm.md b/doc/sources/argoCDHelm.md new file mode 100644 index 0000000..2e32cee --- /dev/null +++ b/doc/sources/argoCDHelm.md @@ -0,0 +1,80 @@ +# AWS ArgoCDHelm Source + +### Description +The ArgoCDHelm source finds all the [ArgoCD](https://argo-cd.readthedocs.io/en/stable/) Applications targeting a Helm configuration. A Helm configuration is an application with a populated `spec.source.helm` field of the Application object. +It does so by listing Applications objects in the namespace defined in `config.sources.argocdhelm.argocd-namespace`. +When it finds an application, it clones the source repository and parses the `Chart.yaml` file. +If the `Chart.yaml` file references Chart dependencies the obsolescence scores of the dependencies will be added to the software only if the top-level chart is already up-to-date. + +### New versions discovery +This source discovers new versions by fetching the `index.yaml` file available at the root of the helm repository. The `index.yaml` file lists all versions for a given chart. + +### Git Repository Connection +In order to clone private git repositories, `upgrade-manager` requires credentials. +1. At startup, upgrade-manager lists `secrets` in its namespace or whatever is defined by `git-credentials-secrets-namespace`. +2. Then, it loads credentials from all secrets matching this regular expression: `.*-repo.*` or whatever is defined by `git-credentials-secrets-pattern` +The HTTPS connection secrets should have the following keys: + +```yaml +apiVersion: v1 +kind: Secret +metadata: + name: private-repo-creds-https + namespace: upgrade-manager +data: + password: password-in-base64 + url: git-url-in-base64 + username: username-in-base64 +``` + +After loading the secrets, upgrade-manager will use them to connect software's repositories using a prefix-based approach. +For example: +- Let's say we have two secrets: +Secret 1 is providing credentials to connect to the git url `https://foo.bar.com` +Secret 2 is providing credentials to connect to the git url `https://foo.bar.com/team1` +- Now, let's say we have an `app1` ArgoCD Helm application deployed with its source repository available at `https://foo.bar.com/team1/app1` + - upgrade-manager will use Secret 2's credentials to connect to the instance + +For test purposes, it is recommended to start with a single secret pointing at the root of your git repository. The credentials need readonly access to the desired repositories. + +### Custom filtering logic to assess a version's eligibility +In order to customize which Applications upgrade-manager should keep and which versions upgrade-manager selects as the best version candidates for an ArgoCD Helm software, upgrade-manager uses `filters` (see the detailed list below). + +### Configuration + +```yaml +sources: + argocdHelm: + - enabled: true + argocd-namespace: argocd # namespace where the argocd application object is deployed + git-credentials-secrets-namespace: upgrade-manager # namespace where secrets containing git credentials are deployed + git-credentials-secrets-pattern: ".*-repo-.*" # regex to filter which secrets to fetch + filters: + semver-versions: + remove-pre-release: true + remove-first-major-version: true + recent-versions: + days: 21 # number of days since the version was released + destination-namespace: # namespace where the app resources will be deployed + include: [] + # ["kyverno"] # regular expression, if include is empty, it includes all namespaces + exclude: [] + # ["default", "temporary-*", "feature-*", "kube-system"] # regular expression if exclude is empty, it does not exclude any namespace + ``` +Parameters: +- **argocd-namespace**: namespace where the ArgoCD Application objects are deployed. +- **git-credentials-secrets-namespace**: namespace where secrets containing git credentials are deployed. +- **git-credentials-secrets-pattern**: regex to filter which secrets to load git credentials from. +- **filters.semver-versions.remove-pre-release**: whether pre-release versions should be considered as eligible version candidates or skipped. +- **filters.semver-versions.remove-first-major-version**: whether versions X.0.0 should be considered as eligible candidates or skipped. +- **filters.recent-versions.days**: the minimum age of a version before it should be considered as an eligible version candidate. +- **filters.destination-namespace.include**: list of destination namespaces (corresponding to the destination namespace in an ArgoCD application spec) to compute a score/discover new targets for. If `include:` is empty, it includes all namespaces. Wildcard patterns (ex: `feat-*`) can be used. +- **filters.destination-namespace.exclude**: list of destination namespaces (corresponding to the destination namespace in an ArgoCD application spec) to avoid computing a score/discover new targets for. If `exclude:` is empty, it does not exclude any namespace. Wildcard patterns (ex: `feat-*`) can be used. + +### Known limitations +Some limitations are known and will be tackled in future updates: +- add support for ArgoCD Helm repositories aliases in the form of "@myalias or alias:myalias" +- add support for dynamic revision tracking strategies (^0.1, 1.2.* etc.) by failing-over using the helm.sh/chart label + +### Obsolescence score calculation +The ArgoCD Helm source is using the [Semver Calculator](../calculators/semver_calculator.md) \ No newline at end of file diff --git a/doc/sources/aws_eks.md b/doc/sources/aws_eks.md new file mode 100644 index 0000000..728795c --- /dev/null +++ b/doc/sources/aws_eks.md @@ -0,0 +1,22 @@ +# AWS EKS Source + +### Description +The EKS source finds all the EKS clusters in the current AWS account with the current versions for their k8s engine and the different EKS addons deployed (vpc-cni, kube-proxy etc.). + +### New versions discovery +This source discovers new versions by calling the `DescribeAddonVersions` method with the default Go EKS client. + +### Configuration + +```yaml +sources: + aws: + eks: + enabled: true + request-timeout: 15s # (default: 15s) +``` +Parameters: +- **request-timeout**: is the timeout duration when calling the AWS EKS API + +### Obsolescence Score Calculation +The EKS source is using the [Semver Calculator](../calculators/semver_calculator.md) for `addons` and [Augmented Semver Calculator](../calculators/semver_calculator.md#augmented-semver-calculator) for the `k8s-engine`. \ No newline at end of file diff --git a/doc/sources/aws_elasticache.md b/doc/sources/aws_elasticache.md new file mode 100644 index 0000000..37c8f2f --- /dev/null +++ b/doc/sources/aws_elasticache.md @@ -0,0 +1,23 @@ +# AWS Elasticache Source + +### Description +The Elasticache source finds all the Elasticache clusters in the current AWS account with their current versions. + +### New versions discovery +This source discovers new versions by calling the `GetCompatibleKafkaVersion` method with the default Go Elasticache client. + +### Configuration + +```yaml +sources: + aws: + elasticache: + enabled: true + request-timeout: 15s # (default: 15s) +``` +Parameters: +- **request-timeout**: is the timeout duration when calling the AWS Elasticache API + +### Obsolescence Score Calculation +The Elasticache source is using the [Semver Calculator](../calculators/semver_calculator.md) + diff --git a/doc/sources/aws_lambda.md b/doc/sources/aws_lambda.md new file mode 100644 index 0000000..8e49ed1 --- /dev/null +++ b/doc/sources/aws_lambda.md @@ -0,0 +1,62 @@ +# AWS Lambda Source + +### Description +The Lambda source finds all the Lambda clusters in the current AWS account with their current versions. + +### New versions discovery +This source discovers new versions by reading the supported runtimes list provided by AWS in the Go Lambda SDK Library + +### Default deprecated runtimes list: +AWS does not expose any API to retrieve the deprecated runtimes, therefore we use a hard-coded list which will be updated in time: +Here is the default list: +```yaml + "nodejs", + "nodejs4.3", + "nodejs4.3-edge", + "nodejs6.10", + "nodejs8.10", + "nodejs10.x", + "nodejs12.x", + "java8", + "java8.al2", + "python2.7", + "python3.6", + "dotnetcore1.0", + "dotnetcore2.0", + "dotnet6", + "ruby2.5", +``` + +### Configuration + +```yaml +sources: + aws: + lambda: + enabled: true + request-timeout: 15s + deprecated-runtimes-score: 100 # defaults to 100 + # deprecated-runtimes: + # - "nodejs" + # - "nodejs4.3" + # - "nodejs4.3-edge" + # - "nodejs6.10" + # - "nodejs8.10" + # - "nodejs10.x" + # - "nodejs12.x" + # - "java8" + # - "java8.al2" + # - "python2.7" + # - "python3.6" + # - "dotnetcore1.0" + # - "dotnetcore2.0" + # - "dotnet6" + # - "ruby2.5" +``` +Parameters: +- **request-timeout**: the timeout duration when calling the AWS Lambda API +- **deprecated-runtimes-score**: arbitrarily sets the score for runtimes that are considered deprecated. It is recommended to set it to a score above your [alert threshold](../../README.md#alerting-patterns-deciding-when-to-update-softwares) because a deprecated runtime is not maintained by AWS anymore and could stop working anytime. +- **deprecated-runtimes**: custom list of runtimes to consider deprecated (overrides the default deprecated runtimes list) + +### Obsolescence Score Calculation +The Lambda source is using the [Candidate Count Calculator](../calculators/candidate_count_calculator.md) \ No newline at end of file diff --git a/doc/sources/aws_msk.md b/doc/sources/aws_msk.md new file mode 100644 index 0000000..c4e18df --- /dev/null +++ b/doc/sources/aws_msk.md @@ -0,0 +1,23 @@ +# AWS MSK Source + +### Description +The MSK source finds all the MSK clusters in the current AWS account with their current Kafka versions. + +### New versions discovery +This source discovers new versions by calling the `GetCompatibleKafkaVersion` method with the default Go MSK client. + +### Configuration + +```yaml +sources: + aws: + msk: + enabled: true + request-timeout: 15s # (default: 15s) +``` +Parameters: +- **request-timeout**: is the timeout duration when calling the AWS MSK API + +### Obsolescence Score Calculation +The MSK source is using the [Semver Calculator](../calculators/semver_calculator.md) + diff --git a/doc/sources/aws_rds.md b/doc/sources/aws_rds.md new file mode 100644 index 0000000..fc0520d --- /dev/null +++ b/doc/sources/aws_rds.md @@ -0,0 +1,34 @@ +# AWS RDS Source + +### Description +The RDS source finds all the RDS clusters in the current AWS account with their current database engine version. + +Supported database engines are: +- rds-postgresql +- rds-aurora-postgresql +- rds-aurora-mysql +- rds-mariadb +- rds-docdb +- rds-mysql +- rds-oracle-ee +- rds-neptune + +### New versions discovery +This source discovers new versions by calling the `DescribeDBEngineVersions` method with the default Go RDS client. + +### Configuration + +```yaml +sources: + aws: + rds: + enabled: true + aggregation-level: cluster | instance # (default: cluster) + request-timeout: 15s # (default: 15s) +``` +Parameters: +- **request-timeout**: the timeout duration when calling the AWS MSK API +- **aggregation-level**: whether several RDS instances from the same RDS cluster should be considered a single software or if they should be considered different softwares. + +### Obsolescence Score Calculation +The RDS source is using the [Semver Calculator](../calculators/semver_calculator.md) \ No newline at end of file diff --git a/doc/sources/deployments.md b/doc/sources/deployments.md new file mode 100644 index 0000000..e69de29 diff --git a/doc/sources/filesystemhelm.md b/doc/sources/filesystemhelm.md new file mode 100644 index 0000000..d4e5bd6 --- /dev/null +++ b/doc/sources/filesystemhelm.md @@ -0,0 +1,39 @@ +# AWS FileSystemHelm Source + +### Description +The FileSystemHelm source parses all Chart.yaml files provided. +If the `Chart.yaml` file references Chart dependencies the obsolescence scores of the dependencies will be added to the software only if the top-level chart is already up-to-date. + +### New versions discovery +This source discovers new versions by fetching the `index.yaml` file available at the root of the helm repository. The `index.yaml` file lists all versions for a given chart. + +### Custom filtering logic to assess a version's eligibility +In order to customize which Applications upgrade-manager should keep and which versions upgrade-manager selects as the best version candidates for an ArgoCD Helm software, upgrade-manager uses `filters`. + + +### Configuration + +```yaml +sources: + filesystemHelm: + - enabled: true + paths: + - "./test_chart/Chart.yaml" + - "./test_chart2/Chart.yaml" + filters: + semver-versions: + remove-pre-release: true + remove-first-major-version: true + recent-versions: + days: 21 + ``` +Parameters: +- **filters.semver-versions.remove-pre-release**: whether pre-release versions should be considered as eligible version candidates or skipped. +- **filters.semver-versions.remove-first-major-version**: whether versions X.0.0 should be considered as eligible candidates or skipped. +- **filters.recent-versions.days**: the minimum age of a version before it should be considered as an eligible version candidate. +- **filters.destination-namespace.include**: list of destination namespaces (corresponding to the destination namespace in an ArgoCD application spec) to compute a score/discover new targets for. If `include:` is empty, it includes all namespaces. Wildcard patterns (ex: `feat-*`) can be used. +- **filters.destination-namespace.exclude**: list of destination namespaces (corresponding to the destination namespace in an ArgoCD application spec) to avoid computing a score/discover new targets for. If `exclude:` is empty, it does not exclude any namespace. Wildcard patterns (ex: `feat-*`) can be used. + + +### Obsolescence Score Calculation +The ArgoCD Helm source is using the [Semver Calculator](../calculators/semver_calculator.md) \ No newline at end of file diff --git a/internal/app/calculators/default.go b/internal/app/calculators/default.go index 5e81d5d..a7581bf 100644 --- a/internal/app/calculators/default.go +++ b/internal/app/calculators/default.go @@ -21,7 +21,7 @@ const ( // The default calculator parses versions using Semantic Versioning. // It takes the last version available and computes the obsolescence score: // -// x major versions late = m * 50 +// x major versions late = x * 50 // // y minor versions late = y * 5 // diff --git a/internal/infra/kubernetes/argocd.go b/internal/infra/kubernetes/argocd.go index bdbd64b..c1c6b17 100644 --- a/internal/infra/kubernetes/argocd.go +++ b/internal/infra/kubernetes/argocd.go @@ -77,7 +77,7 @@ func rawToArgoApplication(raw map[string]any) (*ArgoCDApplication, error) { if !isHelmApp || !isDeployedApp { return newApp, fmt.Errorf("not a properly deployed Argo Helm application") } - // These fields exist an all apps + // These fields exist in all apps server, found, err := unstructured.NestedString(raw, "spec", "destination", "server") if err != nil || !found { return newApp, err