diff --git a/cmd/multi_service_engine_versions.go b/cmd/multi_service_engine_versions.go index a27b80a72..97c8c05a5 100644 --- a/cmd/multi_service_engine_versions.go +++ b/cmd/multi_service_engine_versions.go @@ -420,6 +420,9 @@ func extractNumericPrefix(s string) string { // 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). +// An extended-support entry with no start date is an anomalous payload: it is +// skipped with a warning rather than read as "already started", and if no +// other entry proves extended support is active, known is false (issue #1315). func isInExtendedSupport(engine, fullVersion string, versionInfo map[string]MajorEngineVersionInfo) (extended, known bool) { majorVersion := extractMajorVersion(engine, fullVersion) if majorVersion == "" { @@ -439,10 +442,19 @@ func isInExtendedSupport(engine, fullVersion string, versionInfo map[string]Majo // Check if current date falls within extended support period now := time.Now() + missingStart := false for _, lifecycle := range info.SupportedEngineLifecycles { if lifecycle.LifecycleSupportName != string(rdstypes.LifecycleSupportNameOpenSourceRdsExtendedSupport) { continue } + // A zero start date would pass the start gate below and claim the + // window is open with no evidence; skip it loudly (issue #1315). + if lifecycle.LifecycleSupportStartDate.IsZero() { + missingStart = true + log.Printf("⚠️ Extended-support lifecycle for %s %s has no start date: ignoring the entry, extended-support window unknown", + normalizedEngine, majorVersion) + continue + } // Not in extended support before its start date if now.Before(lifecycle.LifecycleSupportStartDate) { continue @@ -454,7 +466,7 @@ func isInExtendedSupport(engine, fullVersion string, versionInfo map[string]Majo } } - return false, true + return false, !missingStart } // adjustRecommendationForExcludedVersions reduces the instance count in a recommendation diff --git a/cmd/multi_service_engine_versions_test.go b/cmd/multi_service_engine_versions_test.go index 7c452fd95..a34155be4 100644 --- a/cmd/multi_service_engine_versions_test.go +++ b/cmd/multi_service_engine_versions_test.go @@ -745,6 +745,91 @@ func TestIsInExtendedSupport(t *testing.T) { expected: true, expectedKnown: true, }, + { + // Issue #1315: both dates absent must not read as "in extended support". + name: "Zero start and zero end date - unknown, not silently true", + engine: "mysql", + version: "5.7.44", + versionInfo: map[string]MajorEngineVersionInfo{ + "mysql:5.7": { + Engine: "mysql", + MajorEngineVersion: "5.7", + SupportedEngineLifecycles: []EngineLifecycleInfo{ + { + LifecycleSupportName: string(rdstypes.LifecycleSupportNameOpenSourceRdsExtendedSupport), + }, + }, + }, + }, + expected: false, + expectedKnown: false, + }, + { + // Issue #1315: a missing start date is skipped even with a future end date. + name: "Zero start date with future end date - unknown, not silently true", + engine: "mysql", + version: "5.7.44", + versionInfo: map[string]MajorEngineVersionInfo{ + "mysql:5.7": { + Engine: "mysql", + MajorEngineVersion: "5.7", + SupportedEngineLifecycles: []EngineLifecycleInfo{ + { + LifecycleSupportName: string(rdstypes.LifecycleSupportNameOpenSourceRdsExtendedSupport), + LifecycleSupportEndDate: futureDate, + }, + }, + }, + }, + expected: false, + expectedKnown: false, + }, + { + // A well-formed active entry still proves extended support even + // when another extended entry is missing its start date. + name: "Zero start entry alongside a valid active entry", + engine: "mysql", + version: "5.7.44", + versionInfo: map[string]MajorEngineVersionInfo{ + "mysql:5.7": { + Engine: "mysql", + MajorEngineVersion: "5.7", + SupportedEngineLifecycles: []EngineLifecycleInfo{ + { + LifecycleSupportName: string(rdstypes.LifecycleSupportNameOpenSourceRdsExtendedSupport), + LifecycleSupportEndDate: futureDate, + }, + { + LifecycleSupportName: string(rdstypes.LifecycleSupportNameOpenSourceRdsExtendedSupport), + LifecycleSupportStartDate: pastDate, + LifecycleSupportEndDate: futureDate, + }, + }, + }, + }, + expected: true, + expectedKnown: true, + }, + { + // A zero start date on a non-extended entry is irrelevant. + name: "Zero start date on standard support entry is ignored", + engine: "mysql", + version: "8.0.35", + versionInfo: map[string]MajorEngineVersionInfo{ + "mysql:8.0": { + Engine: "mysql", + MajorEngineVersion: "8.0", + SupportedEngineLifecycles: []EngineLifecycleInfo{ + { + LifecycleSupportName: string(rdstypes.LifecycleSupportNameOpenSourceRdsStandardSupport), + LifecycleSupportEndDate: futureDate, + }, + }, + }, + }, + expected: false, + expectedKnown: true, + }, { name: "Engine name normalization with spaces", engine: "Aurora MySQL", @@ -776,6 +861,53 @@ func TestIsInExtendedSupport(t *testing.T) { } } +// TestIsInExtendedSupport_ZeroStartDateWarns covers issue #1315: an +// extended-support entry with no start date is logged with the engine and +// major version, and the instance stays in the recommendation count. +func TestIsInExtendedSupport_ZeroStartDateWarns(t *testing.T) { + versionInfo := map[string]MajorEngineVersionInfo{ + "mysql:5.7": { + Engine: "mysql", + MajorEngineVersion: "5.7", + SupportedEngineLifecycles: []EngineLifecycleInfo{ + { + LifecycleSupportName: string(rdstypes.LifecycleSupportNameOpenSourceRdsExtendedSupport), + LifecycleSupportEndDate: time.Now().AddDate(1, 0, 0), + }, + }, + }, + } + recommendation := common.Recommendation{ + Service: common.ServiceRDS, + Region: "us-east-1", + ResourceType: "db.r5.large", + Count: 2, + Details: &common.DatabaseDetails{Engine: "MySQL"}, + } + instanceVersions := map[string][]InstanceEngineVersion{ + "db.r5.large": { + {Engine: "mysql", EngineVersion: "5.7.44", InstanceClass: "db.r5.large", Region: "us-east-1"}, + }, + } + + 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, 2, result.Count, "missing start date must not exclude the instance") + logged := logBuf.String() + assert.Contains(t, logged, "mysql 5.7 has no start date", "warning must name the engine and major version") + assert.Contains(t, logged, "No lifecycle data for mysql 5.7.44", "caller must surface the unknown status") +} + // TestQueryRunningInstanceEngineVersions_ProfileSelection asserts the // validation-profile precedence deterministically: the profiles name entries // absent from the (redirected, empty) shared config, so config loading fails