diff --git a/internal/projectconfig/component_test.go b/internal/projectconfig/component_test.go index 9f5f3d87..eac7df5e 100644 --- a/internal/projectconfig/component_test.go +++ b/internal/projectconfig/component_test.go @@ -942,7 +942,7 @@ func TestValidateComponentGroupMembership(t *testing.T) { Components: []string{"missing-component"}, } - err := cfg.Validate() + err := cfg.Validate(false) require.Error(t, err) require.ErrorIs(t, err, projectconfig.ErrUndefinedComponent) assert.Contains(t, err.Error(), "my-group") @@ -956,7 +956,7 @@ func TestValidateComponentGroupMembership(t *testing.T) { Components: []string{"real-component"}, } - assert.NoError(t, cfg.Validate()) + assert.NoError(t, cfg.Validate(false)) }) t.Run("group with only spec patterns is valid", func(t *testing.T) { @@ -965,7 +965,7 @@ func TestValidateComponentGroupMembership(t *testing.T) { SpecPathPatterns: []string{"SPECS/**/*.spec"}, } - assert.NoError(t, cfg.Validate()) + assert.NoError(t, cfg.Validate(false)) }) t.Run("reports all undefined references together", func(t *testing.T) { @@ -978,11 +978,20 @@ func TestValidateComponentGroupMembership(t *testing.T) { Components: []string{"missing-3"}, } - err := cfg.Validate() + err := cfg.Validate(false) require.Error(t, err) require.ErrorIs(t, err, projectconfig.ErrUndefinedComponent) assert.Contains(t, err.Error(), "missing-1") assert.Contains(t, err.Error(), "missing-2") assert.Contains(t, err.Error(), "missing-3") }) + + t.Run("permissive parsing ignores undefined component reference", func(t *testing.T) { + cfg := projectconfig.NewProjectConfig() + cfg.ComponentGroups["my-group"] = projectconfig.ComponentGroupConfig{ + Components: []string{"missing-component"}, + } + + assert.NoError(t, cfg.Validate(true)) + }) } diff --git a/internal/projectconfig/loader.go b/internal/projectconfig/loader.go index 664c46fa..8a8ebbd4 100644 --- a/internal/projectconfig/loader.go +++ b/internal/projectconfig/loader.go @@ -56,7 +56,7 @@ func loadAndResolveProjectConfig( } // Validate the resulting configuration. - err := resolvedCfg.Validate() + err := resolvedCfg.Validate(permissiveConfigParsing) if err != nil { return nil, err } diff --git a/internal/projectconfig/package_test.go b/internal/projectconfig/package_test.go index c6d129a2..cea71f41 100644 --- a/internal/projectconfig/package_test.go +++ b/internal/projectconfig/package_test.go @@ -155,6 +155,27 @@ func TestPackageGroupConfig_Validate(t *testing.T) { }) } +func TestValidatePackageGroupMembership(t *testing.T) { + t.Run("same package in two groups is rejected", func(t *testing.T) { + cfg := projectconfig.NewProjectConfig() + cfg.PackageGroups["group-a"] = projectconfig.PackageGroupConfig{Packages: []string{"curl"}} + cfg.PackageGroups["group-b"] = projectconfig.PackageGroupConfig{Packages: []string{"curl"}} + + err := cfg.Validate(false) + require.Error(t, err) + assert.Contains(t, err.Error(), "curl") + assert.Contains(t, err.Error(), "may only belong to one group") + }) + + t.Run("permissive parsing ignores package in two groups", func(t *testing.T) { + cfg := projectconfig.NewProjectConfig() + cfg.PackageGroups["group-a"] = projectconfig.PackageGroupConfig{Packages: []string{"curl"}} + cfg.PackageGroups["group-b"] = projectconfig.PackageGroupConfig{Packages: []string{"curl"}} + + assert.NoError(t, cfg.Validate(true)) + }) +} + func TestPackageConfig_MergeUpdatesFrom(t *testing.T) { t.Run("non-zero other overrides zero base", func(t *testing.T) { base := projectconfig.PackageConfig{} diff --git a/internal/projectconfig/project.go b/internal/projectconfig/project.go index adb862aa..8cec0571 100644 --- a/internal/projectconfig/project.go +++ b/internal/projectconfig/project.go @@ -6,6 +6,7 @@ package projectconfig import ( "errors" "fmt" + "log/slog" "sort" "dario.cat/mergo" @@ -68,23 +69,43 @@ func NewProjectConfig() ProjectConfig { } } -// Validates the configuration, returning an error if any semantic errors are found. -func (cfg *ProjectConfig) Validate() error { +// Validate performs semantic validation of the configuration, returning an error if any +// semantic errors are found. When permissive is true, cross-reference consistency checks +// (component-group membership, package-group membership, and image/test-suite references) +// are downgraded to informational logs rather than hard errors. +// +// Permissive validation exists for best-effort loads such as historical overlay replay, +// where a partial or point-in-time config may legitimately reference entities that are +// defined in a different revision. Structural validation (required fields, value formats) +// is always enforced. +func (cfg *ProjectConfig) Validate(permissive bool) error { err := validator.New().Struct(cfg) if err != nil { return fmt.Errorf("config error:\n%w", err) } if err := validateComponentGroupMembership(cfg.ComponentGroups, cfg.Components); err != nil { - return err + if !permissive { + return err + } + + slog.Info("Ignoring component group membership error (permissive parsing)", "err", err) } if err := validatePackageGroupMembership(cfg.PackageGroups); err != nil { - return err + if !permissive { + return err + } + + slog.Info("Ignoring package group membership error (permissive parsing)", "err", err) } if err := validateImageTestReferences(cfg.Images, cfg.TestSuites); err != nil { - return err + if !permissive { + return err + } + + slog.Info("Ignoring image test suite reference error (permissive parsing)", "err", err) } if err := validateRpmRepos(cfg.Resources.RpmRepos); err != nil { diff --git a/internal/projectconfig/testsuite_test.go b/internal/projectconfig/testsuite_test.go index e24f758f..4d31523f 100644 --- a/internal/projectconfig/testsuite_test.go +++ b/internal/projectconfig/testsuite_test.go @@ -312,7 +312,7 @@ func TestValidateTestSuiteReferences(t *testing.T) { GroupsByComponent: make(map[string][]string), PackageGroups: make(map[string]projectconfig.PackageGroupConfig), } - assert.NoError(t, cfg.Validate()) + assert.NoError(t, cfg.Validate(false)) }) t.Run("undefined test reference", func(t *testing.T) { @@ -330,7 +330,7 @@ func TestValidateTestSuiteReferences(t *testing.T) { GroupsByComponent: make(map[string][]string), PackageGroups: make(map[string]projectconfig.PackageGroupConfig), } - err := cfg.Validate() + err := cfg.Validate(false) require.Error(t, err) require.ErrorIs(t, err, projectconfig.ErrUndefinedTestSuite) assert.Contains(t, err.Error(), "nonexistent") @@ -348,6 +348,24 @@ func TestValidateTestSuiteReferences(t *testing.T) { GroupsByComponent: make(map[string][]string), PackageGroups: make(map[string]projectconfig.PackageGroupConfig), } - assert.NoError(t, cfg.Validate()) + assert.NoError(t, cfg.Validate(false)) + }) + + t.Run("permissive parsing ignores undefined test reference", func(t *testing.T) { + cfg := projectconfig.ProjectConfig{ + Images: map[string]projectconfig.ImageConfig{ + "myimage": { + Name: "myimage", + Tests: projectconfig.ImageTestsConfig{TestSuites: []projectconfig.TestSuiteRef{{Name: "nonexistent"}}}, + }, + }, + TestSuites: make(map[string]projectconfig.TestSuiteConfig), + Components: make(map[string]projectconfig.ComponentConfig), + ComponentGroups: make(map[string]projectconfig.ComponentGroupConfig), + Distros: make(map[string]projectconfig.DistroDefinition), + GroupsByComponent: make(map[string][]string), + PackageGroups: make(map[string]projectconfig.PackageGroupConfig), + } + assert.NoError(t, cfg.Validate(true)) }) }