Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions NEXT_CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

### Bug Fixes

* Fix permanent drift in `databricks_model_serving` when using `*_plaintext` credential fields for external models ([#5125](https://github.com/databricks/terraform-provider-databricks/pull/5125))
* Remove unnecessary `SetSuppressDiff()` for `workload_size` in `databricks_model_serving` ([#5152](https://github.com/databricks/terraform-provider-databricks/pull/5152)).

### Documentation
Expand Down
102 changes: 102 additions & 0 deletions serving/resource_model_serving.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package serving
import (
"context"
"log"
"reflect"
"strings"
"time"

Expand Down Expand Up @@ -60,6 +61,105 @@ func suppressRouteModelEntityNameDiff(k, old, new string, d *schema.ResourceData
return false
}

// copySensitiveFields recursively copies sensitive plaintext fields from source to destination.
// This is needed because the GET API doesn't return sensitive values, causing drift in Terraform state.
// The function uses reflection to automatically handle all plaintext fields without manual enumeration.
func copySensitiveFields(src, dst reflect.Value) {
// Handle nil pointers
if !src.IsValid() || !dst.IsValid() {
return
}

// Dereference pointers
if src.Kind() == reflect.Ptr {
if src.IsNil() {
return
}
src = src.Elem()
}
if dst.Kind() == reflect.Ptr {
if dst.IsNil() {
return
}
dst = dst.Elem()
}

// Only process structs
if src.Kind() != reflect.Struct || dst.Kind() != reflect.Struct {
return
}

// Ensure types match
if src.Type() != dst.Type() {
return
}

// Iterate through all fields
for i := 0; i < src.NumField(); i++ {
srcField := src.Field(i)
dstField := dst.Field(i)
fieldType := src.Type().Field(i)

// Skip unexported fields
if !dstField.CanSet() {
continue
}

fieldName := fieldType.Name

// Check if this is a sensitive plaintext field (ends with "Plaintext")
if strings.HasSuffix(fieldName, "Plaintext") && srcField.Kind() == reflect.String {
srcValue := srcField.String()
dstValue := dstField.String()

// Copy from source to destination if source has a value and destination is empty
if srcValue != "" && dstValue == "" {
dstField.SetString(srcValue)
log.Printf("[DEBUG] Copied sensitive field %s from state", fieldName)
}
continue
}

// Recursively process nested structs, pointers, slices, and maps
switch srcField.Kind() {
case reflect.Struct:
copySensitiveFields(srcField, dstField)
case reflect.Ptr:
if !srcField.IsNil() && !dstField.IsNil() {
copySensitiveFields(srcField, dstField)
}
case reflect.Slice:
// Process slice elements (e.g., served_entities)
if srcField.Len() > 0 && dstField.Len() > 0 {
minLen := srcField.Len()
if dstField.Len() < minLen {
minLen = dstField.Len()
}
for j := 0; j < minLen; j++ {
copySensitiveFields(srcField.Index(j), dstField.Index(j))
}
}
case reflect.Map:
// Process map values if needed in the future
continue
}
}
}

// copySensitiveExternalModelFields copies sensitive plaintext credential fields from the source
// endpoint (from state) to the destination endpoint (from API response).
func copySensitiveExternalModelFields(src, dst *serving.ServingEndpointDetailed) {
if src == nil || dst == nil {
return
}

// Use reflection to copy all sensitive fields recursively
srcVal := reflect.ValueOf(src)
dstVal := reflect.ValueOf(dst)

copySensitiveFields(srcVal, dstVal)
}

// updateConfig updates the configuration of the provided serving endpoint to the provided config.
func updateConfig(ctx context.Context, w *databricks.WorkspaceClient, name string, e *serving.EndpointCoreConfigInput, d *schema.ResourceData) error {
e.Name = name
Expand Down Expand Up @@ -260,6 +360,8 @@ func ResourceModelServing() common.Resource {
if err != nil {
return err
}
// Copy sensitive plaintext fields from state to API response to prevent drift
copySensitiveExternalModelFields(&sOrig, endpoint)
if sOrig.Config == nil {
// If it is a new resource, then we only return ServedEntities
if endpoint.Config != nil {
Expand Down
2 changes: 2 additions & 0 deletions serving/resource_model_serving_provisioned_throughput.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ func ResourceModelServingProvisionedThroughput() common.Resource {
if err != nil {
return err
}
// Copy sensitive plaintext fields from state to API response to prevent drift
copySensitiveExternalModelFields(&sOrig, endpoint)
err = common.StructToData(*endpoint, s, d)
if err != nil {
return err
Expand Down
Loading
Loading