Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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