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/README.md b/README.md index 026522c..c3a48f8 100644 --- a/README.md +++ b/README.md @@ -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 . +``` 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..1468b5c --- /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 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 +}