-
Notifications
You must be signed in to change notification settings - Fork 180
Add validation for files with a .(resource-name).yml extension
#1780
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 12 commits
aa51b63
667fe6b
458dbfb
5c351d7
e9426be
fa6b68c
ccbd199
3571410
f3d7f09
5bf9163
e31dcc2
894f4aa
fd01824
5308ad8
d14b1e2
4734249
d9cf582
6040e58
4cc0dd1
fec73db
3203858
28917eb
4373e7b
65ad2f0
b4bd47f
dd28a56
14e73ac
e22ba75
367048b
dd20a52
dd10eed
90f360b
a737977
d064566
b288b07
b1d7c7b
361f590
caa8d64
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,12 +3,144 @@ package loader | |
| import ( | ||
| "context" | ||
| "fmt" | ||
| "sort" | ||
| "strings" | ||
|
|
||
| "github.com/databricks/cli/bundle" | ||
| "github.com/databricks/cli/bundle/config" | ||
| "github.com/databricks/cli/libs/diag" | ||
| "github.com/databricks/cli/libs/dyn" | ||
| "golang.org/x/exp/maps" | ||
| ) | ||
|
|
||
| var resourceTypes = []string{ | ||
| "job", | ||
| "pipeline", | ||
| "model", | ||
| "experiment", | ||
| "model_serving_endpoint", | ||
| "registered_model", | ||
| "quality_monitor", | ||
| "schema", | ||
| "cluster", | ||
| } | ||
|
|
||
| func validateFileFormat(r *config.Root, filePath string) diag.Diagnostics { | ||
| for _, typ := range resourceTypes { | ||
| for _, ext := range []string{fmt.Sprintf(".%s.yml", typ), fmt.Sprintf(".%s.yaml", typ)} { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @fjakobs Can you comment on why we should check both? Useful context to include here.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe the plan is to have DABs in the workspace support both
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, I agree. @fjakobs Can you provide a snippet we can include for future readers wrt the expected extensions?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We do recommend using It's a SHOULD in https://docs.google.com/document/d/1M4UqmNdpDgKMfuBVSghm_UtaDdYp4r-1SA0SuWsgG_k/edit#heading=h.bs9f118p0tig and not a must. |
||
| if strings.HasSuffix(filePath, ext) { | ||
| return validateSingleResourceDefined(r, ext, typ) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return nil | ||
| } | ||
|
|
||
| func validateSingleResourceDefined(r *config.Root, ext, typ string) diag.Diagnostics { | ||
| type resource struct { | ||
| path dyn.Path | ||
| value dyn.Value | ||
| typ string | ||
| key string | ||
| } | ||
|
|
||
| resources := []resource{} | ||
|
|
||
| // Gather all resources defined in the resources block. | ||
| _, err := dyn.MapByPattern( | ||
| r.Value(), | ||
| dyn.NewPattern(dyn.Key("resources"), 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. | ||
|
shreyas-goenka marked this conversation as resolved.
Outdated
|
||
| k := p[2].Key() | ||
| // The type of the resource. Eg: "job" for jobs.my_job. | ||
|
shreyas-goenka marked this conversation as resolved.
Outdated
|
||
| typ := strings.TrimSuffix(p[1].Key(), "s") | ||
|
shreyas-goenka marked this conversation as resolved.
Outdated
|
||
|
|
||
| resources = append(resources, resource{path: p, value: v, typ: typ, key: k}) | ||
| return v, nil | ||
| }) | ||
| if err != nil { | ||
| return diag.FromErr(err) | ||
| } | ||
|
|
||
| // Gather all resources defined in a target block. | ||
| _, err = dyn.MapByPattern( | ||
|
shreyas-goenka marked this conversation as resolved.
|
||
| r.Value(), | ||
| dyn.NewPattern(dyn.Key("targets"), dyn.AnyKey(), dyn.Key("resources"), 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. | ||
|
shreyas-goenka marked this conversation as resolved.
Outdated
|
||
| k := p[4].Key() | ||
| // The type of the resource. Eg: "job" for jobs.my_job. | ||
|
shreyas-goenka marked this conversation as resolved.
Outdated
|
||
| typ := strings.TrimSuffix(p[3].Key(), "s") | ||
|
|
||
| resources = append(resources, resource{path: p, value: v, typ: typ, key: k}) | ||
| return v, nil | ||
| }) | ||
| if err != nil { | ||
| return diag.FromErr(err) | ||
| } | ||
|
|
||
| typeMatch := true | ||
| seenKeys := map[string]struct{}{} | ||
| for _, rr := range resources { | ||
| // case: The resource is not of the correct type. | ||
| if rr.typ != typ { | ||
| typeMatch = false | ||
| break | ||
| } | ||
|
|
||
| seenKeys[rr.key] = struct{}{} | ||
| } | ||
|
|
||
| // Format matches. There's less than or equal to one resource defined in the file. | ||
|
shreyas-goenka marked this conversation as resolved.
Outdated
|
||
| // The resource is also of the correct type. | ||
| if typeMatch && len(seenKeys) <= 1 { | ||
| return nil | ||
| } | ||
|
|
||
| msg := strings.Builder{} | ||
| msg.WriteString(fmt.Sprintf("We recommend only defining a single %s in a file with the %s extension.\n", typ, ext)) | ||
|
|
||
| // Dedup the list of resources before adding them the diagnostic message. This | ||
| // is needed because we do not dedup earlier when gathering the resources and | ||
| // it's valid to define the same resource in both the resources and targets block. | ||
| msg.WriteString("The following resources are defined or configured in this file:\n") | ||
| setOfLines := map[string]struct{}{} | ||
| for _, r := range resources { | ||
| setOfLines[fmt.Sprintf(" - %s (%s)\n", r.key, r.typ)] = struct{}{} | ||
| } | ||
| // Sort the line s to print to make the output deterministic. | ||
| listOfLines := maps.Keys(setOfLines) | ||
| sort.Strings(listOfLines) | ||
| for _, l := range listOfLines { | ||
| msg.WriteString(l) | ||
| } | ||
|
shreyas-goenka marked this conversation as resolved.
|
||
|
|
||
| locations := []dyn.Location{} | ||
| paths := []dyn.Path{} | ||
| for _, rr := range resources { | ||
| locations = append(locations, rr.value.Locations()...) | ||
| paths = append(paths, rr.path) | ||
| } | ||
| // Sort the locations and paths to make the output deterministic. | ||
| sort.Slice(locations, func(i, j int) bool { | ||
| return locations[i].String() < locations[j].String() | ||
| }) | ||
| sort.Slice(paths, func(i, j int) bool { | ||
| return paths[i].String() < paths[j].String() | ||
| }) | ||
|
|
||
| return diag.Diagnostics{ | ||
| { | ||
| Severity: diag.Info, | ||
| Summary: msg.String(), | ||
| Locations: locations, | ||
| Paths: paths, | ||
|
shreyas-goenka marked this conversation as resolved.
|
||
| }, | ||
| } | ||
| } | ||
|
|
||
| type processInclude struct { | ||
| fullPath string | ||
| relPath string | ||
|
|
@@ -31,6 +163,13 @@ func (m *processInclude) Apply(_ context.Context, b *bundle.Bundle) diag.Diagnos | |
| if diags.HasError() { | ||
| return diags | ||
| } | ||
|
|
||
| // Add any diagnostics associated with the file format. | ||
| diags = append(diags, validateFileFormat(this, m.relPath)...) | ||
|
shreyas-goenka marked this conversation as resolved.
Outdated
|
||
| if diags.HasError() { | ||
| return diags | ||
| } | ||
|
|
||
| err := b.Config.Merge(this) | ||
| if err != nil { | ||
| diags = diags.Extend(diag.FromErr(err)) | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.