Skip to content

Commit 8ebdcd2

Browse files
Report an actionable error for unsupported bundle init template URLs (#5902)
## Changes `databricks bundle init <url>` with an unsupported protocol previously fell through to the local-path reader and failed with a confusing error: ``` $ databricks bundle init http://github.com/databricks/cli.git Error: not a bundle template: expected to find a template schema file at databricks_template_schema.json ``` Now the deprecated/insecure transports (`http://`, `git://`, `ftp://`, `ftps://`) are recognized as Git URLs but flagged invalid, so `ResolveReader` returns an actionable error naming the supported protocols: ``` Error: unsupported protocol in Git URL "http://github.com/databricks/cli.git": only https://, ssh://, and git@ URLs are supported ``` `ResolveReader` now returns an `error`; both callers (`Resolver.Resolve` and the `render-template-schema` debug command) propagate it. ## Why Follow-up to #5891. The fall-through behavior gave no hint that the protocol was the problem. ## Tests - Unit tests in `resolver_test.go` cover the invalid-protocol path in `ResolveReader`. - New acceptance test `bundle/templates-machinery/unsupported-url` asserts the CLI error. The error is raised at resolve time, before any network access, so no request mocking is needed. _This PR was written by Claude Code._
1 parent 0a6a4a8 commit 8ebdcd2

10 files changed

Lines changed: 134 additions & 28 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
`databricks bundle init` now reports an actionable error when given a template URL with an unsupported protocol (`http://`, `git://`, `ftp://`, `ftps://`) instead of failing with a confusing "not a bundle template" message ([#5902](https://github.com/databricks/cli/pull/5902)).

acceptance/bundle/templates-machinery/supported-url/out.test.toml

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
2+
=== https:// URL is recognized as a Git URL
3+
=== ssh:// URL is recognized as a Git URL
4+
=== git@ URL is recognized as a Git URL
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# These hosts do not resolve (.invalid is reserved by RFC 6761), so the clone
2+
# fails fast without network access. The point of the test is that these URLs
3+
# are recognized as Git URLs and a clone is attempted, rather than being misread
4+
# as local template paths (which would report a "not a bundle template" error
5+
# instead). The git error text varies by platform, so we route it to LOG
6+
# (excluded from the diff) and assert the invariant with contains.py.
7+
title "https:// URL is recognized as a Git URL"
8+
errcode $CLI bundle init https://nonexistent.databricks.invalid/databricks/cli &> LOG.https
9+
contains.py "git clone https://nonexistent.databricks.invalid/databricks/cli" "!not a bundle template" < LOG.https > /dev/null
10+
11+
title "ssh:// URL is recognized as a Git URL"
12+
errcode $CLI bundle init ssh://git@nonexistent.databricks.invalid/databricks/cli &> LOG.ssh
13+
contains.py "git clone ssh://git@nonexistent.databricks.invalid/databricks/cli" "!not a bundle template" < LOG.ssh > /dev/null
14+
15+
title "git@ URL is recognized as a Git URL"
16+
errcode $CLI bundle init git@nonexistent.databricks.invalid:databricks/cli.git &> LOG.git
17+
contains.py "git clone git@nonexistent.databricks.invalid:databricks/cli.git" "!not a bundle template" < LOG.git > /dev/null

acceptance/bundle/templates-machinery/unsupported-url/out.test.toml

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
2+
>>> musterr [CLI] bundle init http://github.com/databricks/cli
3+
Error: unsupported protocol in Git URL "http://github.com/databricks/cli": only https://, ssh://, git@ URLs are supported
4+
5+
>>> musterr [CLI] bundle init git://github.com/databricks/cli
6+
Error: unsupported protocol in Git URL "git://github.com/databricks/cli": only https://, ssh://, git@ URLs are supported
7+
8+
>>> musterr [CLI] bundle init ftp://github.com/databricks/cli
9+
Error: unsupported protocol in Git URL "ftp://github.com/databricks/cli": only https://, ssh://, git@ URLs are supported
10+
11+
>>> musterr [CLI] bundle init ftps://github.com/databricks/cli
12+
Error: unsupported protocol in Git URL "ftps://github.com/databricks/cli": only https://, ssh://, git@ URLs are supported
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
trace musterr $CLI bundle init http://github.com/databricks/cli
2+
trace musterr $CLI bundle init git://github.com/databricks/cli
3+
trace musterr $CLI bundle init ftp://github.com/databricks/cli
4+
trace musterr $CLI bundle init ftps://github.com/databricks/cli

cmd/bundle/debug/render_template_schema.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,10 @@ func NewRenderTemplateSchemaCommand() *cobra.Command {
4444
}
4545

4646
// Resolve the template reader
47-
reader, isGitReader := template.ResolveReader(templatePathOrUrl, templateDir, ref)
47+
reader, isGitReader, err := template.ResolveReader(templatePathOrUrl, templateDir, ref)
48+
if err != nil {
49+
return err
50+
}
4851
defer reader.Cleanup(ctx)
4952

5053
// For git reader, load schema first to initialize the temp directory

libs/template/resolver.go

Lines changed: 55 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,40 +3,75 @@ package template
33
import (
44
"context"
55
"errors"
6+
"fmt"
67
"strings"
78

89
"github.com/databricks/cli/libs/git"
910
)
1011

11-
// See https://git-scm.com/docs/git-clone#_git_urls for the set of supported
12-
// Git URL forms. We deliberately exclude deprecated/insecure protocols (git, http, ftp[s]).
13-
var gitUrlPrefixes = []string{
14-
"https://",
15-
"ssh://",
12+
type gitUrlPrefix struct {
13+
prefix string
14+
15+
// invalid marks a prefix that git recognizes as a URL but that we refuse to
16+
// clone from, so we can report an actionable error instead of falling through
17+
// to the local-path reader.
18+
invalid bool
19+
}
20+
21+
// See https://git-scm.com/docs/git-clone#_git_urls for the set of Git URL forms.
22+
// We deliberately reject the deprecated/insecure transports (git, http, ftp[s]).
23+
var gitUrlPrefixes = []gitUrlPrefix{
24+
{prefix: "https://"},
25+
{prefix: "ssh://"},
1626
// recognize git@ without ssh:// protocol because this is very common
17-
"git@",
27+
{prefix: "git@"},
28+
{prefix: "http://", invalid: true},
29+
{prefix: "git://", invalid: true},
30+
{prefix: "ftp://", invalid: true},
31+
{prefix: "ftps://", invalid: true},
1832
}
1933

20-
func IsGitRepoUrl(url string) bool {
21-
for _, prefix := range gitUrlPrefixes {
22-
if strings.HasPrefix(url, prefix) {
23-
return true
34+
// matchGitUrlPrefix returns the matching prefix entry, or nil if the input does
35+
// not look like a Git URL.
36+
func matchGitUrlPrefix(url string) *gitUrlPrefix {
37+
for i := range gitUrlPrefixes {
38+
if strings.HasPrefix(url, gitUrlPrefixes[i].prefix) {
39+
return &gitUrlPrefixes[i]
2440
}
2541
}
26-
return false
42+
return nil
43+
}
44+
45+
func IsGitRepoUrl(url string) bool {
46+
p := matchGitUrlPrefix(url)
47+
return p != nil && !p.invalid
2748
}
2849

2950
// ResolveReader resolves a template path/URL to a Reader (built-in, git or local)
30-
func ResolveReader(templatePathOrUrl, templateDir, ref string) (Reader, bool) {
51+
func ResolveReader(templatePathOrUrl, templateDir, ref string) (Reader, bool, error) {
3152
if tmpl := GetDatabricksTemplate(TemplateName(templatePathOrUrl)); tmpl != nil {
32-
return tmpl.Reader, false
53+
return tmpl.Reader, false, nil
3354
}
3455

35-
if IsGitRepoUrl(templatePathOrUrl) {
36-
return NewGitReader(templatePathOrUrl, ref, templateDir, git.Clone), true
56+
if p := matchGitUrlPrefix(templatePathOrUrl); p != nil {
57+
if p.invalid {
58+
return nil, false, fmt.Errorf("unsupported protocol in Git URL %q: only %s URLs are supported", templatePathOrUrl, strings.Join(supportedGitUrlPrefixes(), ", "))
59+
}
60+
return NewGitReader(templatePathOrUrl, ref, templateDir, git.Clone), true, nil
3761
}
3862

39-
return NewLocalReader(templatePathOrUrl), false
63+
return NewLocalReader(templatePathOrUrl), false, nil
64+
}
65+
66+
// supportedGitUrlPrefixes returns the valid (non-rejected) Git URL prefixes.
67+
func supportedGitUrlPrefixes() []string {
68+
var prefixes []string
69+
for i := range gitUrlPrefixes {
70+
if !gitUrlPrefixes[i].invalid {
71+
prefixes = append(prefixes, gitUrlPrefixes[i].prefix)
72+
}
73+
}
74+
return prefixes
4075
}
4176

4277
type Resolver struct {
@@ -107,7 +142,10 @@ func (r Resolver) Resolve(ctx context.Context) (*Template, error) {
107142
//
108143
// We resolve the appropriate reader according to the reference provided by the user.
109144
if tmpl == nil {
110-
reader, _ := ResolveReader(r.TemplatePathOrUrl, r.TemplateDir, ref)
145+
reader, _, err := ResolveReader(r.TemplatePathOrUrl, r.TemplateDir, ref)
146+
if err != nil {
147+
return nil, err
148+
}
111149
tmpl = &Template{
112150
name: Custom,
113151
Reader: reader,

libs/template/resolver_test.go

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -120,22 +120,43 @@ func TestBundleInitIsGitRepoUrl(t *testing.T) {
120120

121121
func TestResolveReader(t *testing.T) {
122122
t.Run("builtin template", func(t *testing.T) {
123-
reader, isGit := ResolveReader("default-python", "", "")
123+
reader, isGit, err := ResolveReader("default-python", "", "")
124+
require.NoError(t, err)
124125
assert.False(t, isGit)
125126
assert.Equal(t, &builtinReader{name: "default-python"}, reader)
126127
})
127128

128-
t.Run("git URL", func(t *testing.T) {
129-
reader, isGit := ResolveReader("https://github.com/example/repo", "/template", "v1.0")
130-
assert.True(t, isGit)
131-
gitReader := reader.(*gitReader)
132-
assert.Equal(t, "https://github.com/example/repo", gitReader.gitUrl)
133-
assert.Equal(t, "/template", gitReader.templateDir)
134-
assert.Equal(t, "v1.0", gitReader.ref)
135-
})
129+
for _, url := range []string{
130+
"https://github.com/example/repo",
131+
"ssh://git@github.com/example/repo",
132+
"git@github.com:example/repo",
133+
} {
134+
t.Run("git URL "+url, func(t *testing.T) {
135+
reader, isGit, err := ResolveReader(url, "/template", "v1.0")
136+
require.NoError(t, err)
137+
assert.True(t, isGit)
138+
gitReader := reader.(*gitReader)
139+
assert.Equal(t, url, gitReader.gitUrl)
140+
assert.Equal(t, "/template", gitReader.templateDir)
141+
assert.Equal(t, "v1.0", gitReader.ref)
142+
})
143+
}
144+
145+
for _, url := range []string{
146+
"http://github.com/example/repo",
147+
"git://github.com/example/repo",
148+
"ftp://github.com/example/repo",
149+
"ftps://github.com/example/repo",
150+
} {
151+
t.Run("unsupported protocol "+url, func(t *testing.T) {
152+
_, _, err := ResolveReader(url, "", "")
153+
assert.ErrorContains(t, err, "unsupported protocol")
154+
})
155+
}
136156

137157
t.Run("local path", func(t *testing.T) {
138-
reader, isGit := ResolveReader("/local/path", "", "")
158+
reader, isGit, err := ResolveReader("/local/path", "", "")
159+
require.NoError(t, err)
139160
assert.False(t, isGit)
140161
assert.Equal(t, "/local/path", reader.(*localReader).path)
141162
})

0 commit comments

Comments
 (0)