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
17 changes: 13 additions & 4 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:**
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down
9 changes: 8 additions & 1 deletion cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
30 changes: 28 additions & 2 deletions pkg/config/defaults/resources.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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/.
33 changes: 33 additions & 0 deletions pkg/config/loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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)
}
Expand All @@ -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:
//
Expand Down
14 changes: 12 additions & 2 deletions pkg/config/loader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand Down
9 changes: 6 additions & 3 deletions pkg/config/types.go
Original file line number Diff line number Diff line change
@@ -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"`
Expand Down Expand Up @@ -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"`
}
Loading
Loading