diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index c1f0100..a97ee80 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -96,7 +96,15 @@ resources: eol: provider: endoflife-date # EOL data provider product: amazon-eks # endoflife.date product ID - schema: eks_adapter # Schema adapter (standard or eks_adapter) + schema: declarative # YAML-defined lifecycle semantics + lifecycle: + deprecation_date: + field: eol + extended_support_end: + field: extendedSupport + bool_true_fallback: eol + deprecated_window: extended_support + past_extended_support: unsupported ``` **Environment Variable:** @@ -292,12 +300,12 @@ type Provider interface { ``` **Implementations:** -- `endoflife.Provider` - endoflife.date HTTP API (config-driven via `eol.product` + `eol.schema`) +- `endoflife.Provider` - endoflife.date HTTP API (config-driven via `eol.product`, `eol.schema`, and optional `eol.lifecycle`) - `mock.EOLProvider` - For testing **Single-Source Strategy:** - All EOL data comes from endoflife.date — no cloud provider credentials required for lifecycle lookups. -- Per-product semantics are handled by schema adapters (`standard`, `eks_adapter`, …) selected from YAML. +- Per-product semantics are handled by either the built-in `standard` schema or a YAML-defined `declarative` lifecycle block. ### 3. VersionPolicy @@ -729,7 +737,8 @@ narrow — no expressions, one named op per field. Full reference: **EOL Configuration:** - `provider`: Currently only `endoflife-date` supported - `product`: The endoflife.date product ID (e.g., `postgresql`, `amazon-eks`) -- `schema`: Adapter for EOL data semantics (`standard` or `eks_adapter`) +- `schema`: EOL data semantics (`standard` or `declarative`) +- `lifecycle`: Required for `schema: declarative`; maps upstream fields into deprecation, extended-support, and EOL boundaries ### 3. Add Report ID to Environment Variable diff --git a/cmd/server/main.go b/cmd/server/main.go index f384c8e..909457f 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -275,7 +275,14 @@ func (s *ServerCLI) Run(_ *kong.Context) error { // Create EOL provider based on config var eolProvider eol.Provider if resourceCfg.EOL.Provider == "endoflife-date" { - provider, err := eolendoflife.NewProvider(eolHTTPClient, resourceCfg.EOL.Product, resourceCfg.EOL.Schema, cacheTTL, logger) + provider, err := eolendoflife.NewProviderWithLifecycle( + eolHTTPClient, + resourceCfg.EOL.Product, + resourceCfg.EOL.Schema, + resourceCfg.EOL.Lifecycle, + cacheTTL, + logger, + ) if err != nil { return fmt.Errorf("failed to create EOL provider for %s: %w", resourceCfg.ID, err) } diff --git a/pkg/config/defaults/resources.yaml b/pkg/config/defaults/resources.yaml index 01d3331..a02a864 100644 --- a/pkg/config/defaults/resources.yaml +++ b/pkg/config/defaults/resources.yaml @@ -111,7 +111,21 @@ resources: eol: provider: endoflife-date product: amazon-eks - schema: eks_adapter + schema: declarative + lifecycle: + # EKS cycle.eol is the end of standard support, not true EOL. + deprecation_date: + field: eol + # Current EKS data uses extendedSupport as a date; archived data + # used boolean true, so fall back to eol for replay safety. + extended_support_end: + field: extendedSupport + bool_true_fallback: eol + deprecated_window: extended_support + # EKS clusters keep running after extended support, but AWS stops + # patching them. Leave eol_date unset and classify post-extended as + # unsupported rather than true EOL. + past_extended_support: unsupported # ElastiCache is split per-engine because each engine has its own # endoflife.date product with different lifecycle semantics, and Wiz @@ -299,7 +313,19 @@ resources: eol: provider: endoflife-date product: aws-lambda - schema: standard + schema: declarative + lifecycle: + # Lambda uses "Standard Support" and "Deprecated Support" instead + # of extendedSupport. Treat the deprecated-support window as the + # same YELLOW warning state Version Guard uses for extended support. + deprecation_date: + field: support + extended_support_end: + field: eol + eol_date: + field: eol + deprecated_window: extended_support + past_extended_support: eol # To add a new resource type, follow skills/add-version-guard-resource. # Standalone copy-paste templates live in skills/add-version-guard-resource/examples/. diff --git a/pkg/config/loader.go b/pkg/config/loader.go index 1fe4b5d..cae978d 100644 --- a/pkg/config/loader.go +++ b/pkg/config/loader.go @@ -7,6 +7,7 @@ import ( "gopkg.in/yaml.v3" "github.com/block/Version-Guard/pkg/config/defaults" + "github.com/block/Version-Guard/pkg/eol/endoflife" ) // LoadResourcesConfig loads and parses the resources configuration. @@ -80,6 +81,9 @@ func validateConfig(config *ResourcesConfig) error { if resource.EOL.Product == "" { return errors.Errorf("resource[%d]: eol.product is required", i) } + if err := validateEOLConfig(resource); err != nil { + return errors.Wrapf(err, "resource[%d] %q", i, resource.ID) + } if err := validateMappings(resource); err != nil { return errors.Wrapf(err, "resource[%d] %q", i, resource.ID) } @@ -91,6 +95,35 @@ func validateConfig(config *ResourcesConfig) error { return nil } +func validateEOLConfig(resource *ResourceConfig) error { + if resource.EOL.Schema == endoflife.SchemaDeclarative && resource.EOL.Lifecycle == nil { + return errors.New("eol.lifecycle is required when eol.schema is declarative") + } + if resource.EOL.Lifecycle != nil { + if resource.EOL.Schema == "" { + resource.EOL.Schema = endoflife.SchemaDeclarative + } + if resource.EOL.Schema != endoflife.SchemaDeclarative { + return errors.New("eol.lifecycle requires eol.schema to be declarative") + } + if err := endoflife.ValidateDeclarativeLifecycleConfig(resource.EOL.Lifecycle); err != nil { + return errors.Wrap(err, "invalid eol.lifecycle") + } + return nil + } + if _, err := endoflife.GetSchemaAdapter(defaultSchema(resource.EOL.Schema)); err != nil { + return err + } + return nil +} + +func defaultSchema(schema string) string { + if schema == "" { + return endoflife.SchemaStandard + } + return schema +} + // validateMappings enforces three rules on a resource's // required_mappings / field_mappings split: // diff --git a/pkg/config/loader_test.go b/pkg/config/loader_test.go index c6d9be0..ec47218 100644 --- a/pkg/config/loader_test.go +++ b/pkg/config/loader_test.go @@ -102,7 +102,15 @@ resources: eol: provider: endoflife-date product: amazon-eks - schema: eks_adapter + schema: declarative + lifecycle: + deprecation_date: + field: eol + extended_support_end: + field: extendedSupport + bool_true_fallback: eol + deprecated_window: extended_support + past_extended_support: unsupported ` err := os.WriteFile(configFile, []byte(configContent), 0644) @@ -116,7 +124,9 @@ resources: assert.Equal(t, "aurora-postgresql", cfg.Resources[0].ID) assert.Equal(t, "eks", cfg.Resources[1].ID) assert.Equal(t, "standard", cfg.Resources[0].EOL.Schema) - assert.Equal(t, "eks_adapter", cfg.Resources[1].EOL.Schema) + assert.Equal(t, "declarative", cfg.Resources[1].EOL.Schema) + require.NotNil(t, cfg.Resources[1].EOL.Lifecycle) + assert.Equal(t, "eol", cfg.Resources[1].EOL.Lifecycle.DeprecationDate.Field) } // TestLoadResourcesConfig_EmbeddedDefault asserts the binary works diff --git a/pkg/config/types.go b/pkg/config/types.go index 7e6a709..d1d3f74 100644 --- a/pkg/config/types.go +++ b/pkg/config/types.go @@ -1,5 +1,7 @@ package config +import "github.com/block/Version-Guard/pkg/eol/endoflife" + // ResourcesConfig represents the root configuration structure type ResourcesConfig struct { Version string `yaml:"version"` @@ -48,7 +50,8 @@ type InventoryConfig struct { // EOLConfig defines EOL provider configuration type EOLConfig struct { - Provider string `yaml:"provider"` - Product string `yaml:"product"` - Schema string `yaml:"schema"` + Provider string `yaml:"provider"` + Product string `yaml:"product"` + Schema string `yaml:"schema"` + Lifecycle *endoflife.DeclarativeLifecycleConfig `yaml:"lifecycle,omitempty"` } diff --git a/pkg/eol/endoflife/ADAPTERS.md b/pkg/eol/endoflife/ADAPTERS.md index b182403..fd653cd 100644 --- a/pkg/eol/endoflife/ADAPTERS.md +++ b/pkg/eol/endoflife/ADAPTERS.md @@ -1,25 +1,21 @@ -# Schema Adapters — and why EKS still needs its own +# Lifecycle Schemas `endoflife.date` is the single upstream source for every EOL provider in -Version Guard, but it is **not** a uniform schema. Most products use -the "standard" cycle shape; a handful use product-specific semantics -where the same field name means a different thing. The `SchemaAdapter` -interface in [adapters.go](./adapters.go) is the seam where those -deviations are absorbed so the rest of Version Guard sees a single, -canonical `types.VersionLifecycle`. +Version Guard, but it is not a uniform schema. Most products use the +built-in `standard` schema. Products with different field semantics +should use `schema: declarative` plus an `eol.lifecycle` block in YAML. -This doc exists because EKS is the kind of deviation that will silently -mis-classify clusters in production if you wire it up the "obvious" -way. +The goal is the same pattern used by inventory transforms: product +quirks live next to the resource config, while Go provides a small set +of reusable operations. --- -## The standard schema (what most products look like) +## Standard Schema -Three real-world cycle shapes are all handled by the single -`StandardSchemaAdapter`: +Three real-world cycle shapes are handled by `StandardSchemaAdapter`. -### 1. Plain OSS (PostgreSQL, etc.) +### Plain OSS ```json { @@ -29,11 +25,10 @@ Three real-world cycle shapes are all handled by the single } ``` -`support` = end of standard support, `eol` = true end of life. There -is no extended-support concept; past `eol` is RED, past `support` but -before `eol` (if they differ) is YELLOW (deprecated). +`support` = end of standard support, `eol` = true end of life. There is +no extended-support concept. -### 2. Aurora pattern (support + eol + extendedSupport date) +### Support + Extended Support ```json { @@ -44,12 +39,11 @@ before `eol` (if they differ) is YELLOW (deprecated). } ``` -`support` = end of standard support, `extendedSupport` = end of paid -extended support and **the true terminal date**. Past `support` but -before `extendedSupport` is in extended support (YELLOW); past -`extendedSupport` is RED. +`support` = end of standard support. `extendedSupport` = end of paid +extended support and the true terminal date. Past `support` but before +`extendedSupport` is YELLOW; past `extendedSupport` is RED. -### 3. AWS ElastiCache / Aurora MySQL pattern (no `support` field) +### AWS Pattern Without `support` ```json { @@ -59,128 +53,126 @@ before `extendedSupport` is in extended support (YELLOW); past } ``` -There is no `support` field. Upstream uses `eol` as shorthand for -"end of standard support" *because* there's a real terminal date in -`extendedSupport`. The adapter recognizes the AWS pattern by -`extendedSupport` being a date, and treats `eol` as the -standard-support boundary, with `extendedSupport` as both the -extended-support end **and** the true EOL date. +When `extendedSupport` is a date and `support` is absent, the standard +adapter treats `eol` as the standard-support boundary and +`extendedSupport` as both the extended-support end and true EOL. -The same `StandardSchemaAdapter` handles all three shapes — see -`deriveBoundaries` in [adapters.go](./adapters.go) for the three-way -switch. - -| `VersionLifecycle` field | Sourced from | -| ------------------------ | ------------------------------------------------------------------------------- | -| `DeprecationDate` | `support` if present, else `eol` when `extendedSupport` is also present (AWS pattern). | -| `ExtendedSupportEnd` | `extendedSupport` (date), or legacy boolean `true` falling back to `eol`. | -| `EOLDate` | `extendedSupport` when set (true terminal); else `eol`; else nil. | +| `VersionLifecycle` field | Standard source | +| --- | --- | +| `DeprecationDate` | `support`, else `eol` when `extendedSupport` is also present | +| `ExtendedSupportEnd` | `extendedSupport` date, or legacy boolean `true` falling back to `eol` | +| `EOLDate` | `extendedSupport` when set, else `eol`, else nil | --- -## EKS — still its own adapter, but for narrower reasons now +## Declarative Schema -EKS still doesn't fit the standard schema because EKS clusters -**never truly stop working** — once you're past extended support, AWS -stops issuing patches but the control plane keeps running. There's no -"true EOL" for an EKS version, only "out of AWS support". +Use `schema: declarative` when a product's field names do not match the +standard semantics. The lifecycle block maps upstream fields into +Version Guard boundaries and names the status applied in each window. -`EKSSchemaAdapter` therefore: +Supported field names: -1. Maps `cycle.eol` → `DeprecationDate` (end of standard support; - start of paid extended support). -2. Maps `cycle.extendedSupport` (date) → `ExtendedSupportEnd`. -3. Hard-sets `lifecycle.EOLDate = nil` regardless of input. -4. Classifies past-extended-support as RED via - `IsDeprecated && !IsExtendedSupport` (same effect as the standard - adapter's terminal RED branch, but without claiming the cluster is - dead). +- `support` +- `eol` +- `extendedSupport` -The adapter also tolerates the pre-2026 shape where -`cycle.extendedSupport` was a boolean — a `true` value falls back to -`cycle.eol` as the extended-support boundary. +Supported actions: -### Live example +- `extended_support` - supported, deprecated, `IsExtendedSupport=true`; + policy reports this as YELLOW. +- `unsupported` - unsupported and deprecated, but not true EOL; policy + reports this as RED. +- `eol` - true end of life; policy reports this as RED. +- `supported` - currently supported. -For amazon-eks cycle 1.30 (`eol: 2025-07-23`, -`extendedSupport: 2026-07-23`), evaluated on 2026-04-29: +### EKS -| Field | Value | Source / note | -| ------------------------------ | -------------- | -------------------------------------------------------- | -| `EOLDate` | `nil` | always nil for EKS | -| `DeprecationDate` | `2025-07-23` | `cycle.eol` (end of standard support) | -| `ExtendedSupportEnd` | `2026-07-23` | `cycle.extendedSupport` | -| `IsExtendedSupport` | `true` | now is between standard-end and extended-end | -| `IsSupported` | `true` | still in paid extended support | -| `IsDeprecated` | `true` | past standard support | +EKS cycle `eol` means end of standard support, not true EOL. Clusters +keep running after extended support, but AWS stops patching them. -→ Policy classifies as **YELLOW**. +```yaml +eol: + provider: endoflife-date + product: amazon-eks + schema: declarative + lifecycle: + deprecation_date: + field: eol + extended_support_end: + field: extendedSupport + bool_true_fallback: eol + deprecated_window: extended_support + past_extended_support: unsupported +``` -For the same cycle past `extendedSupport`, status flips to -`IsDeprecated=true, IsExtendedSupport=false, IsSupported=false` → -**RED**, with `EOLDate` still nil. +For amazon-eks cycle 1.30 (`eol: 2025-07-23`, +`extendedSupport: 2026-07-23`), evaluated on 2026-04-29: ---- +| Field | Value | Source / note | +| --- | --- | --- | +| `EOLDate` | `nil` | omitted in YAML | +| `DeprecationDate` | `2025-07-23` | `cycle.eol` | +| `ExtendedSupportEnd` | `2026-07-23` | `cycle.extendedSupport` | +| `IsExtendedSupport` | `true` | `deprecated_window: extended_support` | -## Picking the right adapter +Policy classifies that as YELLOW. Past `extendedSupport`, the +`past_extended_support: unsupported` action makes it RED without +claiming the cluster has a true EOL date. -The adapter is selected per-resource via YAML — `eol.schema` on the -resource entry, validated by the config loader at startup: +### Lambda -```yaml -- id: eks - eol: - provider: endoflife-date - product: amazon-eks - schema: eks_adapter # ← EKS-only — no true EOL -``` +Lambda uses Standard Support and Deprecated Support columns instead of +`extendedSupport`. The deprecated-support window should be YELLOW. ```yaml -- id: aurora-postgresql - eol: - provider: endoflife-date - product: amazon-aurora-postgresql - schema: standard # ← the default for almost everything, - # including AWS ElastiCache/RDS/Aurora - # that ship eol+extendedSupport +eol: + provider: endoflife-date + product: aws-lambda + schema: declarative + lifecycle: + deprecation_date: + field: support + extended_support_end: + field: eol + eol_date: + field: eol + deprecated_window: extended_support + past_extended_support: eol ``` -Empty `schema` defaults to `standard`. Adding a new schema means -implementing `SchemaAdapter`, registering it in `SchemaAdapters` in -[adapters.go](./adapters.go), and naming it from YAML — no Go change -in the activities or the policy layer. +For `python3.8` (`support: 2024-10-14`, `eol: 2026-09-30`), dates +between those two boundaries become YELLOW. Dates after `eol` become +true EOL / RED. --- -## Adding a new adapter — the rule of thumb +## Adding Products -If a new product cycle's fields have different semantics from the -standard ones (in any of the three shapes above), write an adapter. -Symptoms that indicate you need one: +Use `schema: standard` when the product matches one of the standard +shapes. Use `schema: declarative` when the same field name means +something product-specific. -- A field's name suggests one thing but the dates encode another (the - EKS `eol`-isn't-EOL case). -- The product is missing a concept the standard schema relies on - (EKS having no true EOL). -- A field is a boolean where the standard schema expects a date in a - way that the existing `extendedSupport: true` fallback doesn't cover. +Examples that call for declarative YAML: -If a new product matches one of the three standard shapes, do not -write an adapter — use `standard` and move on. The point of this seam -is to keep deviations explicit and small, not to encode every product -separately. +- A field's name suggests one thing but the dates encode another. +- The product has no true EOL, but still has an unsupported-after date. +- The product has a supported post-standard window without using the + upstream `extendedSupport` field. + +If the available lifecycle actions are not expressive enough, add a new +generic action. Avoid adding product-named adapters unless the product +needs behavior that cannot be described as boundary mapping plus window +actions. --- -## When in doubt, fetch the live cycle +## When In Doubt + +Fetch the live cycle and compare the field meanings: ```sh curl -s https://endoflife.date/api/amazon-eks.json | jq '.[0]' +curl -s https://endoflife.date/api/aws-lambda.json | jq '.[0]' curl -s https://endoflife.date/api/amazon-elasticache-redis.json | jq '.[0]' -curl -s https://endoflife.date/api/amazon-aurora-postgresql.json | jq '.[0]' ``` - -Three cycles side-by-side will show you in seconds which shape you're -looking at. Match against the table at the top — if the field shapes -are one of the three standard patterns, ship it as `schema: standard`. -If not, write an adapter and add a section here. diff --git a/pkg/eol/endoflife/adapters.go b/pkg/eol/endoflife/adapters.go index 55253bb..52d8c63 100644 --- a/pkg/eol/endoflife/adapters.go +++ b/pkg/eol/endoflife/adapters.go @@ -9,8 +9,9 @@ import ( ) // SchemaAdapter adapts endoflife.date ProductCycle to VersionLifecycle. -// Some products use non-standard field semantics and need custom -// adapters; see ADAPTERS.md for the catalog. +// Most products use the built-in standard schema; product-specific +// field semantics should prefer DeclarativeSchemaAdapter so adding a +// new product is a YAML change rather than a Go adapter. type SchemaAdapter interface { AdaptCycle(cycle *ProductCycle) (*types.VersionLifecycle, error) } @@ -175,67 +176,144 @@ func (a *StandardSchemaAdapter) AdaptCycle(cycle *ProductCycle) (*types.VersionL return lifecycle, nil } -// EKSSchemaAdapter handles amazon-eks, whose cycles ship in a shape -// that disagrees with the standard schema in two ways: +const ( + SchemaStandard = "standard" + SchemaDeclarative = "declarative" + + lifecycleFieldEOL = "eol" + lifecycleFieldSupport = "support" + lifecycleFieldExtendedSupport = "extendedSupport" + + lifecycleActionExtendedSupport = "extended_support" + lifecycleActionUnsupported = "unsupported" + lifecycleActionEOL = "eol" + lifecycleActionSupported = "supported" +) + +// DeclarativeLifecycleConfig lets YAML describe product-specific +// lifecycle semantics without adding another Go adapter. It maps +// endoflife.date fields into VersionLifecycle boundaries, then declares +// how to classify the post-standard-support windows. // -// - There is no `support` field; `cycle.eol` is the end of *standard* -// support (start of paid extended support), not a true terminal date. -// - `cycle.extendedSupport` is now a *date* (the end of paid extended -// support); it used to be a boolean and the adapter still tolerates -// that legacy shape. -// - EKS clusters never truly stop working, so we always leave -// lifecycle.EOLDate = nil. Past-extended-support is still classified -// RED via `IsDeprecated && !IsExtendedSupport`, matching the prior -// product behavior of urging upgrades. -type EKSSchemaAdapter struct{} - -// parseEKSExtendedEnd resolves cycle.extendedSupport into an end date. -// The current schema uses a date string; older cycles used boolean -// `true` to mean "extended support exists, ends at cycle.eol", so we -// fall back to standardEnd in that case to keep upgrade nudges firing. -func (a *EKSSchemaAdapter) parseEKSExtendedEnd(extSupport interface{}, standardEnd *time.Time) *time.Time { - switch v := extSupport.(type) { - case string: - if v != "" && v != falseBool { - if parsed, err := parseDate(v); err == nil { - return &parsed - } +// Supported field names: support, eol, extendedSupport. +// Supported actions: extended_support, unsupported, eol, supported. +type DeclarativeLifecycleConfig struct { + DeprecationDate LifecycleDateSource `yaml:"deprecation_date"` + ExtendedSupportEnd LifecycleDateSource `yaml:"extended_support_end"` + EOLDate LifecycleDateSource `yaml:"eol_date"` + + // DeprecatedWindow is applied after DeprecationDate and before + // ExtendedSupportEnd. Most AWS paid/deprecated support windows use + // "extended_support", which policy reports as YELLOW. + DeprecatedWindow string `yaml:"deprecated_window"` + + // PastExtendedSupport is applied after ExtendedSupportEnd when + // EOLDate is omitted or later than ExtendedSupportEnd. EKS uses + // "unsupported" here because clusters keep running, but AWS stops + // patching them. + PastExtendedSupport string `yaml:"past_extended_support"` +} + +// LifecycleDateSource declares which ProductCycle field supplies a +// VersionLifecycle date. BoolTrueFallback handles archived upstream +// shapes such as old EKS data where extendedSupport was true instead of +// a date. +type LifecycleDateSource struct { + Field string `yaml:"field"` + BoolTrueFallback string `yaml:"bool_true_fallback,omitempty"` +} + +// DeclarativeSchemaAdapter adapts cycles according to a YAML-provided +// DeclarativeLifecycleConfig. +type DeclarativeSchemaAdapter struct { + config *DeclarativeLifecycleConfig +} + +// NewDeclarativeSchemaAdapter validates config and returns an adapter. +func NewDeclarativeSchemaAdapter(config *DeclarativeLifecycleConfig) (*DeclarativeSchemaAdapter, error) { + if err := ValidateDeclarativeLifecycleConfig(config); err != nil { + return nil, err + } + return &DeclarativeSchemaAdapter{config: config}, nil +} + +// ValidateDeclarativeLifecycleConfig checks that a YAML lifecycle block +// only references fields and actions the generic adapter understands. +func ValidateDeclarativeLifecycleConfig(config *DeclarativeLifecycleConfig) error { + if config == nil { + return errors.New("declarative lifecycle config is required") + } + + for name, source := range map[string]LifecycleDateSource{ + "deprecation_date": config.DeprecationDate, + "extended_support_end": config.ExtendedSupportEnd, + "eol_date": config.EOLDate, + } { + if err := validateLifecycleDateSource(source); err != nil { + return errors.Wrapf(err, "%s", name) } - case bool: - if v && standardEnd != nil { - return standardEnd + } + + if err := validateLifecycleAction(config.DeprecatedWindow, true); err != nil { + return errors.Wrap(err, "deprecated_window") + } + if err := validateLifecycleAction(config.PastExtendedSupport, true); err != nil { + return errors.Wrap(err, "past_extended_support") + } + + if config.DeprecationDate.Field == "" && + config.ExtendedSupportEnd.Field == "" && + config.EOLDate.Field == "" { + return errors.New("at least one lifecycle date source is required") + } + + return nil +} + +func validateLifecycleDateSource(source LifecycleDateSource) error { + if source.Field != "" { + if !isSupportedLifecycleField(source.Field) { + return errors.Errorf("unsupported field %q", source.Field) + } + } + if source.BoolTrueFallback != "" { + if source.Field == "" { + return errors.New("bool_true_fallback requires field") + } + if !isSupportedLifecycleField(source.BoolTrueFallback) { + return errors.Errorf("unsupported bool_true_fallback field %q", source.BoolTrueFallback) } } return nil } -// classifyEKS sets the lifecycle status flags from the derived -// boundaries. EKS has no true EOL, so past-extended-support is -// represented as RED via IsDeprecated && !IsExtendedSupport. -func (a *EKSSchemaAdapter) classifyEKS(lifecycle *types.VersionLifecycle, standardEnd, extendedEnd *time.Time) { - now := time.Now() - switch { - case extendedEnd != nil && now.After(*extendedEnd): - // Past extended support — AWS no longer issues patches. - lifecycle.IsSupported = false - lifecycle.IsDeprecated = true - lifecycle.IsExtendedSupport = false - case standardEnd != nil && now.After(*standardEnd): - // In the paid extended-support window. - lifecycle.IsSupported = true - lifecycle.IsExtendedSupport = true - lifecycle.IsDeprecated = true +func isSupportedLifecycleField(field string) bool { + switch field { + case lifecycleFieldEOL, lifecycleFieldSupport, lifecycleFieldExtendedSupport: + return true default: - // Still in standard support (or no date info at all). - lifecycle.IsSupported = true + return false + } +} + +func validateLifecycleAction(action string, allowEmpty bool) error { + if action == "" && allowEmpty { + return nil + } + switch action { + case lifecycleActionExtendedSupport, lifecycleActionUnsupported, lifecycleActionEOL, lifecycleActionSupported: + return nil + default: + return errors.Errorf("unsupported action %q", action) } } -// AdaptCycle converts an amazon-eks ProductCycle to VersionLifecycle. -func (a *EKSSchemaAdapter) AdaptCycle(cycle *ProductCycle) (*types.VersionLifecycle, error) { +// AdaptCycle converts a ProductCycle to VersionLifecycle using the +// declarative YAML semantics. +func (a *DeclarativeSchemaAdapter) AdaptCycle(cycle *ProductCycle) (*types.VersionLifecycle, error) { lifecycle := &types.VersionLifecycle{ Version: cycle.Cycle, - Engine: "eks", + Engine: "", // Set by caller Source: providerName, FetchedAt: time.Now(), } @@ -246,27 +324,105 @@ func (a *EKSSchemaAdapter) AdaptCycle(cycle *ProductCycle) (*types.VersionLifecy } } - // cycle.eol is end-of-standard-support for amazon-eks. - var standardEnd *time.Time - if dateStr := anyToDateString(cycle.EOL); dateStr != "" { + lifecycle.DeprecationDate = a.parseDateSource(cycle, a.config.DeprecationDate) + lifecycle.ExtendedSupportEnd = a.parseDateSource(cycle, a.config.ExtendedSupportEnd) + lifecycle.EOLDate = a.parseDateSource(cycle, a.config.EOLDate) + + a.classify(lifecycle) + + return lifecycle, nil +} + +func (a *DeclarativeSchemaAdapter) parseDateSource(cycle *ProductCycle, source LifecycleDateSource) *time.Time { + value, ok := cycleFieldValue(cycle, source.Field) + if !ok { + return nil + } + + if dateStr := anyToDateString(value); dateStr != "" { if parsed, err := parseDate(dateStr); err == nil { - standardEnd = &parsed + return &parsed } } - lifecycle.DeprecationDate = standardEnd - lifecycle.ExtendedSupportEnd = a.parseEKSExtendedEnd(cycle.ExtendedSupport, standardEnd) - // EKS has no true EOL — clusters keep running indefinitely. - lifecycle.EOLDate = nil - a.classifyEKS(lifecycle, standardEnd, lifecycle.ExtendedSupportEnd) + if boolValue, ok := value.(bool); ok && boolValue && source.BoolTrueFallback != "" { + fallback, ok := cycleFieldValue(cycle, source.BoolTrueFallback) + if !ok { + return nil + } + if dateStr := anyToDateString(fallback); dateStr != "" { + if parsed, err := parseDate(dateStr); err == nil { + return &parsed + } + } + } - return lifecycle, nil + return nil +} + +func cycleFieldValue(cycle *ProductCycle, field string) (any, bool) { + switch field { + case lifecycleFieldEOL: + return cycle.EOL, true + case lifecycleFieldSupport: + return cycle.Support, true + case lifecycleFieldExtendedSupport: + return cycle.ExtendedSupport, true + default: + return nil, false + } +} + +func (a *DeclarativeSchemaAdapter) classify(lifecycle *types.VersionLifecycle) { + now := time.Now() + + switch { + case lifecycle.EOLDate != nil && now.After(*lifecycle.EOLDate): + applyLifecycleAction(lifecycle, lifecycleActionEOL) + case lifecycle.ExtendedSupportEnd != nil && now.After(*lifecycle.ExtendedSupportEnd): + applyLifecycleAction(lifecycle, defaultLifecycleAction(a.config.PastExtendedSupport, lifecycleActionUnsupported)) + case lifecycle.DeprecationDate != nil && lifecycle.ExtendedSupportEnd != nil && + now.After(*lifecycle.DeprecationDate) && now.Before(*lifecycle.ExtendedSupportEnd): + applyLifecycleAction(lifecycle, defaultLifecycleAction(a.config.DeprecatedWindow, lifecycleActionUnsupported)) + case lifecycle.DeprecationDate != nil && now.After(*lifecycle.DeprecationDate): + applyLifecycleAction(lifecycle, lifecycleActionUnsupported) + default: + applyLifecycleAction(lifecycle, lifecycleActionSupported) + } +} + +func defaultLifecycleAction(action, fallback string) string { + if action == "" { + return fallback + } + return action +} + +func applyLifecycleAction(lifecycle *types.VersionLifecycle, action string) { + switch action { + case lifecycleActionExtendedSupport: + lifecycle.IsSupported = true + lifecycle.IsDeprecated = true + lifecycle.IsExtendedSupport = true + case lifecycleActionUnsupported: + lifecycle.IsSupported = false + lifecycle.IsDeprecated = true + lifecycle.IsExtendedSupport = false + case lifecycleActionEOL: + lifecycle.IsEOL = true + lifecycle.IsSupported = false + lifecycle.IsDeprecated = true + lifecycle.IsExtendedSupport = false + case lifecycleActionSupported: + lifecycle.IsSupported = true + lifecycle.IsDeprecated = false + lifecycle.IsExtendedSupport = false + } } // SchemaAdapters is a registry of available schema adapters. var SchemaAdapters = map[string]SchemaAdapter{ - "standard": &StandardSchemaAdapter{}, - "eks_adapter": &EKSSchemaAdapter{}, + SchemaStandard: &StandardSchemaAdapter{}, } // GetSchemaAdapter returns the appropriate schema adapter for a product. diff --git a/pkg/eol/endoflife/adapters_test.go b/pkg/eol/endoflife/adapters_test.go index 63952c1..33ea08c 100644 --- a/pkg/eol/endoflife/adapters_test.go +++ b/pkg/eol/endoflife/adapters_test.go @@ -240,8 +240,104 @@ func TestStandardSchemaAdapter_ExtendedSupportOverridesPastEOL(t *testing.T) { assert.True(t, lifecycle.IsDeprecated) } -func TestEKSSchemaAdapter_CurrentVersion(t *testing.T) { - adapter := &EKSSchemaAdapter{} +func eksDeclarativeAdapter(t *testing.T) *DeclarativeSchemaAdapter { + t.Helper() + + adapter, err := NewDeclarativeSchemaAdapter(&DeclarativeLifecycleConfig{ + DeprecationDate: LifecycleDateSource{Field: lifecycleFieldEOL}, + ExtendedSupportEnd: LifecycleDateSource{ + Field: lifecycleFieldExtendedSupport, + BoolTrueFallback: lifecycleFieldEOL, + }, + DeprecatedWindow: lifecycleActionExtendedSupport, + PastExtendedSupport: lifecycleActionUnsupported, + }) + require.NoError(t, err) + return adapter +} + +func lambdaDeclarativeAdapter(t *testing.T) *DeclarativeSchemaAdapter { + t.Helper() + + adapter, err := NewDeclarativeSchemaAdapter(&DeclarativeLifecycleConfig{ + DeprecationDate: LifecycleDateSource{Field: lifecycleFieldSupport}, + ExtendedSupportEnd: LifecycleDateSource{Field: lifecycleFieldEOL}, + EOLDate: LifecycleDateSource{Field: lifecycleFieldEOL}, + DeprecatedWindow: lifecycleActionExtendedSupport, + PastExtendedSupport: lifecycleActionEOL, + }) + require.NoError(t, err) + return adapter +} + +func TestDeclarativeSchemaAdapter_LambdaDeprecatedSupportWindow(t *testing.T) { + adapter := lambdaDeclarativeAdapter(t) + + pastYear := time.Now().Year() - 1 + futureYear := time.Now().Year() + 1 + cycle := &ProductCycle{ + Cycle: "python3.8", + ReleaseDate: "2019-11-18", + Support: time.Date(pastYear, 10, 14, 0, 0, 0, 0, time.UTC).Format("2006-01-02"), + EOL: time.Date(futureYear, 9, 30, 0, 0, 0, 0, time.UTC).Format("2006-01-02"), + } + + lifecycle, err := adapter.AdaptCycle(cycle) + require.NoError(t, err) + + assert.Equal(t, "python3.8", lifecycle.Version) + assert.Empty(t, lifecycle.Engine) + assert.True(t, lifecycle.IsSupported) + assert.True(t, lifecycle.IsDeprecated) + assert.True(t, lifecycle.IsExtendedSupport) + assert.False(t, lifecycle.IsEOL) + assert.NotNil(t, lifecycle.DeprecationDate) + assert.NotNil(t, lifecycle.ExtendedSupportEnd) + assert.NotNil(t, lifecycle.EOLDate) + assert.Equal(t, *lifecycle.EOLDate, *lifecycle.ExtendedSupportEnd) +} + +func TestDeclarativeSchemaAdapter_LambdaPastDeprecatedSupport(t *testing.T) { + adapter := lambdaDeclarativeAdapter(t) + + cycle := &ProductCycle{ + Cycle: "nodejs12.x", + ReleaseDate: "2019-11-18", + Support: "2023-03-31", + EOL: "2023-04-30", + } + + lifecycle, err := adapter.AdaptCycle(cycle) + require.NoError(t, err) + + assert.False(t, lifecycle.IsSupported) + assert.True(t, lifecycle.IsDeprecated) + assert.False(t, lifecycle.IsExtendedSupport) + assert.True(t, lifecycle.IsEOL) +} + +func TestDeclarativeSchemaAdapter_LambdaCurrentStandardSupport(t *testing.T) { + adapter := lambdaDeclarativeAdapter(t) + + futureYear := time.Now().Year() + 1 + cycle := &ProductCycle{ + Cycle: "python3.13", + ReleaseDate: "2024-11-14", + Support: time.Date(futureYear, 6, 30, 0, 0, 0, 0, time.UTC).Format("2006-01-02"), + EOL: time.Date(futureYear+1, 8, 31, 0, 0, 0, 0, time.UTC).Format("2006-01-02"), + } + + lifecycle, err := adapter.AdaptCycle(cycle) + require.NoError(t, err) + + assert.True(t, lifecycle.IsSupported) + assert.False(t, lifecycle.IsDeprecated) + assert.False(t, lifecycle.IsExtendedSupport) + assert.False(t, lifecycle.IsEOL) +} + +func TestDeclarativeSchemaAdapter_EKSCurrentVersion(t *testing.T) { + adapter := eksDeclarativeAdapter(t) // Live amazon-eks shape: cycle.eol is end-of-standard-support and // cycle.extendedSupport is end-of-extended-support. Both in the @@ -258,7 +354,7 @@ func TestEKSSchemaAdapter_CurrentVersion(t *testing.T) { require.NoError(t, err) assert.Equal(t, "1.31", lifecycle.Version) - assert.Equal(t, "eks", lifecycle.Engine) + assert.Empty(t, lifecycle.Engine) assert.True(t, lifecycle.IsSupported) assert.False(t, lifecycle.IsDeprecated) assert.False(t, lifecycle.IsEOL) @@ -272,8 +368,8 @@ func TestEKSSchemaAdapter_CurrentVersion(t *testing.T) { assert.NotNil(t, lifecycle.ExtendedSupportEnd) } -func TestEKSSchemaAdapter_InExtendedSupport(t *testing.T) { - adapter := &EKSSchemaAdapter{} +func TestDeclarativeSchemaAdapter_EKSInExtendedSupport(t *testing.T) { + adapter := eksDeclarativeAdapter(t) // Past cycle.eol (end of standard support) but before cycle.extendedSupport // (end of extended support) → IN extended support → YELLOW. @@ -295,8 +391,8 @@ func TestEKSSchemaAdapter_InExtendedSupport(t *testing.T) { assert.True(t, lifecycle.IsExtendedSupport) } -func TestEKSSchemaAdapter_PastExtendedSupport(t *testing.T) { - adapter := &EKSSchemaAdapter{} +func TestDeclarativeSchemaAdapter_EKSPastExtendedSupport(t *testing.T) { + adapter := eksDeclarativeAdapter(t) // Past both cycle.eol AND cycle.extendedSupport — AWS no longer patches. cycle := &ProductCycle{ @@ -315,8 +411,8 @@ func TestEKSSchemaAdapter_PastExtendedSupport(t *testing.T) { assert.False(t, lifecycle.IsExtendedSupport) } -func TestEKSSchemaAdapter_NoTrueEOL(t *testing.T) { - adapter := &EKSSchemaAdapter{} +func TestDeclarativeSchemaAdapter_EKSNoTrueEOL(t *testing.T) { + adapter := eksDeclarativeAdapter(t) cycle := &ProductCycle{ Cycle: "1.20", @@ -342,13 +438,13 @@ func TestEKSSchemaAdapter_NoTrueEOL(t *testing.T) { assert.Equal(t, expectedStd, *lifecycle.DeprecationDate) } -// TestEKSSchemaAdapter_LegacyBooleanExtendedSupport guards the +// TestDeclarativeSchemaAdapter_EKSLegacyBooleanExtendedSupport guards the // pre-2026 amazon-eks shape where cycle.extendedSupport was a boolean. -// Live data now uses dates, but the adapter still tolerates the +// Live data now uses dates, but the YAML bool_true_fallback still tolerates the // legacy boolean so a hypothetical replay against archived JSON // classifies clusters consistently. -func TestEKSSchemaAdapter_LegacyBooleanExtendedSupport(t *testing.T) { - adapter := &EKSSchemaAdapter{} +func TestDeclarativeSchemaAdapter_EKSLegacyBooleanExtendedSupport(t *testing.T) { + adapter := eksDeclarativeAdapter(t) // Past cycle.eol with extendedSupport=true bool — the bool falls // back to standardEnd as the extended-support boundary, so we @@ -375,10 +471,19 @@ func TestGetSchemaAdapter_Standard(t *testing.T) { assert.IsType(t, &StandardSchemaAdapter{}, adapter) } -func TestGetSchemaAdapter_EKS(t *testing.T) { - adapter, err := GetSchemaAdapter("eks_adapter") +func TestNewDeclarativeSchemaAdapter_Validation(t *testing.T) { + adapter, err := NewDeclarativeSchemaAdapter(&DeclarativeLifecycleConfig{ + DeprecationDate: LifecycleDateSource{Field: lifecycleFieldSupport}, + DeprecatedWindow: lifecycleActionExtendedSupport, + }) require.NoError(t, err) - assert.IsType(t, &EKSSchemaAdapter{}, adapter) + assert.IsType(t, &DeclarativeSchemaAdapter{}, adapter) + + _, err = NewDeclarativeSchemaAdapter(&DeclarativeLifecycleConfig{ + DeprecationDate: LifecycleDateSource{Field: "unsupportedField"}, + }) + require.Error(t, err) + assert.Contains(t, err.Error(), "unsupported field") } func TestGetSchemaAdapter_Unknown(t *testing.T) { @@ -410,8 +515,8 @@ func TestStandardSchemaAdapter_EmptyDates(t *testing.T) { assert.False(t, lifecycle.IsDeprecated) } -func TestEKSSchemaAdapter_EmptyDates(t *testing.T) { - adapter := &EKSSchemaAdapter{} +func TestDeclarativeSchemaAdapter_EmptyDates(t *testing.T) { + adapter := eksDeclarativeAdapter(t) cycle := &ProductCycle{ Cycle: "1.32", diff --git a/pkg/eol/endoflife/provider.go b/pkg/eol/endoflife/provider.go index ac7d2f8..e532cd3 100644 --- a/pkg/eol/endoflife/provider.go +++ b/pkg/eol/endoflife/provider.go @@ -21,14 +21,12 @@ const ( // Provider fetches EOL data for a single endoflife.date product. // // The product (e.g. "amazon-aurora-postgresql", "amazon-eks") and the -// schema adapter (StandardSchemaAdapter / EKSSchemaAdapter / future -// per-product adapters) are both set at construction time from -// YAML-declared eol.product / eol.schema. One Provider instance per -// resource keeps each product's cache and singleflight key isolated -// and pushes the "which schema?" decision into config — adding a new -// product with non-standard endoflife.date semantics is a new adapter -// + a new schema string in YAML, not a hardcoded product check in -// Provider. +// schema adapter are both set at construction time from YAML-declared +// eol.product / eol.schema / eol.lifecycle. One Provider instance per +// resource keeps each product's cache and singleflight key isolated. +// Products with non-standard endoflife.date semantics should use the +// declarative lifecycle schema in YAML rather than adding a hardcoded +// product check in Provider. // //nolint:govet // field alignment sacrificed for readability type Provider struct { @@ -54,12 +52,37 @@ type cachedVersions struct { // validates this so misconfiguration fails at startup rather than // mid-scan. func NewProvider(client Client, product, schema string, cacheTTL time.Duration, logger *slog.Logger) (*Provider, error) { + return NewProviderWithLifecycle(client, product, schema, nil, cacheTTL, logger) +} + +// NewProviderWithLifecycle creates a provider with an optional YAML +// lifecycle mapping. Non-nil lifecycle config selects the declarative +// adapter; otherwise schema names a built-in adapter such as "standard". +func NewProviderWithLifecycle( + client Client, + product string, + schema string, + lifecycle *DeclarativeLifecycleConfig, + cacheTTL time.Duration, + logger *slog.Logger, +) (*Provider, error) { if schema == "" { - schema = "standard" + schema = SchemaStandard } - adapter, err := GetSchemaAdapter(schema) - if err != nil { - return nil, errors.Wrapf(err, "endoflife provider for product %q", product) + + var adapter SchemaAdapter + if lifecycle != nil { + declarativeAdapter, err := NewDeclarativeSchemaAdapter(lifecycle) + if err != nil { + return nil, errors.Wrapf(err, "endoflife provider for product %q", product) + } + adapter = declarativeAdapter + } else { + var err error + adapter, err = GetSchemaAdapter(schema) + if err != nil { + return nil, errors.Wrapf(err, "endoflife provider for product %q", product) + } } if cacheTTL == 0 { cacheTTL = 24 * time.Hour // Default: cache for 24 hours diff --git a/pkg/eol/endoflife/provider_test.go b/pkg/eol/endoflife/provider_test.go index 7bdf734..f09ec47 100644 --- a/pkg/eol/endoflife/provider_test.go +++ b/pkg/eol/endoflife/provider_test.go @@ -313,12 +313,11 @@ func TestProvider_Engines(t *testing.T) { } } -// TestProvider_EKS pins the EKS-adapter wiring: when the YAML declares -// schema: eks_adapter, the provider must dispatch cycle conversion -// through the EKS adapter (NOT the standard one) so EKS's non-standard -// endoflife.date schema (cycle.eol = end of EXTENDED support, not true -// EOL) is interpreted correctly. -func TestProvider_EKS(t *testing.T) { +// TestProvider_DeclarativeLifecycle pins the YAML-driven lifecycle +// wiring: when the resource declares a lifecycle block, the provider +// must dispatch cycle conversion through the declarative adapter so +// product-specific endoflife.date field semantics stay out of Go code. +func TestProvider_DeclarativeLifecycle(t *testing.T) { mockClient := &MockClient{ GetProductCyclesFunc: func(ctx context.Context, product string) ([]*ProductCycle, error) { if product != "amazon-eks" { @@ -335,9 +334,25 @@ func TestProvider_EKS(t *testing.T) { }, } - provider, err := NewProvider(mockClient, "amazon-eks", "eks_adapter", 1*time.Hour, nil) + lifecycleConfig := &DeclarativeLifecycleConfig{ + DeprecationDate: LifecycleDateSource{Field: lifecycleFieldEOL}, + ExtendedSupportEnd: LifecycleDateSource{ + Field: lifecycleFieldExtendedSupport, + BoolTrueFallback: lifecycleFieldEOL, + }, + DeprecatedWindow: lifecycleActionExtendedSupport, + PastExtendedSupport: lifecycleActionUnsupported, + } + provider, err := NewProviderWithLifecycle( + mockClient, + "amazon-eks", + "declarative", + lifecycleConfig, + 1*time.Hour, + nil, + ) if err != nil { - t.Fatalf("NewProvider() error = %v", err) + t.Fatalf("NewProviderWithLifecycle() error = %v", err) } engines := []string{"kubernetes", "k8s", "eks"} @@ -354,18 +369,26 @@ func TestProvider_EKS(t *testing.T) { if v.Version != "1.32" { t.Errorf("Expected version 1.32, got %s", v.Version) } - // EKS adapter: cycle.EOL → ExtendedSupportEnd; EOLDate stays nil + // Declarative EKS config: cycle.EOL → DeprecationDate and + // cycle.ExtendedSupport → ExtendedSupportEnd. EOLDate stays nil // because EKS clusters never truly EOL. if v.EOLDate != nil { t.Errorf("EOLDate = %v, want nil (EKS has no true EOL)", v.EOLDate) } if v.ExtendedSupportEnd == nil { - t.Error("ExtendedSupportEnd should be set from cycle.EOL under eks_adapter") + t.Error("ExtendedSupportEnd should be set from cycle.ExtendedSupport") } }) } } +func TestNewProvider_DeclarativeLifecycleRequiresConfig(t *testing.T) { + _, err := NewProviderWithLifecycle(&MockClient{}, "amazon-eks", "declarative", nil, 1*time.Hour, nil) + if err == nil { + t.Fatal("expected error for declarative schema without lifecycle config") + } +} + // TestNewProvider_InvalidSchema asserts construction-time rejection of // unknown schema names — the same coverage applies whether the bad // value comes from a YAML typo or a future adapter that's not yet diff --git a/skills/add-version-guard-resource/SKILL.md b/skills/add-version-guard-resource/SKILL.md index adaf913..19296e7 100644 --- a/skills/add-version-guard-resource/SKILL.md +++ b/skills/add-version-guard-resource/SKILL.md @@ -189,9 +189,9 @@ Use similar patterns for new resources. - `cycle.eol` → True end of life - `cycle.extendedSupport` → End of extended support -**Known non-standard schemas** (require custom adapters): -- **EKS (amazon-eks)**: `cycle.eol` means "end of extended support" NOT true EOL - - Use `schema: eks_adapter` in config +**Known non-standard schemas** (use YAML-defined lifecycle semantics): +- **EKS (amazon-eks)**: `cycle.eol` means "end of standard support" NOT true EOL + - Use `schema: declarative` with an `eol.lifecycle` block in config **Default**: Use `schema: standard` unless you know it's non-standard like EKS. @@ -317,7 +317,7 @@ git commit -m "Add {resource-type} support to Version Guard - Added config entry with id: {resource-id} - Uses endoflife.date product: {eol-product-name} - Cloud provider: {cloud-provider} -- Schema: {standard|eks_adapter} +- Schema: {standard|declarative} NOTE: Add Wiz report ID to WIZ_REPORT_IDS environment variable: '{\"resource-id\":\"wiz-report-uuid\"}' diff --git a/skills/add-version-guard-resource/examples/eks.yaml b/skills/add-version-guard-resource/examples/eks.yaml index 121031f..106a8b1 100644 --- a/skills/add-version-guard-resource/examples/eks.yaml +++ b/skills/add-version-guard-resource/examples/eks.yaml @@ -1,7 +1,7 @@ -# Example: EKS (Non-Standard Schema) +# Example: EKS (Declarative Lifecycle Schema) # This is an example of a resource using NON-STANDARD endoflife.date schema -# EKS uses cycle.eol to mean "end of extended support" NOT "true EOL" -# Requires custom adapter: eks_adapter +# EKS uses cycle.eol to mean "end of standard support" NOT "true EOL" +# Lifecycle semantics are declared in YAML; no custom Go adapter is required. # # IMPORTANT: The resource 'id' is used to look up the Wiz report ID # in the WIZ_REPORT_IDS JSON map environment variable. @@ -35,4 +35,12 @@ eol: provider: endoflife-date product: amazon-eks - schema: eks_adapter # Non-standard! cycle.eol → ExtendedSupportEnd (NOT EOLDate) + schema: declarative + lifecycle: + deprecation_date: + field: eol + extended_support_end: + field: extendedSupport + bool_true_fallback: eol + deprecated_window: extended_support + past_extended_support: unsupported