Skip to content

✨ Undeprecate plugin optionals - #794

Merged
aufi merged 4 commits into
migtools:mainfrom
aufi:transform-optionals
Aug 14, 2026
Merged

✨ Undeprecate plugin optionals#794
aufi merged 4 commits into
migtools:mainfrom
aufi:transform-optionals

Conversation

@aufi

@aufi aufi commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

There was a deprecated transform flag plugin optionals allowing pass args to plugins (from #545), this is now needed for e.g. PVC mapping for data migration flow support or more powerful plugins. This PR 1. un-deprecates this flag and 2. adds its full support for multistage transformations including instructions file update.

Doc update is not part of this PR as I understood it should be automated in different PR. Examples on use-cases is at https://gist.github.com/aufi/f7421e1238efb760c412b77e9261bdf1, cc @Tamar-Dinavetsky

Fixes: #791

Summary by CodeRabbit

New Features

  • Added stage-specific optional flags for transform commands.
  • Configure stage options with repeatable --stage-optionals arguments or instructions files.
  • Instructions files support stage entries with optional flags while retaining compatibility with existing stage lists.
  • Stage-specific options override global options, with global settings used as a fallback.

Bug Fixes

  • Added validation for malformed, duplicate, empty, or invalid stage option values.
  • Prevented combining instructions files with command-line stage options.

Improvements

  • The optionals command is no longer marked as deprecated.

There was a deprecated transform flag plugin optionals allowing pass
args to plugins, this is now needed for e.g. PVC mapping or more
powerful plugins. This PR 1. un-deprecates this flag and 2. adds its full
support for multistage transformations including instructions file
update.

Fixes: migtools#791

Signed-off-by: Marek Aufart <maufart@redhat.com>
@aufi aufi changed the title Undeprecate plugin optionals ✨ Undeprecate plugin optionals Aug 12, 2026
@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The transform command now supports repeatable stage-specific optional flags from CLI arguments or instructions files. YAML stages can include optional configurations. The orchestrator resolves stage-specific flags with global fallback, and the E2E runner forwards these arguments.

Changes

Stage-specific transform optionals

Layer / File(s) Summary
Instruction stage optionals
internal/transform/instructions.go, internal/transform/instructions_test.go
Instructions files accept string or object stages. Object stages can define name and optionals. Stage names and optional keys are normalized and validated.
Command input and validation
cmd/transform/optionals/optionals.go, cmd/transform/transform.go, cmd/transform/transform_test.go
The optionals command is no longer deprecated. The transform command adds repeatable --stage-optionals arguments, validates conflicts, parses StageName=JSON, and passes normalized values to the orchestrator.
Stage-specific orchestration
internal/transform/orchestrator.go, internal/transform/orchestrator_test.go, e2e-tests/framework/crane.go
The orchestrator prefers stage-specific flags and falls back to global flags. The E2E runner forwards each stage optional as a command-line argument.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant TransformCommand
  participant InstructionsFile
  participant Orchestrator
  participant TransformRunner
  TransformCommand->>InstructionsFile: load stage names and optionals
  TransformCommand->>Orchestrator: pass StageOptionalFlags
  Orchestrator->>TransformRunner: run stage with resolved flags
Loading

Possibly related PRs

  • migtools/crane#211: Both changes update cmd/transform/transform.go and internal/transform/orchestrator.go for stage-related transform configuration.
  • migtools/crane#256: Both changes modify internal/transform/orchestrator.go for multi-stage transform execution.
  • migtools/crane#433: Both changes update e2e-tests/framework/crane.go and transform instructions-file coverage.

Suggested labels: ready-for-review-notified

Suggested reviewers: msajidmansoori12, stillalearner

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes restore optionals and add multistage support through CLI flags, instructions, orchestration, and tests as required by issue #791.
Out of Scope Changes check ✅ Passed The changes support the optionals feature and multistage requirements; no unrelated code changes are evident.
Docstring Coverage ✅ Passed Docstring coverage is 83.33% which is sufficient. The required threshold is 80.00%.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the primary change: restoring the plugin optionals feature.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cmd/transform/transform.go`:
- Around line 362-366: Update the stage-optionals parsing around json.Unmarshal
so the decoded value must be a JSON object and reject null before assigning
result[stageName]. Preserve the existing invalid-JSON error and ensure null
produces an error instead of an empty stage map, allowing resolveOptionalFlags
to retain global-flag fallback behavior.
- Line 164: Change the stage-optionals flag registration in the command’s option
setup from StringSliceVar to StringArrayVar so each repeated StageName=JSON
argument remains intact, including JSON containing commas. Add a command-level
regression test covering a multi-field JSON stage optional and verify
parseStageOptionals receives and parses it as one value.

In `@internal/transform/instructions.go`:
- Around line 182-186: Reject case-insensitive duplicate optional keys before
normalization instead of allowing map iteration to overwrite values: update the
optional-key handling around the normalized map construction in
internal/transform/instructions.go (lines 182-186) to validate and return an
error on collisions, and add the same validation before optionalFlagsToLower in
cmd/transform/transform.go (lines 362-366). Add regression tests covering both
input paths and preserve the non-destructive export → transform → apply
behavior.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f06d16e1-e90b-4970-80fb-1645877f274e

📥 Commits

Reviewing files that changed from the base of the PR and between 876005a and d61cf67.

📒 Files selected for processing (8)
  • cmd/transform/optionals/optionals.go
  • cmd/transform/transform.go
  • cmd/transform/transform_test.go
  • e2e-tests/framework/crane.go
  • internal/transform/instructions.go
  • internal/transform/instructions_test.go
  • internal/transform/orchestrator.go
  • internal/transform/orchestrator_test.go

Comment thread cmd/transform/transform.go Outdated
Comment thread cmd/transform/transform.go Outdated
Comment thread internal/transform/instructions.go
Signed-off-by: Marek Aufart <maufart@redhat.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
internal/transform/instructions.go (1)

182-189: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Use one shared optional-key normalizer.

StageOptionals repeats the lowercasing and collision checks already used by optionalFlagsToLowerChecked in cmd/transform/transform.go. These input paths can diverge when the normalization policy changes. Move the shared utility to crane-lib and call it from both paths.

As per coding guidelines: “Place shared utilities in crane-lib rather than duplicating them.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/transform/instructions.go` around lines 182 - 189, Move the
optional-key normalization and case-insensitive collision validation from the
StageOptionals logic into a shared crane-lib utility, preserving its error
behavior and normalized map output. Update both StageOptionals and
optionalFlagsToLowerChecked in cmd/transform/transform.go to call that utility,
removing their duplicated lowercasing and collision-checking logic.

Source: Coding guidelines

cmd/transform/transform_test.go (1)

1214-1231: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Exercise flag binding in the comma regression test.

TestParseStageOptionals_MultiFieldJSON passes an already intact string directly to parseStageOptionals. It would pass even if command flag binding split comma-containing values before parsing. Add a command-level test that supplies --stage-optionals and verifies that one intact value reaches the parser.

As per coding guidelines: “Add tests for new features and regression tests for bug fixes when possible.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/transform/transform_test.go` around lines 1214 - 1231, Extend
TestParseStageOptionals_MultiFieldJSON with command-level flag binding: supply
the multi-field JSON through --stage-optionals using the project’s command/flag
setup, execute parsing, and verify both fields are preserved from one intact
value. Do not rely solely on directly calling parseStageOptionals; ensure the
test exercises StringArrayVar behavior before reaching that parser.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@cmd/transform/transform_test.go`:
- Around line 1214-1231: Extend TestParseStageOptionals_MultiFieldJSON with
command-level flag binding: supply the multi-field JSON through
--stage-optionals using the project’s command/flag setup, execute parsing, and
verify both fields are preserved from one intact value. Do not rely solely on
directly calling parseStageOptionals; ensure the test exercises StringArrayVar
behavior before reaching that parser.

In `@internal/transform/instructions.go`:
- Around line 182-189: Move the optional-key normalization and case-insensitive
collision validation from the StageOptionals logic into a shared crane-lib
utility, preserving its error behavior and normalized map output. Update both
StageOptionals and optionalFlagsToLowerChecked in cmd/transform/transform.go to
call that utility, removing their duplicated lowercasing and collision-checking
logic.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 68d98acc-4250-49dd-b8ca-1a83cfc19d91

📥 Commits

Reviewing files that changed from the base of the PR and between d61cf67 and 2fd9033.

📒 Files selected for processing (4)
  • cmd/transform/transform.go
  • cmd/transform/transform_test.go
  • internal/transform/instructions.go
  • internal/transform/instructions_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • cmd/transform/transform.go
  • internal/transform/instructions_test.go

aufi added 2 commits August 12, 2026 10:37
Signed-off-by: Marek Aufart <maufart@redhat.com>
Signed-off-by: Marek Aufart <maufart@redhat.com>
@aufi

aufi commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

/rfr

@aufi

aufi commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

[review-docs]

@Tamar-Dinavetsky

Tamar-Dinavetsky commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

📚 Documentation Review

Analyzed PR: #794
Latest commit: 512677e

Found 2 file(s) that may need updates:

📋 Select files to update

Uncheck any files you do not want updated:

  • multistage-pipeline.md: This documentation outlines the multi-stage Kustomize pipeline feature in Crane for managing resource transformations. I suggest adding sections to explain the new support for passing optional plugin configuration flags via CLI arguments and the newly implemented instructions file workflow for better pipeline reproducibility.
  • commands/transform.md: The documentation explains how to use the crane transform command to manage multi-stage resource transformations via plugins and Kustomize. I suggest updating the documentation to include the new "Configuring Optional Plugin Flags" section, which details how to pass custom configuration flags to plugins both globally and on a per-stage basis.

💡 Next Steps:

  • Uncheck any files above that you don't want updated
  • When ready, comment [​update-docs] to generate a PR with only the checked files
  • You can add instructions in your [​update-docs] comment:
    • Global (first line): [​update-docs] keep changes minimal, don't add new sections
    • Per-file (next lines): config-ref.rst: only update the CLI usage example

Powered by code-to-docs AI

@aufi

aufi commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

[​update-docs] first read https://gist.github.com/aufi/f7421e1238efb760c412b77e9261bdf1 to understand how this change is expected to be used by end-users

@stillalearner stillalearner left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

csoceanu added a commit to redhat-community-ai-tools/code-to-docs that referenced this pull request Aug 13, 2026
Bot-posted PR comments on fork PRs contained literal [update-docs]
text in the footer, which re-triggered the workflow when the bot user
was in the allowed users list — creating ~20 duplicate comments
(see migtools/crane#794).

Also escapes [review-feature] and [review-docs] in error comments
posted when Jira credentials are missing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@aufi
aufi merged commit 546eefc into migtools:main Aug 14, 2026
10 checks passed
@aufi

aufi commented Aug 14, 2026

Copy link
Copy Markdown
Contributor Author

[update-docs] Take into account following usage examples for feature in this PR:

Transform Optional Flags: Usage Examples

Optional flags control plugin behavior during crane transform. They can be set globally (for all stages) or per-stage (overriding the global value for that stage).

Discovering Available Flags

crane transform optionals

Output lists flags per plugin with descriptions and examples:

Plugin: KubernetesPlugin (version 0.1.6)
    registry-replacement: Map of image registry paths to swap on transform
        Example: docker-registry.default.svc:5000=image-registry.openshift-image-registry.svc:5000
    add-annotations: Annotations to add to each resource
        Example: annotation1=value1,annotation2=value2
    strip-default-rbac: Whether to strip default RBAC including default serviceAccount (default: true)
        Example: true
    ...

Global Optional Flags

--optional-flags (applies to all stages)

Pass a JSON object. All plugins in all stages receive these flags.

# Registry replacement for all stages
crane transform --optional-flags '{"registry-replacement": "docker.io=quay.io"}'

# Multiple flags
crane transform --optional-flags '{"registry-replacement": "docker.io=quay.io", "add-annotations": "migrated-by=crane"}'

# Disable default RBAC stripping
crane transform --optional-flags '{"strip-default-rbac": "false"}'

Per-Stage Optional Flags (CLI)

--stage-optionals (repeatable, per-stage override)

Format: StageName=JSON. The stage name is the plugin name (e.g. KubernetesPlugin), not the directory name (10_KubernetesPlugin).

# Different registry replacement per stage
crane transform \
  --stage-optionals 'KubernetesPlugin={"registry-replacement": "docker.io=quay.io"}' \
  --stage-optionals 'OpenshiftPlugin={"registry-replacement": "docker.io=registry.redhat.io"}'

Combining global and per-stage

Per-stage overrides the entire global set for that stage (no merging). Stages without per-stage flags inherit global.

# Global: add annotations to all stages
# KubernetesPlugin: also replace registry (overrides global entirely)
crane transform \
  --optional-flags '{"add-annotations": "env=prod"}' \
  --stage-optionals 'KubernetesPlugin={"registry-replacement": "docker.io=quay.io", "add-annotations": "env=prod"}'

Note: because per-stage overrides (not merges), KubernetesPlugin must repeat add-annotations if it still needs it.

PVC rename with registry swap

crane transform \
  --stage-optionals 'KubernetesPlugin={"registry-replacement": "docker.io=quay.io", "pvc-rename-map": "old-data:new-data,old-logs:new-logs"}'

Selective whiteout control

# Keep only Deployments and Services, whiteout everything else
crane transform \
  --stage-optionals 'KubernetesPlugin={"include-only": "Deployment.apps,Service"}'

# Extra whiteouts on top of defaults
crane transform \
  --stage-optionals 'KubernetesPlugin={"extra-whiteouts": "Route.route.openshift.io,ImageStream.image.openshift.io"}'

Per-Stage Optional Flags (Instructions File)

Mixed-list format

Stages can be plain strings or objects with name and optionals. Plain strings inherit global --optional-flags.

# instructions.yaml
stages:
  - KubernetesPlugin
  - name: OpenshiftPlugin
    optionals:
      registry-replacement: "docker.io=registry.redhat.io"
  - CustomEdits
crane transform --instructions-file instructions.yaml

All stages with optionals

stages:
  - name: KubernetesPlugin
    optionals:
      registry-replacement: "docker.io=quay.io"
      add-annotations: "migrated-by=crane,env=production"
      strip-default-rbac: "false"
  - name: OpenshiftPlugin
    optionals:
      registry-replacement: "docker.io=registry.redhat.io"

Instructions file + global CLI fallback

Stages without optionals in the file inherit the global --optional-flags value:

# instructions.yaml
stages:
  - KubernetesPlugin
  - name: OpenshiftPlugin
    optionals:
      registry-replacement: "docker.io=registry.redhat.io"
# KubernetesPlugin gets the global flags; OpenshiftPlugin gets its own
crane transform \
  --instructions-file instructions.yaml \
  --optional-flags '{"add-annotations": "migrated-by=crane"}'

Backward-compatible format

Old instructions files still work identically:

stages:
  - KubernetesPlugin
  - OpenshiftPlugin

Precedence Rules

Mode Highest priority Lowest priority
With --instructions-file per-stage optionals from file --optional-flags CLI
Without --instructions-file --stage-optionals CLI --optional-flags CLI

Per-stage overrides (replaces) the global set for that stage. There is no merging.

Mutual Exclusivity

--instructions-file and --stage-optionals cannot be used together:

# This fails with an error
crane transform \
  --instructions-file instructions.yaml \
  --stage-optionals 'KubernetesPlugin={"registry-replacement": "docker.io=quay.io"}'

Use optionals inside the instructions file instead.

KubernetesPlugin Available Flags

Flag Description Example
registry-replacement Registry path swap map docker.io=quay.io,gcr.io=ghcr.io
add-annotations Annotations to add key1=val1,key2=val2
remove-annotations Annotations to remove annotation1,annotation2
strip-default-rbac Strip default RBAC (default: true) true / false
strip-default-cabundle Strip default CA bundle (default: true) true / false
disable-whiteout-owned Disable whiting out owned resources true
extra-whiteouts Additional resources to whiteout Deployment.apps,Route.route.openshift.io
include-only Keep only listed resources Deployment.apps,Service
pvc-rename-map PVC rename mapping old-name:new-name

@github-actions

Copy link
Copy Markdown
Contributor

📚 Documentation Update

Updated 2 file(s) based on your review selections:

  • multistage-pipeline.md
  • commands/transform.md

📄 Changes

multistage-pipeline.md

View diff
--- a/multistage-pipeline.md
+++ b/multistage-pipeline.md
@@ -138,6 +138,22 @@
 crane transform 10_KubernetesPlugin 20_OpenshiftPlugin

+#### Optional Flags
+
+You can pass configuration parameters to plugins globally or per-stage.
+
+Global Optional Flags:
+Passed to all plugins.
+bash +crane transform --optional-flags='{"registry-replacement": "docker.io=quay.io"}' +
+
+Per-Stage Optional Flags:
+Target specific plugins by stage name.
+bash +crane transform --stage-optionals 'KubernetesPlugin={"registry-replacement":"docker.io=quay.io"}' +
+

Force Overwrite

Override dirty check protection:
@@ -232,30 +248,20 @@
kubectl apply -f output/output.yaml


-### Example 2: Multi-Stage Pipeline
-
-```bash
-# Export resources
-crane export --kubeconfig source.yaml --export-dir export
-
-# Create Kubernetes base transformations
-crane transform \
-  --export-dir export \
-  --transform-dir transform \
-  10_KubernetesPlugin
-
-# Create OpenShift-specific transformations
-crane transform \
-  --transform-dir transform \
-  20_OpenshiftPlugin
-
-# Create ImageStream transformations
-crane transform \
-  --transform-dir transform \
-  30_ImagestreamPlugin
-
-# Apply all stages
-crane apply --transform-dir transform --output-dir output
+### Example 2: Multi-Stage Pipeline with Per-Stage Configuration
+
+You can configure per-stage options via an instructions file for complex pipelines:
+
+```yaml
+stages:
+  - name: KubernetesPlugin
+    optionals:
+      registry-replacement: "docker.io=quay.io"
+  - name: CustomEdits
+```
+
+```bash
+crane transform --instructions-file=instructions.yaml --transform-dir=transform

Example 3: Iterative Development


</details>

#### `commands/transform.md`

<details>
<summary><b>View diff</b></summary>

```diff
--- a/commands/transform.md
+++ b/commands/transform.md
@@ -156,6 +156,38 @@

Shell autocompletion is available for plugin and stage names.
+
+### Passing Optional Flags to Plugins
+
+Plugins can accept additional configuration via optional flags. These can be defined globally for all plugins or per-stage.
+
+#### Global Optional Flags
+Pass a JSON string of flag-value pairs using --optional-flags. These apply to all plugins in the pipeline.
+
+bash +crane transform --optional-flags='{"registry-replacement": "docker.io=quay.io"}' +
+
+#### Per-Stage Optional Flags
+For granular control, specify optional flags per stage using the --stage-optionals flag. This flag is repeatable.
+
+```bash
+crane transform \

  • --stage-optionals 'KubernetesPlugin={"registry-replacement":"docker.io=quay.io"}' \
  • --stage-optionals 'RegistryPlugin={"registry-replacement":"quay.io=ghcr.io"}'
    +```

+Alternatively, you can define per-stage optionals directly in your instructions.yaml file:
+
+```yaml
+stages:

    • name: KubernetesPlugin
  • optionals:
  •  registry-replacement: "docker.io=quay.io"
    
    • name: CustomEdits
      +```

+Note: Per-stage optional flags override global flags if keys collide for that specific stage. Using --instructions-file and --stage-optionals simultaneously is not supported.

Applying Transforms


</details>

Failed to create a docs PR. The changes are shown above for manual application.

@Tamar-Dinavetsky

Copy link
Copy Markdown
Contributor

[update-docs] Take into account following usage examples for feature in this PR:

Transform Optional Flags: Usage Examples

Optional flags control plugin behavior during crane transform. They can be set globally (for all stages) or per-stage (overriding the global value for that stage).

Discovering Available Flags

crane transform optionals

Output lists flags per plugin with descriptions and examples:

Plugin: KubernetesPlugin (version 0.1.6)
    registry-replacement: Map of image registry paths to swap on transform
        Example: docker-registry.default.svc:5000=image-registry.openshift-image-registry.svc:5000
    add-annotations: Annotations to add to each resource
        Example: annotation1=value1,annotation2=value2
    strip-default-rbac: Whether to strip default RBAC including default serviceAccount (default: true)
        Example: true
    ...

Global Optional Flags

--optional-flags (applies to all stages)

Pass a JSON object. All plugins in all stages receive these flags.

# Registry replacement for all stages
crane transform --optional-flags '{"registry-replacement": "docker.io=quay.io"}'

# Multiple flags
crane transform --optional-flags '{"registry-replacement": "docker.io=quay.io", "add-annotations": "migrated-by=crane"}'

# Disable default RBAC stripping
crane transform --optional-flags '{"strip-default-rbac": "false"}'

Per-Stage Optional Flags (CLI)

--stage-optionals (repeatable, per-stage override)

Format: StageName=JSON. The stage name is the plugin name (e.g. KubernetesPlugin), not the directory name (10_KubernetesPlugin).

# Different registry replacement per stage
crane transform \
  --stage-optionals 'KubernetesPlugin={"registry-replacement": "docker.io=quay.io"}' \
  --stage-optionals 'OpenshiftPlugin={"registry-replacement": "docker.io=registry.redhat.io"}'

Combining global and per-stage

Per-stage overrides the entire global set for that stage (no merging). Stages without per-stage flags inherit global.

# Global: add annotations to all stages
# KubernetesPlugin: also replace registry (overrides global entirely)
crane transform \
  --optional-flags '{"add-annotations": "env=prod"}' \
  --stage-optionals 'KubernetesPlugin={"registry-replacement": "docker.io=quay.io", "add-annotations": "env=prod"}'

Note: because per-stage overrides (not merges), KubernetesPlugin must repeat add-annotations if it still needs it.

PVC rename with registry swap

crane transform \
  --stage-optionals 'KubernetesPlugin={"registry-replacement": "docker.io=quay.io", "pvc-rename-map": "old-data:new-data,old-logs:new-logs"}'

Selective whiteout control

# Keep only Deployments and Services, whiteout everything else
crane transform \
  --stage-optionals 'KubernetesPlugin={"include-only": "Deployment.apps,Service"}'

# Extra whiteouts on top of defaults
crane transform \
  --stage-optionals 'KubernetesPlugin={"extra-whiteouts": "Route.route.openshift.io,ImageStream.image.openshift.io"}'

Per-Stage Optional Flags (Instructions File)

Mixed-list format

Stages can be plain strings or objects with name and optionals. Plain strings inherit global --optional-flags.

# instructions.yaml
stages:
  - KubernetesPlugin
  - name: OpenshiftPlugin
    optionals:
      registry-replacement: "docker.io=registry.redhat.io"
  - CustomEdits
crane transform --instructions-file instructions.yaml

All stages with optionals

stages:
  - name: KubernetesPlugin
    optionals:
      registry-replacement: "docker.io=quay.io"
      add-annotations: "migrated-by=crane,env=production"
      strip-default-rbac: "false"
  - name: OpenshiftPlugin
    optionals:
      registry-replacement: "docker.io=registry.redhat.io"

Instructions file + global CLI fallback

Stages without optionals in the file inherit the global --optional-flags value:

# instructions.yaml
stages:
  - KubernetesPlugin
  - name: OpenshiftPlugin
    optionals:
      registry-replacement: "docker.io=registry.redhat.io"
# KubernetesPlugin gets the global flags; OpenshiftPlugin gets its own
crane transform \
  --instructions-file instructions.yaml \
  --optional-flags '{"add-annotations": "migrated-by=crane"}'

Backward-compatible format

Old instructions files still work identically:

stages:
  - KubernetesPlugin
  - OpenshiftPlugin

Precedence Rules

Mode Highest priority Lowest priority
With --instructions-file per-stage optionals from file --optional-flags CLI
Without --instructions-file --stage-optionals CLI --optional-flags CLI

Per-stage overrides (replaces) the global set for that stage. There is no merging.

Mutual Exclusivity

--instructions-file and --stage-optionals cannot be used together:

# This fails with an error
crane transform \
  --instructions-file instructions.yaml \
  --stage-optionals 'KubernetesPlugin={"registry-replacement": "docker.io=quay.io"}'

Use optionals inside the instructions file instead.

KubernetesPlugin Available Flags

Flag Description Example
registry-replacement Registry path swap map docker.io=quay.io,gcr.io=ghcr.io
add-annotations Annotations to add key1=val1,key2=val2
remove-annotations Annotations to remove annotation1,annotation2
strip-default-rbac Strip default RBAC (default: true) true / false
strip-default-cabundle Strip default CA bundle (default: true) true / false
disable-whiteout-owned Disable whiting out owned resources true
extra-whiteouts Additional resources to whiteout Deployment.apps,Route.route.openshift.io
include-only Keep only listed resources Deployment.apps,Service
pvc-rename-map PVC rename mapping old-name:new-name

@github-actions

Copy link
Copy Markdown
Contributor

📚 Documentation Update

Updated 2 file(s) based on your review selections:

  • multistage-pipeline.md
  • commands/transform.md

📄 Changes

multistage-pipeline.md

View diff
--- a/multistage-pipeline.md
+++ b/multistage-pipeline.md
@@ -171,6 +171,58 @@
 
 If no stages are specified, all discovered stages are applied sequentially to ensure sequential consistency.
 
+## Optional Flags
+
+Optional flags control plugin behavior during `crane transform`. They can be set globally (for all stages) or per-stage.
+
+### Discovering Available Flags
+
+```bash
+crane transform optionals
+```
+
+### Global Optional Flags
+
+Pass a JSON object using `--optional-flags`. All plugins in all stages receive these flags.
+
+```bash
+# Registry replacement for all stages
+crane transform --optional-flags '{"registry-replacement": "docker.io=quay.io"}'
+
+# Disable default RBAC stripping
+crane transform --optional-flags '{"strip-default-rbac": "false"}'
+```
+
+### Per-Stage Optional Flags (CLI)
+
+Use repeatable `--stage-optionals` arguments in `StageName=JSON` format. The stage name is the plugin name (e.g., `KubernetesPlugin`), not the directory name.
+
+```bash
+# Different registry replacement per stage
+crane transform \
+--stage-optionals 'KubernetesPlugin={"registry-replacement": "docker.io=quay.io"}' \
+--stage-optionals 'OpenshiftPlugin={"registry-replacement": "docker.io=registry.redhat.io"}'
+```
+
+*Note: Per-stage flags override the global set for that stage (no merging). Stages without per-stage flags inherit global flags.*
+
+### Per-Stage Optional Flags (Instructions File)
+
+Stages in the instructions file can be defined as objects with `name` and `optionals`.
+
+```yaml
+# instructions.yaml
+stages:
+- name: KubernetesPlugin
+  optionals:
+    registry-replacement: "docker.io=quay.io"
+- name: OpenshiftPlugin
+  optionals:
+    registry-replacement: "docker.io=registry.redhat.io"
+```
+
+*Note: Plain strings in the instructions file inherit the global `--optional-flags` value.*
+
 ## Priority Assignment
 
 ### Auto-Assignment
@@ -185,8 +237,6 @@

Recommended Priority Order

-The system provides heuristic-based recommendations:

Plugin Type Recommended Priority Keywords
@@ -208,12 +258,6 @@
export/ → 10_KubernetesPlugin/ → 20_OpenshiftPlugin/ → 30_ImagestreamPlugin/ → output/

-Each stage:
-1. Reads input resources (from export or previous stage)
-2. Applies transformations via plugins
-3. Writes output to its stage directory
-4. Next stage uses this output as input
-
## Workflow Examples

### Example 1: Simple Transform and Apply
@@ -249,59 +293,11 @@
  --transform-dir transform \
  20_OpenshiftPlugin

-# Create ImageStream transformations
-crane transform \
-  --transform-dir transform \
-  30_ImagestreamPlugin
-
# Apply all stages
crane apply --transform-dir transform --output-dir output

-### Example 3: Iterative Development

-```bash
-# Initial transform
-crane transform --export-dir export --transform-dir transform

-# Make manual edits to resources in transform/10_KubernetesPlugin/input/
-# Edit deployment.yaml to add annotations, etc.

-# Try to re-run transform (will fail due to dirty check)
-crane transform --export-dir export --transform-dir transform
-# Error: contains user modifications

-# Force overwrite if needed
-crane transform --export-dir export --transform-dir transform --force

-# Or preserve changes by creating a new stage
-crane transform 20_custom
-```

Migration from JSONPatch Workflow

-### Old Workflow

-text -transform/ -├── namespace/ -│ └── default/ -│ └── deployment/ -│ └── myapp.json # JSONPatch per resource -

-### New Workflow

-```text
-transform/
-└── 10_KubernetesPlugin/

  • ├── input/
  • │ └── deployment.yaml # Grouped by type
  • ├── patches/
  • │ └── deployment-myapp-default.yaml
  • ├── output/ # Materialized output
  • └── kustomization.yaml
    -```

Benefits

@@ -311,48 +307,6 @@
4. Better Diff: Deterministic ordering produces stable Git diffs
5. Dirty Check: Prevents accidental overwrites of user modifications

-## Advanced Features

-### Stage Validation

-Validate stage names before applying:

-```go
-import (

  • "github.com/konveyor/crane/internal/transform"
    -)

-// Validate stage names
-stages, err := transform.DiscoverStages(transformDir)
-if err != nil {

  • fmt.Printf("failed to discover stages: %v\n", err)
    -}

-for _, stage := range stages {

  • if err := transform.ValidateStageName(stage.DirName); err != nil {
  •    fmt.Printf("Invalid stage name %s: %v\n", stage.DirName, err)
    
  • }
    -}
    -```

-### Custom Stage Naming

-Generate stage names with priority numbers:

-```go
-import "github.com/konveyor/crane/internal/transform"

-// Generate a stage name with priority
-stageName := transform.GenerateStageName(15, "my-plugin")
-// Returns: "15_my-plugin"

-// Validate a stage name
-err := transform.ValidateStageName("15_my-plugin")
-if err != nil {

  • // Handle invalid stage name
    -}
    -```

Troubleshooting

Issue: Transform fails with "contains user modifications"

@@ -372,40 +326,6 @@

  1. Check kustomization.yaml syntax
  2. Verify all resource files exist in input/
  3. Run crane apply <stage> to isolate the failing stage

-### Issue: Resources not appearing in output

-Cause: Resources may be whiteout (excluded) by plugins.

-Solution:
-1. Check whiteout-report.yaml in stage directory
-2. Review plugin configuration
-3. Check plugin logs for whiteout decisions

-### Issue: Patches not being applied

-Cause: Patch file or target selector may be incorrect.

-Solution:
-1. Verify patch file exists in patches/
-2. Check target selector matches resource metadata
-3. Review ignored-patches-report.yaml for conflicts

-## Best Practices

-1. Stage Naming: Use descriptive names that indicate the transformation purpose

    • Good: 10_KubernetesPlugin-base, 20_OpenshiftPlugin-routes, 30_security-context
    • Bad: 10_stage1, 20_stage2

-2. Priority Spacing: Leave gaps (10, 20, 30) to allow insertion of new stages

-3. Version Control: Commit transform directories to Git to track changes

-4. Testing: Always test transformed output before applying to production

-5. Incremental Changes: Use separate stages for different concerns (security, networking, storage)

-6. Documentation: Include README.md in transform directory explaining pipeline purpose

API Reference

@@ -427,35 +347,6 @@
last := transform.GetLastStage(stages)
prev := transform.GetPreviousStage(stages, currentStage)
next := transform.GetNextStage(stages, currentStage)

-// Stage name validation and generation
-err = transform.ValidateStageName("10_KubernetesPlugin")
-stageName := transform.GenerateStageName(10, "kubernetes")
-```

-### Apply Package

-```go
-// Kustomize apply (embedded — no kubectl dependency)
-applier := &apply.KustomizeApplier{

  • Log: logger,
  • TransformDir: transformDir,
  • OutputDir: outputDir,
  • SkipClusterScoped: false,
    -}

-// Apply a single stage
-err = applier.ApplySingleStage("10_KubernetesPlugin")

-// Apply multiple stages with selector
-selector := transform.StageSelector{

  • FromStage: "10_KubernetesPlugin",
  • ToStage: "30_ImagestreamPlugin",
    -}
    -err = applier.ApplyMultiStage(selector)

-// Apply all stages sequentially
-err = applier.ApplyMultiStage(transform.StageSelector{})


## Further Reading
@@ -472,40 +363,12 @@

> **Warning — Namespace renaming:** If you manually rename the namespace in resource files within a custom stage (for example, changing `namespace: old-ns` to `namespace: new-ns`), Crane does not automatically update **ClusterRoleBinding subjects** referencing the old namespace name, or **NetworkPolicy `namespaceSelector`** entries matching the old namespace by label (e.g., `kubernetes.io/metadata.name: old-ns`). These will silently break after migration. Manually update them as well.

-### Example
-
-```bash
-# Create a multi-stage pipeline
-crane transform 10_KubernetesPlugin   # Plugin-backed
-crane transform 50_ManualEdits        # No matching plugin
-crane transform 90_FinalCleanup       # No matching plugin
-```
-
-**What happens:**
-
-1. **10_KubernetesPlugin**: Resources transformed by KubernetesPlugin (removes metadata.uid, etc.)
-2. **50_ManualEdits**: Resources copied unchanged to `transform/50_ManualEdits/input/`
-   - No plugins match "ManualEdits" 
-   - No patches generated
-   - You can manually edit resources in this stage
-3. **90_FinalCleanup**: Resources from previous stage copied unchanged
-   - User can add manual patches or edits
-
-### Behavior
-
-When stage name doesn't match any plugin:
-- `filterPluginsByStage()` returns empty list `[]`
-- `runner.Run()` called with empty plugin list
-- Resources written unchanged (no transformations)
-- No patches generated
-- **This is intentional** - allows user-controlled stages
-
### Mixed Pipeline Example

```text
export/
└── resources/
-    └── deployment.yaml (raw export with uid, resourceVersion, etc.)
+    └── deployment.yaml
        ↓
transform/10_KubernetesPlugin/  (plugin: removes server-managed fields)
└── input/deployment.yaml (cleaned)

commands/transform.md

View diff
--- a/commands/transform.md
+++ b/commands/transform.md
@@ -188,6 +188,71 @@
 - **`resources/<namespace>/`**: Individual resource files organized by namespace for easier review and selective application
 - **`resources/_cluster/`**: Cluster-scoped resources (omitted when `--skip-cluster-scoped` is set)
 
+## Plugin Optional Flags
+
+Plugins may accept optional flags to modify their transformation logic.
+
+### Discovering Available Flags
+
+Run the following command to see which optional fields are accepted by configured plugins:
+
+```bash
+crane transform optionals
+```
+
+### Global Optional Flags
+
+Use `--optional-flags` to pass a JSON object of flags to all plugins across all stages.
+
+```bash
+# Registry replacement for all stages
+crane transform --optional-flags '{"registry-replacement": "docker.io=quay.io"}'
+
+# Multiple flags
+crane transform --optional-flags '{"registry-replacement": "docker.io=quay.io", "add-annotations": "migrated-by=crane"}'
+```
+
+### Per-Stage Optional Flags (CLI)
+
+Use the repeatable `--stage-optionals` flag to set options for specific stages. The stage name refers to the plugin name (e.g., `KubernetesPlugin`), not the directory name (`10_KubernetesPlugin`).
+
+```bash
+# Different registry replacement per stage
+crane transform \
+--stage-optionals 'KubernetesPlugin={"registry-replacement": "docker.io=quay.io"}' \
+--stage-optionals 'OpenshiftPlugin={"registry-replacement": "docker.io=registry.redhat.io"}'
+```
+
+### Per-Stage Optional Flags (Instructions File)
+
+When using an `--instructions-file`, you can define optionals directly within the file. 
+
+```yaml
+# instructions.yaml
+stages:
+- name: KubernetesPlugin
+  optionals:
+    registry-replacement: "docker.io=quay.io"
+- name: OpenshiftPlugin
+  optionals:
+    registry-replacement: "docker.io=registry.redhat.io"
+```
+
+```bash
+crane transform --instructions-file instructions.yaml
+```
+
+### Precedence and Constraints
+
+- **Overrides**: Per-stage optionals replace the entire global set for that stage (there is no merging).
+- **Fallback**: Stages without per-stage flags inherit global `--optional-flags`.
+- **Mutual Exclusivity**: `--instructions-file` and `--stage-optionals` cannot be used together.
+
+| Mode | Highest priority | Lowest priority |
+|------|-----------------|-----------------|
+| With `--instructions-file` | per-stage `optionals` from file | `--optional-flags` CLI |
+| Without `--instructions-file` | `--stage-optionals` CLI | `--optional-flags` CLI |
+
 ## Automatic Stage Creation
 
 When no stages exist in the transform directory, `crane transform` automatically creates stages for **all available plugins** (not just KubernetesPlugin). Plugins are sorted alphabetically and assigned priorities starting at 10, incrementing by 5. Use `--skip-plugins` to exclude specific plugins from this default behavior.
@@ -309,7 +374,7 @@
 
 ### kustomization.yaml
 
-Kustomize configuration that ties everything together:
+Kustomize configuration that ties together:
 
 ```yaml
 apiVersion: kustomize.config.k8s.io/v1beta1
@@ -326,12 +391,7 @@
 - input/ConfigMap__v1_default_nginx-config.yaml
 - input/Deployment_apps_v1_default_wordpress.yaml
 - input/Service__v1_default_kubernetes.yaml
-
-# Whiteout resources are written to input/ for complete snapshot
-# but excluded from active resources list above:
-# - input/Pod__v1_default_wordpress-74b89cc84c-nm9f8.yaml
-```
-
+```
 
 ## Common Workflows
 

A docs PR has been created: #816

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Un-deprecate and update transform optionals flag

3 participants