diff --git a/acceptance/localenv/constraints-only/output.txt b/acceptance/localenv/constraints-only/output.txt index 44f4f96f28..a85265f8cc 100644 --- a/acceptance/localenv/constraints-only/output.txt +++ b/acceptance/localenv/constraints-only/output.txt @@ -1,5 +1,5 @@ ->>> [CLI] environments setup-local --serverless-version v4 --constraints-only --dry-run --output json +>>> [CLI] environments setup-local --serverless-version 4 --constraints-only --dry-run --output json { "schemaVersion": 1, "command": "environments setup-local", diff --git a/acceptance/localenv/constraints-only/script b/acceptance/localenv/constraints-only/script index fc7a8d4201..b4f777e798 100644 --- a/acceptance/localenv/constraints-only/script +++ b/acceptance/localenv/constraints-only/script @@ -1 +1 @@ -trace $CLI environments setup-local --serverless-version v4 --constraints-only --dry-run --output json +trace $CLI environments setup-local --serverless-version 4 --constraints-only --dry-run --output json diff --git a/acceptance/localenv/env-unsupported/output.txt b/acceptance/localenv/env-unsupported/output.txt index c586b55337..c13d1a1d7d 100644 --- a/acceptance/localenv/env-unsupported/output.txt +++ b/acceptance/localenv/env-unsupported/output.txt @@ -1,6 +1,6 @@ preflight ok check resolve ok source=cluster envKey=dbr/15.4.x-scala2.12 -fetch error no published environment for "dbr/15.4.x-scala2.12". If this is a new runtime, try the latest LTS target (e.g. --serverless-version v4 or a supported --cluster-id DBR): GET [DATABRICKS_URL]/dbr/15.4.x-scala2.12/pyproject.toml: environment key not found +fetch error no published environment for "dbr/15.4.x-scala2.12". If this is a new runtime, try the latest LTS target (e.g. --serverless-version 5 or a supported --cluster-id DBR): GET [DATABRICKS_URL]/dbr/15.4.x-scala2.12/pyproject.toml: environment key not found merge pending provision pending validate pending diff --git a/acceptance/localenv/flag-conflict-json/script b/acceptance/localenv/flag-conflict-json/script index a41f74e8de..408aae874b 100644 --- a/acceptance/localenv/flag-conflict-json/script +++ b/acceptance/localenv/flag-conflict-json/script @@ -1 +1 @@ -musterr $CLI environments setup-local --cluster-id abc --serverless-version v4 --output json +musterr $CLI environments setup-local --cluster-id abc --serverless-version 4 --output json diff --git a/acceptance/localenv/flag-conflict/script b/acceptance/localenv/flag-conflict/script index 1a63e078c3..b251350e3c 100644 --- a/acceptance/localenv/flag-conflict/script +++ b/acceptance/localenv/flag-conflict/script @@ -1 +1 @@ -musterr $CLI environments setup-local --cluster-id abc --serverless-version v4 +musterr $CLI environments setup-local --cluster-id abc --serverless-version 4 diff --git a/acceptance/localenv/help/output.txt b/acceptance/localenv/help/output.txt index 2e0a6d1e2e..f96a2db80c 100644 --- a/acceptance/localenv/help/output.txt +++ b/acceptance/localenv/help/output.txt @@ -16,7 +16,7 @@ Flags: --dry-run compute the plan without writing files or provisioning -h, --help help for setup-local --job-id string job ID to use as the compute target - --serverless-version string serverless version to use as the compute target (e.g. v4) + --serverless-version string serverless version to use as the compute target (e.g. 5) Global Flags: --debug enable debug logging diff --git a/acceptance/localenv/manager-unsupported/script b/acceptance/localenv/manager-unsupported/script index 24b88530e4..f7a1e15df5 100644 --- a/acceptance/localenv/manager-unsupported/script +++ b/acceptance/localenv/manager-unsupported/script @@ -1 +1 @@ -musterr $CLI environments setup-local --serverless-version v4 --dry-run +musterr $CLI environments setup-local --serverless-version 4 --dry-run diff --git a/acceptance/localenv/serverless-check/output.txt b/acceptance/localenv/serverless-check/output.txt index 6df4933f44..2b36b789ac 100644 --- a/acceptance/localenv/serverless-check/output.txt +++ b/acceptance/localenv/serverless-check/output.txt @@ -1,5 +1,5 @@ ->>> [CLI] environments setup-local --serverless-version v4 --dry-run +>>> [CLI] environments setup-local --serverless-version 4 --dry-run preflight ok check resolve ok source=serverless envKey=serverless/serverless-v4 fetch ok source=[DATABRICKS_URL]/serverless/serverless-v4/pyproject.toml fromCache=false diff --git a/acceptance/localenv/serverless-check/script b/acceptance/localenv/serverless-check/script index 3c14eef868..c02eb92223 100644 --- a/acceptance/localenv/serverless-check/script +++ b/acceptance/localenv/serverless-check/script @@ -1 +1 @@ -trace $CLI environments setup-local --serverless-version v4 --dry-run +trace $CLI environments setup-local --serverless-version 4 --dry-run diff --git a/acceptance/localenv/serverless-json/output.txt b/acceptance/localenv/serverless-json/output.txt index cd9d3e2ad4..056a3fa34f 100644 --- a/acceptance/localenv/serverless-json/output.txt +++ b/acceptance/localenv/serverless-json/output.txt @@ -1,5 +1,5 @@ ->>> [CLI] environments setup-local --serverless-version v4 --dry-run --output json +>>> [CLI] environments setup-local --serverless-version 4 --dry-run --output json { "schemaVersion": 1, "command": "environments setup-local", diff --git a/acceptance/localenv/serverless-json/script b/acceptance/localenv/serverless-json/script index 56b8372bf4..0bda75a032 100644 --- a/acceptance/localenv/serverless-json/script +++ b/acceptance/localenv/serverless-json/script @@ -1 +1 @@ -trace $CLI environments setup-local --serverless-version v4 --dry-run --output json +trace $CLI environments setup-local --serverless-version 4 --dry-run --output json diff --git a/cmd/environments/sync.go b/cmd/environments/sync.go index fa2d450bcc..441e1be64b 100644 --- a/cmd/environments/sync.go +++ b/cmd/environments/sync.go @@ -49,7 +49,7 @@ env-owned sections are refreshed, user-owned content is preserved).`, func addTargetFlags(cmd *cobra.Command) { cmd.Flags().String("cluster-id", "", "cluster ID to use as the compute target") cmd.Flags().String("cluster-name", "", "cluster name to use as the compute target (resolved to an ID via the Clusters API)") - cmd.Flags().String("serverless-version", "", "serverless version to use as the compute target (e.g. v4)") + cmd.Flags().String("serverless-version", "", "serverless version to use as the compute target (e.g. 5)") cmd.Flags().String("job-id", "", "job ID to use as the compute target") cmd.Flags().Bool("constraints-only", false, "apply the Python version and constraints without adding the databricks-connect dependency") cmd.Flags().Bool("dry-run", false, "compute the plan without writing files or provisioning") diff --git a/libs/localenv/constraints.go b/libs/localenv/constraints.go index 6f381cc155..b048d6ef85 100644 --- a/libs/localenv/constraints.go +++ b/libs/localenv/constraints.go @@ -184,7 +184,7 @@ func FetchConstraints(ctx context.Context, baseURL, envKey, cacheDir string, wri // fallback: the target resolved to an environment that isn't published. if errors.Is(fetchErr, errEnvKeyNotFound) { return nil, NewError(ErrEnvUnsupported, fetchErr, - "no published environment for %q. If this is a new runtime, try the latest LTS target (e.g. --serverless-version v4 or a supported --cluster-id DBR)", envKey) + "no published environment for %q. If this is a new runtime, try the latest LTS target (e.g. --serverless-version 5 or a supported --cluster-id DBR)", envKey) } // Network or HTTP failure: attempt to serve from cache. diff --git a/libs/localenv/envkey.go b/libs/localenv/envkey.go index 23dc0f9ca5..4e947b0c71 100644 --- a/libs/localenv/envkey.go +++ b/libs/localenv/envkey.go @@ -20,10 +20,24 @@ var clauseRe = regexp.MustCompile(`^(>=|<=|===|==|~=|!=|<|>)?\s*(\d+)\.(\d+)(\.\ // latest LTS (spec §4.3 / §target-resolution); VS Code resolves the real version // itself and passes --serverless-version explicitly, so this fallback only // applies when the version is genuinely unknown. -const defaultServerlessVersion = "v5" +const defaultServerlessVersion = "5" + +// serverlessVersionRe matches an accepted --serverless-version input: a bare +// number ("5", the documented form) or a "v"-prefixed one ("v5"/"V5"). +var serverlessVersionRe = regexp.MustCompile(`^[vV]?[0-9]+$`) + +// ValidServerlessVersion reports whether s is an accepted serverless version +// input. It is validated at resolve time so a malformed value (e.g. "vv5", "v", +// " 5") fails fast with an actionable error rather than resolving to a bogus +// env key that only 404s two phases later at fetch. +func ValidServerlessVersion(s string) bool { + return serverlessVersionRe.MatchString(s) +} // NormalizeServerless returns the canonical "vN" spelling of a serverless -// version accepting "4", "v4", or "V4". +// version. The documented input is a bare number ("5"), but a "v"-prefixed form +// ("v5"/"V5") is also accepted; both map to the "serverless-vN" env key. +// Callers should validate with ValidServerlessVersion first. func NormalizeServerless(version string) string { return "v" + strings.TrimPrefix(strings.ToLower(version), "v") } diff --git a/libs/localenv/envkey_test.go b/libs/localenv/envkey_test.go index 898e93434b..c502cac57b 100644 --- a/libs/localenv/envkey_test.go +++ b/libs/localenv/envkey_test.go @@ -8,9 +8,30 @@ import ( ) func TestEnvKeyForServerless(t *testing.T) { + // The documented input is a bare number; a "v"-prefixed form is also accepted. + // Both map to the "serverless-vN" env key. for _, in := range []string{"4", "v4", "V4"} { assert.Equal(t, "serverless/serverless-v4", EnvKeyForServerless(in)) } + for _, in := range []string{"5", "v5", "V5"} { + assert.Equal(t, "serverless/serverless-v5", EnvKeyForServerless(in)) + } +} + +func TestDefaultServerlessVersionIsBareNumber(t *testing.T) { + // The default stand-in is stored in the documented bare form; it still maps + // to the serverless-vN env key via NormalizeServerless. + assert.Equal(t, "5", defaultServerlessVersion) + assert.Equal(t, "serverless/serverless-v5", EnvKeyForServerless(defaultServerlessVersion)) +} + +func TestValidServerlessVersion(t *testing.T) { + for _, ok := range []string{"5", "4", "v5", "V5", "17", "0"} { + assert.True(t, ValidServerlessVersion(ok), "%q should be valid", ok) + } + for _, bad := range []string{"", "v", "vv5", "5x", "latest", " 5", "5 ", "v5.1", "-5"} { + assert.False(t, ValidServerlessVersion(bad), "%q should be invalid", bad) + } } func TestEnvKeyForSparkVersion(t *testing.T) { diff --git a/libs/localenv/target.go b/libs/localenv/target.go index 814f6d722b..119bc7a053 100644 --- a/libs/localenv/target.go +++ b/libs/localenv/target.go @@ -104,6 +104,9 @@ func ResolveTarget(ctx context.Context, f TargetFlags, c ComputeClient, bt Bundl } if f.Serverless != "" { + if !ValidServerlessVersion(f.Serverless) { + return nil, NewError(ErrResolve, nil, "invalid --serverless-version %q: expected a version number like 5", f.Serverless) + } return &TargetInfo{ Source: "serverless", ServerlessVersion: NormalizeServerless(f.Serverless), diff --git a/libs/localenv/target_test.go b/libs/localenv/target_test.go index 6438921a64..a202cac683 100644 --- a/libs/localenv/target_test.go +++ b/libs/localenv/target_test.go @@ -37,6 +37,24 @@ func TestResolveServerlessFlag(t *testing.T) { assert.Equal(t, "serverless/serverless-v4", ti.EnvKey) } +func TestResolveServerlessFlagBareNumber(t *testing.T) { + // The documented input is a bare number; it normalizes to the vN env key. + ti, err := ResolveTarget(t.Context(), TargetFlags{Serverless: "5"}, stubCompute{}, BundleTarget{}) + require.NoError(t, err) + assert.Equal(t, "serverless/serverless-v5", ti.EnvKey) +} + +func TestResolveServerlessFlagRejectsMalformed(t *testing.T) { + // Malformed values fail fast at resolve (E_RESOLVE) rather than resolving to + // a bogus env key that only 404s at fetch. + for _, bad := range []string{"vv5", "v", " 5", "5x", "latest"} { + _, err := ResolveTarget(t.Context(), TargetFlags{Serverless: bad}, stubCompute{}, BundleTarget{}) + var pe *PipelineError + require.ErrorAs(t, err, &pe, "input %q should error", bad) + assert.Equal(t, ErrResolve, pe.Code, "input %q", bad) + } +} + func TestResolveClusterFlag(t *testing.T) { c := stubCompute{clusterVersion: "15.4.x-scala2.12"} ti, err := ResolveTarget(t.Context(), TargetFlags{Cluster: "abc"}, c, BundleTarget{})