From c4bcedbb28ebbb1fc33a135afe3c69dada69130a Mon Sep 17 00:00:00 2001 From: Jan Rose Date: Wed, 4 Nov 2026 15:41:34 +0000 Subject: [PATCH] Define deploy and destroy approval prompts in one table Deploy and destroy each kept their own list of groups that get a consent prompt, so a group added to one could silently miss the other. Both lists now derive from one table, and a test names the groups that have no destroy prompt. Co-authored-by: Isaac --- bundle/phases/approval.go | 56 ++++++++++++++++++++++++++++++++++ bundle/phases/approval_test.go | 30 ++++++++++++++++++ bundle/phases/deploy.go | 16 ---------- bundle/phases/destroy.go | 15 --------- 4 files changed, 86 insertions(+), 31 deletions(-) create mode 100644 bundle/phases/approval_test.go diff --git a/bundle/phases/approval.go b/bundle/phases/approval.go index fdbcd8ecea9..b364c1c7443 100644 --- a/bundle/phases/approval.go +++ b/bundle/phases/approval.go @@ -15,6 +15,62 @@ type approvalGroup struct { skipChildren bool // skip actions where IsChildResource() is true } +// approvalEntry is one resource type in approvalTable, the single list that +// deploy and destroy derive their prompts from. +type approvalEntry struct { + group string + deployMessage string + deploySkipChildren bool + // destroyMessage is nil when destroy has no prompt of its own for the group. + destroyMessage *string +} + +// approvalTable is in deploy's order. Destroy's groups keep the same relative order. +var approvalTable = []approvalEntry{ + {group: "schemas", deployMessage: deleteOrRecreateSchemaMessage, deploySkipChildren: true, destroyMessage: new(deleteSchemaMessage)}, + // Destroy prompts for pipelines in logPipelineDeleteApproval, which picks the message + // per pipeline from its cascade_on_destroy setting. + {group: "pipelines", deployMessage: deleteOrRecreatePipelineMessage}, + {group: "volumes", deployMessage: deleteOrRecreateVolumeMessage, destroyMessage: new(deleteVolumeMessage)}, + // Deploy only. + {group: "dashboards", deployMessage: deleteOrRecreateDashboardMessage}, + {group: "database_instances", deployMessage: deleteOrRecreateDatabaseInstanceMessage, destroyMessage: new(deleteDatabaseInstanceMessage)}, + {group: "synced_database_tables", deployMessage: deleteOrRecreateSyncedDatabaseTableMessage, destroyMessage: new(deleteSyncedDatabaseTableMessage)}, + {group: "postgres_projects", deployMessage: deleteOrRecreatePostgresProjectMessage, destroyMessage: new(deletePostgresProjectMessage)}, + {group: "postgres_branches", deployMessage: deleteOrRecreatePostgresBranchMessage, destroyMessage: new(deletePostgresBranchMessage)}, + {group: "postgres_databases", deployMessage: deleteOrRecreatePostgresDatabaseMessage, destroyMessage: new(deletePostgresDatabaseMessage)}, + {group: "vector_search_indexes", deployMessage: deleteOrRecreateVectorSearchIndexMessage, destroyMessage: new(deleteVectorSearchIndexMessage)}, + {group: "genie_spaces", deployMessage: deleteOrRecreateGenieSpaceMessage, destroyMessage: new(deleteGenieSpaceMessage)}, + // Deploy only. + {group: "materialization_sets", deployMessage: deleteOrRecreateMaterializationSetMessage}, + {group: "features", deployMessage: deleteOrRecreateFeatureMessage, destroyMessage: new(deleteFeatureMessage)}, +} + +var deployApprovalGroups = deployGroups() + +var destroyApprovalGroups = destroyGroups() + +// deployGroups returns the groups deploy prompts for, with the skipChildren setting for schemas. +func deployGroups() []approvalGroup { + groups := make([]approvalGroup, 0, len(approvalTable)) + for _, e := range approvalTable { + groups = append(groups, approvalGroup{group: e.group, message: e.deployMessage, skipChildren: e.deploySkipChildren}) + } + return groups +} + +// destroyGroups returns the groups destroy prompts for through logApprovalGroups. +func destroyGroups() []approvalGroup { + var groups []approvalGroup + for _, e := range approvalTable { + if e.destroyMessage == nil { + continue + } + groups = append(groups, approvalGroup{group: e.group, message: *e.destroyMessage}) + } + return groups +} + // logApprovalGroups filters actions per group and prints non-empty groups. // If trailingNewline is true, an empty line is printed after each non-empty group. // Returns the total number of matched actions across all groups. diff --git a/bundle/phases/approval_test.go b/bundle/phases/approval_test.go new file mode 100644 index 00000000000..6fea72ee3d0 --- /dev/null +++ b/bundle/phases/approval_test.go @@ -0,0 +1,30 @@ +package phases + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// Every group in approvalTable needs a deploy prompt. Groups without a destroy prompt +// are listed explicitly, so adding a group to only one command is a visible choice here. +func TestApprovalTableEntries(t *testing.T) { + var noDestroyPrompt []string + for _, e := range approvalTable { + require.NotEmpty(t, e.deployMessage, "group %s has no deploy message", e.group) + if e.destroyMessage == nil { + noDestroyPrompt = append(noDestroyPrompt, e.group) + continue + } + assert.NotEmpty(t, *e.destroyMessage, "group %s has an empty destroy message", e.group) + } + + assert.ElementsMatch(t, []string{ + // Deploy only. + "dashboards", + "materialization_sets", + // Destroy prompts for pipelines in logPipelineDeleteApproval instead. + "pipelines", + }, noDestroyPrompt) +} diff --git a/bundle/phases/deploy.go b/bundle/phases/deploy.go index c3c3306130e..49b234230a4 100644 --- a/bundle/phases/deploy.go +++ b/bundle/phases/deploy.go @@ -28,22 +28,6 @@ import ( "github.com/databricks/databricks-sdk-go/service/bundledeployments" ) -var deployApprovalGroups = []approvalGroup{ - {group: "schemas", message: deleteOrRecreateSchemaMessage, skipChildren: true}, - {group: "pipelines", message: deleteOrRecreatePipelineMessage}, - {group: "volumes", message: deleteOrRecreateVolumeMessage}, - {group: "dashboards", message: deleteOrRecreateDashboardMessage}, - {group: "database_instances", message: deleteOrRecreateDatabaseInstanceMessage}, - {group: "synced_database_tables", message: deleteOrRecreateSyncedDatabaseTableMessage}, - {group: "postgres_projects", message: deleteOrRecreatePostgresProjectMessage}, - {group: "postgres_branches", message: deleteOrRecreatePostgresBranchMessage}, - {group: "postgres_databases", message: deleteOrRecreatePostgresDatabaseMessage}, - {group: "vector_search_indexes", message: deleteOrRecreateVectorSearchIndexMessage}, - {group: "genie_spaces", message: deleteOrRecreateGenieSpaceMessage}, - {group: "materialization_sets", message: deleteOrRecreateMaterializationSetMessage}, - {group: "features", message: deleteOrRecreateFeatureMessage}, -} - func approvalForDeploy(ctx context.Context, b *bundle.Bundle, plan *deployplan.Plan) (bool, error) { actions := plan.GetActions() diff --git a/bundle/phases/destroy.go b/bundle/phases/destroy.go index b3e885e54d4..b2037253667 100644 --- a/bundle/phases/destroy.go +++ b/bundle/phases/destroy.go @@ -33,21 +33,6 @@ func assertRootPathExists(ctx context.Context, b *bundle.Bundle) (bool, error) { return true, err } -var destroyApprovalGroups = []approvalGroup{ - {group: "schemas", message: deleteSchemaMessage}, - // Pipelines are handled separately in approvalForDestroy so the message reflects each - // pipeline's cascade_on_destroy setting; see logPipelineDeleteApproval. - {group: "volumes", message: deleteVolumeMessage}, - {group: "database_instances", message: deleteDatabaseInstanceMessage}, - {group: "synced_database_tables", message: deleteSyncedDatabaseTableMessage}, - {group: "postgres_projects", message: deletePostgresProjectMessage}, - {group: "postgres_branches", message: deletePostgresBranchMessage}, - {group: "postgres_databases", message: deletePostgresDatabaseMessage}, - {group: "vector_search_indexes", message: deleteVectorSearchIndexMessage}, - {group: "genie_spaces", message: deleteGenieSpaceMessage}, - {group: "features", message: deleteFeatureMessage}, -} - // logPipelineDeleteApproval prints the pipeline deletions. If cascade_on_destroy is true, we will include // a note that datasets will be deleted as well. func logPipelineDeleteApproval(ctx context.Context, b *bundle.Bundle, actions []deployplan.Action, quiet bool) error {