Skip to content

feat(kubernetes): propagate remote-accessible LINSTOR StorageClasses to tenant clusters - #2872

Merged
Aleksei Sviridkin (lexfrei) merged 4 commits into
mainfrom
feat/tenant-linstor-storageclasses
Jun 26, 2026
Merged

feat(kubernetes): propagate remote-accessible LINSTOR StorageClasses to tenant clusters#2872
Aleksei Sviridkin (lexfrei) merged 4 commits into
mainfrom
feat/tenant-linstor-storageclasses

Conversation

@myasnikovdaniil

@myasnikovdaniil myasnikovdaniil commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

What this PR does

Tenant Kubernetes clusters previously exposed a single hardcoded kubevirt StorageClass mapped to one infrastructure class (.Values.storageClass). Users running multiple storage tiers on the management cluster could not reach them from inside tenant clusters (#2094, #1424).

This change auto-discovers the management cluster's remote-accessible LINSTOR StorageClasses (provisioner: linstor.csi.linbit.com with allowRemoteVolumeAccess != "false") via Helm lookup and propagates each to tenant clusters under the same name. No Go changes — the kubevirt-csi-driver already supports multiple infra StorageClasses natively.

Behavior

  • packages/apps/kubernetes csi.yaml builds a storageClasses map filtered to remote-accessible LINSTOR classes and passes it to the kubevirt-csi-node child chart.
  • kubevirt-csi-node renders one tenant StorageClass per entry (provisioner: csi.kubevirt.io, infraStorageClassName: <name>, bus: scsi).
  • The class named by .Values.storageClass (default replicated) is marked the tenant default; exactly one default is guaranteed.
  • The legacy kubevirt StorageClass is retained as a non-default alias, so existing tenant PVCs keep binding with no migration.
  • If the lookup returns empty (e.g. dry-run), a non-destructive fallback preserves the legacy replicated + kubevirt mapping — tenant StorageClasses are never wiped on a transient reconcile.

Scope decisions

  • LINSTOR-only: only StorageClasses cozystack ships and controls are auto-propagated. Node-local classes (e.g. local, allowRemoteVolumeAccess: false) are excluded because the backing volume cannot follow a live-migrated worker-node VM. Other provisioners (NFS, etc.) are out of scope.
  • No enforcement flip: infraStorageClassEnforcement is intentionally left at the current AllowAll default. Enabling the allowList would break tenants that have already self-served custom StorageClasses against the management cluster — exactly the users this feature serves. Tightening tenant isolation is left to a separate, discovery-based change.

This is a focused, additive alternative to #2095 (kept open), scoped per the LINSTOR-only / no-breakage decisions above.

Upgrade note

If you previously created a tenant StorageClass named replicated by hand, delete it before upgrading — otherwise the child Helm release hits an ownership conflict adopting the now-managed replicated class.

Tests

  • Helm unit tests for the fallback map (kubernetes) and per-entry rendering + single-default invariant + absence of an enforcement block (kubevirt-csi-node; new Makefile test target so the package is no longer skipped by the runner).
  • e2e (run-kubernetes.sh): asserts replicated propagates as the default kubevirt-CSI class, the kubevirt alias is retained, and node-local local is filtered out.

Release note

feat(kubernetes): auto-propagate remote-accessible LINSTOR StorageClasses from the management cluster to tenant Kubernetes clusters under the same name. The class set by `storageClass` (default `replicated`) becomes the tenant default; the legacy `kubevirt` StorageClass is kept as an alias so existing PVCs are unaffected. Node-local classes (e.g. `local`) are not propagated. If you manually created a tenant StorageClass named `replicated`, delete it before upgrading to avoid a Helm ownership conflict.

Summary by CodeRabbit

  • New Features
    • Remote-accessible LINSTOR StorageClass entries now auto-propagate into tenant clusters using the same name.
    • Tenant-side CSI/KubeVirt StorageClass rendering now supports multiple configurable classes with deterministic default selection.
  • Improvements
    • Enhanced e2e checks to verify StorageClass provisioner, parameters, default annotation, and legacy kubevirt alias compatibility.
  • Tests
    • Added Helm unittest suites covering fallback and explicit storageClass cases.
    • Added kubevirt CSI node rendering tests and a test target.
  • Documentation
    • Clarified storageClass descriptions and upgrade caveats across charts and schemas.

@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

An error occurred during the review process. Please try again later.

📝 Walkthrough

Walkthrough

This PR updates StorageClass propagation for tenant clusters, renders tenant StorageClasses from a map, and adds matching unit and e2e validation. It also refreshes related API, schema, and README wording.

Changes

StorageClass Auto-Propagation

Layer / File(s) Summary
CSI Controller StorageClass Propagation Logic
packages/apps/kubernetes/templates/helmreleases/csi.yaml, packages/apps/kubernetes/tests/csi_test.yaml
Helm template now builds values.storageClasses from LINSTOR StorageClass lookup results, filters by remote access, adds fallback and legacy alias entries, and selects one default. Helm unittest coverage exercises fallback rendering and storageClass: fast behavior.
Tenant StorageClass Rendering & Configuration
packages/system/kubevirt-csi-node/values.yaml, packages/system/kubevirt-csi-node/templates/deploy.yaml, packages/system/kubevirt-csi-node/tests/storageclasses_test.yaml, packages/system/kubevirt-csi-node/Makefile
Tenant values switch to a storageClasses map, the deploy template renders one StorageClass per entry, and tests verify default annotations, infraStorageClassName mapping, and ConfigMap output. A helm unittest target is added.
Documentation & Schema Definition Updates
api/apps/v1alpha1/kubernetes/types.go, packages/apps/kubernetes/README.md, packages/apps/kubernetes/values.schema.json, packages/apps/kubernetes/values.yaml, packages/system/kubernetes-rd/cozyrds/kubernetes.yaml
StorageClass descriptions are updated across API, values, schema, README, and ApplicationDefinition to describe the tenant-cluster default and auto-propagated LINSTOR classes.
End-to-End StorageClass Propagation Verification
hack/e2e-apps/run-kubernetes.sh
Adds e2e checks for propagated tenant StorageClasses, including existence, provisioner, infraStorageClassName, legacy alias, absence of local, and exactly one default entry.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

Suggested reviewers

  • kvaps
  • androndo
  • sircthulhu

Poem

🐰 I hopped through charts and templates bright,
StorageClasses now render just right.
Replicated leads, kubevirt tags along,
Tests and e2e keep it strong.
Linstor classes flow with careful grace—
A tidy hop across the place!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: propagating remote-accessible LINSTOR StorageClasses into tenant clusters.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/tenant-linstor-storageclasses

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.

@github-actions github-actions Bot added area/kubernetes Issues or PRs related to the tenant Kubernetes app kind/feature Categorizes issue or PR as related to a new feature size/L This PR changes 100-499 lines, ignoring generated files labels Jun 11, 2026
@myasnikovdaniil
myasnikovdaniil marked this pull request as ready for review June 11, 2026 07:40
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces automatic propagation of remote-accessible LINSTOR StorageClasses from the management cluster to tenant Kubernetes clusters. By dynamically discovering these classes, it enables users to utilize multiple storage tiers within tenant environments. The implementation ensures backward compatibility by maintaining the legacy 'kubevirt' alias and provides robust fallback mechanisms for dry-run scenarios, while explicitly excluding node-local classes to prevent migration issues.

Highlights

  • StorageClass Auto-Discovery: Implemented auto-discovery of remote-accessible LINSTOR StorageClasses from the management cluster using Helm lookup, propagating them to tenant clusters.
  • Backward Compatibility: Retained the legacy 'kubevirt' StorageClass as a non-default alias to ensure existing tenant PVCs remain functional without migration.
  • Enhanced Testing: Added comprehensive Helm unit tests and e2e verification to ensure correct StorageClass propagation, default selection, and filtering of node-local classes.
New Features

🧠 You can now enable Memory (public preview) to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment Gemini (@gemini-code-assist) Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on Gemini (@gemini-code-assist) comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@dosubot dosubot Bot added area/storage Issues or PRs related to storage (linstor, seaweedfs, bucket, velero, harbor) kind/api-change Categorizes issue or PR as related to adding, removing, or otherwise changing an API labels Jun 11, 2026

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request implements StorageClass propagation from the infrastructure cluster to the tenant cluster, ensuring remote-accessible LINSTOR classes are auto-propagated under the same name while filtering out node-local classes and retaining the legacy 'kubevirt' alias. The changes include updates to Helm templates, schemas, documentation, and new unit and E2E tests. The feedback suggests quoting templated string values in the Helm templates to prevent potential YAML parsing issues.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

kind: StorageClass
metadata:
name: kubevirt
name: {{ $name }}

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.

low

It is a best practice to quote templated string values in Helm charts to prevent potential YAML parsing issues if the value contains special characters or looks like a boolean/number.

  name: {{ $name | quote }}

allowVolumeExpansion: true
parameters:
infraStorageClassName: {{ .Values.storageClass }}
infraStorageClassName: {{ $m.infraStorageClass }}

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.

low

It is a best practice to quote templated string values in Helm charts to prevent potential YAML parsing issues if the value contains special characters or looks like a boolean/number.

  infraStorageClassName: {{ $m.infraStorageClass | quote }}

@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: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/apps/kubernetes/templates/helmreleases/csi.yaml`:
- Line 37: The condition is using the wrong parameter key; update the Helm
template conditional that references $params to check "allowRemoteVolumeAccess"
(without the "linstor.csi.linbit.com/" prefix) instead of
"linstor.csi.linbit.com/allowRemoteVolumeAccess". Specifically, modify the
expression that currently reads (index $params
"linstor.csi.linbit.com/allowRemoteVolumeAccess" | default "true") to use (index
$params "allowRemoteVolumeAccess" | default "true") so the overall if-statement
with $sc.provisioner and the comparison to "false" reflects LINSTOR CSI’s actual
StorageClass parameter name.
🪄 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: CHILL

Plan: Pro

Run ID: 24fcad57-0972-4254-973e-b58e67942905

📥 Commits

Reviewing files that changed from the base of the PR and between 8348f96 and bd65bc4.

📒 Files selected for processing (12)
  • api/apps/v1alpha1/kubernetes/types.go
  • hack/e2e-apps/run-kubernetes.sh
  • packages/apps/kubernetes/README.md
  • packages/apps/kubernetes/templates/helmreleases/csi.yaml
  • packages/apps/kubernetes/tests/csi_test.yaml
  • packages/apps/kubernetes/values.schema.json
  • packages/apps/kubernetes/values.yaml
  • packages/system/kubernetes-rd/cozyrds/kubernetes.yaml
  • packages/system/kubevirt-csi-node/Makefile
  • packages/system/kubevirt-csi-node/templates/deploy.yaml
  • packages/system/kubevirt-csi-node/tests/storageclasses_test.yaml
  • packages/system/kubevirt-csi-node/values.yaml

{{- $out := dict }}
{{- range $sc := (lookup "storage.k8s.io/v1" "StorageClass" "" "").items }}
{{- $params := $sc.parameters | default dict }}
{{- if and (eq $sc.provisioner "linstor.csi.linbit.com") (ne (index $params "linstor.csi.linbit.com/allowRemoteVolumeAccess" | default "true") "false") }}

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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

What is the correct StorageClass parameter name in LINSTOR CSI driver to control remote volume access? Check LINSTOR CSI driver documentation for allowRemoteVolumeAccess parameter.

💡 Result:

The correct StorageClass parameter name to control remote volume access in the LINSTOR CSI driver is allowRemoteVolumeAccess [1][2]. You can specify this parameter in your StorageClass definition to control whether volumes can be attached to nodes where they do not have a physical replica (i.e., diskless access) [1][2]. Example usage: apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: linstor-remote-access-example provisioner: linstor.csi.linbit.com parameters: allowRemoteVolumeAccess: "false"... This parameter supports boolean values (e.g., "true" or "false") [1][2]. Additionally, it can accept more complex configuration values, such as YAML-formatted strings, to define fine-grained policies based on Kubernetes topology labels [1][3][4]. For instance, you can restrict remote access to specific zones or nodes [3][4]: allowRemoteVolumeAccess: | - fromSame: - topology.kubernetes.io/zone If not explicitly defined, this parameter defaults to "true" [1]. Note that all StorageClass parameters for the LINSTOR CSI driver are case-insensitive [1].

Citations:


Fix LINSTOR remote-access StorageClass parameter key.

In packages/apps/kubernetes/templates/helmreleases/csi.yaml (line 37), the template checks $params["linstor.csi.linbit.com/allowRemoteVolumeAccess"], but LINSTOR CSI’s StorageClass parameter is allowRemoteVolumeAccess (defaults to "true" when unset). Adjust the lookup to use allowRemoteVolumeAccess without the linstor.csi.linbit.com/ prefix so the condition reflects the intended remote/diskless access behavior.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/apps/kubernetes/templates/helmreleases/csi.yaml` at line 37, The
condition is using the wrong parameter key; update the Helm template conditional
that references $params to check "allowRemoteVolumeAccess" (without the
"linstor.csi.linbit.com/" prefix) instead of
"linstor.csi.linbit.com/allowRemoteVolumeAccess". Specifically, modify the
expression that currently reads (index $params
"linstor.csi.linbit.com/allowRemoteVolumeAccess" | default "true") to use (index
$params "allowRemoteVolumeAccess" | default "true") so the overall if-statement
with $sc.provisioner and the comparison to "false" reflects LINSTOR CSI’s actual
StorageClass parameter name.

@myasnikovdaniil myasnikovdaniil added the debug Debugging in progress label Jun 11, 2026
@myasnikovdaniil myasnikovdaniil removed the debug Debugging in progress label Jun 11, 2026
@myasnikovdaniil
myasnikovdaniil force-pushed the feat/tenant-linstor-storageclasses branch from bd65bc4 to 61c3776 Compare June 23, 2026 06:38

@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: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@hack/e2e-apps/run-kubernetes.sh`:
- Line 209: Remove the EXIT trap that invokes the _tenant_snapshot_on_fail
function (at line 209 and also at lines 643-644) as this violates the
repository's e2e shell scripting conventions which prohibit EXIT and RETURN
traps. Instead of relying on the trap for teardown/snapshot logic, explicitly
invoke the _tenant_snapshot_on_fail function at the appropriate failure handling
points in the script's control flow where snapshot capture is actually needed,
making the failure-path invocation explicit rather than trap-based.
🪄 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: CHILL

Plan: Pro

Run ID: 1e4fc28b-f2e7-4b1b-a413-1ce33f8333b0

📥 Commits

Reviewing files that changed from the base of the PR and between bd65bc4 and 61c3776.

📒 Files selected for processing (12)
  • api/apps/v1alpha1/kubernetes/types.go
  • hack/e2e-apps/run-kubernetes.sh
  • packages/apps/kubernetes/README.md
  • packages/apps/kubernetes/templates/helmreleases/csi.yaml
  • packages/apps/kubernetes/tests/csi_test.yaml
  • packages/apps/kubernetes/values.schema.json
  • packages/apps/kubernetes/values.yaml
  • packages/system/kubernetes-rd/cozyrds/kubernetes.yaml
  • packages/system/kubevirt-csi-node/Makefile
  • packages/system/kubevirt-csi-node/templates/deploy.yaml
  • packages/system/kubevirt-csi-node/tests/storageclasses_test.yaml
  • packages/system/kubevirt-csi-node/values.yaml
✅ Files skipped from review due to trivial changes (5)
  • packages/apps/kubernetes/values.yaml
  • api/apps/v1alpha1/kubernetes/types.go
  • packages/apps/kubernetes/values.schema.json
  • packages/system/kubernetes-rd/cozyrds/kubernetes.yaml
  • packages/apps/kubernetes/README.md
🚧 Files skipped from review as they are similar to previous changes (4)
  • packages/system/kubevirt-csi-node/tests/storageclasses_test.yaml
  • packages/apps/kubernetes/tests/csi_test.yaml
  • packages/apps/kubernetes/templates/helmreleases/csi.yaml
  • packages/system/kubevirt-csi-node/values.yaml

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

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@hack/e2e-apps/run-kubernetes.sh`:
- Line 209: Remove the EXIT trap that invokes the _tenant_snapshot_on_fail
function (at line 209 and also at lines 643-644) as this violates the
repository's e2e shell scripting conventions which prohibit EXIT and RETURN
traps. Instead of relying on the trap for teardown/snapshot logic, explicitly
invoke the _tenant_snapshot_on_fail function at the appropriate failure handling
points in the script's control flow where snapshot capture is actually needed,
making the failure-path invocation explicit rather than trap-based.
🪄 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: CHILL

Plan: Pro

Run ID: 1e4fc28b-f2e7-4b1b-a413-1ce33f8333b0

📥 Commits

Reviewing files that changed from the base of the PR and between bd65bc4 and 61c3776.

📒 Files selected for processing (12)
  • api/apps/v1alpha1/kubernetes/types.go
  • hack/e2e-apps/run-kubernetes.sh
  • packages/apps/kubernetes/README.md
  • packages/apps/kubernetes/templates/helmreleases/csi.yaml
  • packages/apps/kubernetes/tests/csi_test.yaml
  • packages/apps/kubernetes/values.schema.json
  • packages/apps/kubernetes/values.yaml
  • packages/system/kubernetes-rd/cozyrds/kubernetes.yaml
  • packages/system/kubevirt-csi-node/Makefile
  • packages/system/kubevirt-csi-node/templates/deploy.yaml
  • packages/system/kubevirt-csi-node/tests/storageclasses_test.yaml
  • packages/system/kubevirt-csi-node/values.yaml
✅ Files skipped from review due to trivial changes (5)
  • packages/apps/kubernetes/values.yaml
  • api/apps/v1alpha1/kubernetes/types.go
  • packages/apps/kubernetes/values.schema.json
  • packages/system/kubernetes-rd/cozyrds/kubernetes.yaml
  • packages/apps/kubernetes/README.md
🚧 Files skipped from review as they are similar to previous changes (4)
  • packages/system/kubevirt-csi-node/tests/storageclasses_test.yaml
  • packages/apps/kubernetes/tests/csi_test.yaml
  • packages/apps/kubernetes/templates/helmreleases/csi.yaml
  • packages/system/kubevirt-csi-node/values.yaml
🛑 Comments failed to post (1)
hack/e2e-apps/run-kubernetes.sh (1)

209-209: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Remove EXIT-trap usage in e2e script flow.

This introduces an EXIT trap, which conflicts with the repository’s e2e shell conventions and can create brittle control flow in failure handling. Prefer explicit failure-path invocation instead of trap-based teardown/snapshot logic.

As per coding guidelines, **/*.{bats,sh} changes must use no EXIT/RETURN traps.

Also applies to: 643-644

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@hack/e2e-apps/run-kubernetes.sh` at line 209, Remove the EXIT trap that
invokes the _tenant_snapshot_on_fail function (at line 209 and also at lines
643-644) as this violates the repository's e2e shell scripting conventions which
prohibit EXIT and RETURN traps. Instead of relying on the trap for
teardown/snapshot logic, explicitly invoke the _tenant_snapshot_on_fail function
at the appropriate failure handling points in the script's control flow where
snapshot capture is actually needed, making the failure-path invocation explicit
rather than trap-based.

Source: Coding guidelines

@myasnikovdaniil myasnikovdaniil self-assigned this Jun 23, 2026
@mattia-eleuteri

Copy link
Copy Markdown
Collaborator

LGTM !
thanks for the help on this one 😄

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.

NOT LGTM (REQUEST_CHANGES)

Business context

Tenant Kubernetes clusters previously got a single hardcoded kubevirt StorageClass. This auto-discovers the management cluster's remote-accessible LINSTOR classes via Helm lookup and propagates each into the tenant under the same name, keeping the legacy kubevirt class as a non-default alias.

I verified the design on the points that matter most. Tenant isolation is not widened: the kubevirt-csi-driver defaults to AllowAll enforcement, so a tenant could already reference any infra StorageClass; this change only auto-creates the convenience classes. Evidence: packages/apps/kubernetes/images/kubevirt-csi-driver/main.go:190-192 (empty enforcement string -> AllowAll: true). The filter parameter key linstor.csi.linbit.com/allowRemoteVolumeAccess is correct for this project: it matches the parameter form the LINSTOR classes actually use here, so a bare-key concern does not apply. Evidence: hack/e2e-post-install-prep.sh:82,95. Both helm-unittest suites (132 + 3) and the new e2e assertions render and pass locally.

One blocker remains.

Blockers

B1 - Fallback default can select the kubevirt alias pointing at a non-existent infra class

File: packages/apps/kubernetes/templates/helmreleases/csi.yaml:47-54

Evidence: The kubevirt alias (infraStorageClass: $defaultName, line 48) is inserted into $out before the default is chosen. When neither the configured storageClass nor replicated is among the propagated classes, the default is picked by keys $out | sortAlpha | first (line 54), and that candidate set still contains kubevirt. If every propagated class name sorts after kubevirt (for example nvme, ssd, standard), kubevirt becomes the tenant default with infraStorageClassName: <defaultName> (for example replicated), which by assumption does not exist on the management cluster.

Impact: The tenant's default StorageClass then provisions against a non-existent infra class - default PVCs stay Pending with no error surfaced, while the real working classes are left non-default. This is the feature's own target configuration (operators running multiple custom tiers without a replicated class). The standard path (a remote replicated class present) is unaffected and is covered by e2e, so this is not a regression - but the new fallback has the information to avoid the broken default and does not.

Fix: Choose the default among the propagated classes before inserting the kubevirt alias, or exclude kubevirt from the sortAlpha candidate set.

Non-blocking

  1. Lookup result is invisible to the Flux release digest. Evidence: packages/apps/kubernetes/templates/helmreleases/csi.yaml:36. The propagated set is computed from live cluster state via lookup, but helm-controller re-renders on a digest of chart plus values that does not include lookup output. StorageClasses added to or removed from the management cluster after a tenant exists will not propagate or be pruned on a normal reconcile - only when the kubernetes HelmRelease inputs otherwise change. This limits the auto-discover behavior to create/update time. Consider routing the discovered set through valuesFrom (a controller-maintained ConfigMap) so changes enter the digest, or documenting the limitation.

  2. allowRemoteVolumeAccess absent is treated as remote. Evidence: packages/apps/kubernetes/templates/helmreleases/csi.yaml:38 (| default "true"). A LINSTOR class whose layer list has no network-replication layer (for example layerList: storage without DRBD) is effectively node-local even with the parameter unset, so it would be propagated as if remote and its volumes could not follow a live-migrated VM. The project's own node-local class sets allowRemoteVolumeAccess: "false" explicitly, so in-convention setups are safe; operator-authored classes that omit the flag are the gap. Consider also gating on the layer list, or documenting the requirement.

  3. Filter logic has no unit coverage. Evidence: packages/apps/kubernetes/tests/csi_test.yaml (header comment). helm-unittest has no live cluster, so the provisioner and allowRemoteVolumeAccess filter is exercised only by e2e; the unit tests cover the fallback map only. This is acknowledged in the test, and is acceptable, but the core selection has no fast-feedback coverage.

  4. Quote templated scalars. Evidence: packages/system/kubevirt-csi-node/templates/deploy.yaml:274,282. name: {{ $name }} and infraStorageClassName: {{ $m.infraStorageClass }} render unquoted. StorageClass names are DNS-1123 so the risk is low, but | quote is the safer default.

  5. Upgrade caveat lives only in the PR description. Evidence: PR body "Upgrade note". The instruction that a manually-created tenant StorageClass named replicated must be deleted before upgrade (to avoid a Helm ownership conflict) is operator-facing and is not in any persistent doc; I found no sibling website docs PR. Worth a durable note.

{{- $chosen := "" }}
{{- if hasKey $out $defaultName }}{{ $chosen = $defaultName }}
{{- else if hasKey $out "replicated" }}{{ $chosen = "replicated" }}
{{- else }}{{ $chosen = (keys $out | sortAlpha | first) }}{{ end }}

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.

B1 (blocker): the sortAlpha | first fallback runs after the kubevirt alias was inserted (line 48), so kubevirt is a default candidate. If every propagated class name sorts after kubevirt (e.g. nvme, ssd), kubevirt becomes the tenant default with infraStorageClassName: <defaultName> pointing at an infra class that is not present -> default PVCs stay Pending silently. Pick the default among the propagated classes before adding the alias, or exclude kubevirt from the candidate set.

{{- $out := dict }}
{{- range $sc := (lookup "storage.k8s.io/v1" "StorageClass" "" "").items }}
{{- $params := $sc.parameters | default dict }}
{{- if and (eq $sc.provisioner "linstor.csi.linbit.com") (ne (index $params "linstor.csi.linbit.com/allowRemoteVolumeAccess" | default "true") "false") }}

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.

Non-blocking: | default "true" treats an absent allowRemoteVolumeAccess as remote-accessible. A class with no network-replication layer (e.g. layerList: storage without DRBD) is effectively node-local even when the flag is unset, and would be propagated as if remote. In-convention classes set the flag explicitly, so this only affects operator-authored classes that omit it.

{{- /* Propagate remote-accessible LINSTOR infra StorageClasses to the tenant under the same name. */}}
{{- $defaultName := .Values.storageClass | default "replicated" }}
{{- $out := dict }}
{{- range $sc := (lookup "storage.k8s.io/v1" "StorageClass" "" "").items }}

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.

Non-blocking: lookup output is not part of the Flux release digest, so helm-controller will not re-render when the management cluster's StorageClass set changes. New classes will not propagate (and removed ones will not prune) on a normal reconcile - only when the HelmRelease inputs otherwise change. Consider valuesFrom for the discovered set, or document the create/update-time limitation.

kind: StorageClass
metadata:
name: kubevirt
name: {{ $name }}

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.

Nit: quote templated scalars - name: {{ $name | quote }} here and infraStorageClassName: {{ $m.infraStorageClass | quote }} on line 282. DNS-1123 names make this low-risk, but quoting is the safer default.

@myasnikovdaniil

Copy link
Copy Markdown
Contributor Author

Aleksei Sviridkin (@lexfrei) addressed in 59d22f7:

  • B1 (blocker) — default selection now runs on the propagated set before the kubevirt alias is inserted, so the legacy alias (which points at $defaultName, possibly absent on the infra cluster) can never be auto-selected as the tenant default (templates/helmreleases/csi.yaml).
  • Quotingname/infraStorageClassName now | quote in kubevirt-csi-node deploy.yaml.
  • Docs — added a Breaking-Changes note to the kubernetes README covering the upgrade caveat (delete a colliding hand-made tenant SC first), the render-time-only propagation limitation, and the explicit-allowRemoteVolumeAccess requirement.

All helm-unittest suites pass (132 kubernetes + 3 kubevirt-csi-node); make generate leaves values.schema.json/cozyrds unchanged. The selection path still isn't unit-testable — it depends on lookup, which is empty under helm template — so it remains covered by e2e as before.

On the two CodeRabbit bot findings: the prefixed linstor.csi.linbit.com/allowRemoteVolumeAccess key is correct for this project's StorageClasses (hack/e2e-post-install-prep.sh:82,95), and the EXIT trap it flagged is pre-existing on main (commit 5149683), outside this PR's diff — no change for either.

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.

NOT LGTM (REQUEST_CHANGES) — re-review on the current head. The B1 fix is correct, but the behavior it fixes has no automated regression coverage, and that path is the feature's own target configuration.

What's resolved since the previous review

  • B1 fixed (verified by inspection). Default selection now runs before the kubevirt alias is inserted into $out, so keys $out | sortAlpha | first chooses among the propagated classes only — the kubevirt alias can no longer be auto-selected as the tenant default. csi.yaml rule order confirms it.
  • Non-blocking #4 addressed: name and infraStorageClassName are now | quoted.
  • Non-blocking #5 addressed: the upgrade caveat now lives in packages/apps/kubernetes/README.md (durable doc), not just the PR body.
  • Non-blocking #1 and #2 are now documented in that same README note (render-time lookup semantics; absent allowRemoteVolumeAccess treated as remote).

Blocker

B1-test: the fallback-default fix ships with no test pinning it

The fix is correct, but nothing guards it, and the branch it lives in — propagated classes present, none named the configured storageClass, none named replicated — is the exact multi-tier config this feature targets, with zero automated coverage today:

  • helm-unittest can't reach it. With no live cluster, lookup returns empty, so $out is empty and every unit test falls into the replicated fallback. The two existing unit tests only exercise that fallback map — not the propagated-classes branch where B1 lived.
  • e2e doesn't cover it either. The e2e provisions a setup where a remote replicated class is present, so the no-replicated multi-tier branch is never exercised.

So the regression that was just fixed can silently return. Since the logic is lookup-gated and not reachable by helm-unittest, the regression belongs in e2e: add a case that provisions >=2 remote LINSTOR classes with none named replicated (ideally names sorting after kubevirt, e.g. nvme/ssd), then asserts:

  1. exactly one tenant default StorageClass,
  2. the default is one of the propagated classes — never the kubevirt alias,
  3. the kubevirt alias exists with default: false.

That locks the fix and documents the intended behavior for the multi-tier configuration. Everything else is ready.

myasnikovdaniil and others added 4 commits June 26, 2026 21:20
…to tenant clusters

Tenant Kubernetes clusters previously exposed a single hardcoded
"kubevirt" StorageClass mapped to one infra class. Discover all
remote-accessible LINSTOR infra StorageClasses (provisioner
linstor.csi.linbit.com, allowRemoteVolumeAccess != false) via Helm
lookup and propagate each to the tenant under the same name.

- csi.yaml builds a storageClasses map filtered to remote LINSTOR
  classes; node-local classes (e.g. "local") are excluded because the
  backing volume cannot follow a live-migrated worker VM.
- The class named by .Values.storageClass (default "replicated") is
  marked the tenant default; the legacy "kubevirt" StorageClass is kept
  as a non-default alias so existing tenant PVCs keep binding.
- A non-destructive fallback preserves the legacy mapping when the
  lookup returns empty (dry-run), so tenant StorageClasses are never
  wiped on a transient reconcile.
- infraStorageClassEnforcement is intentionally left disabled (AllowAll)
  to avoid breaking tenants that already self-served custom
  StorageClasses under the current default.

Refs: #2094, #1424

Assisted-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
- csi_test.yaml: assert the fallback storageClasses map yields a default
  "replicated" plus the non-default "kubevirt" alias, and that the
  alias tracks the storageClass selector.
- kubevirt-csi-node: add a Makefile test target (otherwise the package
  is silently skipped by the unit-test runner) and storageclasses_test
  covering per-entry rendering, the single-default invariant, and the
  absence of any enforcement allowList in driver-config.
- run-kubernetes.sh: verify in e2e that "replicated" propagates as the
  default kubevirt-CSI class, the "kubevirt" alias is retained, and the
  node-local "local" infra class is filtered out.

Refs: #2094

Assisted-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
…lt SC

The legacy "kubevirt" StorageClass alias was inserted into the candidate
map before the default was chosen, so the sortAlpha fallback could pick it
when propagated class names sort after "kubevirt" (e.g. nvme, ssd) and no
configured/replicated class is present. That made the tenant default point
at an infraStorageClass that may not exist on the infra cluster, leaving
default PVCs Pending. Choose the default from the propagated set before
adding the alias.

Also quote the templated StorageClass name and infraStorageClassName in
kubevirt-csi-node, and document the propagation upgrade caveat, render-time
evaluation limitation, and allowRemoteVolumeAccess assumption in the
kubernetes README.

Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
…B1)

Pin the PR #2872 B1 fix: when the management cluster exposes only remote
LINSTOR StorageClasses whose names sort after "kubevirt" and none is named
the configured storageClass (default "replicated"), the tenant default must
be chosen among the propagated classes and never the legacy "kubevirt" alias.

helm-unittest cannot reach this branch (no live cluster -> lookup returns
empty -> the storageClasses map collapses to the "replicated" fallback), so
it is exercised in e2e via a single server-side dry-run render of the
kubernetes chart against the live management cluster: two remote LINSTOR
classes are added and "replicated" is removed for the one render, then the
rendered -csi HelmRelease storageClasses map is asserted to carry exactly one
default among the propagated classes, never the "kubevirt" alias.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
@myasnikovdaniil
myasnikovdaniil force-pushed the feat/tenant-linstor-storageclasses branch from 59d22f7 to da084ef Compare June 26, 2026 16:30
@myasnikovdaniil

Copy link
Copy Markdown
Contributor Author

Aleksei Sviridkin (@lexfrei) B1-test addressed in da084ef5.

Added an e2e regression (verify_storageclass_fallback_default in run-kubernetes.sh, wired into kubernetes-latest.bats) covering the multi-tier branch helm-unittest can't reach (no live cluster → lookup empty → always the replicated fallback).

It provisions two remote LINSTOR classes sorting after kubevirt (nvme, ssd), drops replicated for one server-side --dry-run=server render, restores inline (no trap), then asserts the rendered -csi HR's storageClasses: exactly one default, among the propagated classes, never kubevirt; kubevirt alias present with default: false. Fails pre-fix, passes post-fix.

Rebased onto main to clear the conflict.

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.

LGTM — the test-completeness blocker is resolved.

The B1 fallback-default fix is intact at the current head: in csi.yaml, $chosen is picked before the kubevirt alias is inserted into $out, so keys $out | sortAlpha | first operates on a candidate set that never contains kubevirt — the alias can no longer become the tenant default.

And there is now a real regression test for the exact path I flagged. The new e2e case (hack/e2e-apps/kubernetes-latest.bats + verify_storageclass_fallback_default in run-kubernetes.sh) provisions two remote LINSTOR classes whose names sort after kubevirt, removes replicated for a single server-side dry-run render (so neither the configured class nor replicated is in the propagated set, forcing the sortAlpha | first branch), and asserts exactly one default, that it is nvme/ssd and never the kubevirt alias, and that the alias is present but non-default. It correctly exercises the lookup-gated branch that helm-unittest cannot reach — helm install --dry-run=server runs Helm lookup against the live cluster — and restores the management-cluster StorageClasses inline before any assertion can exit, per the e2e teardown rules. That is exactly the multi-tier, no-replicated coverage the fix needed.

Code-level checks (Build, Analyze (go), CodeQL, generated-code, pre-commit, DCO) are green; E2E is the separate merge gate and will exercise this new case. Approving.

@lexfrei
Aleksei Sviridkin (lexfrei) merged commit e295f90 into main Jun 26, 2026
15 of 16 checks passed
@lexfrei
Aleksei Sviridkin (lexfrei) deleted the feat/tenant-linstor-storageclasses branch June 26, 2026 19:19
myasnikovdaniil added a commit that referenced this pull request Jul 8, 2026
…ites

- qdrant: add the post-delete PVC-reclaim guard (#3059) — delete the CR
  and poll the ordinal-anchored data PVC to absence via the error op
  (retries transient API errors internally), with a PVC/jobs/pods dump
  on failure; Chainsaw auto-cleanup alone would never notice a leaked
  StatefulSet-templated PVC
- kuberture: make the split-horizon negative log assertions fail loud —
  under set -e a pipeline beginning with ! suppresses errexit, so both
  leak checks silently passed (the same vacuous-negation bug main fixed
  in 12fe325)
- foundationdb: retry the one-shot generations.reconciled read for up
  to 2m — the operator stamps it after the post-bounce uptime gate, so
  it can trail the fdbcli-derived health fields the previous assert
  converged on (a79f800)
- kubernetes-latest: add the version-independent SC-fallback-default
  Test (#2872 B1) calling verify_storageclass_fallback_default from the
  shared lib; a separate Test doc so it runs regardless of the heavy
  bringup Test's outcome, safe under the suite-wide parallel: 1 while
  it temporarily mutates cluster-scoped StorageClasses

Assisted-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
myasnikovdaniil added a commit that referenced this pull request Jul 12, 2026
…2826)

## What this PR does

Migrates the E2E **app** test suite from BATS to [Kyverno
Chainsaw](https://github.com/kyverno/chainsaw) — a declarative,
Kubernetes-native E2E framework (CNCF, part of the Kyverno project;
successor to KUTTL) — and wires it into CI as the replacement for the
per-app BATS loop. This grows the original pilot (postgres + bucket)
into the full suite.

All 22 app suites are ported under `hack/e2e-chainsaw/` and the
`hack/e2e-apps/*.bats` files are removed:

`postgres`, `bucket`, `mariadb`, `mongodb`, `redis`, `qdrant`,
`clickhouse`, `kafka`, `etcd`, `openbao`, `harbor`, `foundationdb`,
`external-dns`, `kuberture`, `vminstance`, `gateway`,
`kubernetes-latest`, `kubernetes-previous`, `kubernetes-oidc-system`,
`kubernetes-oidc-customconfig`, `securitygroup`, `serviceexposure`.

**Approach**

- **Declarative** suites assert on `status.conditions` and concrete
fields. The `timeout N sh -ec "until kubectl get ..."` + `kubectl wait`
pair that appeared ~100 times collapses into a single `assert` that
polls existence and state together, with a structured diff on failure
and automatic `events`/`describe`/`podLogs` capture via `catch`
(previously only `harbor.bats` did this, by hand).
- **Imperative** suites keep their logic in `script` steps: `openbao`
init/unseal, `kuberture` external-dns split-horizon probes,
`vminstance`, the `gateway` admission/impersonation cases, and
`kubernetes-latest`/`previous`, which wrap the relocated
`hack/e2e-chainsaw/_lib/run-kubernetes.sh` verbatim (Kamaji bring-up,
LB/NFS/ouroboros checks).
- `gateway` tests derive the tenant apex from the namespace
`namespace.cozystack.io/host` label at runtime, so they are
host-independent.

**CI**

- The `e2e` job now runs `chainsaw test hack/e2e-chainsaw/` via a new
`test-chainsaw` target and uploads the JUnit `chainsaw-report.xml`.
- The `chainsaw` binary is added to the e2e-sandbox image.
- `install-cozystack` and `test-openapi` stay BATS (cluster bootstrap +
OpenAPI checks).
- The per-app 3-retry loop is dropped — assertion polling replaces the
fixed-timeout flakiness it papered over.

**Validation**

Ran against a development cluster: the DB/app, storage, and VM suites
pass; the three suites that depend on platform features not present on
that cluster (`gateway`, `kuberture`, `external-dns`) and the two
heavyweight `kubernetes-*` suites are exercised by this PR's CI run on a
freshly installed platform.

Note for reviewers: service-port asserts use the `(ports[*].port)`
projection form rather than a number-literal filter (`` ports[?port ==
`N`] ``), because Chainsaw v0.2.15 mis-evaluates JMESPath number-literal
comparisons.

**2026-07-09: reconciled with main (~520 commits of drift)**

- etcd suite re-ported to the v1alpha2 operator contracts (#2859):
`readyMembers=3` gate, pod-label/`/scale`/WorkloadMonitor/metrics/defrag
contracts, plus the `ETCD_E2E_S3_ROUNDTRIP`-gated backup round-trip
driving `examples/backups/etcd`
- 12 new `ingress-hostname-policy` gateway cases ported (apex derived at
runtime from the tenant-root namespace label)
- post-branch bats drift folded in: `mariadb-single` webhook guard,
kafka `c1.small` presets, bucket/harbor 2m BucketClaim fail-fast, bucket
readonly-denial promoted to a hard fail (cosi-driver v0.3.1), qdrant
PVC-reclaim guard (#3059), kuberture fail-loud negations,
SC-fallback-default test (#2872 B1)
- 4 suites that only exist on main since the branch was cut are ported:
`kubernetes-oidc-system`, `kubernetes-oidc-customconfig`,
`securitygroup`, `serviceexposure`
- CI seams: `nightly.yaml` converted to the chainsaw invocation,
`run-kubernetes.sh` reconciled (Talos/CABPT waits, tenant drain, LINSTOR
pool wait, talos-image-cache under `_lib/`), `e2e-capture-dataplane.sh`
replicated in the Chainsaw global catch

### Release note

```release-note
fix(kubernetes): tenant Kubernetes teardown no longer hangs when the cluster has no working nodes — the pre-delete hook now bounds its wait for the in-tenant HelmReleases and force-clears their Flux finalizers on timeout (#3271)
```


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* End-to-end testing is now driven by a unified Chainsaw suite (platform
apps, Kubernetes, networking, security groups, service exposure).
* Test Impact Analysis selects affected suites, with an option to run
the full suite.
* CI produces a JUnit report artifact and collects suite-scoped
diagnostics on failure.
* **Bug Fixes**
* CI E2E now fails fast and emits clearer Kubernetes/HelmRelease and
sandbox bucket state diagnostics.
* Improved teardown reliability with bounded wait + finalizer handling
to avoid hangs.
* **Documentation**
* Updated E2E testing docs and local guidance to reflect the new
Chainsaw-based workflow and suite paths.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/kubernetes Issues or PRs related to the tenant Kubernetes app area/storage Issues or PRs related to storage (linstor, seaweedfs, bucket, velero, harbor) kind/api-change Categorizes issue or PR as related to adding, removing, or otherwise changing an API kind/feature Categorizes issue or PR as related to a new feature size/L This PR changes 100-499 lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants