From 594883c5dca2a682da4e10ce8891937d3e010cd3 Mon Sep 17 00:00:00 2001 From: Marek Aufart Date: Wed, 17 Jun 2026 14:44:39 +0200 Subject: [PATCH 1/4] Provide warning for ImageStreams When plugin process ImageStream resource, it should provide a warning since images are not subject of migration by the plugin. Fixes: https://github.com/migtools/crane/issues/452 Signed-off-by: Marek Aufart --- README.md | 79 ++++++++++++++++- openshift/plugin.go | 7 ++ openshift/plugin_test.go | 187 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 272 insertions(+), 1 deletion(-) create mode 100644 openshift/plugin_test.go diff --git a/README.md b/README.md index 026522c..6c8ba7f 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,79 @@ # crane-plugin-openshift -Openshift plugin for crane + +OpenShift plugin for [crane](https://github.com/konveyor/crane) - handles OpenShift-specific resource transformations during cluster migrations. + +## Features + +This plugin provides transformations for OpenShift-specific resources including: + +- **BuildConfigs**: Updates pull secrets and registry references +- **DeploymentConfigs**: Handles PVC renames and pod template transformations +- **Routes**: Removes auto-generated hostnames +- **ServiceAccounts**: Strips default secrets and pull secrets +- **RoleBindings**: Removes namespace references for ServiceAccount subjects +- **ImageStreams**: Detects usage (see limitations below) +- Automatic whiteout of OpenShift-specific resources (Builds, ImageStreamTags, ImageTags) +- Optional stripping of default RBAC, CA bundles, and pull secrets + +## Limitations + +### Internal Image Registry Migration + +**Important**: This plugin does **NOT** migrate container images stored in OpenShift's internal image registry. + +When the plugin detects `ImageStream` resources during migration, it will log warnings like: + +``` +WARNING: ImageStream 'my-namespace/my-app' detected - images from internal registry are NOT migrated automatically +INFO: To migrate internal registry images, use tools like skopeo. Example: skopeo sync --src docker --dest docker SOURCE_REGISTRY/REPO DEST_REGISTRY/REPO +``` + +#### Why aren't images migrated? + +- Crane focuses on Kubernetes resource manifests (YAML) +- Container images are data stored separately in container registries +- Internal registry images require specialized tools for migration + +#### How to migrate images manually + +Use [skopeo](https://github.com/containers/skopeo) to copy images between registries: + +```bash +# Example: Copy a single image +skopeo copy \ + docker://source-registry.example.com:5000/namespace/image:tag \ + docker://dest-registry.example.com:5000/namespace/image:tag + +# Example: Sync multiple images +skopeo sync \ + --src docker --dest docker \ + source-registry.example.com:5000/namespace \ + dest-registry.example.com:5000/namespace +``` + +For more details, see [crane issue #452](https://github.com/migtools/crane/issues/452). + +## Usage + +This plugin is used automatically by crane when processing OpenShift resources. Optional flags can be configured: + +- `--strip-default-rbac` (default: true) - Strip default RBAC resources +- `--strip-default-cabundle` (default: true) - Strip default CA bundle ConfigMaps +- `--strip-default-pull-secrets` (default: true) - Strip default pull secrets +- `--pull-secret-replacement` - Map of pull secret replacements +- `--registry-replacement` - Map of registry path replacements +- `--pvc-rename-map` - Map of PVC name changes + +## Development + +### Running Tests + +```bash +go test ./... +``` + +### Building + +```bash +go build -o crane-plugin-openshift . +``` diff --git a/openshift/plugin.go b/openshift/plugin.go index 7c8db69..d1b29a7 100644 --- a/openshift/plugin.go +++ b/openshift/plugin.go @@ -89,6 +89,13 @@ func (o *OpenShiftTransformPlugin) Run(request transform.PluginRequest) (transfo case "Build": o.log().Info("found build, adding to whiteout") whiteOut = true + case "ImageStream": + namespace := u.GetNamespace() + name := u.GetName() + o.log().Warnf("ImageStream '%s/%s' detected - images from internal registry are NOT migrated automatically", namespace, name) + o.log().Info("To migrate internal registry images, use tools like skopeo. Example: skopeo sync --src docker --dest docker SOURCE_REGISTRY/REPO DEST_REGISTRY/REPO") + o.log().Info("For more information, see: https://github.com/migtools/crane/issues/452") + whiteOut = true case "ImageStreamTag": o.log().Info("found ImageStreamTag sub-resource, adding to whiteout") whiteOut = true diff --git a/openshift/plugin_test.go b/openshift/plugin_test.go new file mode 100644 index 0000000..b193a9e --- /dev/null +++ b/openshift/plugin_test.go @@ -0,0 +1,187 @@ +package openshift + +import ( + "bytes" + "testing" + + "github.com/konveyor/crane-lib/transform" + "github.com/sirupsen/logrus" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +func TestImageStreamDetection(t *testing.T) { + tests := []struct { + name string + resource *unstructured.Unstructured + expectWhiteOut bool + expectWarning bool + warningContains string + }{ + { + name: "ImageStream should be whitelisted and warn", + resource: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "image.openshift.io/v1", + "kind": "ImageStream", + "metadata": map[string]interface{}{ + "name": "my-app", + "namespace": "my-namespace", + }, + "spec": map[string]interface{}{ + "lookupPolicy": map[string]interface{}{ + "local": false, + }, + }, + }, + }, + expectWhiteOut: true, + expectWarning: true, + warningContains: "my-namespace/my-app", + }, + { + name: "ImageStreamTag should be whitelisted without ImageStream warning", + resource: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "image.openshift.io/v1", + "kind": "ImageStreamTag", + "metadata": map[string]interface{}{ + "name": "my-app:latest", + "namespace": "my-namespace", + }, + }, + }, + expectWhiteOut: true, + expectWarning: false, + }, + { + name: "ImageTag should be whitelisted without ImageStream warning", + resource: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "image.openshift.io/v1", + "kind": "ImageTag", + "metadata": map[string]interface{}{ + "name": "sha256:abc123", + "namespace": "my-namespace", + }, + }, + }, + expectWhiteOut: true, + expectWarning: false, + }, + { + name: "Regular Pod should not be whitelisted", + resource: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "Pod", + "metadata": map[string]interface{}{ + "name": "my-pod", + "namespace": "my-namespace", + }, + }, + }, + expectWhiteOut: false, + expectWarning: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create a buffer to capture log output + var logBuffer bytes.Buffer + logger := logrus.New() + logger.SetOutput(&logBuffer) + + plugin := &OpenShiftTransformPlugin{ + Log: logger, + } + + request := transform.PluginRequest{ + Unstructured: *tt.resource, + Extras: map[string]string{}, + } + + response, err := plugin.Run(request) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if response.IsWhiteOut != tt.expectWhiteOut { + t.Errorf("expected IsWhiteOut=%v, got %v", tt.expectWhiteOut, response.IsWhiteOut) + } + + logOutput := logBuffer.String() + if tt.expectWarning { + if logOutput == "" { + t.Error("expected warning log but got no output") + } + if tt.warningContains != "" && !contains(logOutput, tt.warningContains) { + t.Errorf("expected log to contain '%s', got: %s", tt.warningContains, logOutput) + } + if !contains(logOutput, "NOT migrated automatically") { + t.Errorf("expected warning about migration, got: %s", logOutput) + } + if !contains(logOutput, "skopeo") { + t.Errorf("expected log to mention skopeo tool, got: %s", logOutput) + } + } else { + if contains(logOutput, "NOT migrated automatically") { + t.Errorf("unexpected ImageStream warning for %s: %s", tt.resource.GetKind(), logOutput) + } + } + }) + } +} + +func TestImageStreamWithEmptyNamespace(t *testing.T) { + var logBuffer bytes.Buffer + logger := logrus.New() + logger.SetOutput(&logBuffer) + + plugin := &OpenShiftTransformPlugin{ + Log: logger, + } + + resource := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "image.openshift.io/v1", + "kind": "ImageStream", + "metadata": map[string]interface{}{ + "name": "my-app", + // namespace intentionally omitted + }, + }, + } + + request := transform.PluginRequest{ + Unstructured: *resource, + Extras: map[string]string{}, + } + + response, err := plugin.Run(request) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if !response.IsWhiteOut { + t.Error("expected ImageStream to be whitelisted") + } + + logOutput := logBuffer.String() + if !contains(logOutput, "ImageStream") { + t.Errorf("expected ImageStream warning, got: %s", logOutput) + } +} + +func contains(s, substr string) bool { + return len(s) >= len(substr) && (s == substr || len(substr) == 0 || findSubstring(s, substr)) +} + +func findSubstring(s, substr string) bool { + for i := 0; i <= len(s)-len(substr); i++ { + if s[i:i+len(substr)] == substr { + return true + } + } + return false +} From 74484665d727362578e69d47632472b495ad980b Mon Sep 17 00:00:00 2001 From: Marek Aufart Date: Wed, 17 Jun 2026 16:24:20 +0200 Subject: [PATCH 2/4] Addressing feedback Signed-off-by: Marek Aufart --- README.md | 4 +++- openshift/plugin.go | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6c8ba7f..c3a48f8 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ This plugin provides transformations for OpenShift-specific resources including: When the plugin detects `ImageStream` resources during migration, it will log warnings like: -``` +```text WARNING: ImageStream 'my-namespace/my-app' detected - images from internal registry are NOT migrated automatically INFO: To migrate internal registry images, use tools like skopeo. Example: skopeo sync --src docker --dest docker SOURCE_REGISTRY/REPO DEST_REGISTRY/REPO ``` @@ -66,6 +66,8 @@ This plugin is used automatically by crane when processing OpenShift resources. ## Development +For more information about developing crane plugins, see [crane-plugins](https://github.com/migtools/crane-plugins). + ### Running Tests ```bash diff --git a/openshift/plugin.go b/openshift/plugin.go index d1b29a7..d5487c9 100644 --- a/openshift/plugin.go +++ b/openshift/plugin.go @@ -21,6 +21,7 @@ const ( ) var authorizationGroup = "authorization.openshift.io" +var imageGroup = "image.openshift.io" // OpenShiftTransformPlugin implements transform.Plugin for OpenShift-specific transformations. type OpenShiftTransformPlugin struct { From 588039fce13c9bf4165655ec7189cd47745a575e Mon Sep 17 00:00:00 2001 From: Marek Aufart Date: Wed, 17 Jun 2026 16:37:41 +0200 Subject: [PATCH 3/4] Add basic golang PR checks Signed-off-by: Marek Aufart --- .github/workflows/pr-checks.yml | 24 ++++++++++++++++++++++++ openshift/plugin.go | 1 - 2 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/pr-checks.yml diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml new file mode 100644 index 0000000..d8a0ddb --- /dev/null +++ b/.github/workflows/pr-checks.yml @@ -0,0 +1,24 @@ +name: PR Checks + +on: + pull_request: + branches: + - main + +jobs: + test-and-build: + runs-on: ubuntu-latest + steps: + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: "1.25.0" + + - name: Check out code + uses: actions/checkout@v4 + + - name: Run tests + run: go test ./... + + - name: Build + run: go build -o bin/crane-plugin-openshift . diff --git a/openshift/plugin.go b/openshift/plugin.go index d5487c9..d1b29a7 100644 --- a/openshift/plugin.go +++ b/openshift/plugin.go @@ -21,7 +21,6 @@ const ( ) var authorizationGroup = "authorization.openshift.io" -var imageGroup = "image.openshift.io" // OpenShiftTransformPlugin implements transform.Plugin for OpenShift-specific transformations. type OpenShiftTransformPlugin struct { From 7cecead11b03500e02a4101485168a5ff60a5d45 Mon Sep 17 00:00:00 2001 From: Marek Aufart Date: Thu, 18 Jun 2026 10:28:50 +0200 Subject: [PATCH 4/4] Fix wording in tests Signed-off-by: Marek Aufart --- openshift/plugin_test.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/openshift/plugin_test.go b/openshift/plugin_test.go index b193a9e..1468b5c 100644 --- a/openshift/plugin_test.go +++ b/openshift/plugin_test.go @@ -18,7 +18,7 @@ func TestImageStreamDetection(t *testing.T) { warningContains string }{ { - name: "ImageStream should be whitelisted and warn", + name: "ImageStream should be marked as whiteout and warn", resource: &unstructured.Unstructured{ Object: map[string]interface{}{ "apiVersion": "image.openshift.io/v1", @@ -39,7 +39,7 @@ func TestImageStreamDetection(t *testing.T) { warningContains: "my-namespace/my-app", }, { - name: "ImageStreamTag should be whitelisted without ImageStream warning", + name: "ImageStreamTag should be marked as whiteout without ImageStream warning", resource: &unstructured.Unstructured{ Object: map[string]interface{}{ "apiVersion": "image.openshift.io/v1", @@ -54,7 +54,7 @@ func TestImageStreamDetection(t *testing.T) { expectWarning: false, }, { - name: "ImageTag should be whitelisted without ImageStream warning", + name: "ImageTag should be marked as whiteout without ImageStream warning", resource: &unstructured.Unstructured{ Object: map[string]interface{}{ "apiVersion": "image.openshift.io/v1", @@ -69,7 +69,7 @@ func TestImageStreamDetection(t *testing.T) { expectWarning: false, }, { - name: "Regular Pod should not be whitelisted", + name: "Regular Pod should not be marked as whiteout", resource: &unstructured.Unstructured{ Object: map[string]interface{}{ "apiVersion": "v1", @@ -164,7 +164,7 @@ func TestImageStreamWithEmptyNamespace(t *testing.T) { } if !response.IsWhiteOut { - t.Error("expected ImageStream to be whitelisted") + t.Error("expected ImageStream to be marked as whiteout") } logOutput := logBuffer.String()