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
24 changes: 24 additions & 0 deletions .github/workflows/pr-checks.yml
Original file line number Diff line number Diff line change
@@ -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
Comment thread
aufi marked this conversation as resolved.

- name: Run tests
run: go test ./...

- name: Build
run: go build -o bin/crane-plugin-openshift .
81 changes: 80 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,81 @@
# 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:

```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
```

#### 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

For more information about developing crane plugins, see [crane-plugins](https://github.com/migtools/crane-plugins).

### Running Tests

```bash
go test ./...
```

### Building

```bash
go build -o crane-plugin-openshift .
```
7 changes: 7 additions & 0 deletions openshift/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -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":
Comment thread
aufi marked this conversation as resolved.
o.log().Info("found ImageStreamTag sub-resource, adding to whiteout")
whiteOut = true
Expand Down
187 changes: 187 additions & 0 deletions openshift/plugin_test.go
Original file line number Diff line number Diff line change
@@ -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 marked as whiteout 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 marked as whiteout 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 marked as whiteout 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 marked as whiteout",
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 marked as whiteout")
}

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
}
Loading