diff --git a/bundle/config/mutator/resolve_variable_references.go b/bundle/config/mutator/resolve_variable_references.go index 42c22b61533..2c0417deddf 100644 --- a/bundle/config/mutator/resolve_variable_references.go +++ b/bundle/config/mutator/resolve_variable_references.go @@ -59,7 +59,8 @@ type resolveVariableReferences struct { // outside of 'resources'. includeResources bool - artifactsReferenceUsed bool + artifactsReferenceUsed bool + validateVolumeSchemaRefs bool // excludePaths lists variable reference paths (e.g. "workspace.file_path") whose // resolution should be skipped. References to these paths remain unresolved so a @@ -103,11 +104,12 @@ func ResolveVariableReferencesInLookup() bundle.Mutator { // ResolveVolumePathReferencesOnlyResources resolves only references to resources.volumes.*.volume_path. func ResolveVolumePathReferencesOnlyResources() bundle.Mutator { return &resolveVariableReferences{ - prefixes: []string{"resources"}, - lookupFn: lookup, - allowPathFn: isVolumePathReferencePath, - extraRounds: maxResolutionRounds - 1, - includeResources: true, + prefixes: []string{"resources"}, + lookupFn: lookup, + allowPathFn: isVolumePathReferencePath, + extraRounds: maxResolutionRounds - 1, + includeResources: true, + validateVolumeSchemaRefs: true, } } @@ -169,6 +171,12 @@ func (m *resolveVariableReferences) Apply(ctx context.Context, b *bundle.Bundle) varPath := dyn.NewPath(dyn.Key("var")) var diags diag.Diagnostics + if m.validateVolumeSchemaRefs { + diags = diags.Extend(validateVolumeSchemaReferences(b)) + if diags.HasError() { + return diags + } + } maxRounds := 1 + m.extraRounds for round := range maxRounds { @@ -201,6 +209,28 @@ func (m *resolveVariableReferences) Apply(ctx context.Context, b *bundle.Bundle) return diags } +func validateVolumeSchemaReferences(b *bundle.Bundle) diag.Diagnostics { + var diags diag.Diagnostics + for _, volume := range b.Config.Resources.Volumes { + if volume == nil { + continue + } + for _, value := range []string{volume.SchemaName, volume.CatalogName} { + _, _ = dynvar.Resolve(dyn.V(value), func(path dyn.Path) (dyn.Value, error) { + if len(path) > 0 && path[0].Key() == "schemas" { + diags = diags.Append(diag.Diagnostic{ + Severity: diag.Error, + Summary: fmt.Sprintf("reference does not exist: ${%s}", path.String()), + Detail: fmt.Sprintf("did you mean:\n ${resources.%s}", path.String()), + }) + } + return dyn.InvalidValue, dynvar.ErrSkipResolution + }) + } + } + return diags +} + func (m *resolveVariableReferences) resolveOnce(b *bundle.Bundle, prefixes []dyn.Path, varPath dyn.Path) (bool, diag.Diagnostics) { var diags diag.Diagnostics hasUpdates := false diff --git a/bundle/config/mutator/resolve_variable_references_test.go b/bundle/config/mutator/resolve_variable_references_test.go index 284b78846b4..80be93f6ecf 100644 --- a/bundle/config/mutator/resolve_variable_references_test.go +++ b/bundle/config/mutator/resolve_variable_references_test.go @@ -105,6 +105,50 @@ func TestResolveVolumePathReferencesOnlyResources_MissingTarget(t *testing.T) { require.ErrorContains(t, diags.Error(), "reference does not exist: ${resources.volumes.missing.volume_path}") } +func TestResolveVolumePathReferencesOnlyResources_RejectsUnprefixedSchemaReferences(t *testing.T) { + b := &bundle.Bundle{ + Config: config.Root{ + Resources: config.Resources{ + Schemas: map[string]*resources.Schema{"test": {}}, + Volumes: map[string]*resources.Volume{"foo": {}}, + }, + }, + } + volume := b.Config.Resources.Volumes["foo"] + volume.SchemaName = "${schemas.test.name}" + volume.CatalogName = "${schemas.test.catalog_name}" + + diags := bundle.Apply(t.Context(), b, ResolveVolumePathReferencesOnlyResources()) + require.Len(t, diags, 2) + require.Error(t, diags.Error()) + require.Equal(t, "reference does not exist: ${schemas.test.name}", diags[0].Summary) + require.Equal(t, "did you mean:\n ${resources.schemas.test.name}", diags[0].Detail) + require.Equal(t, "reference does not exist: ${schemas.test.catalog_name}", diags[1].Summary) + require.Equal(t, "did you mean:\n ${resources.schemas.test.catalog_name}", diags[1].Detail) + + b = &bundle.Bundle{ + Config: config.Root{ + Resources: config.Resources{ + Schemas: map[string]*resources.Schema{"test": {}}, + Volumes: map[string]*resources.Volume{"foo": {}}, + }, + }, + } + schema := b.Config.Resources.Schemas["test"] + schema.Name = "myschema" + schema.CatalogName = "main" + volume = b.Config.Resources.Volumes["foo"] + volume.SchemaName = "${resources.schemas.test.name}" + volume.CatalogName = "${resources.schemas.test.catalog_name}" + + diags = bundle.Apply(t.Context(), b, ResolveVolumePathReferencesOnlyResources()) + require.NoError(t, diags.Error()) + diags = bundle.Apply(t.Context(), b, ResolveVariableReferencesOnlyResources("resources")) + require.NoError(t, diags.Error()) + require.Equal(t, "myschema", b.Config.Resources.Volumes["foo"].SchemaName) + require.Equal(t, "main", b.Config.Resources.Volumes["foo"].CatalogName) +} + func TestIsVolumePathReferencePath(t *testing.T) { require.True(t, isVolumePathReferencePath(dyn.MustPathFromString("resources.volumes.foo.volume_path"))) require.False(t, isVolumePathReferencePath(dyn.MustPathFromString("resources.volumes.foo.name")))