retirement-radar 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -2
- package/package.json +11 -6
- package/retirements.yaml +178 -35
- package/src/cli.js +47 -7
- package/src/radar.js +67 -29
package/README.md
CHANGED
|
@@ -24,6 +24,9 @@ npx retirement-radar scan
|
|
|
24
24
|
|---|---|---|
|
|
25
25
|
| EKS | Kubernetes version per cluster | After standard support, clusters move to extended support at 6x the control-plane price, then get force-upgraded |
|
|
26
26
|
| RDS / Aurora | Engine major version (PostgreSQL, MySQL, Aurora) | After standard support, RDS bills Extended Support per vCPU-hour automatically |
|
|
27
|
+
| ElastiCache | Redis OSS / Valkey version per cluster, replication group and serverless cache | Redis OSS 4 and 5 are already on paid Extended Support; 6 follows on 31 January 2027 |
|
|
28
|
+
| OpenSearch | Elasticsearch / OpenSearch version per domain | From 7 November 2026, Extended Support for older versions costs as much as the instances themselves |
|
|
29
|
+
| MSK | Kafka version per provisioned cluster | No paid extension: past the date, MSK can auto-upgrade the cluster at any time |
|
|
27
30
|
| Lambda | Function runtime | Deprecated runtimes get no security patches, then can't be created or updated |
|
|
28
31
|
|
|
29
32
|
Dates live in [`retirements.yaml`](retirements.yaml), each section with its AWS source link. A version that isn't listed shows as `unknown`, never as fine.
|
|
@@ -39,6 +42,7 @@ npx retirement-radar scan
|
|
|
39
42
|
| Option | |
|
|
40
43
|
|---|---|
|
|
41
44
|
| `--region eu-west-1,us-east-1` | Only these regions (default: every enabled region) |
|
|
45
|
+
| `--services rds,elasticache` | Only these services: `eks`, `rds`, `elasticache`, `opensearch`, `msk`, `lambda` (default: all) |
|
|
42
46
|
| `--profile prod` | AWS CLI profile |
|
|
43
47
|
| `--json` | Machine-readable output |
|
|
44
48
|
| `--all` | Also list resources with more than a year left |
|
|
@@ -47,7 +51,7 @@ npx retirement-radar scan
|
|
|
47
51
|
|
|
48
52
|
## Safe by design
|
|
49
53
|
|
|
50
|
-
- **Read-only.** It only calls `sts get-caller-identity`, `ec2 describe-regions`, `eks list-clusters` / `describe-cluster`, `rds describe-db-instances` / `describe-db-clusters` and `lambda list-functions`.
|
|
54
|
+
- **Read-only.** It only calls `sts get-caller-identity`, `ec2 describe-regions`, `eks list-clusters` / `describe-cluster`, `rds describe-db-instances` / `describe-db-clusters`, `elasticache describe-cache-clusters` / `describe-serverless-caches`, `opensearch list-domain-names` / `describe-domains`, `kafka list-clusters-v2` and `lambda list-functions`.
|
|
51
55
|
- **Local.** Nothing is sent anywhere; no telemetry.
|
|
52
56
|
- A region or service you can't read becomes a warning, not a failed scan.
|
|
53
57
|
|
|
@@ -59,7 +63,10 @@ Minimal IAM policy:
|
|
|
59
63
|
"Statement": [{
|
|
60
64
|
"Effect": "Allow",
|
|
61
65
|
"Action": ["ec2:DescribeRegions", "eks:ListClusters", "eks:DescribeCluster",
|
|
62
|
-
"rds:DescribeDBInstances", "rds:DescribeDBClusters",
|
|
66
|
+
"rds:DescribeDBInstances", "rds:DescribeDBClusters",
|
|
67
|
+
"elasticache:DescribeCacheClusters", "elasticache:DescribeServerlessCaches",
|
|
68
|
+
"es:ListDomainNames", "es:DescribeDomains", "kafka:ListClustersV2",
|
|
69
|
+
"lambda:ListFunctions"],
|
|
63
70
|
"Resource": "*"
|
|
64
71
|
}]
|
|
65
72
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "retirement-radar",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Read-only scan of your AWS account for EKS, RDS and Lambda versions near end of support.",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Read-only scan of your AWS account for EKS, RDS, ElastiCache, OpenSearch, MSK and Lambda versions near end of support.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"retirement-radar": "src/cli.js"
|
|
7
7
|
},
|
|
@@ -19,12 +19,17 @@
|
|
|
19
19
|
},
|
|
20
20
|
"keywords": [
|
|
21
21
|
"aws",
|
|
22
|
+
"deprecation",
|
|
23
|
+
"devops",
|
|
22
24
|
"eks",
|
|
23
|
-
"
|
|
24
|
-
"lambda",
|
|
25
|
+
"elasticache",
|
|
25
26
|
"end-of-life",
|
|
26
|
-
"
|
|
27
|
-
"
|
|
27
|
+
"kafka",
|
|
28
|
+
"lambda",
|
|
29
|
+
"msk",
|
|
30
|
+
"opensearch",
|
|
31
|
+
"rds",
|
|
32
|
+
"redis"
|
|
28
33
|
],
|
|
29
34
|
"repository": {
|
|
30
35
|
"type": "git",
|
package/retirements.yaml
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
# End-of-support dates Retirement Radar checks against.
|
|
2
2
|
# Every section links its source. Found a wrong or missing date? Open a PR with the source link.
|
|
3
|
-
#
|
|
4
|
-
|
|
3
|
+
#
|
|
4
|
+
# Conventions
|
|
5
|
+
# standard: end of standard support. After it, the service bills extended support (or deprecates).
|
|
6
|
+
# extended: end of extended support. After it, AWS upgrades the resource for you.
|
|
7
|
+
# A version listed with no dates ({} or null) is current: no end of support announced yet.
|
|
8
|
+
# A version that is not listed at all shows up as "unknown", never as fine.
|
|
9
|
+
updated: "2026-10-05"
|
|
5
10
|
|
|
6
11
|
eks:
|
|
7
12
|
source: https://docs.aws.amazon.com/eks/latest/userguide/kubernetes-versions.html
|
|
8
|
-
#
|
|
9
|
-
|
|
10
|
-
cost: Extended support adds $0.50 per cluster-hour, about $365 a month per cluster.
|
|
13
|
+
# Standard support is 14 months, then 12 months of extended support, then a forced control-plane upgrade.
|
|
14
|
+
cost: Extended support costs $0.60 instead of $0.10 per cluster-hour, about $365 a month extra per cluster.
|
|
11
15
|
monthlyCost: 365
|
|
12
16
|
versions:
|
|
13
17
|
"1.23": { standard: "2023-10-11", extended: "2024-10-11" }
|
|
@@ -21,48 +25,187 @@ eks:
|
|
|
21
25
|
"1.31": { standard: "2025-11-26", extended: "2026-11-26" }
|
|
22
26
|
"1.32": { standard: "2026-03-23", extended: "2027-03-23" }
|
|
23
27
|
"1.33": { standard: "2026-07-29", extended: "2027-07-29" }
|
|
28
|
+
"1.34": { standard: "2026-12-02", extended: "2027-12-02" }
|
|
29
|
+
"1.35": { standard: "2027-03-27", extended: "2028-03-27" }
|
|
30
|
+
"1.36": { standard: "2027-08-02", extended: "2028-08-02" }
|
|
31
|
+
"1.37": { standard: "2027-12-01", extended: "2028-12-01" }
|
|
24
32
|
|
|
25
33
|
rds:
|
|
26
|
-
source: https://docs.aws.amazon.com/AmazonRDS/latest/
|
|
27
|
-
|
|
28
|
-
|
|
34
|
+
source: https://docs.aws.amazon.com/AmazonRDS/latest/PostgreSQLReleaseNotes/postgresql-release-calendar.html
|
|
35
|
+
sources:
|
|
36
|
+
mysql: https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/MySQL.Concepts.VersionMgmt.html
|
|
37
|
+
aurora-mysql: https://docs.aws.amazon.com/AmazonRDS/latest/AuroraMySQLReleaseNotes/AuroraMySQL.release-calendars.html
|
|
38
|
+
aurora-postgresql: https://docs.aws.amazon.com/AmazonRDS/latest/AuroraPostgreSQLReleaseNotes/aurorapostgresql-release-calendar.html
|
|
39
|
+
# After standard support ends, RDS enrols the database in Extended Support automatically.
|
|
40
|
+
cost: RDS Extended Support is billed per vCPU-hour on top of the instance price, and rises in year 3.
|
|
29
41
|
engines:
|
|
30
42
|
postgres:
|
|
31
|
-
"11": { standard: "2024-02-29" }
|
|
32
|
-
"12": { standard: "2025-02-28" }
|
|
33
|
-
"13": { standard: "2026-02-28" }
|
|
34
|
-
"14": { standard: "2027-02-28" }
|
|
35
|
-
"15": { standard: "2028-02-29" }
|
|
36
|
-
"16": { standard: "2029-02-28" }
|
|
37
|
-
|
|
38
|
-
"
|
|
39
|
-
"8.0": { standard: "2026-07-31" }
|
|
43
|
+
"11": { standard: "2024-02-29", extended: "2027-03-31" }
|
|
44
|
+
"12": { standard: "2025-02-28", extended: "2028-02-29" }
|
|
45
|
+
"13": { standard: "2026-02-28", extended: "2029-02-28" }
|
|
46
|
+
"14": { standard: "2027-02-28", extended: "2030-02-28" }
|
|
47
|
+
"15": { standard: "2028-02-29", extended: "2031-02-28" }
|
|
48
|
+
"16": { standard: "2029-02-28", extended: "2032-02-29" }
|
|
49
|
+
"17": { standard: "2030-02-28", extended: "2033-02-28" }
|
|
50
|
+
"18": { standard: "2031-02-28", extended: "2034-02-28" }
|
|
40
51
|
aurora-postgresql:
|
|
41
|
-
"11": { standard: "2024-02-29" }
|
|
42
|
-
"12": { standard: "2025-02-28" }
|
|
43
|
-
"13": { standard: "2026-02-28" }
|
|
44
|
-
"14": { standard: "2027-02-28" }
|
|
45
|
-
"15": { standard: "2028-02-29" }
|
|
46
|
-
"16": { standard: "2029-02-28" }
|
|
52
|
+
"11": { standard: "2024-02-29", extended: "2027-03-31" }
|
|
53
|
+
"12": { standard: "2025-02-28", extended: "2028-02-29" }
|
|
54
|
+
"13": { standard: "2026-02-28", extended: "2029-02-28" }
|
|
55
|
+
"14": { standard: "2027-02-28", extended: "2030-02-28" }
|
|
56
|
+
"15": { standard: "2028-02-29", extended: "2031-02-28" }
|
|
57
|
+
"16": { standard: "2029-02-28", extended: "2032-02-28" }
|
|
58
|
+
"17": { standard: "2030-02-28", extended: "2033-02-28" }
|
|
59
|
+
"18": { standard: "2031-02-28", extended: "2034-02-28" }
|
|
60
|
+
mysql:
|
|
61
|
+
"5.7": { standard: "2024-02-29", extended: "2029-06-30" }
|
|
62
|
+
"8.0": { standard: "2026-07-31", extended: "2029-07-31" }
|
|
63
|
+
"8.4": { standard: "2029-07-31", extended: "2032-07-31" }
|
|
47
64
|
aurora-mysql:
|
|
48
|
-
|
|
65
|
+
# Aurora MySQL version 2 (5.7) and version 3 (8.0). Note: Aurora's 8.0 runs longer than RDS MySQL 8.0.
|
|
66
|
+
"5.7": { standard: "2024-10-31", extended: "2029-06-30" }
|
|
67
|
+
"8.0": { standard: "2028-04-30", extended: "2029-07-31" }
|
|
68
|
+
|
|
69
|
+
elasticache:
|
|
70
|
+
source: https://docs.aws.amazon.com/AmazonElastiCache/latest/dg/extended-support-versions.html
|
|
71
|
+
cost: ElastiCache Extended Support adds a premium on top of the node price, rising in years 2 and 3.
|
|
72
|
+
engines:
|
|
73
|
+
redis:
|
|
74
|
+
"4": { standard: "2026-01-31", extended: "2029-01-31" }
|
|
75
|
+
"5": { standard: "2026-01-31", extended: "2029-01-31" }
|
|
76
|
+
"6": { standard: "2027-01-31", extended: "2030-01-31" }
|
|
77
|
+
"7": {}
|
|
78
|
+
valkey:
|
|
79
|
+
"7": {}
|
|
80
|
+
"8": {}
|
|
81
|
+
|
|
82
|
+
opensearch:
|
|
83
|
+
source: https://aws.amazon.com/blogs/big-data/amazon-opensearch-service-extends-version-lifecycle-support-timelines/
|
|
84
|
+
# From 2026-11-07, Extended Support for the "early" versions below costs as much as the instances
|
|
85
|
+
# themselves (the bill roughly doubles). Later versions pay $0.0065 per normalized instance-hour.
|
|
86
|
+
cost: OpenSearch Extended Support is billed per normalized instance-hour; for older versions, from 2026-11-07 it equals the instance price.
|
|
87
|
+
versions:
|
|
88
|
+
Elasticsearch_1.5: { standard: "2025-11-07", extended: "2027-11-07" }
|
|
89
|
+
Elasticsearch_2.3: { standard: "2025-11-07", extended: "2027-11-07" }
|
|
90
|
+
Elasticsearch_5.1: { standard: "2025-11-07", extended: "2027-11-07" }
|
|
91
|
+
Elasticsearch_5.3: { standard: "2025-11-07", extended: "2027-11-07" }
|
|
92
|
+
Elasticsearch_5.5: { standard: "2025-11-07", extended: "2027-11-07" }
|
|
93
|
+
Elasticsearch_5.6: { standard: "2025-11-07", extended: "2028-11-07" }
|
|
94
|
+
Elasticsearch_6.0: { standard: "2025-11-07", extended: "2027-11-07" }
|
|
95
|
+
Elasticsearch_6.2: { standard: "2025-11-07", extended: "2027-11-07" }
|
|
96
|
+
Elasticsearch_6.3: { standard: "2025-11-07", extended: "2027-11-07" }
|
|
97
|
+
Elasticsearch_6.4: { standard: "2025-11-07", extended: "2027-11-07" }
|
|
98
|
+
Elasticsearch_6.5: { standard: "2025-11-07", extended: "2027-11-07" }
|
|
99
|
+
Elasticsearch_6.7: { standard: "2025-11-07", extended: "2027-11-07" }
|
|
100
|
+
Elasticsearch_6.8: { standard: "2027-11-07", extended: "2030-11-07" }
|
|
101
|
+
Elasticsearch_7.1: { standard: "2025-11-07", extended: "2027-11-07" }
|
|
102
|
+
Elasticsearch_7.4: { standard: "2025-11-07", extended: "2027-11-07" }
|
|
103
|
+
Elasticsearch_7.7: { standard: "2025-11-07", extended: "2027-11-07" }
|
|
104
|
+
Elasticsearch_7.8: { standard: "2025-11-07", extended: "2027-11-07" }
|
|
105
|
+
Elasticsearch_7.9: { standard: "2027-11-07", extended: "2028-11-07" }
|
|
106
|
+
Elasticsearch_7.10: { standard: "2027-11-07", extended: "2030-11-07" }
|
|
107
|
+
OpenSearch_1.0: { standard: "2025-11-07", extended: "2027-11-07" }
|
|
108
|
+
OpenSearch_1.1: { standard: "2025-11-07", extended: "2027-11-07" }
|
|
109
|
+
OpenSearch_1.2: { standard: "2025-11-07", extended: "2027-11-07" }
|
|
110
|
+
OpenSearch_1.3: { standard: "2027-11-07", extended: "2030-11-07" }
|
|
111
|
+
OpenSearch_2.3: { standard: "2025-11-07", extended: "2027-11-07" }
|
|
112
|
+
OpenSearch_2.5: { standard: "2025-11-07", extended: "2027-11-07" }
|
|
113
|
+
OpenSearch_2.7: { standard: "2025-11-07", extended: "2027-11-07" }
|
|
114
|
+
OpenSearch_2.9: { standard: "2025-11-07", extended: "2027-11-07" }
|
|
115
|
+
OpenSearch_2.11: { standard: "2027-11-07", extended: "2028-11-07" }
|
|
116
|
+
OpenSearch_2.13: { standard: "2027-11-07", extended: "2028-11-07" }
|
|
117
|
+
OpenSearch_2.15: { standard: "2027-11-07", extended: "2028-11-07" }
|
|
118
|
+
OpenSearch_2.17: { standard: "2027-11-07", extended: "2028-11-07" }
|
|
119
|
+
OpenSearch_2.19: { standard: "2027-11-07", extended: "2030-11-07" }
|
|
120
|
+
OpenSearch_3.1: {}
|
|
121
|
+
OpenSearch_3.3: {}
|
|
122
|
+
|
|
123
|
+
msk:
|
|
124
|
+
source: https://docs.aws.amazon.com/msk/latest/developerguide/supported-kafka-versions.html
|
|
125
|
+
# No paid extension: after the end of support date MSK auto-upgrades the cluster, at any time.
|
|
126
|
+
versions:
|
|
127
|
+
"1.1.1": "2024-06-05"
|
|
128
|
+
"2.1.0": "2024-06-05"
|
|
129
|
+
"2.2.1": "2024-06-08"
|
|
130
|
+
"2.3.1": "2024-06-08"
|
|
131
|
+
"2.4.1": "2024-06-08"
|
|
132
|
+
"2.4.1.1": "2024-06-08"
|
|
133
|
+
"2.5.1": "2024-06-08"
|
|
134
|
+
"2.6.0": "2024-09-11"
|
|
135
|
+
"2.6.1": "2024-09-11"
|
|
136
|
+
"2.6.2": "2024-09-11"
|
|
137
|
+
"2.6.3": "2024-09-11"
|
|
138
|
+
"2.7.0": "2024-09-11"
|
|
139
|
+
"2.7.1": "2024-09-11"
|
|
140
|
+
"2.7.2": "2024-09-11"
|
|
141
|
+
"2.8.0": "2024-09-11"
|
|
142
|
+
"2.8.1": "2024-09-11"
|
|
143
|
+
"2.8.2.tiered": "2025-01-14"
|
|
144
|
+
"3.1.1": "2024-09-11"
|
|
145
|
+
"3.2.0": "2024-09-11"
|
|
146
|
+
"3.3.1": "2024-09-11"
|
|
147
|
+
"3.3.2": "2024-09-11"
|
|
148
|
+
"3.4.0": "2025-08-04"
|
|
149
|
+
"3.5.1": "2025-10-23"
|
|
150
|
+
"3.6.0": "2026-06-01"
|
|
151
|
+
"3.7.x": "2026-09-01"
|
|
152
|
+
"3.8.x": null
|
|
153
|
+
"3.9.x": null
|
|
154
|
+
"4.0.x": null
|
|
155
|
+
"4.1.x": null
|
|
156
|
+
"4.2.x": null
|
|
49
157
|
|
|
50
158
|
lambda:
|
|
51
159
|
source: https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html
|
|
52
|
-
#
|
|
160
|
+
# Deprecation date. After it: no security patches; AWS later blocks creating, then updating, functions.
|
|
53
161
|
runtimes:
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
162
|
+
# supported, with announced deprecation dates
|
|
163
|
+
nodejs24.x: "2028-04-30"
|
|
164
|
+
nodejs22.x: "2027-04-30"
|
|
165
|
+
python3.14: "2029-06-30"
|
|
166
|
+
python3.13: "2029-06-30"
|
|
167
|
+
python3.12: "2028-10-31"
|
|
168
|
+
python3.11: "2027-06-30"
|
|
169
|
+
python3.10: "2026-10-31"
|
|
170
|
+
java25: "2029-06-30"
|
|
171
|
+
java21: "2029-06-30"
|
|
172
|
+
java17.al2023: "2029-06-30"
|
|
173
|
+
java11.al2023: "2029-06-30"
|
|
174
|
+
java8.al2023: "2029-06-30"
|
|
175
|
+
java17: "2027-06-30"
|
|
176
|
+
java11: "2027-06-30"
|
|
177
|
+
java8.al2: "2027-06-30"
|
|
178
|
+
dotnet10: "2028-11-14"
|
|
179
|
+
dotnet9: "2026-11-10"
|
|
180
|
+
dotnet8: "2026-11-10"
|
|
181
|
+
ruby4.0: "2029-03-31"
|
|
182
|
+
ruby3.4: "2028-03-31"
|
|
183
|
+
ruby3.3: "2027-03-31"
|
|
184
|
+
provided.al2023: "2029-06-30"
|
|
185
|
+
# preview, no date yet
|
|
186
|
+
nodejs26.x: null
|
|
187
|
+
python3.15: null
|
|
188
|
+
# deprecated
|
|
189
|
+
provided.al2: "2026-07-31"
|
|
57
190
|
nodejs20.x: "2026-04-30"
|
|
58
|
-
python3.7: "2023-12-04"
|
|
59
|
-
python3.8: "2024-10-14"
|
|
60
|
-
python3.9: "2025-12-15"
|
|
61
|
-
ruby2.7: "2023-12-07"
|
|
62
191
|
ruby3.2: "2026-03-31"
|
|
192
|
+
python3.9: "2025-12-15"
|
|
193
|
+
nodejs18.x: "2025-09-01"
|
|
194
|
+
dotnet6: "2024-12-20"
|
|
195
|
+
python3.8: "2024-10-14"
|
|
196
|
+
nodejs16.x: "2024-06-12"
|
|
197
|
+
dotnet7: "2024-05-14"
|
|
63
198
|
java8: "2024-01-08"
|
|
64
199
|
go1.x: "2024-01-08"
|
|
65
200
|
provided: "2024-01-08"
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
201
|
+
ruby2.7: "2023-12-07"
|
|
202
|
+
nodejs14.x: "2023-12-04"
|
|
203
|
+
python3.7: "2023-12-04"
|
|
204
|
+
dotnetcore3.1: "2023-04-03"
|
|
205
|
+
nodejs12.x: "2023-03-31"
|
|
206
|
+
python3.6: "2022-07-18"
|
|
207
|
+
dotnet5.0: "2022-05-10"
|
|
208
|
+
dotnetcore2.1: "2022-01-05"
|
|
209
|
+
nodejs10.x: "2021-07-30"
|
|
210
|
+
ruby2.5: "2021-07-30"
|
|
211
|
+
python2.7: "2021-07-15"
|
package/src/cli.js
CHANGED
|
@@ -9,6 +9,7 @@ const HELP = `Usage: retirement-radar scan [options]
|
|
|
9
9
|
|
|
10
10
|
Options:
|
|
11
11
|
--region a,b Regions to scan (default: every enabled region)
|
|
12
|
+
--services a,b Only these: eks, rds, elasticache, opensearch, msk, lambda (default: all)
|
|
12
13
|
--profile name AWS CLI profile
|
|
13
14
|
--json Print findings as JSON
|
|
14
15
|
--all Also list resources that are fine
|
|
@@ -21,7 +22,7 @@ function parseArgs(argv) {
|
|
|
21
22
|
for (let i = 1; i < argv.length; i++) {
|
|
22
23
|
const a = argv[i];
|
|
23
24
|
if (a === '--json' || a === '--all') o[a.slice(2)] = true;
|
|
24
|
-
else if (['--region', '--profile', '--fail-on', '--data'].includes(a)) o[a.slice(2)] = argv[++i];
|
|
25
|
+
else if (['--region', '--profile', '--fail-on', '--data', '--services'].includes(a)) o[a.slice(2)] = argv[++i];
|
|
25
26
|
else throw new Error(`Unknown option ${a}`);
|
|
26
27
|
}
|
|
27
28
|
return o;
|
|
@@ -40,9 +41,14 @@ function awsRunner(profile) {
|
|
|
40
41
|
|
|
41
42
|
const DB_ENGINE = /^(aurora|mysql|postgres|mariadb|oracle|sqlserver)/;
|
|
42
43
|
|
|
43
|
-
|
|
44
|
+
const ALL_SERVICES = ['eks', 'rds', 'elasticache', 'opensearch', 'msk', 'lambda'];
|
|
45
|
+
|
|
46
|
+
async function collectRegion(region, aws, warn, services = ALL_SERVICES) {
|
|
44
47
|
const items = [];
|
|
45
|
-
const step = async (label, fn) => {
|
|
48
|
+
const step = async (label, fn) => {
|
|
49
|
+
if (!services.includes(label.toLowerCase())) return;
|
|
50
|
+
try { await fn(); } catch (e) { warn(`${region} ${label}: ${e.message}`); }
|
|
51
|
+
};
|
|
46
52
|
await Promise.all([
|
|
47
53
|
step('EKS', async () => {
|
|
48
54
|
const { clusters = [] } = await aws(['eks', 'list-clusters'], region);
|
|
@@ -66,16 +72,47 @@ async function collectRegion(region, aws, warn) {
|
|
|
66
72
|
const { Functions = [] } = await aws(['lambda', 'list-functions'], region);
|
|
67
73
|
for (const f of Functions) if (f.Runtime) items.push({ service: 'Lambda', region, resource: f.FunctionName, version: f.Runtime });
|
|
68
74
|
}),
|
|
75
|
+
step('ElastiCache', async () => {
|
|
76
|
+
const { CacheClusters = [] } = await aws(['elasticache', 'describe-cache-clusters'], region);
|
|
77
|
+
// Nodes of one replication group share its engine version: report the group once.
|
|
78
|
+
const seen = new Set();
|
|
79
|
+
for (const c of CacheClusters) {
|
|
80
|
+
const name = c.ReplicationGroupId || c.CacheClusterId;
|
|
81
|
+
if (seen.has(name)) continue;
|
|
82
|
+
seen.add(name);
|
|
83
|
+
items.push({ service: 'ElastiCache', region, resource: name, engine: c.Engine, version: c.EngineVersion });
|
|
84
|
+
}
|
|
85
|
+
const { ServerlessCaches = [] } = await aws(['elasticache', 'describe-serverless-caches'], region);
|
|
86
|
+
for (const c of ServerlessCaches) {
|
|
87
|
+
items.push({ service: 'ElastiCache', region, resource: c.ServerlessCacheName, engine: c.Engine, version: c.FullEngineVersion || c.MajorEngineVersion });
|
|
88
|
+
}
|
|
89
|
+
}),
|
|
90
|
+
step('OpenSearch', async () => {
|
|
91
|
+
const { DomainNames = [] } = await aws(['opensearch', 'list-domain-names'], region);
|
|
92
|
+
const names = DomainNames.map((d) => d.DomainName);
|
|
93
|
+
for (let i = 0; i < names.length; i += 5) { // describe-domains takes at most 5 names
|
|
94
|
+
const { DomainStatusList = [] } = await aws(['opensearch', 'describe-domains', '--domain-names', ...names.slice(i, i + 5)], region);
|
|
95
|
+
for (const d of DomainStatusList) items.push({ service: 'OpenSearch', region, resource: d.DomainName, version: d.EngineVersion });
|
|
96
|
+
}
|
|
97
|
+
}),
|
|
98
|
+
step('MSK', async () => {
|
|
99
|
+
const { ClusterInfoList = [] } = await aws(['kafka', 'list-clusters-v2'], region);
|
|
100
|
+
// Serverless clusters have no Kafka version to retire.
|
|
101
|
+
for (const c of ClusterInfoList) {
|
|
102
|
+
const v = c.Provisioned?.CurrentBrokerSoftwareInfo?.KafkaVersion;
|
|
103
|
+
if (v) items.push({ service: 'MSK', region, resource: c.ClusterName, version: v });
|
|
104
|
+
}
|
|
105
|
+
}),
|
|
69
106
|
]);
|
|
70
107
|
return items;
|
|
71
108
|
}
|
|
72
109
|
|
|
73
|
-
async function scan({ regions, aws, db, now = Date.now(), warn = () => {} }) {
|
|
110
|
+
async function scan({ regions, aws, db, now = Date.now(), warn = () => {}, services = ALL_SERVICES }) {
|
|
74
111
|
if (!regions || !regions.length) {
|
|
75
112
|
const { Regions = [] } = await aws(['ec2', 'describe-regions'], 'us-east-1');
|
|
76
113
|
regions = Regions.map((r) => r.RegionName);
|
|
77
114
|
}
|
|
78
|
-
const items = (await Promise.all(regions.map((r) => collectRegion(r, aws, warn)))).flat();
|
|
115
|
+
const items = (await Promise.all(regions.map((r) => collectRegion(r, aws, warn, services)))).flat();
|
|
79
116
|
return sortFindings(items.map((i) => evaluate(i, db, now)));
|
|
80
117
|
}
|
|
81
118
|
|
|
@@ -102,6 +139,9 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
102
139
|
let o;
|
|
103
140
|
try { o = parseArgs(argv); } catch (e) { console.error(e.message + '\n\n' + HELP); return 2; }
|
|
104
141
|
if (o.cmd !== 'scan') { console.log(HELP); return o.cmd ? 2 : 0; }
|
|
142
|
+
const services = o.services ? o.services.toLowerCase().split(',').map((x) => x.trim()) : ALL_SERVICES;
|
|
143
|
+
const bad = services.filter((x) => !ALL_SERVICES.includes(x));
|
|
144
|
+
if (bad.length) { console.error(`Unknown service: ${bad.join(', ')}. Use: ${ALL_SERVICES.join(', ')}`); return 2; }
|
|
105
145
|
const db = loadRetirements(o.data);
|
|
106
146
|
const warnings = [];
|
|
107
147
|
let findings;
|
|
@@ -111,7 +151,7 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
111
151
|
const id = await aws(['sts', 'get-caller-identity']);
|
|
112
152
|
if (!o.json) console.log(paint(90, `Scanning AWS account ${id.Account} (read-only)...
|
|
113
153
|
`));
|
|
114
|
-
findings = await scan({ regions: o.region && o.region.split(','), aws, db, warn: (w) => warnings.push(w) });
|
|
154
|
+
findings = await scan({ regions: o.region && o.region.split(','), aws, db, services, warn: (w) => warnings.push(w) });
|
|
115
155
|
} catch (e) { console.error(`retirement-radar: ${e.message}`); return 2; }
|
|
116
156
|
|
|
117
157
|
if (o.json) {
|
|
@@ -126,7 +166,7 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
126
166
|
if (eksExtra) console.log(paint(31, `EKS extended support is costing about $${eksExtra.toLocaleString()} a month.`));
|
|
127
167
|
if (count('unknown')) console.log(paint(90, 'Unknown = version not in retirements.yaml yet. PRs with a source link welcome.'));
|
|
128
168
|
if (!o.all && count('ok')) console.log(paint(90, `${count('ok')} fine (more than a year left); --all lists them.`));
|
|
129
|
-
console.log(paint(90, `Dates updated ${db.updated}.
|
|
169
|
+
console.log(paint(90, `Dates updated ${db.updated}. Every date links its AWS source in retirements.yaml.`));
|
|
130
170
|
for (const w of warnings) console.error(paint(33, `warning: ${w}`));
|
|
131
171
|
}
|
|
132
172
|
return o['fail-on'] && failsAt(findings, o['fail-on']) ? 1 : 0;
|
package/src/radar.js
CHANGED
|
@@ -23,37 +23,75 @@ function majorVersion(engine, version) {
|
|
|
23
23
|
const daysUntil = (date, now) => Math.ceil((Date.parse(date) - now) / DAY);
|
|
24
24
|
const byDays = (days) => (days <= SOON_DAYS ? 'soon' : days <= UPCOMING_DAYS ? 'upcoming' : 'ok');
|
|
25
25
|
|
|
26
|
-
//
|
|
26
|
+
// MSK reports "3.7.x.kraft"; the calendar lists "3.7.x".
|
|
27
|
+
const mskVersion = (v) => String(v || '').replace(/\.kraft$/, '');
|
|
28
|
+
|
|
29
|
+
// Per service: where its dates live, and what each status means for it in plain words.
|
|
30
|
+
const SERVICES = {
|
|
31
|
+
EKS: {
|
|
32
|
+
lookup: (db, i) => db.eks.versions[i.version],
|
|
33
|
+
source: (db) => db.eks.source,
|
|
34
|
+
retired: () => 'Extended support ended; AWS force-upgrades the control plane',
|
|
35
|
+
extended: (db) => `Paid extended support until this date, then AWS force-upgrades. ${db.eks.cost}`,
|
|
36
|
+
upcoming: () => 'Standard support ends; extended support charges start',
|
|
37
|
+
monthlyCost: (db) => db.eks.monthlyCost,
|
|
38
|
+
},
|
|
39
|
+
RDS: {
|
|
40
|
+
lookup: (db, i) => (db.rds.engines[i.engine] || {})[majorVersion(i.engine, i.version)],
|
|
41
|
+
source: (db, i) => (db.rds.sources || {})[i.engine] || db.rds.source,
|
|
42
|
+
retired: () => 'RDS Extended Support has ended; RDS upgrades the database',
|
|
43
|
+
extended: (db) => `Paid Extended Support until this date. ${db.rds.cost}`,
|
|
44
|
+
upcoming: () => 'Standard support ends; Extended Support billing starts',
|
|
45
|
+
},
|
|
46
|
+
ElastiCache: {
|
|
47
|
+
lookup: (db, i) => (db.elasticache.engines[i.engine] || {})[String(i.version || '').split('.')[0]],
|
|
48
|
+
source: (db) => db.elasticache.source,
|
|
49
|
+
retired: () => 'Extended Support has ended; the version is end of life',
|
|
50
|
+
extended: (db) => `Paid Extended Support until this date. ${db.elasticache.cost}`,
|
|
51
|
+
upcoming: () => 'Standard support ends; Extended Support charges start',
|
|
52
|
+
},
|
|
53
|
+
OpenSearch: {
|
|
54
|
+
lookup: (db, i) => db.opensearch.versions[i.version],
|
|
55
|
+
source: (db) => db.opensearch.source,
|
|
56
|
+
retired: () => 'Extended Support has ended; upgrade required',
|
|
57
|
+
extended: (db) => `Paid Extended Support until this date. ${db.opensearch.cost}`,
|
|
58
|
+
upcoming: () => 'Standard support ends; Extended Support charges start',
|
|
59
|
+
},
|
|
60
|
+
MSK: {
|
|
61
|
+
lookup: (db, i) => db.msk.versions[mskVersion(i.version)],
|
|
62
|
+
source: (db) => db.msk.source,
|
|
63
|
+
retired: () => 'Past end of support: MSK can auto-upgrade the cluster at any time',
|
|
64
|
+
upcoming: () => 'End of support; MSK auto-upgrades the cluster after this date',
|
|
65
|
+
},
|
|
66
|
+
Lambda: {
|
|
67
|
+
lookup: (db, i) => db.lambda.runtimes[i.version],
|
|
68
|
+
source: (db) => db.lambda.source,
|
|
69
|
+
retired: () => 'Runtime deprecated: no security patches',
|
|
70
|
+
upcoming: () => 'Runtime deprecation',
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// item: { service, region, resource, version, engine? }
|
|
27
75
|
function evaluate(item, db, now = Date.now()) {
|
|
76
|
+
const svc = SERVICES[item.service];
|
|
28
77
|
const f = { ...item, status: 'unknown', date: null, days: null, note: 'Version not in retirements.yaml', source: null };
|
|
29
|
-
if (item.service
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
39
|
-
if (
|
|
40
|
-
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const std = daysUntil(v.standard, now);
|
|
45
|
-
if (std < 0) return { ...f, status: 'extended', date: v.standard, days: std, note: `Past standard support. ${db.rds.cost}` };
|
|
46
|
-
return { ...f, status: byDays(std), date: v.standard, days: std, note: 'Standard support ends; Extended Support billing starts' };
|
|
47
|
-
}
|
|
48
|
-
if (item.service === 'Lambda') {
|
|
49
|
-
const date = db.lambda.runtimes[item.version];
|
|
50
|
-
f.source = db.lambda.source;
|
|
51
|
-
if (!date) return f;
|
|
52
|
-
const d = daysUntil(date, now);
|
|
53
|
-
if (d < 0) return { ...f, status: 'retired', date, days: d, note: 'Runtime deprecated: no security patches' };
|
|
54
|
-
return { ...f, status: byDays(d), date, days: d, note: 'Runtime deprecation' };
|
|
78
|
+
if (!svc || !db[item.service.toLowerCase()]) return f;
|
|
79
|
+
f.source = svc.source(db, item);
|
|
80
|
+
const raw = svc.lookup(db, item);
|
|
81
|
+
if (raw === undefined) return f; // not listed: unknown, never fine
|
|
82
|
+
const v = typeof raw === 'string' ? { standard: raw } : raw || {};
|
|
83
|
+
if (!v.standard) return { ...f, status: 'ok', note: 'No end of support announced' };
|
|
84
|
+
|
|
85
|
+
const std = daysUntil(v.standard, now);
|
|
86
|
+
const ext = v.extended ? daysUntil(v.extended, now) : null;
|
|
87
|
+
if (ext !== null && ext < 0) return { ...f, status: 'retired', date: v.extended, days: ext, note: svc.retired(db) };
|
|
88
|
+
if (std < 0) {
|
|
89
|
+
if (ext === null || !svc.extended) return { ...f, status: 'retired', date: v.standard, days: std, note: svc.retired(db) };
|
|
90
|
+
const out = { ...f, status: 'extended', date: v.extended, days: ext, note: svc.extended(db) };
|
|
91
|
+
if (svc.monthlyCost) out.monthlyCost = svc.monthlyCost(db);
|
|
92
|
+
return out;
|
|
55
93
|
}
|
|
56
|
-
return f;
|
|
94
|
+
return { ...f, status: byDays(std), date: v.standard, days: std, note: svc.upcoming(db) };
|
|
57
95
|
}
|
|
58
96
|
|
|
59
97
|
const sortFindings = (list) => [...list].sort((a, b) =>
|
|
@@ -65,4 +103,4 @@ const failsAt = (findings, level) => {
|
|
|
65
103
|
return max >= 0 && findings.some((f) => f.status !== 'unknown' && SEVERITY.indexOf(f.status) <= max);
|
|
66
104
|
};
|
|
67
105
|
|
|
68
|
-
module.exports = { SEVERITY, loadRetirements, majorVersion, evaluate, sortFindings, failsAt };
|
|
106
|
+
module.exports = { SEVERITY, SERVICES, loadRetirements, majorVersion, mskVersion, evaluate, sortFindings, failsAt };
|