From 1b4b9884338999dc318b672ac3ec8a3d6210e0d0 Mon Sep 17 00:00:00 2001 From: Alex Khakhlyuk Date: Thu, 27 Aug 2026 14:51:47 +0000 Subject: [PATCH 1/2] Add direct-mode bundle support for AI Gateway securables Add three Unity Catalog AI Gateway securables as Databricks Asset Bundle resources on the direct deployment engine: model_service, mcp_service, and model_provider_service. Each resource follows the existing direct-engine pattern (cf. volume / database_instance): - bundle/config/resources/.go: a flat state struct exposing the immutable identity (parent + _id, which the server composes into the resource name) plus the mutable comment/config body. The SDK models the identity inputs as URL params, so the create request cannot be embedded directly. - bundle/direct/dresources/.go: CRUD mapped 1:1 onto the SDK AiGateway service (Create/Get/Update/Delete). The engine tracks the bare {catalog}.{schema}.{leaf} id and re-adds the resource name prefix at each call; DoRead reconstructs the identity from the server-returned name so it participates in drift detection. resources.yml classification: parent + _id are provided id fields (immutable, recreate on change); model_provider_service.config.provider_type is immutable (recreate); output-only config fields (inference_table.*, mcp source_connection.is_deleted) and the write-only provider secrets (*.plaintext, never returned by GET) are suppressed to avoid phantom drift. Wires up registration, the resource test server, the generated schema, and the shared "all resources" coverage tests. Builds and the dresources/config/testserver unit tests pass. Follow-up (not in this commit): acceptance tests, invariant configs, bind/unbind tests, and live-cloud validation (including confirming the model_provider_service update mask is accepted over the immutable provider_type field). Co-authored-by: Isaac --- acceptance/bundle/refschema/out.fields.txt | 175 +++ .../apply_bundle_permissions_test.go | 3 + .../resourcemutator/apply_target_mode_test.go | 9 + .../mutator/resourcemutator/run_as_test.go | 6 + bundle/config/resources.go | 9 + bundle/config/resources/mcp_service.go | 84 + .../resources/model_provider_service.go | 92 ++ bundle/config/resources/model_service.go | 91 ++ bundle/config/resources_test.go | 23 + bundle/direct/dresources/all.go | 3 + bundle/direct/dresources/all_test.go | 24 + .../direct/dresources/apitypes.generated.yml | 6 + bundle/direct/dresources/mcp_service.go | 113 ++ .../dresources/model_provider_service.go | 114 ++ bundle/direct/dresources/model_service.go | 118 ++ .../direct/dresources/resources.generated.yml | 6 + bundle/direct/dresources/resources.yml | 82 + bundle/internal/schema/annotations.yml | 63 + bundle/schema/jsonschema.json | 1351 +++++++++++++++-- libs/testserver/fake_workspace.go | 40 +- libs/testserver/handlers.go | 54 + libs/testserver/mcp_services.go | 69 + libs/testserver/model_provider_services.go | 70 + libs/testserver/model_services.go | 76 + 24 files changed, 2576 insertions(+), 105 deletions(-) create mode 100644 bundle/config/resources/mcp_service.go create mode 100644 bundle/config/resources/model_provider_service.go create mode 100644 bundle/config/resources/model_service.go create mode 100644 bundle/direct/dresources/mcp_service.go create mode 100644 bundle/direct/dresources/model_provider_service.go create mode 100644 bundle/direct/dresources/model_service.go create mode 100644 libs/testserver/mcp_services.go create mode 100644 libs/testserver/model_provider_services.go create mode 100644 libs/testserver/model_services.go diff --git a/acceptance/bundle/refschema/out.fields.txt b/acceptance/bundle/refschema/out.fields.txt index de252dc416..a136147260 100644 --- a/acceptance/bundle/refschema/out.fields.txt +++ b/acceptance/bundle/refschema/out.fields.txt @@ -2043,6 +2043,181 @@ resources.jobs.*.permissions[*].group_name string ALL resources.jobs.*.permissions[*].level iam.PermissionLevel ALL resources.jobs.*.permissions[*].service_principal_name string ALL resources.jobs.*.permissions[*].user_name string ALL +resources.mcp_services.*.comment string ALL +resources.mcp_services.*.config *catalog.McpServiceConfig ALL +resources.mcp_services.*.config.include_tool_selectors []string ALL +resources.mcp_services.*.config.include_tool_selectors[*] string ALL +resources.mcp_services.*.config.rate_limits []catalog.RateLimit ALL +resources.mcp_services.*.config.rate_limits[*] catalog.RateLimit ALL +resources.mcp_services.*.config.rate_limits[*].key catalog.RateLimitRateLimitKey ALL +resources.mcp_services.*.config.rate_limits[*].principal string ALL +resources.mcp_services.*.config.rate_limits[*].renewal_period catalog.RateLimitRateLimitRenewalPeriod ALL +resources.mcp_services.*.config.rate_limits[*].request_tag_key string ALL +resources.mcp_services.*.config.rate_limits[*].request_tag_value string ALL +resources.mcp_services.*.config.rate_limits[*].requests int64 ALL +resources.mcp_services.*.config.rate_limits[*].tokens int64 ALL +resources.mcp_services.*.config.source_connection *catalog.McpServiceConfigSourceConnection ALL +resources.mcp_services.*.config.source_connection.is_deleted bool ALL +resources.mcp_services.*.config.source_connection.name string ALL +resources.mcp_services.*.id string INPUT +resources.mcp_services.*.lifecycle resources.Lifecycle INPUT +resources.mcp_services.*.lifecycle.prevent_destroy bool INPUT +resources.mcp_services.*.mcp_service_id string ALL +resources.mcp_services.*.modified_status string INPUT +resources.mcp_services.*.parent string ALL +resources.mcp_services.*.url string INPUT +resources.model_provider_services.*.comment string ALL +resources.model_provider_services.*.config *catalog.ModelProviderServiceConfig ALL +resources.model_provider_services.*.config.allow_all_targets bool ALL +resources.model_provider_services.*.config.amazon_bedrock *catalog.ModelProviderServiceConfigAmazonBedrockProviderConfig ALL +resources.model_provider_services.*.config.amazon_bedrock.direct *catalog.ModelProviderServiceConfigAmazonBedrockProviderDirectConfig ALL +resources.model_provider_services.*.config.amazon_bedrock.direct.aws_access_key *catalog.ModelProviderServiceConfigAwsAccessKey ALL +resources.model_provider_services.*.config.amazon_bedrock.direct.aws_access_key.access_key_id string ALL +resources.model_provider_services.*.config.amazon_bedrock.direct.aws_access_key.secret_access_key *catalog.ModelProviderServiceConfigProviderSecret ALL +resources.model_provider_services.*.config.amazon_bedrock.direct.aws_access_key.secret_access_key.plaintext string ALL +resources.model_provider_services.*.config.amazon_bedrock.direct.region string ALL +resources.model_provider_services.*.config.amazon_bedrock.direct.service_credential *catalog.ModelProviderServiceConfigServiceCredential ALL +resources.model_provider_services.*.config.amazon_bedrock.direct.service_credential.name string ALL +resources.model_provider_services.*.config.anthropic *catalog.ModelProviderServiceConfigAnthropicProviderConfig ALL +resources.model_provider_services.*.config.anthropic.direct *catalog.ModelProviderServiceConfigAnthropicProviderDirectConfig ALL +resources.model_provider_services.*.config.anthropic.direct.api_key *catalog.ModelProviderServiceConfigProviderSecret ALL +resources.model_provider_services.*.config.anthropic.direct.api_key.plaintext string ALL +resources.model_provider_services.*.config.anthropic.relayed *catalog.ModelProviderServiceConfigAnthropicProviderRelayedConfig ALL +resources.model_provider_services.*.config.anthropic.relayed.plan_type catalog.ModelProviderServiceConfigAnthropicProviderRelayedConfigAnthropicRelayedPlanType ALL +resources.model_provider_services.*.config.azure_openai *catalog.ModelProviderServiceConfigAzureOpenAiProviderConfig ALL +resources.model_provider_services.*.config.azure_openai.direct *catalog.ModelProviderServiceConfigAzureOpenAiProviderDirectConfig ALL +resources.model_provider_services.*.config.azure_openai.direct.api_key *catalog.ModelProviderServiceConfigProviderSecret ALL +resources.model_provider_services.*.config.azure_openai.direct.api_key.plaintext string ALL +resources.model_provider_services.*.config.azure_openai.direct.base_url string ALL +resources.model_provider_services.*.config.azure_openai.direct.entra_service_principal *catalog.ModelProviderServiceConfigEntraServicePrincipal ALL +resources.model_provider_services.*.config.azure_openai.direct.entra_service_principal.client_id string ALL +resources.model_provider_services.*.config.azure_openai.direct.entra_service_principal.client_secret *catalog.ModelProviderServiceConfigProviderSecret ALL +resources.model_provider_services.*.config.azure_openai.direct.entra_service_principal.client_secret.plaintext string ALL +resources.model_provider_services.*.config.azure_openai.direct.entra_service_principal.tenant_id string ALL +resources.model_provider_services.*.config.azure_openai.direct.service_credential *catalog.ModelProviderServiceConfigServiceCredential ALL +resources.model_provider_services.*.config.azure_openai.direct.service_credential.name string ALL +resources.model_provider_services.*.config.custom *catalog.ModelProviderServiceConfigCustomProviderConfig ALL +resources.model_provider_services.*.config.custom.direct *catalog.ModelProviderServiceConfigCustomProviderDirectConfig ALL +resources.model_provider_services.*.config.custom.direct.api_key *catalog.ModelProviderServiceConfigProviderSecret ALL +resources.model_provider_services.*.config.custom.direct.api_key.plaintext string ALL +resources.model_provider_services.*.config.custom.direct.base_url string ALL +resources.model_provider_services.*.config.forward_headers bool ALL +resources.model_provider_services.*.config.forward_query_parameters bool ALL +resources.model_provider_services.*.config.forward_unmanaged_paths bool ALL +resources.model_provider_services.*.config.gemini_enterprise *catalog.ModelProviderServiceConfigGeminiEnterpriseProviderConfig ALL +resources.model_provider_services.*.config.gemini_enterprise.direct *catalog.ModelProviderServiceConfigGeminiEnterpriseProviderDirectConfig ALL +resources.model_provider_services.*.config.gemini_enterprise.direct.api_key *catalog.ModelProviderServiceConfigProviderSecret ALL +resources.model_provider_services.*.config.gemini_enterprise.direct.api_key.plaintext string ALL +resources.model_provider_services.*.config.gemini_enterprise.direct.project_id string ALL +resources.model_provider_services.*.config.gemini_enterprise.direct.region string ALL +resources.model_provider_services.*.config.inference_table *catalog.InferenceTableConfig ALL +resources.model_provider_services.*.config.inference_table.disabled bool ALL +resources.model_provider_services.*.config.inference_table.is_deleted bool ALL +resources.model_provider_services.*.config.inference_table.parent string ALL +resources.model_provider_services.*.config.inference_table.table string ALL +resources.model_provider_services.*.config.inference_table.table_name_prefix string ALL +resources.model_provider_services.*.config.microsoft_foundry *catalog.ModelProviderServiceConfigMicrosoftFoundryProviderConfig ALL +resources.model_provider_services.*.config.microsoft_foundry.direct *catalog.ModelProviderServiceConfigMicrosoftFoundryProviderDirectConfig ALL +resources.model_provider_services.*.config.microsoft_foundry.direct.api_key *catalog.ModelProviderServiceConfigProviderSecret ALL +resources.model_provider_services.*.config.microsoft_foundry.direct.api_key.plaintext string ALL +resources.model_provider_services.*.config.microsoft_foundry.direct.base_url string ALL +resources.model_provider_services.*.config.microsoft_foundry.direct.entra_service_principal *catalog.ModelProviderServiceConfigEntraServicePrincipal ALL +resources.model_provider_services.*.config.microsoft_foundry.direct.entra_service_principal.client_id string ALL +resources.model_provider_services.*.config.microsoft_foundry.direct.entra_service_principal.client_secret *catalog.ModelProviderServiceConfigProviderSecret ALL +resources.model_provider_services.*.config.microsoft_foundry.direct.entra_service_principal.client_secret.plaintext string ALL +resources.model_provider_services.*.config.microsoft_foundry.direct.entra_service_principal.tenant_id string ALL +resources.model_provider_services.*.config.microsoft_foundry.direct.service_credential *catalog.ModelProviderServiceConfigServiceCredential ALL +resources.model_provider_services.*.config.microsoft_foundry.direct.service_credential.name string ALL +resources.model_provider_services.*.config.openai *catalog.ModelProviderServiceConfigOpenAiProviderConfig ALL +resources.model_provider_services.*.config.openai.direct *catalog.ModelProviderServiceConfigOpenAiProviderDirectConfig ALL +resources.model_provider_services.*.config.openai.direct.api_key *catalog.ModelProviderServiceConfigProviderSecret ALL +resources.model_provider_services.*.config.openai.direct.api_key.plaintext string ALL +resources.model_provider_services.*.config.openai.direct.base_url string ALL +resources.model_provider_services.*.config.openai.direct.organization string ALL +resources.model_provider_services.*.config.provider_type catalog.ModelProviderServiceConfigExternalModelProviderType ALL +resources.model_provider_services.*.config.rate_limits []catalog.RateLimit ALL +resources.model_provider_services.*.config.rate_limits[*] catalog.RateLimit ALL +resources.model_provider_services.*.config.rate_limits[*].key catalog.RateLimitRateLimitKey ALL +resources.model_provider_services.*.config.rate_limits[*].principal string ALL +resources.model_provider_services.*.config.rate_limits[*].renewal_period catalog.RateLimitRateLimitRenewalPeriod ALL +resources.model_provider_services.*.config.rate_limits[*].request_tag_key string ALL +resources.model_provider_services.*.config.rate_limits[*].request_tag_value string ALL +resources.model_provider_services.*.config.rate_limits[*].requests int64 ALL +resources.model_provider_services.*.config.rate_limits[*].tokens int64 ALL +resources.model_provider_services.*.config.targets []catalog.ModelProviderServiceConfigModelTargetConfig ALL +resources.model_provider_services.*.config.targets[*] catalog.ModelProviderServiceConfigModelTargetConfig ALL +resources.model_provider_services.*.config.targets[*].model string ALL +resources.model_provider_services.*.config.targets[*].native_api_types []string ALL +resources.model_provider_services.*.config.targets[*].native_api_types[*] string ALL +resources.model_provider_services.*.id string INPUT +resources.model_provider_services.*.lifecycle resources.Lifecycle INPUT +resources.model_provider_services.*.lifecycle.prevent_destroy bool INPUT +resources.model_provider_services.*.model_provider_service_id string ALL +resources.model_provider_services.*.modified_status string INPUT +resources.model_provider_services.*.parent string ALL +resources.model_provider_services.*.url string INPUT +resources.model_services.*.comment string ALL +resources.model_services.*.config *catalog.ModelServiceConfig ALL +resources.model_services.*.config.inference_table *catalog.InferenceTableConfig ALL +resources.model_services.*.config.inference_table.disabled bool ALL +resources.model_services.*.config.inference_table.is_deleted bool ALL +resources.model_services.*.config.inference_table.parent string ALL +resources.model_services.*.config.inference_table.table string ALL +resources.model_services.*.config.inference_table.table_name_prefix string ALL +resources.model_services.*.config.rate_limits []catalog.RateLimit ALL +resources.model_services.*.config.rate_limits[*] catalog.RateLimit ALL +resources.model_services.*.config.rate_limits[*].key catalog.RateLimitRateLimitKey ALL +resources.model_services.*.config.rate_limits[*].principal string ALL +resources.model_services.*.config.rate_limits[*].renewal_period catalog.RateLimitRateLimitRenewalPeriod ALL +resources.model_services.*.config.rate_limits[*].request_tag_key string ALL +resources.model_services.*.config.rate_limits[*].request_tag_value string ALL +resources.model_services.*.config.rate_limits[*].requests int64 ALL +resources.model_services.*.config.rate_limits[*].tokens int64 ALL +resources.model_services.*.config.routing *catalog.ModelServiceConfigRoutingConfig ALL +resources.model_services.*.config.routing.destinations []catalog.ModelServiceConfigDestinationConfig ALL +resources.model_services.*.config.routing.destinations[*] catalog.ModelServiceConfigDestinationConfig ALL +resources.model_services.*.config.routing.destinations[*].destination_type catalog.ModelServiceConfigDestinationConfigDestinationType ALL +resources.model_services.*.config.routing.destinations[*].external_model_config *catalog.ModelServiceConfigExternalModelConfig ALL +resources.model_services.*.config.routing.destinations[*].external_model_config.model_provider_service string ALL +resources.model_services.*.config.routing.destinations[*].external_model_config.target catalog.ModelProviderServiceConfigModelTargetConfig ALL +resources.model_services.*.config.routing.destinations[*].external_model_config.target.model string ALL +resources.model_services.*.config.routing.destinations[*].external_model_config.target.native_api_types []string ALL +resources.model_services.*.config.routing.destinations[*].external_model_config.target.native_api_types[*] string ALL +resources.model_services.*.config.routing.destinations[*].is_deleted bool ALL +resources.model_services.*.config.routing.destinations[*].name string ALL +resources.model_services.*.config.routing.destinations[*].pay_per_token_config *catalog.ModelServiceConfigPayPerTokenConfig ALL +resources.model_services.*.config.routing.destinations[*].pay_per_token_config.model string ALL +resources.model_services.*.config.routing.destinations[*].provisioned_throughput_config *catalog.ModelServiceConfigProvisionedThroughputConfig ALL +resources.model_services.*.config.routing.destinations[*].provisioned_throughput_config.model string ALL +resources.model_services.*.config.routing.destinations[*].provisioned_throughput_config.model_serving_endpoint string ALL +resources.model_services.*.config.routing.destinations[*].traffic_percentage int ALL +resources.model_services.*.config.routing.fallback *catalog.ModelServiceConfigFallbackConfig ALL +resources.model_services.*.config.routing.fallback.destinations []catalog.ModelServiceConfigDestinationConfig ALL +resources.model_services.*.config.routing.fallback.destinations[*] catalog.ModelServiceConfigDestinationConfig ALL +resources.model_services.*.config.routing.fallback.destinations[*].destination_type catalog.ModelServiceConfigDestinationConfigDestinationType ALL +resources.model_services.*.config.routing.fallback.destinations[*].external_model_config *catalog.ModelServiceConfigExternalModelConfig ALL +resources.model_services.*.config.routing.fallback.destinations[*].external_model_config.model_provider_service string ALL +resources.model_services.*.config.routing.fallback.destinations[*].external_model_config.target catalog.ModelProviderServiceConfigModelTargetConfig ALL +resources.model_services.*.config.routing.fallback.destinations[*].external_model_config.target.model string ALL +resources.model_services.*.config.routing.fallback.destinations[*].external_model_config.target.native_api_types []string ALL +resources.model_services.*.config.routing.fallback.destinations[*].external_model_config.target.native_api_types[*] string ALL +resources.model_services.*.config.routing.fallback.destinations[*].is_deleted bool ALL +resources.model_services.*.config.routing.fallback.destinations[*].name string ALL +resources.model_services.*.config.routing.fallback.destinations[*].pay_per_token_config *catalog.ModelServiceConfigPayPerTokenConfig ALL +resources.model_services.*.config.routing.fallback.destinations[*].pay_per_token_config.model string ALL +resources.model_services.*.config.routing.fallback.destinations[*].provisioned_throughput_config *catalog.ModelServiceConfigProvisionedThroughputConfig ALL +resources.model_services.*.config.routing.fallback.destinations[*].provisioned_throughput_config.model string ALL +resources.model_services.*.config.routing.fallback.destinations[*].provisioned_throughput_config.model_serving_endpoint string ALL +resources.model_services.*.config.routing.fallback.destinations[*].traffic_percentage int ALL +resources.model_services.*.config.routing.first_token_timeout *duration.Duration ALL +resources.model_services.*.config.routing.traffic_splitting *catalog.ModelServiceConfigRoutingConfigTrafficSplitting ALL +resources.model_services.*.id string INPUT +resources.model_services.*.lifecycle resources.Lifecycle INPUT +resources.model_services.*.lifecycle.prevent_destroy bool INPUT +resources.model_services.*.model_service_id string ALL +resources.model_services.*.modified_status string INPUT +resources.model_services.*.parent string ALL +resources.model_services.*.url string INPUT resources.model_serving_endpoints.*.ai_gateway *serving.AiGatewayConfig ALL resources.model_serving_endpoints.*.ai_gateway.fallback_config *serving.FallbackConfig ALL resources.model_serving_endpoints.*.ai_gateway.fallback_config.enabled bool ALL diff --git a/bundle/config/mutator/resourcemutator/apply_bundle_permissions_test.go b/bundle/config/mutator/resourcemutator/apply_bundle_permissions_test.go index 99ac6759ee..a727a77daa 100644 --- a/bundle/config/mutator/resourcemutator/apply_bundle_permissions_test.go +++ b/bundle/config/mutator/resourcemutator/apply_bundle_permissions_test.go @@ -25,6 +25,9 @@ var unsupportedResources = []string{ "schemas", "quality_monitors", "registered_models", + "model_services", + "mcp_services", + "model_provider_services", "database_catalogs", "synced_database_tables", "postgres_branches", diff --git a/bundle/config/mutator/resourcemutator/apply_target_mode_test.go b/bundle/config/mutator/resourcemutator/apply_target_mode_test.go index 35eed05cc5..7977c971d5 100644 --- a/bundle/config/mutator/resourcemutator/apply_target_mode_test.go +++ b/bundle/config/mutator/resourcemutator/apply_target_mode_test.go @@ -108,6 +108,15 @@ func mockBundle(mode config.Mode) *bundle.Bundle { ModelServingEndpoints: map[string]*resources.ModelServingEndpoint{ "servingendpoint1": {CreateServingEndpoint: serving.CreateServingEndpoint{Name: "servingendpoint1"}}, }, + ModelServices: map[string]*resources.ModelService{ + "modelservice1": {ModelServiceConfig: resources.ModelServiceConfig{Parent: "schemas/main.default", ModelServiceId: "modelservice1"}}, + }, + McpServices: map[string]*resources.McpService{ + "mcpservice1": {McpServiceConfig: resources.McpServiceConfig{Parent: "schemas/main.default", McpServiceId: "mcpservice1"}}, + }, + ModelProviderServices: map[string]*resources.ModelProviderService{ + "modelproviderservice1": {ModelProviderServiceConfig: resources.ModelProviderServiceConfig{Parent: "schemas/main.default", ModelProviderServiceId: "modelproviderservice1"}}, + }, RegisteredModels: map[string]*resources.RegisteredModel{ "registeredmodel1": {CreateRegisteredModelRequest: catalog.CreateRegisteredModelRequest{Name: "registeredmodel1"}}, }, diff --git a/bundle/config/mutator/resourcemutator/run_as_test.go b/bundle/config/mutator/resourcemutator/run_as_test.go index be9abfe683..c47aca401a 100644 --- a/bundle/config/mutator/resourcemutator/run_as_test.go +++ b/bundle/config/mutator/resourcemutator/run_as_test.go @@ -47,6 +47,9 @@ func allResourceTypes(t *testing.T) []string { "instance_pools", "job_runs", "jobs", + "mcp_services", + "model_provider_services", + "model_services", "model_serving_endpoints", "models", "pipelines", @@ -185,6 +188,9 @@ var allowList = []string{ "jobs", "pipelines", "models", + "model_services", + "mcp_services", + "model_provider_services", "postgres_branches", "postgres_catalogs", "postgres_databases", diff --git a/bundle/config/resources.go b/bundle/config/resources.go index 633332a78c..ba08ff8ae0 100644 --- a/bundle/config/resources.go +++ b/bundle/config/resources.go @@ -18,6 +18,9 @@ type Resources struct { Models map[string]*resources.MlflowModel `json:"models,omitempty"` Experiments map[string]*resources.MlflowExperiment `json:"experiments,omitempty"` ModelServingEndpoints map[string]*resources.ModelServingEndpoint `json:"model_serving_endpoints,omitempty"` + ModelServices map[string]*resources.ModelService `json:"model_services,omitempty"` + McpServices map[string]*resources.McpService `json:"mcp_services,omitempty"` + ModelProviderServices map[string]*resources.ModelProviderService `json:"model_provider_services,omitempty"` RegisteredModels map[string]*resources.RegisteredModel `json:"registered_models,omitempty"` QualityMonitors map[string]*resources.QualityMonitor `json:"quality_monitors,omitempty"` Catalogs map[string]*resources.Catalog `json:"catalogs,omitempty"` @@ -105,6 +108,9 @@ func (r *Resources) AllResources() []ResourceGroup { collectResourceMap(descriptions["models"], r.Models), collectResourceMap(descriptions["experiments"], r.Experiments), collectResourceMap(descriptions["model_serving_endpoints"], r.ModelServingEndpoints), + collectResourceMap(descriptions["model_services"], r.ModelServices), + collectResourceMap(descriptions["mcp_services"], r.McpServices), + collectResourceMap(descriptions["model_provider_services"], r.ModelProviderServices), collectResourceMap(descriptions["registered_models"], r.RegisteredModels), collectResourceMap(descriptions["quality_monitors"], r.QualityMonitors), collectResourceMap(descriptions["catalogs"], r.Catalogs), @@ -171,6 +177,9 @@ func SupportedResources() map[string]resources.ResourceDescription { "experiments": (&resources.MlflowExperiment{}).ResourceDescription(), "instance_pools": (&resources.InstancePool{}).ResourceDescription(), "model_serving_endpoints": (&resources.ModelServingEndpoint{}).ResourceDescription(), + "model_services": (&resources.ModelService{}).ResourceDescription(), + "mcp_services": (&resources.McpService{}).ResourceDescription(), + "model_provider_services": (&resources.ModelProviderService{}).ResourceDescription(), "registered_models": (&resources.RegisteredModel{}).ResourceDescription(), "quality_monitors": (&resources.QualityMonitor{}).ResourceDescription(), "catalogs": (&resources.Catalog{}).ResourceDescription(), diff --git a/bundle/config/resources/mcp_service.go b/bundle/config/resources/mcp_service.go new file mode 100644 index 0000000000..65c2307e4f --- /dev/null +++ b/bundle/config/resources/mcp_service.go @@ -0,0 +1,84 @@ +package resources + +import ( + "context" + "net/url" + + "github.com/databricks/cli/libs/log" + "github.com/databricks/databricks-sdk-go" + "github.com/databricks/databricks-sdk-go/apierr" + "github.com/databricks/databricks-sdk-go/marshal" + "github.com/databricks/databricks-sdk-go/service/catalog" +) + +// McpServiceConfig is the bundle-authored state for an AI Gateway MCP service. +// +// It mirrors ModelServiceConfig: the SDK models the create inputs `parent` and +// `mcp_service_id` as URL parameters (`json:"-"`) outside the McpService body +// and derives the resource `name` +// (`mcp-services/{catalog}.{schema}.{mcp_service}`) server-side, so we expose a +// flat struct with the immutable identity plus the mutable body. Owner is not +// exposed yet (the API returns effective_owner on read, not owner). +type McpServiceConfig struct { + // Parent schema, format `schemas/{catalog}.{schema}`. Immutable: the server + // derives `name` from parent + mcp_service_id, so changing it recreates the + // resource. + Parent string `json:"parent"` + // Leaf id of the MCP service, e.g. "my_mcp_service". Immutable. + McpServiceId string `json:"mcp_service_id"` + // User-provided description. + Comment string `json:"comment,omitempty"` + // Operational configuration: source connection, tool selectors, rate limit. + Config *catalog.McpServiceConfig `json:"config,omitempty"` + + ForceSendFields []string `json:"-" url:"-"` +} + +func (c *McpServiceConfig) UnmarshalJSON(b []byte) error { + return marshal.Unmarshal(b, c) +} + +func (c McpServiceConfig) MarshalJSON() ([]byte, error) { + return marshal.Marshal(c) +} + +type McpService struct { + BaseResource + McpServiceConfig +} + +func (m *McpService) Exists(ctx context.Context, w *databricks.WorkspaceClient, id string) (bool, error) { + // The engine tracks the id as the bare {catalog}.{schema}.{mcp_service}; + // the API addresses the resource by its full name. + _, err := w.AiGateway.GetMcpService(ctx, catalog.GetMcpServiceRequest{Name: "mcp-services/" + id}) + if err != nil { + log.Debugf(ctx, "mcp service %s does not exist", id) + if apierr.IsMissing(err) { + return false, nil + } + return false, err + } + return true, nil +} + +func (*McpService) ResourceDescription() ResourceDescription { + return ResourceDescription{ + SingularName: "mcp_service", + PluralName: "mcp_services", + SingularTitle: "MCP service", + PluralTitle: "MCP services", + } +} + +func (m *McpService) InitializeURL(_ url.URL) { + // AI Gateway MCP services do not have a Catalog Explorer URL wired up here + // yet; leave URL unset until the UI route is confirmed. +} + +func (m *McpService) GetName() string { + return m.McpServiceId +} + +func (m *McpService) GetURL() string { + return m.URL +} diff --git a/bundle/config/resources/model_provider_service.go b/bundle/config/resources/model_provider_service.go new file mode 100644 index 0000000000..583268b694 --- /dev/null +++ b/bundle/config/resources/model_provider_service.go @@ -0,0 +1,92 @@ +package resources + +import ( + "context" + "net/url" + + "github.com/databricks/cli/libs/log" + "github.com/databricks/databricks-sdk-go" + "github.com/databricks/databricks-sdk-go/apierr" + "github.com/databricks/databricks-sdk-go/marshal" + "github.com/databricks/databricks-sdk-go/service/catalog" +) + +// ModelProviderServiceConfig is the bundle-authored state for an AI Gateway +// model provider service. +// +// It mirrors ModelServiceConfig: the SDK models the create inputs `parent` and +// `model_provider_service_id` as URL parameters (`json:"-"`) outside the +// ModelProviderService body and derives the resource `name` +// (`model-provider-services/{catalog}.{schema}.{model_provider_service}`) +// server-side, so we expose a flat struct with the immutable identity plus the +// mutable body. Owner is not exposed yet (the API returns effective_owner on +// read, not owner). +// +// Note: config.provider_type is immutable after create (the API rejects +// changing it via Update); it is classified recreate_on_changes in +// resources.yml. +type ModelProviderServiceConfig struct { + // Parent schema, format `schemas/{catalog}.{schema}`. Immutable: the server + // derives `name` from parent + model_provider_service_id, so changing it + // recreates the resource. + Parent string `json:"parent"` + // Leaf id of the model provider service, e.g. "openai_prod". Immutable. + ModelProviderServiceId string `json:"model_provider_service_id"` + // User-provided description. + Comment string `json:"comment,omitempty"` + // Behavioral configuration: provider connection, model catalog, passthrough + // policy. config.provider_type is required at create and immutable after. + Config *catalog.ModelProviderServiceConfig `json:"config,omitempty"` + + ForceSendFields []string `json:"-" url:"-"` +} + +func (c *ModelProviderServiceConfig) UnmarshalJSON(b []byte) error { + return marshal.Unmarshal(b, c) +} + +func (c ModelProviderServiceConfig) MarshalJSON() ([]byte, error) { + return marshal.Marshal(c) +} + +type ModelProviderService struct { + BaseResource + ModelProviderServiceConfig +} + +func (m *ModelProviderService) Exists(ctx context.Context, w *databricks.WorkspaceClient, id string) (bool, error) { + // The engine tracks the id as the bare + // {catalog}.{schema}.{model_provider_service}; the API addresses the + // resource by its full name. + _, err := w.AiGateway.GetModelProviderService(ctx, catalog.GetModelProviderServiceRequest{Name: "model-provider-services/" + id}) + if err != nil { + log.Debugf(ctx, "model provider service %s does not exist", id) + if apierr.IsMissing(err) { + return false, nil + } + return false, err + } + return true, nil +} + +func (*ModelProviderService) ResourceDescription() ResourceDescription { + return ResourceDescription{ + SingularName: "model_provider_service", + PluralName: "model_provider_services", + SingularTitle: "Model provider service", + PluralTitle: "Model provider services", + } +} + +func (m *ModelProviderService) InitializeURL(_ url.URL) { + // AI Gateway model provider services do not have a Catalog Explorer URL + // wired up here yet; leave URL unset until the UI route is confirmed. +} + +func (m *ModelProviderService) GetName() string { + return m.ModelProviderServiceId +} + +func (m *ModelProviderService) GetURL() string { + return m.URL +} diff --git a/bundle/config/resources/model_service.go b/bundle/config/resources/model_service.go new file mode 100644 index 0000000000..9fdfb0fd47 --- /dev/null +++ b/bundle/config/resources/model_service.go @@ -0,0 +1,91 @@ +package resources + +import ( + "context" + "net/url" + + "github.com/databricks/cli/libs/log" + "github.com/databricks/databricks-sdk-go" + "github.com/databricks/databricks-sdk-go/apierr" + "github.com/databricks/databricks-sdk-go/marshal" + "github.com/databricks/databricks-sdk-go/service/catalog" +) + +// ModelServiceConfig is the bundle-authored state for an AI Gateway model +// service. +// +// The SDK models the create inputs `parent` and `model_service_id` as URL +// parameters (`json:"-"`) that sit outside the ModelService body, and derives +// the resource `name` (`model-services/{catalog}.{schema}.{model_service}`) +// server-side. We therefore cannot embed catalog.CreateModelServiceRequest the +// way volume embeds catalog.CreateVolumeRequestContent: its identity fields +// would be invisible to the bundle schema. Instead we expose a flat struct with +// the immutable identity (parent + model_service_id) plus the mutable body. +// +// Owner is intentionally not exposed yet (mirrors volume, which does not manage +// owner): the API returns effective_owner rather than owner on read, so round +// tripping it needs extra care. See the direct engine resource for the CRUD. +type ModelServiceConfig struct { + // Parent schema, format `schemas/{catalog}.{schema}`. Immutable: the server + // derives `name` from parent + model_service_id, so changing it recreates + // the resource. + Parent string `json:"parent"` + // Leaf id of the model service, e.g. "my_model_service". Immutable. + ModelServiceId string `json:"model_service_id"` + // User-provided description. + Comment string `json:"comment,omitempty"` + // Operational configuration: destinations, routing, rate limits, inference + // table. + Config *catalog.ModelServiceConfig `json:"config,omitempty"` + + ForceSendFields []string `json:"-" url:"-"` +} + +func (c *ModelServiceConfig) UnmarshalJSON(b []byte) error { + return marshal.Unmarshal(b, c) +} + +func (c ModelServiceConfig) MarshalJSON() ([]byte, error) { + return marshal.Marshal(c) +} + +type ModelService struct { + BaseResource + ModelServiceConfig +} + +func (m *ModelService) Exists(ctx context.Context, w *databricks.WorkspaceClient, id string) (bool, error) { + // The engine tracks the id as the bare {catalog}.{schema}.{model_service}; + // the API addresses the resource by its full name. + _, err := w.AiGateway.GetModelService(ctx, catalog.GetModelServiceRequest{Name: "model-services/" + id}) + if err != nil { + log.Debugf(ctx, "model service %s does not exist", id) + if apierr.IsMissing(err) { + return false, nil + } + return false, err + } + return true, nil +} + +func (*ModelService) ResourceDescription() ResourceDescription { + return ResourceDescription{ + SingularName: "model_service", + PluralName: "model_services", + SingularTitle: "Model service", + PluralTitle: "Model services", + } +} + +func (m *ModelService) InitializeURL(_ url.URL) { + // AI Gateway model services do not have a Catalog Explorer URL wired up + // here yet; leave URL unset until the UI route is confirmed. +} + +func (m *ModelService) GetName() string { + return m.ModelServiceId +} + +func (m *ModelService) GetURL() string { + return m.URL +} diff --git a/bundle/config/resources_test.go b/bundle/config/resources_test.go index 7e56f47a64..720ed01e97 100644 --- a/bundle/config/resources_test.go +++ b/bundle/config/resources_test.go @@ -125,6 +125,11 @@ func TestBundleResourcePluralNamesResolveInWorkspaceURLs(t *testing.T) { // Resources that intentionally have no workspace URL. noURL := map[string]bool{ "external_locations": true, + // AI Gateway securables are addressed by a server-derived resource name + // and have no single-ID Catalog Explorer URL wired up yet. + "model_services": true, + "mcp_services": true, + "model_provider_services": true, // A job run does have a workspace URL, but it's addressed by two IDs // (job + run) so it can't be expressed as a single-ID pattern here; it's // built in JobRun.InitializeURL via workspaceurls.JobRunURL instead. @@ -238,6 +243,21 @@ func TestResourcesBindSupport(t *testing.T) { CreateServingEndpoint: serving.CreateServingEndpoint{}, }, }, + ModelServices: map[string]*resources.ModelService{ + "my_model_service": { + ModelServiceConfig: resources.ModelServiceConfig{}, + }, + }, + McpServices: map[string]*resources.McpService{ + "my_mcp_service": { + McpServiceConfig: resources.McpServiceConfig{}, + }, + }, + ModelProviderServices: map[string]*resources.ModelProviderService{ + "my_model_provider_service": { + ModelProviderServiceConfig: resources.ModelProviderServiceConfig{}, + }, + }, SecretScopes: map[string]*resources.SecretScope{ "my_secret_scope": { Name: "0", @@ -377,6 +397,9 @@ func TestResourcesBindSupport(t *testing.T) { m.GetMockAlertsV2API().EXPECT().GetAlertById(mock.Anything, mock.Anything).Return(nil, nil) m.GetMockQualityMonitorsAPI().EXPECT().Get(mock.Anything, mock.Anything).Return(nil, nil) m.GetMockServingEndpointsAPI().EXPECT().Get(mock.Anything, mock.Anything).Return(nil, nil) + m.GetMockAiGatewayAPI().EXPECT().GetModelService(mock.Anything, mock.Anything).Return(nil, nil) + m.GetMockAiGatewayAPI().EXPECT().GetMcpService(mock.Anything, mock.Anything).Return(nil, nil) + m.GetMockAiGatewayAPI().EXPECT().GetModelProviderService(mock.Anything, mock.Anything).Return(nil, nil) m.GetMockSecretsAPI().EXPECT().ListScopesAll(mock.Anything).Return([]workspace.SecretScope{ {Name: "0"}, }, nil) diff --git a/bundle/direct/dresources/all.go b/bundle/direct/dresources/all.go index 391fb0684d..374caa941e 100644 --- a/bundle/direct/dresources/all.go +++ b/bundle/direct/dresources/all.go @@ -35,6 +35,9 @@ var SupportedResources = map[string]any{ "genie_spaces": (*ResourceGenieSpace)(nil), "secret_scopes": (*ResourceSecretScope)(nil), "model_serving_endpoints": (*ResourceModelServingEndpoint)(nil), + "model_services": (*ResourceModelService)(nil), + "mcp_services": (*ResourceMcpService)(nil), + "model_provider_services": (*ResourceModelProviderService)(nil), "quality_monitors": (*ResourceQualityMonitor)(nil), "vector_search_endpoints": (*ResourceVectorSearchEndpoint)(nil), "vector_search_indexes": (*ResourceVectorSearchIndex)(nil), diff --git a/bundle/direct/dresources/all_test.go b/bundle/direct/dresources/all_test.go index 36eaa3e27d..3e8f864174 100644 --- a/bundle/direct/dresources/all_test.go +++ b/bundle/direct/dresources/all_test.go @@ -95,6 +95,30 @@ var testConfig map[string]any = map[string]any{ }, }, + "model_services": &resources.ModelService{ + ModelServiceConfig: resources.ModelServiceConfig{ + Parent: "schemas/main.default", + ModelServiceId: "my_model_service", + Comment: "Test model service", + }, + }, + + "mcp_services": &resources.McpService{ + McpServiceConfig: resources.McpServiceConfig{ + Parent: "schemas/main.default", + McpServiceId: "my_mcp_service", + Comment: "Test mcp service", + }, + }, + + "model_provider_services": &resources.ModelProviderService{ + ModelProviderServiceConfig: resources.ModelProviderServiceConfig{ + Parent: "schemas/main.default", + ModelProviderServiceId: "my_model_provider_service", + Comment: "Test model provider service", + }, + }, + "registered_models": &resources.RegisteredModel{ CreateRegisteredModelRequest: catalog.CreateRegisteredModelRequest{ Name: "my_registered_model", diff --git a/bundle/direct/dresources/apitypes.generated.yml b/bundle/direct/dresources/apitypes.generated.yml index ec2e3c2519..7ce6f6496c 100644 --- a/bundle/direct/dresources/apitypes.generated.yml +++ b/bundle/direct/dresources/apitypes.generated.yml @@ -28,6 +28,12 @@ job_runs: jobs.RunNow jobs: jobs.JobSettings +mcp_services: catalog.CreateMcpServiceRequest + +model_provider_services: catalog.CreateModelProviderServiceRequest + +model_services: catalog.CreateModelServiceRequest + model_serving_endpoints: serving.CreateServingEndpoint models: ml.CreateModelRequest diff --git a/bundle/direct/dresources/mcp_service.go b/bundle/direct/dresources/mcp_service.go new file mode 100644 index 0000000000..7590eff5d0 --- /dev/null +++ b/bundle/direct/dresources/mcp_service.go @@ -0,0 +1,113 @@ +package dresources + +import ( + "context" + "fmt" + "strings" + + "github.com/databricks/cli/bundle/config/resources" + "github.com/databricks/databricks-sdk-go" + "github.com/databricks/databricks-sdk-go/common/types/fieldmask" + "github.com/databricks/databricks-sdk-go/service/catalog" +) + +// AI Gateway MCP service. +// API: https://docs.databricks.com/api/workspace/aigateway +// Terraform: databricks_ai_gateway_mcp_service +// +// Mirrors ResourceModelService: the remote type returned by DoRead is the same +// bundle-local resources.McpServiceConfig used for state (so RemapState is not +// needed), and DoRead reconstructs the create-time identity (parent + +// mcp_service_id) from the server-derived resource name. +const mcpServiceNamePrefix = "mcp-services/" + +type ResourceMcpService struct { + client *databricks.WorkspaceClient +} + +func (*ResourceMcpService) New(client *databricks.WorkspaceClient) *ResourceMcpService { + return &ResourceMcpService{client: client} +} + +func (*ResourceMcpService) PrepareState(input *resources.McpService) *resources.McpServiceConfig { + return &input.McpServiceConfig +} + +// mcpServiceIdentityFromName reconstructs the create-time parent and leaf id +// from the server-derived resource name +// `mcp-services/{catalog}.{schema}.{mcp_service}`. +func mcpServiceIdentityFromName(name string) (parent, mcpServiceId string, err error) { + rest, ok := strings.CutPrefix(name, mcpServiceNamePrefix) + if !ok { + return "", "", fmt.Errorf("unexpected mcp service name %q (want mcp-services/{catalog}.{schema}.{mcp_service})", name) + } + parts := strings.Split(rest, ".") + if len(parts) != 3 { + return "", "", fmt.Errorf("unexpected mcp service name %q (want three dot-separated components)", name) + } + return "schemas/" + parts[0] + "." + parts[1], parts[2], nil +} + +func responseToMcpServiceConfig(ms *catalog.McpService) (*resources.McpServiceConfig, error) { + parent, id, err := mcpServiceIdentityFromName(ms.Name) + if err != nil { + return nil, err + } + return &resources.McpServiceConfig{ + Parent: parent, + McpServiceId: id, + Comment: ms.Comment, + Config: ms.Config, + }, nil +} + +func (r *ResourceMcpService) DoRead(ctx context.Context, id string) (*resources.McpServiceConfig, error) { + ms, err := r.client.AiGateway.GetMcpService(ctx, catalog.GetMcpServiceRequest{Name: mcpServiceNamePrefix + id}) + if err != nil { + return nil, err + } + return responseToMcpServiceConfig(ms) +} + +func (r *ResourceMcpService) DoCreate(ctx context.Context, config *resources.McpServiceConfig) (string, *resources.McpServiceConfig, error) { + resp, err := r.client.AiGateway.CreateMcpService(ctx, catalog.CreateMcpServiceRequest{ + Parent: config.Parent, + McpServiceId: config.McpServiceId, + McpService: catalog.McpService{ + Comment: config.Comment, + Config: config.Config, + }, + }) + if err != nil { + return "", nil, err + } + state, err := responseToMcpServiceConfig(resp) + if err != nil { + return "", nil, err + } + return strings.TrimPrefix(resp.Name, mcpServiceNamePrefix), state, nil +} + +// mcpServiceUpdateMask lists the mutable fields sent on every update. name, +// parent and mcp_service_id are immutable (provided_id_fields in resources.yml). +// The API rejects wildcard masks, so each path is explicit. +var mcpServiceUpdateMask = []string{"comment", "config"} + +func (r *ResourceMcpService) DoUpdate(ctx context.Context, id string, config *resources.McpServiceConfig, _ *PlanEntry) (*resources.McpServiceConfig, error) { + resp, err := r.client.AiGateway.UpdateMcpService(ctx, catalog.UpdateMcpServiceRequest{ + Name: mcpServiceNamePrefix + id, + McpService: catalog.McpService{ + Comment: config.Comment, + Config: config.Config, + }, + UpdateMask: fieldmask.FieldMask{Paths: mcpServiceUpdateMask}, + }) + if err != nil { + return nil, err + } + return responseToMcpServiceConfig(resp) +} + +func (r *ResourceMcpService) DoDelete(ctx context.Context, id string, _ *resources.McpServiceConfig) error { + return r.client.AiGateway.DeleteMcpService(ctx, catalog.DeleteMcpServiceRequest{Name: mcpServiceNamePrefix + id}) +} diff --git a/bundle/direct/dresources/model_provider_service.go b/bundle/direct/dresources/model_provider_service.go new file mode 100644 index 0000000000..3729c75f52 --- /dev/null +++ b/bundle/direct/dresources/model_provider_service.go @@ -0,0 +1,114 @@ +package dresources + +import ( + "context" + "fmt" + "strings" + + "github.com/databricks/cli/bundle/config/resources" + "github.com/databricks/databricks-sdk-go" + "github.com/databricks/databricks-sdk-go/common/types/fieldmask" + "github.com/databricks/databricks-sdk-go/service/catalog" +) + +// AI Gateway model provider service. +// API: https://docs.databricks.com/api/workspace/aigateway +// Terraform: databricks_ai_gateway_model_provider_service +// +// Mirrors ResourceModelService: the remote type returned by DoRead is the same +// bundle-local resources.ModelProviderServiceConfig used for state (so +// RemapState is not needed), and DoRead reconstructs the create-time identity +// (parent + model_provider_service_id) from the server-derived resource name. +const modelProviderServiceNamePrefix = "model-provider-services/" + +type ResourceModelProviderService struct { + client *databricks.WorkspaceClient +} + +func (*ResourceModelProviderService) New(client *databricks.WorkspaceClient) *ResourceModelProviderService { + return &ResourceModelProviderService{client: client} +} + +func (*ResourceModelProviderService) PrepareState(input *resources.ModelProviderService) *resources.ModelProviderServiceConfig { + return &input.ModelProviderServiceConfig +} + +// modelProviderServiceIdentityFromName reconstructs the create-time parent and +// leaf id from the server-derived resource name +// `model-provider-services/{catalog}.{schema}.{model_provider_service}`. +func modelProviderServiceIdentityFromName(name string) (parent, modelProviderServiceId string, err error) { + rest, ok := strings.CutPrefix(name, modelProviderServiceNamePrefix) + if !ok { + return "", "", fmt.Errorf("unexpected model provider service name %q (want model-provider-services/{catalog}.{schema}.{model_provider_service})", name) + } + parts := strings.Split(rest, ".") + if len(parts) != 3 { + return "", "", fmt.Errorf("unexpected model provider service name %q (want three dot-separated components)", name) + } + return "schemas/" + parts[0] + "." + parts[1], parts[2], nil +} + +func responseToModelProviderServiceConfig(ms *catalog.ModelProviderService) (*resources.ModelProviderServiceConfig, error) { + parent, id, err := modelProviderServiceIdentityFromName(ms.Name) + if err != nil { + return nil, err + } + return &resources.ModelProviderServiceConfig{ + Parent: parent, + ModelProviderServiceId: id, + Comment: ms.Comment, + Config: ms.Config, + }, nil +} + +func (r *ResourceModelProviderService) DoRead(ctx context.Context, id string) (*resources.ModelProviderServiceConfig, error) { + ms, err := r.client.AiGateway.GetModelProviderService(ctx, catalog.GetModelProviderServiceRequest{Name: modelProviderServiceNamePrefix + id}) + if err != nil { + return nil, err + } + return responseToModelProviderServiceConfig(ms) +} + +func (r *ResourceModelProviderService) DoCreate(ctx context.Context, config *resources.ModelProviderServiceConfig) (string, *resources.ModelProviderServiceConfig, error) { + resp, err := r.client.AiGateway.CreateModelProviderService(ctx, catalog.CreateModelProviderServiceRequest{ + Parent: config.Parent, + ModelProviderServiceId: config.ModelProviderServiceId, + ModelProviderService: catalog.ModelProviderService{ + Comment: config.Comment, + Config: config.Config, + }, + }) + if err != nil { + return "", nil, err + } + state, err := responseToModelProviderServiceConfig(resp) + if err != nil { + return "", nil, err + } + return strings.TrimPrefix(resp.Name, modelProviderServiceNamePrefix), state, nil +} + +// modelProviderServiceUpdateMask lists the mutable fields sent on every update. +// name, parent and model_provider_service_id are immutable (provided_id_fields), +// and config.provider_type is immutable (recreate_on_changes). The API rejects +// wildcard masks, so each path is explicit. +var modelProviderServiceUpdateMask = []string{"comment", "config"} + +func (r *ResourceModelProviderService) DoUpdate(ctx context.Context, id string, config *resources.ModelProviderServiceConfig, _ *PlanEntry) (*resources.ModelProviderServiceConfig, error) { + resp, err := r.client.AiGateway.UpdateModelProviderService(ctx, catalog.UpdateModelProviderServiceRequest{ + Name: modelProviderServiceNamePrefix + id, + ModelProviderService: catalog.ModelProviderService{ + Comment: config.Comment, + Config: config.Config, + }, + UpdateMask: fieldmask.FieldMask{Paths: modelProviderServiceUpdateMask}, + }) + if err != nil { + return nil, err + } + return responseToModelProviderServiceConfig(resp) +} + +func (r *ResourceModelProviderService) DoDelete(ctx context.Context, id string, _ *resources.ModelProviderServiceConfig) error { + return r.client.AiGateway.DeleteModelProviderService(ctx, catalog.DeleteModelProviderServiceRequest{Name: modelProviderServiceNamePrefix + id}) +} diff --git a/bundle/direct/dresources/model_service.go b/bundle/direct/dresources/model_service.go new file mode 100644 index 0000000000..295270e990 --- /dev/null +++ b/bundle/direct/dresources/model_service.go @@ -0,0 +1,118 @@ +package dresources + +import ( + "context" + "fmt" + "strings" + + "github.com/databricks/cli/bundle/config/resources" + "github.com/databricks/databricks-sdk-go" + "github.com/databricks/databricks-sdk-go/common/types/fieldmask" + "github.com/databricks/databricks-sdk-go/service/catalog" +) + +// AI Gateway model service. +// API: https://docs.databricks.com/api/workspace/aigateway +// Terraform: databricks_ai_gateway_model_service +// +// The remote type returned by DoRead is the same bundle-local +// resources.ModelServiceConfig used for state, so RemapState is not needed. +// DoRead reconstructs the create-time identity (parent + model_service_id) from +// the server-derived resource name so those fields participate in normal drift +// detection rather than being suppressed as missing-in-remote. +// modelServiceNamePrefix is the fixed prefix of the resource name +// (model-services/{catalog}.{schema}.{model_service}). The engine tracks the id +// as the bare {catalog}.{schema}.{model_service} portion and this prefix is +// re-added when addressing the resource through the SDK. +const modelServiceNamePrefix = "model-services/" + +type ResourceModelService struct { + client *databricks.WorkspaceClient +} + +func (*ResourceModelService) New(client *databricks.WorkspaceClient) *ResourceModelService { + return &ResourceModelService{client: client} +} + +func (*ResourceModelService) PrepareState(input *resources.ModelService) *resources.ModelServiceConfig { + return &input.ModelServiceConfig +} + +// modelServiceIdentityFromName reconstructs the create-time parent and leaf id +// from the server-derived resource name +// `model-services/{catalog}.{schema}.{model_service}`. +func modelServiceIdentityFromName(name string) (parent, modelServiceId string, err error) { + rest, ok := strings.CutPrefix(name, "model-services/") + if !ok { + return "", "", fmt.Errorf("unexpected model service name %q (want model-services/{catalog}.{schema}.{model_service})", name) + } + parts := strings.Split(rest, ".") + if len(parts) != 3 { + return "", "", fmt.Errorf("unexpected model service name %q (want three dot-separated components)", name) + } + return "schemas/" + parts[0] + "." + parts[1], parts[2], nil +} + +func responseToModelServiceConfig(ms *catalog.ModelService) (*resources.ModelServiceConfig, error) { + parent, id, err := modelServiceIdentityFromName(ms.Name) + if err != nil { + return nil, err + } + return &resources.ModelServiceConfig{ + Parent: parent, + ModelServiceId: id, + Comment: ms.Comment, + Config: ms.Config, + }, nil +} + +func (r *ResourceModelService) DoRead(ctx context.Context, id string) (*resources.ModelServiceConfig, error) { + ms, err := r.client.AiGateway.GetModelService(ctx, catalog.GetModelServiceRequest{Name: modelServiceNamePrefix + id}) + if err != nil { + return nil, err + } + return responseToModelServiceConfig(ms) +} + +func (r *ResourceModelService) DoCreate(ctx context.Context, config *resources.ModelServiceConfig) (string, *resources.ModelServiceConfig, error) { + resp, err := r.client.AiGateway.CreateModelService(ctx, catalog.CreateModelServiceRequest{ + Parent: config.Parent, + ModelServiceId: config.ModelServiceId, + ModelService: catalog.ModelService{ + Comment: config.Comment, + Config: config.Config, + }, + }) + if err != nil { + return "", nil, err + } + state, err := responseToModelServiceConfig(resp) + if err != nil { + return "", nil, err + } + return strings.TrimPrefix(resp.Name, modelServiceNamePrefix), state, nil +} + +// modelServiceUpdateMask lists the mutable fields sent on every update. name, +// parent and model_service_id are immutable (recreate_on_changes in +// resources.yml). The API rejects wildcard masks, so each path is explicit. +var modelServiceUpdateMask = []string{"comment", "config"} + +func (r *ResourceModelService) DoUpdate(ctx context.Context, id string, config *resources.ModelServiceConfig, _ *PlanEntry) (*resources.ModelServiceConfig, error) { + resp, err := r.client.AiGateway.UpdateModelService(ctx, catalog.UpdateModelServiceRequest{ + Name: modelServiceNamePrefix + id, + ModelService: catalog.ModelService{ + Comment: config.Comment, + Config: config.Config, + }, + UpdateMask: fieldmask.FieldMask{Paths: modelServiceUpdateMask}, + }) + if err != nil { + return nil, err + } + return responseToModelServiceConfig(resp) +} + +func (r *ResourceModelService) DoDelete(ctx context.Context, id string, _ *resources.ModelServiceConfig) error { + return r.client.AiGateway.DeleteModelService(ctx, catalog.DeleteModelServiceRequest{Name: modelServiceNamePrefix + id}) +} diff --git a/bundle/direct/dresources/resources.generated.yml b/bundle/direct/dresources/resources.generated.yml index d2dfda855b..31fa98ca29 100644 --- a/bundle/direct/dresources/resources.generated.yml +++ b/bundle/direct/dresources/resources.generated.yml @@ -209,6 +209,12 @@ resources: # jobs: no api field behaviors + # mcp_services: no api field behaviors + + # model_provider_services: no api field behaviors + + # model_services: no api field behaviors + model_serving_endpoints: ignore_remote_changes: diff --git a/bundle/direct/dresources/resources.yml b/bundle/direct/dresources/resources.yml index 07eab3b689..a1c9f597ef 100644 --- a/bundle/direct/dresources/resources.yml +++ b/bundle/direct/dresources/resources.yml @@ -355,6 +355,88 @@ resources: # and the remote diverges, normal drift detection still applies. - field: config.traffic_config + model_services: + provided_id_fields: + # parent + model_service_id compose the server-derived resource name + # (model-services/{catalog}.{schema}.{model_service}), which is the ID the + # resource is fetched by. Both are immutable; a local change recreates. + # DoRead reconstructs them from the returned name, so a remote-only + # difference can only be normalization and is skipped. + - field: parent + reason: id_field + - field: model_service_id + reason: id_field + ignore_remote_changes: + # Server-resolved / tombstone fields returned on read, not user-authored. + # The generator maps model_services to the create-request type (config + # nested under model_service.*) so it can't emit these config.* paths; + # classify them by hand to avoid phantom drift. + - field: config.inference_table.table + reason: output_only + - field: config.inference_table.is_deleted + reason: output_only + + mcp_services: + provided_id_fields: + # parent + mcp_service_id compose the server-derived resource name + # (mcp-services/{catalog}.{schema}.{mcp_service}), which is the ID the + # resource is fetched by. Both are immutable; a local change recreates. + # DoRead reconstructs them from the returned name, so a remote-only + # difference can only be normalization and is skipped. + - field: parent + reason: id_field + - field: mcp_service_id + reason: id_field + ignore_remote_changes: + # Tombstone marker returned on read when the bound UC connection is + # deleted; server-populated, not user-authored (see model_services note). + - field: config.source_connection.is_deleted + reason: output_only + + model_provider_services: + provided_id_fields: + # parent + model_provider_service_id compose the server-derived resource + # name (model-provider-services/{catalog}.{schema}.{model_provider_service}), + # which is the ID the resource is fetched by. Both immutable; a local + # change recreates. DoRead reconstructs them from the returned name. + - field: parent + reason: id_field + - field: model_provider_service_id + reason: id_field + recreate_on_changes: + # provider_type is required at create and immutable thereafter; the API + # rejects changing it via Update, so a local change must recreate. + - field: config.provider_type + reason: immutable + ignore_remote_changes: + # Server-resolved / tombstone fields returned on read (see model_services + # note on why the generator can't emit these config.* paths). + - field: config.inference_table.table + reason: output_only + - field: config.inference_table.is_deleted + reason: output_only + # Write-only provider credentials: accepted on create/update, never + # returned by GET, so remote always reports empty. Suppress to avoid + # phantom drift on every deploy. + - field: config.amazon_bedrock.direct.aws_access_key.secret_access_key.plaintext + reason: input_only + - field: config.anthropic.direct.api_key.plaintext + reason: input_only + - field: config.azure_openai.direct.api_key.plaintext + reason: input_only + - field: config.azure_openai.direct.entra_service_principal.client_secret.plaintext + reason: input_only + - field: config.custom.direct.api_key.plaintext + reason: input_only + - field: config.gemini_enterprise.direct.api_key.plaintext + reason: input_only + - field: config.microsoft_foundry.direct.api_key.plaintext + reason: input_only + - field: config.microsoft_foundry.direct.entra_service_principal.client_secret.plaintext + reason: input_only + - field: config.openai.direct.api_key.plaintext + reason: input_only + registered_models: ignore_remote_changes: # Output-only timestamp/user fields populated by the backend on read. diff --git a/bundle/internal/schema/annotations.yml b/bundle/internal/schema/annotations.yml index e47f89c505..d8e28f5fa5 100644 --- a/bundle/internal/schema/annotations.yml +++ b/bundle/internal/schema/annotations.yml @@ -1172,6 +1172,69 @@ resources: "table_update": "description": |- PLACEHOLDER + "mcp_services": + "description": |- + PLACEHOLDER + "$fields": + "comment": + "description": |- + PLACEHOLDER + "config": + "description": |- + PLACEHOLDER + "$fields": + "source_connection": + "$fields": + "is_deleted": + "description": |- + PLACEHOLDER + "lifecycle": + "description": |- + PLACEHOLDER + "mcp_service_id": + "description": |- + PLACEHOLDER + "parent": + "description": |- + PLACEHOLDER + "model_provider_services": + "description": |- + PLACEHOLDER + "$fields": + "comment": + "description": |- + PLACEHOLDER + "config": + "description": |- + PLACEHOLDER + "lifecycle": + "description": |- + PLACEHOLDER + "model_provider_service_id": + "description": |- + PLACEHOLDER + "parent": + "description": |- + PLACEHOLDER + "model_services": + "description": |- + PLACEHOLDER + "$fields": + "comment": + "description": |- + PLACEHOLDER + "config": + "description": |- + PLACEHOLDER + "lifecycle": + "description": |- + PLACEHOLDER + "model_service_id": + "description": |- + PLACEHOLDER + "parent": + "description": |- + PLACEHOLDER "model_serving_endpoints": "description": |- The model serving endpoint definitions for the bundle, where each key is the name of the model serving endpoint. diff --git a/bundle/schema/jsonschema.json b/bundle/schema/jsonschema.json index 425956ab06..d244457263 100644 --- a/bundle/schema/jsonschema.json +++ b/bundle/schema/jsonschema.json @@ -1444,6 +1444,39 @@ } ] }, + "resources.McpService": { + "oneOf": [ + { + "type": "object", + "properties": { + "comment": { + "$ref": "#/$defs/string" + }, + "config": { + "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/catalog.McpServiceConfig" + }, + "lifecycle": { + "$ref": "#/$defs/github.com/databricks/cli/bundle/config/resources.Lifecycle" + }, + "mcp_service_id": { + "$ref": "#/$defs/string" + }, + "parent": { + "$ref": "#/$defs/string" + } + }, + "additionalProperties": false, + "required": [ + "parent", + "mcp_service_id" + ] + }, + { + "type": "string", + "pattern": "\\$\\{(var(\\.\\p{L}+([-_]*[\\p{L}\\p{N}]+)*(\\[[0-9]+\\])*)+)\\}" + } + ] + }, "resources.MlflowExperiment": { "oneOf": [ { @@ -1594,6 +1627,72 @@ } ] }, + "resources.ModelProviderService": { + "oneOf": [ + { + "type": "object", + "properties": { + "comment": { + "$ref": "#/$defs/string" + }, + "config": { + "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/catalog.ModelProviderServiceConfig" + }, + "lifecycle": { + "$ref": "#/$defs/github.com/databricks/cli/bundle/config/resources.Lifecycle" + }, + "model_provider_service_id": { + "$ref": "#/$defs/string" + }, + "parent": { + "$ref": "#/$defs/string" + } + }, + "additionalProperties": false, + "required": [ + "parent", + "model_provider_service_id" + ] + }, + { + "type": "string", + "pattern": "\\$\\{(var(\\.\\p{L}+([-_]*[\\p{L}\\p{N}]+)*(\\[[0-9]+\\])*)+)\\}" + } + ] + }, + "resources.ModelService": { + "oneOf": [ + { + "type": "object", + "properties": { + "comment": { + "$ref": "#/$defs/string" + }, + "config": { + "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/catalog.ModelServiceConfig" + }, + "lifecycle": { + "$ref": "#/$defs/github.com/databricks/cli/bundle/config/resources.Lifecycle" + }, + "model_service_id": { + "$ref": "#/$defs/string" + }, + "parent": { + "$ref": "#/$defs/string" + } + }, + "additionalProperties": false, + "required": [ + "parent", + "model_service_id" + ] + }, + { + "type": "string", + "pattern": "\\$\\{(var(\\.\\p{L}+([-_]*[\\p{L}\\p{N}]+)*(\\[[0-9]+\\])*)+)\\}" + } + ] + }, "resources.ModelServingEndpoint": { "oneOf": [ { @@ -3534,6 +3633,15 @@ "$ref": "#/$defs/map/github.com/databricks/cli/bundle/config/resources.Job", "markdownDescription": "The job definitions for the bundle, where each key is the name of the job. See [jobs](https://docs.databricks.com/dev-tools/bundles/resources.html#jobs)." }, + "mcp_services": { + "$ref": "#/$defs/map/github.com/databricks/cli/bundle/config/resources.McpService" + }, + "model_provider_services": { + "$ref": "#/$defs/map/github.com/databricks/cli/bundle/config/resources.ModelProviderService" + }, + "model_services": { + "$ref": "#/$defs/map/github.com/databricks/cli/bundle/config/resources.ModelService" + }, "model_serving_endpoints": { "description": "The model serving endpoint definitions for the bundle, where each key is the name of the model serving endpoint.", "$ref": "#/$defs/map/github.com/databricks/cli/bundle/config/resources.ModelServingEndpoint", @@ -4649,15 +4757,902 @@ } ] }, - "apps.TelemetryExportDestination": { + "apps.TelemetryExportDestination": { + "oneOf": [ + { + "type": "object", + "description": "A single telemetry export destination with its configuration and status.", + "properties": { + "unity_catalog": { + "description": "[Public Preview] Unity Catalog Destinations for OTEL telemetry export.", + "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/apps.UnityCatalog" + } + }, + "additionalProperties": false + }, + { + "type": "string", + "pattern": "\\$\\{(var(\\.\\p{L}+([-_]*[\\p{L}\\p{N}]+)*(\\[[0-9]+\\])*)+)\\}" + } + ] + }, + "apps.UnityCatalog": { + "oneOf": [ + { + "type": "object", + "description": "Unity Catalog Destinations for OTEL telemetry export.", + "properties": { + "logs_table": { + "description": "[Public Preview] Unity Catalog table for OTEL logs.", + "$ref": "#/$defs/string" + }, + "metrics_table": { + "description": "[Public Preview] Unity Catalog table for OTEL metrics.", + "$ref": "#/$defs/string" + }, + "traces_table": { + "description": "[Public Preview] Unity Catalog table for OTEL traces (spans).", + "$ref": "#/$defs/string" + } + }, + "additionalProperties": false, + "required": [ + "logs_table", + "metrics_table", + "traces_table" + ] + }, + { + "type": "string", + "pattern": "\\$\\{(var(\\.\\p{L}+([-_]*[\\p{L}\\p{N}]+)*(\\[[0-9]+\\])*)+)\\}" + } + ] + }, + "catalog.AwsSqsQueue": { + "oneOf": [ + { + "type": "object", + "properties": { + "queue_url": { + "description": "The AQS queue url in the format https://sqs.{region}.amazonaws.com/{account id}/{queue name}.\nOnly required for provided_sqs.", + "$ref": "#/$defs/string" + } + }, + "additionalProperties": false + }, + { + "type": "string", + "pattern": "\\$\\{(var(\\.\\p{L}+([-_]*[\\p{L}\\p{N}]+)*(\\[[0-9]+\\])*)+)\\}" + } + ] + }, + "catalog.AzureEncryptionSettings": { + "oneOf": [ + { + "type": "object", + "properties": { + "azure_cmk_access_connector_id": { + "$ref": "#/$defs/string" + }, + "azure_cmk_managed_identity_id": { + "$ref": "#/$defs/string" + }, + "azure_tenant_id": { + "$ref": "#/$defs/string" + } + }, + "additionalProperties": false, + "required": [ + "azure_tenant_id" + ] + }, + { + "type": "string", + "pattern": "\\$\\{(var(\\.\\p{L}+([-_]*[\\p{L}\\p{N}]+)*(\\[[0-9]+\\])*)+)\\}" + } + ] + }, + "catalog.AzureQueueStorage": { + "oneOf": [ + { + "type": "object", + "properties": { + "queue_url": { + "description": "The AQS queue url in the format https://{storage account}.queue.core.windows.net/{queue name}\nOnly required for provided_aqs.", + "$ref": "#/$defs/string" + }, + "resource_group": { + "description": "Optional resource group for the queue, event grid subscription, and external location storage\naccount.\nOnly required for locations with a service principal storage credential", + "$ref": "#/$defs/string" + }, + "subscription_id": { + "description": "Optional subscription id for the queue, event grid subscription, and external location storage\naccount.\nRequired for locations with a service principal storage credential", + "$ref": "#/$defs/string" + } + }, + "additionalProperties": false + }, + { + "type": "string", + "pattern": "\\$\\{(var(\\.\\p{L}+([-_]*[\\p{L}\\p{N}]+)*(\\[[0-9]+\\])*)+)\\}" + } + ] + }, + "catalog.EncryptionDetails": { + "oneOf": [ + { + "type": "object", + "description": "Encryption options that apply to clients connecting to cloud storage.", + "properties": { + "sse_encryption_details": { + "description": "Server-Side Encryption properties for clients communicating with AWS s3.", + "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/catalog.SseEncryptionDetails" + } + }, + "additionalProperties": false + }, + { + "type": "string", + "pattern": "\\$\\{(var(\\.\\p{L}+([-_]*[\\p{L}\\p{N}]+)*(\\[[0-9]+\\])*)+)\\}" + } + ] + }, + "catalog.EncryptionSettings": { + "oneOf": [ + { + "type": "object", + "description": "Encryption Settings are used to carry metadata for securable encryption at rest.\nCurrently used for catalogs, we can use the information supplied here to interact with a CMK.", + "properties": { + "azure_encryption_settings": { + "description": "optional Azure settings - only required if an Azure CMK is used.", + "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/catalog.AzureEncryptionSettings" + }, + "azure_key_vault_key_id": { + "description": "the AKV URL in Azure, null otherwise.", + "$ref": "#/$defs/string" + }, + "customer_managed_key_id": { + "description": "the CMK uuid in AWS and GCP, null otherwise.", + "$ref": "#/$defs/string" + } + }, + "additionalProperties": false + }, + { + "type": "string", + "pattern": "\\$\\{(var(\\.\\p{L}+([-_]*[\\p{L}\\p{N}]+)*(\\[[0-9]+\\])*)+)\\}" + } + ] + }, + "catalog.FileEventQueue": { + "oneOf": [ + { + "type": "object", + "properties": { + "managed_aqs": { + "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/catalog.AzureQueueStorage" + }, + "managed_pubsub": { + "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/catalog.GcpPubsub" + }, + "managed_sqs": { + "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/catalog.AwsSqsQueue" + }, + "provided_aqs": { + "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/catalog.AzureQueueStorage" + }, + "provided_pubsub": { + "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/catalog.GcpPubsub" + }, + "provided_sqs": { + "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/catalog.AwsSqsQueue" + } + }, + "additionalProperties": false + }, + { + "type": "string", + "pattern": "\\$\\{(var(\\.\\p{L}+([-_]*[\\p{L}\\p{N}]+)*(\\[[0-9]+\\])*)+)\\}" + } + ] + }, + "catalog.GcpPubsub": { + "oneOf": [ + { + "type": "object", + "properties": { + "subscription_name": { + "description": "The Pub/Sub subscription name in the format projects/{project}/subscriptions/{subscription name}.\nOnly required for provided_pubsub.", + "$ref": "#/$defs/string" + } + }, + "additionalProperties": false + }, + { + "type": "string", + "pattern": "\\$\\{(var(\\.\\p{L}+([-_]*[\\p{L}\\p{N}]+)*(\\[[0-9]+\\])*)+)\\}" + } + ] + }, + "catalog.InferenceTableConfig": { + "oneOf": [ + { + "type": "object", + "description": "Inference table configuration for payload logging on a model service.\n\n`parent` is always REQUIRED when the sub-message is set; the destination\nUC schema is needed to construct or rebind the payload TABLE regardless of\nwhether payload logging is currently active. Payload logging is active by\ndefault; set `disabled = true` to pause runtime logging without dropping the\ntable or the binding.", + "properties": { + "disabled": { + "description": "[Beta] Indicates whether payload logging is disabled (opt-out). Unset means that\npayload logging is active (the on-by-default state coincides with the proto\nzero-value, so the server never fills this field for a client that leaves it\nunset). Set `disabled = true` to pause runtime logging while keeping the\nsub-message attached (preserving `parent` and `table_name_prefix` for a\nlater flip back to active). `parent` remains required either way.", + "$ref": "#/$defs/bool" + }, + "parent": { + "description": "[Beta] Parent UC schema where the inference table is created.\nFormat: `schemas/{catalog}.{schema}`. Set at create time and immutable\nthereafter; changing it on an existing service is rejected.", + "$ref": "#/$defs/string" + }, + "table_name_prefix": { + "description": "[Beta] Prefix for the inference-table's UC-registered name. The actual leaf name UC\nstores is `\u003ctable_name_prefix\u003e_payload`; the `_payload` suffix is appended\nautomatically. To find the actual UC table after Create, read the `table`\nfield on the response. Defaults to `\u003cmodel_service_name\u003e_payload` when unset.\nSet at create time and immutable thereafter; changing it on an existing\nservice is rejected.", + "$ref": "#/$defs/string" + } + }, + "additionalProperties": false, + "required": [ + "parent" + ] + }, + { + "type": "string", + "pattern": "\\$\\{(var(\\.\\p{L}+([-_]*[\\p{L}\\p{N}]+)*(\\[[0-9]+\\])*)+)\\}" + } + ] + }, + "catalog.McpServiceConfig": { + "oneOf": [ + { + "type": "object", + "description": "Operational configuration for an MCP service. Groups the source reference,\ntool selectors, and rate limit -- the fields that configure how the MCP\nservice behaves at invocation time.", + "properties": { + "include_tool_selectors": { + "description": "[Beta] Glob or exact-match patterns selecting which tools from the MCP server\nto expose. Prefix match for patterns with `*`, exact match otherwise.\nAn empty list means all tools are included. Per-element max 256 chars.", + "$ref": "#/$defs/slice/string" + }, + "rate_limits": { + "description": "[Beta] Per-principal rate limits applied to tool invocations routed through this\nMCP service. Repeated to support per-USER / USER_GROUP / SERVICE_PRINCIPAL\n/ SERVICE / USER_DEFAULT scopes simultaneously, mirroring the\n`ModelServiceConfig.rate_limits` shape. Empty when no rate limit is\nconfigured.", + "$ref": "#/$defs/slice/github.com/databricks/databricks-sdk-go/service/catalog.RateLimit" + }, + "source_connection": { + "description": "[Beta] UC Connection referencing the MCP server.", + "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/catalog.McpServiceConfigSourceConnection" + } + }, + "additionalProperties": false + }, + { + "type": "string", + "pattern": "\\$\\{(var(\\.\\p{L}+([-_]*[\\p{L}\\p{N}]+)*(\\[[0-9]+\\])*)+)\\}" + } + ] + }, + "catalog.McpServiceConfigSourceConnection": { + "oneOf": [ + { + "type": "object", + "description": "UC Connection that hosts the MCP server. On create, provide `name` in the\nschema-scoped form `connections/{catalog}.{schema}.{connection}`. On read,\nthe service populates the resolved connection metadata and preserves a\ndangling source so callers can diagnose a deleted backing connection.", + "properties": { + "name": { + "description": "[Beta] Name of the UC connection that hosts the MCP server, as\n`connections/{catalog}.{schema}.{connection}`.", + "$ref": "#/$defs/string" + } + }, + "additionalProperties": false, + "required": [ + "name" + ] + }, + { + "type": "string", + "pattern": "\\$\\{(var(\\.\\p{L}+([-_]*[\\p{L}\\p{N}]+)*(\\[[0-9]+\\])*)+)\\}" + } + ] + }, + "catalog.ModelProviderServiceConfig": { + "oneOf": [ + { + "type": "object", + "description": "Behavioral configuration for a ModelProviderService: provider connection\n(auth + provider-specific fields), the catalog of models this provider\nservice can route to, and the passthrough policy that governs how request\nheaders, query parameters, and unmanaged subpaths cross the trust boundary\nto the upstream provider.", + "properties": { + "allow_all_targets": { + "description": "[Beta] When true, accepts any model exposed by the upstream provider; `targets`\nis not required and does not restrict routability. When false, only\nmodels listed in `targets` are routable.", + "$ref": "#/$defs/bool" + }, + "amazon_bedrock": { + "description": "[Beta] Amazon Bedrock provider configuration.", + "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/catalog.ModelProviderServiceConfigAmazonBedrockProviderConfig" + }, + "anthropic": { + "description": "[Beta] Anthropic provider configuration. Exactly one of `direct` or `relayed` must\nbe set on Create; the two are mutually exclusive.", + "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/catalog.ModelProviderServiceConfigAnthropicProviderConfig" + }, + "azure_openai": { + "description": "[Beta] Azure OpenAI provider configuration.", + "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/catalog.ModelProviderServiceConfigAzureOpenAiProviderConfig" + }, + "custom": { + "description": "[Beta] Custom provider configuration: arbitrary HTTP endpoint with bearer-token auth.", + "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/catalog.ModelProviderServiceConfigCustomProviderConfig" + }, + "forward_headers": { + "description": "[Beta] Whether to forward incoming request headers to the upstream provider.\nApplies to managed (multi-model) requests as well as passthrough requests\nserved by this provider service. Governance-level decision by the provider\nservice owner; not selectable per inference call.", + "$ref": "#/$defs/bool" + }, + "forward_query_parameters": { + "description": "[Beta] Whether to forward incoming request query parameters to the upstream\nprovider. Same trust-boundary semantics as `forward_headers`.", + "$ref": "#/$defs/bool" + }, + "forward_unmanaged_paths": { + "description": "[Beta] Whether to forward request paths that fall outside this service's managed\nAPI set to the upstream provider as opaque passthrough. When true,\nrequests addressed to subpaths not recognized by the managed API surface\nare proxied to the upstream provider over the same provider connection.\nWhen false, only managed-API paths are served. Governance-level decision\nby the provider service owner; expanding this expands the trust boundary\nthat the ModelProviderService exposes.", + "$ref": "#/$defs/bool" + }, + "gemini_enterprise": { + "description": "[Beta] Gemini Enterprise provider configuration.", + "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/catalog.ModelProviderServiceConfigGeminiEnterpriseProviderConfig" + }, + "inference_table": { + "description": "[Beta] Inference table configuration for payload logging when this provider\nservice is invoked directly. When it is invoked through a model service,\nthe model service's own inference table captures the invocation instead.\nMirrors `ModelServiceConfig.inference_table` /\n`AgentServiceConfig.inference_table`.", + "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/catalog.InferenceTableConfig" + }, + "microsoft_foundry": { + "description": "[Beta] Microsoft Foundry provider configuration.", + "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/catalog.ModelProviderServiceConfigMicrosoftFoundryProviderConfig" + }, + "openai": { + "description": "[Beta] OpenAI provider configuration.", + "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/catalog.ModelProviderServiceConfigOpenAiProviderConfig" + }, + "provider_type": { + "description": "[Beta] Provider type discriminator. Required at create time; immutable after.\nDetermines which variant of the `provider` oneof must be set. May not be\nchanged via Update; attempts to include `config.provider_type` in\n`UpdateModelProviderServiceRequest.update_mask` are rejected.\n\nRequired on CreateModelProviderService and immutable thereafter.", + "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/catalog.ModelProviderServiceConfigExternalModelProviderType" + }, + "rate_limits": { + "description": "[Beta] Rate limits applied when this provider service is invoked directly. When\nit is invoked through a model service, the model service's own\n`rate_limits` apply instead. Mirrors `ModelServiceConfig.rate_limits` /\n`McpServiceConfig.rate_limits`.", + "$ref": "#/$defs/slice/github.com/databricks/databricks-sdk-go/service/catalog.RateLimit" + }, + "targets": { + "description": "[Beta] Routing targets this provider service exposes (provider-side model\nidentifier + unified API types per entry). Required (\u003e=1) when\n`allow_all_targets = false`; optional and additive when\n`allow_all_targets = true`. References from `ExternalModelConfig.target`\nmust match an entry here unless `allow_all_targets = true`.", + "$ref": "#/$defs/slice/github.com/databricks/databricks-sdk-go/service/catalog.ModelProviderServiceConfigModelTargetConfig" + } + }, + "additionalProperties": false + }, + { + "type": "string", + "pattern": "\\$\\{(var(\\.\\p{L}+([-_]*[\\p{L}\\p{N}]+)*(\\[[0-9]+\\])*)+)\\}" + } + ] + }, + "catalog.ModelProviderServiceConfigAmazonBedrockProviderConfig": { + "oneOf": [ + { + "type": "object", + "description": "Amazon Bedrock provider configuration.", + "properties": { + "direct": { + "description": "[Beta] Direct form of Amazon Bedrock provider config.\n\nAuthentication is one of two mutually exclusive modes, exactly one of which\nmust be supplied on Create:\n- Access keys: set `aws_access_key`, leave `service_credential` unset.\n- UC service credential: set `service_credential.name` to the AIP-122\nresource-name form `credentials/{name}`, leave `aws_access_key` unset. The\ncredential value lives in UC and is referenced by name, not held on this\nmessage.\nSetting more than one mode is rejected.", + "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/catalog.ModelProviderServiceConfigAmazonBedrockProviderDirectConfig" + } + }, + "additionalProperties": false + }, + { + "type": "string", + "pattern": "\\$\\{(var(\\.\\p{L}+([-_]*[\\p{L}\\p{N}]+)*(\\[[0-9]+\\])*)+)\\}" + } + ] + }, + "catalog.ModelProviderServiceConfigAmazonBedrockProviderDirectConfig": { + "oneOf": [ + { + "type": "object", + "description": "Direct form of Amazon Bedrock provider config.\n\nAuthentication is one of two mutually exclusive modes, exactly one of which\nmust be supplied on Create:\n- Access keys: set `aws_access_key`, leave `service_credential` unset.\n- UC service credential: set `service_credential.name` to the AIP-122\nresource-name form `credentials/{name}`, leave `aws_access_key` unset. The\ncredential value lives in UC and is referenced by name, not held on this\nmessage.\nSetting more than one mode is rejected.", + "properties": { + "aws_access_key": { + "description": "[Beta] AWS access-key-pair auth. Mutually exclusive with `service_credential`.", + "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/catalog.ModelProviderServiceConfigAwsAccessKey" + }, + "region": { + "description": "[Beta] AWS region where the Bedrock endpoint is hosted (e.g., `us-east-1`).\nRequired on Create.", + "$ref": "#/$defs/string" + }, + "service_credential": { + "description": "[Beta] Reference to a UC service credential authorizing Bedrock requests. On\nCreate the caller supplies `service_credential.name` in the AIP-122\nresource-name form `credentials/{name}`. Required on Create when using\nUC-service-credential auth; mutually exclusive with `aws_access_key`. The\ncredential is referenced by name; its value is not carried here. On read the\nresolved `id` and `is_deleted` are also populated. Only supported on AWS-hosted\nworkspaces; Create requests from other clouds are rejected with\nINVALID_PARAMETER_VALUE.", + "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/catalog.ModelProviderServiceConfigServiceCredential" + } + }, + "additionalProperties": false + }, + { + "type": "string", + "pattern": "\\$\\{(var(\\.\\p{L}+([-_]*[\\p{L}\\p{N}]+)*(\\[[0-9]+\\])*)+)\\}" + } + ] + }, + "catalog.ModelProviderServiceConfigAnthropicProviderConfig": { + "oneOf": [ + { + "type": "object", + "description": "Anthropic provider configuration. Exactly one of `direct` or `relayed` must\nbe set on Create; the two are mutually exclusive.", + "properties": { + "direct": { + "description": "[Beta] Direct (inline-credentials) form: caller supplies the API key in the\nrequest body. Required on Create unless `relayed` is set.", + "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/catalog.ModelProviderServiceConfigAnthropicProviderDirectConfig" + }, + "relayed": { + "description": "[Beta] Relayed (credential-less) form: no Anthropic credential is stored. Each\ninference request instead carries the caller's own OAuth token, which the\nplatform forwards to Anthropic on outbound requests. Mutually exclusive\nwith `direct`; no `api_key` is required or persisted.", + "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/catalog.ModelProviderServiceConfigAnthropicProviderRelayedConfig" + } + }, + "additionalProperties": false + }, + { + "type": "string", + "pattern": "\\$\\{(var(\\.\\p{L}+([-_]*[\\p{L}\\p{N}]+)*(\\[[0-9]+\\])*)+)\\}" + } + ] + }, + "catalog.ModelProviderServiceConfigAnthropicProviderDirectConfig": { + "oneOf": [ + { + "type": "object", + "description": "Direct form of Anthropic provider config.", + "properties": { + "api_key": { + "description": "[Beta] Anthropic API key. Required on Create. Sent as the `x-api-key` header on\noutbound requests. Supplied as inline plaintext via\n`ProviderSecret.plaintext`.", + "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/catalog.ModelProviderServiceConfigProviderSecret" + } + }, + "additionalProperties": false + }, + { + "type": "string", + "pattern": "\\$\\{(var(\\.\\p{L}+([-_]*[\\p{L}\\p{N}]+)*(\\[[0-9]+\\])*)+)\\}" + } + ] + }, + "catalog.ModelProviderServiceConfigAnthropicProviderRelayedConfig": { + "oneOf": [ + { + "type": "object", + "description": "Relayed form of Anthropic provider config: no credential is stored.\nAuthentication is the caller's own OAuth token, forwarded to Anthropic on\noutbound requests, so there is no persisted secret. Presence of this variant\nis the signal that the provider service uses relayed auth; `plan_type`\nfurther distinguishes which Anthropic subscription tier the token belongs to.", + "properties": { + "plan_type": { + "description": "[Beta] Which Anthropic subscription tier the relayed token belongs to. Optional;\nwhen unset the MPS gets the full governance surface (see TEAM_ENTERPRISE).\nImmutable after Create, so the tier cannot be flipped in place.", + "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/catalog.ModelProviderServiceConfigAnthropicProviderRelayedConfigAnthropicRelayedPlanType" + } + }, + "additionalProperties": false + }, + { + "type": "string", + "pattern": "\\$\\{(var(\\.\\p{L}+([-_]*[\\p{L}\\p{N}]+)*(\\[[0-9]+\\])*)+)\\}" + } + ] + }, + "catalog.ModelProviderServiceConfigAnthropicProviderRelayedConfigAnthropicRelayedPlanType": { + "oneOf": [ + { + "type": "string", + "description": "Which Anthropic subscription tier the relayed OAuth token belongs to.\nImmutable after Create (switching tiers changes which governance controls\nthe platform enforces). Only MAX and TEAM_ENTERPRISE differ in the\ngovernance surface the gateway can enforce, not in how the token is\nrelayed.", + "enum": [ + "ANTHROPIC_RELAYED_PLAN_TYPE_MAX", + "ANTHROPIC_RELAYED_PLAN_TYPE_TEAM_ENTERPRISE" + ], + "enumDescriptions": [ + "[Beta]", + "[Beta]" + ] + }, + { + "type": "string", + "pattern": "\\$\\{(var(\\.\\p{L}+([-_]*[\\p{L}\\p{N}]+)*(\\[[0-9]+\\])*)+)\\}" + } + ] + }, + "catalog.ModelProviderServiceConfigAwsAccessKey": { + "oneOf": [ + { + "type": "object", + "description": "AWS access-key-pair auth for Amazon Bedrock: a SigV4-signing key pair.", + "properties": { + "access_key_id": { + "description": "[Beta] AWS access key ID. Required on Create when using access-key auth. Treated as\nusername-equivalent (not a secret value): round-trips on reads and is\nscrubbed from audit logs.", + "$ref": "#/$defs/string" + }, + "secret_access_key": { + "description": "[Beta] AWS secret access key paired with `access_key_id`. Required on Create when\nusing access-key auth. Supplied as inline plaintext via\n`ProviderSecret.plaintext`.", + "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/catalog.ModelProviderServiceConfigProviderSecret" + } + }, + "additionalProperties": false + }, + { + "type": "string", + "pattern": "\\$\\{(var(\\.\\p{L}+([-_]*[\\p{L}\\p{N}]+)*(\\[[0-9]+\\])*)+)\\}" + } + ] + }, + "catalog.ModelProviderServiceConfigAzureOpenAiProviderConfig": { + "oneOf": [ + { + "type": "object", + "description": "Azure OpenAI provider configuration.", + "properties": { + "direct": { + "description": "[Beta] Direct form of Azure OpenAI provider config. Exactly one of three\nmutually-exclusive auth modes must be supplied on Create:\n- API key: set `api_key`, leave `entra_service_principal` and\n`service_credential` unset.\n- Entra ID (service principal): set `entra_service_principal`, leave\n`api_key` and `service_credential` unset.\n- UC service credential: set `service_credential.name` to the AIP-122\nresource-name form `credentials/{name}`, leave `api_key` and\n`entra_service_principal` unset. The credential value lives in UC and is\nreferenced by name, not held on this message. Only supported on\nAzure-hosted workspaces.\nSetting more than one mode is rejected.", + "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/catalog.ModelProviderServiceConfigAzureOpenAiProviderDirectConfig" + } + }, + "additionalProperties": false + }, + { + "type": "string", + "pattern": "\\$\\{(var(\\.\\p{L}+([-_]*[\\p{L}\\p{N}]+)*(\\[[0-9]+\\])*)+)\\}" + } + ] + }, + "catalog.ModelProviderServiceConfigAzureOpenAiProviderDirectConfig": { + "oneOf": [ + { + "type": "object", + "description": "Direct form of Azure OpenAI provider config. Exactly one of three\nmutually-exclusive auth modes must be supplied on Create:\n- API key: set `api_key`, leave `entra_service_principal` and\n`service_credential` unset.\n- Entra ID (service principal): set `entra_service_principal`, leave\n`api_key` and `service_credential` unset.\n- UC service credential: set `service_credential.name` to the AIP-122\nresource-name form `credentials/{name}`, leave `api_key` and\n`entra_service_principal` unset. The credential value lives in UC and is\nreferenced by name, not held on this message. Only supported on\nAzure-hosted workspaces.\nSetting more than one mode is rejected.", + "properties": { + "api_key": { + "description": "[Beta] Azure OpenAI API key. Mutually exclusive with the Entra and\nservice-credential modes. Supplied as inline plaintext via\n`ProviderSecret.plaintext`.", + "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/catalog.ModelProviderServiceConfigProviderSecret" + }, + "base_url": { + "description": "[Beta] Full Azure OpenAI endpoint base URL, e.g.\n`https://myresource.openai.azure.com`. Required on Create.", + "$ref": "#/$defs/string" + }, + "entra_service_principal": { + "description": "[Beta] Entra ID (service principal) auth. Mutually exclusive with `api_key` and\n`service_credential`.", + "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/catalog.ModelProviderServiceConfigEntraServicePrincipal" + }, + "service_credential": { + "description": "[Beta] Reference to a UC service credential authorizing Azure OpenAI requests. On\nCreate the caller supplies `service_credential.name` in the AIP-122\nresource-name form `credentials/{name}`. Required on Create when using\nUC-service-credential auth; mutually exclusive with `api_key` and\n`entra_service_principal`. The credential is\nreferenced by name; its value is not carried here. On read the resolved `id`\nand `is_deleted` are also populated. Only supported on Azure-hosted\nworkspaces; Create requests from other clouds are rejected with\nINVALID_PARAMETER_VALUE.", + "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/catalog.ModelProviderServiceConfigServiceCredential" + } + }, + "additionalProperties": false + }, + { + "type": "string", + "pattern": "\\$\\{(var(\\.\\p{L}+([-_]*[\\p{L}\\p{N}]+)*(\\[[0-9]+\\])*)+)\\}" + } + ] + }, + "catalog.ModelProviderServiceConfigCustomProviderConfig": { + "oneOf": [ + { + "type": "object", + "description": "Custom provider configuration: arbitrary HTTP endpoint with bearer-token auth.", + "properties": { + "direct": { + "description": "[Beta] Direct form of custom provider config.\n\nAuthentication is one of two mutually exclusive modes, exactly one of which\nmust be supplied on Create:\n- Bearer: set `api_key`, leave `header_auth` unset. The secret is\nforwarded as `Authorization: Bearer \u003csecret\u003e`.\n- Header: set `header_auth`, leave `api_key` unset. The secret is\nforwarded as `\u003capi_key_name\u003e: \u003capi_key_value\u003e`.\nSetting both modes or neither mode is rejected.", + "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/catalog.ModelProviderServiceConfigCustomProviderDirectConfig" + } + }, + "additionalProperties": false + }, + { + "type": "string", + "pattern": "\\$\\{(var(\\.\\p{L}+([-_]*[\\p{L}\\p{N}]+)*(\\[[0-9]+\\])*)+)\\}" + } + ] + }, + "catalog.ModelProviderServiceConfigCustomProviderDirectConfig": { + "oneOf": [ + { + "type": "object", + "description": "Direct form of custom provider config.\n\nAuthentication is one of two mutually exclusive modes, exactly one of which\nmust be supplied on Create:\n- Bearer: set `api_key`, leave `header_auth` unset. The secret is\nforwarded as `Authorization: Bearer \u003csecret\u003e`.\n- Header: set `header_auth`, leave `api_key` unset. The secret is\nforwarded as `\u003capi_key_name\u003e: \u003capi_key_value\u003e`.\nSetting both modes or neither mode is rejected.", + "properties": { + "api_key": { + "description": "[Beta] Bearer token forwarded as the `Authorization: Bearer ...` header on\noutbound requests. Supplied as inline plaintext via\n`ProviderSecret.plaintext`. Set this for bearer-token auth.", + "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/catalog.ModelProviderServiceConfigProviderSecret" + }, + "base_url": { + "description": "[Beta] Endpoint URL of the OpenAI-compatible service (e.g.,\n`https://api.example.com/v1`). Required on Create.", + "$ref": "#/$defs/string" + } + }, + "additionalProperties": false + }, + { + "type": "string", + "pattern": "\\$\\{(var(\\.\\p{L}+([-_]*[\\p{L}\\p{N}]+)*(\\[[0-9]+\\])*)+)\\}" + } + ] + }, + "catalog.ModelProviderServiceConfigEntraServicePrincipal": { + "oneOf": [ + { + "type": "object", + "description": "Entra ID (Azure AD) service-principal auth: AI Gateway exchanges the\n`tenant_id` + `client_id` identify the service principal, and the `credential`\noneof proves that identity, exchanged for an Entra bearer token on outbound\nrequests via the OAuth2 client-credentials grant. Shared by the Azure OpenAI\nand Microsoft Foundry provider configs.", + "properties": { + "client_id": { + "description": "[Beta] Entra ID client (application) ID. Required on Create.", + "$ref": "#/$defs/string" + }, + "client_secret": { + "description": "[Beta] Entra ID client secret. Supplied as inline plaintext via\n`ProviderSecret.plaintext`.", + "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/catalog.ModelProviderServiceConfigProviderSecret" + }, + "tenant_id": { + "description": "[Beta] Entra ID (Azure AD) tenant ID. Required on Create.", + "$ref": "#/$defs/string" + } + }, + "additionalProperties": false + }, + { + "type": "string", + "pattern": "\\$\\{(var(\\.\\p{L}+([-_]*[\\p{L}\\p{N}]+)*(\\[[0-9]+\\])*)+)\\}" + } + ] + }, + "catalog.ModelProviderServiceConfigExternalModelProviderType": { + "oneOf": [ + { + "type": "string", + "description": "External LLM provider for an EXTERNAL_FOUNDATION_MODEL destination.", + "enum": [ + "EXTERNAL_MODEL_PROVIDER_TYPE_OPENAI", + "EXTERNAL_MODEL_PROVIDER_TYPE_AZURE_OPENAI", + "EXTERNAL_MODEL_PROVIDER_TYPE_ANTHROPIC", + "EXTERNAL_MODEL_PROVIDER_TYPE_AMAZON_BEDROCK", + "EXTERNAL_MODEL_PROVIDER_TYPE_CUSTOM", + "EXTERNAL_MODEL_PROVIDER_TYPE_MICROSOFT_FOUNDRY", + "EXTERNAL_MODEL_PROVIDER_TYPE_GEMINI_ENTERPRISE" + ], + "enumDescriptions": [ + "[Beta]", + "[Beta]", + "[Beta]", + "[Beta]", + "[Beta]", + "[Beta]", + "[Beta]" + ] + }, + { + "type": "string", + "pattern": "\\$\\{(var(\\.\\p{L}+([-_]*[\\p{L}\\p{N}]+)*(\\[[0-9]+\\])*)+)\\}" + } + ] + }, + "catalog.ModelProviderServiceConfigGeminiEnterpriseProviderConfig": { + "oneOf": [ + { + "type": "object", + "description": "Gemini Enterprise provider configuration.", + "properties": { + "direct": { + "description": "[Beta] Direct form of Gemini Enterprise provider config.\n\nAuthentication is one of two mutually exclusive modes; exactly one must be\nsupplied on Create:\n- API key: set `api_key`, leave `service_credential` unset.\n- UC service credential: set `service_credential`, leave `api_key` unset.", + "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/catalog.ModelProviderServiceConfigGeminiEnterpriseProviderDirectConfig" + } + }, + "additionalProperties": false + }, + { + "type": "string", + "pattern": "\\$\\{(var(\\.\\p{L}+([-_]*[\\p{L}\\p{N}]+)*(\\[[0-9]+\\])*)+)\\}" + } + ] + }, + "catalog.ModelProviderServiceConfigGeminiEnterpriseProviderDirectConfig": { + "oneOf": [ + { + "type": "object", + "description": "Direct form of Gemini Enterprise provider config.\n\nAuthentication is one of two mutually exclusive modes; exactly one must be\nsupplied on Create:\n- API key: set `api_key`, leave `service_credential` unset.\n- UC service credential: set `service_credential`, leave `api_key` unset.", + "properties": { + "api_key": { + "description": "[Beta] Google Gemini Enterprise API key. Required on Create when using API-key\nauth; mutually exclusive with `service_credential`. Supplied as inline\nplaintext via `ProviderSecret.plaintext`.", + "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/catalog.ModelProviderServiceConfigProviderSecret" + }, + "project_id": { + "description": "[Beta] GCP project ID hosting the Gemini Enterprise endpoint. Required on Create.", + "$ref": "#/$defs/string" + }, + "region": { + "description": "[Beta] GCP region of the Gemini Enterprise endpoint (e.g., `us-central1`).\nRequired on Create.", + "$ref": "#/$defs/string" + } + }, + "additionalProperties": false + }, + { + "type": "string", + "pattern": "\\$\\{(var(\\.\\p{L}+([-_]*[\\p{L}\\p{N}]+)*(\\[[0-9]+\\])*)+)\\}" + } + ] + }, + "catalog.ModelProviderServiceConfigMicrosoftFoundryProviderConfig": { + "oneOf": [ + { + "type": "object", + "description": "Microsoft Foundry provider configuration.", + "properties": { + "direct": { + "description": "[Beta] Direct form of Microsoft Foundry provider config.\n\nAuthentication is one of three mutually exclusive modes, exactly one of which\nmust be supplied on Create:\n- API key: set `api_key`, leave `entra_service_principal` and\n`service_credential` unset.\n- Entra ID (service principal): set `entra_service_principal`, leave\n`api_key` and `service_credential` unset. AI Gateway exchanges these for\nan Entra bearer token on outbound requests via the OAuth2\nclient-credentials grant.\n- UC service credential: set `service_credential.name` to the AIP-122\nresource-name form `credentials/{name}`, leave `api_key` and\n`entra_service_principal` unset. The credential value lives in UC and is\nreferenced by name, not held on this message. Only supported on\nAzure-hosted workspaces.\nSetting more than one mode is rejected.", + "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/catalog.ModelProviderServiceConfigMicrosoftFoundryProviderDirectConfig" + } + }, + "additionalProperties": false + }, + { + "type": "string", + "pattern": "\\$\\{(var(\\.\\p{L}+([-_]*[\\p{L}\\p{N}]+)*(\\[[0-9]+\\])*)+)\\}" + } + ] + }, + "catalog.ModelProviderServiceConfigMicrosoftFoundryProviderDirectConfig": { + "oneOf": [ + { + "type": "object", + "description": "Direct form of Microsoft Foundry provider config.\n\nAuthentication is one of three mutually exclusive modes, exactly one of which\nmust be supplied on Create:\n- API key: set `api_key`, leave `entra_service_principal` and\n`service_credential` unset.\n- Entra ID (service principal): set `entra_service_principal`, leave\n`api_key` and `service_credential` unset. AI Gateway exchanges these for\nan Entra bearer token on outbound requests via the OAuth2\nclient-credentials grant.\n- UC service credential: set `service_credential.name` to the AIP-122\nresource-name form `credentials/{name}`, leave `api_key` and\n`entra_service_principal` unset. The credential value lives in UC and is\nreferenced by name, not held on this message. Only supported on\nAzure-hosted workspaces.\nSetting more than one mode is rejected.", + "properties": { + "api_key": { + "description": "[Beta] Microsoft AI Foundry API key. Mutually exclusive with the Entra and\nservice-credential modes. Supplied as inline plaintext via\n`ProviderSecret.plaintext`.", + "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/catalog.ModelProviderServiceConfigProviderSecret" + }, + "base_url": { + "description": "[Beta] Microsoft AI Foundry endpoint URL. Required on Create.", + "$ref": "#/$defs/string" + }, + "entra_service_principal": { + "description": "[Beta] Entra ID (service principal) auth. Mutually exclusive with `api_key` and\n`service_credential`.", + "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/catalog.ModelProviderServiceConfigEntraServicePrincipal" + }, + "service_credential": { + "description": "[Beta] Reference to a UC service credential authorizing Microsoft Foundry requests.\nOn Create the caller supplies `service_credential.name` in the AIP-122\nresource-name form `credentials/{name}`. Required on Create when using\nUC-service-credential auth; mutually exclusive with `api_key` and\n`entra_service_principal`. The credential is\nreferenced by name; its value is not carried here. On read the resolved `id`\nand `is_deleted` are also populated. Only supported on Azure-hosted\nworkspaces; Create requests from other clouds are rejected with\nINVALID_PARAMETER_VALUE.", + "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/catalog.ModelProviderServiceConfigServiceCredential" + } + }, + "additionalProperties": false + }, + { + "type": "string", + "pattern": "\\$\\{(var(\\.\\p{L}+([-_]*[\\p{L}\\p{N}]+)*(\\[[0-9]+\\])*)+)\\}" + } + ] + }, + "catalog.ModelProviderServiceConfigModelTargetConfig": { + "oneOf": [ + { + "type": "object", + "description": "Model target configuration for an external model destination.", + "properties": { + "model": { + "description": "[Beta] Provider-side model identifier (e.g. \"gpt-5\", \"claude-opus-4-7\"). This is\na string on the LLM provider's side, not a UC entity. The UC governance\nhook for external destinations is the ModelProviderService referenced by\n`ExternalModelConfig.model_provider_service`, not the model itself.", + "$ref": "#/$defs/string" + }, + "native_api_types": { + "description": "[Beta] Provider-native API types the model supports (e.g.\n\"openai/v1/chat/completions\"). Used by the platform for request/response\ntranslation from the unified API type. At most 64 entries of at most 256\ncharacters each; the list is persisted into the destination binding's\nbounded storage envelope.", + "$ref": "#/$defs/slice/string" + } + }, + "additionalProperties": false, + "required": [ + "model" + ] + }, + { + "type": "string", + "pattern": "\\$\\{(var(\\.\\p{L}+([-_]*[\\p{L}\\p{N}]+)*(\\[[0-9]+\\])*)+)\\}" + } + ] + }, + "catalog.ModelProviderServiceConfigOpenAiProviderConfig": { + "oneOf": [ + { + "type": "object", + "description": "OpenAI provider configuration.", + "properties": { + "direct": { + "description": "[Beta] Direct (inline-credentials) form of the OpenAI provider config.", + "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/catalog.ModelProviderServiceConfigOpenAiProviderDirectConfig" + } + }, + "additionalProperties": false + }, + { + "type": "string", + "pattern": "\\$\\{(var(\\.\\p{L}+([-_]*[\\p{L}\\p{N}]+)*(\\[[0-9]+\\])*)+)\\}" + } + ] + }, + "catalog.ModelProviderServiceConfigOpenAiProviderDirectConfig": { + "oneOf": [ + { + "type": "object", + "description": "Direct (inline-credentials) form of the OpenAI provider config.", + "properties": { + "api_key": { + "description": "[Beta] OpenAI API key. Required on Create. Supplied as inline plaintext via\n`ProviderSecret.plaintext`.", + "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/catalog.ModelProviderServiceConfigProviderSecret" + }, + "base_url": { + "description": "[Beta] Optional custom base URL. Defaults to `https://api.openai.com/v1`. Use for\nOpenAI-API-compatible third-party endpoints or in-network proxies.", + "$ref": "#/$defs/string" + }, + "organization": { + "description": "[Beta] Optional OpenAI organization ID. When set, the platform forwards it as\nthe `OpenAI-Organization` header.", + "$ref": "#/$defs/string" + } + }, + "additionalProperties": false + }, + { + "type": "string", + "pattern": "\\$\\{(var(\\.\\p{L}+([-_]*[\\p{L}\\p{N}]+)*(\\[[0-9]+\\])*)+)\\}" + } + ] + }, + "catalog.ModelProviderServiceConfigProviderSecret": { + "oneOf": [ + { + "type": "object", + "description": "A secret value supplied as part of an inline provider config. The caller\nsupplies the value as inline `plaintext` on writes; the platform stores it\nencrypted. The `plaintext` field is `INPUT_ONLY` and never round-trips on\nreads.", + "properties": { + "plaintext": { + "description": "[Beta] Inline plaintext credential. INPUT_ONLY: the value never round-trips on\nreads. Get and List responses omit `plaintext`; the field's presence in\nthe read shape only indicates that a secret is configured.", + "$ref": "#/$defs/string" + } + }, + "additionalProperties": false + }, + { + "type": "string", + "pattern": "\\$\\{(var(\\.\\p{L}+([-_]*[\\p{L}\\p{N}]+)*(\\[[0-9]+\\])*)+)\\}" + } + ] + }, + "catalog.ModelProviderServiceConfigServiceCredential": { + "oneOf": [ + { + "type": "object", + "description": "---- Provider configuration (nested; see the `provider` oneof below) ----\nThe customer-owned UC service credential a ModelProviderService uses to\nauthenticate to its provider, referenced by name.", + "properties": { + "name": { + "description": "[Beta] Resource name of the bound UC service credential, in the AIP-122 form\n`credentials/{name}` (a metastore-level single-part credential name). On\ncreate the caller supplies the name here. On read it reflects the\ncredential's current name at read time.", + "$ref": "#/$defs/string" + } + }, + "additionalProperties": false, + "required": [ + "name" + ] + }, + { + "type": "string", + "pattern": "\\$\\{(var(\\.\\p{L}+([-_]*[\\p{L}\\p{N}]+)*(\\[[0-9]+\\])*)+)\\}" + } + ] + }, + "catalog.ModelServiceConfig": { "oneOf": [ { "type": "object", - "description": "A single telemetry export destination with its configuration and status.", + "description": "Operational configuration wrapped around the ModelService resource.", "properties": { - "unity_catalog": { - "description": "[Public Preview] Unity Catalog Destinations for OTEL telemetry export.", - "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/apps.UnityCatalog" + "inference_table": { + "description": "[Beta] Inference table config for payload logging.", + "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/catalog.InferenceTableConfig" + }, + "rate_limits": { + "description": "[Beta] Rate limits applied to requests routed through this model service.", + "$ref": "#/$defs/slice/github.com/databricks/databricks-sdk-go/service/catalog.RateLimit" + }, + "routing": { + "description": "[Beta] Routing configuration: destinations, routing strategy, and fallback.", + "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/catalog.ModelServiceConfigRoutingConfig" } }, "additionalProperties": false @@ -4668,30 +5663,41 @@ } ] }, - "apps.UnityCatalog": { + "catalog.ModelServiceConfigDestinationConfig": { "oneOf": [ { "type": "object", - "description": "Unity Catalog Destinations for OTEL telemetry export.", + "description": "A destination the model service can route traffic to. Exactly one of the\nper-type configs inside `type_config` must be set, and it must match\n`destination_type`.", "properties": { - "logs_table": { - "description": "[Public Preview] Unity Catalog table for OTEL logs.", - "$ref": "#/$defs/string" + "destination_type": { + "description": "[Beta] Backing-model category. Determines which oneof variant is populated.", + "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/catalog.ModelServiceConfigDestinationConfigDestinationType" }, - "metrics_table": { - "description": "[Public Preview] Unity Catalog table for OTEL metrics.", - "$ref": "#/$defs/string" + "external_model_config": { + "description": "[Beta] Configuration for an external-foundation-model destination. Provider auth\nand provider-specific cloud configuration are owned by a separate, governed\nModelProviderService entity referenced via `model_provider_service`; the\nplatform resolves the provider at invocation time.", + "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/catalog.ModelServiceConfigExternalModelConfig" }, - "traces_table": { - "description": "[Public Preview] Unity Catalog table for OTEL traces (spans).", + "name": { + "description": "[Beta] User-facing label for this destination, used in routing references.", "$ref": "#/$defs/string" + }, + "pay_per_token_config": { + "description": "[Beta] Configuration for a pay-per-token foundation-model destination. Identifies\nthe foundation model by its UC resource name; the platform resolves it to a\nModel Serving endpoint at request time.", + "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/catalog.ModelServiceConfigPayPerTokenConfig" + }, + "provisioned_throughput_config": { + "description": "[Beta] Configuration for a provisioned-throughput foundation-model destination.\nReferences a pre-existing Model Serving endpoint that serves the model;\nsizing (provisioned throughput, burst scaling, model version) is owned by\nthe Model Serving endpoint itself, not by this message.", + "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/catalog.ModelServiceConfigProvisionedThroughputConfig" + }, + "traffic_percentage": { + "description": "[Beta] Share of traffic sent to this destination, 0-100. Optional on fallback\ndestinations; see FallbackConfig.", + "$ref": "#/$defs/int" } }, "additionalProperties": false, "required": [ - "logs_table", - "metrics_table", - "traces_table" + "destination_type", + "name" ] }, { @@ -4700,17 +5706,21 @@ } ] }, - "catalog.AwsSqsQueue": { + "catalog.ModelServiceConfigDestinationConfigDestinationType": { "oneOf": [ { - "type": "object", - "properties": { - "queue_url": { - "description": "The AQS queue url in the format https://sqs.{region}.amazonaws.com/{account id}/{queue name}.\nOnly required for provided_sqs.", - "$ref": "#/$defs/string" - } - }, - "additionalProperties": false + "type": "string", + "description": "Backing-model category for a model service destination.", + "enum": [ + "DESTINATION_TYPE_PAY_PER_TOKEN_FOUNDATION_MODEL", + "DESTINATION_TYPE_PROVISIONED_THROUGHPUT_FOUNDATION_MODEL", + "DESTINATION_TYPE_EXTERNAL_FOUNDATION_MODEL" + ], + "enumDescriptions": [ + "[Beta]", + "[Beta]", + "[Beta]" + ] }, { "type": "string", @@ -4718,24 +5728,25 @@ } ] }, - "catalog.AzureEncryptionSettings": { + "catalog.ModelServiceConfigExternalModelConfig": { "oneOf": [ { "type": "object", + "description": "Configuration for an external-foundation-model destination. Provider auth\nand provider-specific cloud configuration are owned by a separate, governed\nModelProviderService entity referenced via `model_provider_service`; the\nplatform resolves the provider at invocation time.", "properties": { - "azure_cmk_access_connector_id": { - "$ref": "#/$defs/string" - }, - "azure_cmk_managed_identity_id": { + "model_provider_service": { + "description": "[Beta] Resource name of the governed ModelProviderService that owns provider\nauth and provider-specific configuration. The referenced\nModelProviderService also carries the provider type, so this message\ndoes not surface it directly.\nFormat: `model-provider-services/{catalog}.{schema}.{model_provider_service}`.\nEach `{...}` component is capped at 255 characters individually.", "$ref": "#/$defs/string" }, - "azure_tenant_id": { - "$ref": "#/$defs/string" + "target": { + "description": "[Beta] Routing target for the destination: the provider-side model selected from\nthe referenced ModelProviderService's `targets` catalog, plus the unified\nAPI types the platform should translate to/from at request time.", + "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/catalog.ModelProviderServiceConfigModelTargetConfig" } }, "additionalProperties": false, "required": [ - "azure_tenant_id" + "model_provider_service", + "target" ] }, { @@ -4744,22 +5755,15 @@ } ] }, - "catalog.AzureQueueStorage": { + "catalog.ModelServiceConfigFallbackConfig": { "oneOf": [ { "type": "object", + "description": "Fallback routing, applied after the primary destination returns a retryable\nerror. Traversal is in list order; the attempt count is the length of the\nlist.", "properties": { - "queue_url": { - "description": "The AQS queue url in the format https://{storage account}.queue.core.windows.net/{queue name}\nOnly required for provided_aqs.", - "$ref": "#/$defs/string" - }, - "resource_group": { - "description": "Optional resource group for the queue, event grid subscription, and external location storage\naccount.\nOnly required for locations with a service principal storage credential", - "$ref": "#/$defs/string" - }, - "subscription_id": { - "description": "Optional subscription id for the queue, event grid subscription, and external location storage\naccount.\nRequired for locations with a service principal storage credential", - "$ref": "#/$defs/string" + "destinations": { + "description": "[Beta] Ordered list of fallback destinations. Traversal is in list order; the\nattempt count is the length of the list. At most 5 are allowed.", + "$ref": "#/$defs/slice/github.com/databricks/databricks-sdk-go/service/catalog.ModelServiceConfigDestinationConfig" } }, "additionalProperties": false @@ -4770,18 +5774,21 @@ } ] }, - "catalog.EncryptionDetails": { + "catalog.ModelServiceConfigPayPerTokenConfig": { "oneOf": [ { "type": "object", - "description": "Encryption options that apply to clients connecting to cloud storage.", + "description": "Configuration for a pay-per-token foundation-model destination. Identifies\nthe foundation model by its UC resource name; the platform resolves it to a\nModel Serving endpoint at request time.", "properties": { - "sse_encryption_details": { - "description": "Server-Side Encryption properties for clients communicating with AWS s3.", - "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/catalog.SseEncryptionDetails" + "model": { + "description": "[Beta] Resource name of the UC model.\nFormat: `models/{catalog}.{schema}.{model}`.", + "$ref": "#/$defs/string" } }, - "additionalProperties": false + "additionalProperties": false, + "required": [ + "model" + ] }, { "type": "string", @@ -4789,26 +5796,21 @@ } ] }, - "catalog.EncryptionSettings": { + "catalog.ModelServiceConfigProvisionedThroughputConfig": { "oneOf": [ { "type": "object", - "description": "Encryption Settings are used to carry metadata for securable encryption at rest.\nCurrently used for catalogs, we can use the information supplied here to interact with a CMK.", + "description": "Configuration for a provisioned-throughput foundation-model destination.\nReferences a pre-existing Model Serving endpoint that serves the model;\nsizing (provisioned throughput, burst scaling, model version) is owned by\nthe Model Serving endpoint itself, not by this message.", "properties": { - "azure_encryption_settings": { - "description": "optional Azure settings - only required if an Azure CMK is used.", - "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/catalog.AzureEncryptionSettings" - }, - "azure_key_vault_key_id": { - "description": "the AKV URL in Azure, null otherwise.", - "$ref": "#/$defs/string" - }, - "customer_managed_key_id": { - "description": "the CMK uuid in AWS and GCP, null otherwise.", + "model_serving_endpoint": { + "description": "[Beta] Name of the backing Model Serving endpoint serving the provisioned-\nthroughput foundation model, as the AIP-122 typed resource name\n`serving-endpoints/{name}`. The same UC model can be served on multiple\nModel Serving endpoints (different throughput / region / config); the\ncaller picks which one this destination routes to. The endpoint must\nexist at create time.", "$ref": "#/$defs/string" } }, - "additionalProperties": false + "additionalProperties": false, + "required": [ + "model_serving_endpoint" + ] }, { "type": "string", @@ -4816,28 +5818,27 @@ } ] }, - "catalog.FileEventQueue": { + "catalog.ModelServiceConfigRoutingConfig": { "oneOf": [ { "type": "object", + "description": "Routing configuration for a model service, nesting destinations, routing\nstrategy, and fallback under a single sub-message.", "properties": { - "managed_aqs": { - "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/catalog.AzureQueueStorage" - }, - "managed_pubsub": { - "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/catalog.GcpPubsub" - }, - "managed_sqs": { - "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/catalog.AwsSqsQueue" + "destinations": { + "description": "[Beta] Primary routing destinations. At most 10 are allowed. At least one is\nrequired on CreateModelService; on UpdateModelService it is required only\nwhen `config.routing` (or a `config.routing.*` subpath) appears in\n`update_mask`.", + "$ref": "#/$defs/slice/github.com/databricks/databricks-sdk-go/service/catalog.ModelServiceConfigDestinationConfig" }, - "provided_aqs": { - "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/catalog.AzureQueueStorage" + "fallback": { + "description": "[Beta] Fallback routing config, applied after primary destinations fail.", + "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/catalog.ModelServiceConfigFallbackConfig" }, - "provided_pubsub": { - "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/catalog.GcpPubsub" + "first_token_timeout": { + "description": "[Beta] Timeout for the first token of a streaming response. If a destination does\nnot return its first token within this duration, AI Gateway aborts the\nattempt and fails over to the next destination. Applies to streaming\nrequests only. Leave unset for no first-token timeout.", + "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/common/types/duration.Duration" }, - "provided_sqs": { - "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/catalog.AwsSqsQueue" + "traffic_splitting": { + "description": "[Beta] Marker message selecting request-based traffic splitting. Traffic is\ndistributed according to each destination's traffic_percentage value;\nno configuration lives on this message itself.", + "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/catalog.ModelServiceConfigRoutingConfigTrafficSplitting" } }, "additionalProperties": false @@ -4848,16 +5849,11 @@ } ] }, - "catalog.GcpPubsub": { + "catalog.ModelServiceConfigRoutingConfigTrafficSplitting": { "oneOf": [ { "type": "object", - "properties": { - "subscription_name": { - "description": "The Pub/Sub subscription name in the format projects/{project}/subscriptions/{subscription name}.\nOnly required for provided_pubsub.", - "$ref": "#/$defs/string" - } - }, + "description": "Marker message selecting request-based traffic splitting across primary\ndestinations. Split weights are read from each DestinationConfig.traffic_percentage.", "additionalProperties": false }, { @@ -5225,6 +6221,101 @@ } ] }, + "catalog.RateLimit": { + "oneOf": [ + { + "type": "object", + "description": "A rate limit applied to service requests. Leave `requests` or `tokens`\nunset to impose no limit on that dimension; set a value to cap that dimension\nwithin the renewal period.", + "properties": { + "key": { + "description": "[Beta] Scope key. Determines whether `principal` is required.", + "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/catalog.RateLimitRateLimitKey" + }, + "principal": { + "description": "[Beta] Principal this limit applies to: user email, group name, or service\nprincipal application ID. Required unless `key` is\n`RATE_LIMIT_KEY_SERVICE`, `RATE_LIMIT_KEY_USER_DEFAULT`, or\n`RATE_LIMIT_KEY_REQUEST_TAG` (which must not set a principal).", + "$ref": "#/$defs/string" + }, + "renewal_period": { + "description": "[Beta] Renewal period.", + "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/catalog.RateLimitRateLimitRenewalPeriod" + }, + "request_tag_key": { + "description": "[Beta] Request tag key this limit applies to. Required when `key` is\n`RATE_LIMIT_KEY_REQUEST_TAG`, forbidden otherwise.", + "$ref": "#/$defs/string" + }, + "request_tag_value": { + "description": "[Beta] Request tag value this limit applies to. Only valid when `key` is\n`RATE_LIMIT_KEY_REQUEST_TAG`. Leave unset to apply the limit to every\nvalue of `request_tag_key` (an any-value default); a set value is a\nspecific override for that value.", + "$ref": "#/$defs/string" + }, + "requests": { + "description": "[Beta] Max requests allowed within a renewal period. Leave unset for no request limit.", + "$ref": "#/$defs/int64" + }, + "tokens": { + "description": "[Beta] Max tokens allowed within a renewal period. Leave unset for no token limit.", + "$ref": "#/$defs/int64" + } + }, + "additionalProperties": false, + "required": [ + "key", + "renewal_period" + ] + }, + { + "type": "string", + "pattern": "\\$\\{(var(\\.\\p{L}+([-_]*[\\p{L}\\p{N}]+)*(\\[[0-9]+\\])*)+)\\}" + } + ] + }, + "catalog.RateLimitRateLimitKey": { + "oneOf": [ + { + "type": "string", + "description": "Scope key for a rate limit.", + "enum": [ + "RATE_LIMIT_KEY_USER", + "RATE_LIMIT_KEY_USER_GROUP", + "RATE_LIMIT_KEY_SERVICE_PRINCIPAL", + "RATE_LIMIT_KEY_SERVICE", + "RATE_LIMIT_KEY_USER_DEFAULT", + "RATE_LIMIT_KEY_REQUEST_TAG" + ], + "enumDescriptions": [ + "[Beta]", + "[Beta]", + "[Beta]", + "[Beta]", + "[Beta]", + "[Beta]" + ] + }, + { + "type": "string", + "pattern": "\\$\\{(var(\\.\\p{L}+([-_]*[\\p{L}\\p{N}]+)*(\\[[0-9]+\\])*)+)\\}" + } + ] + }, + "catalog.RateLimitRateLimitRenewalPeriod": { + "oneOf": [ + { + "type": "string", + "description": "Renewal period for a rate limit.", + "enum": [ + "RATE_LIMIT_RENEWAL_PERIOD_MINUTE", + "RATE_LIMIT_RENEWAL_PERIOD_HOUR" + ], + "enumDescriptions": [ + "[Beta]", + "[Beta]" + ] + }, + { + "type": "string", + "pattern": "\\$\\{(var(\\.\\p{L}+([-_]*[\\p{L}\\p{N}]+)*(\\[[0-9]+\\])*)+)\\}" + } + ] + }, "catalog.RegisteredModelAlias": { "oneOf": [ { @@ -15240,6 +16331,20 @@ } ] }, + "resources.McpService": { + "oneOf": [ + { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/github.com/databricks/cli/bundle/config/resources.McpService" + } + }, + { + "type": "string", + "pattern": "\\$\\{(var(\\.\\p{L}+([-_]*[\\p{L}\\p{N}]+)*(\\[[0-9]+\\])*)+)\\}" + } + ] + }, "resources.MlflowExperiment": { "oneOf": [ { @@ -15268,6 +16373,34 @@ } ] }, + "resources.ModelProviderService": { + "oneOf": [ + { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/github.com/databricks/cli/bundle/config/resources.ModelProviderService" + } + }, + { + "type": "string", + "pattern": "\\$\\{(var(\\.\\p{L}+([-_]*[\\p{L}\\p{N}]+)*(\\[[0-9]+\\])*)+)\\}" + } + ] + }, + "resources.ModelService": { + "oneOf": [ + { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/github.com/databricks/cli/bundle/config/resources.ModelService" + } + }, + { + "type": "string", + "pattern": "\\$\\{(var(\\.\\p{L}+([-_]*[\\p{L}\\p{N}]+)*(\\[[0-9]+\\])*)+)\\}" + } + ] + }, "resources.ModelServingEndpoint": { "oneOf": [ { @@ -15915,6 +17048,34 @@ } ] }, + "catalog.ModelProviderServiceConfigModelTargetConfig": { + "oneOf": [ + { + "type": "array", + "items": { + "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/catalog.ModelProviderServiceConfigModelTargetConfig" + } + }, + { + "type": "string", + "pattern": "\\$\\{(var(\\.\\p{L}+([-_]*[\\p{L}\\p{N}]+)*(\\[[0-9]+\\])*)+)\\}" + } + ] + }, + "catalog.ModelServiceConfigDestinationConfig": { + "oneOf": [ + { + "type": "array", + "items": { + "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/catalog.ModelServiceConfigDestinationConfig" + } + }, + { + "type": "string", + "pattern": "\\$\\{(var(\\.\\p{L}+([-_]*[\\p{L}\\p{N}]+)*(\\[[0-9]+\\])*)+)\\}" + } + ] + }, "catalog.MonitorMetric": { "oneOf": [ { @@ -15957,6 +17118,20 @@ } ] }, + "catalog.RateLimit": { + "oneOf": [ + { + "type": "array", + "items": { + "$ref": "#/$defs/github.com/databricks/databricks-sdk-go/service/catalog.RateLimit" + } + }, + { + "type": "string", + "pattern": "\\$\\{(var(\\.\\p{L}+([-_]*[\\p{L}\\p{N}]+)*(\\[[0-9]+\\])*)+)\\}" + } + ] + }, "catalog.RegisteredModelAlias": { "oneOf": [ { diff --git a/libs/testserver/fake_workspace.go b/libs/testserver/fake_workspace.go index 2f077b1b63..51fdc751c8 100644 --- a/libs/testserver/fake_workspace.go +++ b/libs/testserver/fake_workspace.go @@ -204,6 +204,9 @@ type FakeWorkspace struct { Catalogs map[string]catalog.CatalogInfo ExternalLocations map[string]catalog.ExternalLocationInfo RegisteredModels map[string]catalog.RegisteredModelInfo + ModelServices map[string]catalog.ModelService + McpServices map[string]catalog.McpService + ModelProviderServices map[string]catalog.ModelProviderService ServingEndpoints map[string]serving.ServingEndpointDetailed VectorSearchEndpoints map[string]vectorsearch.EndpointInfo VectorSearchIndexes map[string]fakeVectorSearchIndex @@ -449,23 +452,26 @@ func NewFakeWorkspace(url, token string) *FakeWorkspace { files: make(map[string]FileEntry), repoIdByPath: make(map[string]int64), - Jobs: map[int64]jobs.Job{}, - JobRuns: map[int64]jobs.Run{}, - JobRunOutputs: map[int64]jobs.RunOutput{}, - JobRunIdempotency: map[string]int64{}, - Grants: map[string][]catalog.PrivilegeAssignment{}, - Pipelines: map[string]pipelines.GetPipelineResponse{}, - PipelineUpdates: map[string]bool{}, - Monitors: map[string]catalog.MonitorInfo{}, - Apps: map[string]apps.App{}, - Catalogs: map[string]catalog.CatalogInfo{}, - ExternalLocations: map[string]catalog.ExternalLocationInfo{}, - Schemas: map[string]catalog.SchemaInfo{}, - RegisteredModels: map[string]catalog.RegisteredModelInfo{}, - Volumes: map[string]catalog.VolumeInfo{}, - Dashboards: NewEventualMap[string, *fakeDashboard](strings.HasPrefix(token, EventualConsistencyTokenPrefix)), - PublishedDashboards: map[string]dashboards.PublishedDashboard{}, - GenieSpaces: map[string]dashboards.GenieSpace{}, + Jobs: map[int64]jobs.Job{}, + JobRuns: map[int64]jobs.Run{}, + JobRunOutputs: map[int64]jobs.RunOutput{}, + JobRunIdempotency: map[string]int64{}, + Grants: map[string][]catalog.PrivilegeAssignment{}, + Pipelines: map[string]pipelines.GetPipelineResponse{}, + PipelineUpdates: map[string]bool{}, + Monitors: map[string]catalog.MonitorInfo{}, + Apps: map[string]apps.App{}, + Catalogs: map[string]catalog.CatalogInfo{}, + ExternalLocations: map[string]catalog.ExternalLocationInfo{}, + Schemas: map[string]catalog.SchemaInfo{}, + RegisteredModels: map[string]catalog.RegisteredModelInfo{}, + ModelServices: map[string]catalog.ModelService{}, + McpServices: map[string]catalog.McpService{}, + ModelProviderServices: map[string]catalog.ModelProviderService{}, + Volumes: map[string]catalog.VolumeInfo{}, + Dashboards: NewEventualMap[string, *fakeDashboard](strings.HasPrefix(token, EventualConsistencyTokenPrefix)), + PublishedDashboards: map[string]dashboards.PublishedDashboard{}, + GenieSpaces: map[string]dashboards.GenieSpace{}, SqlWarehouses: map[string]sql.GetWarehouseResponse{ TestDefaultWarehouseId: { Id: TestDefaultWarehouseId, diff --git a/libs/testserver/handlers.go b/libs/testserver/handlers.go index 8c44d97eae..214206dac4 100644 --- a/libs/testserver/handlers.go +++ b/libs/testserver/handlers.go @@ -598,6 +598,60 @@ func AddDefaultHandlers(server *Server) { return MapDelete(req.Workspace, req.Workspace.RegisteredModels, req.Vars["full_name"]) }) + // Model Services (AI Gateway): + + server.Handle("POST", "/api/2.1/unity-catalog/model-services", func(req Request) any { + return req.Workspace.ModelServicesCreate(req) + }) + + server.Handle("GET", "/api/2.1/unity-catalog/model-services/{name}", func(req Request) any { + return MapGet(req.Workspace, req.Workspace.ModelServices, req.Vars["name"]) + }) + + server.Handle("PATCH", "/api/2.1/unity-catalog/model-services/{name}", func(req Request) any { + return req.Workspace.ModelServicesUpdate(req, req.Vars["name"]) + }) + + server.Handle("DELETE", "/api/2.1/unity-catalog/model-services/{name}", func(req Request) any { + return MapDelete(req.Workspace, req.Workspace.ModelServices, req.Vars["name"]) + }) + + // MCP Services (AI Gateway): + + server.Handle("POST", "/api/2.1/unity-catalog/mcp-services", func(req Request) any { + return req.Workspace.McpServicesCreate(req) + }) + + server.Handle("GET", "/api/2.1/unity-catalog/mcp-services/{name}", func(req Request) any { + return MapGet(req.Workspace, req.Workspace.McpServices, req.Vars["name"]) + }) + + server.Handle("PATCH", "/api/2.1/unity-catalog/mcp-services/{name}", func(req Request) any { + return req.Workspace.McpServicesUpdate(req, req.Vars["name"]) + }) + + server.Handle("DELETE", "/api/2.1/unity-catalog/mcp-services/{name}", func(req Request) any { + return MapDelete(req.Workspace, req.Workspace.McpServices, req.Vars["name"]) + }) + + // Model Provider Services (AI Gateway): + + server.Handle("POST", "/api/2.1/unity-catalog/model-provider-services", func(req Request) any { + return req.Workspace.ModelProviderServicesCreate(req) + }) + + server.Handle("GET", "/api/2.1/unity-catalog/model-provider-services/{name}", func(req Request) any { + return MapGet(req.Workspace, req.Workspace.ModelProviderServices, req.Vars["name"]) + }) + + server.Handle("PATCH", "/api/2.1/unity-catalog/model-provider-services/{name}", func(req Request) any { + return req.Workspace.ModelProviderServicesUpdate(req, req.Vars["name"]) + }) + + server.Handle("DELETE", "/api/2.1/unity-catalog/model-provider-services/{name}", func(req Request) any { + return MapDelete(req.Workspace, req.Workspace.ModelProviderServices, req.Vars["name"]) + }) + // Volumes: server.Handle("GET", "/api/2.1/unity-catalog/volumes/{full_name}", func(req Request) any { diff --git a/libs/testserver/mcp_services.go b/libs/testserver/mcp_services.go new file mode 100644 index 0000000000..41200d0695 --- /dev/null +++ b/libs/testserver/mcp_services.go @@ -0,0 +1,69 @@ +package testserver + +import ( + "encoding/json" + "fmt" + "net/http" + "strings" + + "github.com/databricks/databricks-sdk-go/service/catalog" +) + +// McpServicesCreate fakes POST /api/2.1/unity-catalog/mcp-services. See +// ModelServicesCreate for the shape: the McpService body is sent directly, +// parent and mcp_service_id are query parameters, and the map is keyed by the +// {catalog}.{schema}.{mcp_service} path segment. +func (s *FakeWorkspace) McpServicesCreate(req Request) Response { + defer s.LockUnlock()() + + var ms catalog.McpService + if err := json.Unmarshal(req.Body, &ms); err != nil { + return Response{ + Body: fmt.Sprintf("internal error: %s", err), + StatusCode: http.StatusInternalServerError, + } + } + + schema := strings.TrimPrefix(req.URL.Query().Get("parent"), "schemas/") + key := schema + "." + req.URL.Query().Get("mcp_service_id") + + ms.Name = "mcp-services/" + key + ms.CreatedBy = s.CurrentUser().UserName + ms.UpdatedBy = s.CurrentUser().UserName + ms.EffectiveOwner = s.CurrentUser().UserName + ms.MetastoreId = nextUUID() + + s.McpServices[key] = ms + return Response{ + Body: ms, + } +} + +func (s *FakeWorkspace) McpServicesUpdate(req Request, name string) Response { + defer s.LockUnlock()() + + existing, ok := s.McpServices[name] + if !ok { + return Response{ + StatusCode: http.StatusNotFound, + Body: fmt.Sprintf("mcp service %s not found", name), + } + } + + var incoming catalog.McpService + if err := json.Unmarshal(req.Body, &incoming); err != nil { + return Response{ + Body: fmt.Sprintf("internal error: %s", err), + StatusCode: http.StatusInternalServerError, + } + } + + existing.Comment = incoming.Comment + existing.Config = incoming.Config + existing.UpdatedBy = s.CurrentUser().UserName + + s.McpServices[name] = existing + return Response{ + Body: existing, + } +} diff --git a/libs/testserver/model_provider_services.go b/libs/testserver/model_provider_services.go new file mode 100644 index 0000000000..622bf75888 --- /dev/null +++ b/libs/testserver/model_provider_services.go @@ -0,0 +1,70 @@ +package testserver + +import ( + "encoding/json" + "fmt" + "net/http" + "strings" + + "github.com/databricks/databricks-sdk-go/service/catalog" +) + +// ModelProviderServicesCreate fakes POST +// /api/2.1/unity-catalog/model-provider-services. See ModelServicesCreate for +// the shape: the ModelProviderService body is sent directly, parent and +// model_provider_service_id are query parameters, and the map is keyed by the +// {catalog}.{schema}.{model_provider_service} path segment. +func (s *FakeWorkspace) ModelProviderServicesCreate(req Request) Response { + defer s.LockUnlock()() + + var ms catalog.ModelProviderService + if err := json.Unmarshal(req.Body, &ms); err != nil { + return Response{ + Body: fmt.Sprintf("internal error: %s", err), + StatusCode: http.StatusInternalServerError, + } + } + + schema := strings.TrimPrefix(req.URL.Query().Get("parent"), "schemas/") + key := schema + "." + req.URL.Query().Get("model_provider_service_id") + + ms.Name = "model-provider-services/" + key + ms.CreatedBy = s.CurrentUser().UserName + ms.UpdatedBy = s.CurrentUser().UserName + ms.EffectiveOwner = s.CurrentUser().UserName + ms.MetastoreId = nextUUID() + + s.ModelProviderServices[key] = ms + return Response{ + Body: ms, + } +} + +func (s *FakeWorkspace) ModelProviderServicesUpdate(req Request, name string) Response { + defer s.LockUnlock()() + + existing, ok := s.ModelProviderServices[name] + if !ok { + return Response{ + StatusCode: http.StatusNotFound, + Body: fmt.Sprintf("model provider service %s not found", name), + } + } + + var incoming catalog.ModelProviderService + if err := json.Unmarshal(req.Body, &incoming); err != nil { + return Response{ + Body: fmt.Sprintf("internal error: %s", err), + StatusCode: http.StatusInternalServerError, + } + } + + existing.Comment = incoming.Comment + existing.Config = incoming.Config + existing.UpdatedBy = s.CurrentUser().UserName + + s.ModelProviderServices[name] = existing + return Response{ + Body: existing, + } +} diff --git a/libs/testserver/model_services.go b/libs/testserver/model_services.go new file mode 100644 index 0000000000..506e6db01d --- /dev/null +++ b/libs/testserver/model_services.go @@ -0,0 +1,76 @@ +package testserver + +import ( + "encoding/json" + "fmt" + "net/http" + "strings" + + "github.com/databricks/databricks-sdk-go/service/catalog" +) + +// ModelServicesCreate fakes POST /api/2.1/unity-catalog/model-services. +// +// `parent` (schemas/{catalog}.{schema}) and `model_service_id` arrive as query +// parameters; the ModelService body is nested under "model_service". The server +// derives the resource name model-services/{catalog}.{schema}.{model_service}. +// The map is keyed by the {catalog}.{schema}.{model_service} portion, which is +// the path segment used on subsequent get/update/delete. +func (s *FakeWorkspace) ModelServicesCreate(req Request) Response { + defer s.LockUnlock()() + + // The SDK sends the ModelService body directly (not wrapped in + // "model_service"); parent and model_service_id are query parameters. + var ms catalog.ModelService + if err := json.Unmarshal(req.Body, &ms); err != nil { + return Response{ + Body: fmt.Sprintf("internal error: %s", err), + StatusCode: http.StatusInternalServerError, + } + } + + schema := strings.TrimPrefix(req.URL.Query().Get("parent"), "schemas/") + key := schema + "." + req.URL.Query().Get("model_service_id") + + ms.Name = "model-services/" + key + ms.CreatedBy = s.CurrentUser().UserName + ms.UpdatedBy = s.CurrentUser().UserName + ms.EffectiveOwner = s.CurrentUser().UserName + ms.MetastoreId = nextUUID() + + s.ModelServices[key] = ms + return Response{ + Body: ms, + } +} + +func (s *FakeWorkspace) ModelServicesUpdate(req Request, name string) Response { + defer s.LockUnlock()() + + existing, ok := s.ModelServices[name] + if !ok { + return Response{ + StatusCode: http.StatusNotFound, + Body: fmt.Sprintf("model service %s not found", name), + } + } + + // The SDK sends the ModelService body directly; update_mask is a query param. + var incoming catalog.ModelService + if err := json.Unmarshal(req.Body, &incoming); err != nil { + return Response{ + Body: fmt.Sprintf("internal error: %s", err), + StatusCode: http.StatusInternalServerError, + } + } + + // Apply the mutable fields carried in the update mask (comment, config). + existing.Comment = incoming.Comment + existing.Config = incoming.Config + existing.UpdatedBy = s.CurrentUser().UserName + + s.ModelServices[name] = existing + return Response{ + Body: existing, + } +} From 803cc5e7b518356e3ad7f8455889ca4ca5878658 Mon Sep 17 00:00:00 2001 From: Alex Khakhlyuk Date: Fri, 28 Aug 2026 11:44:53 +0000 Subject: [PATCH 2/2] Add acceptance, invariant, and bind/unbind tests for AI Gateway bundle resources Test coverage for the model_services, mcp_services, and model_provider_services direct-engine bundle resources, all running offline against the libs/testserver fakes: - Acceptance tests (acceptance/bundle/resources//): a `basic` subtest (deploy -> summary -> read -> update comment in place -> plan a recreate on an immutable field -> destroy) and a `remote-delete` subtest (out-of-band delete -> plan detects drift). Pinned to the direct engine via EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"] since these resources have no terraform converter (cf. genie_spaces). - Invariant configs (acceptance/bundle/invariant/configs/*.yml.tmpl) wired into the invariant matrix; excluded from the cloud run (no routing target / provider / connection is provisioned there) and from the migrate test (direct-only resources can't be terraform-seeded), matching genie_space / cluster_policy. - Bind/unbind tests (acceptance/bundle/deployment/bind//), with a [[Server]] GET stub so bind sees the remote resource. The invariant no_drift test also confirms the resources.yml output-only / secret suppression works: the model_provider_service's write-only config.custom.direct.api_key.plaintext produces no post-deploy drift. Co-authored-by: Isaac --- .../bind/mcp_service/databricks.yml | 10 ++ .../deployment/bind/mcp_service/out.test.toml | 2 + .../deployment/bind/mcp_service/output.txt | 30 ++++ .../bundle/deployment/bind/mcp_service/script | 5 + .../deployment/bind/mcp_service/test.toml | 18 +++ .../model_provider_service/databricks.yml | 16 ++ .../bind/model_provider_service/out.test.toml | 2 + .../bind/model_provider_service/output.txt | 30 ++++ .../bind/model_provider_service/script | 5 + .../bind/model_provider_service/test.toml | 18 +++ .../bind/model_service/databricks.yml | 10 ++ .../bind/model_service/out.test.toml | 2 + .../deployment/bind/model_service/output.txt | 30 ++++ .../deployment/bind/model_service/script | 5 + .../deployment/bind/model_service/test.toml | 18 +++ .../invariant/configs/mcp_service.yml.tmpl | 9 ++ .../configs/model_provider_service.yml.tmpl | 20 +++ .../invariant/configs/model_service.yml.tmpl | 9 ++ .../invariant/continue_293/out.test.toml | 5 +- .../invariant/delete_idempotent/out.test.toml | 5 +- .../destroy_idempotent/out.test.toml | 5 +- acceptance/bundle/invariant/migrate/test.toml | 4 + .../bundle/invariant/no_drift/out.test.toml | 5 +- acceptance/bundle/invariant/test.toml | 9 ++ .../mcp_services/basic/databricks.yml | 10 ++ .../mcp_services/basic/out.test.toml | 2 + .../resources/mcp_services/basic/output.txt | 92 +++++++++++ .../resources/mcp_services/basic/script | 29 ++++ .../resources/mcp_services/basic/test.toml | 3 + .../mcp_services/remote-delete/databricks.yml | 10 ++ .../mcp_services/remote-delete/out.test.toml | 2 + .../mcp_services/remote-delete/output.txt | 20 +++ .../mcp_services/remote-delete/script | 9 ++ .../mcp_services/remote-delete/test.toml | 5 + .../bundle/resources/mcp_services/test.toml | 3 + .../basic/databricks.yml | 20 +++ .../basic/out.test.toml | 2 + .../model_provider_services/basic/output.txt | 149 ++++++++++++++++++ .../model_provider_services/basic/script | 29 ++++ .../model_provider_services/basic/test.toml | 3 + .../remote-delete/databricks.yml | 20 +++ .../remote-delete/out.test.toml | 2 + .../remote-delete/output.txt | 20 +++ .../remote-delete/script | 9 ++ .../remote-delete/test.toml | 5 + .../model_provider_services/test.toml | 3 + .../model_services/basic/databricks.yml | 10 ++ .../model_services/basic/out.test.toml | 2 + .../resources/model_services/basic/output.txt | 92 +++++++++++ .../resources/model_services/basic/script | 29 ++++ .../resources/model_services/basic/test.toml | 3 + .../remote-delete/databricks.yml | 10 ++ .../remote-delete/out.test.toml | 2 + .../model_services/remote-delete/output.txt | 20 +++ .../model_services/remote-delete/script | 9 ++ .../model_services/remote-delete/test.toml | 5 + .../bundle/resources/model_services/test.toml | 3 + 57 files changed, 900 insertions(+), 4 deletions(-) create mode 100644 acceptance/bundle/deployment/bind/mcp_service/databricks.yml create mode 100644 acceptance/bundle/deployment/bind/mcp_service/out.test.toml create mode 100644 acceptance/bundle/deployment/bind/mcp_service/output.txt create mode 100644 acceptance/bundle/deployment/bind/mcp_service/script create mode 100644 acceptance/bundle/deployment/bind/mcp_service/test.toml create mode 100644 acceptance/bundle/deployment/bind/model_provider_service/databricks.yml create mode 100644 acceptance/bundle/deployment/bind/model_provider_service/out.test.toml create mode 100644 acceptance/bundle/deployment/bind/model_provider_service/output.txt create mode 100644 acceptance/bundle/deployment/bind/model_provider_service/script create mode 100644 acceptance/bundle/deployment/bind/model_provider_service/test.toml create mode 100644 acceptance/bundle/deployment/bind/model_service/databricks.yml create mode 100644 acceptance/bundle/deployment/bind/model_service/out.test.toml create mode 100644 acceptance/bundle/deployment/bind/model_service/output.txt create mode 100644 acceptance/bundle/deployment/bind/model_service/script create mode 100644 acceptance/bundle/deployment/bind/model_service/test.toml create mode 100644 acceptance/bundle/invariant/configs/mcp_service.yml.tmpl create mode 100644 acceptance/bundle/invariant/configs/model_provider_service.yml.tmpl create mode 100644 acceptance/bundle/invariant/configs/model_service.yml.tmpl create mode 100644 acceptance/bundle/resources/mcp_services/basic/databricks.yml create mode 100644 acceptance/bundle/resources/mcp_services/basic/out.test.toml create mode 100644 acceptance/bundle/resources/mcp_services/basic/output.txt create mode 100644 acceptance/bundle/resources/mcp_services/basic/script create mode 100644 acceptance/bundle/resources/mcp_services/basic/test.toml create mode 100644 acceptance/bundle/resources/mcp_services/remote-delete/databricks.yml create mode 100644 acceptance/bundle/resources/mcp_services/remote-delete/out.test.toml create mode 100644 acceptance/bundle/resources/mcp_services/remote-delete/output.txt create mode 100644 acceptance/bundle/resources/mcp_services/remote-delete/script create mode 100644 acceptance/bundle/resources/mcp_services/remote-delete/test.toml create mode 100644 acceptance/bundle/resources/mcp_services/test.toml create mode 100644 acceptance/bundle/resources/model_provider_services/basic/databricks.yml create mode 100644 acceptance/bundle/resources/model_provider_services/basic/out.test.toml create mode 100644 acceptance/bundle/resources/model_provider_services/basic/output.txt create mode 100644 acceptance/bundle/resources/model_provider_services/basic/script create mode 100644 acceptance/bundle/resources/model_provider_services/basic/test.toml create mode 100644 acceptance/bundle/resources/model_provider_services/remote-delete/databricks.yml create mode 100644 acceptance/bundle/resources/model_provider_services/remote-delete/out.test.toml create mode 100644 acceptance/bundle/resources/model_provider_services/remote-delete/output.txt create mode 100644 acceptance/bundle/resources/model_provider_services/remote-delete/script create mode 100644 acceptance/bundle/resources/model_provider_services/remote-delete/test.toml create mode 100644 acceptance/bundle/resources/model_provider_services/test.toml create mode 100644 acceptance/bundle/resources/model_services/basic/databricks.yml create mode 100644 acceptance/bundle/resources/model_services/basic/out.test.toml create mode 100644 acceptance/bundle/resources/model_services/basic/output.txt create mode 100644 acceptance/bundle/resources/model_services/basic/script create mode 100644 acceptance/bundle/resources/model_services/basic/test.toml create mode 100644 acceptance/bundle/resources/model_services/remote-delete/databricks.yml create mode 100644 acceptance/bundle/resources/model_services/remote-delete/out.test.toml create mode 100644 acceptance/bundle/resources/model_services/remote-delete/output.txt create mode 100644 acceptance/bundle/resources/model_services/remote-delete/script create mode 100644 acceptance/bundle/resources/model_services/remote-delete/test.toml create mode 100644 acceptance/bundle/resources/model_services/test.toml diff --git a/acceptance/bundle/deployment/bind/mcp_service/databricks.yml b/acceptance/bundle/deployment/bind/mcp_service/databricks.yml new file mode 100644 index 0000000000..924be6ae45 --- /dev/null +++ b/acceptance/bundle/deployment/bind/mcp_service/databricks.yml @@ -0,0 +1,10 @@ +bundle: + name: test-bundle + +resources: + mcp_services: + mcp1: + parent: schemas/main.myschema + mcp_service_id: mysvc + comment: bound service + diff --git a/acceptance/bundle/deployment/bind/mcp_service/out.test.toml b/acceptance/bundle/deployment/bind/mcp_service/out.test.toml new file mode 100644 index 0000000000..0938e67898 --- /dev/null +++ b/acceptance/bundle/deployment/bind/mcp_service/out.test.toml @@ -0,0 +1,2 @@ +Cloud = false +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"] diff --git a/acceptance/bundle/deployment/bind/mcp_service/output.txt b/acceptance/bundle/deployment/bind/mcp_service/output.txt new file mode 100644 index 0000000000..ff151e2b64 --- /dev/null +++ b/acceptance/bundle/deployment/bind/mcp_service/output.txt @@ -0,0 +1,30 @@ + +>>> [CLI] bundle deployment bind mcp1 main.myschema.mysvc --auto-approve +Successfully bound mcp_service with an id 'main.myschema.mysvc' +Run 'bundle deploy' to deploy changes to your workspace + +>>> [CLI] bundle summary +Name: test-bundle +Target: default +Workspace: + User: [USERNAME] + Path: /Workspace/Users/[USERNAME]/.bundle/test-bundle/default +Resources: + MCP services: + mcp1: + Name: mysvc + URL: (not deployed) + +>>> [CLI] bundle deployment unbind mcp1 + +>>> [CLI] bundle summary +Name: test-bundle +Target: default +Workspace: + User: [USERNAME] + Path: /Workspace/Users/[USERNAME]/.bundle/test-bundle/default +Resources: + MCP services: + mcp1: + Name: mysvc + URL: (not deployed) diff --git a/acceptance/bundle/deployment/bind/mcp_service/script b/acceptance/bundle/deployment/bind/mcp_service/script new file mode 100644 index 0000000000..5314f84d15 --- /dev/null +++ b/acceptance/bundle/deployment/bind/mcp_service/script @@ -0,0 +1,5 @@ +trace $CLI bundle deployment bind mcp1 main.myschema.mysvc --auto-approve +trace $CLI bundle summary + +trace $CLI bundle deployment unbind mcp1 +trace $CLI bundle summary diff --git a/acceptance/bundle/deployment/bind/mcp_service/test.toml b/acceptance/bundle/deployment/bind/mcp_service/test.toml new file mode 100644 index 0000000000..98cf498465 --- /dev/null +++ b/acceptance/bundle/deployment/bind/mcp_service/test.toml @@ -0,0 +1,18 @@ +# AI Gateway securables are direct-engine only. +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"] + +Cloud = false + +Ignore = [ + ".databricks", +] + +# The bind flow issues a GET to confirm the remote resource exists before binding. +[[Server]] +Pattern = "GET /api/2.1/unity-catalog/mcp-services/{name}" +Response.Body = ''' +{ + "name": "mcp-services/main.myschema.mysvc", + "comment": "bound service" +} +''' \ No newline at end of file diff --git a/acceptance/bundle/deployment/bind/model_provider_service/databricks.yml b/acceptance/bundle/deployment/bind/model_provider_service/databricks.yml new file mode 100644 index 0000000000..18717f5715 --- /dev/null +++ b/acceptance/bundle/deployment/bind/model_provider_service/databricks.yml @@ -0,0 +1,16 @@ +bundle: + name: test-bundle + +resources: + model_provider_services: + mps1: + parent: schemas/main.myschema + model_provider_service_id: mysvc + comment: bound service + config: + provider_type: EXTERNAL_MODEL_PROVIDER_TYPE_CUSTOM + custom: + direct: + base_url: https://api.example.com/v1 + api_key: + plaintext: dummy-api-key diff --git a/acceptance/bundle/deployment/bind/model_provider_service/out.test.toml b/acceptance/bundle/deployment/bind/model_provider_service/out.test.toml new file mode 100644 index 0000000000..0938e67898 --- /dev/null +++ b/acceptance/bundle/deployment/bind/model_provider_service/out.test.toml @@ -0,0 +1,2 @@ +Cloud = false +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"] diff --git a/acceptance/bundle/deployment/bind/model_provider_service/output.txt b/acceptance/bundle/deployment/bind/model_provider_service/output.txt new file mode 100644 index 0000000000..ca0a2333ae --- /dev/null +++ b/acceptance/bundle/deployment/bind/model_provider_service/output.txt @@ -0,0 +1,30 @@ + +>>> [CLI] bundle deployment bind mps1 main.myschema.mysvc --auto-approve +Successfully bound model_provider_service with an id 'main.myschema.mysvc' +Run 'bundle deploy' to deploy changes to your workspace + +>>> [CLI] bundle summary +Name: test-bundle +Target: default +Workspace: + User: [USERNAME] + Path: /Workspace/Users/[USERNAME]/.bundle/test-bundle/default +Resources: + Model provider services: + mps1: + Name: mysvc + URL: (not deployed) + +>>> [CLI] bundle deployment unbind mps1 + +>>> [CLI] bundle summary +Name: test-bundle +Target: default +Workspace: + User: [USERNAME] + Path: /Workspace/Users/[USERNAME]/.bundle/test-bundle/default +Resources: + Model provider services: + mps1: + Name: mysvc + URL: (not deployed) diff --git a/acceptance/bundle/deployment/bind/model_provider_service/script b/acceptance/bundle/deployment/bind/model_provider_service/script new file mode 100644 index 0000000000..c003c48256 --- /dev/null +++ b/acceptance/bundle/deployment/bind/model_provider_service/script @@ -0,0 +1,5 @@ +trace $CLI bundle deployment bind mps1 main.myschema.mysvc --auto-approve +trace $CLI bundle summary + +trace $CLI bundle deployment unbind mps1 +trace $CLI bundle summary diff --git a/acceptance/bundle/deployment/bind/model_provider_service/test.toml b/acceptance/bundle/deployment/bind/model_provider_service/test.toml new file mode 100644 index 0000000000..175f9175b9 --- /dev/null +++ b/acceptance/bundle/deployment/bind/model_provider_service/test.toml @@ -0,0 +1,18 @@ +# AI Gateway securables are direct-engine only. +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"] + +Cloud = false + +Ignore = [ + ".databricks", +] + +# The bind flow issues a GET to confirm the remote resource exists before binding. +[[Server]] +Pattern = "GET /api/2.1/unity-catalog/model-provider-services/{name}" +Response.Body = ''' +{ + "name": "model-provider-services/main.myschema.mysvc", + "comment": "bound service" +} +''' \ No newline at end of file diff --git a/acceptance/bundle/deployment/bind/model_service/databricks.yml b/acceptance/bundle/deployment/bind/model_service/databricks.yml new file mode 100644 index 0000000000..a49f478d48 --- /dev/null +++ b/acceptance/bundle/deployment/bind/model_service/databricks.yml @@ -0,0 +1,10 @@ +bundle: + name: test-bundle + +resources: + model_services: + ms1: + parent: schemas/main.myschema + model_service_id: mysvc + comment: bound service + diff --git a/acceptance/bundle/deployment/bind/model_service/out.test.toml b/acceptance/bundle/deployment/bind/model_service/out.test.toml new file mode 100644 index 0000000000..0938e67898 --- /dev/null +++ b/acceptance/bundle/deployment/bind/model_service/out.test.toml @@ -0,0 +1,2 @@ +Cloud = false +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"] diff --git a/acceptance/bundle/deployment/bind/model_service/output.txt b/acceptance/bundle/deployment/bind/model_service/output.txt new file mode 100644 index 0000000000..64df67b3d1 --- /dev/null +++ b/acceptance/bundle/deployment/bind/model_service/output.txt @@ -0,0 +1,30 @@ + +>>> [CLI] bundle deployment bind ms1 main.myschema.mysvc --auto-approve +Successfully bound model_service with an id 'main.myschema.mysvc' +Run 'bundle deploy' to deploy changes to your workspace + +>>> [CLI] bundle summary +Name: test-bundle +Target: default +Workspace: + User: [USERNAME] + Path: /Workspace/Users/[USERNAME]/.bundle/test-bundle/default +Resources: + Model services: + ms1: + Name: mysvc + URL: (not deployed) + +>>> [CLI] bundle deployment unbind ms1 + +>>> [CLI] bundle summary +Name: test-bundle +Target: default +Workspace: + User: [USERNAME] + Path: /Workspace/Users/[USERNAME]/.bundle/test-bundle/default +Resources: + Model services: + ms1: + Name: mysvc + URL: (not deployed) diff --git a/acceptance/bundle/deployment/bind/model_service/script b/acceptance/bundle/deployment/bind/model_service/script new file mode 100644 index 0000000000..e886f5dcd8 --- /dev/null +++ b/acceptance/bundle/deployment/bind/model_service/script @@ -0,0 +1,5 @@ +trace $CLI bundle deployment bind ms1 main.myschema.mysvc --auto-approve +trace $CLI bundle summary + +trace $CLI bundle deployment unbind ms1 +trace $CLI bundle summary diff --git a/acceptance/bundle/deployment/bind/model_service/test.toml b/acceptance/bundle/deployment/bind/model_service/test.toml new file mode 100644 index 0000000000..067e1b6e27 --- /dev/null +++ b/acceptance/bundle/deployment/bind/model_service/test.toml @@ -0,0 +1,18 @@ +# AI Gateway securables are direct-engine only. +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"] + +Cloud = false + +Ignore = [ + ".databricks", +] + +# The bind flow issues a GET to confirm the remote resource exists before binding. +[[Server]] +Pattern = "GET /api/2.1/unity-catalog/model-services/{name}" +Response.Body = ''' +{ + "name": "model-services/main.myschema.mysvc", + "comment": "bound service" +} +''' \ No newline at end of file diff --git a/acceptance/bundle/invariant/configs/mcp_service.yml.tmpl b/acceptance/bundle/invariant/configs/mcp_service.yml.tmpl new file mode 100644 index 0000000000..4b463bc5b7 --- /dev/null +++ b/acceptance/bundle/invariant/configs/mcp_service.yml.tmpl @@ -0,0 +1,9 @@ +bundle: + name: test-bundle-$UNIQUE_NAME + +resources: + mcp_services: + foo: + parent: schemas/main.default + mcp_service_id: test-mcp-service-$UNIQUE_NAME + comment: test mcp service diff --git a/acceptance/bundle/invariant/configs/model_provider_service.yml.tmpl b/acceptance/bundle/invariant/configs/model_provider_service.yml.tmpl new file mode 100644 index 0000000000..2c7b091002 --- /dev/null +++ b/acceptance/bundle/invariant/configs/model_provider_service.yml.tmpl @@ -0,0 +1,20 @@ +bundle: + name: test-bundle-$UNIQUE_NAME + +resources: + model_provider_services: + foo: + parent: schemas/main.default + model_provider_service_id: test-model-provider-service-$UNIQUE_NAME + comment: test model provider service + config: + provider_type: EXTERNAL_MODEL_PROVIDER_TYPE_CUSTOM + targets: + - model: gpt-4o + native_api_types: + - openai/v1/chat/completions + custom: + direct: + base_url: https://api.example.com/v1 + api_key: + plaintext: dummy-api-key diff --git a/acceptance/bundle/invariant/configs/model_service.yml.tmpl b/acceptance/bundle/invariant/configs/model_service.yml.tmpl new file mode 100644 index 0000000000..e4ea6c39dc --- /dev/null +++ b/acceptance/bundle/invariant/configs/model_service.yml.tmpl @@ -0,0 +1,9 @@ +bundle: + name: test-bundle-$UNIQUE_NAME + +resources: + model_services: + foo: + parent: schemas/main.default + model_service_id: test-model-service-$UNIQUE_NAME + comment: test model service diff --git a/acceptance/bundle/invariant/continue_293/out.test.toml b/acceptance/bundle/invariant/continue_293/out.test.toml index f5883975fc..10b5d32fa7 100644 --- a/acceptance/bundle/invariant/continue_293/out.test.toml +++ b/acceptance/bundle/invariant/continue_293/out.test.toml @@ -43,5 +43,8 @@ EnvMatrix.INPUT_CONFIG = [ "synced_database_table.yml.tmpl", "volume.yml.tmpl", "volume_external.yml.tmpl", - "volume_uppercase_name.yml.tmpl" + "volume_uppercase_name.yml.tmpl", + "model_service.yml.tmpl", + "mcp_service.yml.tmpl", + "model_provider_service.yml.tmpl" ] diff --git a/acceptance/bundle/invariant/delete_idempotent/out.test.toml b/acceptance/bundle/invariant/delete_idempotent/out.test.toml index 8ed109d482..684002d496 100644 --- a/acceptance/bundle/invariant/delete_idempotent/out.test.toml +++ b/acceptance/bundle/invariant/delete_idempotent/out.test.toml @@ -57,6 +57,9 @@ EnvMatrix.INPUT_CONFIG = [ "volume.yml.tmpl", "volume_external.yml.tmpl", "volume_path_job_ref.yml.tmpl", - "volume_uppercase_name.yml.tmpl" + "volume_uppercase_name.yml.tmpl", + "model_service.yml.tmpl", + "mcp_service.yml.tmpl", + "model_provider_service.yml.tmpl" ] EnvMatrix.READPLAN = ["", "1"] diff --git a/acceptance/bundle/invariant/destroy_idempotent/out.test.toml b/acceptance/bundle/invariant/destroy_idempotent/out.test.toml index 8ed109d482..684002d496 100644 --- a/acceptance/bundle/invariant/destroy_idempotent/out.test.toml +++ b/acceptance/bundle/invariant/destroy_idempotent/out.test.toml @@ -57,6 +57,9 @@ EnvMatrix.INPUT_CONFIG = [ "volume.yml.tmpl", "volume_external.yml.tmpl", "volume_path_job_ref.yml.tmpl", - "volume_uppercase_name.yml.tmpl" + "volume_uppercase_name.yml.tmpl", + "model_service.yml.tmpl", + "mcp_service.yml.tmpl", + "model_provider_service.yml.tmpl" ] EnvMatrix.READPLAN = ["", "1"] diff --git a/acceptance/bundle/invariant/migrate/test.toml b/acceptance/bundle/invariant/migrate/test.toml index aa24bf58ee..c743545aba 100644 --- a/acceptance/bundle/invariant/migrate/test.toml +++ b/acceptance/bundle/invariant/migrate/test.toml @@ -16,6 +16,10 @@ EnvMatrixExclude.no_genie_space = ["INPUT_CONFIG=genie_space.yml.tmpl"] EnvMatrixExclude.no_instance_pool = ["INPUT_CONFIG=instance_pool.yml.tmpl"] # Cluster policies are direct-only; the terraform deploy that seeds the migration fails for them. EnvMatrixExclude.no_cluster_policy = ["INPUT_CONFIG=cluster_policy.yml.tmpl"] +# AI Gateway securables are direct-only; the terraform deploy that seeds the migration fails for them. +EnvMatrixExclude.no_model_service = ["INPUT_CONFIG=model_service.yml.tmpl"] +EnvMatrixExclude.no_mcp_service = ["INPUT_CONFIG=mcp_service.yml.tmpl"] +EnvMatrixExclude.no_model_provider_service = ["INPUT_CONFIG=model_provider_service.yml.tmpl"] # Cross-resource permission references (e.g. ${resources.jobs.job_b.permissions[0].level}) # don't work in terraform mode: the terraform interpolator converts the path to diff --git a/acceptance/bundle/invariant/no_drift/out.test.toml b/acceptance/bundle/invariant/no_drift/out.test.toml index 8ed109d482..684002d496 100644 --- a/acceptance/bundle/invariant/no_drift/out.test.toml +++ b/acceptance/bundle/invariant/no_drift/out.test.toml @@ -57,6 +57,9 @@ EnvMatrix.INPUT_CONFIG = [ "volume.yml.tmpl", "volume_external.yml.tmpl", "volume_path_job_ref.yml.tmpl", - "volume_uppercase_name.yml.tmpl" + "volume_uppercase_name.yml.tmpl", + "model_service.yml.tmpl", + "mcp_service.yml.tmpl", + "model_provider_service.yml.tmpl" ] EnvMatrix.READPLAN = ["", "1"] diff --git a/acceptance/bundle/invariant/test.toml b/acceptance/bundle/invariant/test.toml index ac4584e034..7275512cb5 100644 --- a/acceptance/bundle/invariant/test.toml +++ b/acceptance/bundle/invariant/test.toml @@ -76,6 +76,9 @@ EnvMatrix.INPUT_CONFIG = [ "volume_external.yml.tmpl", "volume_path_job_ref.yml.tmpl", "volume_uppercase_name.yml.tmpl", + "model_service.yml.tmpl", + "mcp_service.yml.tmpl", + "model_provider_service.yml.tmpl", ] EnvMatrixExclude.no_alert_on_cloud = ["CONFIG_Cloud=true", "INPUT_CONFIG=alert.yml.tmpl"] @@ -106,6 +109,12 @@ EnvMatrixExclude.no_external_location_on_cloud = ["CONFIG_Cloud=true", "INPUT_CO EnvMatrixExclude.no_external_volume_on_cloud = ["CONFIG_Cloud=true", "INPUT_CONFIG=volume_external.yml.tmpl"] # Telemetry requires a real model, which cloud invariant tests do not provision. EnvMatrixExclude.no_model_serving_endpoint_telemetry_on_cloud = ["CONFIG_Cloud=true", "INPUT_CONFIG=model_serving_endpoint_telemetry.yml.tmpl"] +# AI Gateway securables (direct-only) need a real routing target / provider config / +# UC connection to create on cloud, which the invariant tests don't provision; the +# local mock server accepts them, so these run locally only. +EnvMatrixExclude.no_model_service_on_cloud = ["CONFIG_Cloud=true", "INPUT_CONFIG=model_service.yml.tmpl"] +EnvMatrixExclude.no_mcp_service_on_cloud = ["CONFIG_Cloud=true", "INPUT_CONFIG=mcp_service.yml.tmpl"] +EnvMatrixExclude.no_model_provider_service_on_cloud = ["CONFIG_Cloud=true", "INPUT_CONFIG=model_provider_service.yml.tmpl"] # Fake SQL endpoint for local tests [[Server]] diff --git a/acceptance/bundle/resources/mcp_services/basic/databricks.yml b/acceptance/bundle/resources/mcp_services/basic/databricks.yml new file mode 100644 index 0000000000..2526fff328 --- /dev/null +++ b/acceptance/bundle/resources/mcp_services/basic/databricks.yml @@ -0,0 +1,10 @@ +bundle: + name: test-bundle + +resources: + mcp_services: + mcp1: + parent: schemas/main.myschema + mcp_service_id: myservice + comment: COMMENT1 + diff --git a/acceptance/bundle/resources/mcp_services/basic/out.test.toml b/acceptance/bundle/resources/mcp_services/basic/out.test.toml new file mode 100644 index 0000000000..0938e67898 --- /dev/null +++ b/acceptance/bundle/resources/mcp_services/basic/out.test.toml @@ -0,0 +1,2 @@ +Cloud = false +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"] diff --git a/acceptance/bundle/resources/mcp_services/basic/output.txt b/acceptance/bundle/resources/mcp_services/basic/output.txt new file mode 100644 index 0000000000..0f5911d3a9 --- /dev/null +++ b/acceptance/bundle/resources/mcp_services/basic/output.txt @@ -0,0 +1,92 @@ + +=== Initial summary before deploy +>>> [CLI] bundle summary -o json +{ + "comment": "COMMENT1", + "mcp_service_id": "myservice", + "modified_status": "created", + "parent": "schemas/main.myschema" +} + +=== Verify it does not exist yet +>>> musterr [CLI] ai-gateway get-mcp-service mcp-services/main.myschema.myservice +Error: Resource catalog.McpService not found: main.myschema.myservice + +>>> [CLI] bundle deploy +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bundle/default/files... +Created mcp_services.mcp1 +Files: 5 uploaded, 0 deleted +Resources: 1 created, 0 changed, 0 deleted, 0 unchanged + +>>> print_requests.py //unity +{ + "method": "POST", + "path": "/api/2.1/unity-catalog/mcp-services", + "q": { + "mcp_service_id": "myservice", + "parent": "schemas/main.myschema" + }, + "body": { + "comment": "COMMENT1" + } +} + +=== Summary should show the id (url is not wired for these resources) +>>> [CLI] bundle summary -o json +{ + "id": "main.myschema.myservice", + "url": null +} + +=== Verify deployment +>>> [CLI] ai-gateway get-mcp-service mcp-services/main.myschema.myservice +{ + "name": "mcp-services/main.myschema.myservice", + "comment": "COMMENT1" +} + +=== Update comment (should update in place, not recreate) +>>> update_file.py databricks.yml COMMENT1 COMMENT2 + +>>> [CLI] bundle deploy +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bundle/default/files... +Updated mcp_services.mcp1 +Files: 3 uploaded, 0 deleted +Resources: 0 created, 1 changed, 0 deleted, 0 unchanged + +>>> print_requests.py //unity +{ + "method": "PATCH", + "path": "/api/2.1/unity-catalog/mcp-services/main.myschema.myservice", + "q": { + "update_mask": "comment,config" + }, + "body": { + "comment": "COMMENT2" + } +} + +>>> [CLI] ai-gateway get-mcp-service mcp-services/main.myschema.myservice +"COMMENT2" + +=== Change an immutable field (should plan a recreate) +>>> update_file.py databricks.yml myservice myservice-renamed + +>>> [CLI] bundle plan +recreate mcp_services.mcp1 + +Plan: 1 to add, 0 to change, 1 to delete, 0 unchanged + +>>> [CLI] bundle destroy --auto-approve +The following resources will be deleted: + delete resources.mcp_services.mcp1 + +All files and directories at the following location will be deleted: /Workspace/Users/[USERNAME]/.bundle/test-bundle/default + +Destroy: 1 deleted + +>>> print_requests.py //unity +{ + "method": "DELETE", + "path": "/api/2.1/unity-catalog/mcp-services/main.myschema.myservice" +} diff --git a/acceptance/bundle/resources/mcp_services/basic/script b/acceptance/bundle/resources/mcp_services/basic/script new file mode 100644 index 0000000000..4de138424a --- /dev/null +++ b/acceptance/bundle/resources/mcp_services/basic/script @@ -0,0 +1,29 @@ +title "Initial summary before deploy" +trace $CLI bundle summary -o json | jq .resources.mcp_services.mcp1 + +title "Verify it does not exist yet" +trace musterr $CLI ai-gateway get-mcp-service mcp-services/main.myschema.myservice + +trace $CLI bundle deploy +trace print_requests.py //unity + +title "Summary should show the id (url is not wired for these resources)" +trace $CLI bundle summary -o json | jq ".resources.mcp_services.mcp1 | {id, url}" + +title "Verify deployment" +trace $CLI ai-gateway get-mcp-service mcp-services/main.myschema.myservice | jq '{name, comment}' + +title "Update comment (should update in place, not recreate)" +trace update_file.py databricks.yml COMMENT1 COMMENT2 +trace $CLI bundle deploy +trace print_requests.py //unity +trace $CLI ai-gateway get-mcp-service mcp-services/main.myschema.myservice | jq .comment + +title "Change an immutable field (should plan a recreate)" +trace update_file.py databricks.yml myservice myservice-renamed +trace $CLI bundle plan + +trace $CLI bundle destroy --auto-approve +trace print_requests.py //unity + +rm -f out.requests.txt diff --git a/acceptance/bundle/resources/mcp_services/basic/test.toml b/acceptance/bundle/resources/mcp_services/basic/test.toml new file mode 100644 index 0000000000..bd4b66fe75 --- /dev/null +++ b/acceptance/bundle/resources/mcp_services/basic/test.toml @@ -0,0 +1,3 @@ +Ignore = [ + ".databricks", +] diff --git a/acceptance/bundle/resources/mcp_services/remote-delete/databricks.yml b/acceptance/bundle/resources/mcp_services/remote-delete/databricks.yml new file mode 100644 index 0000000000..2526fff328 --- /dev/null +++ b/acceptance/bundle/resources/mcp_services/remote-delete/databricks.yml @@ -0,0 +1,10 @@ +bundle: + name: test-bundle + +resources: + mcp_services: + mcp1: + parent: schemas/main.myschema + mcp_service_id: myservice + comment: COMMENT1 + diff --git a/acceptance/bundle/resources/mcp_services/remote-delete/out.test.toml b/acceptance/bundle/resources/mcp_services/remote-delete/out.test.toml new file mode 100644 index 0000000000..0938e67898 --- /dev/null +++ b/acceptance/bundle/resources/mcp_services/remote-delete/out.test.toml @@ -0,0 +1,2 @@ +Cloud = false +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"] diff --git a/acceptance/bundle/resources/mcp_services/remote-delete/output.txt b/acceptance/bundle/resources/mcp_services/remote-delete/output.txt new file mode 100644 index 0000000000..9bbe11cdb9 --- /dev/null +++ b/acceptance/bundle/resources/mcp_services/remote-delete/output.txt @@ -0,0 +1,20 @@ + +>>> [CLI] bundle deploy +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bundle/default/files... +Created mcp_services.mcp1 +Files: 4 uploaded, 0 deleted +Resources: 1 created, 0 changed, 0 deleted, 0 unchanged + +=== Delete the resource out of band +>>> [CLI] ai-gateway delete-mcp-service mcp-services/main.myschema.myservice + +=== Plan should detect the resource is gone and re-create it +>>> [CLI] bundle plan +create mcp_services.mcp1 + +Plan: 1 to add, 0 to change, 0 to delete, 0 unchanged + +>>> [CLI] bundle destroy --auto-approve +All files and directories at the following location will be deleted: /Workspace/Users/[USERNAME]/.bundle/test-bundle/default + +Destroy: 0 deleted diff --git a/acceptance/bundle/resources/mcp_services/remote-delete/script b/acceptance/bundle/resources/mcp_services/remote-delete/script new file mode 100644 index 0000000000..413e6174fd --- /dev/null +++ b/acceptance/bundle/resources/mcp_services/remote-delete/script @@ -0,0 +1,9 @@ +trace $CLI bundle deploy + +title "Delete the resource out of band" +trace $CLI ai-gateway delete-mcp-service mcp-services/main.myschema.myservice + +title "Plan should detect the resource is gone and re-create it" +trace $CLI bundle plan + +trace $CLI bundle destroy --auto-approve diff --git a/acceptance/bundle/resources/mcp_services/remote-delete/test.toml b/acceptance/bundle/resources/mcp_services/remote-delete/test.toml new file mode 100644 index 0000000000..daa475f7ce --- /dev/null +++ b/acceptance/bundle/resources/mcp_services/remote-delete/test.toml @@ -0,0 +1,5 @@ +RecordRequests = false + +Ignore = [ + ".databricks", +] diff --git a/acceptance/bundle/resources/mcp_services/test.toml b/acceptance/bundle/resources/mcp_services/test.toml new file mode 100644 index 0000000000..b5361a56ad --- /dev/null +++ b/acceptance/bundle/resources/mcp_services/test.toml @@ -0,0 +1,3 @@ +# AI Gateway securables are only deployable via the direct deployment engine +# (there is no Terraform provider path for them in bundles). +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"] diff --git a/acceptance/bundle/resources/model_provider_services/basic/databricks.yml b/acceptance/bundle/resources/model_provider_services/basic/databricks.yml new file mode 100644 index 0000000000..aeac81263d --- /dev/null +++ b/acceptance/bundle/resources/model_provider_services/basic/databricks.yml @@ -0,0 +1,20 @@ +bundle: + name: test-bundle + +resources: + model_provider_services: + mps1: + parent: schemas/main.myschema + model_provider_service_id: myservice + comment: COMMENT1 + config: + provider_type: EXTERNAL_MODEL_PROVIDER_TYPE_CUSTOM + targets: + - model: gpt-4o + native_api_types: + - openai/v1/chat/completions + custom: + direct: + base_url: https://api.example.com/v1 + api_key: + plaintext: dummy-api-key diff --git a/acceptance/bundle/resources/model_provider_services/basic/out.test.toml b/acceptance/bundle/resources/model_provider_services/basic/out.test.toml new file mode 100644 index 0000000000..0938e67898 --- /dev/null +++ b/acceptance/bundle/resources/model_provider_services/basic/out.test.toml @@ -0,0 +1,2 @@ +Cloud = false +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"] diff --git a/acceptance/bundle/resources/model_provider_services/basic/output.txt b/acceptance/bundle/resources/model_provider_services/basic/output.txt new file mode 100644 index 0000000000..bbf31d8c9e --- /dev/null +++ b/acceptance/bundle/resources/model_provider_services/basic/output.txt @@ -0,0 +1,149 @@ + +=== Initial summary before deploy +>>> [CLI] bundle summary -o json +{ + "comment": "COMMENT1", + "config": { + "custom": { + "direct": { + "api_key": { + "plaintext": "dummy-api-key" + }, + "base_url": "https://api.example.com/v1" + } + }, + "provider_type": "EXTERNAL_MODEL_PROVIDER_TYPE_CUSTOM", + "targets": [ + { + "model": "gpt-4o", + "native_api_types": [ + "openai/v1/chat/completions" + ] + } + ] + }, + "model_provider_service_id": "myservice", + "modified_status": "created", + "parent": "schemas/main.myschema" +} + +=== Verify it does not exist yet +>>> musterr [CLI] ai-gateway get-model-provider-service model-provider-services/main.myschema.myservice +Error: Resource catalog.ModelProviderService not found: main.myschema.myservice + +>>> [CLI] bundle deploy +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bundle/default/files... +Created model_provider_services.mps1 +Files: 5 uploaded, 0 deleted +Resources: 1 created, 0 changed, 0 deleted, 0 unchanged + +>>> print_requests.py //unity +{ + "method": "POST", + "path": "/api/2.1/unity-catalog/model-provider-services", + "q": { + "model_provider_service_id": "myservice", + "parent": "schemas/main.myschema" + }, + "body": { + "comment": "COMMENT1", + "config": { + "custom": { + "direct": { + "api_key": { + "plaintext": "dummy-api-key" + }, + "base_url": "https://api.example.com/v1" + } + }, + "provider_type": "EXTERNAL_MODEL_PROVIDER_TYPE_CUSTOM", + "targets": [ + { + "model": "gpt-4o", + "native_api_types": [ + "openai/v1/chat/completions" + ] + } + ] + } + } +} + +=== Summary should show the id (url is not wired for these resources) +>>> [CLI] bundle summary -o json +{ + "id": "main.myschema.myservice", + "url": null +} + +=== Verify deployment +>>> [CLI] ai-gateway get-model-provider-service model-provider-services/main.myschema.myservice +{ + "name": "model-provider-services/main.myschema.myservice", + "comment": "COMMENT1" +} + +=== Update comment (should update in place, not recreate) +>>> update_file.py databricks.yml COMMENT1 COMMENT2 + +>>> [CLI] bundle deploy +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bundle/default/files... +Updated model_provider_services.mps1 +Files: 3 uploaded, 0 deleted +Resources: 0 created, 1 changed, 0 deleted, 0 unchanged + +>>> print_requests.py //unity +{ + "method": "PATCH", + "path": "/api/2.1/unity-catalog/model-provider-services/main.myschema.myservice", + "q": { + "update_mask": "comment,config" + }, + "body": { + "comment": "COMMENT2", + "config": { + "custom": { + "direct": { + "api_key": { + "plaintext": "dummy-api-key" + }, + "base_url": "https://api.example.com/v1" + } + }, + "provider_type": "EXTERNAL_MODEL_PROVIDER_TYPE_CUSTOM", + "targets": [ + { + "model": "gpt-4o", + "native_api_types": [ + "openai/v1/chat/completions" + ] + } + ] + } + } +} + +>>> [CLI] ai-gateway get-model-provider-service model-provider-services/main.myschema.myservice +"COMMENT2" + +=== Change an immutable field (should plan a recreate) +>>> update_file.py databricks.yml EXTERNAL_MODEL_PROVIDER_TYPE_CUSTOM EXTERNAL_MODEL_PROVIDER_TYPE_ANTHROPIC + +>>> [CLI] bundle plan +recreate model_provider_services.mps1 + +Plan: 1 to add, 0 to change, 1 to delete, 0 unchanged + +>>> [CLI] bundle destroy --auto-approve +The following resources will be deleted: + delete resources.model_provider_services.mps1 + +All files and directories at the following location will be deleted: /Workspace/Users/[USERNAME]/.bundle/test-bundle/default + +Destroy: 1 deleted + +>>> print_requests.py //unity +{ + "method": "DELETE", + "path": "/api/2.1/unity-catalog/model-provider-services/main.myschema.myservice" +} diff --git a/acceptance/bundle/resources/model_provider_services/basic/script b/acceptance/bundle/resources/model_provider_services/basic/script new file mode 100644 index 0000000000..799ded9e8a --- /dev/null +++ b/acceptance/bundle/resources/model_provider_services/basic/script @@ -0,0 +1,29 @@ +title "Initial summary before deploy" +trace $CLI bundle summary -o json | jq .resources.model_provider_services.mps1 + +title "Verify it does not exist yet" +trace musterr $CLI ai-gateway get-model-provider-service model-provider-services/main.myschema.myservice + +trace $CLI bundle deploy +trace print_requests.py //unity + +title "Summary should show the id (url is not wired for these resources)" +trace $CLI bundle summary -o json | jq ".resources.model_provider_services.mps1 | {id, url}" + +title "Verify deployment" +trace $CLI ai-gateway get-model-provider-service model-provider-services/main.myschema.myservice | jq '{name, comment}' + +title "Update comment (should update in place, not recreate)" +trace update_file.py databricks.yml COMMENT1 COMMENT2 +trace $CLI bundle deploy +trace print_requests.py //unity +trace $CLI ai-gateway get-model-provider-service model-provider-services/main.myschema.myservice | jq .comment + +title "Change an immutable field (should plan a recreate)" +trace update_file.py databricks.yml EXTERNAL_MODEL_PROVIDER_TYPE_CUSTOM EXTERNAL_MODEL_PROVIDER_TYPE_ANTHROPIC +trace $CLI bundle plan + +trace $CLI bundle destroy --auto-approve +trace print_requests.py //unity + +rm -f out.requests.txt diff --git a/acceptance/bundle/resources/model_provider_services/basic/test.toml b/acceptance/bundle/resources/model_provider_services/basic/test.toml new file mode 100644 index 0000000000..bd4b66fe75 --- /dev/null +++ b/acceptance/bundle/resources/model_provider_services/basic/test.toml @@ -0,0 +1,3 @@ +Ignore = [ + ".databricks", +] diff --git a/acceptance/bundle/resources/model_provider_services/remote-delete/databricks.yml b/acceptance/bundle/resources/model_provider_services/remote-delete/databricks.yml new file mode 100644 index 0000000000..aeac81263d --- /dev/null +++ b/acceptance/bundle/resources/model_provider_services/remote-delete/databricks.yml @@ -0,0 +1,20 @@ +bundle: + name: test-bundle + +resources: + model_provider_services: + mps1: + parent: schemas/main.myschema + model_provider_service_id: myservice + comment: COMMENT1 + config: + provider_type: EXTERNAL_MODEL_PROVIDER_TYPE_CUSTOM + targets: + - model: gpt-4o + native_api_types: + - openai/v1/chat/completions + custom: + direct: + base_url: https://api.example.com/v1 + api_key: + plaintext: dummy-api-key diff --git a/acceptance/bundle/resources/model_provider_services/remote-delete/out.test.toml b/acceptance/bundle/resources/model_provider_services/remote-delete/out.test.toml new file mode 100644 index 0000000000..0938e67898 --- /dev/null +++ b/acceptance/bundle/resources/model_provider_services/remote-delete/out.test.toml @@ -0,0 +1,2 @@ +Cloud = false +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"] diff --git a/acceptance/bundle/resources/model_provider_services/remote-delete/output.txt b/acceptance/bundle/resources/model_provider_services/remote-delete/output.txt new file mode 100644 index 0000000000..80fce79127 --- /dev/null +++ b/acceptance/bundle/resources/model_provider_services/remote-delete/output.txt @@ -0,0 +1,20 @@ + +>>> [CLI] bundle deploy +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bundle/default/files... +Created model_provider_services.mps1 +Files: 4 uploaded, 0 deleted +Resources: 1 created, 0 changed, 0 deleted, 0 unchanged + +=== Delete the resource out of band +>>> [CLI] ai-gateway delete-model-provider-service model-provider-services/main.myschema.myservice + +=== Plan should detect the resource is gone and re-create it +>>> [CLI] bundle plan +create model_provider_services.mps1 + +Plan: 1 to add, 0 to change, 0 to delete, 0 unchanged + +>>> [CLI] bundle destroy --auto-approve +All files and directories at the following location will be deleted: /Workspace/Users/[USERNAME]/.bundle/test-bundle/default + +Destroy: 0 deleted diff --git a/acceptance/bundle/resources/model_provider_services/remote-delete/script b/acceptance/bundle/resources/model_provider_services/remote-delete/script new file mode 100644 index 0000000000..e7377733cc --- /dev/null +++ b/acceptance/bundle/resources/model_provider_services/remote-delete/script @@ -0,0 +1,9 @@ +trace $CLI bundle deploy + +title "Delete the resource out of band" +trace $CLI ai-gateway delete-model-provider-service model-provider-services/main.myschema.myservice + +title "Plan should detect the resource is gone and re-create it" +trace $CLI bundle plan + +trace $CLI bundle destroy --auto-approve diff --git a/acceptance/bundle/resources/model_provider_services/remote-delete/test.toml b/acceptance/bundle/resources/model_provider_services/remote-delete/test.toml new file mode 100644 index 0000000000..daa475f7ce --- /dev/null +++ b/acceptance/bundle/resources/model_provider_services/remote-delete/test.toml @@ -0,0 +1,5 @@ +RecordRequests = false + +Ignore = [ + ".databricks", +] diff --git a/acceptance/bundle/resources/model_provider_services/test.toml b/acceptance/bundle/resources/model_provider_services/test.toml new file mode 100644 index 0000000000..b5361a56ad --- /dev/null +++ b/acceptance/bundle/resources/model_provider_services/test.toml @@ -0,0 +1,3 @@ +# AI Gateway securables are only deployable via the direct deployment engine +# (there is no Terraform provider path for them in bundles). +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"] diff --git a/acceptance/bundle/resources/model_services/basic/databricks.yml b/acceptance/bundle/resources/model_services/basic/databricks.yml new file mode 100644 index 0000000000..36d7b8f54b --- /dev/null +++ b/acceptance/bundle/resources/model_services/basic/databricks.yml @@ -0,0 +1,10 @@ +bundle: + name: test-bundle + +resources: + model_services: + ms1: + parent: schemas/main.myschema + model_service_id: myservice + comment: COMMENT1 + diff --git a/acceptance/bundle/resources/model_services/basic/out.test.toml b/acceptance/bundle/resources/model_services/basic/out.test.toml new file mode 100644 index 0000000000..0938e67898 --- /dev/null +++ b/acceptance/bundle/resources/model_services/basic/out.test.toml @@ -0,0 +1,2 @@ +Cloud = false +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"] diff --git a/acceptance/bundle/resources/model_services/basic/output.txt b/acceptance/bundle/resources/model_services/basic/output.txt new file mode 100644 index 0000000000..718c201575 --- /dev/null +++ b/acceptance/bundle/resources/model_services/basic/output.txt @@ -0,0 +1,92 @@ + +=== Initial summary before deploy +>>> [CLI] bundle summary -o json +{ + "comment": "COMMENT1", + "model_service_id": "myservice", + "modified_status": "created", + "parent": "schemas/main.myschema" +} + +=== Verify it does not exist yet +>>> musterr [CLI] ai-gateway get-model-service model-services/main.myschema.myservice +Error: Resource catalog.ModelService not found: main.myschema.myservice + +>>> [CLI] bundle deploy +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bundle/default/files... +Created model_services.ms1 +Files: 5 uploaded, 0 deleted +Resources: 1 created, 0 changed, 0 deleted, 0 unchanged + +>>> print_requests.py //unity +{ + "method": "POST", + "path": "/api/2.1/unity-catalog/model-services", + "q": { + "model_service_id": "myservice", + "parent": "schemas/main.myschema" + }, + "body": { + "comment": "COMMENT1" + } +} + +=== Summary should show the id (url is not wired for these resources) +>>> [CLI] bundle summary -o json +{ + "id": "main.myschema.myservice", + "url": null +} + +=== Verify deployment +>>> [CLI] ai-gateway get-model-service model-services/main.myschema.myservice +{ + "name": "model-services/main.myschema.myservice", + "comment": "COMMENT1" +} + +=== Update comment (should update in place, not recreate) +>>> update_file.py databricks.yml COMMENT1 COMMENT2 + +>>> [CLI] bundle deploy +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bundle/default/files... +Updated model_services.ms1 +Files: 3 uploaded, 0 deleted +Resources: 0 created, 1 changed, 0 deleted, 0 unchanged + +>>> print_requests.py //unity +{ + "method": "PATCH", + "path": "/api/2.1/unity-catalog/model-services/main.myschema.myservice", + "q": { + "update_mask": "comment,config" + }, + "body": { + "comment": "COMMENT2" + } +} + +>>> [CLI] ai-gateway get-model-service model-services/main.myschema.myservice +"COMMENT2" + +=== Change an immutable field (should plan a recreate) +>>> update_file.py databricks.yml myservice myservice-renamed + +>>> [CLI] bundle plan +recreate model_services.ms1 + +Plan: 1 to add, 0 to change, 1 to delete, 0 unchanged + +>>> [CLI] bundle destroy --auto-approve +The following resources will be deleted: + delete resources.model_services.ms1 + +All files and directories at the following location will be deleted: /Workspace/Users/[USERNAME]/.bundle/test-bundle/default + +Destroy: 1 deleted + +>>> print_requests.py //unity +{ + "method": "DELETE", + "path": "/api/2.1/unity-catalog/model-services/main.myschema.myservice" +} diff --git a/acceptance/bundle/resources/model_services/basic/script b/acceptance/bundle/resources/model_services/basic/script new file mode 100644 index 0000000000..ab07b28ce0 --- /dev/null +++ b/acceptance/bundle/resources/model_services/basic/script @@ -0,0 +1,29 @@ +title "Initial summary before deploy" +trace $CLI bundle summary -o json | jq .resources.model_services.ms1 + +title "Verify it does not exist yet" +trace musterr $CLI ai-gateway get-model-service model-services/main.myschema.myservice + +trace $CLI bundle deploy +trace print_requests.py //unity + +title "Summary should show the id (url is not wired for these resources)" +trace $CLI bundle summary -o json | jq ".resources.model_services.ms1 | {id, url}" + +title "Verify deployment" +trace $CLI ai-gateway get-model-service model-services/main.myschema.myservice | jq '{name, comment}' + +title "Update comment (should update in place, not recreate)" +trace update_file.py databricks.yml COMMENT1 COMMENT2 +trace $CLI bundle deploy +trace print_requests.py //unity +trace $CLI ai-gateway get-model-service model-services/main.myschema.myservice | jq .comment + +title "Change an immutable field (should plan a recreate)" +trace update_file.py databricks.yml myservice myservice-renamed +trace $CLI bundle plan + +trace $CLI bundle destroy --auto-approve +trace print_requests.py //unity + +rm -f out.requests.txt diff --git a/acceptance/bundle/resources/model_services/basic/test.toml b/acceptance/bundle/resources/model_services/basic/test.toml new file mode 100644 index 0000000000..bd4b66fe75 --- /dev/null +++ b/acceptance/bundle/resources/model_services/basic/test.toml @@ -0,0 +1,3 @@ +Ignore = [ + ".databricks", +] diff --git a/acceptance/bundle/resources/model_services/remote-delete/databricks.yml b/acceptance/bundle/resources/model_services/remote-delete/databricks.yml new file mode 100644 index 0000000000..36d7b8f54b --- /dev/null +++ b/acceptance/bundle/resources/model_services/remote-delete/databricks.yml @@ -0,0 +1,10 @@ +bundle: + name: test-bundle + +resources: + model_services: + ms1: + parent: schemas/main.myschema + model_service_id: myservice + comment: COMMENT1 + diff --git a/acceptance/bundle/resources/model_services/remote-delete/out.test.toml b/acceptance/bundle/resources/model_services/remote-delete/out.test.toml new file mode 100644 index 0000000000..0938e67898 --- /dev/null +++ b/acceptance/bundle/resources/model_services/remote-delete/out.test.toml @@ -0,0 +1,2 @@ +Cloud = false +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"] diff --git a/acceptance/bundle/resources/model_services/remote-delete/output.txt b/acceptance/bundle/resources/model_services/remote-delete/output.txt new file mode 100644 index 0000000000..c7f74e3b69 --- /dev/null +++ b/acceptance/bundle/resources/model_services/remote-delete/output.txt @@ -0,0 +1,20 @@ + +>>> [CLI] bundle deploy +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bundle/default/files... +Created model_services.ms1 +Files: 4 uploaded, 0 deleted +Resources: 1 created, 0 changed, 0 deleted, 0 unchanged + +=== Delete the resource out of band +>>> [CLI] ai-gateway delete-model-service model-services/main.myschema.myservice + +=== Plan should detect the resource is gone and re-create it +>>> [CLI] bundle plan +create model_services.ms1 + +Plan: 1 to add, 0 to change, 0 to delete, 0 unchanged + +>>> [CLI] bundle destroy --auto-approve +All files and directories at the following location will be deleted: /Workspace/Users/[USERNAME]/.bundle/test-bundle/default + +Destroy: 0 deleted diff --git a/acceptance/bundle/resources/model_services/remote-delete/script b/acceptance/bundle/resources/model_services/remote-delete/script new file mode 100644 index 0000000000..ad9170ef68 --- /dev/null +++ b/acceptance/bundle/resources/model_services/remote-delete/script @@ -0,0 +1,9 @@ +trace $CLI bundle deploy + +title "Delete the resource out of band" +trace $CLI ai-gateway delete-model-service model-services/main.myschema.myservice + +title "Plan should detect the resource is gone and re-create it" +trace $CLI bundle plan + +trace $CLI bundle destroy --auto-approve diff --git a/acceptance/bundle/resources/model_services/remote-delete/test.toml b/acceptance/bundle/resources/model_services/remote-delete/test.toml new file mode 100644 index 0000000000..daa475f7ce --- /dev/null +++ b/acceptance/bundle/resources/model_services/remote-delete/test.toml @@ -0,0 +1,5 @@ +RecordRequests = false + +Ignore = [ + ".databricks", +] diff --git a/acceptance/bundle/resources/model_services/test.toml b/acceptance/bundle/resources/model_services/test.toml new file mode 100644 index 0000000000..b5361a56ad --- /dev/null +++ b/acceptance/bundle/resources/model_services/test.toml @@ -0,0 +1,3 @@ +# AI Gateway securables are only deployable via the direct deployment engine +# (there is no Terraform provider path for them in bundles). +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"]