databricks-cli/bundle/config/validate/unique_resource_keys.go

117 lines
3.1 KiB
Go
Raw Normal View History

package validate
import (
"context"
"fmt"
"slices"
"sort"
"github.com/databricks/cli/bundle"
"github.com/databricks/cli/libs/diag"
"github.com/databricks/cli/libs/dyn"
)
// This mutator validates that:
//
// 1. Each resource key is unique across different resource types. No two resources
// of the same type can have the same key. This is because command like "bundle run"
// rely on the resource key to identify the resource to run.
// Eg: jobs.foo and pipelines.foo are not allowed simultaneously.
//
// 2. Each resource definition is contained within a single file, and is not spread
// across multiple files. Note: This is not applicable to resource configuration
// defined in a target override. That is why this mutator MUST run before the target
// overrides are merged.
func UniqueResourceKeys() bundle.Mutator {
return &uniqueResourceKeys{}
}
type uniqueResourceKeys struct{}
func (m *uniqueResourceKeys) Name() string {
return "validate:unique_resource_keys"
}
func (m *uniqueResourceKeys) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics {
diags := diag.Diagnostics{}
type metadata struct {
locations []dyn.Location
paths []dyn.Path
}
// Maps of resource key to the paths and locations the resource is defined at.
resourceMetadata := map[string]*metadata{}
rv := b.Config.Value().Get("resources")
// return early if no resources are defined or the resources block is empty.
if rv.Kind() == dyn.KindInvalid || rv.Kind() == dyn.KindNil {
return diags
}
// Gather the paths and locations of all resources.
_, err := dyn.MapByPattern(
rv,
dyn.NewPattern(dyn.AnyKey(), dyn.AnyKey()),
func(p dyn.Path, v dyn.Value) (dyn.Value, error) {
// The key for the resource. Eg: "my_job" for jobs.my_job.
k := p[1].Key()
m, ok := resourceMetadata[k]
if !ok {
m = &metadata{
paths: []dyn.Path{},
locations: []dyn.Location{},
}
}
// dyn.Path under the hood is a slice. The code that walks the configuration
// tree uses the same underlying slice to track the path as it walks
// the tree. So, we need to clone it here.
m.paths = append(m.paths, slices.Clone(p))
m.locations = append(m.locations, v.Locations()...)
resourceMetadata[k] = m
return v, nil
},
)
if err != nil {
return diag.FromErr(err)
}
for k, v := range resourceMetadata {
if len(v.locations) <= 1 {
continue
}
// Sort the locations and paths for consistent error messages. This helps
// with unit testing.
sort.Slice(v.locations, func(i, j int) bool {
l1 := v.locations[i]
l2 := v.locations[j]
if l1.File != l2.File {
return l1.File < l2.File
}
if l1.Line != l2.Line {
return l1.Line < l2.Line
}
return l1.Column < l2.Column
})
sort.Slice(v.paths, func(i, j int) bool {
return v.paths[i].String() < v.paths[j].String()
})
// If there are multiple resources with the same key, report an error.
diags = append(diags, diag.Diagnostic{
Severity: diag.Error,
Summary: fmt.Sprintf("multiple resources have been defined with the same key: %s", k),
Locations: v.locations,
Paths: v.paths,
})
}
return diags
}