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
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
bundle:
name: catalog-empty-name-$UNIQUE_NAME

resources:
catalogs:
mycatalog:
name: ""

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

11 changes: 11 additions & 0 deletions acceptance/bundle/resources/catalogs/empty-name/output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@

>>> musterr [CLI] bundle deploy
Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/catalog-empty-name-[UNIQUE_NAME]/default/files...
Deploying resources...
Error: cannot create resources.catalogs.mycatalog: Invalid input: RPC CreateCatalog Field managedcatalog.CatalogInfo.name: name "" is not a valid name. Valid names cannot contain spaces, periods, forward slashes, or control characters. (400 INVALID_PARAMETER_VALUE)

Endpoint: POST [DATABRICKS_URL]/api/2.1/unity-catalog/catalogs
HTTP Status: 400 Bad Request
API error_code: INVALID_PARAMETER_VALUE
API message: Invalid input: RPC CreateCatalog Field managedcatalog.CatalogInfo.name: name "" is not a valid name. Valid names cannot contain spaces, periods, forward slashes, or control characters.

5 changes: 5 additions & 0 deletions acceptance/bundle/resources/catalogs/empty-name/script
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# The deploy fails only after the files are uploaded, so $UNIQUE_NAME in the bundle
# name keeps concurrent cloud legs apart and lets the sweeper find what is left.
envsubst < databricks.yml.tmpl > databricks.yml

trace musterr $CLI bundle deploy
10 changes: 10 additions & 0 deletions acceptance/bundle/resources/catalogs/empty-name/test.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
Local = true
# The golden asserts UC's message verbatim, so run on cloud to catch it drifting.
Cloud = true
RequiresUnityCatalog = true
RecordRequests = false
Ignore = [".databricks"]

# Terraform rejects catalog resources before any API call, so there is nothing to
# assert on that engine.
EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
bundle:
name: model-empty-name-$UNIQUE_NAME

resources:
models:
mymodel:
name: ""
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@

>>> musterr [CLI] bundle deploy
Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/model-empty-name-[UNIQUE_NAME]/default/files...
Deploying resources...
Error: cannot create resources.models.mymodel: Got an invalid name ''. Registered Model names cannot be empty strings. (400 INVALID_PARAMETER_VALUE)

Endpoint: POST [DATABRICKS_URL]/api/2.0/mlflow/registered-models/create
HTTP Status: 400 Bad Request
API error_code: INVALID_PARAMETER_VALUE
API message: Got an invalid name ''. Registered Model names cannot be empty strings.

Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@

>>> musterr [CLI] bundle deploy
Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/model-empty-name-[UNIQUE_NAME]/default/files...
Deploying resources...
Error: terraform apply: exit status 1

Error: cannot create mlflow model: Got an invalid name ''. Registered Model names cannot be empty strings.

with databricks_mlflow_model.mymodel,
on bundle.tf.json line 17, in resource.databricks_mlflow_model.mymodel:
17: }



Updating deployment state...
3 changes: 3 additions & 0 deletions acceptance/bundle/resources/models/empty-name/out.test.toml

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

Empty file.
7 changes: 7 additions & 0 deletions acceptance/bundle/resources/models/empty-name/script
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# The deploy fails only after the files are uploaded, so $UNIQUE_NAME in the bundle
# name keeps concurrent cloud legs apart and lets the sweeper find what is left.
envsubst < databricks.yml.tmpl > databricks.yml

# Both engines reach the create call, but terraform wraps the message in its own
# output, so the goldens are per-engine.
trace musterr $CLI bundle deploy &> out.deploy.$DATABRICKS_BUNDLE_ENGINE.txt
5 changes: 5 additions & 0 deletions acceptance/bundle/resources/models/empty-name/test.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Local = true
# The golden asserts MLflow's message verbatim, so run on cloud to catch it drifting.
Cloud = true
RecordRequests = false
Ignore = [".databricks"]
12 changes: 12 additions & 0 deletions libs/testserver/catalogs.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,18 @@ func (s *FakeWorkspace) CatalogsCreate(req Request) Response {
}
}

// UC rejects an empty name; the fake would otherwise store a catalog under a key
// nothing can look up. Message is UC's canned error, which names more than we check.
if createRequest.Name == "" {
return Response{
StatusCode: http.StatusBadRequest,
Body: map[string]string{
"error_code": "INVALID_PARAMETER_VALUE",
"message": `Invalid input: RPC CreateCatalog Field managedcatalog.CatalogInfo.name: name "" is not a valid name. Valid names cannot contain spaces, periods, forward slashes, or control characters.`,
},
}
}

// Echo back every field create accepts: a dropped one makes the next plan see a
// phantom change.
catalogInfo := catalog.CatalogInfo{
Expand Down
33 changes: 33 additions & 0 deletions libs/testserver/catalogs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,39 @@ import (
"github.com/stretchr/testify/require"
)

func TestCatalogsCreate_RejectsEmptyName(t *testing.T) {
workspace := NewFakeWorkspace("http://test", "dbapi123")

response := workspace.CatalogsCreate(Request{Body: []byte(`{"name": ""}`)})
assert.Equal(t, 400, response.StatusCode)

// A stored-but-unreadable catalog is the original bug. Asserted before the
// require below so it is still reported when the rejection is missing.
assert.Empty(t, workspace.Catalogs)

body, ok := response.Body.(map[string]string)
require.True(t, ok)
assert.Equal(t, "INVALID_PARAMETER_VALUE", body["error_code"])
assert.Contains(t, body["message"], "is not a valid name")
}

func TestCatalogsCreate_AllowsNonEmptyName(t *testing.T) {
workspace := NewFakeWorkspace("http://test", "dbapi123")

response := workspace.CatalogsCreate(Request{Body: []byte(`{"name": "my_catalog"}`)})
// StatusCode 0 gets converted to 200 by normalizeResponse in the server
require.Equal(t, 0, response.StatusCode)

// Read back through the same helper the GET route uses: a mis-keyed store must fail here.
getResponse := MapGet(workspace, workspace.Catalogs, "my_catalog")
require.Equal(t, 0, getResponse.StatusCode)

body, ok := getResponse.Body.(catalog.CatalogInfo)
require.True(t, ok)
assert.Equal(t, "my_catalog", body.Name)
assert.Equal(t, "my_catalog", body.FullName)
}

// createCatalogRequest sets every field CreateCatalog accepts. Tests below assert
// the fake echoes all of them back and that the request stays exhaustive.
const createCatalogRequest = `{
Expand Down
12 changes: 12 additions & 0 deletions libs/testserver/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,18 @@ func (s *FakeWorkspace) ModelRegistryCreateModel(req Request) any {
}
}

// MLflow rejects an empty name; the fake would otherwise store a model under a key
// nothing can look up.
if request.Name == "" {
return Response{
StatusCode: 400,
Body: map[string]string{
"error_code": "INVALID_PARAMETER_VALUE",
"message": "Got an invalid name ''. Registered Model names cannot be empty strings.",
},
}
}

// Create the model with a numeric ID (matching real API behavior)
modelId := strconv.FormatInt(nextID(), 10)
model := ml.Model{
Expand Down
48 changes: 48 additions & 0 deletions libs/testserver/models_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package testserver

import (
"net/url"
"testing"

"github.com/databricks/databricks-sdk-go/service/ml"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestModelRegistryCreateModel_RejectsEmptyName(t *testing.T) {
workspace := NewFakeWorkspace("http://test", "dbapi123")

response, ok := workspace.ModelRegistryCreateModel(Request{Body: []byte(`{"name": ""}`)}).(Response)
require.True(t, ok)
assert.Equal(t, 400, response.StatusCode)

// A stored-but-unreadable model is the original bug. Asserted before the
// require below so it is still reported when the rejection is missing.
assert.Empty(t, workspace.ModelRegistryModels)

body, ok := response.Body.(map[string]string)
require.True(t, ok)
assert.Equal(t, "INVALID_PARAMETER_VALUE", body["error_code"])
assert.Contains(t, body["message"], "cannot be empty strings")
}

func TestModelRegistryCreateModel_AllowsNonEmptyName(t *testing.T) {
workspace := NewFakeWorkspace("http://test", "dbapi123")

response, ok := workspace.ModelRegistryCreateModel(Request{Body: []byte(`{"name": "my_model"}`)}).(Response)
require.True(t, ok)
// StatusCode 0 gets converted to 200 by normalizeResponse in the server
require.Equal(t, 0, response.StatusCode)

// Read back through the GET handler: a mis-keyed store must fail here.
getResponse, ok := workspace.ModelRegistryGetModel(Request{
URL: &url.URL{RawQuery: "name=my_model"},
}).(Response)
require.True(t, ok)
require.Equal(t, 0, getResponse.StatusCode)

body, ok := getResponse.Body.(ml.GetModelResponse)
require.True(t, ok)
assert.Equal(t, "my_model", body.RegisteredModelDatabricks.Name)
assert.NotEmpty(t, body.RegisteredModelDatabricks.Id)
}
Loading