Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions bundle/config/bundle.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,7 @@ type Bundle struct {

// Deployment section specifies deployment related configuration for bundle
Deployment Deployment `json:"deployment,omitempty"`

// Databricks CLI version constraints required to run the bundle.
DatabricksCliVersion string `json:"databricks_cli_version,omitempty"`
}
3 changes: 3 additions & 0 deletions bundle/config/mutator/mutator.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ func DefaultMutators() []bundle.Mutator {
loader.EntryPoint(),
loader.ProcessRootIncludes(),

// Verify that the CLI version is within the specified range.
VerifyCliVersion(),

// Execute preinit script after loading all configuration files.
scripts.Execute(config.ScriptPreInit),
EnvironmentsToTargets(),
Expand Down
82 changes: 82 additions & 0 deletions bundle/config/mutator/verify_cli_version.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package mutator

import (
"context"
"fmt"
"regexp"

semver "github.com/Masterminds/semver/v3"
"github.com/databricks/cli/bundle"
"github.com/databricks/cli/internal/build"
"github.com/databricks/cli/libs/diag"
)

func VerifyCliVersion() bundle.Mutator {
return &verifyCliVersion{}
}

type verifyCliVersion struct {
}

func (v *verifyCliVersion) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics {
// No constraints specified, skip the check.
if b.Config.Bundle.DatabricksCliVersion == "" {
return nil
}

constraint := b.Config.Bundle.DatabricksCliVersion
if err := validateConstraintSyntax(constraint); err != nil {
return diag.FromErr(err)
}
currentVersion := build.GetInfo().Version
c, err := semver.NewConstraint(constraint)
if err != nil {
return diag.FromErr(err)
}

version, err := semver.NewVersion(currentVersion)
if err != nil {
return diag.Errorf("parsing CLI version %q failed", currentVersion)
}

if !c.Check(version) {
return diag.Errorf("Databricks CLI version constraint not satisfied. Required: %s, current: %s", constraint, currentVersion)
}

return nil
}

func (v *verifyCliVersion) Name() string {
return "VerifyCliVersion"
}

// validateConstraintSyntax validates the syntax of the version constraint.
func validateConstraintSyntax(constraint string) error {
r := generateConstraintSyntaxRegexp()
if !r.MatchString(constraint) {
return fmt.Errorf("invalid version constraint %q specified. Please specify the version constraint in the format (>=) 0.0.0(, <= 1.0.0)", constraint)
}

return nil
}

// Generate regexp which matches the supported version constraint syntax.
func generateConstraintSyntaxRegexp() *regexp.Regexp {
// Supported formats are:
Comment thread
andrewnester marked this conversation as resolved.
Outdated
// 1. 0.0.0
// 2. >= 0.0.0
// 3. <= 0.0.0
// 4. > 0.0.0
// 5. < 0.0.0
// 6. != 0.0.0
// 7. 0.0.*
// 8. 0.*
// 9. >= 0.0.0, <= 1.0.0
// 10. 0.0.0-0
// 11. 0.0.0-beta
// 12. >= 0.0.0-0, <= 1.0.0-0

matchVersion := `(\d+\.\d+\.\d+(\-\w+)?|\d+\.\d+.\*|\d+\.\*)`
matchOperators := `(>=|<=|>|<|!=)?`
return regexp.MustCompile(fmt.Sprintf(`^%s ?%s(, %s %s)?$`, matchOperators, matchVersion, matchOperators, matchVersion))
}
170 changes: 170 additions & 0 deletions bundle/config/mutator/verify_cli_version_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
package mutator

import (
"context"
"fmt"
"testing"

"github.com/databricks/cli/bundle"
"github.com/databricks/cli/bundle/config"
"github.com/databricks/cli/internal/build"
"github.com/stretchr/testify/require"
)

type testCase struct {
currentVersion string
constraint string
expectedError string
}

func TestVerifyCliVersion(t *testing.T) {
Comment thread
andrewnester marked this conversation as resolved.
testCases := []testCase{
{
currentVersion: "0.0.1",
},
{
currentVersion: "0.0.1",
constraint: "0.100.0",
expectedError: "Databricks CLI version constraint not satisfied. Required: 0.100.0, current: 0.0.1",
},
{
currentVersion: "0.0.1",
constraint: ">= 0.100.0",
expectedError: "Databricks CLI version constraint not satisfied. Required: >= 0.100.0, current: 0.0.1",
},
{
currentVersion: "0.100.0",
constraint: "0.100.0",
},
{
currentVersion: "0.100.1",
constraint: "0.100.0",
expectedError: "Databricks CLI version constraint not satisfied. Required: 0.100.0, current: 0.100.1",
},
{
currentVersion: "0.100.1",
constraint: ">= 0.100.0",
},
{
currentVersion: "0.100.0",
constraint: "<= 1.0.0",
},
{
currentVersion: "1.0.0",
constraint: "<= 1.0.0",
},
{
currentVersion: "1.0.0",
constraint: "<= 0.100.0",
expectedError: "Databricks CLI version constraint not satisfied. Required: <= 0.100.0, current: 1.0.0",
},
{
currentVersion: "0.99.0",
constraint: ">= 0.100.0, <= 0.100.2",
expectedError: "Databricks CLI version constraint not satisfied. Required: >= 0.100.0, <= 0.100.2, current: 0.99.0",
},
{
currentVersion: "0.100.0",
constraint: ">= 0.100.0, <= 0.100.2",
},
{
currentVersion: "0.100.1",
constraint: ">= 0.100.0, <= 0.100.2",
},
{
currentVersion: "0.100.2",
constraint: ">= 0.100.0, <= 0.100.2",
},
{
currentVersion: "0.101.0",
constraint: ">= 0.100.0, <= 0.100.2",
expectedError: "Databricks CLI version constraint not satisfied. Required: >= 0.100.0, <= 0.100.2, current: 0.101.0",
},
{
currentVersion: "0.100.0-beta",
constraint: ">= 0.100.0, <= 0.100.2",
expectedError: "Databricks CLI version constraint not satisfied. Required: >= 0.100.0, <= 0.100.2, current: 0.100.0-beta",
},
{
currentVersion: "0.100.0-beta",
constraint: ">= 0.100.0-0, <= 0.100.2-0",
},
{
currentVersion: "0.100.1-beta",
constraint: ">= 0.100.0-0, <= 0.100.2-0",
},
{
currentVersion: "0.100.3-beta",
constraint: ">= 0.100.0, <= 0.100.2",
expectedError: "Databricks CLI version constraint not satisfied. Required: >= 0.100.0, <= 0.100.2, current: 0.100.3-beta",
},
{
currentVersion: "0.100.123",
constraint: "0.100.*",
Comment thread
andrewnester marked this conversation as resolved.
},
{
currentVersion: "0.100.123",
constraint: "^0.100",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should just stick to the operators of requirementst.txt? (https://pip.pypa.io/en/stable/reference/requirements-file-format/)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have a strong opinion here but since it's allowed by library we use for version check I'd prefer to have it as it's pretty common syntax in other languages / tools as well

expectedError: "invalid version constraint \"^0.100\" specified. Please specify the version constraint in the format (>=) 0.0.0(, <= 1.0.0)",
},
}

t.Cleanup(func() {
build.SetBuildVersion(build.DefaultSemver)
Comment thread
andrewnester marked this conversation as resolved.
})

for i, tc := range testCases {
t.Run(fmt.Sprintf("testcase #%d", i), func(t *testing.T) {
build.SetBuildVersion(tc.currentVersion)
b := &bundle.Bundle{
Config: config.Root{
Bundle: config.Bundle{
DatabricksCliVersion: tc.constraint,
},
},
}
diags := bundle.Apply(context.Background(), b, VerifyCliVersion())
if tc.expectedError != "" {
require.NotEmpty(t, diags)
require.Equal(t, tc.expectedError, diags.Error().Error())
} else {
require.Empty(t, diags)
}
})
}
}

func TestValidateConstraint(t *testing.T) {
testCases := []struct {
constraint string
expected bool
}{
{"0.0.0", true},
{">= 0.0.0", true},
{"<= 0.0.0", true},
{"> 0.0.0", true},
{"< 0.0.0", true},
{"!= 0.0.0", true},
{"0.0.*", true},
{"0.*", true},
{">= 0.0.0, <= 1.0.0", true},
{">= 0.0.0-0, <= 1.0.0-0", true},
{"0.0.0-0", true},
{"0.0.0-beta", true},
{"^0.0.0", false},
{"~0.0.0", false},
{"0.0.0 1.0.0", false},
{"> 0.0.0 < 1.0.0", false},
}

for _, tc := range testCases {
t.Run(tc.constraint, func(t *testing.T) {
err := validateConstraintSyntax(tc.constraint)
if tc.expected {
require.NoError(t, err)
} else {
require.Error(t, err)
}
})
}
}
5 changes: 4 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ require (
gopkg.in/ini.v1 v1.67.0 // Apache 2.0
)

require gopkg.in/yaml.v3 v3.0.1
require (
github.com/Masterminds/semver/v3 v3.2.1
gopkg.in/yaml.v3 v3.0.1
)
Comment thread
andrewnester marked this conversation as resolved.

require (
cloud.google.com/go/compute v1.23.4 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions internal/build/variables.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,9 @@ var buildPatch string = "0"
var buildPrerelease string = ""
var buildIsSnapshot string = "false"
var buildTimestamp string = "0"

// This function is used to set the build version for testing purposes.
func SetBuildVersion(version string) {
buildVersion = version
info.Version = version
}