Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
4 changes: 4 additions & 0 deletions NOTICE
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ ghodss/yaml - https://github.com/ghodss/yaml
Copyright (c) 2014 Sam Ghods
License - https://github.com/ghodss/yaml/blob/master/LICENSE

Masterminds/semver - https://github.com/Masterminds/semver
Copyright (C) 2014-2019, Matt Butcher and Matt Farina
License - https://github.com/Masterminds/semver/blob/master/LICENSE.txt

mattn/go-isatty - https://github.com/mattn/go-isatty
Copyright (c) Yasuhiro MATSUMOTO <mattn.jp@gmail.com>
https://github.com/mattn/go-isatty/blob/master/LICENSE
Expand Down
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 {
// We intentionally only support the format supported by requirements.txt:
// 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))
}
174 changes: 174 additions & 0 deletions bundle/config/mutator/verify_cli_version_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
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) {
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.*",
},
{
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() {
// Reset the build version to the default version
// so that it doesn't affect other tests
// It doesn't really matter what we configure this to when testing
// as long as it is a valid semver version.
build.SetBuildVersion(build.DefaultSemver)
})

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)
}
})
}
}
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/databricks/cli
go 1.21

require (
github.com/Masterminds/semver/v3 v3.2.1 // MIT
github.com/briandowns/spinner v1.23.0 // Apache 2.0
github.com/databricks/databricks-sdk-go v0.36.0 // Apache 2.0
github.com/fatih/color v1.16.0 // MIT
Expand All @@ -27,10 +28,9 @@ require (
golang.org/x/term v0.18.0
golang.org/x/text v0.14.0
gopkg.in/ini.v1 v1.67.0 // Apache 2.0
gopkg.in/yaml.v3 v3.0.1
)

require gopkg.in/yaml.v3 v3.0.1

require (
cloud.google.com/go/compute v1.23.4 // indirect
cloud.google.com/go/compute/metadata v0.2.3 // 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
}