Return diagnostics from `config.Load` (#1324)

## Changes

We no longer need to store load diagnostics on the `config.Root` type
itself and instead can return them from the `config.Load` call directly.
It is up to the caller of this function to append them to previous
diagnostics, if any.

Background: previous commits moved configuration loading of the entry
point into a mutator, so now all diagnostics naturally flow from
applying mutators.

This PR depends on #1319.

## Tests

Unit and manual validation of the debug statements in the validate
command.
This commit is contained in:
Pieter Noordhuis 2024-03-28 11:59:03 +01:00 committed by GitHub
parent b21e3c81cd
commit eea34b2504
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 34 additions and 42 deletions

View File

@ -24,11 +24,13 @@ func (m *entryPoint) Apply(_ context.Context, b *bundle.Bundle) diag.Diagnostics
if err != nil { if err != nil {
return diag.FromErr(err) return diag.FromErr(err)
} }
this, err := config.Load(path) this, diags := config.Load(path)
if err != nil { if diags.HasError() {
return diag.FromErr(err) return diags
} }
// TODO: Return actual warnings.
err = b.Config.Merge(this) err = b.Config.Merge(this)
return diag.FromErr(err) if err != nil {
diags = diags.Extend(diag.FromErr(err))
}
return diags
} }

View File

@ -27,11 +27,13 @@ func (m *processInclude) Name() string {
} }
func (m *processInclude) Apply(_ context.Context, b *bundle.Bundle) diag.Diagnostics { func (m *processInclude) Apply(_ context.Context, b *bundle.Bundle) diag.Diagnostics {
this, err := config.Load(m.fullPath) this, diags := config.Load(m.fullPath)
if err != nil { if diags.HasError() {
return diag.FromErr(err) return diags
} }
// TODO: Return actual warnings. err := b.Config.Merge(this)
err = b.Config.Merge(this) if err != nil {
return diag.FromErr(err) diags = diags.Extend(diag.FromErr(err))
}
return diags
} }

View File

@ -20,7 +20,6 @@ import (
type Root struct { type Root struct {
value dyn.Value value dyn.Value
diags diag.Diagnostics
depth int depth int
// Contains user defined variables // Contains user defined variables
@ -69,10 +68,10 @@ type Root struct {
} }
// Load loads the bundle configuration file at the specified path. // Load loads the bundle configuration file at the specified path.
func Load(path string) (*Root, error) { func Load(path string) (*Root, diag.Diagnostics) {
raw, err := os.ReadFile(path) raw, err := os.ReadFile(path)
if err != nil { if err != nil {
return nil, err return nil, diag.FromErr(err)
} }
r := Root{} r := Root{}
@ -80,31 +79,29 @@ func Load(path string) (*Root, error) {
// Load configuration tree from YAML. // Load configuration tree from YAML.
v, err := yamlloader.LoadYAML(path, bytes.NewBuffer(raw)) v, err := yamlloader.LoadYAML(path, bytes.NewBuffer(raw))
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to load %s: %w", path, err) return nil, diag.Errorf("failed to load %s: %v", path, err)
} }
// Rewrite configuration tree where necessary. // Rewrite configuration tree where necessary.
v, err = rewriteShorthands(v) v, err = rewriteShorthands(v)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to rewrite %s: %w", path, err) return nil, diag.Errorf("failed to rewrite %s: %v", path, err)
} }
// Normalize dynamic configuration tree according to configuration type. // Normalize dynamic configuration tree according to configuration type.
v, diags := convert.Normalize(r, v) v, diags := convert.Normalize(r, v)
// Keep track of diagnostics (warnings and errors in the schema).
// We delay acting on diagnostics until we have loaded all
// configuration files and merged them together.
r.diags = diags
// Convert normalized configuration tree to typed configuration. // Convert normalized configuration tree to typed configuration.
err = r.updateWithDynamicValue(v) err = r.updateWithDynamicValue(v)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to load %s: %w", path, err) return nil, diag.Errorf("failed to load %s: %v", path, err)
} }
_, err = r.Resources.VerifyUniqueResourceIdentifiers() _, err = r.Resources.VerifyUniqueResourceIdentifiers()
return &r, err if err != nil {
diags = diags.Extend(diag.FromErr(err))
}
return &r, diags
} }
func (r *Root) initializeDynamicValue() error { func (r *Root) initializeDynamicValue() error {
@ -126,11 +123,9 @@ func (r *Root) initializeDynamicValue() error {
func (r *Root) updateWithDynamicValue(nv dyn.Value) error { func (r *Root) updateWithDynamicValue(nv dyn.Value) error {
// Hack: restore state; it may be cleared by [ToTyped] if // Hack: restore state; it may be cleared by [ToTyped] if
// the configuration equals nil (happens in tests). // the configuration equals nil (happens in tests).
diags := r.diags
depth := r.depth depth := r.depth
defer func() { defer func() {
r.diags = diags
r.depth = depth r.depth = depth
}() }()
@ -224,10 +219,6 @@ func (r *Root) MarkMutatorExit(ctx context.Context) error {
return nil return nil
} }
func (r *Root) Diagnostics() diag.Diagnostics {
return r.diags
}
// SetConfigFilePath configures the path that its configuration // SetConfigFilePath configures the path that its configuration
// was loaded from in configuration leafs that require it. // was loaded from in configuration leafs that require it.
func (r *Root) ConfigureConfigFilePath() { func (r *Root) ConfigureConfigFilePath() {
@ -261,9 +252,6 @@ func (r *Root) InitializeVariables(vars []string) error {
} }
func (r *Root) Merge(other *Root) error { func (r *Root) Merge(other *Root) error {
// Merge diagnostics.
r.diags = append(r.diags, other.diags...)
// Check for safe merge, protecting against duplicate resource identifiers // Check for safe merge, protecting against duplicate resource identifiers
err := r.Resources.VerifySafeMerge(&other.Resources) err := r.Resources.VerifySafeMerge(&other.Resources)
if err != nil { if err != nil {

View File

@ -25,24 +25,24 @@ func TestRootMarshalUnmarshal(t *testing.T) {
} }
func TestRootLoad(t *testing.T) { func TestRootLoad(t *testing.T) {
root, err := Load("../tests/basic/databricks.yml") root, diags := Load("../tests/basic/databricks.yml")
require.NoError(t, err) require.NoError(t, diags.Error())
assert.Equal(t, "basic", root.Bundle.Name) assert.Equal(t, "basic", root.Bundle.Name)
} }
func TestDuplicateIdOnLoadReturnsError(t *testing.T) { func TestDuplicateIdOnLoadReturnsError(t *testing.T) {
_, err := Load("./testdata/duplicate_resource_names_in_root/databricks.yml") _, diags := Load("./testdata/duplicate_resource_names_in_root/databricks.yml")
assert.ErrorContains(t, err, "multiple resources named foo (job at ./testdata/duplicate_resource_names_in_root/databricks.yml, pipeline at ./testdata/duplicate_resource_names_in_root/databricks.yml)") assert.ErrorContains(t, diags.Error(), "multiple resources named foo (job at ./testdata/duplicate_resource_names_in_root/databricks.yml, pipeline at ./testdata/duplicate_resource_names_in_root/databricks.yml)")
} }
func TestDuplicateIdOnMergeReturnsError(t *testing.T) { func TestDuplicateIdOnMergeReturnsError(t *testing.T) {
root, err := Load("./testdata/duplicate_resource_name_in_subconfiguration/databricks.yml") root, diags := Load("./testdata/duplicate_resource_name_in_subconfiguration/databricks.yml")
require.NoError(t, err) require.NoError(t, diags.Error())
other, err := Load("./testdata/duplicate_resource_name_in_subconfiguration/resources.yml") other, diags := Load("./testdata/duplicate_resource_name_in_subconfiguration/resources.yml")
require.NoError(t, err) require.NoError(t, diags.Error())
err = root.Merge(other) err := root.Merge(other)
assert.ErrorContains(t, err, "multiple resources named foo (job at ./testdata/duplicate_resource_name_in_subconfiguration/databricks.yml, pipeline at ./testdata/duplicate_resource_name_in_subconfiguration/resources.yml)") assert.ErrorContains(t, err, "multiple resources named foo (job at ./testdata/duplicate_resource_name_in_subconfiguration/databricks.yml, pipeline at ./testdata/duplicate_resource_name_in_subconfiguration/resources.yml)")
} }

View File

@ -32,7 +32,7 @@ func newValidateCommand() *cobra.Command {
// Until we change up the output of this command to be a text representation, // Until we change up the output of this command to be a text representation,
// we'll just output all diagnostics as debug logs. // we'll just output all diagnostics as debug logs.
for _, diag := range b.Config.Diagnostics() { for _, diag := range diags {
log.Debugf(cmd.Context(), "[%s]: %s", diag.Location, diag.Summary) log.Debugf(cmd.Context(), "[%s]: %s", diag.Location, diag.Summary)
} }