Skip to content

feat: Add test configuration and execution tool - #321

Merged
myakove merged 7 commits into
RedHatQE:mainfrom
rgolangh:helper-to-create-manifests
Mar 30, 2026
Merged

myakove merged 7 commits into
RedHatQE:mainfrom
rgolangh:helper-to-create-manifests

Conversation

@rgolangh

@rgolangh rgolangh commented Mar 9, 2026 •

Copy link
Copy Markdown
Contributor

mtv-api-tests.py automates configuration and test execution.
It discovers vSphere and OpenShift resources via their APIs,
generates .providers.json and a self-contained mtv-api-tests-manifest.yaml
(Namespace + Secret + Job), and can run tests locally or as an OpenShift Job.

Generate Configuration

uv run mtv-api-tests generate

The wizard will:

  1. Prompt for vSphere credentials (or read from VSPHERE_HOST, VSPHERE_USERNAME, VSPHERE_PASSWORD)
  2. Connect to vSphere and let you select datastores, a test VM, and an ESXi host
  3. Prompt for storage vendor and credentials
  4. Connect to OpenShift and let you select a storage class
  5. Ask which test category to run (all, copyoffload, tier0, warm, remote)
  6. Write .providers.json and mtv-api-tests-manifest.yaml

Run Tests

uv run mtv-api-tests --mode local
# or run in a pod
uv run mtv-api-tests --mode job

Signed-off-by: Roy Golan rgolan@redhat.com

Summary by CodeRabbit

  • New Features

    • Interactive MTV API Tests CLI with setup wizard, generate and run commands (local or OpenShift Job) and a top-level console entrypoint/CLI command.
  • Documentation

    • Expanded README with interactive setup, run modes, CI/non-interactive usage, security guidance; updated copy-offload guide to include the quick interactive workflow.
  • Chores

    • Docker build converted to multi-stage for smaller runtime and streamlined environment; packaging updated to expose the new CLI.
  • Ignore rules

    • Generated manifests and related files added to ignore lists; expanded tooling ignore patterns.

@coderabbitai

coderabbitai Bot commented Mar 9, 2026 •

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds a Typer CLI (mtv-api-tests) with interactive generate and run commands for vSphere/OpenShift discovery and Job manifest generation, implements atomic providers.json handling and multi-doc Job YAML creation, converts the Dockerfile to a multi-stage build, updates packaging (hatchling/console script) and ignore rules, and tweaks cluster-client env/SSL fallback logic.

Changes

Cohort / File(s) Summary
Docker build & context
Dockerfile, .dockerignore
Convert to multi-stage build (builder + runtime), move uv sync --locked into builder, copy cli earlier, relocate ENV (JUNITFILE/PYTHON_* ) to runtime, remove redundant final sync; expand .dockerignore to exclude .env*, .providers.json, and mtv-api-tests-manifests.yaml.
CLI package & entry
pyproject.toml, cli/mtv_api_tests/__init__.py, cli/mtv_api_tests/__main__.py
Add Typer app with generate/run commands and RunMode enum; register mtv-api-tests console script; change project license to Apache-2.0; add Hatch (hatchling) build-system and wheel target packages.
Interactive wizard & shared utilities
cli/mtv_api_tests/common.py
Large new module: Rich/Typer helpers, credential prompts, vSphere and OpenShift discovery (versions, datastores, VMs, storageclasses), provider builders, masking, base64 helpers, atomic write of .providers.json (0o600), and generate_job_yaml() producing Namespace+Secret+Job multi-doc YAML.
Generate command
cli/mtv_api_tests/generate.py
New interactive generate_command(image, category) implementing the setup wizard: gather OCP/vSphere creds, discover resources, collect vendor/storage settings, build/confirm providers, write .providers.json, and emit mtv-api-tests-manifests.yaml with next-step guidance.
Run command
cli/mtv_api_tests/run.py
New run_command(...) supporting --mode job (validate/apply Job YAML, detect unchanged, show oc logs -f) and --mode local (load/validate .providers.json, select providers/storage-class, resolve cluster auth/token or prompt, build and run uv run pytest with --tc, -m, -k).
Utilities change
utilities/utils.py
Refactor get_cluster_client() to prefer py_config but fallback to CLUSTER_* env vars; change CLUSTER_VERIFY_SSL parsing to invert previous semantics for insecure_verify_skip; simplify client validation/return path.
Docs & ignores
README.md, guides/copyoffload/...md, .gitignore
Add Interactive Setup Tool docs, generate/run usage (interactive and non-interactive/CI flags), security note for generated manifests; update copyoffload guide to include interactive quick path; add .gitignore entry for mtv-api-tests-manifests.yaml and broaden .claude/* ignores with explicit un-ignore exceptions.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant CLI as mtv-api-tests CLI
    participant RichUI as Rich Prompts
    participant vSphere
    participant OpenShift
    participant FileSystem as File System

    User->>CLI: uv run mtv-api-tests generate
    CLI->>RichUI: Prompt for category & filters
    RichUI-->>CLI: category, filter

    rect rgba(200,150,100,0.5)
        Note over CLI,OpenShift: OpenShift discovery
        CLI->>OpenShift: Connect & auth (kubeconfig/API creds)
        OpenShift-->>CLI: OCP version, StorageClasses, detect MTV
        CLI->>RichUI: Prompt select storage class
        RichUI-->>CLI: storage_class
    end

    rect rgba(100,150,200,0.5)
        Note over CLI,vSphere: vSphere discovery
        CLI->>vSphere: Connect & auth
        vSphere-->>CLI: version, datastores, VMs, hosts
        CLI->>RichUI: Prompt select datastores, VM, host, vendor fields
        RichUI-->>CLI: selections & credentials
    end

    rect rgba(150,200,100,0.5)
        Note over CLI,FileSystem: Config generation
        CLI->>CLI: Build providers.json and Job YAML
        CLI->>FileSystem: Atomic write .providers.json (0o600)
        CLI->>FileSystem: Write mtv-api-tests-manifests.yaml
    end

    CLI->>RichUI: Show next-steps (local/job run commands)
    RichUI->>User: Display commands
Loading
sequenceDiagram
    participant User
    participant CLI as mtv-api-tests run
    participant FileSystem as File System
    participant Provider as Providers.json
    participant OCP as oc / OpenShift
    participant Local as uv pytest

    User->>CLI: uv run mtv-api-tests run --mode local --category copyoffload
    CLI->>FileSystem: Read .providers.json
    FileSystem-->>CLI: providers data
    CLI->>Provider: Validate providers and types
    CLI->>CLI: Select source (non-openshift) & destination (openshift)

    rect rgba(100,150,200,0.5)
        Note over CLI,OCP: Cluster auth resolution
        alt provider has host/creds
            CLI->>OCP: Use provider host/user/pass as --tc env
        else kubeconfig/token available
            CLI->>OCP: oc whoami -t
            OCP-->>CLI: token
        else
            CLI->>User: Prompt for API/creds
        end
    end

    rect rgba(150,200,100,0.5)
        Note over CLI,Local: Local execution
        CLI->>Local: Build pytest command (uv run pytest --tc ... -m ... -k ...)
        Local-->>CLI: Run and return exit code
    end

    CLI->>User: Exit with pytest return code
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main objective: introducing a comprehensive CLI tool (mtv-api-tests) for interactive test configuration and execution, covering both setup and running workflows.
Docstring Coverage ✅ Passed Docstring coverage is 96.55% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 and usage tips.

@redhat-qe-bot

Copy link
Copy Markdown

Report bugs in Issues

Welcome! 🎉

This pull request will be automatically processed with the following features:

🔄 Automatic Actions

  • Reviewer Assignment: Reviewers are automatically assigned based on the OWNERS file in the repository root
  • Size Labeling: PR size labels (XS, S, M, L, XL, XXL) are automatically applied based on changes
  • Issue Creation: Disabled for this repository
  • Branch Labeling: Branch-specific labels are applied to track the target branch
  • Auto-verification: Auto-verified users have their PRs automatically marked as verified
  • Labels: All label categories are enabled (default configuration)

📋 Available Commands

PR Status Management

  • /wip - Mark PR as work in progress (adds WIP: prefix to title)
  • /wip cancel - Remove work in progress status
  • /hold - Block PR merging (approvers only)
  • /hold cancel - Unblock PR merging
  • /verified - Mark PR as verified
  • /verified cancel - Remove verification status
  • /reprocess - Trigger complete PR workflow reprocessing (useful if webhook failed or configuration changed)
  • /regenerate-welcome - Regenerate this welcome message

Review & Approval

  • /lgtm - Approve changes (looks good to me)
  • /approve - Approve PR (approvers only)
  • /automerge - Enable automatic merging when all requirements are met (maintainers and approvers only)
  • /assign-reviewers - Assign reviewers based on OWNERS file
  • /assign-reviewer @username - Assign specific reviewer
  • /check-can-merge - Check if PR meets merge requirements

Testing & Validation

  • /retest tox - Run Python test suite with tox
  • /retest build-container - Rebuild and test container image
  • /retest conventional-title - Validate commit message format
  • /retest all - Run all available tests

Container Operations

  • /build-and-push-container - Build and push container image (tagged with PR number)
    • Supports additional build arguments: /build-and-push-container --build-arg KEY=value

Cherry-pick Operations

  • /cherry-pick <branch> - Schedule cherry-pick to target branch when PR is merged
    • Multiple branches: /cherry-pick branch1 branch2 branch3

Label Management

  • /<label-name> - Add a label to the PR
  • /<label-name> cancel - Remove a label from the PR

✅ Merge Requirements

This PR will be automatically approved when the following conditions are met:

  1. Approval: /approve from at least one approver
  2. Status Checks: All required status checks must pass
  3. No Blockers: No wip, hold, has-conflicts labels and PR must be mergeable (no conflicts)
  4. Verified: PR must be marked as verified

📊 Review Process

Approvers and Reviewers

Approvers:

  • myakove

Reviewers:

  • krcmarik
  • myakove
Available Labels
  • hold
  • verified
  • wip
  • lgtm
  • approve
  • automerge

💡 Tips

  • WIP Status: Use /wip when your PR is not ready for review
  • Verification: The verified label is automatically removed on each new commit
  • Cherry-picking: Cherry-pick labels are processed when the PR is merged
  • Container Builds: Container images are automatically tagged with the PR number
  • Permission Levels: Some commands require approver permissions
  • Auto-verified Users: Certain users have automatic verification and merge privileges

For more information, please refer to the project documentation or contact the maintainers.

@qodo-code-review

Copy link
Copy Markdown

Review Summary by Qodo

Add interactive MTV test configuration and execution CLI tool

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Add interactive CLI tool for MTV test configuration and execution
  - Discovers vSphere/OpenShift resources via APIs
  - Generates .providers.json and self-contained job.yaml
  - Supports local and OpenShift Job test execution modes
• Modernize Dockerfile with UBI-minimal base and improved security
  - Switch from Fedora to UBI-minimal for production readiness
  - Add non-root user (1001) and security context settings
  - Improve Python environment variables for logging and caching
• Add Makefile with convenient targets for common operations
• Document interactive setup tool in README and copyoffload guide
Diagram
flowchart LR
  A["User"] -->|"make generate"| B["mtv-api-tests.py<br/>generate command"]
  B -->|"Discover resources"| C["vSphere API"]
  B -->|"Discover resources"| D["OpenShift API"]
  C -->|"Return datastores,<br/>VMs, hosts"| E["Interactive Wizard"]
  D -->|"Return storage<br/>classes"| E
  E -->|"Gather credentials<br/>& selections"| F[".providers.json<br/>+ job.yaml"]
  F -->|"make run-local"| G["Local pytest<br/>execution"]
  F -->|"make run-job"| H["oc apply<br/>OpenShift Job"]
Loading

Grey Divider

File Changes

1. tools/mtv-api-tests.py ✨ Enhancement +1179/-0

Interactive CLI tool for MTV test configuration and execution

• New 1179-line interactive CLI tool with two main commands: generate and run
• generate command: Interactive wizard that discovers vSphere/OpenShift resources and produces
 .providers.json and job.yaml
• run command: Executes tests locally via pytest or as OpenShift Job via oc apply
• Comprehensive credential gathering from environment variables or interactive prompts
• vSphere discovery functions for datastores, VMs, ESXi hosts with PropertyCollector optimization
• OpenShift discovery for storage classes and MTV validation
• Support for multiple storage vendors (NetApp ONTAP, Hitachi Vantara, HPE Primera/3PAR, Pure, Dell
 PowerFlex/PowerMax/PowerStore, IBM FlashSystem, Infinidat)
• Test category selection (all, copyoffload, tier0, warm, remote) with pytest marker support
• YAML generation for Kubernetes Secret and Job with base64-encoded configuration

tools/mtv-api-tests.py


2. Dockerfile ⚙️ Configuration changes +16/-15

Modernize Dockerfile with UBI-minimal and security hardening

• Replace Fedora 41 base image with UBI-minimal 10.1 for production-grade container
• Add environment variables for Python optimization: PYTHONDONTWRITEBYTECODE, PYTHONUNBUFFERED,
 UV_NO_CACHE
• Switch package manager from dnf to microdnf for minimal footprint
• Simplify package cleanup and remove redundant cache clearing
• Copy entire project directory instead of selective files
• Add non-root user (1001) with group permissions for OpenShift security compliance
• Install uv via pip instead of copying from builder image

Dockerfile


3. Makefile ⚙️ Configuration changes +18/-0

Add Makefile with build and test execution targets

• New Makefile with convenient targets for common operations
• build-image: Build container image with podman
• push-image: Push image to registry
• generate: Run interactive configuration wizard
• run-local: Execute tests locally
• run-job: Execute tests as OpenShift Job

Makefile


View more (2)
4. README.md 📝 Documentation +37/-0

Document interactive setup tool and execution modes

• Add new "Interactive Setup Tool" section documenting the CLI tool
• Explain the generate command workflow and credential sources
• Document run-local and run-job execution modes
• Note interactive prompts when optional parameters are omitted
• Update copyoffload guide to reference the interactive tool as the recommended quick approach
• Add link to main README from copyoffload documentation

README.md


5. docs/copyoffload/how-to-run-copyoffload-tests.md Additional files +21/-1

...

docs/copyoffload/how-to-run-copyoffload-tests.md


Grey Divider

Qodo Logo

@qodo-code-review

qodo-code-review Bot commented Mar 9, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (18) 📘 Rule violations (7) 📎 Requirement gaps (0) 📐 Spec deviations (0)

Grey Divider


Action required

1. OCP host field required 🐞 Bug ✓ Correctness ⭐ New
Description
The CLI local-run path requires the OpenShift provider to contain a "host" field; the repository’s
documented example provider uses "api_url" and will cause the CLI to abort with “missing required
fields”. This breaks compatibility with existing/manual .providers.json files created from the repo
example and prevents using the OCP provider for local-run cluster credentials/storage_class
autofill.
Code

cli/mtv_api_tests/run.py[R124-135]

+    if ocp_provider:
+        host = ocp_provider.get("host")
+        username = ocp_provider.get("username")
+        password = ocp_provider.get("password")
+        if not all([host, username, password]):
+            missing = [k for k in ("host", "username", "password") if not ocp_provider.get(k)]
+            console.print(
+                f"[red]Error: OCP provider is missing required fields: {', '.join(missing)}. "
+                f"Update the provider in .providers.json or run 'generate' again.[/red]"
+            )
+            raise typer.Exit(code=1)
+        else:
Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`cli/mtv_api_tests/run.py:_get_cluster_config` requires `ocp_provider['host']`, but the repo’s `.providers.json.example` uses `api_url` for OpenShift providers. This causes CLI local runs to exit even with a valid OpenShift provider.

### Issue Context
- The new `generate` command writes both `host` and `api_url`, but users may already have `.providers.json` created from the example file or older schema.

### Fix Focus Areas
- cli/mtv_api_tests/run.py[124-146]  
 - Accept `api_url` as an alias for `host` (e.g., `host = ocp_provider.get('host') or ocp_provider.get('api_url')`).
 - Update the missing-fields error message to mention accepted keys.
- (Optional) cli/mtv_api_tests/common.py[695-734]  
 - Keep writing both keys for forward/backward compatibility.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. CA bundle ignored in Job 🐞 Bug ⛯ Reliability ⭐ New
Description
The wizard collects and stores an OpenShift CA bundle in the generated Job Secret, but the Job never
mounts/uses it and the test runner never consumes it, so TLS verification with a custom CA cannot
work in Job mode. Users enabling verify_ssl with a custom CA will still fail TLS handshakes and may
be forced to disable verification.
Code

cli/mtv_api_tests/common.py[R856-878]

+    verify_ssl = ocp_creds.get("verify_ssl", "false")
+    ca_bundle = ocp_creds.get("ca_bundle", "")
+
+    # ca_bundle may be a file path; read its contents so we encode the PEM data, not the path.
+    ca_bundle_content = ""
+    if ca_bundle:
+        ca_path = Path(ca_bundle)
+        if ca_path.exists():
+            ca_bundle_content = ca_path.read_text()
+        else:
+            ca_bundle_content = ca_bundle  # already PEM content, not a path
+
+    return (
+        f"  providers.json: {b64(providers_json_content)}\n"
+        f"  cluster_host: {b64(ocp_creds['host'])}\n"
+        f"  cluster_username: {b64(ocp_creds['username'])}\n"
+        f"  cluster_password: {b64(ocp_creds['password'])}\n"
+        f"  cluster_verify_ssl: {b64(verify_ssl)}\n"
+        f"  # TODO: ca_bundle is stored here but not yet consumed by the test runner container.\n"
+        f"  # When the test runner supports CA bundle injection, mount this as a file and set\n"
+        f"  # KUBERNETES_CLIENT_CA_BUNDLE or pass via --tc flag.\n"
+        f"  cluster_ca_bundle: {b64(ca_bundle_content)}"
+    )
Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The generated Job manifest stores a CA bundle but does not apply it, making `verify_ssl=true` unusable for clusters with non-system/self-signed CAs.

### Issue Context
- `cli/mtv_api_tests/common.py:_build_job_secret_data` writes `cluster_ca_bundle` to the Secret.
- The Job spec does not mount it as a file, and the test code (`utilities/utils.py:get_cluster_client`) does not read it.

### Fix Focus Areas
- cli/mtv_api_tests/common.py[846-979]
 - Mount `cluster_ca_bundle` as a file (e.g., `/etc/pki/ca-trust/source/anchors/cluster-ca.pem` or `/tmp/cluster-ca.pem`) via an additional `volumeMounts` entry with `subPath: cluster_ca_bundle`.
 - Set an env var the client respects (e.g., `KUBERNETES_CLIENT_CA_BUNDLE=/tmp/cluster-ca.pem`) or extend the tests to accept a new `--tc=cluster_ca_bundle_path:`.
- utilities/utils.py[601-640]
 - Add support for a CA bundle path/content (preferably path) when creating the DynamicClient (e.g., pass/propagate `KUBERNETES_CLIENT_CA_BUNDLE` or equivalent expected by `ocp_resources.resource.get_client`).
- (Optional) cli/mtv_api_tests/run.py[110-171]
 - When using OCP provider config locally, propagate CA bundle info similarly for local runs.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. VERIFY_SSL env inconsistent 🐞 Bug ⛨ Security
Description
CLUSTER_VERIFY_SSL is parsed inconsistently: the CLI treats true/1/yes as enabling verification,
but get_cluster_client() only treats the exact string true as enabling verification. This can
silently disable SSL verification when users set CLUSTER_VERIFY_SSL=1 or yes.
Code

utilities/utils.py[R633-640]

+    verify_ssl_env = os.environ.get("CLUSTER_VERIFY_SSL")
+    if verify_ssl_env is not None:
+        insecure_verify_skip = verify_ssl_env.lower() != "true"
+    else:
+        insecure_verify_skip = get_value_from_py_config("insecure_verify_skip")
+        if insecure_verify_skip is None:
+            insecure_verify_skip = True
 _client = get_client(host=host, username=username, password=password, verify_ssl=not insecure_verify_skip)
Evidence
The CLI’s SSL prompt/env parsing accepts multiple truthy values (true, 1, yes). The suite’s
new env override in get_cluster_client() only checks lower() != "true", so 1/yes are treated
as false, flipping verify_ssl behavior compared to what the CLI generated/prompted.

cli/mtv_api_tests/common.py[269-308]
utilities/utils.py[601-644]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`CLUSTER_VERIFY_SSL` env var is interpreted differently by the CLI and the suite. CLI treats `1/yes` as true, but `utilities.utils.get_cluster_client()` treats anything other than the literal `true` as false.
## Issue Context
This can cause unexpected insecure connections when users set `CLUSTER_VERIFY_SSL=1` or `CLUSTER_VERIFY_SSL=yes`.
## Fix Focus Areas
- utilities/utils.py[633-640]
- cli/mtv_api_tests/common.py[269-308]
## Implementation guidance
- Implement a shared boolean parsing helper (e.g., `parse_bool_env(value)`) and use it in `get_cluster_client()`.
- Accept at least: truthy = {"true","1","yes"} and falsy = {"false","0","no"}; if set to an unrecognized value, fail fast with a clear error.
- Keep the inverted semantics documented: `CLUSTER_VERIFY_SSL=true` => `insecure_verify_skip=False`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (20)
4. Wrong default test image 🐞 Bug ✓ Correctness
Description
The CLI’s default Job image is set to quay.io/rgolangh/mtv-api-tests:latest, which conflicts with
the README’s documented public image (ghcr.io/redhatqe/mtv-api-tests:latest) and will pull/run an
unexpected image by default.
Code

cli/mtv_api_tests/common.py[R31-34]

+console = Console()
+JOB_YAML_PATH = Path("mtv-api-tests-manifests.yaml")
+DEFAULT_TEST_IMAGE = "quay.io/rgolangh/mtv-api-tests:latest"
+
Evidence
DEFAULT_TEST_IMAGE in the new CLI points to a different registry/org than the project
documentation. Since generate uses this as the default for the Job manifest, users who don’t
override --image will run the wrong container image.

cli/mtv_api_tests/common.py[31-34]
README.md[50-58]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The CLI defaults to a personal image (`quay.io/rgolangh/...`) while the repository documentation points to the official published image (`ghcr.io/redhatqe/...`).
## Issue Context
This affects generated Job manifests and will surprise users.
## Fix Focus Areas
- cli/mtv_api_tests/common.py[31-34]
- README.md[50-58]
## Suggested fix
- Change `DEFAULT_TEST_IMAGE` to `ghcr.io/redhatqe/mtv-api-tests:latest` (or update docs if the intended default has changed).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. connect_ocp() wrong get_client 📘 Rule violation ⛨ Security
Description
connect_ocp() instantiates the cluster client via ocp_resources.resource.get_client() instead of
the required ocp_utilities.infra.get_client(). This violates the DynamicClient coupling
restrictions and may introduce unsupported runtime behavior.
Code

cli/mtv_api_tests/common.py[R568-579]

+            if "kubeconfig" in creds:
+                client = get_client()
+            else:
+                if verify_ssl and ca_bundle:
+                    os.environ["KUBERNETES_CLIENT_CA_BUNDLE"] = ca_bundle
+
+                client = get_client(
+                    host=creds["host"],
+                    username=creds["username"],
+                    password=creds["password"],
+                    verify_ssl=verify_ssl,
+                )
Evidence
PR Compliance ID 9 requires DynamicClient instantiation to be done via
ocp_utilities.infra.get_client(). The added code calls get_client(...) imported from
ocp_resources.resource when creating the OpenShift client.

CLAUDE.md
cli/mtv_api_tests/common.py[568-579]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`cli/mtv_api_tests/common.py` creates the OpenShift client with `ocp_resources.resource.get_client()`, but the compliance rule requires instantiation via `ocp_utilities.infra.get_client()`.
## Issue Context
This repo enforces restrictions around `DynamicClient` runtime coupling.
## Fix Focus Areas
- cli/mtv_api_tests/common.py[568-579]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. Missing libcurl in runtime 🐞 Bug ⛯ Reliability
Description
The new runtime stage does not install any libcurl runtime package, but the lockfile includes
pycurl, so container runs can fail at import/runtime due to missing libcurl.so. Builder installs
libcurl-devel, which further indicates a libcurl-linked dependency that must exist in the final
image too.
Code

Dockerfile[R59-63]

+RUN microdnf -y install \
+  python3 \
+  libxml2 \
+  openssl \
+  && microdnf clean all
Evidence
In the new multi-stage Dockerfile, the builder installs libcurl-devel but the runtime stage
installs only python3, libxml2, and openssl, leaving no guarantee that a libcurl shared
library exists in the final image. The repo’s uv.lock includes the pycurl package, which
requires libcurl at runtime; without it, imports can fail and tests won’t start.

Dockerfile[17-26]
Dockerfile[55-63]
uv.lock[1628-1633]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The runtime stage of `Dockerfile` omits libcurl, but the Python environment includes `pycurl` (from `uv.lock`). This can cause runtime failures when importing/using pycurl due to missing `libcurl.so`.
## Issue Context
- Builder installs `libcurl-devel`.
- Runtime stage installs `python3`, `libxml2`, `openssl` only.
- `uv.lock` contains `pycurl`.
## Fix Focus Areas
- Dockerfile[55-63]
### Suggested change
Add an appropriate runtime libcurl package (e.g., `libcurl` or `libcurl-minimal`, whichever provides `libcurl.so` on UBI10) to the runtime `microdnf install` list, and keep the comment accurate.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. insecure_verify_skip can be None 📘 Rule violation ✓ Correctness
Description
insecure_verify_skip is set from get_value_from_py_config() without a default, so it can become
None and invert SSL behavior (not None -> True) unexpectedly. This violates the requirement
for consistent variable types and can silently change TLS verification behavior.
Code

utilities/utils.py[R633-637]

+    verify_ssl_env = os.environ.get("CLUSTER_VERIFY_SSL")
+    if verify_ssl_env is not None:
+        insecure_verify_skip = verify_ssl_env.lower() != "true"
+    else:
+        insecure_verify_skip = get_value_from_py_config("insecure_verify_skip")
Evidence
PR Compliance ID 10 requires normalizing variables to a consistent type; here insecure_verify_skip
can be bool (from env) or None (from missing py_config), and is later used as a boolean. PR
Compliance ID 7 also applies because a missing config value becomes None instead of being handled
explicitly with a clear default/exception.

CLAUDE.md
CLAUDE.md
utilities/utils.py[633-637]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`insecure_verify_skip` can be `None` when neither `CLUSTER_VERIFY_SSL` nor `py_config["insecure_verify_skip"]` are set, causing inconsistent types and potentially incorrect SSL verification behavior.
## Issue Context
`get_value_from_py_config("insecure_verify_skip")` returns `None` when unset. The function should set an explicit boolean default (per the docstring intent) or raise a clear error if the value is required.
## Fix Focus Areas
- utilities/utils.py[633-637]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


8. Kubeconfig defaults to insecure SSL 🐞 Bug ⛨ Security
Description
When OpenShift auth is via kubeconfig, SSL settings are never collected, so generate_job_yaml()
defaults cluster_verify_ssl to "false" and the generated Job will skip SSL verification. This
silently weakens security for the common kubeconfig-based flow.
Code

cli/mtv_api_tests/common.py[R351-356]

+    console.print("\n[bold]OpenShift Credentials[/bold]")
+    kubeconfig = os.environ.get("KUBECONFIG", "").strip()
+    if kubeconfig:
+        console.print(f"  [dim]Using KUBECONFIG:[/dim] {kubeconfig}")
+        return {"kubeconfig": kubeconfig}
+
Evidence
get_ocp_credentials() returns early with only {"kubeconfig": ...} and never calls
_get_ssl_config() in that path. Later, generate_job_yaml() uses `ocp_creds.get("verify_ssl",
"false") and embeds it as cluster_verify_ssl` in the Secret; the test harness interprets
CLUSTER_VERIFY_SSL != "true" as skipping verification.

cli/mtv_api_tests/common.py[339-376]
cli/mtv_api_tests/common.py[885-926]
utilities/utils.py[601-637]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Kubeconfig-based OpenShift discovery returns without SSL config, and Job generation defaults `verify_ssl` to "false", causing generated Jobs to skip TLS verification.
### Issue Context
- `get_ocp_credentials()` returns `{"kubeconfig": ...}` early.
- `generate_job_yaml()` defaults `verify_ssl` to "false" when not present.
- `get_cluster_client()` maps `CLUSTER_VERIFY_SSL` to `verify_ssl` (inverted via `insecure_verify_skip`).
### Fix Focus Areas
- cli/mtv_api_tests/common.py[339-376]
- cli/mtv_api_tests/common.py[885-926]
- cli/mtv_api_tests/generate.py[228-244]
### Implementation notes
- For kubeconfig flows, still ask (or read env) for `CLUSTER_VERIFY_SSL` and optional CA bundle, and propagate into `job_creds` / provider config.
- Change `generate_job_yaml()` default to secure-by-default (e.g., default `verify_ssl` to "true") when SSL config is absent.
- Ensure `cluster_verify_ssl` in the Secret matches the semantics expected by `utilities/utils.py`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


9. Partial secret file writes 🐞 Bug ⛯ Reliability
Description
_write_secret_file() uses a single os.write() call and ignores its return value, so partial writes
can truncate the generated .providers.json or mtv-api-tests-manifests.yaml. Because the truncated
temp file is then fsynced and atomically os.replace()'d into place, users can end up with
persistently corrupted JSON/YAML and confusing downstream errors.
Code

cli/mtv_api_tests/common.py[R815-822]

+        tmp_fd, tmp_path = tempfile.mkstemp(dir=str(path.parent), prefix=f".{path.name}.", suffix=".tmp")
+        os.fchmod(tmp_fd, 0o600)
+        os.write(tmp_fd, content.encode())
+        os.fsync(tmp_fd)
+        os.close(tmp_fd)
+        tmp_fd = None
+        os.replace(tmp_path, str(path))
+        tmp_path = None
Evidence
In the new CLI helper, _write_secret_file() writes content via os.write(tmp_fd, content.encode())
without checking that all bytes were written. os.write can legally return fewer bytes than
requested; the function then fsyncs/closes and replaces the destination, making the truncated file
the new authoritative config/manifest. This function is used for both providers.json writes
(write_providers_json -> _write_secret_file) and the generated Job manifest (generate.py ->
_write_secret_file).

cli/mtv_api_tests/common.py[800-831]
cli/mtv_api_tests/common.py[770-797]
cli/mtv_api_tests/generate.py[328-340]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`cli/mtv_api_tests/common.py:_write_secret_file()` uses a single `os.write()` and ignores the return value. `os.write()` may write fewer bytes than requested, resulting in truncated output. Since the function then `fsync`s and `os.replace()`s the temp file, it can atomically persist corrupted `.providers.json` or `mtv-api-tests-manifests.yaml`.
## Issue Context
This helper is used both when writing/merging `.providers.json` and when writing the generated Job manifest.
## Fix Focus Areas
- cli/mtv_api_tests/common.py[800-831]
- cli/mtv_api_tests/common.py[770-797]
- cli/mtv_api_tests/generate.py[328-340]
## Implementation guidance
- Replace the raw-fd `os.write()` usage with a file object (`os.fdopen(tmp_fd, 'wb')`) and `f.write(...)` (which loops internally), then `f.flush()` + `os.fsync(f.fileno())`.
- Be careful to not double-close the fd: once wrapped by `fdopen`, manage it via the context manager and set `tmp_fd = None` after successful wrapping.
- Alternatively, implement a loop around `os.write()` until all bytes are written.
- Keep current atomic replace + 0600 permission behavior.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


10. PVC missing storageClassName 🐞 Bug ⛯ Reliability
Description
The generated Job manifest creates a PersistentVolumeClaim without spec.storageClassName, which
can leave the Job stuck Pending indefinitely on clusters without a default StorageClass.
Code

cli/mtv_api_tests/common.py[R902-913]

+apiVersion: v1
+kind: PersistentVolumeClaim
+metadata:
+  name: {namespace}-artifacts
+  namespace: {namespace}
+spec:
+  accessModes:
+    - ReadWriteOnce
+  resources:
+    requests:
+      storage: 5Gi
+
Evidence
generate_job_yaml() emits a PVC spec without storageClassName even though the wizard already
prompts the user for a storage class; on clusters lacking a default StorageClass, such PVCs won’t
bind and the Job never starts.

cli/mtv_api_tests/common.py[902-913]
cli/mtv_api_tests/generate.py[214-245]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The generated manifest includes a PVC for `/artifacts` but does not set `spec.storageClassName`. On clusters without a default StorageClass, the PVC will remain `Pending` and the Job will never run.
### Issue Context
The wizard already collects `storage_class`, so the manifest generator has the value available.
### Fix Focus Areas
- cli/mtv_api_tests/common.py[902-913]
### Suggested fix
Add `storageClassName: {storage_class}` under the PVC `spec:` block, or remove the PVC entirely and write JUnit to the existing `/app` path if persistence is not required.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


11. oc apply bypasses create helper 📘 Rule violation ⛯ Reliability
Description
The new CLI deploys OpenShift resources by shelling out to oc apply with a generated manifest,
bypassing create_and_store_resource() and centralized teardown tracking. This can lead to leaked
resources and naming collisions under repeated/parallel runs.
Code

cli/mtv_api_tests/run.py[R224-230]

+    console.print("\n[bold]Deploying Namespace, Secret, and Job...[/bold]")
+    result = subprocess.run(
+        ["oc", "apply", "-f", str(job_yaml_path)],
+        capture_output=True,
+        text=True,
+        check=False,
+    )
Evidence
PR Compliance ID 7 requires all OpenShift resource creation to go through
create_and_store_resource(). The PR creates Namespace/Secret/Job by generating YAML and applying
it with oc apply, which bypasses the required helper and teardown tracking.

CLAUDE.md
cli/mtv_api_tests/run.py[224-230]
cli/mtv_api_tests/common.py[859-947]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
OpenShift resources are created via `oc apply` on a generated YAML manifest, bypassing the required `create_and_store_resource()` path and teardown tracking.
## Issue Context
Resource creation must be centralized to ensure consistent naming, waits, and cleanup.
## Fix Focus Areas
- cli/mtv_api_tests/run.py[224-234]
- cli/mtv_api_tests/common.py[822-947]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


12. Missing cluster_host fallback 🐞 Bug ⛯ Reliability
Description
Local runs that rely on an existing oc token don't pass --tc=cluster_host, and
get_cluster_client() still has no env fallback for cluster_host, causing client creation to fail
when the default pytest-testconfig lacks it.
Code

utilities/utils.py[R615-619]

host = get_value_from_py_config("cluster_host")
-    username = get_value_from_py_config("cluster_username")
-    password = get_value_from_py_config("cluster_password")
+    username = get_value_from_py_config("cluster_username") or os.environ.get("CLUSTER_USERNAME")
+    password = get_value_from_py_config("cluster_password") or os.environ.get("CLUSTER_PASSWORD")
insecure_verify_skip = get_value_from_py_config("insecure_verify_skip")
_client = get_client(host=host, username=username, password=password, verify_ssl=not insecure_verify_skip)
Evidence
get_cluster_client() always reads cluster_host only from pytest-testconfig and passes it to
get_client(), but the default tests/tests_config/config.py has no cluster_host key. In the new
CLI, when oc whoami -t succeeds, _get_cluster_config() returns no --tc= args, so
cluster_host remains unset and the test session can fail during client creation (before tests
execute).

utilities/utils.py[601-623]
tests/tests_config/config.py[1-13]
cli/mtv_api_tests/run.py[143-166]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Local runs can fail when relying on an existing `oc` token because `cluster_host` is not provided via `--tc=` and `get_cluster_client()` has no environment fallback for it.
### Issue Context
- Default `tests/tests_config/config.py` does not define `cluster_host`.
- `_get_cluster_config()` returns `[], {}` when `oc whoami -t` succeeds.
- `get_cluster_client()` still requires `cluster_host`.
### Fix Focus Areas
- Add `CLUSTER_HOST` env fallback in cluster client creation:
- utilities/utils.py[615-619]
- Ensure local token path still supplies `cluster_host` (either via `--tc=cluster_host:...` or setting `CLUSTER_HOST` from kubeconfig/`oc whoami --show-server`):
- cli/mtv_api_tests/run.py[143-166]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


13. Secret perms not enforced 🐞 Bug ⛨ Security
Description
_write_secret_file() intends to write secrets with 0600, but when overwriting an existing file,
POSIX preserves the original file mode, potentially leaving regenerated .providers.json or the
manifest readable by other users.
Code

cli/mtv_api_tests/common.py[R803-807]

+    fd = os.open(str(path), os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
+    try:
+        os.write(fd, content.encode())
+    finally:
+        os.close(fd)
Evidence
The code uses os.open(..., mode=0o600) with O_TRUNC. If the file already exists (e.g.,
.providers.json created earlier with 0644), truncation does not reset its permissions; the
previous mode remains. This impacts both the providers file writer and the generated Job YAML writer
which call _write_secret_file().

cli/mtv_api_tests/common.py[763-808]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Overwriting an existing secrets file may keep permissive file modes, leaking credentials.
### Issue Context
`os.open(..., 0o600)` only applies the mode on file creation; existing file permissions are preserved.
### Fix Focus Areas
- Enforce permissions even when overwriting (e.g., `os.fchmod(fd, 0o600)` after open, before write, or `os.chmod(path, 0o600)` after close):
- cli/mtv_api_tests/common.py[793-808]
- Confirm both call sites are covered (.providers.json and manifest YAML):
- cli/mtv_api_tests/common.py[763-790]
- cli/mtv_api_tests/generate.py[324-336]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


14. Wrong ContainerView type 🐞 Bug ✓ Correctness
Description
discover_vms() builds a PropertyCollector traversal spec using type=vim.ContainerView, which is
likely not a valid pyVmomi type (ContainerView is under vim.view.ContainerView), risking runtime
failure during VM discovery.
Code

cli/mtv_api_tests/common.py[R475-485]

+        obj_spec = vim.PropertyCollector.ObjectSpec(  # type: ignore[attr-defined]
+            obj=container,
+            skip=True,
+            selectSet=[
+                vim.PropertyCollector.TraversalSpec(  # type: ignore[attr-defined]
+                    type=vim.ContainerView,
+                    path="view",
+                    skip=False,
+                )
+            ],
+        )
Evidence
The traversal spec references vim.ContainerView, but elsewhere in the repo pyVmomi view types are
accessed via vim.view.* (e.g., vim.view.ViewManager). Using a non-existent type here would break
VM discovery in the interactive wizard.

cli/mtv_api_tests/common.py[473-485]
libs/providers/vmware.py[217-224]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
VM discovery may crash because the traversal spec uses an incorrect ContainerView type.
### Issue Context
`CreateContainerView(...)` returns a `vim.view.ContainerView`, and pyVmomi view classes are typically under `vim.view`.
### Fix Focus Areas
- Update traversal spec type to the correct pyVmomi class (likely `vim.view.ContainerView`):
- cli/mtv_api_tests/common.py[475-485]
- (Optional) Add a small unit/integration test or defensive fallback to a simpler iteration if PropertyCollector fails:
- cli/mtv_api_tests/common.py[464-510]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


15. run_command executes pytest 📘 Rule violation ⛨ Security
Description
The new CLI includes code paths that execute uv run pytest, which is explicitly forbidden in this
environment. This can trigger unsafe test execution against live infrastructure requiring
credentials.
Code

cli/mtv_api_tests/run.py[R169-206]

+def _run_local(
+    provider_key: str, storage_class: str, category: str, test_filter: str, ocp_provider: dict[str, Any] | None
+) -> None:
+    """Execute tests locally via uv run pytest.
+
+    Args:
+        provider_key: Source provider key from .providers.json.
+        storage_class: OCP storage class name.
+        category: Pytest marker name (e.g. 'copyoffload', 'tier0').
+        test_filter: Optional pytest -k filter.
+        ocp_provider: OpenShift provider config dict, or None.
+
+    Raises:
+        typer.Exit: With the pytest process return code.
+    """
+    cluster_args, cluster_env = _get_cluster_config(ocp_provider)
+
+    cmd = [
+        "uv",
+        "run",
+        "pytest",
+        "-v",
+        f"--tc=source_provider:{provider_key}",
+        f"--tc=storage_class:{storage_class}",
+        *cluster_args,
+    ]
+    if category != "all":
+        cmd.extend(["-m", category])
+    if test_filter:
+        cmd.extend(["-k", test_filter])
+
+    console.print(f"\n[bold]Running:[/bold] {shlex.join(cmd)}\n")
+
+    # Merge cluster credentials into subprocess environment
+    env = {**os.environ, **cluster_env} if cluster_env else None
+    result = subprocess.run(cmd, env=env, check=False)
+    raise typer.Exit(code=result.returncode)
+
Evidence
PR Compliance ID 23 forbids adding scripts/automation that runs tests via pytest/uv run pytest.
The new local execution path builds and runs a subprocess command containing uv run pytest, and
the generated Job manifest also embeds uv run pytest in its container command.

CLAUDE.md
cli/mtv_api_tests/run.py[169-206]
cli/mtv_api_tests/common.py[927-938]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new CLI directly runs tests using `uv run pytest` (locally and via generated Job manifests), which is forbidden by compliance rule PR Compliance ID 23 for this environment.
## Issue Context
- Local execution path uses `subprocess.run([... &amp;quot;uv&amp;quot;, &amp;quot;run&amp;quot;, &amp;quot;pytest&amp;quot;, ...])`.
- Generated OpenShift Job manifest embeds `uv run pytest ...` in the container command.
## Fix Focus Areas
- cli/mtv_api_tests/run.py[169-206]
- cli/mtv_api_tests/common.py[927-938]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


16. Wrong container group permissions 🐞 Bug ⛯ Reliability
Description
The Dockerfile changes ownership to GID 0 but then forces USER 1001 (typically GID 1001), so
/app may not be writable and uv sync / runtime outputs can fail under OpenShift or locally.
Code

Dockerfile[R46-49]

+RUN chgrp -R 0 ${APP_DIR} && chmod -R g=u ${APP_DIR}
+
+USER 1001
Evidence
The image sets /app group ownership to 0 and copies files before dropping privileges, implying it
expects the runtime user to be in group 0 for write access. However it switches to USER 1001
without setting the group to 0, so the chgrp -R 0 ... && chmod -R g=u ... permission pattern may
not apply to the running user, causing write failures in ${APP_DIR} (e.g., uv creating
environments/caches or pytest writing outputs).

Dockerfile[1-51]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The Dockerfile sets group ownership of `/app` to GID 0 and enables group write permissions, but then runs as `USER 1001` without GID 0. This can make `/app` non-writable, breaking `uv sync` and test execution.
## Issue Context
This is a common OpenShift pattern: make content group-owned by 0 and run with an arbitrary UID that is in group 0. The current Dockerfile only does the first half.
## Fix Focus Areas
- Dockerfile[46-49]
## Suggested change
- Change `USER 1001` to `USER 1001:0` (or otherwise ensure the runtime user’s primary group is 0 / includes group 0).
- If you must keep GID 1001, then chown/chgrp `/app` accordingly or set appropriate ACLs so the runtime user can write where needed.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


17. Credentials leaked in Job args 🐞 Bug ⛨ Security
Description
The generated OpenShift Job passes CLUSTER_USERNAME and CLUSTER_PASSWORD as pytest --tc= CLI
arguments, which exposes credentials via oc describe, logs, and /proc/*/cmdline inside the
container.
Code

cli/mtv_api_tests/common.py[R931-938]

+            uv run pytest {marker_flag}{filter_flag}\\
+              -v \\
+              --tc=source_provider:{provider_key} \\
+              --tc=storage_class:{storage_class} \\
+              ${{CLUSTER_HOST:+--tc=cluster_host:${{CLUSTER_HOST}}}} \\
+              ${{CLUSTER_USERNAME:+--tc=cluster_username:${{CLUSTER_USERNAME}}}} \\
+              ${{CLUSTER_PASSWORD:+--tc=cluster_password:${{CLUSTER_PASSWORD}}}} \\
+              $([ "${{CLUSTER_VERIFY_SSL}}" = "true" ] && echo "--tc=insecure_verify_skip:false")
Evidence
generate_job_yaml() sets CLUSTER_USERNAME/CLUSTER_PASSWORD as env vars from the Secret, but then
redundantly appends them into the command line via --tc=cluster_username:... and
--tc=cluster_password:.... This directly defeats the later change in utilities/utils.py that was
added specifically to allow passing sensitive values via environment variables rather than process
args.

cli/mtv_api_tests/common.py[906-938]
utilities/utils.py[601-620]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The Job manifest generated by `cli/mtv_api_tests/common.py::generate_job_yaml()` leaks cluster credentials by embedding them in the pytest command line using `--tc=cluster_username:...` and `--tc=cluster_password:...`. These values are visible via `oc describe`, container logs, and `/proc/.../cmdline`.
### Issue Context
The Job already injects the same values as environment variables, and `utilities/utils.py::get_cluster_client()` now supports reading `CLUSTER_USERNAME`/`CLUSTER_PASSWORD` from environment when `--tc` is not provided.
### Fix Focus Areas
- cli/mtv_api_tests/common.py[906-938]
- utilities/utils.py[601-620]
### What to change
- In the generated Job command, remove the `--tc=cluster_username:${CLUSTER_USERNAME}` and `--tc=cluster_password:${CLUSTER_PASSWORD}` expansions.
- Keep `--tc=cluster_host:...` and the `insecure_verify_skip` override as needed.
- Ensure the test harness reads username/password via env vars (already implemented in `get_cluster_client()`).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


18. get_cluster_client() passes None 📘 Rule violation ✓ Correctness
Description
The updated cluster-client construction can pass None for username/password into
get_client(), allowing invalid None values to propagate into later logic. This makes failures
occur deeper in the stack and harder to diagnose, instead of failing fast with a clear error near
the missing value source.
Code

utilities/utils.py[R616-620]

+    username = get_value_from_py_config("cluster_username") or os.environ.get("CLUSTER_USERNAME")
+    password = get_value_from_py_config("cluster_password") or os.environ.get("CLUSTER_PASSWORD")
insecure_verify_skip = get_value_from_py_config("insecure_verify_skip")
_client = get_client(host=host, username=username, password=password, verify_ssl=not insecure_verify_skip)
Evidence
PR Compliance ID 7 requires explicit None checks and a clear exception when None is not valid;
the new code explicitly allows username/password to become None (if both testconfig and env
are missing) and still passes them into get_client().

CLAUDE.md
utilities/utils.py[616-620]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`get_cluster_client()` may pass `None` as `username`/`password` into `get_client()`, which violates the fail-fast requirement and can cause later, harder-to-debug errors.
## Issue Context
The function now sources credentials from pytest-testconfig first and then environment variables; if neither source provides credentials, `username`/`password` can be `None`.
## Fix Focus Areas
- utilities/utils.py[616-620]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


19. main() docstring has Returns 📘 Rule violation ⚙ Maintainability
Description
New functions returning None include a Returns: section (e.g., Returns: None), which violates
the docstring standard requiring Returns only for meaningful non-None return values. This reduces
docstring signal and creates inconsistency across the codebase.
Code

cli/mtv_api_tests/init.py[R27-35]

+def main(ctx: typer.Context) -> None:
+    """MTV test setup and execution tool.
+
+    Args:
+        ctx: Typer context with invoked subcommand info.
+
+    Returns:
+        None
+    """
Evidence
PR Compliance ID 16 requires docstrings to include sections only when applicable, explicitly
disallowing Returns for void/None-returning functions. The new main() callback docstring
includes a Returns: None section despite returning None.

CLAUDE.md
cli/mtv_api_tests/init.py[27-35]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Docstrings for new `None`-returning functions include an inappropriate `Returns:` section (e.g., `Returns: None`). The docstring standard requires omitting `Returns` when the function does not return a meaningful value.
## Issue Context
This occurs in newly added CLI functions (Typer callback/commands). Similar patterns likely exist in other new `-&amp;amp;amp;amp;amp;amp;gt; None` functions added in this PR and should be updated for consistency.
## Fix Focus Areas
- cli/mtv_api_tests/__init__.py[27-35]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


20. SSL verify default off 🐞 Bug ⛨ Security
Description
The wizard defaults SSL verification to disabled, causing vSphere/OpenShift connections to skip
certificate validation unless the user explicitly opts in. This is insecure-by-default and exposes
credentials to MITM risk during discovery and configuration.
Code

cli/mtv_api_tests/common.py[R280-286]

+    verify_env = os.environ.get(f"{env_prefix}_VERIFY_SSL", "").strip().lower()
+    if verify_env:
+        verify_ssl = verify_env in ("true", "1", "yes")
+        console.print(f"  [dim]SSL verification:[/dim] {verify_ssl} [dim](from {env_prefix}_VERIFY_SSL)[/dim]")
+    else:
+        verify_ssl = Confirm.ask("  Verify SSL certificates?", default=False)
+
Evidence
SSL verification is prompted with a default of False, and the connection helpers act on that by
disabling certificate validation for vSphere and suppressing insecure warnings for OpenShift when
verification is off.

cli/mtv_api_tests/common.py[268-287]
cli/mtv_api_tests/common.py[372-399]
cli/mtv_api_tests/common.py[537-568]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The CLI defaults SSL verification to disabled (`default=False`), which results in connections that skip certificate validation unless users explicitly opt in.
## Issue Context
- `_get_ssl_config()` is used for both vSphere and OpenShift credential flows.
- `connect_vsphere()` disables SSL cert validation when `verify_ssl` is false.
- `connect_ocp()` disables urllib3 insecure warnings when `verify_ssl` is false.
## Fix Focus Areas
- cli/mtv_api_tests/common.py[268-302]
- cli/mtv_api_tests/common.py[372-399]
- cli/mtv_api_tests/common.py[537-568]
## Implementation notes
- Change the interactive prompt default to `True` (secure-by-default) and allow explicit opt-out.
- Keep the env var override behavior, but consider validating/normalizing values and clearly printing when verification is disabled.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


21. Password exposed in argv 🐞 Bug ⛨ Security
Description
Local mode passes the OpenShift password in the pytest command line via --tc=cluster_password:,
which is readable by other users via process listings. This leaks cluster credentials on shared
machines for the duration of the test run.
Code

cli/mtv_api_tests/run.py[R121-135]

+    if ocp_provider:
+        host = ocp_provider.get("host")
+        username = ocp_provider.get("username")
+        password = ocp_provider.get("password")
+        if not all([host, username, password]):
+            console.print("[yellow]OCP provider missing host/username/password, falling back to token/prompt[/yellow]")
+        else:
+            console.print(f"  Using cluster credentials from OCP provider (v{ocp_provider.get('version', '?')})")
+            # NOTE: cluster_password is visible in /proc/cmdline. This is a known
+            # limitation of pytest-testconfig --tc= args. Job mode uses Secrets instead.
+            args = [
+                f"--tc=cluster_host:{host}",
+                f"--tc=cluster_username:{username}",
+                f"--tc=cluster_password:{password}",
+            ]
Evidence
The CLI constructs a --tc=cluster_password:... argument and passes it directly to
subprocess.run(cmd), meaning the password is part of the process argv.

cli/mtv_api_tests/run.py[121-135]
cli/mtv_api_tests/run.py[182-199]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The local runner leaks `cluster_password` via process argv by passing it as a `--tc=` argument.
## Issue Context
- This is explicitly acknowledged in code comments, but the behavior still exposes real credentials.
- Job mode avoids this by using a Kubernetes Secret.
## Fix Focus Areas
- cli/mtv_api_tests/run.py[109-199]
- utilities/utils.py[601-615]
## Implementation notes
- Prefer passing credentials via environment variables to the subprocess, and update `get_cluster_client()` to read `CLUSTER_HOST/CLUSTER_USERNAME/CLUSTER_PASSWORD` env vars when `py_config` values are missing.
- If `pytest-testconfig` must be used, consider a file-based config mechanism (if supported) with `0600` permissions instead of argv.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


22. Empty Secret value becomes null 🐞 Bug ✓ Correctness
Description
generate_job_yaml() always emits cluster_ca_bundle: {b64(ca_bundle)}; when ca_bundle is empty,
b64('') becomes an empty scalar and YAML parses cluster_ca_bundle: as null. Kubernetes Secret
data values are expected to be strings (base64), so this can make the generated manifest invalid
or apply inconsistently.
Code

cli/mtv_api_tests/common.py[R853-878]

+    verify_ssl = ocp_creds.get("verify_ssl", "false")
+    ca_bundle = ocp_creds.get("ca_bundle", "")
+    yaml_content = f"""apiVersion: v1
+kind: Namespace
+metadata:
+  name: {namespace}
+
+---
+
+apiVersion: v1
+kind: Secret
+metadata:
+  name: {secret_name}
+  namespace: {namespace}
+type: Opaque
+data:
+  providers.json: {b64(providers_json_content)}
+  cluster_host: {b64(ocp_creds["host"])}
+  cluster_username: {b64(ocp_creds["username"])}
+  cluster_password: {b64(ocp_creds["password"])}
+  cluster_verify_ssl: {b64(verify_ssl)}
+  # TODO: ca_bundle is stored here but not yet consumed by the test runner container.
+  # When the test runner supports CA bundle injection, mount this as a file and set
+  # KUBERNETES_CLIENT_CA_BUNDLE or pass via --tc flag.
+  cluster_ca_bundle: {b64(ca_bundle)}
+
Evidence
ca_bundle defaults to empty when not provided, and _get_ssl_config() explicitly allows skipping
a CA bundle even when SSL verification is enabled. Since b64() returns an empty string for empty
input, the rendered YAML line becomes cluster_ca_bundle: with no value, which YAML treats as null
rather than an empty string.

cli/mtv_api_tests/common.py[268-302]
cli/mtv_api_tests/common.py[806-816]
cli/mtv_api_tests/common.py[847-878]

Agent prompt

@rgolangh rgolangh changed the title Add test configuration and execution tool feat: Add test configuration and execution tool Mar 9, 2026
Comment thread cli/mtv_api_tests/common.py Resolved
Comment thread tools/mtv-api-tests.py Resolved
Comment thread tools/mtv-api-tests.py Resolved
Comment thread tools/mtv-api-tests.py Resolved
Comment thread tools/mtv-api-tests.py Resolved
Comment thread tools/mtv-api-tests.py Resolved
Comment thread cli/mtv_api_tests/common.py Resolved
@qodo-code-review

qodo-code-review Bot commented Mar 9, 2026 •

Copy link
Copy Markdown

Persistent review updated to latest commit ec88acd

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Dockerfile`:
- Line 34: The Dockerfile currently uses a blanket COPY . . which bakes
generated secrets (e.g., .providers.json and job.yaml created by
tools/mtv-api-tests.py) into the image; replace this with explicit COPY
instructions for only the files needed by the image (reference the COPY . . line
in the Dockerfile) and add a .dockerignore that excludes generated artifacts
such as .providers.json, job.yaml, and any other test-generated outputs to
prevent secrets from entering the build context or final image.

In `@README.md`:
- Around line 448-483: The README's "Interactive Setup Tool" docs do not warn
that the generated job.yaml contains a Kubernetes Secret with provider and
cluster credentials; update the section describing job.yaml (referencing
tools/mtv-api-tests.py generate, job.yaml, and .providers.json) to explicitly
state that job.yaml embeds secrets, advise not to commit it to version control,
recommend restricting file permissions and deleting it after use, and mirror the
same cautionary language currently used for .providers.json so users understand
the sensitive nature of the file.

In `@tools/mtv-api-tests.py`:
- Around line 632-633: The generated secret files are written with
Path.write_text which inherits the process umask; after writing
PROVIDERS_JSON_PATH (and the equivalent job.yaml path), immediately set
restrictive permissions with a chmod to 0o600 so only the owner can read/write;
update the code around the PROVIDERS_JSON_PATH.write_text call to follow it with
PROVIDERS_JSON_PATH.chmod(0o600) and do the same for the job.yaml path (use its
variable name in the diff) to ensure secrets are not world-readable.
- Around line 679-680: The code constructs filter_flag by interpolating
user-controlled test_filter directly into a shell command; update the
construction to shell-quote the value (use shlex.quote(test_filter)) before
embedding so single quotes and metacharacters are escaped, e.g. replace f"-k
'{test_filter}' " with f"-k {shlex.quote(test_filter)} " and add the necessary
import for shlex; apply the same change to the other occurrence around lines
742-748 that builds a similar filter_flag.
- Around line 968-969: The generate step collects OCP credentials via
_gather_ocp_storage_class() but run --mode local discards ocp_creds and only
passes source_provider/storage_class, so local runs fail if the user provided
kubeconfig/host+user+pass interactively; update the flow so that generate
persists the OCP auth inputs (kubeconfig path or host/username/password and/or
SA token) into the produced artifacts (e.g., extend .providers.json or job.yaml
with an ocp_creds block) and change the run (--mode local) path to load and
reuse that ocp_creds when launching pytest, or alternatively make run --mode
local re-prompt for missing cluster credentials by invoking the same
_gather_ocp_storage_class() logic before starting tests; target references:
_gather_ocp_storage_class(), generate step that writes .providers.json/job.yaml,
and the run-local path that launches pytest.
- Around line 1047-1052: The code calls generate() when the requested job_yaml
path is missing but generate() always writes the default "job.yaml", causing
_run_job(Path(job_yaml)) to still fail for a custom --job-yaml; modify the flow
so that when mode == RunMode.job and the provided job_yaml path does not exist
you either call generate(target_path=job_yaml) (i.e. make generate accept an
output path and write the generated YAML to that custom path) or after calling
generate() update job_yaml to the generated default path (e.g., set job_yaml =
"job.yaml") before calling _run_job(Path(job_yaml)), ensuring _run_job is
invoked against the file that was actually created; update the generate()
signature/usage and any callers accordingly or adjust the post-generate job_yaml
reassignment.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 7a81e1a7-8dae-4445-bc54-5962dd7189f3

📥 Commits

Reviewing files that changed from the base of the PR and between 4e0f502 and 8faaa79.

📒 Files selected for processing (5)
  • Dockerfile
  • Makefile
  • README.md
  • docs/copyoffload/how-to-run-copyoffload-tests.md
  • tools/mtv-api-tests.py

Comment thread Dockerfile Resolved
Comment thread cli/mtv_api_tests/common.py Resolved
Comment thread cli/mtv_api_tests/common.py Resolved
Comment thread tools/mtv-api-tests.py Resolved
Comment thread tools/mtv-api-tests.py Resolved
Comment thread cli/mtv_api_tests/common.py Resolved
Comment thread cli/mtv_api_tests/common.py Resolved
Comment thread tools/mtv-api-tests.py Resolved
@qodo-code-review

qodo-code-review Bot commented Mar 9, 2026 •

Copy link
Copy Markdown

Persistent review updated to latest commit ec88acd

Comment thread tools/mtv-api-tests.py Resolved
Comment thread cli/mtv_api_tests/common.py Resolved
Comment thread tools/mtv-api-tests.py Resolved
Comment thread Dockerfile Resolved

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 6

♻️ Duplicate comments (5)
Dockerfile (1)

34-34: ⚠️ Potential issue | 🔴 Critical

CRITICAL: COPY . . will bake generated secrets into the image.

With this blanket copy, anyone who runs make generate before make build-image ships .providers.json and job.yaml (which contain credentials) into the build context and final image. Add a .dockerignore to exclude these files.

📝 Create `.dockerignore`
.providers.json
job.yaml
.env
.git/
.cache/
output/
__pycache__/
*.pyc
.data-collector/
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Dockerfile` at line 34, The Dockerfile uses a blanket COPY . . which bakes
generated secrets (like .providers.json and job.yaml) into the image; create a
.dockerignore at repo root to exclude those files and other build artifacts so
they never enter the build context (exclude .providers.json, job.yaml, .env,
.git/, .cache/, output/, __pycache__/, *.pyc, .data-collector/ as suggested) and
then keep the existing COPY . . in Dockerfile unchanged so only safe files are
copied during make build-image after make generate.
tools/mtv-api-tests.py (3)

675-677: ⚠️ Potential issue | 🟠 Major

HIGH: Shell-quote test_filter before embedding into bash command.

test_filter is user input spliced directly into a shell script that runs inside the Job. A filter containing ' breaks the command; shell metacharacters can escape the pytest invocation entirely.

🔧 Proposed fix
+import shlex
...
-    filter_flag = f"-k '{test_filter}' " if test_filter else ""
+    filter_flag = f"-k {shlex.quote(test_filter)} " if test_filter else ""
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/mtv-api-tests.py` around lines 675 - 677, The filter_flag currently
interpolates raw user input test_filter into a shell command; to prevent
injection and broken quoting, import and use shlex.quote to shell-escape
test_filter and build filter_flag from the escaped value (e.g. replace the
current filter_flag = f"-k '{test_filter}' " with a version that uses
shlex.quote(test_filter)); update places that construct or pass filter_flag
accordingly so the command string always contains the safely quoted filter.

631-633: ⚠️ Potential issue | 🟠 Major

HIGH: Write generated secret files with 0600 permissions.

Both .providers.json and job.yaml contain live vSphere/OpenShift credentials. Path.write_text() inherits the process umask, so on a common 022 shell these files become world-readable. Lock them down immediately after writing.

🔒 Proposed fix
     PROVIDERS_JSON_PATH.write_text(json.dumps(existing, indent=2) + "\n")
+    PROVIDERS_JSON_PATH.chmod(0o600)
     console.print(f"  [green]Wrote {PROVIDERS_JSON_PATH}[/green]")

Also applies to line 1016-1017 for JOB_YAML_PATH.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/mtv-api-tests.py` around lines 631 - 633, After writing the sensitive
files, immediately tighten their permissions instead of relying on the process
umask: after the existing PROVIDERS_JSON_PATH.write_text(...) call, add
PROVIDERS_JSON_PATH.chmod(0o600) to set owner read/write only, and do the same
after JOB_YAML_PATH.write_text(...) (add JOB_YAML_PATH.chmod(0o600)). This
ensures .providers.json and job.yaml are locked down to mode 0600 regardless of
the umask.

1045-1050: ⚠️ Potential issue | 🟠 Major

HIGH: Auto-generate fallback is wrong for a custom --job-yaml path.

run --mode job --job-yaml custom.yaml calls generate() when the file is missing, but generate() always writes to the default job.yaml. The command then tries to apply custom.yaml which still doesn't exist.

🔧 Proposed fix
     if mode == RunMode.job:
-        if not Path(job_yaml).exists():
-            console.print("[yellow]job.yaml not found. Starting configuration wizard...[/yellow]\n")
-            generate()
-        _run_job(Path(job_yaml))
+        job_yaml_path = Path(job_yaml)
+        if not job_yaml_path.exists():
+            if job_yaml_path != JOB_YAML_PATH:
+                console.print(f"[red]Error: {job_yaml_path} not found.[/red]")
+                raise typer.Exit(code=1)
+            console.print("[yellow]job.yaml not found. Starting configuration wizard...[/yellow]\n")
+            generate()
+        _run_job(job_yaml_path)
         return
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/mtv-api-tests.py` around lines 1045 - 1050, The code calls generate()
when a custom job_yaml is missing but generate() always writes the default
"job.yaml", so the requested custom file remains absent; change the logic to
either error or make generate write to the requested path: update generate() to
accept a target path/output filename (e.g., generate(target_path: Path)) and in
the RunMode.job branch replace generate() with generate(Path(job_yaml)) (or, if
you prefer failing behavior, raise a clear error when job_yaml != "job.yaml" and
the file is missing); ensure _run_job(Path(job_yaml)) then runs against the
newly created file.
README.md (1)

448-469: ⚠️ Potential issue | 🟠 Major

HIGH: Add security warning that job.yaml contains embedded secrets.

The section introduces job.yaml as part of the workflow but doesn't warn users that it embeds a Kubernetes Secret with base64-encoded provider data and cluster credentials. It should receive the same "don't commit / restrict permissions / delete when done" guidance as .providers.json.

📝 Suggested addition after line 468
 6. Write `.providers.json` and `job.yaml`

+> ⚠️ **Security note**: `job.yaml` contains a Kubernetes `Secret` with base64-encoded
+> provider configuration and cluster credentials. Treat it like `.providers.json`:
+> do not commit it to version control, restrict file permissions (`chmod 600 job.yaml`),
+> and delete it when you are done.

As per coding guidelines: README.md: Update README.md when code changes affect usage, requirements, installation, or configuration.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` around lines 448 - 469, Add a security warning after the
interactive wizard description noting that job.yaml (generated by
tools/mtv-api-tests.py) contains an embedded Kubernetes Secret with
base64-encoded provider data and cluster credentials; instruct users not to
commit job.yaml (or .providers.json) to VCS, to restrict filesystem and
repository access, to delete the files when no longer needed, and to treat them
the same way as .providers.json regarding permissions and rotation of
credentials.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Dockerfile`:
- Line 30: The RUN pip install uv line installs an unpinned package; update the
Dockerfile to pin the uv package to a tested version (e.g., replace the bare
"uv" with "uv==<your-tested-version>") and add the --no-cache-dir flag to the
pip install invocation to avoid caching the wheel and keep the image small;
modify the RUN pip install uv invocation accordingly (use the chosen version
number in place of <your-tested-version>).

In `@tools/mtv-api-tests.py`:
- Around line 79-84: Replace the current RunMode definition that inherits from
(str, Enum) with Python 3.11+'s StrEnum: import StrEnum from enum and change the
class signature to inherit from StrEnum (class RunMode(StrEnum):) while keeping
the same members local = "local" and job = "job"; update any type references if
necessary to reflect StrEnum usage and ensure the file imports enum.StrEnum
instead of Enum.
- Around line 374-376: Update the exception handling around the get_client()
call: instead of raising a new ConnectionError without chaining, re-raise it
using exception chaining (raise ConnectionError(...) from exc) so the original
exception is preserved; locate the except Exception as exc block that currently
raises ConnectionError(f"Failed to connect to OpenShift: {exc}") and change it
to chain the original exception (using the same message) to aid debugging while
keeping the bare Exception catch.
- Around line 222-228: The except handlers around the SmartConnect call should
chain the original exceptions into the new ConnectionError so the traceback is
preserved; update the vim.fault.InvalidLogin and generic except Exception as exc
branches to re-raise ConnectionError using "raise ConnectionError(... ) from
exc" (use the exc variable in both places) so callers see the causal chain from
the original SmartConnect/vim.fault.InvalidLogin failure to the ConnectionError.
- Around line 1119-1134: Validate the user-controlled values provider_key and
storage_class before building cmd and calling subprocess.run to prevent
malformed or unsafe values; in the function or block that constructs cmd
(referencing variables provider_key, storage_class, cmd and the subprocess.run
invocation), add a small sanitizer/validator that rejects or normalizes values
containing unexpected characters (e.g., whitespace, shell metacharacters, path
separators) or check them against an allowlist of expected provider/storage
names, and raise a clear error or prompt the user if validation fails before
proceeding to run pytest.
- Around line 778-781: When handling ConnectionError from connect_vsphere,
preserve the original exception context before exiting: call
console.print_exception() (or log the traceback) to emit the full traceback,
then raise typer.Exit(...) using exception chaining (raise typer.Exit(code=1)
from exc) so the underlying ConnectionError is linked; apply the same change to
the other identical handler around lines 898-901. Ensure you reference the
connect_vsphere caller, the console.print_exception()/console.print usage, and
the typer.Exit raise so the traceback is preserved for diagnostics.

---

Duplicate comments:
In `@Dockerfile`:
- Line 34: The Dockerfile uses a blanket COPY . . which bakes generated secrets
(like .providers.json and job.yaml) into the image; create a .dockerignore at
repo root to exclude those files and other build artifacts so they never enter
the build context (exclude .providers.json, job.yaml, .env, .git/, .cache/,
output/, __pycache__/, *.pyc, .data-collector/ as suggested) and then keep the
existing COPY . . in Dockerfile unchanged so only safe files are copied during
make build-image after make generate.

In `@README.md`:
- Around line 448-469: Add a security warning after the interactive wizard
description noting that job.yaml (generated by tools/mtv-api-tests.py) contains
an embedded Kubernetes Secret with base64-encoded provider data and cluster
credentials; instruct users not to commit job.yaml (or .providers.json) to VCS,
to restrict filesystem and repository access, to delete the files when no longer
needed, and to treat them the same way as .providers.json regarding permissions
and rotation of credentials.

In `@tools/mtv-api-tests.py`:
- Around line 675-677: The filter_flag currently interpolates raw user input
test_filter into a shell command; to prevent injection and broken quoting,
import and use shlex.quote to shell-escape test_filter and build filter_flag
from the escaped value (e.g. replace the current filter_flag = f"-k
'{test_filter}' " with a version that uses shlex.quote(test_filter)); update
places that construct or pass filter_flag accordingly so the command string
always contains the safely quoted filter.
- Around line 631-633: After writing the sensitive files, immediately tighten
their permissions instead of relying on the process umask: after the existing
PROVIDERS_JSON_PATH.write_text(...) call, add PROVIDERS_JSON_PATH.chmod(0o600)
to set owner read/write only, and do the same after
JOB_YAML_PATH.write_text(...) (add JOB_YAML_PATH.chmod(0o600)). This ensures
.providers.json and job.yaml are locked down to mode 0600 regardless of the
umask.
- Around line 1045-1050: The code calls generate() when a custom job_yaml is
missing but generate() always writes the default "job.yaml", so the requested
custom file remains absent; change the logic to either error or make generate
write to the requested path: update generate() to accept a target path/output
filename (e.g., generate(target_path: Path)) and in the RunMode.job branch
replace generate() with generate(Path(job_yaml)) (or, if you prefer failing
behavior, raise a clear error when job_yaml != "job.yaml" and the file is
missing); ensure _run_job(Path(job_yaml)) then runs against the newly created
file.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8cd02302-f909-4b79-a2c8-6315e9083111

📥 Commits

Reviewing files that changed from the base of the PR and between 8faaa79 and ef268e7.

📒 Files selected for processing (5)
  • Dockerfile
  • Makefile
  • README.md
  • docs/copyoffload/how-to-run-copyoffload-tests.md
  • tools/mtv-api-tests.py

Comment thread Dockerfile Resolved
Comment thread tools/mtv-api-tests.py Resolved
Comment thread cli/mtv_api_tests/common.py Resolved
Comment thread cli/mtv_api_tests/common.py Resolved
Comment thread tools/mtv-api-tests.py Resolved
Comment thread tools/mtv-api-tests.py Resolved

@myakove myakove left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Please split mtv-api-tests.py into smaller logical files

Comment thread tools/mtv-api-tests.py Resolved
Comment thread cli/mtv_api_tests/common.py Resolved
Comment thread cli/mtv_api_tests/common.py Resolved
Comment thread cli/mtv_api_tests/common.py Resolved
Comment thread cli/mtv_api_tests/common.py Resolved
Comment thread cli/mtv_api_tests/common.py Resolved
Comment thread tools/mtv-api-tests.py Resolved
Comment thread tools/mtv-api-tests.py Resolved
Comment thread tools/mtv-api-tests.py Resolved
@rgolangh

rgolangh commented Mar 9, 2026

Copy link
Copy Markdown
Contributor Author

Please split mtv-api-tests.py into smaller logical files

give me guidance please

@myakove

myakove commented Mar 9, 2026

Copy link
Copy Markdown
Collaborator

you can use it once merged #323

@rgolangh
rgolangh force-pushed the helper-to-create-manifests branch from ef268e7 to de0b7de Compare March 9, 2026 18:33
@rgolangh

Copy link
Copy Markdown
Contributor Author

@coderabbitai[bot]

The following review comments were reviewed and a decision was made:

utilities/utils.py:623 (outside diff) — CRITICAL: Remove the isinstance() runtime check against DynamicClient — it directly violates c

Addressed: Fixed: Replaced isinstance(_client, DynamicClient) with client is None check per coding guidelines.

@coderabbitai

coderabbitai Bot commented Mar 25, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

@rgolangh: Thanks for confirming! Replacing isinstance(_client, DynamicClient) with client is None is the correct fix — DynamicClient must never be used in runtime isinstance() checks per the repository's coding guidelines (it's only valid as a TYPE_CHECKING-gated annotation or string annotation). The client is None identity check is the proper guard here.


🧠 Learnings used
Learnt from: rgolangh
Repo: RedHatQE/mtv-api-tests PR: 321
File: cli/mtv_api_tests/run.py:0-0
Timestamp: 2026-03-23T14:28:32.199Z
Learning: In repository RedHatQE/mtv-api-tests (cli/mtv_api_tests/run.py): `generate` and `run` are intentionally separate CLI subcommands. `run --mode job` expects the Job YAML manifest to already exist (created by a prior `generate` invocation). Do not suggest or add auto-generation fallbacks inside the `run` command's job mode path.

Learnt from: shellymiron
Repo: RedHatQE/mtv-api-tests PR: 239
File: README.md:270-331
Timestamp: 2026-01-12T16:24:42.450Z
Learning: Repository RedHatQE/mtv-api-tests: The OpenShift Job templates in README.md and docs/how-to-run-copyoffload-tests.md are intentionally duplicated with identical sections because the files are meant to be different but share common Job template structure; do not suggest consolidating them into a single YAML manifest.

Learnt from: shellymiron
Repo: RedHatQE/mtv-api-tests PR: 239
File: README.md:122-134
Timestamp: 2026-01-13T09:53:56.701Z
Learning: Repository RedHatQE/mtv-api-tests: The JSON examples in README.md intentionally contain inline `# pragma: allowlist secret` comments (which make them invalid JSON) because pre-commit hooks require these pragmas to pass secret scanning. Lines 114-116 prominently warn users not to copy these comments to their actual `.providers.json` files. This is a deliberate design decision to balance pre-commit requirements with documentation needs. Do not suggest removing these pragmas or flag them as invalid JSON.

Learnt from: rgolangh
Repo: RedHatQE/mtv-api-tests PR: 321
File: cli/mtv_api_tests/common.py:352-355
Timestamp: 2026-03-25T12:40:23.422Z
Learning: In RedHatQE/mtv-api-tests (cli/mtv_api_tests/common.py): `get_ocp_credentials()` intentionally returns only `{"kubeconfig": kubeconfig}` when `KUBECONFIG` is set, without merging `CLUSTER_HOST`, `CLUSTER_USERNAME`, `CLUSTER_PASSWORD`, `CLUSTER_VERIFY_SSL`, or `CLUSTER_CA_BUNDLE`. This is by design — callers that need explicit credentials (e.g., `generate.py:_gather_ocp_storage_class`) are responsible for detecting and prompting for the missing fields. Do NOT flag this early-return pattern as discarding auth/TLS settings.

Learnt from: myakove
Repo: RedHatQE/mtv-api-tests PR: 328
File: tests/copyoffload/conftest.py:240-307
Timestamp: 2026-03-11T09:45:35.130Z
Learning: In repository RedHatQE/mtv-api-tests (tests/copyoffload/conftest.py): The `copyoffload_ssh_key` fixture intentionally uses `scope="session"` even under pytest-xdist (--dist=loadscope). `install_ssh_key_on_esxi` and `remove_ssh_key_from_esxi` in utilities/esxi.py are idempotent — each checks whether the operation is already done before proceeding — so concurrent worker setup/teardown is safe. Do NOT flag session scope on this fixture as a concurrency hazard or suggest refactoring to worker-unique keys.

Learnt from: elsapassaro
Repo: RedHatQE/mtv-api-tests PR: 285
File: tests/tests_config/config.py:373-388
Timestamp: 2026-01-30T14:43:00.204Z
Learning: In repository RedHatQE/mtv-api-tests, for test configurations in tests/tests_config/config.py: VM entries within a test should use the same base name (e.g., "xcopy-template-test") and let the framework auto-generate unique names with UUIDs when cloning is enabled. Explicit `clone_name` fields should only be used for tests that specifically verify operator behavior with non-conforming or special VM names (e.g., test_copyoffload_nonconforming_name_migration).

Learnt from: krcmarik
Repo: RedHatQE/mtv-api-tests PR: 314
File: utilities/vmware_guest_operations.py:103-115
Timestamp: 2026-03-12T00:10:06.804Z
Learning: In RedHatQE/mtv-api-tests (utilities/vmware_guest_operations.py): In `run_command_in_vmware_guest`, NOT fetching the guest temp-file output when `process_info.exitCode != 0` is intentional. The exit code and VM name in `GuestCommandError` are considered sufficient for debugging. Fetching the failed command's output would add an extra Guest Operations API call on an already-failed, rare code path, which the author considers unnecessary complexity. Do not flag this as a deficiency.

Learnt from: MiriSafra
Repo: RedHatQE/mtv-api-tests PR: 0
File: :0-0
Timestamp: 2026-03-04T15:47:50.626Z
Learning: In repository RedHatQE/mtv-api-tests: In shared-disk migration tests (utilities/shared_disk.py), the unmount/remount sequence in verify_shared_disk_data is for cache flushing purposes (forcing buffer flush between writes), NOT to avoid concurrent filesystem access. Shared disks use RWX PVCs and are designed for multi-writer access; SCSI-level coordination handles concurrent mount safety. Do not flag this as a concurrency bug.

Learnt from: prabinovRedhat
Repo: RedHatQE/mtv-api-tests PR: 358
File: utilities/copyoffload_migration.py:280-281
Timestamp: 2026-03-22T21:14:33.207Z
Learning: In repository RedHatQE/mtv-api-tests (utilities/copyoffload_migration.py): When Kubernetes Pod objects are fetched via `Pod.get(label_selector=...)`, `pod.instance.metadata.labels` is guaranteed to be non-null because the label selector query itself proves the labels map exists and is populated. Do NOT flag missing defensive guards (e.g., `pod.instance.metadata.labels or {}`) on pods retrieved this way — it is unnecessary noise. This follows the same fail-fast philosophy used for other guaranteed Kubernetes structural fields in the repo.

Learnt from: MiriSafra
Repo: RedHatQE/mtv-api-tests PR: 0
File: :0-0
Timestamp: 2026-03-04T16:25:43.654Z
Learning: In repository RedHatQE/mtv-api-tests: In utilities/shared_disk.py verify_shared_disk_data, VM1's shared disk is intentionally left mounted while VM2 also mounts it. This concurrent mount window is the feature under test (RWX PVC multi-writer access for MTV-454). Do NOT add try/finally around VM1's SSH session to force unmount — that would serialize the access and eliminate the concurrent-access test coverage entirely.

Learnt from: MiriSafra
Repo: RedHatQE/mtv-api-tests PR: 0
File: :0-0
Timestamp: 2026-03-04T16:25:43.654Z
Learning: In repository RedHatQE/mtv-api-tests: In libs/providers/vmware.py relink_shared_disks(), defensive vm.config guards (e.g., `if vm.config is None`) are intentionally absent. Unlike vm_dict() which queries arbitrary external VMs in unknown states, relink_shared_disks() operates exclusively on VMs freshly cloned by the test fixture. Missing vm.config on a just-cloned VM indicates catastrophic infrastructure failure and should raise AttributeError loudly (fail-fast), not be silently swallowed.

Learnt from: prabinovRedhat
Repo: RedHatQE/mtv-api-tests PR: 317
File: utilities/copyoffload_migration.py:124-129
Timestamp: 2026-03-08T12:36:28.550Z
Learning: In repository RedHatQE/mtv-api-tests (utilities/copyoffload_migration.py): The `_get_ip()` helper inside `wait_for_cloud_init` intentionally takes the first IP address returned by `source_provider.vm_dict()` without filtering for routability. In the project's vSphere environments, `vm_dict()` only returns routable IPv4 addresses — link-local or IPv6 addresses have never been observed in practice. The outer `TimeoutSampler` retry loop on `_check_file` provides a safety net if an unusable address is ever returned. Do not flag missing IP-routability filtering on this function.

Learnt from: prabinovRedhat
Repo: RedHatQE/mtv-api-tests PR: 369
File: libs/providers/vmware.py:813-859
Timestamp: 2026-03-23T16:28:09.390Z
Learning: In repository RedHatQE/mtv-api-tests (libs/providers/vmware.py): `VMWareProvider.change_disk_mode()` intentionally does NOT validate the `disk_mode` string against an allowlist before calling `vm.ReconfigVM_Task`. vSphere's API already rejects invalid disk modes with a clear fault. Maintaining an allowlist would create a vSphere-version-coupled maintenance burden. The method is called from a single fixture with a hardcoded value, so the risk is negligible. Do not suggest adding an allowlist or pre-validation for `disk_mode` in this method.

Learnt from: myakove
Repo: RedHatQE/mtv-api-tests PR: 328
File: tests/copyoffload/test_copyoffload_migration.py:896-896
Timestamp: 2026-03-11T09:45:41.336Z
Learning: In repository RedHatQE/mtv-api-tests (tests/copyoffload/test_copyoffload_migration.py): The `copyoffload_ssh_key` fixture (formerly `setup_copyoffload_ssh`) is intentionally attached only to specific copy-offload test classes that require ESXi SSH-based copy-offload. Classes like TestCopyoffloadRdmVirtualDiskMigration, TestCopyoffloadMultiDatastoreMigration, TestCopyoffloadIndependentPersistentDiskMigration, TestCopyoffloadIndependentNonpersistentDiskMigration, TestCopyoffload10MixedDisksMigration, TestCopyoffloadLargeVmMigration, and TestCopyoffloadWarmMigration intentionally omit `copyoffload_ssh_key`. Do not flag missing `copyoffload_ssh_key` on copy-offload test classes that never had the SSH fixture — only classes that explicitly need SSH-based copy-offload should include it.

Learnt from: krcmarik
Repo: RedHatQE/mtv-api-tests PR: 314
File: utilities/vmware_guest_operations.py:244-249
Timestamp: 2026-03-11T23:33:27.611Z
Learning: In repository RedHatQE/mtv-api-tests (utilities/vmware_guest_operations.py): `detect_vmware_ip_origins_via_guest_ops` is an intentional best-effort workaround for open-vm-tools issue `#694`. When `nmcli` returns no parseable data (e.g., NetworkManager not installed or empty output), logging a warning and returning normally is correct — the migration must not fail due to this optional IP-origin detection step. Unresolved `is_static_ip=None` values are the default state and are handled downstream by the caller and post-migration checks. Do not flag the silent-return path or the absence of a "still-unknown origins" check in this function.

Learnt from: krcmarik
Repo: RedHatQE/mtv-api-tests PR: 314
File: conftest.py:1027-1046
Timestamp: 2026-03-12T00:24:31.956Z
Learning: In repository RedHatQE/mtv-api-tests (conftest.py): The detect_vmware_ip_origins_via_guest_ops call in prepared_plan is intentionally executed for ALL Linux/vSphere VMs, regardless of whether the plan has preserve_static_ips set. This is because Linux static IP preservation happens natively on VMware, making IP-origin detection always relevant. The unconditional `except ValueError: raise` (failing hard on ValueError for all Linux vSphere VMs) is intentional — missing Guest Ops credentials or similar prerequisites are a hard requirement, not optional. Do not suggest gating this block or the ValueError re-raise behind plan.get("preserve_static_ips").

Learnt from: krcmarik
Repo: RedHatQE/mtv-api-tests PR: 288
File: libs/forklift_inventory.py:79-82
Timestamp: 2026-02-05T09:32:12.657Z
Learning: In repository RedHatQE/mtv-api-tests, for OpenStack VM cloning in libs/forklift_inventory.py: ALL cloned VMs use volume-first Block Device Mapping (see libs/providers/openstack.py:272-279), which guarantees that every VM is created with boot volumes. The `attachedVolumes` field will never remain perpetually empty in practice. Therefore, defensive checks distinguishing "no volumes ever attached" from "volumes still syncing" are unnecessary and would reduce code clarity by guarding against impossible scenarios.

Learnt from: krcmarik
Repo: RedHatQE/mtv-api-tests PR: 247
File: libs/providers/openshift.py:185-190
Timestamp: 2026-01-29T11:33:17.513Z
Learning: In repository RedHatQE/mtv-api-tests, for Kubernetes VirtualMachine CRDs in libs/providers/openshift.py: template.metadata and template.spec are guaranteed structural elements that cannot be None per the Kubernetes API schema. Defensive None guards on these fields are unnecessary but harmless. Line 178 accesses template.spec.domain directly without checks, confirming this guarantee.

Learnt from: CR
Repo: RedHatQE/mtv-api-tests PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-24T08:43:38.088Z
Learning: Applies to **/*.py : DynamicClient can be imported inside TYPE_CHECKING block or used as string annotation (e.g., 'DynamicClient'). Never instantiate DynamicClient directly - use get_client() instead. Do not use in isinstance() or runtime checks.

Learnt from: MiriSafra
Repo: RedHatQE/mtv-api-tests PR: 0
File: :0-0
Timestamp: 2026-03-02T14:03:22.214Z
Learning: In repository RedHatQE/mtv-api-tests: Test methods (especially in test_*.py files) should remain explicit and self-contained rather than extracting duplication into helper functions. Each test step should be clearly visible in the test method itself for readability, following pytest best practices where test intent should be immediately obvious.

Learnt from: myakove
Repo: RedHatQE/mtv-api-tests PR: 295
File: conftest.py:353-355
Timestamp: 2026-02-13T11:43:13.262Z
Learning: When reviewing PRs in the RedHatQE/mtv-api-tests repository, always explicitly acknowledge and analyze commit messages as a key part of the review process, mentioning them prominently in review comments when relevant to the code changes being discussed.

Learnt from: rgolangh
Repo: RedHatQE/mtv-api-tests PR: 321
File: Dockerfile:59-63
Timestamp: 2026-03-25T11:24:46.138Z
Learning: In RedHatQE/mtv-api-tests Dockerfile (PR `#321` multi-stage build): the runtime stage intentionally installs only `python3`, `libxml2`, and `openssl` (no build toolchain: no gcc, clang, libxml2-devel, libcurl-devel, openssl-devel, git, python3-pip). All Python extensions are pre-compiled in the builder stage via `uv sync --locked` with `UV_COMPILE_BYTECODE=1`, and the full `/app` directory is copied to the runtime stage. Do NOT suggest adding toolchain or -devel packages to the runtime stage of this multi-stage Dockerfile.

Learnt from: MiriSafra
Repo: RedHatQE/mtv-api-tests PR: 0
File: :0-0
Timestamp: 2026-03-02T14:03:22.214Z
Learning: In repository RedHatQE/mtv-api-tests: PVC name filtering using substring match (e.g., `plan_name in pvc.name`) is safe when plan names include UUID suffixes (e.g., `auto-wn5y-source-vsphere-7-0-3-5buv-cold`) because MTV creates PVCs as `plan-{plan_name}-{random}`, making collision risk effectively zero due to the UUID-based naming scheme.

Learnt from: krcmarik
Repo: RedHatQE/mtv-api-tests PR: 299
File: tests/test_warm_migration_comprehensive.py:101-110
Timestamp: 2026-02-18T21:09:16.904Z
Learning: In repository RedHatQE/mtv-api-tests: For very simple, self-documenting one-line operations (e.g., list comprehensions like `[vm["name"] for vm in prepared_plan["virtual_machines"]]`), do not suggest extraction into helper functions when they appear only 2 times. The overhead of function definition and docstring outweighs the DRY benefit. Follow the "Rule of Three": only suggest extraction after 3+ occurrences of identical logic.

Learnt from: MiriSafra
Repo: RedHatQE/mtv-api-tests PR: 0
File: :0-0
Timestamp: 2026-03-04T15:47:50.626Z
Learning: In repository RedHatQE/mtv-api-tests: In the shared-disk migration feature (libs/providers/vmware.py relink_shared_disks), owner VM selection using positions[0] is correct and intentional. The test config in tests/tests_config/config.py explicitly orders VMs so that the first VM (migrate_shared_disks=true/owner) always appears first. Adding flag-based owner selection would be redundant since config ordering already encodes the semantics.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 10

♻️ Duplicate comments (2)
cli/mtv_api_tests/generate.py (1)

48-62: ⚠️ Potential issue | 🟠 Major

HIGH: _vsphere_session() can still mask the real discovery failure.

The recent cleanup refactor moved teardown into a context manager, but disconnect_vsphere(si) still runs unguarded in finally. If disconnect fails during unwinding, Python replaces the original discovery/selection exception with the disconnect error, which makes the actual failure much harder to diagnose.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cli/mtv_api_tests/generate.py` around lines 48 - 62, The finally block in
_vsphere_session currently calls disconnect_vsphere(si) unguarded which can
raise and replace the original error from connect_vsphere/usage; wrap the
disconnect_vsphere(si) call in its own try/except inside the finally so any
exception raised during teardown is caught and logged (using
logging.getLogger(__name__).exception or an existing logger) and not re-raised,
ensuring the original discovery/selection exception from connect_vsphere or the
context body is preserved; reference _vsphere_session, connect_vsphere, and
disconnect_vsphere when making the change.
cli/mtv_api_tests/common.py (1)

847-879: ⚠️ Potential issue | 🟠 Major

HIGH: The generated Job still cannot use cluster_ca_bundle.

Lines 875-878 explicitly note that the PEM is only stored, and Lines 946-979 never mount that key or expose KUBERNETES_CLIENT_CA_BUNDLE to the container. A cluster that needed a custom CA during generate will still fail TLS inside the pod.

Either wire cluster_ca_bundle into the pod when it is present, or fail fast in generate_job_yaml() for that configuration so the manifest never advertises an unsupported path.

Based on learnings: when reviewing this repository, relevant commit messages should be analyzed explicitly; the recent hardening work preserved the PEM in the Secret, but the pod wiring is still incomplete.

Also applies to: 911-980

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cli/mtv_api_tests/common.py` around lines 847 - 879, The Secret builder
_build_job_secret_data writes cluster_ca_bundle but generate_job_yaml() never
mounts or forwards it; either wire cluster_ca_bundle into the pod (add the
secret key to the SecretVolume/volumeMount and set the env
KUBERNETES_CLIENT_CA_BUNDLE to that mounted path in generate_job_yaml()/Job
spec) or detect a non-empty ca_bundle in _build_job_secret_data and fail fast in
generate_job_yaml() with a clear error; update the Job manifest generation to
mount the secret key path and export KUBERNETES_CLIENT_CA_BUNDLE when ca_bundle
is present so TLS succeeds inside the test runner container.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cli/mtv_api_tests/common.py`:
- Around line 770-797: The function write_providers_json currently only handles
JSON parse errors but will crash with TypeError if the existing file contains
valid JSON whose root is not an object (e.g., list or string); update
write_providers_json to validate the parsed value (the variable existing after
json.loads) and if it's not a dict, log a warning via console.print about
invalid root type for providers_path, set existing = {} to recover, then
continue (so checks that use provider_key and the eventual _write_secret_file
operate on a dict and do not raise TypeError).
- Around line 311-322: get_vsphere_credentials collects ssl_config but those TLS
fields are not being persisted into the provider artifact, so ensure the vSphere
TLS settings survive the generate->run boundary by adding ssl_config into the
provider dict written to .providers.json (modify the code path that serializes
providers/the ".providers.json" writer to include the ssl_config keys obtained
from get_vsphere_credentials), and if the serializer cannot/should not include
these fields, change get_vsphere_credentials to validate and fail fast (raise or
exit) when non-replayable TLS options (e.g., VSPHERE_VERIFY_SSL=false or custom
CA) are provided; update the provider-serialization location that emits
".providers.json" to include the SSL fields (or perform the early failure) so
discovery settings are reproducible.

In `@cli/mtv_api_tests/generate.py`:
- Around line 199-260: _gather_ocp_storage_class() declares it returns
dict[str,str] for job_creds but actually includes non-string types (e.g.,
verify_ssl: bool, ca_bundle could be bytes/str) and may return the original
ocp_creds; update the type signatures and local annotations to reflect
heterogenous values: change the function return type to tuple[dict[str, Any],
str, str] (import Any from typing) and change the local job_creds annotation to
dict[str, Any]; ensure any other variables that assume dict[str,str] are
adjusted to accept the broader dict type (e.g., callers of
_gather_ocp_storage_class should accept dict[str, Any] for job creds).

In `@cli/mtv_api_tests/run.py`:
- Around line 240-248: The current check inspects the entire oc apply output
(combined_output) for the substring "unchanged" which can match non-Job
resources (e.g., Namespace) and incorrectly bail out; update the logic to only
consider "unchanged" status lines that refer to Job resources. Split
combined_output into lines, filter those lines for Job-related status (match
lines containing the resource type "job" or "job.batch" or resource names like
"job/..." — case-insensitive), and only raise typer.Exit(code=1) if any of those
Job-specific lines contain "unchanged"; keep using the existing variables
result.stdout/result.stderr, combined_output and job_yaml_path for context in
the warning message.
- Around line 229-238: Wrap the subprocess.run call that executes ["oc",
"apply", "-f", str(job_yaml_path)] in a try/except that catches
FileNotFoundError and subprocess.TimeoutExpired (same pattern used at line 149),
so a missing oc binary or timeout is handled rather than raising; in the except
block use console.print to show a clear error (include exception message) and
raise typer.Exit(code=1); keep the existing returncode check for non-zero
results and only run it inside the try block to locate this fix around the
subprocess.run invocation in run.py.
- Around line 330-337: The interactive prompts are being invoked before
validating explicit provider keys; move the provider resolution and validation
calls (_resolve_source_provider_key(providers, source_provider) and
_resolve_ocp_provider(providers, destination_provider)) to run before
select_category() and prompt_test_filter(), and if either resolution fails
return/raise a clear error (or exit) so CLI wrappers/CI do not land in the
interactive flow; ensure you still pass the resolved_provider values into later
calls (select_category, prompt_test_filter, test logic) after validation.

In `@conftest.py`:
- Around line 123-131: The teardown currently unconditionally rmtree's
config.option.basetemp; modify pytest_configure (function pytest_configure) to
set a marker (e.g., config._auto_basetemp = True) only when you create the
basetemp automatically, and update the teardown/cleanup path that calls
shutil.rmtree to check that marker (use getattr(session.config,
"_auto_basetemp", False)) before deleting so user-provided --basetemp
directories are never removed by the test session.

In `@utilities/naming.py`:
- Around line 67-72: Remove the unnecessary intermediate variable reassignment
by returning the result of the final transformation directly: avoid assigning
back to `sanitized` before returning and instead return the `re.sub(r"[^\w\-]",
"-", sanitized)` result (using the existing `test_name`/`sanitized` flow) so you
eliminate the redundant `sanitized =` assignment; update the function that
performs the replacements (references: `test_name`, `sanitized`, and the
`re.sub` call) accordingly.

In `@utilities/utils.py`:
- Around line 633-635: Normalize the CLUSTER_VERIFY_SSL environment value before
checking it: when verify_ssl_env is not None, call
verify_ssl_env.strip().lower() and use that normalized value in the existing
membership test so insecure_verify_skip = normalized not in ("true","1","yes");
keep the same variable names (verify_ssl_env, insecure_verify_skip) and lenient
true-set logic to preserve existing behavior.
- Around line 604-640: The code fails to forward CA bundles to the local-mode
client and mis-parses CLUSTER_VERIFY_SSL with trailing whitespace; update
run.py:_get_cluster_config and generate.py to preserve and pass the ca_bundle
into the pytest config, then extend utilities/utils.py (where get_client is
called) to accept and pass a ca_bundle/ca_cert argument into the client creation
logic (add a parameter to get_client/get_cluster_client and use it when
constructing the DynamicClient or HTTP(S) session), and fix CLUSTER_VERIFY_SSL
parsing by calling verify_ssl_env.strip().lower() (instead of
verify_ssl_env.lower()) before comparing to ("true","1","yes") so
whitespace-trimmed values are handled correctly.

---

Duplicate comments:
In `@cli/mtv_api_tests/common.py`:
- Around line 847-879: The Secret builder _build_job_secret_data writes
cluster_ca_bundle but generate_job_yaml() never mounts or forwards it; either
wire cluster_ca_bundle into the pod (add the secret key to the
SecretVolume/volumeMount and set the env KUBERNETES_CLIENT_CA_BUNDLE to that
mounted path in generate_job_yaml()/Job spec) or detect a non-empty ca_bundle in
_build_job_secret_data and fail fast in generate_job_yaml() with a clear error;
update the Job manifest generation to mount the secret key path and export
KUBERNETES_CLIENT_CA_BUNDLE when ca_bundle is present so TLS succeeds inside the
test runner container.

In `@cli/mtv_api_tests/generate.py`:
- Around line 48-62: The finally block in _vsphere_session currently calls
disconnect_vsphere(si) unguarded which can raise and replace the original error
from connect_vsphere/usage; wrap the disconnect_vsphere(si) call in its own
try/except inside the finally so any exception raised during teardown is caught
and logged (using logging.getLogger(__name__).exception or an existing logger)
and not re-raised, ensuring the original discovery/selection exception from
connect_vsphere or the context body is preserved; reference _vsphere_session,
connect_vsphere, and disconnect_vsphere when making the change.
🪄 Autofix (Beta)

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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 82ed0fe3-3d1a-44f6-a1bb-932cda232be1

📥 Commits

Reviewing files that changed from the base of the PR and between 69997f0 and 6eb5daa.

📒 Files selected for processing (18)
  • cli/mtv_api_tests/common.py
  • cli/mtv_api_tests/generate.py
  • cli/mtv_api_tests/run.py
  • conftest.py
  • docs/automation-and-release.md
  • docs/container-and-openshift-job-execution.md
  • docs/development-workflow.md
  • docs/introduction.md
  • docs/optional-integrations-and-secrets.md
  • docs/project-structure.md
  • docs/pytest-options-and-markers.md
  • docs/quickstart-first-run.md
  • docs/runtime-configuration.md
  • docs/troubleshooting-and-diagnostics.md
  • pytest.ini
  • utilities/must_gather.py
  • utilities/naming.py
  • utilities/utils.py
💤 Files with no reviewable changes (10)
  • docs/quickstart-first-run.md
  • docs/optional-integrations-and-secrets.md
  • pytest.ini
  • docs/introduction.md
  • docs/development-workflow.md
  • docs/runtime-configuration.md
  • docs/automation-and-release.md
  • docs/project-structure.md
  • docs/container-and-openshift-job-execution.md
  • docs/troubleshooting-and-diagnostics.md

Comment thread cli/mtv_api_tests/common.py Resolved
Comment thread cli/mtv_api_tests/common.py Resolved
Comment thread cli/mtv_api_tests/generate.py Resolved
Comment thread cli/mtv_api_tests/run.py Resolved
Comment thread cli/mtv_api_tests/run.py Resolved
Comment thread cli/mtv_api_tests/run.py Resolved
Comment thread conftest.py Resolved
Comment thread utilities/naming.py Resolved
Comment thread utilities/utils.py Resolved
Comment thread utilities/utils.py Resolved
@rgolangh

Copy link
Copy Markdown
Contributor Author

@coderabbitai[bot]

The following review comments were reviewed and a decision was made:

cli/mtv_api_tests/generate.py:48 (duplicate) — HIGH: _vsphere_session() can still mask the real discovery failure.

Skipped: Skipped: diminishing returns — too many rounds of AI feedback on this PR.

cli/mtv_api_tests/common.py:847 (duplicate) — HIGH: The generated Job still cannot use cluster_ca_bundle.

Skipped: Skipped: diminishing returns — too many rounds of AI feedback on this PR.

coderabbitai[bot]
coderabbitai Bot previously approved these changes Mar 25, 2026
@coderabbitai

coderabbitai Bot commented Mar 25, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

@rgolangh: Understood — no objections. Both items noted:

  1. _vsphere_session() masking discovery failures — skipped by design decision.
  2. cluster_ca_bundle not consumed by the Job runner — already tracked as a known TODO; the PEM content is stored in the Secret and ready for when the runner-side wiring is added.

If you'd like, I can open a follow-up GitHub issue to track the CA bundle consumption gap so it doesn't get lost after this PR merges. Just say the word.

😌


🧠 Learnings used
Learnt from: rgolangh
Repo: RedHatQE/mtv-api-tests PR: 321
File: cli/mtv_api_tests/run.py:0-0
Timestamp: 2026-03-23T14:28:32.199Z
Learning: In repository RedHatQE/mtv-api-tests (cli/mtv_api_tests/run.py): `generate` and `run` are intentionally separate CLI subcommands. `run --mode job` expects the Job YAML manifest to already exist (created by a prior `generate` invocation). Do not suggest or add auto-generation fallbacks inside the `run` command's job mode path.

Learnt from: shellymiron
Repo: RedHatQE/mtv-api-tests PR: 239
File: README.md:270-331
Timestamp: 2026-01-12T16:24:42.450Z
Learning: Repository RedHatQE/mtv-api-tests: The OpenShift Job templates in README.md and docs/how-to-run-copyoffload-tests.md are intentionally duplicated with identical sections because the files are meant to be different but share common Job template structure; do not suggest consolidating them into a single YAML manifest.

Learnt from: shellymiron
Repo: RedHatQE/mtv-api-tests PR: 239
File: README.md:122-134
Timestamp: 2026-01-13T09:53:56.701Z
Learning: Repository RedHatQE/mtv-api-tests: The JSON examples in README.md intentionally contain inline `# pragma: allowlist secret` comments (which make them invalid JSON) because pre-commit hooks require these pragmas to pass secret scanning. Lines 114-116 prominently warn users not to copy these comments to their actual `.providers.json` files. This is a deliberate design decision to balance pre-commit requirements with documentation needs. Do not suggest removing these pragmas or flag them as invalid JSON.

Learnt from: rgolangh
Repo: RedHatQE/mtv-api-tests PR: 321
File: cli/mtv_api_tests/common.py:352-355
Timestamp: 2026-03-25T12:40:23.422Z
Learning: In RedHatQE/mtv-api-tests (cli/mtv_api_tests/common.py): `get_ocp_credentials()` intentionally returns only `{"kubeconfig": kubeconfig}` when `KUBECONFIG` is set, without merging `CLUSTER_HOST`, `CLUSTER_USERNAME`, `CLUSTER_PASSWORD`, `CLUSTER_VERIFY_SSL`, or `CLUSTER_CA_BUNDLE`. This is by design — callers that need explicit credentials (e.g., `generate.py:_gather_ocp_storage_class`) are responsible for detecting and prompting for the missing fields. Do NOT flag this early-return pattern as discarding auth/TLS settings.

Learnt from: myakove
Repo: RedHatQE/mtv-api-tests PR: 328
File: tests/copyoffload/conftest.py:240-307
Timestamp: 2026-03-11T09:45:35.130Z
Learning: In repository RedHatQE/mtv-api-tests (tests/copyoffload/conftest.py): The `copyoffload_ssh_key` fixture intentionally uses `scope="session"` even under pytest-xdist (--dist=loadscope). `install_ssh_key_on_esxi` and `remove_ssh_key_from_esxi` in utilities/esxi.py are idempotent — each checks whether the operation is already done before proceeding — so concurrent worker setup/teardown is safe. Do NOT flag session scope on this fixture as a concurrency hazard or suggest refactoring to worker-unique keys.

Learnt from: elsapassaro
Repo: RedHatQE/mtv-api-tests PR: 285
File: tests/tests_config/config.py:373-388
Timestamp: 2026-01-30T14:43:00.204Z
Learning: In repository RedHatQE/mtv-api-tests, for test configurations in tests/tests_config/config.py: VM entries within a test should use the same base name (e.g., "xcopy-template-test") and let the framework auto-generate unique names with UUIDs when cloning is enabled. Explicit `clone_name` fields should only be used for tests that specifically verify operator behavior with non-conforming or special VM names (e.g., test_copyoffload_nonconforming_name_migration).

Learnt from: krcmarik
Repo: RedHatQE/mtv-api-tests PR: 314
File: utilities/vmware_guest_operations.py:103-115
Timestamp: 2026-03-12T00:10:06.804Z
Learning: In RedHatQE/mtv-api-tests (utilities/vmware_guest_operations.py): In `run_command_in_vmware_guest`, NOT fetching the guest temp-file output when `process_info.exitCode != 0` is intentional. The exit code and VM name in `GuestCommandError` are considered sufficient for debugging. Fetching the failed command's output would add an extra Guest Operations API call on an already-failed, rare code path, which the author considers unnecessary complexity. Do not flag this as a deficiency.

Learnt from: MiriSafra
Repo: RedHatQE/mtv-api-tests PR: 0
File: :0-0
Timestamp: 2026-03-04T15:47:50.626Z
Learning: In repository RedHatQE/mtv-api-tests: In shared-disk migration tests (utilities/shared_disk.py), the unmount/remount sequence in verify_shared_disk_data is for cache flushing purposes (forcing buffer flush between writes), NOT to avoid concurrent filesystem access. Shared disks use RWX PVCs and are designed for multi-writer access; SCSI-level coordination handles concurrent mount safety. Do not flag this as a concurrency bug.

Learnt from: prabinovRedhat
Repo: RedHatQE/mtv-api-tests PR: 358
File: utilities/copyoffload_migration.py:280-281
Timestamp: 2026-03-22T21:14:33.207Z
Learning: In repository RedHatQE/mtv-api-tests (utilities/copyoffload_migration.py): When Kubernetes Pod objects are fetched via `Pod.get(label_selector=...)`, `pod.instance.metadata.labels` is guaranteed to be non-null because the label selector query itself proves the labels map exists and is populated. Do NOT flag missing defensive guards (e.g., `pod.instance.metadata.labels or {}`) on pods retrieved this way — it is unnecessary noise. This follows the same fail-fast philosophy used for other guaranteed Kubernetes structural fields in the repo.

Learnt from: CR
Repo: RedHatQE/mtv-api-tests PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-24T08:43:38.088Z
Learning: Never run tests directly (pytest, uv run pytest). Tests require live clusters, provider connections, and credentials. AI can read/analyze/write/fix tests and review structure.

Learnt from: MiriSafra
Repo: RedHatQE/mtv-api-tests PR: 0
File: :0-0
Timestamp: 2026-03-04T16:25:43.654Z
Learning: In repository RedHatQE/mtv-api-tests: In libs/providers/vmware.py relink_shared_disks(), defensive vm.config guards (e.g., `if vm.config is None`) are intentionally absent. Unlike vm_dict() which queries arbitrary external VMs in unknown states, relink_shared_disks() operates exclusively on VMs freshly cloned by the test fixture. Missing vm.config on a just-cloned VM indicates catastrophic infrastructure failure and should raise AttributeError loudly (fail-fast), not be silently swallowed.

Learnt from: krcmarik
Repo: RedHatQE/mtv-api-tests PR: 247
File: libs/providers/openshift.py:185-190
Timestamp: 2026-01-29T11:33:17.513Z
Learning: In repository RedHatQE/mtv-api-tests, for Kubernetes VirtualMachine CRDs in libs/providers/openshift.py: template.metadata and template.spec are guaranteed structural elements that cannot be None per the Kubernetes API schema. Defensive None guards on these fields are unnecessary but harmless. Line 178 accesses template.spec.domain directly without checks, confirming this guarantee.

Learnt from: prabinovRedhat
Repo: RedHatQE/mtv-api-tests PR: 317
File: utilities/copyoffload_migration.py:124-129
Timestamp: 2026-03-08T12:36:28.550Z
Learning: In repository RedHatQE/mtv-api-tests (utilities/copyoffload_migration.py): The `_get_ip()` helper inside `wait_for_cloud_init` intentionally takes the first IP address returned by `source_provider.vm_dict()` without filtering for routability. In the project's vSphere environments, `vm_dict()` only returns routable IPv4 addresses — link-local or IPv6 addresses have never been observed in practice. The outer `TimeoutSampler` retry loop on `_check_file` provides a safety net if an unusable address is ever returned. Do not flag missing IP-routability filtering on this function.

Learnt from: rgolangh
Repo: RedHatQE/mtv-api-tests PR: 321
File: cli/mtv_api_tests/__init__.py:39-62
Timestamp: 2026-03-23T14:28:39.174Z
Learning: In repository RedHatQE/mtv-api-tests: `# noqa: M511` comments on `typer.Option(...)` default argument lines are required and intentional. M511 is the `flake8-mutable` rule (not McCabe complexity), actively selected via `.flake8` (`select=M511`) and enforced by the pre-commit `flake8` hook with `additional_dependencies: [flake8-mutable]`. Do NOT suggest removing these suppressions.

Learnt from: rgolangh
Repo: RedHatQE/mtv-api-tests PR: 321
File: utilities/utils.py:633-639
Timestamp: 2026-03-25T12:40:39.371Z
Learning: In repository RedHatQE/mtv-api-tests (utilities/utils.py): The `CLUSTER_VERIFY_SSL` env var parsing in `get_cluster_client()` intentionally uses lenient boolean parsing — any value that is NOT in ("true", "1", "yes") (case-insensitive, after strip) resolves to `insecure_verify_skip=True`. Unknown, typo, or empty values default to insecure rather than raising a ValueError. This matches common boolean env-var parsing conventions and is a deliberate design decision. Do NOT suggest adding strict validation or raising ValueError for unrecognized CLUSTER_VERIFY_SSL values.

Learnt from: krcmarik
Repo: RedHatQE/mtv-api-tests PR: 270
File: libs/providers/rhv.py:229-230
Timestamp: 2026-01-22T02:37:52.606Z
Learning: In repository RedHatQE/mtv-api-tests, for RHV provider in libs/providers/rhv.py, the snapshot enum fields `snapshot.snapshot_status` and `snapshot.snapshot_type` are intentionally accessed without None guards (using `.value` directly). This crash-on-None behavior is by design: these fields are reliably populated by the oVirt SDK in practice, and any None value indicates an SDK/API contract violation that should fail fast rather than be silently masked. Downstream validation doesn't use these fields, so defensive handling is unnecessary.

Learnt from: krcmarik
Repo: RedHatQE/mtv-api-tests PR: 308
File: libs/providers/openshift.py:210-220
Timestamp: 2026-02-26T12:29:05.059Z
Learning: In repository RedHatQE/mtv-api-tests, for ocp_resources VirtualMachine objects in libs/providers/openshift.py: When the VM is created with `ensure_exists=True`, the `.instance.status` object is guaranteed to be present, so direct attribute access (e.g., `cnv_vm.instance.status.printableStatus`) is correct and consistent throughout the file. However, the `conditions` field within status is optional per KubeVirt's schema (json:"conditions,omitempty" optional:"true"), so use `.get("conditions", [])` for defensive access. The pattern `cnv_vm.instance.status.get("conditions", [])` is the correct middle ground. This differs from polling contexts (e.g., TimeoutSampler results in wait_for_cnv_vm_guest_agent) where `status` itself may not be populated yet, requiring `.get("status", {}).get("conditions", {})`.

Learnt from: krcmarik
Repo: RedHatQE/mtv-api-tests PR: 314
File: conftest.py:1027-1046
Timestamp: 2026-03-12T00:24:31.956Z
Learning: In repository RedHatQE/mtv-api-tests (conftest.py): The detect_vmware_ip_origins_via_guest_ops call in prepared_plan is intentionally executed for ALL Linux/vSphere VMs, regardless of whether the plan has preserve_static_ips set. This is because Linux static IP preservation happens natively on VMware, making IP-origin detection always relevant. The unconditional `except ValueError: raise` (failing hard on ValueError for all Linux vSphere VMs) is intentional — missing Guest Ops credentials or similar prerequisites are a hard requirement, not optional. Do not suggest gating this block or the ValueError re-raise behind plan.get("preserve_static_ips").

Learnt from: krcmarik
Repo: RedHatQE/mtv-api-tests PR: 314
File: utilities/vmware_guest_operations.py:244-249
Timestamp: 2026-03-11T23:33:27.611Z
Learning: In repository RedHatQE/mtv-api-tests (utilities/vmware_guest_operations.py): `detect_vmware_ip_origins_via_guest_ops` is an intentional best-effort workaround for open-vm-tools issue `#694`. When `nmcli` returns no parseable data (e.g., NetworkManager not installed or empty output), logging a warning and returning normally is correct — the migration must not fail due to this optional IP-origin detection step. Unresolved `is_static_ip=None` values are the default state and are handled downstream by the caller and post-migration checks. Do not flag the silent-return path or the absence of a "still-unknown origins" check in this function.

Learnt from: prabinovRedhat
Repo: RedHatQE/mtv-api-tests PR: 369
File: libs/providers/vmware.py:813-859
Timestamp: 2026-03-23T16:28:09.390Z
Learning: In repository RedHatQE/mtv-api-tests (libs/providers/vmware.py): `VMWareProvider.change_disk_mode()` intentionally does NOT validate the `disk_mode` string against an allowlist before calling `vm.ReconfigVM_Task`. vSphere's API already rejects invalid disk modes with a clear fault. Maintaining an allowlist would create a vSphere-version-coupled maintenance burden. The method is called from a single fixture with a hardcoded value, so the risk is negligible. Do not suggest adding an allowlist or pre-validation for `disk_mode` in this method.

Learnt from: myakove
Repo: RedHatQE/mtv-api-tests PR: 295
File: conftest.py:353-355
Timestamp: 2026-02-13T11:43:13.262Z
Learning: When reviewing PRs in the RedHatQE/mtv-api-tests repository, always explicitly acknowledge and analyze commit messages as a key part of the review process, mentioning them prominently in review comments when relevant to the code changes being discussed.

Learnt from: MiriSafra
Repo: RedHatQE/mtv-api-tests PR: 0
File: :0-0
Timestamp: 2026-03-02T14:03:22.214Z
Learning: In repository RedHatQE/mtv-api-tests: Test methods (especially in test_*.py files) should remain explicit and self-contained rather than extracting duplication into helper functions. Each test step should be clearly visible in the test method itself for readability, following pytest best practices where test intent should be immediately obvious.

Learnt from: MiriSafra
Repo: RedHatQE/mtv-api-tests PR: 0
File: :0-0
Timestamp: 2026-03-02T14:03:22.214Z
Learning: In repository RedHatQE/mtv-api-tests: PVC name filtering using substring match (e.g., `plan_name in pvc.name`) is safe when plan names include UUID suffixes (e.g., `auto-wn5y-source-vsphere-7-0-3-5buv-cold`) because MTV creates PVCs as `plan-{plan_name}-{random}`, making collision risk effectively zero due to the UUID-based naming scheme.

Learnt from: rgolangh
Repo: RedHatQE/mtv-api-tests PR: 321
File: Dockerfile:59-63
Timestamp: 2026-03-25T11:24:46.138Z
Learning: In RedHatQE/mtv-api-tests Dockerfile (PR `#321` multi-stage build): the runtime stage intentionally installs only `python3`, `libxml2`, and `openssl` (no build toolchain: no gcc, clang, libxml2-devel, libcurl-devel, openssl-devel, git, python3-pip). All Python extensions are pre-compiled in the builder stage via `uv sync --locked` with `UV_COMPILE_BYTECODE=1`, and the full `/app` directory is copied to the runtime stage. Do NOT suggest adding toolchain or -devel packages to the runtime stage of this multi-stage Dockerfile.

Learnt from: krcmarik
Repo: RedHatQE/mtv-api-tests PR: 299
File: tests/test_warm_migration_comprehensive.py:101-110
Timestamp: 2026-02-18T21:09:16.904Z
Learning: In repository RedHatQE/mtv-api-tests: For very simple, self-documenting one-line operations (e.g., list comprehensions like `[vm["name"] for vm in prepared_plan["virtual_machines"]]`), do not suggest extraction into helper functions when they appear only 2 times. The overhead of function definition and docstring outweighs the DRY benefit. Follow the "Rule of Three": only suggest extraction after 3+ occurrences of identical logic.

Learnt from: MiriSafra
Repo: RedHatQE/mtv-api-tests PR: 0
File: :0-0
Timestamp: 2026-03-04T16:25:43.654Z
Learning: In repository RedHatQE/mtv-api-tests: In utilities/shared_disk.py verify_shared_disk_data, VM1's shared disk is intentionally left mounted while VM2 also mounts it. This concurrent mount window is the feature under test (RWX PVC multi-writer access for MTV-454). Do NOT add try/finally around VM1's SSH session to force unmount — that would serialize the access and eliminate the concurrent-access test coverage entirely.

@rgolangh

Copy link
Copy Markdown
Contributor Author

/verified

1 similar comment
@rgolangh

Copy link
Copy Markdown
Contributor Author

/verified

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 4

♻️ Duplicate comments (5)
utilities/utils.py (1)

633-635: ⚠️ Potential issue | 🟠 Major

HIGH: Normalize CLUSTER_VERIFY_SSL before evaluating it.

For the feat: Add test configuration and execution tool flow, this env var is now user-facing. Values like "true " or "YES\n" currently fall through to insecure_verify_skip=True and silently disable TLS verification. Keep the existing lenient true-set, but normalize with strip().lower() first.

Suggested fix
     verify_ssl_env = os.environ.get("CLUSTER_VERIFY_SSL")
     if verify_ssl_env is not None:
-        insecure_verify_skip = verify_ssl_env.lower() not in ("true", "1", "yes")
+        insecure_verify_skip = verify_ssl_env.strip().lower() not in ("true", "1", "yes")

Based on learnings: CLUSTER_VERIFY_SSL parsing here is intentionally lenient, but it should be case-insensitive after strip().

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@utilities/utils.py` around lines 633 - 635, The CLUSTER_VERIFY_SSL env var
parsing is too permissive—normalize its value by calling strip().lower() on
verify_ssl_env before checking membership so values like "true ", " YES\n" are
recognized; update the logic that sets insecure_verify_skip (using the
verify_ssl_env variable) to use the normalized string and keep the same truthy
set ("true","1","yes") while preserving the current inverse assignment
(insecure_verify_skip = normalized not in (...)).
cli/mtv_api_tests/generate.py (1)

199-260: 🛠️ Refactor suggestion | 🟠 Major

HIGH: Fix the job_creds type contract.

This is still annotated as dict[str, str], but the function adds non-string TLS fields (verify_ssl, ca_bundle) and can return the original heterogeneous credential map. That makes the new generate flow’s type contract false and brittle for static checking.

🛠️ Proposed fix
-def _gather_ocp_storage_class() -> tuple[dict[str, str], str, str]:
+def _gather_ocp_storage_class() -> tuple[dict[str, Any], str, str]:
@@
-        job_creds: dict[str, str] = {"host": host, "username": username, "password": password}
+        job_creds: dict[str, Any] = {"host": host, "username": username, "password": password}
#!/bin/bash
python - <<'PY'
from pathlib import Path

text = Path("cli/mtv_api_tests/generate.py").read_text()
checks = [
    "def _gather_ocp_storage_class() -> tuple[dict[str, str], str, str]:",
    'job_creds: dict[str, str] = {"host": host, "username": username, "password": password}',
    'job_creds["verify_ssl"] = ocp_creds["verify_ssl"]',
    'job_creds["ca_bundle"] = ocp_creds["ca_bundle"]',
]
for check in checks:
    print(("FOUND" if check in text else "MISSING"), check)
PY

As per coding guidelines, "Variables must have consistent types. Use type annotations and default to empty containers (e.g., {}, []) to avoid None checks." and "All new functions and functions with signature changes must have complete type annotations using built-in Python typing (dict, list, tuple)."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cli/mtv_api_tests/generate.py` around lines 199 - 260, The function
_gather_ocp_storage_class currently declares a return type of tuple[dict[str,
str], str, str] and creates job_creds: dict[str, str], but it can include
non-string fields ("verify_ssl", "ca_bundle") and may return the original
heterogeneous ocp_creds; change the type annotations to reflect heterogeneous
values (e.g., use dict[str, Any] or Mapping[str, Any]) for the job_creds
variable and the function return, update the job_creds annotation and the
function signature accordingly, and add/adjust the typing import (Any or
Mapping) so static type checkers accept assignments from ocp_creds and the added
TLS fields; keep the logic in _gather_ocp_storage_class, referencing job_creds,
ocp_creds, and get_ocp_credentials.
cli/mtv_api_tests/common.py (3)

782-795: ⚠️ Potential issue | 🟠 Major

HIGH: Reject non-object .providers.json roots before mutating them.

json.loads() succeeding is not enough here. If the file contains valid JSON like [] or "foo", existing[provider_key] = provider_config raises TypeError and the CLI aborts. Treat any non-dict root as invalid, warn, and recover with {}.

🔧 Minimal fix
     if providers_path.exists():
         try:
-            existing = json.loads(providers_path.read_text())
+            loaded = json.loads(providers_path.read_text())
+            if isinstance(loaded, dict):
+                existing = loaded
+            else:
+                console.print(
+                    f"  [yellow]Warning: existing {providers_path} must contain a JSON object, will overwrite[/yellow]"
+                )
+                existing = {}
         except json.JSONDecodeError:
             console.print(f"  [yellow]Warning: existing {providers_path} is invalid, will overwrite[/yellow]")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cli/mtv_api_tests/common.py` around lines 782 - 795, When loading
providers_path into existing, ensure the parsed JSON is a dict before mutating:
after json.loads(...) in the existing assignment block validate that existing is
an instance of dict, and if not (including when JSONDecodeError was caught)
print the same warning and set existing = {} so subsequent
existing[provider_key] = provider_config won't raise TypeError; keep using
Confirm.ask for overwrite prompts and then call
_write_secret_file(providers_path, json.dumps(existing, indent=2) + "\n") as
before.

401-419: ⚠️ Potential issue | 🟠 Major

HIGH: This regresses the vSphere TLS hardening.

ssl_config=None currently becomes verify_ssl=False, and ssl.create_default_context(cafile=...) still executes before the try. That means a caller that forgets to forward TLS settings silently disables cert validation, while unreadable or malformed CA files bypass the normalized ConnectionError flow.

🔧 Minimal fix
-    verify_ssl = ssl_config.get("verify_ssl", "false") == "true"
+    verify_ssl = ssl_config.get("verify_ssl", "true") == "true"
     ca_bundle = ssl_config.get("ca_bundle", "")

     connect_kwargs: dict[str, Any] = {"host": host, "user": username, "pwd": password, "port": 443}
-    if verify_ssl and ca_bundle:
-        ssl_context = ssl.create_default_context(cafile=ca_bundle)
-        connect_kwargs["sslContext"] = ssl_context
-    elif not verify_ssl:
-        console.print("  [dim]SSL certificate validation disabled for vSphere connection[/dim]")
-        connect_kwargs["disableSslCertValidation"] = True

     with console.status("[bold]Connecting to vSphere..."):
         try:
+            if verify_ssl and ca_bundle:
+                connect_kwargs["sslContext"] = ssl.create_default_context(cafile=ca_bundle)
+            elif not verify_ssl:
+                console.print("  [dim]SSL certificate validation disabled for vSphere connection[/dim]")
+                connect_kwargs["disableSslCertValidation"] = True
             si = SmartConnect(**connect_kwargs)
+        except OSError as exc:
+            raise ConnectionError(f"Failed to load vSphere CA bundle '{ca_bundle}': {exc}") from exc
         except vim.fault.InvalidLogin as exc:
             raise ConnectionError(f"Invalid vSphere credentials for {host}") from exc
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cli/mtv_api_tests/common.py` around lines 401 - 419, The code currently
treats ssl_config=None as disabling verification and calls
ssl.create_default_context outside the try, which can raise and bypass the
ConnectionError flow; change the logic so that a None ssl_config defaults to
strict verification (treat None as verify_ssl=True), move creation of
ssl_context (ssl.create_default_context(cafile=ca_bundle)) into the try block
immediately before SmartConnect, and wrap that creation so any exceptions are
caught and re-raised as ConnectionError; update references ssl_config,
verify_ssl, ca_bundle, connect_kwargs and SmartConnect accordingly and only set
connect_kwargs["sslContext"] when ssl_context was successfully created.

649-692: ⚠️ Potential issue | 🟠 Major

HIGH: The feat: Add test configuration and execution tool handoff still drops vSphere TLS state.

get_vsphere_credentials() collects verify_ssl / ca_bundle, but build_providers_json() never serializes them. Discovery can succeed with non-default vSphere trust and the saved provider then loses that state for later local/job execution. Persist the TLS fields here, or fail fast during generate when the selected trust mode is not replayable.

🔧 Minimal direction
 def build_providers_json(
     vsphere_host: str,
     vsphere_user: str,
     vsphere_pass: str,
     version: str,
     guest_linux_user: str,
     guest_linux_pass: str,
     copyoffload_config: dict[str, Any],
+    ssl_config: dict[str, str],
     guest_vm_win_user: str = "",
     guest_vm_win_password: str = "",
 ) -> tuple[str, dict[str, Any]]:
@@
     provider_config: dict[str, Any] = {
         "type": "vsphere",
         "version": version,
         "fqdn": vsphere_host,
         "api_url": f"https://{vsphere_host}/sdk",
         "username": vsphere_user,
         "password": vsphere_pass,
         "guest_vm_linux_user": guest_linux_user,
         "guest_vm_linux_password": guest_linux_pass,
         "copyoffload": copyoffload_config,
     }
+    if "verify_ssl" in ssl_config:
+        provider_config["verify_ssl"] = ssl_config["verify_ssl"]
+    if "ca_bundle" in ssl_config:
+        provider_config["ca_bundle"] = ssl_config["ca_bundle"]

Based on learnings: generate and run are intentionally separate CLI subcommands. run --mode job expects the Job YAML manifest to already exist (created by a prior generate invocation).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cli/mtv_api_tests/common.py` around lines 649 - 692, build_providers_json
currently omits the vSphere TLS state collected earlier, so update
build_providers_json to include the TLS fields (the verify_ssl boolean and
ca_bundle path/content) into provider_config (use the same keys produced by
get_vsphere_credentials, e.g. "verify_ssl" and "ca_bundle" or the existing
canonical keys used elsewhere) so the generated .providers.json preserves trust
mode; if those fields are absent or indicate a non-replayable trust mode, either
persist explicit values or raise a clear error in build_providers_json (or in
generate) to fail-fast when the selected trust mode cannot be replayed later by
run --mode job.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cli/mtv_api_tests/generate.py`:
- Around line 359-375: Update the generated shell commands to include the
documented "uv run " prefix and safely quote variable interpolations: prepend
"uv run " to the local command assembly stored in local_cmd (where vsphere_key,
ocp_key, storage_class, resolved_category and test_filter are used) and wrap
vsphere_key, ocp_key, storage_class, resolved_category, namespace and job_name
with shlex.quote() when interpolating; also change the hardcoded job invocation
in the Panel (currently "mtv-api-tests run --mode job") to "uv run mtv-api-tests
run --mode job" and ensure the oc logs command uses shlex.quote(namespace) and
shlex.quote(job_name) when building "oc logs -f -n ... job/...".

In `@cli/mtv_api_tests/run.py`:
- Around line 128-146: The else after the raise is redundant; remove the else
and dedent the block that creates tc_args, reads verify_ssl, builds env_vars and
returns them so the code after the raise executes in the normal path;
specifically update the logic around ocp_provider to keep the console.print, the
tc_args initialization, handling of verify_ssl (verify_ssl_bool and the
insecure_verify_skip tc arg), the env_vars dict construction
(CLUSTER_USERNAME/CLUSTER_PASSWORD) and the final return tc_args, env_vars at
the top level of the if-not-all check instead of inside an else.

In `@Dockerfile`:
- Around line 42-46: The Dockerfile runs pre-compilation via "uv sync"
(UV_COMPILE_BYTECODE=1) but then removes compiled bytecode with the "find
${APP_DIR} -type d -name \"__pycache__\" ... rm -rfv" line; either stop deleting
those __pycache__ directories or disable bytecode compilation. To fix, decide
which behavior you want and implement it: (A) preserve precompiled files by
removing the "find ${APP_DIR} -type d -name \"__pycache__\" -print0 | xargs -0
-r rm -rfv" removal (and keep UV_COMPILE_BYTECODE=1 for "uv sync"), or (B)
disable compilation by ensuring UV_COMPILE_BYTECODE=0 (or unset) when running
"uv sync" so the deletion becomes unnecessary; update the Dockerfile around the
"uv sync" invocation and the cleanup step accordingly and keep references to
UV_COMPILE_BYTECODE, uv sync, and the find __pycache__ command consistent.

In `@README.md`:
- Around line 501-519: The README incorrectly states "All interactive prompts
can be bypassed" while cli/mtv_api_tests/generate.py still prompts for
discovered resources and vendor-specific inputs; either update the README
examples and wording for "uv run mtv-api-tests generate" to clearly state that
generate currently requires interactive inputs for discovered resources
(datastores, VM/ESXi host, storage class, storage vendor, guest/storage
credentials) or implement CLI flags in generate.py to accept those values
non-interactively (e.g., add flags for datastore, vm, esxi_host, storage_class,
storage_vendor, guest_creds, storage_creds) and then document those flags in
README so CI usage is actually non-interactive.

---

Duplicate comments:
In `@cli/mtv_api_tests/common.py`:
- Around line 782-795: When loading providers_path into existing, ensure the
parsed JSON is a dict before mutating: after json.loads(...) in the existing
assignment block validate that existing is an instance of dict, and if not
(including when JSONDecodeError was caught) print the same warning and set
existing = {} so subsequent existing[provider_key] = provider_config won't raise
TypeError; keep using Confirm.ask for overwrite prompts and then call
_write_secret_file(providers_path, json.dumps(existing, indent=2) + "\n") as
before.
- Around line 401-419: The code currently treats ssl_config=None as disabling
verification and calls ssl.create_default_context outside the try, which can
raise and bypass the ConnectionError flow; change the logic so that a None
ssl_config defaults to strict verification (treat None as verify_ssl=True), move
creation of ssl_context (ssl.create_default_context(cafile=ca_bundle)) into the
try block immediately before SmartConnect, and wrap that creation so any
exceptions are caught and re-raised as ConnectionError; update references
ssl_config, verify_ssl, ca_bundle, connect_kwargs and SmartConnect accordingly
and only set connect_kwargs["sslContext"] when ssl_context was successfully
created.
- Around line 649-692: build_providers_json currently omits the vSphere TLS
state collected earlier, so update build_providers_json to include the TLS
fields (the verify_ssl boolean and ca_bundle path/content) into provider_config
(use the same keys produced by get_vsphere_credentials, e.g. "verify_ssl" and
"ca_bundle" or the existing canonical keys used elsewhere) so the generated
.providers.json preserves trust mode; if those fields are absent or indicate a
non-replayable trust mode, either persist explicit values or raise a clear error
in build_providers_json (or in generate) to fail-fast when the selected trust
mode cannot be replayed later by run --mode job.

In `@cli/mtv_api_tests/generate.py`:
- Around line 199-260: The function _gather_ocp_storage_class currently declares
a return type of tuple[dict[str, str], str, str] and creates job_creds:
dict[str, str], but it can include non-string fields ("verify_ssl", "ca_bundle")
and may return the original heterogeneous ocp_creds; change the type annotations
to reflect heterogeneous values (e.g., use dict[str, Any] or Mapping[str, Any])
for the job_creds variable and the function return, update the job_creds
annotation and the function signature accordingly, and add/adjust the typing
import (Any or Mapping) so static type checkers accept assignments from
ocp_creds and the added TLS fields; keep the logic in _gather_ocp_storage_class,
referencing job_creds, ocp_creds, and get_ocp_credentials.

In `@utilities/utils.py`:
- Around line 633-635: The CLUSTER_VERIFY_SSL env var parsing is too
permissive—normalize its value by calling strip().lower() on verify_ssl_env
before checking membership so values like "true ", " YES\n" are recognized;
update the logic that sets insecure_verify_skip (using the verify_ssl_env
variable) to use the normalized string and keep the same truthy set
("true","1","yes") while preserving the current inverse assignment
(insecure_verify_skip = normalized not in (...)).
🪄 Autofix (Beta)

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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 7bd02645-2d5f-4dac-be0c-c2ee08e07201

📥 Commits

Reviewing files that changed from the base of the PR and between 6eb5daa and 76d1923.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (13)
  • .dockerignore
  • .gitignore
  • Dockerfile
  • README.md
  • cli/__init__.py
  • cli/mtv_api_tests/__init__.py
  • cli/mtv_api_tests/__main__.py
  • cli/mtv_api_tests/common.py
  • cli/mtv_api_tests/generate.py
  • cli/mtv_api_tests/run.py
  • guides/copyoffload/how-to-run-copyoffload-tests.md
  • pyproject.toml
  • utilities/utils.py

Comment thread cli/mtv_api_tests/generate.py Resolved
Comment thread cli/mtv_api_tests/run.py Resolved
Comment thread Dockerfile Resolved
Comment thread README.md Resolved

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 5

♻️ Duplicate comments (5)
utilities/utils.py (1)

633-635: ⚠️ Potential issue | 🟠 Major

HIGH: Normalize CLUSTER_VERIFY_SSL with strip() before evaluating.

verify_ssl_env.lower() misclassifies values like "true " / "yes\n" and silently flips TLS verification off. Keep lenient parsing, but normalize first.

🔧 Proposed fix
     verify_ssl_env = os.environ.get("CLUSTER_VERIFY_SSL")
     if verify_ssl_env is not None:
-        insecure_verify_skip = verify_ssl_env.lower() not in ("true", "1", "yes")
+        insecure_verify_skip = verify_ssl_env.strip().lower() not in ("true", "1", "yes")

Based on learnings, CLUSTER_VERIFY_SSL parsing is intentionally lenient but expected to be case-insensitive after strip.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@utilities/utils.py` around lines 633 - 635, The CLUSTER_VERIFY_SSL
environment value is not normalized before checking, so update the logic that
reads verify_ssl_env (from os.environ.get("CLUSTER_VERIFY_SSL")) to call strip()
before lowercasing and evaluating; specifically, normalize verify_ssl_env =
verify_ssl_env.strip() (or combine .strip().lower()) then test membership
against ("true","1","yes") when setting insecure_verify_skip so values like
"true " or "yes\n" are handled correctly.
cli/mtv_api_tests/common.py (1)

649-692: ⚠️ Potential issue | 🟠 Major

HIGH: The generated vSphere provider drops the TLS settings collected during generate.

For the feat: Add test configuration and execution tool path, .providers.json is the replay artifact. get_vsphere_credentials() collects verify_ssl/ca_bundle, and connect_vsphere() uses them for discovery, but build_providers_json() serializes neither. A custom-CA vCenter or verify_ssl=false setup can therefore work during generate and then reconnect with different trust settings later.

Possible fix
 def build_providers_json(
     vsphere_host: str,
     vsphere_user: str,
     vsphere_pass: str,
     version: str,
     guest_linux_user: str,
     guest_linux_pass: str,
     copyoffload_config: dict[str, Any],
+    ssl_config: dict[str, str],
     guest_vm_win_user: str = "",
     guest_vm_win_password: str = "",
 ) -> tuple[str, dict[str, Any]]:
@@
     provider_config: dict[str, Any] = {
         "type": "vsphere",
         "version": version,
         "fqdn": vsphere_host,
         "api_url": f"https://{vsphere_host}/sdk",
         "username": vsphere_user,
         "password": vsphere_pass,
         "guest_vm_linux_user": guest_linux_user,
         "guest_vm_linux_password": guest_linux_pass,
         "copyoffload": copyoffload_config,
     }
+    if "verify_ssl" in ssl_config:
+        provider_config["verify_ssl"] = ssl_config["verify_ssl"]
+    if "ca_bundle" in ssl_config:
+        provider_config["ca_bundle"] = ssl_config["ca_bundle"]

Based on learnings: generate and run are intentionally separate CLI subcommands, and run --mode job expects artifacts created by a prior generate invocation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cli/mtv_api_tests/common.py` around lines 649 - 692, build_providers_json
currently omits the vSphere TLS trust settings, causing artifacts from generate
to lose verify_ssl/ca_bundle; update the build_providers_json signature to
accept optional verify_ssl (bool | None) and ca_bundle (str | None) parameters
and, inside the function, add those keys to provider_config (e.g.,
provider_config["verify_ssl"] = verify_ssl and provider_config["ca_bundle"] =
ca_bundle) only when they are not None so the serialized provider entry
preserves the TLS settings used during generate; reference build_providers_json
and provider_config when making this change.
cli/mtv_api_tests/run.py (1)

240-248: ⚠️ Potential issue | 🟠 Major

HIGH: Only treat job/... unchanged as a stale rerun.

This applies Namespace, Secret, and Job together. The current substring check bails out on any unchanged, so a valid result like namespace/... unchanged plus job.batch/... created still exits 1 after the Job was actually created.

🛠️ Narrow the check to Job status lines
-    combined_output = (result.stdout or "") + (result.stderr or "")
-    if "unchanged" in combined_output:
+    combined_output = (result.stdout or "") + "\n" + (result.stderr or "")
+    job_status_lines = [
+        line.strip().lower()
+        for line in combined_output.splitlines()
+        if line.lstrip().startswith(("job/", "job.batch/"))
+    ]
+    if any(line.endswith(" unchanged") for line in job_status_lines):
         console.print(
             "[yellow]Warning: Job already exists and was not modified. "
             "A completed Job will not re-run.[/yellow]\n"
             f"  [dim]Delete first:[/dim] oc delete -f {job_yaml_path}"
         )
Can `oc apply -f` on a multi-document manifest report `namespace/... unchanged` and `job.batch/... created` in the same invocation, and does reapplying an existing Kubernetes Job leave the Job unchanged instead of starting a new run?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cli/mtv_api_tests/run.py` around lines 240 - 248, The current check inspects
combined_output for the substring "unchanged" and exits even if that "unchanged"
refers to Namespace or Secret; update the logic around combined_output (used
where job_yaml_path is referenced) to only treat a stale rerun when a Job line
reports unchanged — e.g., split combined_output into lines and use a regex or
string match to detect lines containing a Job resource (match "job." or
"job.batch/" or lines beginning with "job" plus " unchanged" on the same line)
and only call console.print and raise typer.Exit(code=1) when such a
Job-specific unchanged line is found. Ensure other "unchanged" lines do not
trigger the exit.
cli/mtv_api_tests/generate.py (2)

245-256: ⚠️ Potential issue | 🟠 Major

HIGH: the kubeconfig flow still drops TLS settings from discovery.

Here job_creds is rebuilt from prompt values, but the source dict in the kubeconfig branch intentionally contains only kubeconfig. That makes the verify_ssl / ca_bundle copies on Lines 253-256 effectively dead code for the path that needs them most, so the generated provider and Job can stop matching the cluster TLS settings that discovery just used. Derive these values from ocp_client.configuration or prompt for them in this branch. For feat: Add test configuration and execution tool, this is a functional gap on self-signed/custom-CA clusters.

Based on learnings, get_ocp_credentials() intentionally returns only {"kubeconfig": kubeconfig} when KUBECONFIG is set, so callers that need explicit TLS fields must derive or prompt them locally.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cli/mtv_api_tests/generate.py` around lines 245 - 256, The kubeconfig branch
rebuilds job_creds from prompts but never pulls TLS settings, so
verify_ssl/ca_bundle are missing for KUBECONFIG flows; update the branch that
creates job_creds (the block using Prompt.ask and variable job_creds) to
populate verify_ssl and ca_bundle by reading ocp_client.configuration (e.g.,
ocp_client.configuration.verify_ssl and ocp_client.configuration.ssl_ca_cert or
equivalent) or, if those aren’t available, prompt the user for those values and
then add them into job_creds so the generated provider/Job preserves the cluster
TLS settings used by discovery.

58-62: ⚠️ Potential issue | 🟠 Major

HIGH: cleanup can still replace the real wizard failure.

If the with _vsphere_session(...) body raises and disconnect_vsphere(si) also raises here, Python will surface the disconnect error and discard the original discovery/selection failure. Handle disconnect errors separately during unwind—log/suppress them only when another exception is already active, and re-raise them on the clean path.

#!/bin/bash
python - <<'PY'
from contextlib import contextmanager

`@contextmanager`
def cm():
    try:
        yield
    finally:
        raise RuntimeError("cleanup")

try:
    with cm():
        raise ValueError("body")
except Exception as exc:
    print(type(exc).__name__, exc)
PY

rg -n -A12 -B4 '^def disconnect_vsphere\b' cli/mtv_api_tests/common.py

Expected result: the Python snippet prints RuntimeError cleanup, showing that a cleanup exception replaces the body exception; if disconnect_vsphere() has no internal guard, the same masking risk exists here.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cli/mtv_api_tests/generate.py` around lines 58 - 62, The finally block in the
_vsphere_session context manager currently calls disconnect_vsphere(si) directly
which can mask an exception from the with-body if disconnect_vsphere also
raises; wrap the disconnect in a try/except inside the finally, detect whether
an exception is already active (via sys.exc_info() or sys.exc_info()[0]), and if
there is an active exception log/suppress the disconnect error (use the module
logger or logging.exception) but do not re-raise it; if there is no active
exception, re-raise the disconnect error so cleanup failures surface normally —
apply this change around the disconnect_vsphere(si) call that follows
connect_vsphere(host, user, password, ssl_config=ssl_config) and yield si.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cli/mtv_api_tests/common.py`:
- Around line 405-419: Exceptions from creating ssl_context (when verify_ssl and
ca_bundle are set) occur outside the try/except, bypassing the ConnectionError
handling; move the CA bundle loading and ssl_context creation into the same
guarded block that calls SmartConnect so any PEM/read errors are caught and
wrapped. Specifically, inside the block that uses console.status and the
try/except around SmartConnect, attempt to create ssl_context (using
ssl.create_default_context with cafile=ca_bundle) and set
connect_kwargs["sslContext"]=ssl_context there, keeping the existing branch that
sets connect_kwargs["disableSslCertValidation"]=True when not verify_ssl; ensure
you still catch vim.fault.InvalidLogin and general Exception and raise
ConnectionError with the host context.
- Around line 559-579: The code incorrectly treats kubeconfig-only auth as
insecure because verify_ssl is derived from creds.get before branching; move the
verify_ssl logic so it only applies when the non-kubeconfig branch is taken:
compute verify_ssl and ca_bundle and call
urllib3.disable_warnings/InsecureRequestWarning and console.print only inside
the else branch where get_client(...) is called with host/username/password, and
set KUBERNETES_CLIENT_CA_BUNDLE only in that same branch when verify_ssl and
ca_bundle are truthy; keep the kubeconfig branch (the "kubeconfig" in creds
path) untouched so it does not suppress warnings or print the insecure message.

In `@cli/mtv_api_tests/generate.py`:
- Around line 111-114: The wizard currently allows selecting any VM via
discover_vms() / select_from_list() but only collects and persists Linux guest
credentials (see selected_vm["guest_os"], credential-collection block at lines
164-166 and persistence at 305-313), so a Windows VM selection will produce an
unusable config; either restrict the choices in select_from_list() to Linux
guests (filter discover_vms() results where guest_os indicates Linux) or add
branching after selection on selected_vm["guest_os"] to (a) prompt for
Windows-specific credentials when guest_os is Windows, (b) persist the
corresponding auth fields in the same code path that now writes Linux auth, and
(c) if an unsupported OS is selected, fail fast with a clear error instead of
accepting it.

In `@cli/mtv_api_tests/run.py`:
- Around line 110-123: The docstring for _get_cluster_config is missing a Raises
section; update its docstring to include a Raises: entry that documents that
typer.Exit is raised when provider credentials are incomplete (e.g., missing
username/password) so callers and readers know this behavior; ensure the Raises
section uses the same docstring style as Args/Returns and mentions typer.Exit
and the condition under which it is raised.
- Around line 138-145: The provider-selected TLS choice is only passed via the
test-case flag (--tc=insecure_verify_skip) while the preserved environment can
still contain CLUSTER_VERIFY_SSL which utilities/utils.py:get_cluster_client()
reads first; update the local run environment in _run_local() (the env_vars dict
built alongside tc_args in _get_cluster_config()/run.py) to explicitly set
CLUSTER_VERIFY_SSL to the provider-derived value (or remove/unset it) based on
the computed verify_ssl_bool so the selected provider cannot be silently
overridden by ambient env.

---

Duplicate comments:
In `@cli/mtv_api_tests/common.py`:
- Around line 649-692: build_providers_json currently omits the vSphere TLS
trust settings, causing artifacts from generate to lose verify_ssl/ca_bundle;
update the build_providers_json signature to accept optional verify_ssl (bool |
None) and ca_bundle (str | None) parameters and, inside the function, add those
keys to provider_config (e.g., provider_config["verify_ssl"] = verify_ssl and
provider_config["ca_bundle"] = ca_bundle) only when they are not None so the
serialized provider entry preserves the TLS settings used during generate;
reference build_providers_json and provider_config when making this change.

In `@cli/mtv_api_tests/generate.py`:
- Around line 245-256: The kubeconfig branch rebuilds job_creds from prompts but
never pulls TLS settings, so verify_ssl/ca_bundle are missing for KUBECONFIG
flows; update the branch that creates job_creds (the block using Prompt.ask and
variable job_creds) to populate verify_ssl and ca_bundle by reading
ocp_client.configuration (e.g., ocp_client.configuration.verify_ssl and
ocp_client.configuration.ssl_ca_cert or equivalent) or, if those aren’t
available, prompt the user for those values and then add them into job_creds so
the generated provider/Job preserves the cluster TLS settings used by discovery.
- Around line 58-62: The finally block in the _vsphere_session context manager
currently calls disconnect_vsphere(si) directly which can mask an exception from
the with-body if disconnect_vsphere also raises; wrap the disconnect in a
try/except inside the finally, detect whether an exception is already active
(via sys.exc_info() or sys.exc_info()[0]), and if there is an active exception
log/suppress the disconnect error (use the module logger or logging.exception)
but do not re-raise it; if there is no active exception, re-raise the disconnect
error so cleanup failures surface normally — apply this change around the
disconnect_vsphere(si) call that follows connect_vsphere(host, user, password,
ssl_config=ssl_config) and yield si.

In `@cli/mtv_api_tests/run.py`:
- Around line 240-248: The current check inspects combined_output for the
substring "unchanged" and exits even if that "unchanged" refers to Namespace or
Secret; update the logic around combined_output (used where job_yaml_path is
referenced) to only treat a stale rerun when a Job line reports unchanged —
e.g., split combined_output into lines and use a regex or string match to detect
lines containing a Job resource (match "job." or "job.batch/" or lines beginning
with "job" plus " unchanged" on the same line) and only call console.print and
raise typer.Exit(code=1) when such a Job-specific unchanged line is found.
Ensure other "unchanged" lines do not trigger the exit.

In `@utilities/utils.py`:
- Around line 633-635: The CLUSTER_VERIFY_SSL environment value is not
normalized before checking, so update the logic that reads verify_ssl_env (from
os.environ.get("CLUSTER_VERIFY_SSL")) to call strip() before lowercasing and
evaluating; specifically, normalize verify_ssl_env = verify_ssl_env.strip() (or
combine .strip().lower()) then test membership against ("true","1","yes") when
setting insecure_verify_skip so values like "true " or "yes\n" are handled
correctly.
🪄 Autofix (Beta)

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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 48ad26f2-ad3f-4fd1-bc70-453d709f0b42

📥 Commits

Reviewing files that changed from the base of the PR and between 76d1923 and 7e7601e.

📒 Files selected for processing (4)
  • cli/mtv_api_tests/common.py
  • cli/mtv_api_tests/generate.py
  • cli/mtv_api_tests/run.py
  • utilities/utils.py

Comment thread cli/mtv_api_tests/common.py Resolved
Comment thread cli/mtv_api_tests/common.py Resolved
Comment thread cli/mtv_api_tests/generate.py Resolved
Comment thread cli/mtv_api_tests/run.py Resolved
Comment thread cli/mtv_api_tests/run.py Resolved
@rgolangh

Copy link
Copy Markdown
Contributor Author

@coderabbitai[bot]

The following review comments were reviewed and a decision was made:

utilities/utils.py:633 (duplicate) — HIGH: Normalize CLUSTER_VERIFY_SSL before evaluating it.

Skipped: Skipped: diminishing returns — re-raised variants of previously reviewed items.

cli/mtv_api_tests/generate.py:199 (duplicate) — HIGH: Fix the job_creds type contract.

Skipped: Skipped: diminishing returns — re-raised variants of previously reviewed items.

cli/mtv_api_tests/common.py:782 (duplicate) — HIGH: Reject non-object .providers.json roots before mutating them.

Skipped: Skipped: diminishing returns — re-raised variants of previously reviewed items.

cli/mtv_api_tests/common.py:401 (duplicate) — HIGH: This regresses the vSphere TLS hardening.

Skipped: Skipped: diminishing returns — re-raised variants of previously reviewed items.

cli/mtv_api_tests/common.py:649 (duplicate) — HIGH: The feat: Add test configuration and execution tool handoff still drops vSphere TLS state.

Skipped: Skipped: diminishing returns — re-raised variants of previously reviewed items.

utilities/utils.py:633 (duplicate) — HIGH: Normalize CLUSTER_VERIFY_SSL with strip() before evaluating.

Skipped: Skipped: diminishing returns — re-raised variants of previously reviewed items.

cli/mtv_api_tests/common.py:649 (duplicate) — HIGH: The generated vSphere provider drops the TLS settings collected during generate.

Skipped: Skipped: diminishing returns — re-raised variants of previously reviewed items.

cli/mtv_api_tests/run.py:240 (duplicate) — HIGH: Only treat job/... unchanged as a stale rerun.

Skipped: Skipped: diminishing returns — re-raised variants of previously reviewed items.

cli/mtv_api_tests/generate.py:245 (duplicate) — HIGH: the kubeconfig flow still drops TLS settings from discovery.

Skipped: Skipped: diminishing returns — re-raised variants of previously reviewed items.

cli/mtv_api_tests/generate.py:58 (duplicate) — HIGH: cleanup can still replace the real wizard failure.

Skipped: Skipped: diminishing returns — re-raised variants of previously reviewed items.

coderabbitai[bot]
coderabbitai Bot previously approved these changes Mar 25, 2026
@coderabbitai

coderabbitai Bot commented Mar 25, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

@rgolangh: Understood — all 10 items acknowledged as skipped. Recording the new design decisions for future reviews:


✏️ Learnings added
Learnt from: rgolangh
URL: https://github.com/RedHatQE/mtv-api-tests/pull/321

Timestamp: 2026-03-25T15:43:26.685Z
Learning: In RedHatQE/mtv-api-tests (cli/mtv_api_tests/common.py, cli/mtv_api_tests/generate.py): The generated vSphere provider config in `build_vsphere_provider_json()` intentionally does NOT forward vSphere TLS settings (verify_ssl, ca_bundle) collected during the `generate` wizard. This is a known limitation/deferred TODO, consistent with the same pattern for `get_cluster_client()` not consuming ca_bundle. Do NOT flag vSphere TLS state being dropped in the provider handoff as a bug or regression.

Learnt from: rgolangh
URL: https://github.com/RedHatQE/mtv-api-tests/pull/321

Timestamp: 2026-03-25T15:43:26.685Z
Learning: In RedHatQE/mtv-api-tests (cli/mtv_api_tests/generate.py): The kubeconfig flow in `_gather_ocp_storage_class()` intentionally does not carry forward all TLS settings (e.g., ca_bundle, verify_ssl) from the discovery phase into the generated job credentials. This is a known limitation deferred to a future PR. Do NOT flag the kubeconfig path dropping TLS settings as a bug.

Learnt from: rgolangh
URL: https://github.com/RedHatQE/mtv-api-tests/pull/321

Timestamp: 2026-03-25T15:43:26.685Z
Learning: In RedHatQE/mtv-api-tests (cli/mtv_api_tests/run.py): The "unchanged" string detection in job mode (checking whether `oc apply` output contains "unchanged") intentionally checks broadly across all resource types — not just `job/... unchanged`. This behavior is acceptable for the current implementation. Do NOT suggest narrowing the detection to only `job/...` patterns.

Learnt from: rgolangh
URL: https://github.com/RedHatQE/mtv-api-tests/pull/321

Timestamp: 2026-03-25T15:43:26.685Z
Learning: In RedHatQE/mtv-api-tests (cli/mtv_api_tests/common.py): The `write_providers_json()` / provider merge logic intentionally does NOT pre-validate that the `.providers.json` root is a JSON object before mutating it. A non-object root is treated as a decode error by the existing JSON-decode warning path. Do NOT flag missing explicit root-type validation as a bug.

Learnt from: rgolangh
URL: https://github.com/RedHatQE/mtv-api-tests/pull/321

Timestamp: 2026-03-25T15:43:26.685Z
Learning: In RedHatQE/mtv-api-tests (cli/mtv_api_tests/generate.py): The `job_creds` dict returned by the OCP flow in `generate_command()` may have varying shapes (kubeconfig-only vs full credentials). Callers intentionally handle the variability rather than enforcing a strict type contract at the return site. Do NOT flag the `job_creds` type contract as a bug.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: rgolangh
Repo: RedHatQE/mtv-api-tests PR: 321
File: cli/mtv_api_tests/run.py:0-0
Timestamp: 2026-03-23T14:28:32.199Z
Learning: In repository RedHatQE/mtv-api-tests (cli/mtv_api_tests/run.py): `generate` and `run` are intentionally separate CLI subcommands. `run --mode job` expects the Job YAML manifest to already exist (created by a prior `generate` invocation). Do not suggest or add auto-generation fallbacks inside the `run` command's job mode path.

Learnt from: shellymiron
Repo: RedHatQE/mtv-api-tests PR: 239
File: README.md:270-331
Timestamp: 2026-01-12T16:24:42.450Z
Learning: Repository RedHatQE/mtv-api-tests: The OpenShift Job templates in README.md and docs/how-to-run-copyoffload-tests.md are intentionally duplicated with identical sections because the files are meant to be different but share common Job template structure; do not suggest consolidating them into a single YAML manifest.

Learnt from: rgolangh
Repo: RedHatQE/mtv-api-tests PR: 321
File: utilities/utils.py:604-640
Timestamp: 2026-03-25T14:26:43.703Z
Learning: In RedHatQE/mtv-api-tests (utilities/utils.py, cli/mtv_api_tests/run.py): The `ca_bundle` stored by `generate.py` is intentionally NOT forwarded to `get_cluster_client()` in local run mode. `get_cluster_client()` does not accept or consume a CA bundle argument. This is a known limitation/TODO deferred to a future PR. Do NOT flag this as a bug or suggest adding ca_bundle support to `get_cluster_client()` or `_get_cluster_config()` in run.py.

Learnt from: shellymiron
Repo: RedHatQE/mtv-api-tests PR: 239
File: README.md:122-134
Timestamp: 2026-01-13T09:53:56.701Z
Learning: Repository RedHatQE/mtv-api-tests: The JSON examples in README.md intentionally contain inline `# pragma: allowlist secret` comments (which make them invalid JSON) because pre-commit hooks require these pragmas to pass secret scanning. Lines 114-116 prominently warn users not to copy these comments to their actual `.providers.json` files. This is a deliberate design decision to balance pre-commit requirements with documentation needs. Do not suggest removing these pragmas or flag them as invalid JSON.

Learnt from: rgolangh
Repo: RedHatQE/mtv-api-tests PR: 321
File: cli/mtv_api_tests/common.py:352-355
Timestamp: 2026-03-25T12:40:27.030Z
Learning: In RedHatQE/mtv-api-tests (cli/mtv_api_tests/common.py): `get_ocp_credentials()` intentionally returns only `{"kubeconfig": kubeconfig}` when `KUBECONFIG` is set, without merging `CLUSTER_HOST`, `CLUSTER_USERNAME`, `CLUSTER_PASSWORD`, `CLUSTER_VERIFY_SSL`, or `CLUSTER_CA_BUNDLE`. This is by design — callers that need explicit credentials (e.g., `generate.py:_gather_ocp_storage_class`) are responsible for detecting and prompting for the missing fields. Do NOT flag this early-return pattern as discarding auth/TLS settings.

Learnt from: myakove
Repo: RedHatQE/mtv-api-tests PR: 328
File: tests/copyoffload/conftest.py:240-307
Timestamp: 2026-03-11T09:45:35.130Z
Learning: In repository RedHatQE/mtv-api-tests (tests/copyoffload/conftest.py): The `copyoffload_ssh_key` fixture intentionally uses `scope="session"` even under pytest-xdist (--dist=loadscope). `install_ssh_key_on_esxi` and `remove_ssh_key_from_esxi` in utilities/esxi.py are idempotent — each checks whether the operation is already done before proceeding — so concurrent worker setup/teardown is safe. Do NOT flag session scope on this fixture as a concurrency hazard or suggest refactoring to worker-unique keys.

Learnt from: elsapassaro
Repo: RedHatQE/mtv-api-tests PR: 285
File: tests/tests_config/config.py:373-388
Timestamp: 2026-01-30T14:43:00.204Z
Learning: In repository RedHatQE/mtv-api-tests, for test configurations in tests/tests_config/config.py: VM entries within a test should use the same base name (e.g., "xcopy-template-test") and let the framework auto-generate unique names with UUIDs when cloning is enabled. Explicit `clone_name` fields should only be used for tests that specifically verify operator behavior with non-conforming or special VM names (e.g., test_copyoffload_nonconforming_name_migration).

Learnt from: krcmarik
Repo: RedHatQE/mtv-api-tests PR: 314
File: utilities/vmware_guest_operations.py:103-115
Timestamp: 2026-03-12T00:10:06.804Z
Learning: In RedHatQE/mtv-api-tests (utilities/vmware_guest_operations.py): In `run_command_in_vmware_guest`, NOT fetching the guest temp-file output when `process_info.exitCode != 0` is intentional. The exit code and VM name in `GuestCommandError` are considered sufficient for debugging. Fetching the failed command's output would add an extra Guest Operations API call on an already-failed, rare code path, which the author considers unnecessary complexity. Do not flag this as a deficiency.

Learnt from: MiriSafra
Repo: RedHatQE/mtv-api-tests PR: 0
File: :0-0
Timestamp: 2026-03-04T15:47:50.626Z
Learning: In repository RedHatQE/mtv-api-tests: In shared-disk migration tests (utilities/shared_disk.py), the unmount/remount sequence in verify_shared_disk_data is for cache flushing purposes (forcing buffer flush between writes), NOT to avoid concurrent filesystem access. Shared disks use RWX PVCs and are designed for multi-writer access; SCSI-level coordination handles concurrent mount safety. Do not flag this as a concurrency bug.

Learnt from: prabinovRedhat
Repo: RedHatQE/mtv-api-tests PR: 358
File: utilities/copyoffload_migration.py:280-281
Timestamp: 2026-03-22T21:14:33.207Z
Learning: In repository RedHatQE/mtv-api-tests (utilities/copyoffload_migration.py): When Kubernetes Pod objects are fetched via `Pod.get(label_selector=...)`, `pod.instance.metadata.labels` is guaranteed to be non-null because the label selector query itself proves the labels map exists and is populated. Do NOT flag missing defensive guards (e.g., `pod.instance.metadata.labels or {}`) on pods retrieved this way — it is unnecessary noise. This follows the same fail-fast philosophy used for other guaranteed Kubernetes structural fields in the repo.

Learnt from: MiriSafra
Repo: RedHatQE/mtv-api-tests PR: 0
File: :0-0
Timestamp: 2026-03-04T16:25:43.654Z
Learning: In repository RedHatQE/mtv-api-tests: In libs/providers/vmware.py relink_shared_disks(), defensive vm.config guards (e.g., `if vm.config is None`) are intentionally absent. Unlike vm_dict() which queries arbitrary external VMs in unknown states, relink_shared_disks() operates exclusively on VMs freshly cloned by the test fixture. Missing vm.config on a just-cloned VM indicates catastrophic infrastructure failure and should raise AttributeError loudly (fail-fast), not be silently swallowed.

Learnt from: krcmarik
Repo: RedHatQE/mtv-api-tests PR: 247
File: libs/providers/openshift.py:185-190
Timestamp: 2026-01-29T11:33:17.513Z
Learning: In repository RedHatQE/mtv-api-tests, for Kubernetes VirtualMachine CRDs in libs/providers/openshift.py: template.metadata and template.spec are guaranteed structural elements that cannot be None per the Kubernetes API schema. Defensive None guards on these fields are unnecessary but harmless. Line 178 accesses template.spec.domain directly without checks, confirming this guarantee.

Learnt from: prabinovRedhat
Repo: RedHatQE/mtv-api-tests PR: 317
File: utilities/copyoffload_migration.py:124-129
Timestamp: 2026-03-08T12:36:28.550Z
Learning: In repository RedHatQE/mtv-api-tests (utilities/copyoffload_migration.py): The `_get_ip()` helper inside `wait_for_cloud_init` intentionally takes the first IP address returned by `source_provider.vm_dict()` without filtering for routability. In the project's vSphere environments, `vm_dict()` only returns routable IPv4 addresses — link-local or IPv6 addresses have never been observed in practice. The outer `TimeoutSampler` retry loop on `_check_file` provides a safety net if an unusable address is ever returned. Do not flag missing IP-routability filtering on this function.

Learnt from: rgolangh
Repo: RedHatQE/mtv-api-tests PR: 321
File: cli/mtv_api_tests/__init__.py:39-62
Timestamp: 2026-03-23T14:28:39.174Z
Learning: In repository RedHatQE/mtv-api-tests: `# noqa: M511` comments on `typer.Option(...)` default argument lines are required and intentional. M511 is the `flake8-mutable` rule (not McCabe complexity), actively selected via `.flake8` (`select=M511`) and enforced by the pre-commit `flake8` hook with `additional_dependencies: [flake8-mutable]`. Do NOT suggest removing these suppressions.

Learnt from: rgolangh
Repo: RedHatQE/mtv-api-tests PR: 321
File: utilities/utils.py:633-639
Timestamp: 2026-03-25T12:40:42.535Z
Learning: In repository RedHatQE/mtv-api-tests (utilities/utils.py): The `CLUSTER_VERIFY_SSL` env var parsing in `get_cluster_client()` intentionally uses lenient boolean parsing — any value that is NOT in ("true", "1", "yes") (case-insensitive, after strip) resolves to `insecure_verify_skip=True`. Unknown, typo, or empty values default to insecure rather than raising a ValueError. This matches common boolean env-var parsing conventions and is a deliberate design decision. Do NOT suggest adding strict validation or raising ValueError for unrecognized CLUSTER_VERIFY_SSL values.

Learnt from: krcmarik
Repo: RedHatQE/mtv-api-tests PR: 270
File: libs/providers/rhv.py:229-230
Timestamp: 2026-01-22T02:37:52.606Z
Learning: In repository RedHatQE/mtv-api-tests, for RHV provider in libs/providers/rhv.py, the snapshot enum fields `snapshot.snapshot_status` and `snapshot.snapshot_type` are intentionally accessed without None guards (using `.value` directly). This crash-on-None behavior is by design: these fields are reliably populated by the oVirt SDK in practice, and any None value indicates an SDK/API contract violation that should fail fast rather than be silently masked. Downstream validation doesn't use these fields, so defensive handling is unnecessary.

Learnt from: krcmarik
Repo: RedHatQE/mtv-api-tests PR: 314
File: conftest.py:1027-1046
Timestamp: 2026-03-12T00:24:31.956Z
Learning: In repository RedHatQE/mtv-api-tests (conftest.py): The detect_vmware_ip_origins_via_guest_ops call in prepared_plan is intentionally executed for ALL Linux/vSphere VMs, regardless of whether the plan has preserve_static_ips set. This is because Linux static IP preservation happens natively on VMware, making IP-origin detection always relevant. The unconditional `except ValueError: raise` (failing hard on ValueError for all Linux vSphere VMs) is intentional — missing Guest Ops credentials or similar prerequisites are a hard requirement, not optional. Do not suggest gating this block or the ValueError re-raise behind plan.get("preserve_static_ips").

Learnt from: krcmarik
Repo: RedHatQE/mtv-api-tests PR: 314
File: utilities/vmware_guest_operations.py:244-249
Timestamp: 2026-03-11T23:33:27.611Z
Learning: In repository RedHatQE/mtv-api-tests (utilities/vmware_guest_operations.py): `detect_vmware_ip_origins_via_guest_ops` is an intentional best-effort workaround for open-vm-tools issue `#694`. When `nmcli` returns no parseable data (e.g., NetworkManager not installed or empty output), logging a warning and returning normally is correct — the migration must not fail due to this optional IP-origin detection step. Unresolved `is_static_ip=None` values are the default state and are handled downstream by the caller and post-migration checks. Do not flag the silent-return path or the absence of a "still-unknown origins" check in this function.

Learnt from: prabinovRedhat
Repo: RedHatQE/mtv-api-tests PR: 369
File: libs/providers/vmware.py:813-859
Timestamp: 2026-03-23T16:28:09.390Z
Learning: In repository RedHatQE/mtv-api-tests (libs/providers/vmware.py): `VMWareProvider.change_disk_mode()` intentionally does NOT validate the `disk_mode` string against an allowlist before calling `vm.ReconfigVM_Task`. vSphere's API already rejects invalid disk modes with a clear fault. Maintaining an allowlist would create a vSphere-version-coupled maintenance burden. The method is called from a single fixture with a hardcoded value, so the risk is negligible. Do not suggest adding an allowlist or pre-validation for `disk_mode` in this method.

Learnt from: myakove
Repo: RedHatQE/mtv-api-tests PR: 295
File: conftest.py:353-355
Timestamp: 2026-02-13T11:43:13.262Z
Learning: When reviewing PRs in the RedHatQE/mtv-api-tests repository, always explicitly acknowledge and analyze commit messages as a key part of the review process, mentioning them prominently in review comments when relevant to the code changes being discussed.

Learnt from: krcmarik
Repo: RedHatQE/mtv-api-tests PR: 270
File: libs/base_provider.py:104-126
Timestamp: 2026-01-22T01:26:50.214Z
Learning: In repository RedHatQE/mtv-api-tests, abstract methods in BaseProvider (libs/base_provider.py) consistently include explicit `pass` statements after their body/docstring to maintain uniform code style across all abstract method definitions, even though Ruff PIE790 flags them as unnecessary.

Learnt from: MiriSafra
Repo: RedHatQE/mtv-api-tests PR: 0
File: :0-0
Timestamp: 2026-03-02T14:03:22.214Z
Learning: In repository RedHatQE/mtv-api-tests: Test methods (especially in test_*.py files) should remain explicit and self-contained rather than extracting duplication into helper functions. Each test step should be clearly visible in the test method itself for readability, following pytest best practices where test intent should be immediately obvious.

Learnt from: MiriSafra
Repo: RedHatQE/mtv-api-tests PR: 0
File: :0-0
Timestamp: 2026-03-02T14:03:22.214Z
Learning: In repository RedHatQE/mtv-api-tests: PVC name filtering using substring match (e.g., `plan_name in pvc.name`) is safe when plan names include UUID suffixes (e.g., `auto-wn5y-source-vsphere-7-0-3-5buv-cold`) because MTV creates PVCs as `plan-{plan_name}-{random}`, making collision risk effectively zero due to the UUID-based naming scheme.

Learnt from: rgolangh
Repo: RedHatQE/mtv-api-tests PR: 321
File: Dockerfile:59-63
Timestamp: 2026-03-25T11:24:47.700Z
Learning: In RedHatQE/mtv-api-tests Dockerfile (PR `#321` multi-stage build): the runtime stage intentionally installs only `python3`, `libxml2`, and `openssl` (no build toolchain: no gcc, clang, libxml2-devel, libcurl-devel, openssl-devel, git, python3-pip). All Python extensions are pre-compiled in the builder stage via `uv sync --locked` with `UV_COMPILE_BYTECODE=1`, and the full `/app` directory is copied to the runtime stage. Do NOT suggest adding toolchain or -devel packages to the runtime stage of this multi-stage Dockerfile.

Learnt from: MiriSafra
Repo: RedHatQE/mtv-api-tests PR: 0
File: :0-0
Timestamp: 2026-03-04T15:47:50.626Z
Learning: In repository RedHatQE/mtv-api-tests: In the shared-disk migration feature (libs/providers/vmware.py relink_shared_disks), owner VM selection using positions[0] is correct and intentional. The test config in tests/tests_config/config.py explicitly orders VMs so that the first VM (migrate_shared_disks=true/owner) always appears first. Adding flag-based owner selection would be redundant since config ordering already encodes the semantics.

Comment thread utilities/utils.py Resolved
Comment thread utilities/utils.py Resolved
Comment thread utilities/utils.py Resolved
Comment thread pyproject.toml Resolved

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cli/mtv_api_tests/common.py`:
- Around line 269-308: The _get_ssl_config function currently raises typer.Exit
when a provided CA bundle path does not exist; update its docstring to include a
Raises: section documenting that typer.Exit is raised on an invalid/nonexistent
CA bundle (include the condition: when verify_ssl is true and the specified CA
bundle path is not found), keeping the existing Args and Returns sections intact
so callers and tests can rely on the explicit exit behavior.
- Around line 372-376: The explicit-credentials branch currently reads
CLUSTER_HOST/USERNAME/PASSWORD but then calls
_get_ssl_config("MTV_API_TESTS_CLUSTER"), which ignores CLUSTER_VERIFY_SSL and
CLUSTER_CA_BUNDLE; update the call in that branch to use the CLUSTER prefix
(call _get_ssl_config("CLUSTER")) or merge both prefixes so TLS settings from
CLUSTER_* are honored when host/username/password are supplied; modify the
return construction around host/username/password and _get_ssl_config to ensure
the resulting dict includes the CA/verify values from CLUSTER_VERIFY_SSL and
CLUSTER_CA_BUNDLE.
- Around line 721-729: The provider_config being built in common.py currently
emits "host" and "storage_class" and omits "fqdn", which breaks the documented
provider shape; update the provider_config dict (the variable named
provider_config) to include "fqdn" (set to the cluster hostname value), ensure
"api_url" remains present (pointing to the cluster endpoint), and remove the
"host" and "storage_class" keys so the record matches .providers.json.example
while keeping "type", "version", "username", and "password" intact.
- Around line 401-403: The code currently treats a missing verify_ssl key as
"false" which weakens TLS; update the logic that reads ssl_config (variable
ssl_config and derived verify_ssl/ca_bundle) to treat an omitted verify_ssl as
enabled (true) — e.g. only set verify_ssl to false when the key is present and
explicitly set to "false", otherwise default to true; apply the same fix in the
other occurrences that read ssl_config (the callsites used by connect_vsphere
and the job-generation code that emits cluster_verify_ssl and
--tc=insecure_verify_skip) so missing keys no longer disable certificate
validation.
🪄 Autofix (Beta)

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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 6deb9858-bdc3-4a22-ade9-403f993e1da5

📥 Commits

Reviewing files that changed from the base of the PR and between 7e7601e and d6efb87.

📒 Files selected for processing (3)
  • cli/mtv_api_tests/common.py
  • pyproject.toml
  • utilities/utils.py

Comment thread cli/mtv_api_tests/common.py Resolved
Comment thread cli/mtv_api_tests/common.py Resolved
Comment thread cli/mtv_api_tests/common.py Resolved
Comment thread cli/mtv_api_tests/common.py Resolved

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cli/mtv_api_tests/common.py`:
- Around line 1027-1069: The generated manifest currently creates a unique
namespace and Secret (namespace, secret_name) but never deletes them; update the
YAML assembly where yaml_content is built (and/or the caller that uses
_generate_namespace_name/_build_job_spec) to append an explicit cleanup resource
that removes the namespace and Secret after the test Job completes—for example
add a one-shot Kubernetes Job (e.g., named f"{job_name}-cleanup") that runs a
small container to delete the namespace (or calls the k8s API) once the main Job
finishes, or set appropriate ownerReferences/cleanup logic so the namespace and
Secret are removed; ensure you do not rely on ttlSecondsAfterFinished alone
since it won't remove the namespace/Secret.
- Around line 569-584: The code sets the process-wide env var
KUBERNETES_CLIENT_CA_BUNDLE before calling get_client(), but never restores it,
causing subsequent connect_ocp() calls to inherit the wrong CA bundle; modify
connect_ocp() to save the previous os.environ.get("KUBERNETES_CLIENT_CA_BUNDLE")
value, set the new ca_bundle only for the duration of the get_client(...) call,
and restore the original value in a finally block (clearing the key if it was
previously absent) so that get_client() uses the custom bundle without leaking
it to later calls.
🪄 Autofix (Beta)

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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: a689de11-f10c-46f8-bd35-fe3201513a9d

📥 Commits

Reviewing files that changed from the base of the PR and between d6efb87 and fad891c.

📒 Files selected for processing (1)
  • cli/mtv_api_tests/common.py

Comment thread cli/mtv_api_tests/common.py Resolved
Comment thread cli/mtv_api_tests/common.py Resolved
rgolangh and others added 7 commits March 30, 2026 10:38
Introduce a cli tool mtv-api-tests that automates configuration and test execution.
It discovers vSphere and OpenShift resources via their APIs,
generates `.providers.json` and a self-contained
`mtv-api-tests-manifests.yaml`
(Namespace + Secret + Job), and can run tests locally or as an OpenShift Job.

Generate Configuration

```bash
uv run mtv-api-tests generate
```

The wizard will:

1. Prompt for vSphere credentials (or read from `VSPHERE_HOST`, `VSPHERE_USERNAME`, `VSPHERE_PASSWORD`)
2. Connect to vSphere and let you select datastores, a test VM, and an ESXi host
3. Prompt for storage vendor and credentials
4. Connect to OpenShift and let you select a storage class
5. Ask which test category to run (`all`, `copyoffload`, `tier0`, `warm`, `remote`)
6. Write `.providers.json` and `mtv-api-tests-manifests.yaml`

Run Tests
```bash
uv run mtv-api-tests run --mode local

uv run mtv-api-tests run --mode job
```

Signed-off-by: Roy Golan <rgolan@redhat.com>

fix: address PR review feedback - security, validation, and config improvements

- Atomic file writes in _write_secret_file
- Quote shell variables in Job manifest and Dockerfile
- Validate ocp_creds and provider entries early
- Store CA bundle PEM contents instead of path
- Add env var fallbacks for cluster credentials
- Fail on incomplete OCP provider instead of silent fallback
- Pin uv version in Dockerfile
- Add .env to .dockerignore
- Fix .gitignore .claude/ negation patterns
- Replace os.write() with os.fdopen() in _write_secret_file() to prevent partial writes
- Split generate_job_yaml() into focused helpers
- Add --tc=cluster_host and --tc=insecure_verify_skip to Job pytest command
- Chain typer.Exit exceptions with 'from exc' in generate.py
- Fix CLUSTER_VERIFY_SSL env var priority over py_config default
- Change .claude/ to .claude/* in .gitignore for proper allowlist
- Use neutral wording for unchanged provider messages
- Only set insecure_verify_skip when verify_ssl is explicitly configured
- Multi-stage Dockerfile to remove build-only packages from runtime image
get_cluster_client() only treated the exact string "true" as
enabling SSL verification. Now accepts "true", "1", and "yes"
(case-insensitive), matching the CLI's parsing logic.
- Ensure parent directory exists in _write_secret_file()
- Replace manual try/finally with context manager for vSphere session
- Show fully reproducible command in Next Steps panel
- Detect unchanged Job on oc apply and fail with guidance
- Move .providers.json validation before user prompts
- Replace isinstance(DynamicClient) with None check
- Revert client check to isinstance(client, DynamicClient) for FakeClient support
- Switch build backend from setuptools to hatchling
- Revert CLUSTER_* env var prefix change (out of scope for this PR)
- Default verify_ssl to "true" instead of "false" (secure-by-default)
- Fix leftover MTV_API_TESTS_CLUSTER prefix to CLUSTER
- Add Raises section to _get_ssl_config() docstring
@rgolangh

Copy link
Copy Markdown
Contributor Author

/verified

@myakove

myakove commented Mar 30, 2026

Copy link
Copy Markdown
Collaborator

/approve

@myakove

myakove commented Mar 30, 2026

Copy link
Copy Markdown
Collaborator

@rgolangh
you may want to cherry-pick it into v2.11 v2.10

@redhat-qe-bot

Copy link
Copy Markdown

New container for ghcr.io/redhatqe/mtv-api-tests:latest published

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.

6 participants