Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions cmd/multi_service_coverage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -272,37 +272,43 @@ func TestIsInExtendedSupport_EdgeCases(t *testing.T) {
engine string
fullVersion string
expectedExtended bool
expectedKnown bool
}{
{
name: "MySQL 5.7 in extended support",
engine: "mysql",
fullVersion: "5.7.44",
expectedExtended: true,
expectedKnown: true,
},
{
name: "MySQL 8.0 in standard support",
engine: "mysql",
fullVersion: "8.0.35",
expectedExtended: false,
expectedKnown: true,
},
{
name: "Unknown version",
engine: "mysql",
fullVersion: "9.0.0",
expectedExtended: false,
expectedKnown: false,
},
{
name: "Empty version",
engine: "mysql",
fullVersion: "",
expectedExtended: false,
expectedKnown: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := isInExtendedSupport(tt.engine, tt.fullVersion, versionInfo)
assert.Equal(t, tt.expectedExtended, result)
extended, known := isInExtendedSupport(tt.engine, tt.fullVersion, versionInfo)
assert.Equal(t, tt.expectedExtended, extended)
assert.Equal(t, tt.expectedKnown, known, "known flag mismatch")
})
}
}
Expand Down
86 changes: 55 additions & 31 deletions cmd/multi_service_engine_versions.go
Original file line number Diff line number Diff line change
Expand Up @@ -378,10 +378,14 @@ func extractNumericPrefix(s string) string {
}

// isInExtendedSupport checks if a version is currently in extended support based on lifecycle dates.
func isInExtendedSupport(engine, fullVersion string, versionInfo map[string]MajorEngineVersionInfo) bool {
// The second return value reports whether lifecycle data was available for the
// lookup: known is false when the major version cannot be extracted or the
// engine:majorVersion key is missing from versionInfo. Callers on money paths
// must not treat known=false as "not in extended support" (issue #1313).
func isInExtendedSupport(engine, fullVersion string, versionInfo map[string]MajorEngineVersionInfo) (extended, known bool) {
majorVersion := extractMajorVersion(engine, fullVersion)
if majorVersion == "" {
return false
return false, false
}

// Normalize engine name for lookup
Expand All @@ -392,8 +396,7 @@ func isInExtendedSupport(engine, fullVersion string, versionInfo map[string]Majo
key := fmt.Sprintf("%s:%s", normalizedEngine, majorVersion)
info, exists := versionInfo[key]
if !exists {
// If we don't have info, assume not in extended support
return false
return false, false
}

// Check if current date falls within extended support period
Expand All @@ -409,11 +412,11 @@ func isInExtendedSupport(engine, fullVersion string, versionInfo map[string]Majo
// Past the end date means extended support is over; a zero end date
// is open-ended (issue #1182)
if lifecycle.LifecycleSupportEndDate.IsZero() || now.Before(lifecycle.LifecycleSupportEndDate) {
return true
return true, true
}
}

return false
return false, true
}

// adjustRecommendationForExcludedVersions reduces the instance count in a recommendation
Expand Down Expand Up @@ -445,33 +448,13 @@ func adjustRecommendationForExcludedVersions(rec common.Recommendation, instance
// Count how many instances in this region are running versions in extended support
excludedCount := 0

for _, version := range versions {
// Only count instances in the same region
if version.Region != rec.Region {
continue
}

// Match engine (normalize by removing spaces/hyphens and comparing lowercase)
normalizeEngine := func(engine string) string {
normalized := strings.ToLower(engine)
normalized = strings.ReplaceAll(normalized, "-", "")
normalized = strings.ReplaceAll(normalized, " ", "")
return normalized
}

versionEngineNorm := normalizeEngine(version.Engine)
recEngineNorm := normalizeEngine(recEngine)

if versionEngineNorm != recEngineNorm {
continue
}
// Warn once per engine:version when lifecycle data is missing instead of
// silently treating the instance as not on extended support (issue #1313).
unknownLogged := make(map[string]bool)

// Check if this version is in extended support
if isInExtendedSupport(version.Engine, version.EngineVersion, versionInfo) {
majorVersion := extractMajorVersion(version.Engine, version.EngineVersion)
for _, version := range versions {
if shouldExcludeForExtendedSupport(version, rec, recEngine, versionInfo, unknownLogged) {
excludedCount++
log.Printf("🚫 Found extended support instance: %s %s in %s running version %s (major version %s is in extended support)",
recEngine, rec.ResourceType, rec.Region, version.EngineVersion, majorVersion)
}
}

Expand All @@ -489,3 +472,44 @@ func adjustRecommendationForExcludedVersions(rec common.Recommendation, instance

return rec
}

// shouldExcludeForExtendedSupport reports whether a single running instance
// should be excluded from the recommendation because its engine version is in
// extended support. Instances in other regions or with other engines never
// match. When lifecycle data is missing the instance is kept (conservative
// skip) and a warning is logged once per engine:version via unknownLogged
// (issue #1313). Extracted from adjustRecommendationForExcludedVersions to
// keep both functions under the gocyclo cap.
func shouldExcludeForExtendedSupport(version InstanceEngineVersion, rec common.Recommendation, recEngine string, versionInfo map[string]MajorEngineVersionInfo, unknownLogged map[string]bool) bool {
// Only count instances in the same region
if version.Region != rec.Region {
return false
}

// Match engine (normalize by removing spaces/hyphens and comparing lowercase)
if normalizeEngineNameForVersion(version.Engine) != normalizeEngineNameForVersion(recEngine) {
return false
}

extended, known := isInExtendedSupport(version.Engine, version.EngineVersion, versionInfo)
if !known {
// Leave the instance in the count but make the gap visible so
// incomplete lifecycle data can't silently steer purchases (issue #1313).
warnKey := version.Engine + ":" + version.EngineVersion
if !unknownLogged[warnKey] {
unknownLogged[warnKey] = true
log.Printf("⚠️ No lifecycle data for %s %s in %s: extended-support status unknown, keeping instance in the count",
version.Engine, version.EngineVersion, version.Region)
}
return false
}

if !extended {
return false
}

majorVersion := extractMajorVersion(version.Engine, version.EngineVersion)
log.Printf("🚫 Found extended support instance: %s %s in %s running version %s (major version %s is in extended support)",
recEngine, rec.ResourceType, rec.Region, version.EngineVersion, majorVersion)
return true
}
119 changes: 98 additions & 21 deletions cmd/multi_service_engine_versions_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package main

import (
"bytes"
"context"
"log"
"strings"
"testing"
"time"

Expand Down Expand Up @@ -283,6 +286,69 @@ func TestAdjustRecommendationForExcludedVersions_TypedNilDetails(t *testing.T) {
})
}

// TestAdjustRecommendationForExcludedVersions_UnknownLifecycle pins the
// issue #1313 contract: when an instance's engine/major version is missing
// from versionInfo, the instance must NOT be silently treated as "not on
// extended support". The count stays untouched (conservative skip) and a
// warning naming the engine, version, and region is logged, once per
// engine:version pair.
func TestAdjustRecommendationForExcludedVersions_UnknownLifecycle(t *testing.T) {
recommendation := common.Recommendation{
Service: common.ServiceRDS,
Region: "us-east-1",
ResourceType: "db.r5.large",
Count: 4,
Details: &common.DatabaseDetails{
Engine: "MySQL",
},
}

// Two instances of the same unknown version plus one of another: the
// warning must be deduplicated per engine:version, not per instance.
instanceVersions := map[string][]InstanceEngineVersion{
"db.r5.large": {
{Engine: "mysql", EngineVersion: "9.0.1", InstanceClass: "db.r5.large", Region: "us-east-1"},
{Engine: "mysql", EngineVersion: "9.0.1", InstanceClass: "db.r5.large", Region: "us-east-1"},
{Engine: "mysql", EngineVersion: "10.1.0", InstanceClass: "db.r5.large", Region: "us-east-1"},
},
}

// versionInfo has data for mysql:8.0 only, so 9.x and 10.x lookups miss.
versionInfo := map[string]MajorEngineVersionInfo{
"mysql:8.0": {
Engine: "mysql",
MajorEngineVersion: "8.0",
SupportedEngineLifecycles: []EngineLifecycleInfo{
{
LifecycleSupportName: string(rdstypes.LifecycleSupportNameOpenSourceRdsStandardSupport),
LifecycleSupportStartDate: time.Now().AddDate(-2, 0, 0),
LifecycleSupportEndDate: time.Now().AddDate(3, 0, 0),
},
},
},
}

var logBuf bytes.Buffer
origOutput := log.Writer()
origFlags := log.Flags()
log.SetOutput(&logBuf)
log.SetFlags(0)
defer func() {
log.SetOutput(origOutput)
log.SetFlags(origFlags)
}()

result := adjustRecommendationForExcludedVersions(recommendation, instanceVersions, versionInfo)

assert.Equal(t, 4, result.Count, "unknown lifecycle status must leave the count untouched")

logged := logBuf.String()
assert.Contains(t, logged, "mysql 9.0.1", "warning must name the unknown engine and version")
assert.Contains(t, logged, "mysql 10.1.0", "warning must name the unknown engine and version")
assert.Contains(t, logged, "us-east-1", "warning must name the region")
assert.Equal(t, 2, strings.Count(logged, "No lifecycle data for"), "warning must be logged once per engine:version, not per instance")
}

func TestExtractMajorVersion_Additional(t *testing.T) {
tests := []struct {
name string
Expand Down Expand Up @@ -531,11 +597,12 @@ func TestIsInExtendedSupport(t *testing.T) {
futureDate := now.AddDate(3, 0, 0)

tests := []struct {
versionInfo map[string]MajorEngineVersionInfo
name string
engine string
version string
expected bool
versionInfo map[string]MajorEngineVersionInfo
name string
engine string
version string
expected bool
expectedKnown bool
}{
{
name: "Version in extended support",
Expand All @@ -554,7 +621,8 @@ func TestIsInExtendedSupport(t *testing.T) {
},
},
},
expected: true,
expected: true,
expectedKnown: true,
},
{
name: "Version not in extended support - still in standard support",
Expand All @@ -573,10 +641,11 @@ func TestIsInExtendedSupport(t *testing.T) {
},
},
},
expected: false,
expected: false,
expectedKnown: true,
},
{
name: "Version info not found",
name: "Version info not found - unknown, not silently false",
engine: "mysql",
version: "5.7.44",
versionInfo: map[string]MajorEngineVersionInfo{
Expand All @@ -585,14 +654,16 @@ func TestIsInExtendedSupport(t *testing.T) {
MajorEngineVersion: "13",
},
},
expected: false,
expected: false,
expectedKnown: false,
},
{
name: "Empty version info",
engine: "mysql",
version: "5.7.44",
versionInfo: map[string]MajorEngineVersionInfo{},
expected: false,
name: "Empty version info - unknown, not silently false",
engine: "mysql",
version: "5.7.44",
versionInfo: map[string]MajorEngineVersionInfo{},
expected: false,
expectedKnown: false,
},
{
name: "Extended support not started yet",
Expand All @@ -611,7 +682,8 @@ func TestIsInExtendedSupport(t *testing.T) {
},
},
},
expected: false,
expected: false,
expectedKnown: true,
},
{
name: "Extended support started on current date",
Expand All @@ -630,7 +702,8 @@ func TestIsInExtendedSupport(t *testing.T) {
},
},
},
expected: true,
expected: true,
expectedKnown: true,
},
{
name: "Extended support already ended",
Expand All @@ -649,7 +722,8 @@ func TestIsInExtendedSupport(t *testing.T) {
},
},
},
expected: false,
expected: false,
expectedKnown: true,
},
{
name: "Zero end date treated as open-ended",
Expand All @@ -667,7 +741,8 @@ func TestIsInExtendedSupport(t *testing.T) {
},
},
},
expected: true,
expected: true,
expectedKnown: true,
},
{
name: "Engine name normalization with spaces",
Expand All @@ -686,14 +761,16 @@ func TestIsInExtendedSupport(t *testing.T) {
},
},
},
expected: true,
expected: true,
expectedKnown: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := isInExtendedSupport(tt.engine, tt.version, tt.versionInfo)
assert.Equal(t, tt.expected, result)
extended, known := isInExtendedSupport(tt.engine, tt.version, tt.versionInfo)
assert.Equal(t, tt.expected, extended)
assert.Equal(t, tt.expectedKnown, known, "known flag mismatch")
})
}
}
Expand Down
Loading