Add support for validating CLI version when loading a jsonschema object (#883)

## Changes
Updates to bundle templates can require updated versions of the CLI.
This PR extends the JSON schema representation to allow template authors
to set a min CLI version they require for their templates.

This is required to make improvements/additions to the mlops-stacks repo

## Tests
Tested using unit tests and manually. 

For manualy testing, I created a custom build of the CLI using go
releaser and then tested it against a local instance of mlops-stack
When mlops-stack schema has:
```
  "min_databricks_cli_version": "v5000.1.1",
```

output (error as expected)
```
shreyas.goenka@THW32HFW6T bricks % ./dist/cli_darwin_arm64/databricks bundle init  ~/mlops-stack
Error: minimum CLI version "v5000.1.1" is greater than current CLI version "v0.207.2-dev+1b992c0". Please upgrade your current Databricks CLI
```

When the mlops-stack schema has:
```
  "min_databricks_cli_version": "v0.1.1",
```

output (validation passes)
```
shreyas.goenka@THW32HFW6T bricks % ./dist/cli_darwin_arm64/databricks bundle init  ~/mlops-stack
Welcome to MLOps Stack. For detailed information on project generation, see the README at https://github.com/databricks/mlops-stack/blob/main/README.md.

Project Name [my-mlops-project]: ^C
```
This commit is contained in:
shreyas-goenka 2023-10-19 16:01:48 +02:00 committed by GitHub
parent 7139487c2f
commit 3700785dfa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 109 additions and 7 deletions

View File

@ -33,6 +33,8 @@ var info Info
var once sync.Once var once sync.Once
var DefaultSemver = "0.0.0-dev"
// getDefaultBuildVersion uses build information stored by Go itself // getDefaultBuildVersion uses build information stored by Go itself
// to synthesize a build version if one wasn't set. // to synthesize a build version if one wasn't set.
// This is necessary if the binary was not built through goreleaser. // This is necessary if the binary was not built through goreleaser.
@ -47,7 +49,7 @@ func getDefaultBuildVersion() string {
m[s.Key] = s.Value m[s.Key] = s.Value
} }
out := "0.0.0-dev" out := DefaultSemver
// Append revision as build metadata. // Append revision as build metadata.
if v, ok := m["vcs.revision"]; ok { if v, ok := m["vcs.revision"]; ok {

View File

@ -18,4 +18,9 @@ type Extension struct {
// PatternMatchFailureMessage is a user defined message that is displayed to the // PatternMatchFailureMessage is a user defined message that is displayed to the
// user if a JSON schema pattern match fails. // user if a JSON schema pattern match fails.
PatternMatchFailureMessage string `json:"pattern_match_failure_message,omitempty"` PatternMatchFailureMessage string `json:"pattern_match_failure_message,omitempty"`
// Set the minimum semver version of this CLI to validate when loading this schema.
// If the CLI version is less than this value, then validation for this
// schema will fail.
MinDatabricksCliVersion string `json:"min_databricks_cli_version,omitempty"`
} }

View File

@ -6,6 +6,9 @@ import (
"os" "os"
"regexp" "regexp"
"slices" "slices"
"github.com/databricks/cli/internal/build"
"golang.org/x/mod/semver"
) )
// defines schema for a json object // defines schema for a json object
@ -67,8 +70,8 @@ const (
IntegerType Type = "integer" IntegerType Type = "integer"
) )
func (schema *Schema) validate() error { // Validate property types are all valid JSON schema types.
// Validate property types are all valid JSON schema types. func (schema *Schema) validateSchemaPropertyTypes() error {
for _, v := range schema.Properties { for _, v := range schema.Properties {
switch v.Type { switch v.Type {
case NumberType, BooleanType, StringType, IntegerType: case NumberType, BooleanType, StringType, IntegerType:
@ -83,8 +86,11 @@ func (schema *Schema) validate() error {
return fmt.Errorf("type %s is not a recognized json schema type", v.Type) return fmt.Errorf("type %s is not a recognized json schema type", v.Type)
} }
} }
return nil
}
// Validate default property values are consistent with types. // Validate default property values are consistent with types.
func (schema *Schema) validateSchemaDefaultValueTypes() error {
for name, property := range schema.Properties { for name, property := range schema.Properties {
if property.Default == nil { if property.Default == nil {
continue continue
@ -93,8 +99,11 @@ func (schema *Schema) validate() error {
return fmt.Errorf("type validation for default value of property %s failed: %w", name, err) return fmt.Errorf("type validation for default value of property %s failed: %w", name, err)
} }
} }
return nil
}
// Validate enum field values for properties are consistent with types. // Validate enum field values for properties are consistent with types.
func (schema *Schema) validateSchemaEnumValueTypes() error {
for name, property := range schema.Properties { for name, property := range schema.Properties {
if property.Enum == nil { if property.Enum == nil {
continue continue
@ -106,8 +115,11 @@ func (schema *Schema) validate() error {
} }
} }
} }
return nil
}
// Validate default value is contained in the list of enums if both are defined. // Validate default value is contained in the list of enums if both are defined.
func (schema *Schema) validateSchemaDefaultValueIsInEnums() error {
for name, property := range schema.Properties { for name, property := range schema.Properties {
if property.Default == nil || property.Enum == nil { if property.Default == nil || property.Enum == nil {
continue continue
@ -118,8 +130,11 @@ func (schema *Schema) validate() error {
return fmt.Errorf("list of enum values for property %s does not contain default value %v: %v", name, property.Default, property.Enum) return fmt.Errorf("list of enum values for property %s does not contain default value %v: %v", name, property.Default, property.Enum)
} }
} }
return nil
}
// Validate usage of "pattern" is consistent. // Validate usage of "pattern" is consistent.
func (schema *Schema) validateSchemaPattern() error {
for name, property := range schema.Properties { for name, property := range schema.Properties {
pattern := property.Pattern pattern := property.Pattern
if pattern == "" { if pattern == "" {
@ -153,6 +168,47 @@ func (schema *Schema) validate() error {
return nil return nil
} }
func (schema *Schema) validateSchemaMinimumCliVersion(currentVersion string) func() error {
return func() error {
if schema.MinDatabricksCliVersion == "" {
return nil
}
// Ignore this validation rule for local builds.
if semver.Compare("v"+build.DefaultSemver, currentVersion) == 0 {
return nil
}
// Confirm that MinDatabricksCliVersion is a valid semver.
if !semver.IsValid(schema.MinDatabricksCliVersion) {
return fmt.Errorf("invalid minimum CLI version %q specified. Please specify the version in the format v0.0.0", schema.MinDatabricksCliVersion)
}
// Confirm that MinDatabricksCliVersion is less than or equal to the current version.
if semver.Compare(schema.MinDatabricksCliVersion, currentVersion) > 0 {
return fmt.Errorf("minimum CLI version %q is greater than current CLI version %q. Please upgrade your current Databricks CLI", schema.MinDatabricksCliVersion, currentVersion)
}
return nil
}
}
func (schema *Schema) validate() error {
for _, fn := range []func() error{
schema.validateSchemaPropertyTypes,
schema.validateSchemaDefaultValueTypes,
schema.validateSchemaEnumValueTypes,
schema.validateSchemaDefaultValueIsInEnums,
schema.validateSchemaPattern,
schema.validateSchemaMinimumCliVersion("v" + build.GetInfo().Version),
} {
err := fn()
if err != nil {
return err
}
}
return nil
}
func Load(path string) (*Schema, error) { func Load(path string) (*Schema, error) {
b, err := os.ReadFile(path) b, err := os.ReadFile(path)
if err != nil { if err != nil {

View File

@ -222,3 +222,42 @@ func TestSchemaValidatePatternEnum(t *testing.T) {
} }
assert.NoError(t, s.validate()) assert.NoError(t, s.validate())
} }
func TestValidateSchemaMinimumCliVersionWithInvalidSemver(t *testing.T) {
s := &Schema{
Extension: Extension{
MinDatabricksCliVersion: "1.0.5",
},
}
err := s.validateSchemaMinimumCliVersion("v2.0.1")()
assert.ErrorContains(t, err, "invalid minimum CLI version \"1.0.5\" specified. Please specify the version in the format v0.0.0")
s.MinDatabricksCliVersion = "v1.0.5"
err = s.validateSchemaMinimumCliVersion("v2.0.1")()
assert.NoError(t, err)
}
func TestValidateSchemaMinimumCliVersion(t *testing.T) {
s := &Schema{
Extension: Extension{
MinDatabricksCliVersion: "v1.0.5",
},
}
err := s.validateSchemaMinimumCliVersion("v2.0.1")()
assert.NoError(t, err)
err = s.validateSchemaMinimumCliVersion("v1.0.5")()
assert.NoError(t, err)
err = s.validateSchemaMinimumCliVersion("v1.0.6")()
assert.NoError(t, err)
err = s.validateSchemaMinimumCliVersion("v1.0.4")()
assert.ErrorContains(t, err, `minimum CLI version "v1.0.5" is greater than current CLI version "v1.0.4". Please upgrade your current Databricks CLI`)
err = s.validateSchemaMinimumCliVersion("v0.0.1")()
assert.ErrorContains(t, err, "minimum CLI version \"v1.0.5\" is greater than current CLI version \"v0.0.1\". Please upgrade your current Databricks CLI")
err = s.validateSchemaMinimumCliVersion("v0.0.0-dev")()
assert.NoError(t, err)
}