-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmain.go
493 lines (415 loc) · 13.3 KB
/
main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
package main
import (
"encoding/json"
"fmt"
"io"
"log"
"os"
"sort"
"strings"
"sync"
"time"
"github.com/ansrivas/fiberprometheus/v2"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/logger"
"github.com/hashicorp/go-version"
"github.com/prometheus/client_golang/prometheus"
)
// Platform defines the compatibility of Kubernetes versions with a Rancher version
type Platform struct {
Platform string `json:"platform"`
MinVersion string `json:"min_version"`
MaxVersion string `json:"max_version"`
Notes string `json:"notes,omitempty"`
}
// RancherManagerVersion contains supported platforms for each Rancher version
type RancherManagerVersion struct {
SupportedPlatforms []Platform `json:"supported_platforms"`
}
// UpgradePaths stores all Rancher versions and their compatibility data
type UpgradePaths struct {
RancherManager map[string]RancherManagerVersion `json:"rancher_manager"`
}
// UpgradeStep represents a single upgrade step
type UpgradeStep struct {
Type string `json:"type"` // Rancher or Kubernetes
Platform string `json:"platform"` // RKE1, RKE2, etc.
From string `json:"from"` // Previous version
To string `json:"to"` // New version
}
// Custom metrics
var (
totalRequestsLast60Seconds prometheus.Gauge
versionsSubmitted *prometheus.CounterVec
requestDuration prometheus.Histogram
activeRequests prometheus.Gauge
// For tracking request timestamps
requestTimestamps []time.Time
mu sync.Mutex
)
// Initialize custom metrics
func initMetrics() {
totalRequestsLast60Seconds = prometheus.NewGauge(prometheus.GaugeOpts{
Name: "requests_in_last_60_seconds",
Help: "Number of requests in the last 60 seconds",
})
versionsSubmitted = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "versions_submitted_total",
Help: "Total number of versions submitted",
},
[]string{"platform", "rancher_version", "k8s_version"},
)
requestDuration = prometheus.NewHistogram(prometheus.HistogramOpts{
Name: "request_duration_seconds",
Help: "Histogram of response latency (seconds) of requests.",
Buckets: prometheus.DefBuckets,
})
activeRequests = prometheus.NewGauge(prometheus.GaugeOpts{
Name: "active_requests",
Help: "Current number of active requests.",
})
// Register custom metrics with Prometheus
prometheus.MustRegister(
totalRequestsLast60Seconds,
versionsSubmitted,
requestDuration,
activeRequests,
)
}
// LoadUpgradePaths loads the upgrade paths from the JSON file
func LoadUpgradePaths() (UpgradePaths, error) {
file, err := os.Open("./data/upgrade-paths.json")
if err != nil {
return UpgradePaths{}, fmt.Errorf("failed to load upgrade paths: %v", err)
}
defer file.Close()
bytes, err := io.ReadAll(file)
if err != nil {
return UpgradePaths{}, fmt.Errorf("failed to read upgrade paths file: %v", err)
}
var paths UpgradePaths
err = json.Unmarshal(bytes, &paths)
if err != nil {
return UpgradePaths{}, fmt.Errorf("failed to parse upgrade paths JSON: %v", err)
}
return paths, nil
}
// PlanUpgrade generates the Rancher + Kubernetes upgrade plan
func PlanUpgrade(currentRancher, currentK8s, platform string, versions []string, paths UpgradePaths) ([]UpgradeStep, error) {
var upgradeSteps []UpgradeStep
keyVersions := GetKeyVersions(versions)
// Normalize platform name to lowercase for consistent comparison
platformLower := strings.ToLower(platform)
currentRancherVersion, err := version.NewVersion(currentRancher)
if err != nil {
return nil, fmt.Errorf("invalid current Rancher version: %v", err)
}
for _, v := range keyVersions {
nextVersion, err := version.NewVersion(v)
if err != nil {
return nil, fmt.Errorf("invalid version in key versions: %v", err)
}
if nextVersion.GreaterThan(currentRancherVersion) {
// Add Rancher upgrade step
upgradeSteps = append(upgradeSteps, UpgradeStep{
Type: "Rancher", From: currentRancher, To: v,
})
// Get Kubernetes upgrades for this Rancher version
r1 := paths.RancherManager[currentRancher]
r2 := paths.RancherManager[v]
k8sUpgrades := GetAllowedK8sUpgrades(currentK8s, platformLower, r1, r2)
// Add Kubernetes upgrade steps
for _, upgrade := range k8sUpgrades {
upgradeSteps = append(upgradeSteps, upgrade)
currentK8s = upgrade.To // Update current Kubernetes version
}
currentRancher = v // Update current Rancher version
currentRancherVersion = nextVersion // Update current Rancher version object
}
}
return upgradeSteps, nil
}
// GetAllowedK8sUpgrades determines the Kubernetes upgrade path based on platform rules
func GetAllowedK8sUpgrades(currentK8s, platform string, r1, r2 RancherManagerVersion) []UpgradeStep {
var upgrades []UpgradeStep
k8sVersions := getSortedK8sVersions(platform, r1, r2)
currentVer, err := parseK8sVersion(currentK8s)
if err != nil {
return upgrades
}
// Ensure current version is in the list
if !versionInList(currentVer, k8sVersions) {
k8sVersions = append(k8sVersions, currentVer)
sort.Sort(version.Collection(k8sVersions))
}
// Decide whether to allow skipping minor versions based on platform
allowSkip := platform == "rke1" || platform == "rke2" || platform == "k3s"
for {
nextVer := findNextAcceptableK8sVersion(currentVer, k8sVersions, allowSkip)
if nextVer == nil {
break
}
upgrades = append(upgrades, UpgradeStep{
Type: "Kubernetes",
Platform: platform,
From: "v" + currentVer.Original(),
To: "v" + nextVer.Original(),
})
currentVer = nextVer
}
return upgrades
}
// findNextAcceptableK8sVersion finds the next acceptable Kubernetes version
func findNextAcceptableK8sVersion(currentVer *version.Version, k8sVersions []*version.Version, allowSkip bool) *version.Version {
currentSegments := currentVer.Segments()
if len(currentSegments) < 2 {
return nil
}
currentMinor := currentSegments[1]
maxAllowedMinor := currentMinor + 1
if allowSkip {
maxAllowedMinor = currentMinor + 2
}
var candidate *version.Version
for _, v := range k8sVersions {
if v.LessThanOrEqual(currentVer) {
continue
}
nextSegments := v.Segments()
if len(nextSegments) < 2 {
continue
}
nextMinor := nextSegments[1]
if nextMinor > maxAllowedMinor {
break // No further versions are acceptable
}
candidate = v // Update candidate to the current acceptable version
if !allowSkip {
// For platforms that do not allow skipping, return the first acceptable version immediately
break
}
}
return candidate
}
// Checks if a version is in the list
func versionInList(ver *version.Version, list []*version.Version) bool {
for _, v := range list {
if v.Equal(ver) {
return true
}
}
return false
}
// getSortedK8sVersions retrieves and sorts the Kubernetes versions for the given platform
func getSortedK8sVersions(platform string, r1, r2 RancherManagerVersion) []*version.Version {
versionSet := make(map[string]*version.Version)
platforms := append(r1.SupportedPlatforms, r2.SupportedPlatforms...)
platformLower := strings.ToLower(platform)
for _, p := range platforms {
pPlatformLower := strings.ToLower(p.Platform)
if pPlatformLower == platformLower {
minVerStr := cleanVersion(p.MinVersion)
maxVerStr := cleanVersion(p.MaxVersion)
minVer, err := version.NewVersion(minVerStr)
if err != nil {
continue
}
maxVer, err := version.NewVersion(maxVerStr)
if err != nil {
continue
}
// Generate all minor versions between minVer and maxVer
versionsBetween := getMinorVersionsBetween(minVer, maxVer, p)
for _, v := range versionsBetween {
versionSet[v.Original()] = v
}
}
}
// Convert map to slice
var versionList []*version.Version
for _, v := range versionSet {
versionList = append(versionList, v)
}
// Sort the versions
sort.Sort(version.Collection(versionList))
return versionList
}
// getMinorVersionsBetween returns all minor versions between min and max versions, including exact versions from data
func getMinorVersionsBetween(minVer, maxVer *version.Version, platformData Platform) []*version.Version {
var versions []*version.Version
// Include exact min and max versions with their metadata
minVerWithMeta, err := version.NewVersion(cleanVersion(platformData.MinVersion))
if err == nil {
versions = append(versions, minVerWithMeta)
}
maxVerWithMeta, err := version.NewVersion(cleanVersion(platformData.MaxVersion))
if err == nil && !maxVerWithMeta.Equal(minVerWithMeta) {
versions = append(versions, maxVerWithMeta)
}
// Generate intermediate minor versions
currentVer := minVer
for {
// Increment minor version
segments := currentVer.Segments()
if len(segments) < 2 {
break
}
major := segments[0]
minor := segments[1]
newMinor := minor + 1
newVerStr := fmt.Sprintf("%d.%d.0", major, newMinor)
newVer, err := version.NewVersion(newVerStr)
if err != nil {
break
}
if newVer.GreaterThan(maxVer) {
break
}
versions = append(versions, newVer)
currentVer = newVer
}
return versions
}
// cleanVersion removes the "v" prefix from a version string
func cleanVersion(v string) string {
v = strings.TrimPrefix(v, "v")
return v
}
// parseK8sVersion parses a Kubernetes version string
func parseK8sVersion(v string) (*version.Version, error) {
cleaned := cleanVersion(v)
ver, err := version.NewVersion(cleaned)
if err != nil {
log.Printf("Error parsing Kubernetes version '%s': %v", v, err)
return nil, err
}
return ver, nil
}
// GetKeyVersions returns the key Rancher versions for the upgrade plan
func GetKeyVersions(versions []string) []string {
var keyVersions []*version.Version
for _, v := range versions {
if strings.HasSuffix(v, ".9") || v == "2.7.5" || v == "2.8.8" || v == "2.9.2" {
ver, err := version.NewVersion(v)
if err != nil {
continue
}
keyVersions = append(keyVersions, ver)
}
}
// Sort the versions
sort.Sort(version.Collection(keyVersions))
// Convert back to string slices
sortedKeyVersions := make([]string, len(keyVersions))
for i, v := range keyVersions {
sortedKeyVersions[i] = v.String()
}
return sortedKeyVersions
}
// Main application entry point
func main() {
// Initialize custom metrics
initMetrics()
// Main application Fiber instance
app := fiber.New()
// Add the logger middleware
app.Use(logger.New(logger.Config{
Format: "[${time}] ${ip} ${status} - ${latency} ${method} ${path}\n",
TimeFormat: "2006-01-02 15:04:05",
TimeZone: "Local",
}))
// Load upgrade paths
upgradePaths, err := LoadUpgradePaths()
if err != nil {
log.Fatalf("Error loading upgrade paths: %v", err)
}
app.Static("/", "./static")
app.Get("/healthz", func(c *fiber.Ctx) error {
return c.SendString("OK")
})
// API route to generate the upgrade plan
app.Get("/api/plan-upgrade/:platform/:rancher/:k8s", func(c *fiber.Ctx) error {
// Start timer
timer := prometheus.NewTimer(requestDuration)
defer timer.ObserveDuration()
// Increment active requests gauge
activeRequests.Inc()
defer activeRequests.Dec()
// Handle request timestamps for sliding window
updateRequestTimestamps()
platform := c.Params("platform")
currentRancher := c.Params("rancher")
currentK8s := c.Params("k8s")
// Increment versions submitted counter
versionsSubmitted.WithLabelValues(platform, currentRancher, currentK8s).Inc()
var versions []string
for v := range upgradePaths.RancherManager {
versions = append(versions, v)
}
// Sort versions using semantic versioning
parsedVersions := make([]*version.Version, 0, len(versions))
for _, v := range versions {
ver, err := version.NewVersion(v)
if err != nil {
continue
}
parsedVersions = append(parsedVersions, ver)
}
sort.Sort(version.Collection(parsedVersions))
// Convert back to string slices
sortedKeyVersions := make([]string, len(parsedVersions))
for i, v := range parsedVersions {
sortedKeyVersions[i] = v.String()
}
upgradePath, err := PlanUpgrade(currentRancher, currentK8s, platform, sortedKeyVersions, upgradePaths)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": err.Error(),
})
}
return c.JSON(fiber.Map{
"upgrade_path": upgradePath,
})
})
// Start the metrics server on port 9000
go startMetricsServer()
// Start the main application on port 3000
log.Fatal(app.Listen(":3000"))
}
// updateRequestTimestamps handles the sliding window of request timestamps
func updateRequestTimestamps() {
mu.Lock()
defer mu.Unlock()
now := time.Now()
requestTimestamps = append(requestTimestamps, now)
// Remove timestamps older than 60 seconds
cutoff := now.Add(-60 * time.Second)
idx := 0
for i, t := range requestTimestamps {
if t.After(cutoff) {
idx = i
break
}
}
requestTimestamps = requestTimestamps[idx:]
// Update the gauge
totalRequestsLast60Seconds.Set(float64(len(requestTimestamps)))
}
// startMetricsServer starts a separate Fiber app to serve metrics on port 9000
func startMetricsServer() {
metricsApp := fiber.New()
// Set up Prometheus middleware
prometheusMiddleware := fiberprometheus.New("fiber_app")
prometheusMiddleware.RegisterAt(metricsApp, "/metrics")
metricsApp.Use(prometheusMiddleware.Middleware)
// Expose /metrics endpoint
metricsApp.Get("/metrics", func(c *fiber.Ctx) error {
// The Prometheus middleware handles this
return nil
})
// Start the metrics server
if err := metricsApp.Listen(":9000"); err != nil {
log.Fatalf("Failed to start metrics server: %v", err)
}
}