Skip to content
Merged
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
19 changes: 13 additions & 6 deletions otdfctl/docs/man/migrate/prune/namespaced-policy.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ The parent `migrate` command provides the shared `--commit` and `--interactive`
An object is safe to delete only when prune can tie the legacy source object to the expected migrated target and prove the source is no longer needed.

- `delete`: the source has the expected migrated target and prune found no remaining legacy dependency that still requires the source object.
- `blocked`: prune will not delete the source. Common reasons are that the source is still referenced by legacy policy or that the source object has not actually been migrated yet.
- `unresolved`: prune found something close to a migrated target, but it cannot prove the source and target match safely. Common reasons are missing or mismatched `migrated_from` labels, no matching labeled target, or a registered resource source that still contains values outside the resolved migration view.
- `blocked`: prune will not delete the source. Common reasons are that the source is still referenced by legacy policy, that the source object has not actually been migrated yet, or that an unmigrated registered resource spans multiple target namespaces and must be deleted manually.
- `unresolved`: prune found something close to a migrated target, but it cannot prove the source and target match safely. Common reasons are missing or mismatched `migrated_from` labels or no matching labeled target.

In practice, prune relies on current legacy references plus `migrated_from` metadata on the namespaced targets. If that evidence is incomplete or inconsistent, the object is left in place instead of being deleted.

Expand Down Expand Up @@ -63,10 +63,17 @@ otdfctl migrate prune namespaced-policy --scope=obligation-triggers --interactiv

## Other Information

1. Action / Subject-Condition-Set pruning
Action / Subject-Condition-Set pruning

1a. Actions and subject-condition-sets are pruned a little differently from the other scopes. Instead of reusing the resolved migration view, prune classifies them directly from the current legacy objects, their current legacy references, and the canonical migrated targets it can find.
- Actions and subject-condition-sets are pruned a little differently from the other scopes. Instead of reusing the resolved migration view, prune classifies them directly from the current legacy objects, their current legacy references, and the canonical migrated targets it can find.
- They are expected to be pruned last. By the time you reach those scopes, their legacy dependents such as subject mappings, registered resources, and obligation triggers should already be gone, so the safest decision comes from checking the live legacy dependency graph at prune time.
- Some actions or subject-condition-sets that were never used by any other legacy policy object can still end up `blocked`.
- If prune cannot find a canonical migrated target for the source object, it leaves the source in place as `blocked` instead of assuming it is safe to delete.
- Example: if a custom action `decrypt` is no longer referenced by any legacy subject mapping, registered resource, or obligation trigger and prune finds a namespaced `decrypt` target with `metadata.labels.migrated_from=<legacy-action-id>`, the source action is safe to delete. If no canonical namespaced `decrypt` target exists, the source action is reported as `blocked` because prune cannot prove that the object was actually migrated.

1b. We do that because actions and subject-condition-sets are expected to be pruned last. By the time you reach those scopes, their legacy dependents such as subject mappings, registered resources, and obligation triggers should already be gone, so the safest decision comes from checking the live legacy dependency graph at prune time. That also means some actions or subject-condition-sets that were never used by any other legacy policy object can still end up `blocked`.
Registered-Resource manual deletion

1c. If prune cannot find a canonical migrated target for the source object, it leaves the source in place as `blocked` instead of assuming it is safe to delete. For example, if a custom action `decrypt` is no longer referenced by any legacy subject mapping, registered resource, or obligation trigger and prune finds a namespaced `decrypt` target with `metadata.labels.migrated_from=<legacy-action-id>`, the source action is safe to delete. If no canonical namespaced `decrypt` target exists, the source action is reported as `blocked` because prune cannot prove that the object was actually migrated.
- A legacy registered resource can only be auto-pruned when migration resolved it to one target namespace and prune can match it to the expected migrated target.
- If a registered resource spans multiple target namespaces and was never migrated, prune reports it as `blocked` with the `MultiNamespaceManualDelete` reason.
- In that case prune does not guess which namespace copy should have existed, and it does not delete the legacy source automatically.
- Clean this up manually after you verify the intended namespaced registered resources already exist and the legacy object is no longer needed.
6 changes: 1 addition & 5 deletions otdfctl/migrations/namespacedpolicy/prune_details.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,15 +186,11 @@ func (p *PruneSubjectMappingPlan) pruneDetails(styled bool, styles *migrations.D
}

func (p *PruneRegisteredResourcePlan) pruneDetails(styled bool, styles *migrations.DisplayStyles) []string {
details := []string{
return []string{
formatPruneDetail("source_id", pruneIDValue(styled, styles, p.Source.GetId())),
registeredResourceSourcePruneDetail("source", p.Source, styled, styles),
targetRefPruneDetail("found_migrated_target", p.MigratedTarget, styled, styles),
}
if !styled && p.Reason.Type == PruneStatusReasonTypeRegisteredResourceSourceMismatch {
details = append(details, formatPruneDetail("full_source", plainRegisteredResourceSourceSummary(p.FullSource)))
}
return details
}

func (p *PruneObligationTriggerPlan) pruneDetails(styled bool, styles *migrations.DisplayStyles) []string {
Expand Down
14 changes: 0 additions & 14 deletions otdfctl/migrations/namespacedpolicy/prune_execute_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,24 +173,10 @@ func mixedPrunePlan(scope Scope) *PrunePlan {
RegisteredResources: []*PruneRegisteredResourcePlan{
{
Source: &policy.RegisteredResource{Id: "resource-delete-1"},
FullSource: &policy.RegisteredResource{
Id: "resource-delete-1",
Values: []*policy.RegisteredResourceValue{
{Id: "value-1"},
{Id: "value-2"},
},
},
Status: PruneStatusDelete,
},
{
Source: &policy.RegisteredResource{Id: "resource-delete-2"},
FullSource: &policy.RegisteredResource{
Id: "resource-delete-2",
Values: []*policy.RegisteredResourceValue{
{Id: "value-3"},
{Id: "value-4"},
},
},
Status: PruneStatusDelete,
},
{
Expand Down
40 changes: 19 additions & 21 deletions otdfctl/migrations/namespacedpolicy/prune_plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,23 +20,23 @@ const (
type PruneStatusReasonType string

const (
PruneStatusReasonTypeMigratedTargetNotFound PruneStatusReasonType = "MigratedTargetNotFound"
PruneStatusReasonTypeNoMatchingLabelsFound PruneStatusReasonType = "NoMatchingLabelsFound"
PruneStatusReasonTypeMismatchedMigrationLabel PruneStatusReasonType = "MismatchedMigrationLabel"
PruneStatusReasonTypeMissingMigrationLabel PruneStatusReasonType = "MissingMigrationLabel"
PruneStatusReasonTypeInUse PruneStatusReasonType = "InUse"
PruneStatusReasonTypeNeedsMigration PruneStatusReasonType = "NeedsMigration"
PruneStatusReasonTypeRegisteredResourceSourceMismatch PruneStatusReasonType = "RegisteredResourceSourceMismatch"
PruneStatusReasonTypeSkippedByUser PruneStatusReasonType = "SkippedByUser"

pruneStatusReasonMessageMigratedTargetNotFound = "no migrated target was found for this source"
pruneStatusReasonMessageInUse = "source object is still referenced by legacy policy"
pruneStatusReasonMessageNoMatchingLabelsFound = "canonical migrated targets were found, but none carry migrated_from for this source"
pruneStatusReasonMessageMismatchedMigrationLabel = "migrated target carries migrated_from metadata for a different source"
pruneStatusReasonMessageMissingMigrationLabel = "migrated target is missing migrated_from metadata for this source"
pruneStatusReasonMessageNeedsMigration = "source object does not have a migrated target yet"
pruneStatusReasonMessageSkippedByUser = "skipped by user"
pruneStatusReasonMessageRegisteredResourceSourceMismatchFmt = "resolved registered resource view does not match the full source object for target namespace %q; source contains values outside the resolved migration view"
PruneStatusReasonTypeMigratedTargetNotFound PruneStatusReasonType = "MigratedTargetNotFound"
PruneStatusReasonTypeNoMatchingLabelsFound PruneStatusReasonType = "NoMatchingLabelsFound"
PruneStatusReasonTypeMismatchedMigrationLabel PruneStatusReasonType = "MismatchedMigrationLabel"
PruneStatusReasonTypeMissingMigrationLabel PruneStatusReasonType = "MissingMigrationLabel"
PruneStatusReasonTypeInUse PruneStatusReasonType = "InUse"
PruneStatusReasonTypeNeedsMigration PruneStatusReasonType = "NeedsMigration"
PruneStatusReasonTypeMultiNamespaceManualDelete PruneStatusReasonType = "MultiNamespaceManualDelete"
PruneStatusReasonTypeSkippedByUser PruneStatusReasonType = "SkippedByUser"

pruneStatusReasonMessageMigratedTargetNotFound = "no migrated target was found for this source"
pruneStatusReasonMessageInUse = "source object is still referenced by legacy policy"
pruneStatusReasonMessageNoMatchingLabelsFound = "canonical migrated targets were found, but none carry migrated_from for this source"
pruneStatusReasonMessageMismatchedMigrationLabel = "migrated target carries migrated_from metadata for a different source"
pruneStatusReasonMessageMissingMigrationLabel = "migrated target is missing migrated_from metadata for this source"
pruneStatusReasonMessageNeedsMigration = "source object does not have a migrated target yet"
pruneStatusReasonMessageMultiNamespaceManualDelete = "registered resource spans multiple target namespaces and was not migrated; must be deleted manually"
pruneStatusReasonMessageSkippedByUser = "skipped by user"
)

type PruneStatusReason struct {
Expand Down Expand Up @@ -310,10 +310,8 @@ func (p *PruneSubjectMappingPlan) setExecution(execution *ExecutionResult) {
// for deletion and the single migrated target RR matched to it by migration
// metadata.
type PruneRegisteredResourcePlan struct {
// Source is the resolved RR source from planning and may be filtered by interactive review.
Source *policy.RegisteredResource `json:"source"`
// FullSource is the authoritative RR source reloaded from the global namespace for prune verification.
FullSource *policy.RegisteredResource `json:"full_source,omitempty"`
// Source is the legacy RR source being considered for deletion.
Source *policy.RegisteredResource `json:"source"`
Status PruneStatus `json:"status"`
MigratedTarget TargetRef `json:"migrated_target,omitzero"`
Reason PruneStatusReason `json:"reason,omitzero"`
Expand Down
Loading
Loading