Skip to content
Open
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
1 change: 1 addition & 0 deletions e2e-tests/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ var (
VerboseLogs bool
SourceNonAdminContext string
TargetNonAdminContext string
InsecureSkipTLSVerify bool
)
4 changes: 4 additions & 0 deletions e2e-tests/framework/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ type K8sDeployApp struct {
Bin string
Context string
ExtraVars map[string]any
InsecureSkipTLSVerify bool
}

// Deploy runs k8sdeploy deploy for the configured app and namespace.
Expand Down Expand Up @@ -91,6 +92,9 @@ func (a K8sDeployApp) Cleanup() error {

// buildCommand constructs an exec command with environment adjustments applied.
func (a K8sDeployApp) buildCommand(args ...string) *exec.Cmd {
if a.InsecureSkipTLSVerify {
args = append([]string{"--insecure-skip-tls-verify"}, args...)
}
cmd := exec.Command(a.Bin, args...)
cmd.Env = envWithBinDir(a.Bin)
return cmd
Expand Down
73 changes: 73 additions & 0 deletions e2e-tests/framework/crane.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package framework

import (
"fmt"
"os"
"os/exec"
)

Expand All @@ -11,6 +12,11 @@ type CraneRunner struct {
WorkDir string
}

// SkopeoRunner wraps skopeo for image sync operations.
type SkopeoRunner struct {
Bin string // defaults to "skopeo" if empty
}

// TransferPVCOptions contains arguments for the crane transfer-pvc command.
type TransferPVCOptions struct {
SourceContext string
Expand All @@ -32,6 +38,20 @@ type ValidateOptions struct {
ExtraArgs []string
}

// SkopeoSyncOptions holds arguments for `skopeo sync --src yaml --dest docker`.
type SkopeoSyncOptions struct {
// SrcYAMLPath is the path to the YAML file produced by `crane skopeo-sync-gen`.
SrcYAMLPath string
// DestRegistry is <registry>/<namespace> on the target cluster,
// e.g. default-route-openshift-image-registry.apps.tgt.example.com/my-namespace.
// Do NOT use --scoped; it produces wrong image paths against OCP.
DestRegistry string
// SrcCreds / DestCreds are "user:token" for registry auth.
// Use `oc whoami --show-token` as the token; username is ignored by OCP ("unused").
SrcCreds string
DestCreds string
}

// Export runs crane export for a namespace into the given export directory.
func (c CraneRunner) Export(namespace, exportDir string) error {
args := []string{"export", "--context", c.SourceContext, "--namespace", namespace, "--export-dir", exportDir}
Expand Down Expand Up @@ -177,3 +197,56 @@ func (c CraneRunner) Validate(opts ValidateOptions) (stdout string, err error) {
}
return stdout, nil
}

// Sync runs `skopeo sync --src yaml --dest docker`.
// TLS verify is disabled on both sides — OCP registry routes use self-signed certs.
func (s SkopeoRunner) Sync(opts SkopeoSyncOptions) error {
bin := s.Bin
if bin == "" {
bin = "skopeo"
}

args := []string{
"sync",
"--src", "yaml",
"--dest", "docker",
"--src-tls-verify=false",
"--dest-tls-verify=false",
}
if opts.SrcCreds != "" {
args = append(args, "--src-creds", opts.SrcCreds)
}
if opts.DestCreds != "" {
args = append(args, "--dest-creds", opts.DestCreds)
}
args = append(args, opts.SrcYAMLPath, opts.DestRegistry)

logVerboseCommand(bin, args)
cmd := exec.Command(bin, args...)
out, err := cmd.CombinedOutput()
logVerboseOutput("skopeo sync", out)
if err != nil {
return fmt.Errorf("skopeo sync failed: %v, output: %s", err, string(out))
}
return nil
}

// SkopeoSyncGen runs `crane skopeo-sync-gen` and writes the generated YAML to destPath.
func (c CraneRunner) SkopeoSyncGen(exportDir, registryURL, destPath string) error {
args := []string{
"skopeo-sync-gen",
"--export-dir", exportDir,
"--registry-url", registryURL,
}
logVerboseCommand(c.Bin, args)
cmd := exec.Command(c.Bin, args...)
out, err := cmd.Output()
logVerboseOutput("crane skopeo-sync-gen", out)
if err != nil {
return fmt.Errorf("crane skopeo-sync-gen failed: %v, output: %s", err, string(out))
}
if err := os.WriteFile(destPath, out, 0o644); err != nil {
return fmt.Errorf("write skopeo sync YAML to %q: %w", destPath, err)
}
return nil
}
30 changes: 30 additions & 0 deletions e2e-tests/framework/ocp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package framework

import (
"fmt"
"os/exec"
"strings"
)

// getOCRegistryURL returns the externally-accessible route of the OCP internal
// registry for the given kubeconfig context via `oc registry info --internal=false`.
func GetOCRegistryURL(kubectlContext string) (string, error) {
cmd := exec.Command("oc", "registry", "info", "--internal=false", "--context", kubectlContext)
out, err := cmd.Output()
if err != nil {
return "", fmt.Errorf("oc registry info --context %q failed: %w", kubectlContext, err)
}
return strings.TrimSpace(string(out)), nil
}

// getOCToken retrieves the current OAuth token for a kubeconfig context via
// `oc whoami --show-token`. The username is irrelevant for OCP registry auth;
// skopeo accepts any non-empty string (we use "unused").
func GetOCToken(kubectlContext string) (string, error) {
cmd := exec.Command("oc", "whoami", "--show-token", "--context", kubectlContext)
out, err := cmd.Output()
if err != nil {
return "", fmt.Errorf("oc whoami --show-token --context %q failed: %w", kubectlContext, err)
}
return strings.TrimSpace(string(out)), nil
}
4 changes: 4 additions & 0 deletions e2e-tests/framework/scenario.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,24 +35,28 @@ func NewMigrationScenario(appName, namespace, k8sDeployBin, craneBin, srcCtx, tg
Namespace: namespace,
Bin: k8sDeployBin,
Context: srcCtx,
InsecureSkipTLSVerify: config.InsecureSkipTLSVerify,
},
TgtApp: K8sDeployApp{
Name: appName,
Namespace: namespace,
Bin: k8sDeployBin,
Context: tgtCtx,
InsecureSkipTLSVerify: config.InsecureSkipTLSVerify,
},
SrcAppNonAdmin: K8sDeployApp{
Name: appName,
Namespace: namespace,
Bin: k8sDeployBin,
Context: config.SourceNonAdminContext,
InsecureSkipTLSVerify: config.InsecureSkipTLSVerify,
},
TgtAppNonAdmin: K8sDeployApp{
Name: appName,
Namespace: namespace,
Bin: k8sDeployBin,
Context: config.TargetNonAdminContext,
InsecureSkipTLSVerify: config.InsecureSkipTLSVerify,
},
KubectlSrc: KubectlRunner{Bin: "kubectl", Context: srcCtx},
KubectlTgt: KubectlRunner{Bin: "kubectl", Context: tgtCtx},
Expand Down
1 change: 1 addition & 0 deletions e2e-tests/tests/e2e_suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ func init() {
flag.BoolVar(&config.VerboseLogs, "verbose-logs", false, "Enable verbose command/output logs for e2e runners")
flag.StringVar(&config.SourceNonAdminContext, "source-nonadmin-context", "", "Source cluster non-admin context for RBAC scenarios")
flag.StringVar(&config.TargetNonAdminContext, "target-nonadmin-context", "", "Target cluster non-admin context for RBAC scenarios")
flag.BoolVar(&config.InsecureSkipTLSVerify, "insecure-skip-tls-verify", false, "Skip TLS verification for e2e runners")
}

// TestE2E configures Ginkgo and executes the e2e test suite.
Expand Down
137 changes: 137 additions & 0 deletions e2e-tests/tests/mta_820_s2i_image_migration_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package e2e

import (
"fmt"
"log"
"path/filepath"

"github.com/konveyor/crane/e2e-tests/config"
. "github.com/konveyor/crane/e2e-tests/framework"
"github.com/konveyor/crane/e2e-tests/utils"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)

var _ = Describe("OCP image migration - S2I/BuildConfig-based image", func() {
It("[MTA-820] Should migrate a S2I/BuildConfig-built image from source to target OCP cluster",
Label("tier0", "ocp", "BUG crane-plugin-openshift#25", "BUG crane#331"),
func() {
// App: dockerbuild role — triggers a Dockerfile-strategy BuildConfig
// that builds centos+httpd and pushes to an ImageStream named "centos".
// This is the canonical S2I/internal-registry scenario for MTA-820.
appName := "dockerbuild"
namespace := appName

scenario := NewMigrationScenario(
appName,
namespace,
config.K8sDeployBin,
config.CraneBin,
config.SourceContext,
config.TargetContext,
)
srcApp := scenario.SrcApp
tgtApp := scenario.TgtApp
kubectlSrc := scenario.KubectlSrc
kubectlTgt := scenario.KubectlTgt
runner := scenario.Crane

srcApp.ExtraVars = map[string]any{
"docker_image": "centos",
"docker_image_tag": "7",
"output_imagestream": "centos",
"docker_file": `FROM quay.io/centos/centos:latest
RUN yum install -y httpd`,
}
tgtApp.ExtraVars = map[string]any{
"docker_image": "centos",
"docker_image_tag": "7",
"output_imagestream": "centos",
}

By("Prepare source app (triggers BuildConfig, waits for build, populates ImageStream)")
log.Printf("Preparing source app %s in namespace %s\n", srcApp.Name, srcApp.Namespace)
Expect(PrepareSourceApp(srcApp, kubectlSrc)).NotTo(HaveOccurred())
log.Printf("Source app %s prepared successfully\n", srcApp.Name)

paths, err := NewScenarioPaths("crane-export-ocp-s2i-*")
Expect(err).NotTo(HaveOccurred())
DeferCleanup(func() {
By("Cleanup source and target resources")
if err := CleanupScenario(paths.TempDir, srcApp, tgtApp); err != nil {
log.Printf("cleanup: %v", err)
}
})

runner.WorkDir = paths.TempDir

By("Run crane export (admin context — captures ImageStream + BuildConfig)")
log.Printf("Running crane export for namespace %s\n", namespace)
Expect(runner.Export(namespace, paths.ExportDir)).NotTo(HaveOccurred())
log.Printf("crane export complete\n")

By("Run crane transform --stage 10_KubernetesPlugin")
Expect(runner.TransformStage(paths.ExportDir, paths.TransformDir, "10_KubernetesPlugin")).NotTo(HaveOccurred())

// Workaround for crane#331: OpenShiftPlugin is not auto-staged and must be
// invoked explicitly via --stage. Without this the plugin is silently skipped
// and ImageStream references in the output still point to the source internal
// registry, causing image pull failures on the target cluster.
// TODO: remove explicit --stage once crane#331 is fixed.
By("Run crane transform --stage 20_OpenShiftPlugin (workaround: crane#331)")
Expect(runner.TransformStage(paths.ExportDir, paths.TransformDir, "20_OpenShiftPlugin")).NotTo(HaveOccurred())

By("Run crane apply (renders output manifests)")
Expect(runner.Apply(paths.ExportDir, paths.TransformDir, paths.OutputDir)).NotTo(HaveOccurred())
log.Printf("crane apply complete, output dir: %s\n", paths.OutputDir)

// Workaround for crane-plugin-openshift#25 / #26: crane apply emits
// ImageTag_*.yaml and ImageStreamTag_*.yaml files that the API server
// rejects on apply. Remove them before kubectl apply.
// TODO: remove once crane-plugin-openshift#25 / #26 are fixed.
By("Remove ImageTag and ImageStreamTag output files (workaround: crane-plugin-openshift#25, #26)")
outputResourcesDir := filepath.Join(paths.OutputDir, "resources", namespace)
Expect(utils.RemoveGlob(outputResourcesDir, "ImageTag_*.yaml")).NotTo(HaveOccurred())
Expect(utils.RemoveGlob(outputResourcesDir, "ImageStreamTag_*.yaml")).NotTo(HaveOccurred())

By("Resolve source and target OCP registry routes")
srcRegistry, err := GetOCRegistryURL(config.SourceContext)
Expect(err).NotTo(HaveOccurred())
tgtRegistry, err := GetOCRegistryURL(config.TargetContext)
Expect(err).NotTo(HaveOccurred())

By("Generate skopeo sync YAML from export dir (crane skopeo-sync-gen)")
syncYAMLPath := filepath.Join(paths.TempDir, "skopeo-sync-src.yaml")
Expect(runner.SkopeoSyncGen(paths.ExportDir, srcRegistry, syncYAMLPath)).NotTo(HaveOccurred())
log.Printf("skopeo-sync-gen YAML written to %s\n", syncYAMLPath)

By("Retrieve source and target OCP registry tokens (oc whoami --show-token)")
srcToken, err := GetOCToken(config.SourceContext)
Expect(err).NotTo(HaveOccurred())
tgtToken, err := GetOCToken(config.TargetContext)
Expect(err).NotTo(HaveOccurred())

By("Run skopeo sync: copy images from source to target registry")
// Destination is <TGT_REGISTRY>/<namespace>.
// --scoped must NOT be used — it produces wrong image paths against OCP.
// TLS verify is disabled on both sides; OCP registry routes use self-signed certs.
skopeo := SkopeoRunner{}
syncOpts := SkopeoSyncOptions{
SrcYAMLPath: syncYAMLPath,
DestRegistry: fmt.Sprintf("%s/%s", tgtRegistry, namespace),
SrcCreds: fmt.Sprintf("unused:%s", srcToken),
DestCreds: fmt.Sprintf("unused:%s", tgtToken),
}
Expect(skopeo.Sync(syncOpts)).NotTo(HaveOccurred())
log.Printf("skopeo sync complete\n")

By("Apply rendered manifests to target cluster")
log.Printf("Applying output manifests to target namespace %s\n", namespace)
Expect(ApplyOutputToTarget(kubectlTgt, namespace, paths.OutputDir)).NotTo(HaveOccurred())

By("Validate app on target cluster")
log.Printf("Validating app %s on target cluster\n", tgtApp.Name)
Eventually(tgtApp.Validate, "5m", "15s").Should(Succeed())
log.Printf("Target validation complete for app %s\n", tgtApp.Name)
})
})
16 changes: 16 additions & 0 deletions e2e-tests/utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"encoding/json"
"fmt"
"io"
"log"
"os"
"path/filepath"
"runtime"
Expand Down Expand Up @@ -880,3 +881,18 @@ func AssertKindsNotInActiveKustomizeResources(transformDir string, deniedKinds [
return nil
})
}

// RemoveGlob deletes all files matching pattern inside dir.
func RemoveGlob(dir, pattern string) error {
matches, err := filepath.Glob(filepath.Join(dir, pattern))
if err != nil {
return fmt.Errorf("glob %q in %q: %w", pattern, dir, err)
}
for _, f := range matches {
log.Printf("removing output file: %s\n", f)
if err := os.Remove(f); err != nil {
return fmt.Errorf("remove %q: %w", f, err)
}
}
return nil
}
Loading