diff --git a/acceptance/bundle/resources/apps/lifecycle-started/output.txt b/acceptance/bundle/resources/apps/lifecycle-started/output.txt index b07d44dc5ac..cb858a8e0cc 100644 --- a/acceptance/bundle/resources/apps/lifecycle-started/output.txt +++ b/acceptance/bundle/resources/apps/lifecycle-started/output.txt @@ -84,7 +84,7 @@ Deployment complete! "description": "MY_APP_DESCRIPTION_2", "name": "[UNIQUE_NAME]" }, - "update_mask": "description" + "update_mask": "description,budget_policy_id,usage_policy_id,resources,user_api_scopes,compute_size,git_repository,telemetry_export_destinations" } } @@ -112,7 +112,7 @@ Deployment complete! "description": "MY_APP_DESCRIPTION_3", "name": "[UNIQUE_NAME]" }, - "update_mask": "description" + "update_mask": "description,budget_policy_id,usage_policy_id,resources,user_api_scopes,compute_size,git_repository,telemetry_export_destinations" } } { diff --git a/acceptance/bundle/resources/apps/update/out.requests.direct.json b/acceptance/bundle/resources/apps/update/out.requests.direct.json index 85a9ac2bc63..e33ad270576 100644 --- a/acceptance/bundle/resources/apps/update/out.requests.direct.json +++ b/acceptance/bundle/resources/apps/update/out.requests.direct.json @@ -15,7 +15,7 @@ "description": "MY_APP_DESCRIPTION", "name": "myappname" }, - "update_mask": "description" + "update_mask": "description,budget_policy_id,usage_policy_id,resources,user_api_scopes,compute_size,git_repository,telemetry_export_destinations" }, "method": "POST", "path": "/api/2.0/apps/myappname/update" diff --git a/bundle/direct/dresources/app.go b/bundle/direct/dresources/app.go index b40a22b9412..76a0881f9e5 100644 --- a/bundle/direct/dresources/app.go +++ b/bundle/direct/dresources/app.go @@ -4,7 +4,6 @@ import ( "context" "errors" "fmt" - "slices" "strings" "time" @@ -163,19 +162,23 @@ func (r *ResourceApp) DoCreate(ctx context.Context, config *AppState) (string, * return app.Name, nil, nil } +var UpdateMaskFields = []string{ + "description", + "budget_policy_id", + "usage_policy_id", + "resources", + "user_api_scopes", + "compute_size", + "git_repository", + "telemetry_export_destinations", +} + +var updateMask = strings.Join(UpdateMaskFields, ",") + func (r *ResourceApp) DoUpdate(ctx context.Context, id string, config *AppState, entry *PlanEntry) (*AppRemote, error) { // Deploy-only fields (source_code_path, config, // git_source, lifecycle) are not part of apps.App and thus excluded from the request body. if hasAppChanges(entry) { - fieldPaths := collectUpdatePathsWithPrefix(entry.Changes, "") - slices.Sort(fieldPaths) - for i, fieldPath := range fieldPaths { - fieldPaths[i] = truncateAtIndex(fieldPath) - } - fieldPaths = slices.DeleteFunc(fieldPaths, func(p string) bool { - return deployOnlyFields[p] - }) - updateMask := strings.Join(fieldPaths, ",") request := apps.AsyncUpdateAppRequest{ App: &config.App, AppName: id, diff --git a/bundle/direct/dresources/app_test.go b/bundle/direct/dresources/app_test.go index ad9ca01e8a4..9eeeef505a3 100644 --- a/bundle/direct/dresources/app_test.go +++ b/bundle/direct/dresources/app_test.go @@ -1,6 +1,9 @@ package dresources import ( + "reflect" + "slices" + "strings" "testing" "github.com/databricks/cli/libs/testserver" @@ -120,3 +123,47 @@ func TestAppDoCreate_RetriesWhenGetReturnsNotFound(t *testing.T) { assert.Equal(t, 2, createCallCount, "expected Create to be called twice") assert.Equal(t, 1, getCallCount, "expected Get to be called once to check app state") } + +func TestAppDoUpdate_UpdateMaskHasAllFields(t *testing.T) { + // iterate over all apps.App fields using reflection and ensure that UpdateMaskFields contains all of them. + config := GetGeneratedResourceConfig("apps") + require.NotNil(t, config) + var nonUpdatableFields []string + for _, field := range config.IgnoreRemoteChanges { + nonUpdatableFields = append(nonUpdatableFields, field.Field.String()) + } + + for _, field := range config.RecreateOnChanges { + nonUpdatableFields = append(nonUpdatableFields, field.Field.String()) + } + + config = GetResourceConfig("apps") + require.NotNil(t, config) + for _, field := range config.IgnoreRemoteChanges { + nonUpdatableFields = append(nonUpdatableFields, field.Field.String()) + } + + for _, field := range config.RecreateOnChanges { + nonUpdatableFields = append(nonUpdatableFields, field.Field.String()) + } + + app := apps.App{} + fields := reflect.TypeOf(app) + var allFields []string + for i := range fields.NumField() { + field := fields.Field(i) + jsonTag := field.Tag.Get("json") + if jsonTag == "" || jsonTag == "-" { + continue + } + jsonTag = strings.TrimSuffix(jsonTag, ",omitempty") + allFields = append(allFields, jsonTag) + if !slices.Contains(nonUpdatableFields, jsonTag) { + assert.Contains(t, UpdateMaskFields, jsonTag, "field %s is not in UpdateMaskFields and not marked as non-updatable", jsonTag) + } + } + + for _, field := range UpdateMaskFields { + assert.Contains(t, allFields, field, "field %s is in UpdateMaskFields but not in apps.App struct", field) + } +} diff --git a/bundle/direct/dresources/resources.yml b/bundle/direct/dresources/resources.yml index 532008296b1..569fca9ee82 100644 --- a/bundle/direct/dresources/resources.yml +++ b/bundle/direct/dresources/resources.yml @@ -385,6 +385,9 @@ resources: # drift detection applies (e.g. detecting out-of-band stop). - field: lifecycle - field: lifecycle.started + ignore_remote_changes: + - field: space # This field is not yet supported by Update APIs but exposed in the API spec. TODO: fix when update APIs supports it. + reason: managed secret_scopes: backend_defaults: