Compare commits

..

127 commits

Author SHA1 Message Date
Aleksei Sviridkin
f45facca77
feat(hami): add HAMi GPU virtualization system package (#2484)
## What this PR does

Documentation: https://github.com/cozystack/website/pull/517

Integrates [HAMi](https://github.com/Project-HAMi/HAMi) v2.8.1 (CNCF
Sandbox) into Cozystack as a system-level package for fractional GPU
sharing in tenant Kubernetes clusters.

The integration covers three layers:

- **System chart** (`packages/system/hami/`): Vendors upstream HAMi Helm
chart with device plugin, scheduler extender, mutating webhook, and
RuntimeClass configuration. The broken DRA subchart was removed — it
renders resources even when disabled and references unpublished images.

- **Kubernetes app addon** (`packages/apps/kubernetes/`): HAMi exposed
as an optional toggle (`hami.enabled`). Automatically disables GPU
Operator's native device plugin when active to avoid conflicts. Enforces
hard dependency on GPU Operator.

- **Platform registration** (`packages/core/platform/`): HAMi declared
as PackageSource with gpu-operator dependency, included in the iaas
bundle.

**Known limitation**: HAMi-core relies on a private glibc symbol
(`_dl_sym`) removed in glibc 2.34, which breaks compute isolation on
modern container images (Ubuntu 22.04+, PyTorch/TensorFlow official
images). Alpine/musl is entirely incompatible. See the package README
for details and upstream issue references.

### Screenshots

N/A — no UI changes.

### Release note

```release-note
feat(hami): add HAMi GPU virtualization as an optional system package for fractional GPU sharing. Enables memory and compute isolation for NVIDIA GPUs across tenant workloads. Integrates with GPU Operator and can be enabled per-cluster via the hami.enabled toggle. Note: compute isolation requires glibc < 2.34 in workload containers.
```

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

* **New Features**
* Added HAMi GPU virtualization middleware as an optional cluster addon
to enable fractional GPU sharing.
* HAMi addon includes an enable toggle (default: disabled) and a
customizable Helm values override; when enabled it enforces GPU Operator
presence and adjusts GPU Operator values accordingly.
* New HelmRelease integration for deploying HAMi with dependency
ordering and conditional rendering.

* **Documentation**
* Added HAMi setup guide, compatibility notes, and updated cluster addon
configuration reference.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-04-29 16:22:58 +03:00
Timofei Larkin
69ad74e457
fix(cozystack-engine): add managed Kubernetes without visible control-plane nodes support to lineage-controller-webhook (#2481)
<!-- Thank you for making a contribution! Here are some tips for you:
- Use Conventional Commits for the PR title: `type(scope): description`
- Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore
- Scopes for system components: dashboard, platform, cilium, kube-ovn,
linstor, fluxcd, cluster-api
- Scopes for managed apps: postgres, mariadb, redis, kafka, clickhouse,
virtual-machine, kubernetes
- Scopes for development and maintenance: api, hack, tests, ci, docs,
maintenance
- Breaking changes: append `!` after type/scope (`feat(api)!: ...`) or
add a `BREAKING CHANGE:` footer
- If it's a work in progress, consider creating this PR as a draft.
- Don't hesistate to ask for opinion and review in the community chats,
even if it's still a draft.
- Add the label `backport` if it's a bugfix that needs to be backported
to a previous version.
-->

In environments where control-plane nodes are not available (e.g., AWS
managed nodes, Cozy-in-Cozy tenant Kubernetes clusters), the DaemonSet
cannot schedule any pods because no nodes match this selector.

## What this PR does

This PR adds ability to replace DaemonSet by Deployment with custom
number of replicas for HA purpose. Default values saves current
behavior.

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

* **New Features**
* Optional Deployment mode for lineage-controller-webhook with
configurable replicas
  * PodDisruptionBudget support when Deployment mode is enabled

* **Updates**
* Improved pod scheduling: node affinity replaces nodeSelector, optional
tolerations, and pod anti-affinity when using Deployment
* Clarified behavior for local API endpoint scheduling (only applies
when pod lands on nodes running an API server)

* **Documentation**
  * Added README with usage examples and parameter details
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-04-29 15:24:27 +04:00
myasnikovdaniil
6e045e6940
docs: add changelog for v1.3.1 (#2480)
This PR adds the changelog for release `v1.3.1`.

 Changelog has been automatically generated in
`docs/changelogs/v1.3.1.md`.
2026-04-29 14:47:30 +05:00
Arsolitt
5d58d0f340
chore(hami): document and partially automate vendor patches in Makefile
The `update:` recipe now reproduces the top-level vendoring overrides
(remove broken hami-dra subchart, clear Chart.yaml dependencies, strip
dra/hami-dra/podSecurityPolicy from upstream values.yaml) after
`helm pull`, so they no longer silently disappear on the next bump.

Template-level patches (DRA guards in scheduler/*, indent fix in
device-plugin/monitorservice.yaml) are documented with rationale and
commit references — they remain a manual step because automating them
would be fragile across upstream template restructures.

Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-29 11:14:41 +03:00
Arsolitt
9b5848ed26
fix(platform): register HAMi as kubernetes-application component
The HelmRelease at packages/apps/kubernetes/templates/helmreleases/hami.yaml
references chartRef.name 'cozystack-kubernetes-application-kubevirt-kubernetes-hami',
but that component was missing from the kubernetes-application PackageSource.
The HelmRelease would sit in Stalled: ArtifactNotFound at install time.

Add kubernetes-hami next to kubernetes-gpu-operator under variant kubevirt,
mirroring the existing pattern for every other tenant-cluster addon. The
standalone cozystack.hami PackageSource is retained — same shape as
gpu-operator, which is registered both standalone (for bundles/iaas) and
as a kubernetes-application component (for tenant-cluster HelmReleases).

Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-29 11:13:55 +03:00
Arsolitt
d5caffcc8d
fix(kubernetes): gate HAMi HelmRelease on _namespace.etcd
Match the pattern used by every other tenant-cluster addon HelmRelease
(gpu-operator, cilium, fluxcd, ...): only render when the upstream etcd
namespace is ready, not just when the addon is enabled. Add values-ci.yaml
to the HAMi test suite so the new gate has the fixture it needs.

Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-29 11:13:42 +03:00
Arsolitt
9876acec2f
docs(hami): clarify gpu-operator devicePlugin override behavior
The HAMi-driven default of disabling gpu-operator's device plugin is
applied via valuesOverride and can be re-enabled by users for advanced
topologies (mixed HAMi / vanilla NVIDIA device plugin pools). Document
this explicitly so the README aligns with the existing template merge
order.

Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-29 11:13:24 +03:00
Arsolitt
ba8c1a0535
docs(hami): drop HAMi#173 reference from glibc tracking issues
HAMi#173 was closed as "not planned" and only suggests a typo fix
(< 2.3.0 → < 2.30); it does not establish the actual 2.34 boundary.
HAMi-core#174 (symbol-level cause) and HAMi#1190 (empirical per-glibc
behavior) cover the same ground accurately.

Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-29 11:13:12 +03:00
Myasnikov Daniil
1e0d8acb35
docs(changelog): add 1.3.1 entries for backports landed after auto-gen
The auto-generated changelog only listed #2459/#2467 (velero-configmap
Role move). Five additional PRs were backported and merged into
release-1.3 between then and the v1.3.1 tag (2026-04-28):

- #2471/#2524 - fix(api): IDOR in TenantNamespace Get/Watch
- #2496/#2505 - feat(linstor): linstor-csi v1.10.6 (Protocol-C dual-attach)
- #2462/#2511 - fix(etcd): remove destructive post-upgrade hook
- #2421/#2491 - fix(kamaji): memory limits + startup probe
- #2498/#2518 - build(linstor): wire linstor-gui into root build target

Update the release date to match the actual tag (2026-04-28), rewrite
the intro paragraph, and add @kvaps to contributors.

Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-04-29 12:46:18 +05:00
Myasnikov Daniil
875a940033
docs(changelog): rewrite v1.3.1 with the actual release contents
The AI-generated v1.3.1 changelog (#2480) was generated from
git log v1.3.0..main rather than git log v1.3.0..v1.3.1, because
the workflow checked out main while the v1.3.1 tag points to
release-1.3. As a result the changelog included:

- 8 PRs that were merged to main but never shipped in v1.3.1
- 6 backport PRs that were merged to release-1.3 *after* v1.3.1
  was tagged
- Both originals and their backports as separate duplicate entries
- A 2024 PR (#435) that has nothing to do with this range
- Generic "Documentation updates" placeholders for website entries
- Title duplicated as both the bold label and the description
  (`* **fix(...): X**: fix(...): X (...)`)
- The cozystack-ci bot listed as a human contributor

The actual v1.3.1 release range (v1.3.0..v1.3.1) contains exactly
one user-facing change:

  41bcb0be [Backport release-1.3] fix(backups): move
           velero-configmap Role to velero chart (#2467)

which is the backport of #2459 (myasnikovdaniil) shipped via
#2467 (IvanHunters). This commit replaces the contents of
docs/changelogs/v1.3.1.md with that one entry, the matching
two-person contributors list, and the standard footer.

The workflow + docs fixes that prevent this regression for future
patch releases will land in a separate PR against main.

Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-04-29 12:22:08 +05:00
cozystack-ci[bot]
4541c20e34 docs: add changelog for v1.3.1
Signed-off-by: cozystack-ci[bot] <274107086+cozystack-ci[bot]@users.noreply.github.com>
2026-04-29 04:38:30 +00:00
Aleksei Sviridkin
3977aef509
feat(monitoring): add GPU observability dashboards and recording rules (#2418)
## What this PR does

Adds GPU observability to Cozystack: five Grafana dashboards, a set of
VictoriaMetrics recording rules with a throttle-regression alert, and
reference manifests for wiring the NVIDIA GPU Operator and DCGM Exporter
on Talos when running GPU workloads directly in pods.

### Components

1. **Dashboards** — `dashboards/gpu/*.json`, registered in
`packages/system/monitoring/dashboards-infra.list`. All follow the
project convention: the Prometheus data source is selected through the
`$ds_prometheus` template variable.

- `gpu-performance` — per-GPU utilization (NVML, tensor pipe, graphics
engine, memory copy), VRAM, power, temperature, and health (XID errors,
power/thermal throttling). All metrics are shown per-GPU, filterable by
the `$Hostname` selector.
- `gpu-fleet` — cluster-wide inventory, capacity, utilization, and
per-node power draw as percentage of TDP.
- `gpu-tenants` — per-namespace allocation and 24-hour GPU-hour/kWh
accounting; utilization and power are shown per-node (DCGM cannot
attribute hardware metrics to namespaces).
- `gpu-quotas` — allocated vs. requested GPUs, pending pods, and a
per-namespace requests/limits table. All counts derive from the
`namespace:gpu_count:allocated` recording rule, which excludes
`Failed`/`Succeeded` pod carryover.
- `gpu-efficiency` — cluster and per-GPU efficiency: tensor saturation,
util-per-watt, and per-GPU power/thermal throttle fractions.

2. **Recording rules and alert** —
`packages/system/monitoring-agents/alerts/gpu-recording.rules.yaml`.
Four rule groups:

- `gpu.recording.cluster.1m` — cluster-wide totals consumed by overview
panels.
- `gpu.recording.node.1m` — per-node hardware aggregates (grouped by
`Hostname`) plus per-namespace allocation counts from kube resource
requests; filters `cozy-*`/`kube-*`.
- `gpu.recording.efficiency.1m` — per-GPU tensor saturation and
util-per-watt, plus per-GPU power/thermal throttle fractions bounded via
`clamp_max(..., 1)`.
- `gpu.recording.throttle.validation.5m` — houses the
`GPUThrottleFractionOverOne` warning alert that fires if the pre-clamp
throttle fraction exceeds 1.0. This guards against DCGM counter-unit
drift between GPU families; the `/1e9` divisor is verified on NVIDIA A10
with DCGM 3.x.

Rules are safe on clusters without DCGM — they evaluate to empty series
when no matching metrics are scraped.

3. **Reference manifests** under
`packages/system/gpu-operator/examples/`. These are **not** templates;
they document one working configuration:

- `values-native-talos.yaml` — Cozystack Package values for the
native-pod scenario.
- `dcgm-custom-metrics.yaml` — ConfigMap extending the DCGM CSV with
profiling, ECC, throttling, and energy counters used by the dashboards
and recording rules.
- `nvidia-driver-compat.yaml` — DaemonSet that stages
`libnvidia-ml.so.1` and `nvidia-smi` from the Talos glibc tree into a
location the GPU Operator validator inspects. Workaround for
[NVIDIA/gpu-operator#1687](https://github.com/NVIDIA/gpu-operator/issues/1687).
Requires `pod-security.kubernetes.io/enforce: privileged` on the target
namespace when the cluster enforces baseline/restricted
PodSecurityStandards.
- `README.md` — explains the two deployment paths (sandbox vs. native),
which DCGM metrics each dashboard depends on, and how the pieces fit
together.

4. **Cross-validation test** — `hack/check-gpu-recording-rules.bats`
enforces that every recording-rule reference in tracked dashboards
resolves, and that every DCGM metric referenced by the rules is declared
in either the upstream default CSV snapshot
(`hack/dcgm-default-counters.csv`) or the custom CSV shipped under
`examples/`.

### Why reference manifests, not templates

The out-of-the-box `packages/system/gpu-operator/values-talos.yaml`
targets **sandbox workloads** — GPUs passed through to KubeVirt VMs via
VFIO, driver disabled on the host, device plugin disabled. That is the
sensible default for Cozystack today.

Running GPU workloads directly in pods on Talos is also possible but
requires a different configuration that makes assumptions not every user
shares:

- the NVIDIA Talos system extension must be installed on GPU nodes,
- the operator's own driver and toolkit components must be disabled
because the extension provides them,
- the validator's hardcoded paths need a workaround until
NVIDIA/gpu-operator#1687 lands upstream.

Shipping those as active templates would silently impose those
assumptions. Shipping them as reference files next to the package lets
operators opt in with eyes open and understand the moving parts before
applying them.

### Scope note

The dashboards and recording rules are useful independently of the
reference manifests — any cluster that scrapes DCGM Exporter (however it
is installed) will populate them. The reference manifests are
self-contained and do not change any default behavior of the
`gpu-operator` package.

### Screenshots

<img width="2824" height="1055" alt="2026-04-27_12-36-52"
src="https://github.com/user-attachments/assets/71f85de7-1c80-4817-b36c-aeb1c2bdd39a"
/>
<img width="2913" height="1255" alt="2026-04-27_12-36-39"
src="https://github.com/user-attachments/assets/7a54daca-0727-4510-938c-080d31bdc229"
/>
<img width="2932" height="1251" alt="2026-04-27_12-36-30"
src="https://github.com/user-attachments/assets/f6c6c215-3d05-400c-b21c-29f267c93f23"
/>
<img width="2864" height="1222" alt="2026-04-27_12-36-00"
src="https://github.com/user-attachments/assets/e9d34bab-ade5-434b-92c3-f36712cf68d9"
/>
<img width="2958" height="1226" alt="image"
src="https://github.com/user-attachments/assets/0047a5f8-12a5-4cda-8676-041de139cb76"
/>


### Release note

```release-note
feat(monitoring): add GPU observability — five Grafana dashboards (performance, fleet, tenants, quotas, efficiency), VictoriaMetrics recording rules with a throttle-regression alert, and reference manifests under packages/system/gpu-operator/examples/ for running GPU workloads natively in pods on Talos.
```


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

* **New Features**
* Added five GPU dashboards for performance, efficiency, fleet overview,
quotas/allocation, and tenant usage (utilization, power, throttling,
allocation, billing).
* Added cluster recording rules and a throttling validation alert; new
Prometheus-derived metrics and per-GPU rankings/timeseries.

* **Documentation**
* Added GPU Operator examples and README documenting native Talos setup
and required DCGM counters.

* **Tests**
* Added validation tests ensuring dashboard ↔ recording-rule and DCGM
metric consistency.

* **Chores**
  * Registered new GPU dashboards in infra listings.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-04-28 22:42:01 +03:00
Arsolitt
805b3b17cb
fix(kubernetes): add CI values to GPU Operator HAMi tests
Tests need _namespace.etcd from values-ci.yaml after the merge
introduced an etcd-namespace guard on the gpu-operator HelmRelease
condition.

Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-28 19:11:26 +03:00
Arsolitt
80b86b0c1a
chore(kubernetes): regenerate CRD after merge conflict resolution
Per-package `make generate` corrects keysOrder placement and
openAPISchema formatting in the kubernetes CRD definition.

Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-28 19:02:35 +03:00
Arsolitt
3a8359fa73
chore(kubernetes): regenerate deepcopy after merge
Run `make generate` to pick up the Images type added in main.

Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-28 18:59:43 +03:00
Arsolitt
f6ee54e4ea
Merge remote-tracking branch 'origin/main' into feat/hami-integration
Signed-off-by: Arsolitt <arsolitt@gmail.com>

# Conflicts:
#	api/apps/v1alpha1/kubernetes/types.go
#	packages/apps/kubernetes/templates/helmreleases/gpu-operator.yaml
#	packages/system/kubernetes-rd/cozyrds/kubernetes.yaml
2026-04-28 18:58:57 +03:00
myasnikovdaniil
a961a90357
fix(api): prevent IDOR in TenantNamespace Get and Watch handlers (#2471)
## Summary

Fixed two IDOR (Insecure Direct Object Reference) vulnerabilities in the
TenantNamespace API handlers that allowed authenticated users to access
metadata of tenant namespaces without proper authorization checks.

## Changes

### New optimized function: hasAccessToNamespace()
- Lists RoleBindings **only in the target namespace** instead of all
cluster RoleBindings
- Used by Get() and Watch() for single-namespace access checks
- Order of magnitude faster than the previous approach

### Get() handler
- Uses `hasAccessToNamespace()` for efficient authorization
- Returns `NotFound` instead of `Forbidden` to prevent tenant
enumeration
- Now correctly enforces RoleBinding-based access control

### Watch() handler  
- Uses `hasAccessToNamespace()` for per-event authorization
- Logs authorization errors with klog for security audit
- Events for unauthorized namespaces are silently filtered out
- Ensures users only receive watch events for namespaces they have
access to

### Additional fixes
- Fixed ServiceAccount subject handling when namespace is empty
(defaults to RoleBinding namespace)
- Added proper error logging in Watch handler

## Performance Impact

| Handler | Before | After |
|---------|--------|-------|
| List() | List all RoleBindings × 1 | No change  |
| Get() | List all RoleBindings × 1 | List RoleBindings in 1 namespace 🚀
|
| Watch() | List all RoleBindings × N events | List RoleBindings in 1
namespace × N events 🚀 |

**For Watch with 100 events:**
- Before: 100 × (all cluster RoleBindings) = catastrophic
- After: 100 × (1-5 RoleBindings in namespace) = fast + cached by
controller-runtime

## Security Impact

**Before**: Any authenticated user could:
- Read metadata (labels, annotations, creation time) of any `tenant-*`
namespace via `Get()`
- Stream all tenant namespace events via `Watch()`, including
creation/modification/deletion

**After**: Users can only access tenant namespaces they have explicit
RoleBindings for, matching the behavior of the `List()` handler.

## Testing

Manually verified:
- Users can only `get` their own tenant namespaces
- Users can only `watch` events for their own tenant namespaces
- Unauthorized access returns `NotFound` (not `Forbidden`) to prevent
enumeration
- `List()` behavior remains unchanged and consistent with `Get()` and
`Watch()`
- Authorization errors are logged for security audit

## Checklist

- [x] Code follows project style and conventions
- [x] Security vulnerability is fully mitigated
- [x] Authorization logic is consistent across List/Get/Watch handlers
- [x] Performance optimized per feedback from @timofei.larkin
- [x] Error logging added for security audit
- [ ] Unit tests added (can be done in follow-up PR)

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

* **Bug Fixes**
* Enforced per-namespace access control for Get and Watch; inaccessible
namespaces return Forbidden or NotFound as appropriate
* Forwarded field/label selectors to upstream watches and added
defensive filtering to skip inaccessible events (logged)
* Improved ServiceAccount subject namespace fallback and
privileged-group bypass

* **Tests**
* Added security tests covering RoleBinding subjects, groups, privileged
bypasses, ServiceAccounts, and access-denied behaviors
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-04-28 19:07:07 +05:00
Aleksei Sviridkin
bdaaae92e2
feat(postgres): add serverName parameter for backup recovery (#2362)
## Summary

Add `serverName` field to bootstrap configuration to support backup
recovery when the Barman server name in `backup.info` differs from the
Kubernetes cluster name.

This fixes "no target backup found" errors during CloudNativePG recovery
operations.

## Problem

CloudNativePG forms the backup path as `destinationPath + "postgres-" +
oldName`, but searches for backups using the `server_name` field from
`backup.info`. When these values don't match (e.g., cluster name is
`grafana` but `server_name` is `cloud`), recovery fails with "no target
backup found".

## Solution

- Add optional `serverName` parameter to `bootstrap` configuration
- When specified, CloudNativePG uses this value to search for backups in
S3
- Falls back to `oldName` when `serverName` is not provided (backwards
compatible)

## Changes

- Add `ServerName` field to PostgreSQL CRD type definition
- Add conditional `serverName` to Cluster `externalClusters` template  
- Update `values.yaml` and README.md with `serverName` documentation
- Regenerate `values.schema.json` and `postgres.yaml` CRD

## Test plan

- [ ] Deploy postgres cluster with backups enabled
- [ ] Create backup
- [ ] Create new postgres instance with `bootstrap.enabled=true`,
`bootstrap.oldName=<original-name>`, and
`bootstrap.serverName=<value-from-backup.info>`
- [ ] Verify recovery completes successfully

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

* **New Features**
* Added an optional bootstrap.serverName setting to specify the Barman
server name from the original cluster’s backup.info (used when that
server name differs from the Kubernetes cluster name).

* **Documentation**
* Clarified that bootstrap.oldName must match the serverName value
recorded in backup.info; updated docs and examples to reflect the new
bootstrap.serverName option.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-04-28 15:32:57 +03:00
Aleksei Sviridkin
3364290761
feat(ingress): add loadBalancer exposure mode via CiliumLoadBalancerIPPool (#2468)
## What this PR does

Adds an opt-in `publishing.exposure=loadBalancer` mode for the
ingress-nginx Service as a migration path away from
`Service.spec.externalIPs`, which is deprecated upstream in Kubernetes
v1.36
([KEP-5707](https://github.com/kubernetes/enhancements/issues/5707),
[kubernetes#137293](https://github.com/kubernetes/kubernetes/pull/137293)).
The `AllowServiceExternalIPs` feature gate is expected to default to off
around v1.40 and the implementation to be removed around v1.43.

Stacked on top of #2464 (cilium v1.19.3 bump) — depends on it for the
`CiliumLoadBalancerIPPool` at `cilium.io/v2`.

### Behavior

- New platform value `publishing.exposure` — enum `externalIPs |
loadBalancer`, default `externalIPs` (current behavior unchanged on
upgrade).
- Plumbed through `cozystack-values` into each tenant's ingress
HelmRelease via the new `expose-mode` key.
- When `exposure=loadBalancer` and the current namespace matches
`publishing.ingressName`, the Service becomes `type: LoadBalancer` with
`externalTrafficPolicy: Local` and a `CiliumLoadBalancerIPPool`
announces the addresses from `publishing.externalIPs` via Cilium LB
IPAM.
- The pool uses a namespace-only `serviceSelector`
(`io.kubernetes.service.namespace: <ns>`) — any LoadBalancer Service in
the tenant namespace draws from it. See "Pool ownership" below.
- IPv4 addresses get `/32` CIDRs, IPv6 addresses get `/128`. Mixed
families supported. Pre-CIDR entries (`192.0.2.10/32`) are accepted
without double-suffixing.
- Unknown values, `loadBalancer` with empty externalIPs, and stray empty
entries from `publishing.externalIPs` are rejected at render time with
explicit error messages.

### Pool ownership

The `CiliumLoadBalancerIPPool` is rendered from
`packages/apps/tenant/templates/cilium-lb-pool.yaml`, not from the
ingress chart. The tenant chart is the per-tenant owner of cross-cutting
resources (Namespace, `cozystack-values` Secret, HelmReleases for
ingress and gateway), so one pool per tenant lives there.

Cilium LB IPAM rejects overlapping CIDRs across pools regardless of
`serviceSelector` — the last-added pool gets `cilium.io/PoolConflict`
and stops allocating. Keeping the pool in the ingress chart would
collide with the Gateway-API PR (#2470), which materialises its own
LoadBalancer Service from the same `publishing.externalIPs` range when
`tenant.spec.gateway=true`. Moving the pool to the tenant chart with a
namespace-only selector lets a single pool back both services
(ingress-nginx today, a Cilium Gateway Service once #2470 lands).

Only the ingress-loadBalancer signal is wired here
(`_cluster.expose-mode=loadBalancer` + `.Values.ingress=true` +
publishing tenant). #2470 rebases on top of this PR, drops its own
`packages/extra/gateway/templates/cilium-lb-pool.yaml`, and adds an OR
branch for `.Values.gateway` in the tenant template.

### Scope

Only the ingress-nginx Service is migrated by this setting. Other
cozystack components that still write `Service.spec.externalIPs`
directly (notably the `vpn` app at
`packages/apps/vpn/templates/service.yaml`) need separate follow-up
before Kubernetes v1.40.

### Tests

- `packages/extra/ingress/tests/exposure_test.yaml` — 10 helm-unittest
cases on the ingress-nginx Service: type / externalTrafficPolicy /
externalIPs assertion for both modes, unknown-mode rejection
(case-sensitive enum), empty-IPs failure, empty-entry filtering,
non-publishing-tenant fallback.
- `packages/apps/tenant/tests/exposure_test.yaml` — 9 cases on the pool
itself: IPv4, IPv6, mixed, pre-CIDR input, empty-entry filtering,
`ingress=false` in publishing tenant (no pool), non-publishing tenant
(no pool), empty externalIPs (no pool).
- Both suites are auto-discovered by `hack/helm-unit-tests.sh` via the
`test:` target in each package's Makefile.

### Caveats (copied from the inline `values.yaml` comment)

- `loadBalancer` mode uses `externalTrafficPolicy: Local`. The external
IP must already be routed to a node that hosts an ingress pod (floating
IP / upstream router / podAntiAffinity).
- Cilium does not announce the IP on its own unless L2 announcements or
BGP are enabled in the Cilium values (disabled by default in cozystack).
- Switching the value on a running cluster causes the ingress-nginx
Service to be recreated (`upgrade.force: true` on the HelmRelease +
Service kind change); expect a brief interruption of ingress traffic.

### Release note

```release-note
feat(ingress): add opt-in publishing.exposure=loadBalancer mode that uses type: LoadBalancer + CiliumLoadBalancerIPPool instead of the deprecated Service.spec.externalIPs. Default (externalIPs) preserves existing behavior.
```
2026-04-28 15:21:16 +03:00
Aleksei Sviridkin
6498c61f68
fix(postgres-operator): block HelmRelease Ready until the cnpg webhook actually serves (#2482)
## What this PR does

Closes a bootstrap race where the `cnpg-webhook-service` Service gets
its EndpointSlice populated and the data plane (kube-proxy / Cilium)
programmed a second or two after `helm install --wait` on
`cozy-postgres-operator` declares the HelmRelease Ready. Any HelmRelease
that `dependsOn: postgres-operator` and creates a
`postgresql.cnpg.io/v1` resource in that window (cozy-keycloak, tenant
Postgres apps) has kube-apiserver's call to `mcluster.cnpg.io` fail with

```
Internal error occurred: failed calling webhook "mcluster.cnpg.io":
Post "https://cnpg-webhook-service.cozy-postgres-operator.svc:443/...":
dial tcp <svc-ip>:443: connect: connection refused
```

which fails the downstream release's install.

The fix is a post-install / post-upgrade Helm hook (ServiceAccount +
ClusterRole + ClusterRoleBinding + Job) that probes `/readyz` on the
webhook through the apiserver service proxy. Apiserver's service proxy
uses the same endpoint-resolution and apiserver-initiated pod dial as
the admission webhook path, so once `/readyz` answers through the proxy,
the subsequent admission call will also succeed. Helm `--wait` blocks
the install from completing until the Job exits 0, so the HelmRelease
Ready condition does not lie anymore.

Hardened per review:

- RBAC scoped to a single `services/proxy` resourceName
(`https:cnpg-webhook-service:webhook-server`) — the exact string the
apiserver URL path parser expects for the Service proxy subresource.
- A drift-guard helm-unittest test renders the vendored cnpg Service
template and fails if its `metadata.name` / `ports[0].name` diverge from
the literals in the hook, so a future `make update` that renames the
service forces this template to be updated in the same change.
- Image digest-pinned (`clastix/kubectl:v1.32@sha256:…`) with a
`renovate:` annotation; `imagePullPolicy: IfNotPresent` when
digest-pinned, `Always` when tag-only.
- Job runs under PSA-restricted-compatible securityContext (non-root,
seccomp RuntimeDefault, readOnlyRootFilesystem, drop ALL caps).
- `activeDeadlineSeconds` is derived from `maxAttempts × sleepSeconds +
60s` so a values override raising retries does not get silently cut by a
fixed deadline.
- `backoffLimit: 2` (configurable) so a transient pod-level failure
(image pull rate limit, OOM, CNI hiccup) does not fail the whole
HelmRelease.
- On timeout the Job prints the last `kubectl get --raw` stderr so the
operator can distinguish DNS / refused / 401 / TLS from the Job logs.

21 helm-unittest assertions cover ordering, RBAC/URL parity, subchart
drift, image policy, retry-loop bounds, securityContext, and the
deadline-scaling invariant. Wired via `make test` in
`packages/system/postgres-operator/Makefile`.

First surfaced on #2470 E2E run 24862782568 where
`cozy-keycloak/keycloak` failed with the exact signature above. Not tied
to that PR's branch — this is independently applicable to `main`.

### Release note

```release-note
fix(postgres-operator): add a post-install readiness gate that blocks the HelmRelease from reporting Ready until the cnpg admission webhook actually serves through the cluster Service, preventing "dial tcp <svc-ip>:443: connect: connection refused" on the first HelmRelease that depends on postgres-operator (keycloak, tenant Postgres apps).
```


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

* **New Features**
* Added webhook readiness validation during install/upgrade to block
completion until the admission webhook is reachable.
* New configuration options for the readiness probe image and
retry/timeouts.

* **Tests**
* Introduced a comprehensive test suite validating rendered manifests,
probe behavior, RBAC, image rendering, and retry/timeout logic.

* **Chores**
* Added a test entry point to the project Makefile to run chart/unit
tests.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-04-28 15:14:36 +03:00
Timofei Larkin
1dc02d44f4 refactor(lineage-controller-webhook): align with cozystack-api shape
Collapse the chart to a single Deployment shape that mirrors
cozystack-api: 2 replicas, soft (preferred) nodeAffinity to
node-role.kubernetes.io/control-plane via Exists, permissive
tolerations, soft podAntiAffinity on hostname, an unconditional
PodDisruptionBudget with maxUnavailable: 1, and a Service with
spec.trafficDistribution: PreferClose. The same shape works on
Talos / kubeadm / k3s and on managed Kubernetes / Cozy-in-Cozy
tenant clusters without per-distro overrides — fixing #2417 by
making the soft control-plane affinity gracefully fall back to
worker scheduling when no control-plane nodes are visible.

Drop the DaemonSet path entirely. The previous PR's deployment.enabled
toggle, the workload-kind switch in templates/workload.yaml, the
fail-guard for localK8sAPIEndpoint+nodeAffinity=[], the conditional
PDB, and the values-shape knobs for nodeAffinity and tolerations all
go away as a consequence. Override the standard Deployment fields
through the usual component-values mechanism if a non-default topology
is ever needed.

Mark localK8sAPIEndpoint.enabled deprecated and flip the default to
false. The flag injects KUBERNETES_SERVICE_HOST=status.hostIP, which
is only valid when the pod is actually scheduled on an apiserver-
bearing node. With the new soft control-plane affinity, the pod can
land off-control-plane and crash-loop. The latency motivation for the
flag is real but pending separate webhook performance work; once
addressed, the flag can be removed.

Revert all changes to packages/core/platform/images/migrations/migrations/20.
The earlier ds/...-or-deploy/... fallback was over-engineered: per
run-migrations.sh, migration 20 fires only when CURRENT < 20 (i.e.
direct upgrades from pre-0.37 to 1.3+), and that path is unsupported
anyway. The original ds/... rollout-status line is dead code on every
supported install and upgrade path.

Add helm-unittest coverage for: the default Deployment shape (replicas,
soft nodeAffinity, soft podAntiAffinity, tolerations); no env vars at
the default localK8sAPIEndpoint.enabled=false; PDB rendered
unconditionally with maxUnavailable: 1, including at replicas=1 (the
no-op case); replicas value drives spec.replicas; and that
localK8sAPIEndpoint.enabled=true does inject the env vars. The Service
test continues to assert trafficDistribution: PreferClose with no
internalTrafficPolicy.

Add a slim README documenting the topology, parameters, and the
deprecation note for localK8sAPIEndpoint.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Timofei Larkin <lllamnyp@gmail.com>
2026-04-28 14:55:11 +03:00
Myasnikov Daniil
61ed7ad89c
fix(api): address review feedback on TenantNamespace watch path
- Hoist user identity extraction out of the Watch goroutine; reuse a
  cached username and groups map across events instead of re-fetching
  them per event. Watch now returns Unauthorized up front when no user
  is present in the context, rather than failing silently per event.
- Switch the per-event access-check error log to structured klog.ErrorS
  to comply with the project Go style guide.
- Strengthen TestGet_WithAccess to assert the concrete *TenantNamespace
  type plus Name, Kind, and APIVersion, so type or metadata regressions
  fail fast.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-04-28 16:33:37 +05:00
Aleksei Sviridkin
036eb98554
build(linstor): include linstor-gui in root image build target (#2498)
## What this PR does

The `linstor-gui` package (added in #2382) was never wired into the root
`Makefile`'s `build:` target, so the image is not built or published by
CI. `ghcr.io/cozystack/cozystack/linstor-gui` returns `NAME_UNKNOWN`
from the GHCR API, and `values.yaml` still pins `tag: 2.3.0` without a
digest because the per-package Makefile that rewrites the tag after a
successful push has never run in CI.

This PR adds the missing line so the next build publishes the image and
digest-pins `values.yaml` automatically.

### Release note

```release-note
build(linstor): include linstor-gui in root image build target so the image is built and published by CI (the chart previously referenced an image that did not exist in the registry)
```

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

* **Chores**
* Integrated the linstor-gui container image build into the main image
build workflow so the GUI image is produced as part of standard builds.
* Streamlined the GUI image build configuration to use consolidated
build arguments, improving consistency and maintainability of automated
image builds and metadata updates.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-04-28 13:55:25 +03:00
Andrei Kvapil
d2b4a449de
feat(cozy-proxy): bump to v0.3.0 (#2510)
Bumps the vendored cozy-proxy chart and image tag from v0.2.0 to v0.3.0
by running `make update` in `packages/system/cozy-proxy`.

cozy-proxy v0.3.0 release:
https://github.com/cozystack/cozy-proxy/releases/tag/v0.3.0

## Why

cozy-proxy v0.3.0 is the companion of #2501. The chart fix in #2501
finally honors the documented PortList semantics (only `externalPorts`
reachable, ICMP preserved by default), but only takes effect once
cozy-proxy v0.3.0 is in place — v0.2.0 has no port-aware logic, so the
chart fix silently no-ops on it. This PR ships that runtime side.

## Upgrade impact

For cozystack users this is a **bug fix, not a breaking change**.
cozy-proxy v0.3.0 introduces two contract changes upstream — (1)
label-only selector via `service.kubernetes.io/service-proxy-name:
cozy-proxy`, and (2) port-filter as the default ingress mode — both of
which are absorbed by the vm-instance chart (label landed in #2357,
`wholeIP` + `allowICMP` wired in #2501).

- **`externalMethod: WholeIP`** (default) — chart renders label +
`wholeIP: "true"` in both old and new versions. cozy-proxy keeps
passthrough. **No change.**
- **`externalMethod: PortList`** — previously the chart always rendered
`wholeIP: "true"` regardless of method, and cozy-proxy v0.2.0 had no
port-aware logic, so PortList was silently a no-op (all ports
reachable). After this bump + #2501, PortList behaves as documented:
only ports listed in `externalPorts` reach the VM. Users who configured
PortList but reached undeclared ports (relying on the silent no-op) will
see those ports become unreachable — that's the documented intent of
PortList finally taking effect. ICMP keeps working by default thanks to
`externalAllowICMP: true`.
- **DaemonSet rollover** — re-init of the nft table during cozy-proxy
pod restart is a brief blip on existing flows; same as any DaemonSet
upgrade.

Out-of-tree consumers using cozy-proxy directly (without the
`service-proxy-name` label, or relying on the absent-annotation
passthrough default) need to migrate per the upstream v0.3.0 release
notes — not a cozystack concern.

## Order of operations

This PR is functionally complete on its own (image bump), but the
user-facing PortList fix it enables only happens when #2501 also lands.
Recommended merge order:

1. #2501 first (chart adapts to the new contract)
2. This PR second (cozy-proxy DaemonSet picks up the new image)

The reverse order is also safe — cozy-proxy v0.3.0 with the unmodified
vm-instance chart still produces correct results for `externalMethod:
WholeIP` (most common case) and keeps PortList silently broken until
#2501 lands.

## Test plan

- [ ] `helm template packages/system/cozy-proxy` — image tag `v0.3.0`
rendered
- [ ] `make unit-tests` passes
- [ ] After #2501 lands, end-to-end: `externalMethod: PortList` with
`externalPorts: [22]` filters all other ports, ping works
- [ ] WholeIP-method VM Service unchanged after upgrade

## Companion PRs

- #2501 — vm-instance chart wires `wholeIP` / `allowICMP` per
`externalMethod`
- cozystack/cozy-proxy#13 (merged) — label-only selector + port-filter
default
- cozy-proxy v0.3.0 release notes —
https://github.com/cozystack/cozy-proxy/releases/tag/v0.3.0

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

## Summary by CodeRabbit

* **Chores**
* Released version 0.3.0 with updated Helm chart metadata and container
image configuration.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-04-28 12:21:33 +02:00
myasnikovdaniil
36e326d768
fix(etcd): remove destructive post-upgrade cert-regeneration hook (#2462)
## What this PR does

Removes the `post-upgrade` Helm hook in `packages/extra/etcd` that was
`kubectl delete`ing the etcd TLS chain (`etcd-ca-tls`,
`etcd-peer-ca-tls`,
`etcd-client-tls`, `etcd-peer-tls`, `etcd-server-tls`) and then deleting
etcd pods on every chart upgrade.

The hook was gated by a semver compare of a stored
`etcd-deployed-version`
ConfigMap against `2.6.1`. That gate was written when chart versions
looked
like `2.6.0`, `2.6.1`, etc. Since commit `f871fbdb` ("Remove
versions_map
logic") all chart versions are stamped as `0.0.0+<git-hash>`, which per
semver is always `< 2.6.1`, so the gate always evaluates to "regenerate
certs" and the destructive hook fires on every upgrade.

On clusters running Kamaji-managed tenant control planes the consequence
is
severe: wiping the etcd CA triggers cert-manager to re-issue it with a
brand-new CA, but Kamaji's `datastore-certificate` Secrets still carry
the
old CA bundle mounted into tenant `kube-apiserver` pods. Those
apiservers
hit `x509: certificate signed by unknown authority` against
`etcd.<ns>.svc:2379` and go into CrashLoopBackOff until each tenant
`DataStore` is individually force-reconciled and the Deployments rolled.

Commit `47d81f70` ("Disabled private key rotation in CA certs") already
fixed the underlying `rotationPolicy: Always` issue the hook was written
to
paper over in March 2025, so the migration has no remaining use.

Changes:
- Delete `templates/hook/{job,role,rolebinding,serviceaccount}.yaml`
- Delete `templates/version.yaml` (only the hook read
`etcd-deployed-version`)
- Add `tests/no-post-upgrade-hook_test.yaml` as a regression guard — if
`templates/hook/job.yaml` or `templates/version.yaml` is ever brought
back,
  `make unit-tests` fails

### Release note

```release-note
fix(etcd): remove destructive post-upgrade cert-regeneration hook. Previously the etcd chart ran a `post-upgrade` hook on every Helm upgrade that deleted etcd TLS Secrets and pods, causing cert-manager to re-issue the etcd CA. On clusters with Kamaji-managed tenant control planes this put every tenant `kube-apiserver` into CrashLoopBackOff until each DataStore was manually re-reconciled. The hook was a one-shot `2.6.0 -> 2.6.1` migration that became a permanent footgun once chart versioning moved to `0.0.0+<git-hash>` and the underlying `rotationPolicy: Always` issue was fixed in commit `47d81f70`. This is behavior removal, not cosmetic cleanup.
```


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

## Summary by CodeRabbit

* **Tests**
* Added Helm chart test suite verifying absence of post-upgrade hook Job
and version ConfigMap.

* **Chores**
* Removed post-upgrade hook Job and associated RBAC resources (Role,
RoleBinding, ServiceAccount).
  * Removed version ConfigMap.
  * Added `test` target to Makefile for running Helm chart unit tests.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-04-28 14:36:59 +05:00
IvanHunters
7257b6aed4 fix(api): address review feedback on TenantNamespace RBAC
Address review feedback from sircthulhu and CodeRabbit:

1. Return Forbidden instead of NotFound for unauthorized access
   - Get() now returns 403 Forbidden to follow standard K8s RBAC behavior
   - Previously returned 404 NotFound for security-by-obscurity
   - Updated test expectations to match new behavior

2. Propagate field and label selectors in Watch handler
   - Pass opts.FieldSelector and opts.LabelSelector to upstream Watch
   - Add defensive filtering before authorization to prevent RBAC bypass
   - Fixes potential issue with resourceNames restrictions

3. Refactor subject-matching logic to eliminate duplication
   - Extract matchesSubject() helper for Group/User/ServiceAccount checks
   - Remove duplicated code from filterAccessible and hasAccessToNamespace
   - Consolidates ServiceAccount namespace fallback logic

All tests pass successfully.

Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>
2026-04-28 12:26:57 +03:00
Andrei Kvapil
bbe0e8690c
[vm-instance] Fix PortList not filtering ingress ports (#2501)
## What this PR does

The `vm-instance` chart now drives the cozy-proxy `wholeIP` and
`allowICMP` annotations explicitly so that `externalMethod: PortList`
actually filters ingress traffic to declared ports while keeping
ping/PMTU functional.

- Render `networking.cozystack.io/wholeIP: "false"` when
`externalMethod: PortList` (was always `"true"`, which silently disabled
the PortList semantics in cozy-proxy).
- Add `externalAllowICMP` value (default `true`) propagated as
`networking.cozystack.io/allowICMP` when `externalMethod: PortList`.
Without this, cozy-proxy drops ICMP in port-filter mode (ping/PMTU
broken). Operators can set `externalAllowICMP: false` to opt out.

The changelog entry is intentionally **not** part of this PR — it will
be added in a dedicated `docs: add changelog for vX.Y.Z` commit at
release time, per project convention.

## Why

`externalMethod: PortList` is documented as filtering ingress traffic to
declared ports but has been non-functional on Cozystack v1.3.0 —
verified empirically on a 3-node Talos lab. Root cause was twofold:
chart always set `wholeIP: "true"`, and cozy-proxy v0.2.0 had no
port-aware logic. The cozy-proxy side was fixed in
cozystack/cozy-proxy#11 (merged) and cozystack/cozy-proxy#12 (allowICMP
follow-up); this PR completes the user-visible fix on the chart side.

## Companion PRs

- cozystack/cozy-proxy#11 (merged) — per-service ingress port filtering
- cozystack/cozy-proxy#12 (merged) — `allowICMP` annotation for
port-filter mode

## Test plan

- [x] Built cozy-proxy with the companion fix locally, deployed on a
3-node Talos lab (Cozystack v1.3.0)
- [x] `wholeIP: "false"` Service with `spec.ports: [22]`: only port 22
reachable from outside; ports 80/443/8080/9999 filtered
- [x] WholeIP-annotated Service unchanged: all listening ports reachable
- [x] Egress IP preservation works in both modes (TCP curl + UDP DNS)
- [x] `nft list table ip cozy_proxy` confirms expected ruleset
- [x] `helm template` renders the expected annotation matrix: `PortList`
default → `wholeIP=false, allowICMP=true`; `PortList` opt-out →
`allowICMP=false`; `WholeIP` → only `wholeIP=true`
- [x] `make unit-tests` passes locally
- [ ] CI unit tests
- [ ] CI E2E

## Backport

Suggesting `backport-v1.3` once merged.

## Release note

```release-note
[vm-instance] Make `externalMethod: PortList` actually filter ingress traffic to ports listed in `externalPorts`. New `externalAllowICMP` knob (default true) propagates the cozy-proxy `allowICMP` annotation to keep ping/PMTU functional in port-filter mode. Combined with cozy-proxy v0.3.0+, only listed ports plus ICMP are reachable from the VM's LoadBalancer IP.
```


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

## Summary by CodeRabbit

* **New Features**
* Added `externalAllowICMP` configuration option to control ICMP traffic
acceptance for VM external access in PortList mode (enabled by default).

* **Documentation**
* Updated parameter documentation to include the new ICMP traffic
control setting.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-04-28 11:17:59 +02:00
Arsolitt
b2a8cca3bb
fix(monitoring): filter zero-valued series from active tenants count
Address review feedback from coderabbitai on dashboards/gpu/gpu-tenants.json:39

Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-28 12:15:32 +03:00
Andrei Kvapil
d20285836c
feat(cozy-proxy): bump to v0.3.0
Pulls in the per-port filtering and allowICMP support that the companion
vm-instance chart fix in #2501 relies on. cozy-proxy v0.3.0 also tightens
the selector to the standard service.kubernetes.io/service-proxy-name=cozy-proxy
label and switches the default ingress mode to port-filter; both are
already covered by the vm-instance chart (label landed in #2357,
wholeIP/allowICMP wired explicitly in #2501), so VM workloads upgrade
transparently.

Out-of-tree consumers using cozy-proxy annotations directly (without the
label, or relying on the absent-annotation passthrough default) are
called out in the upstream v0.3.0 release notes:
https://github.com/cozystack/cozy-proxy/releases/tag/v0.3.0

Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2026-04-28 11:14:27 +02:00
Arsolitt
2a6653e11a
docs(monitoring): add panel descriptions to GPU quotas dashboard
Regenerated from SDK source.

Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-28 12:08:15 +03:00
Arsolitt
84f506116f
docs(gpu-operator): clarify violation counter unit ambiguity in DCGM CSV
Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-28 12:07:31 +03:00
Arsolitt
ed6f9bbd1d
fix(monitoring): regenerate gpu-quotas dashboard from SDK
Align dashboard JSON with the SDK source of truth.

Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-28 12:02:18 +03:00
Arsolitt
b0784c0d33
fix(monitoring): generalize GPU temperature description in fleet dashboard
Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-28 11:44:11 +03:00
Arsolitt
cacd3714bd
fix(monitoring): include phase label in GPU limits query for consistency
Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-28 11:44:08 +03:00
Arsolitt
31de9989f6
fix(gpu-operator): add DCGM_FI_DRIVER_VERSION to custom metrics CSV
Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-28 11:44:04 +03:00
Arsolitt
8e6266703d
Revert "chore: ignore CLAUDE.local.md"
This reverts commit 11f7d3589b.

Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-28 11:43:30 +03:00
Arsolitt
aba5ae3fcd
fix(monitoring): prevent many-to-many match in util-per-watt recording rule
Address review feedback from coderabbitai on packages/system/monitoring-agents/alerts/gpu-recording.rules.yaml:119

Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-28 11:21:12 +03:00
Arsolitt
5718740ae3
docs(gpu-operator): correct gpu-quotas dashboard dependencies in README
Address review feedback from coderabbitai on packages/system/gpu-operator/examples/README.md:85

Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-28 11:20:57 +03:00
Arsolitt
bbf338a57d
fix(gpu-operator): fail fast on missing artifacts in driver-compat example
Address review feedback from coderabbitai on packages/system/gpu-operator/examples/nvidia-driver-compat.yaml:77

Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-28 11:20:41 +03:00
Arsolitt
4c697982b2
fix(monitoring): use Hostname label in GPU tenants dashboard legends
Address review feedback from coderabbitai and gemini-code-assist on dashboards/gpu/gpu-tenants.json:532

Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-28 11:20:19 +03:00
Arsolitt
452bff4567
fix(monitoring): remove unused namespace variable from GPU performance dashboard
Address review feedback from coderabbitai on dashboards/gpu/gpu-performance.json:277

Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-28 11:19:56 +03:00
Arsolitt
bb51c88f78
fix(monitoring): remove unused namespace variable from GPU efficiency dashboard
Address review feedback from coderabbitai on dashboards/gpu/gpu-efficiency.json:839

Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-28 11:18:59 +03:00
myasnikovdaniil
82d1f8a1d2
ci(api): add codegen drift check (#2463)
## What this PR does

- Adds a pre-commit hook and a dedicated GitHub Actions workflow that
run `make generate` at the repo root to catch drift in generated API
code (CRDs, DeepCopy, clients, RBAC) before it lands on main.
- Pre-commit hook is scoped to paths that actually affect codegen
(`api/`, `pkg/apis/`, `hack/update-codegen.sh`,
`hack/boilerplate.go.txt`) so unrelated commits are not slowed down.
- CI workflow (`.github/workflows/codegen-drift.yml`) installs Go from
`go.mod`, runs `make generate`, and fails on drift with an error
pointing contributors to the local fix.
- Includes one drift fix the check surfaced: `RestoreJobSpec.Options`
(added in #2437) had no `DeepCopyInto` handling — regenerated.

### Release note

```release-note
ci(api): add pre-commit hook and GitHub Actions workflow that verify generated API code (CRDs, deepcopy, clients, RBAC) is in sync with `make generate`.
```

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

* **Chores**
* Added a CI workflow that checks generated code during PRs and fails
the build if generated artifacts diverge.
* Added a pre-commit hook that runs generation checks locally to prevent
committing outdated generated files.
* Fixed deep-copy behavior for backup restore job specs so nested
options are correctly duplicated.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-04-28 12:41:48 +05:00
mattia-eleuteri
b0afc9a07c
[vm-instance] Add externalAllowICMP knob, drop in-PR changelog
- Add `externalAllowICMP` value (default true) propagated as
  `networking.cozystack.io/allowICMP` annotation on the rendered Service
  when `externalMethod: PortList`. The cozy-proxy companion (released as
  part of cozystack/cozy-proxy#11 + #12) drops ICMP by default in
  port-filter mode, which breaks ping and PMTU discovery; defaulting the
  chart to "true" preserves user expectations while still allowing
  operators to opt out by setting `externalAllowICMP: false`.

- Remove the v1.3.1.md changelog entry. Project convention is to add
  changelogs in a dedicated "docs: add changelog for vX.Y.Z" commit at
  release time, not as part of feature/fix PRs.

Signed-off-by: mattia-eleuteri <mattia@hidora.io>
2026-04-28 08:37:13 +02:00
Andrei Kvapil
f1e56417a0
feat(linstor): bump linstor-csi to v1.10.6 with Protocol-C dual-attach fix (#2496)
## What this PR does

Bumps `linstor-csi` from v1.10.5 to v1.10.6 and ships an out-of-tree
patch that fixes live migration of KubeVirt VMs whose volumes sit on a
DRBD Protocol-A/B resource (e.g. a `replicated-async` StorageClass).

### Problem

DRBD requires Protocol C whenever `allow-two-primaries=yes` is enabled.
Operators commonly opt into Protocol A on a per-resource-group basis for
async / WAN replication, which silently breaks every subsequent live
migration of consumers of those volumes: `drbdadm adjust` rejects the
second-attach with `Protocol C required` (errno 139), KubeVirt's
evacuation loop retries indefinitely, and the VM stays pinned to the
source node.

### Change

- `LINSTOR_CSI_VERSION` 1.10.5 → 1.10.6 (Makefile + Dockerfile default).
- New patch `002-protocol-c-override-for-dual-attach.diff`: when
`Attach` installs `allow-two-primaries=yes` on the resource-definition
during a second attach, it also installs `DrbdOptions/Net/protocol=C` as
an override on the resource-definition. The override applies to every
connection (including diskless TieBreaker peers, where a per-pair
override would still leave one connection broken). It is tagged with
`Aux/csi-protocol-override=yes` so `Detach` removes only the override
this driver installed, leaving any operator-set `Protocol` property on
the resource-definition untouched.
- Existing patch `001-relocate-after-clone-restore.diff` regenerated
against v1.10.6 (context shift only, no logic change — the old patch
hunks no longer aligned cleanly).

### Verification

- `make image-linstor-csi` builds successfully on linux/amd64 with both
patches applied.
- End-to-end test on dev5 cluster (KubeVirt v1.6.3, 3-node Talos):
created a Protocol-A resource-group + StorageClass, provisioned a VM on
top, and triggered live migration. Migration succeeds in a single Attach
with the override installed during dual-attach and removed by Detach.
Reproducer (without the patch) is the well-known evacuation loop with
`(node) Failed to adjust DRBD resource ... Protocol C required`.

### Upstream

Upstreamed as draft PR piraeusdatastore/linstor-csi#435.

### Compatibility

- No behaviour change for resources already using Protocol C (the common
case).
- No behaviour change for resources never attached with
allow-two-primaries.
- Idempotent: re-running `Attach` is a no-op once the override is
installed.
- Operator-set Protocol overrides on the resource-definition are
preserved (gated by the Aux marker).

### Release note

```release-note
fix(linstor): live migration of KubeVirt VMs on Protocol-A/B (async) DRBD volumes no longer fails with "Protocol C required" — linstor-csi now installs a Protocol=C override on the resource-definition during dual-attach and reverts it on detach.
```

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

* **New Features**
* Automatic replica relocation after volume clone and snapshot restore
to improve placement and load distribution.
* Conditional DRBD protocol override to enable/clean up dual-attach
(two-primaries) scenarios more reliably.

* **Chores**
  * Updated LINSTOR CSI default to v1.10.6.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-04-27 20:50:34 +02:00
mattia-eleuteri
80631bc916
[vm-instance] Set wholeIP annotation conditionally on externalMethod
Render `networking.cozystack.io/wholeIP: "false"` on the Service when
`externalMethod: PortList` is configured (was always `"true"` before).
Combined with cozy-proxy v0.3.0+ which adds per-port filtering for
"false"-annotated services, this makes `externalMethod: PortList`
behave as documented: only ports listed in `externalPorts` are
reachable from the LoadBalancer IP.

Backward-compatible: existing services with `externalMethod: WholeIP`
continue to set `wholeIP: "true"` and behave identically. cozy-proxy
versions older than v0.3.0 ignore Services with `wholeIP: "false"`,
which means PortList Services on older cozy-proxy will lose their
egress IP preservation — but that path was already non-functional
ingress-wise, so this is not a regression for users actually relying
on PortList.

Signed-off-by: mattia-eleuteri <mattia@hidora.io>
2026-04-27 16:29:05 +02:00
Andrei Kvapil
073fb1630d
feat(linstor): bump linstor-csi to v1.10.6 with Protocol-C dual-attach fix
Bump linstor-csi to v1.10.6 and add an out-of-tree patch that overrides
DrbdOptions/Net/protocol to C on the resource-definition whenever a
volume is attached to a second node with allow-two-primaries (the
live-migration flow), and reverts it on detach.

Without the patch, any KubeVirt live migration of a VM whose volume
sits on a Protocol-A/B resource (for example a "replicated-async"
StorageClass) ends up in a permanent evacuation loop: drbdadm adjust
on the satellite rejects allow-two-primaries with "Protocol C
required" (errno 139), the migration fails, and KubeVirt retries
until manual intervention.

The override is set at the resource-definition level so it covers
every connection, including diskless TieBreaker peers, and is tagged
with an Aux marker so Detach reverts only the override we installed.
The 001 relocate-after-clone patch was regenerated against v1.10.6
(context shift only, no logic change).

Verified end-to-end on dev5: live migration of a KubeVirt VM whose
PVC sits on a Protocol-A resource-group now succeeds in a single
Attach.

Patch is upstreamed as draft PR piraeusdatastore/linstor-csi#435.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2026-04-27 14:50:47 +02:00
Myasnikov Daniil
a9a66bf066
build(linstor): use shared BUILDX_ARGS for linstor-gui image build
The per-package Makefile added in #2382 hardcoded buildx flags
(--provenance, --builder, --platform=linux/amd64,linux/arm64,
--push, --load, --label) instead of using the shared $(BUILDX_ARGS)
macro from hack/common-envs.mk.

This broke CI: the runner's default docker driver does not support
multi-platform builds, and the hardcoded multi-arch platform list
crashed `make build` with "Multi-platform build is not supported for
the docker driver."

Replace the hardcoded flags with $(BUILDX_ARGS) to match every other
package (e.g. linstor, dashboard, cilium). $(BUILDX_ARGS) injects
--push, --load, --label, --provenance=false, and only sets --builder
or --platform when the operator explicitly exports BUILDER/PLATFORM.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-04-27 17:31:07 +05:00
Myasnikov Daniil
7443e22345
build(linstor): include linstor-gui in root image build target
The linstor-gui package was added in #2382 with its own per-package
Makefile and Dockerfile, but the root Makefile's `build:` target was
not updated to invoke it. As a result `ghcr.io/cozystack/cozystack/
linstor-gui` has never been published (registry returns NAME_UNKNOWN)
and the chart's `image.tag` was never digest-pinned. Any cluster
deploying the chart hits ImagePullBackOff.

Wire the package into the root build alongside the other system
images. The next CI build will publish the image and the per-package
Makefile will rewrite values.yaml `image.repository`/`image.tag` to a
digest-pinned reference automatically.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-04-27 17:24:38 +05:00
Arsolitt
9ba5e58781
fix(kubernetes): avoid rendering empty values key in gpu-operator HelmRelease
Wrap the values: block in a conditional so the key is omitted entirely
when no defaults or overrides produce content. Previously the template
always emitted values: null, triggering unnecessary FluxCD reconciliation.

Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-27 13:44:17 +03:00
Arsolitt
c53f104750
docs(hami): clarify that parameter defaults come from upstream chart
Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-27 13:44:12 +03:00
Arsolitt
f0e033ebcb
style(kubernetes): use with instead of if for hami valuesOverride
Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-27 13:44:08 +03:00
Arsolitt
36852548e5
fix(hami): correct label indentation in device-plugin monitorservice
Use nindent instead of indent with leading whitespace to prevent
broken YAML rendering when devicePlugin.service.labels is set.

Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-27 13:29:15 +03:00
Arsolitt
d8d870cc2a
fix(hami): use RollingUpdate strategy for device plugin DaemonSet
OnDelete requires manual pod deletion to apply updates. RollingUpdate
with maxUnavailable constraint matches upstream default and is consistent
with all other system packages.

Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-27 13:24:27 +03:00
Arsolitt
f866c71b68
fix(gpu-operator): add node relabel to example serviceMonitor values
Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-27 12:51:31 +03:00
Arsolitt
27225f9e83
fix(monitoring): use node-level GPU metrics instead of namespace-level
DCGM exporter metrics carry the exporter's own namespace
(cozy-gpu-operator), not the workload namespace. Recording rules that
filtered namespace!~"cozy-.*" silently dropped all DCGM series,
producing empty dashboard panels.

Replace namespace-level hardware aggregations with node-level
equivalents (grouped by Hostname), keep namespace-level allocation
rules that use kube_pod_container_resource_requests (which carries the
real workload namespace), and rename pod-level efficiency rules to
gpu-level since DCGM cannot attribute hardware metrics to individual
pods.

Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-27 12:46:20 +03:00
Aleksei Sviridkin
7206a6780d
ci: retry CI
Empty commit to retrigger Build that lost the OCI registry tag race
on the previous attempt.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-04-26 02:46:30 +03:00
Aleksei Sviridkin
ea0b4bcf37
ci: retry CI
Empty commit to retrigger Build job that failed on Docker Hub anonymous
pull rate limit (429 Too Many Requests) for golang:1.26.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-04-26 01:24:25 +03:00
Aleksei Sviridkin
500816b71b
refactor(ingress): move CiliumLoadBalancerIPPool to tenant chart
The pool is now rendered from packages/apps/tenant/templates/cilium-lb-pool.yaml instead of packages/extra/ingress/templates/cilium-lb-pool.yaml. Cilium LB IPAM forbids overlapping CIDRs across pools regardless of serviceSelector, so both the ingress-loadBalancer path and the upcoming per-tenant Gateway in #2470 cannot each own their own pool on the same publishing.externalIPs range. The tenant chart is the natural per-tenant owner — it already creates the Namespace, the cozystack-values Secret, and the HelmReleases for both ingress and gateway.

The new pool uses a namespace-only serviceSelector (io.kubernetes.service.namespace: <ns>), which matches any LoadBalancer Service in the tenant namespace. The metadata.name changed from <trim>-ingress to <trim>-exposure to reflect that the pool is not ingress-specific.

Only the ingress-loadBalancer signal is wired in this commit (_cluster.expose-mode=loadBalancer plus .Values.ingress=true on the publishing tenant). The gateway branch is added in #2470 on top of this commit — it rebases, drops its own packages/extra/gateway/templates/cilium-lb-pool.yaml, and adds an OR branch for .Values.gateway in the tenant template.

Pool-rendering unit tests moved from packages/extra/ingress/tests/ to packages/apps/tenant/tests/. The ingress chart tests keep the Service-level asserts. packages/apps/tenant/Makefile gains a test target so hack/helm-unit-tests.sh picks up the new suite.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-04-24 16:44:46 +03:00
Arsolitt
3eeda2ba35
chore(kubernetes): regenerate code after hami addon addition
Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-24 16:12:34 +03:00
Arsolitt
1131e2f113
docs(hami): reference upstream repo for nodeConfiguration format
Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-24 15:33:07 +03:00
Arsolitt
6a9c310a4b
fix(hami): conditional values emission and cosmetic template cleanup
Only emit values key in hami.yaml when valuesOverride has content,
matching gpu-operator pattern. Add test verifying empty valuesOverride
does not produce spec.values. Fix trailing whitespace and missing
newlines in vendored chart templates.

Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-24 15:31:24 +03:00
Arsolitt
37d5ff0c6f
fix(hami): align templates with project patterns and clean dead code
Unconditionally emit values in hami.yaml matching the project pattern.
Remove duplicate test case and add coverage for omitted valuesOverride key.
Delete dead PSP template and RBAC rules (policy/v1beta1 removed in K8s
1.25). Override kube-scheduler image registry to registry.k8s.io to avoid
Chinese registry for international users.

Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-24 14:07:17 +03:00
Arsolitt
2734dc0bcb
fix(hami): clean up leftover hami-dra references and harden defaults
Remove broken hami-dra subchart dependency from vendored chart
(Chart.yaml, Chart.lock, values.yaml) and strip DRA condition guards
from all templates since the subchart was already deleted. Override
devicePlugin updateStrategy to OnDelete to prevent destructive rolling
updates of GPU workloads. Align gpu-operator template with project
pattern (unconditional values emission). Add nodeConfiguration format
documentation and test for conflicting valuesOverride scenario.

Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-24 14:00:48 +03:00
Aleksei Sviridkin
7da29afe6e
docs(postgres-operator): fix backoffLimit/activeDeadlineSeconds comment math
Address review feedback from coderabbitai on
packages/system/postgres-operator/values.yaml:30.

The old comment's 5-minute ImagePullBackOff scenario conflicted with
the 180s activeDeadlineSeconds that the default maxAttempts/sleepSeconds
resolve to, so the numbers could not both be taken at face value.

Rewrite the comment to state the actual deadline math and frame the two
gates as an AND with activeDeadlineSeconds being the shorter one under
defaults, so readers understand why backoffLimit has little headroom
without an accompanying maxAttempts/sleepSeconds bump.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-04-24 13:33:07 +03:00
Aleksei Sviridkin
fc057c0393
fix(postgres-operator): set resources on webhook-ready wait container
Address review feedback from gemini-code-assist on
packages/system/postgres-operator/templates/webhook-ready-hook.yaml:115.

The wait container had no resource requests or limits, so schedulers
treated it as BestEffort and downstream quota enforcement had no signal.
Set small requests (10m CPU, 32Mi memory) and conservative limits
(100m CPU, 64Mi memory) matching the actual footprint of the kubectl
polling loop.

Add a matching unittest assertion so the values stay in sync if anyone
touches the template.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-04-24 13:32:33 +03:00
Aleksei Sviridkin
648ad82dd3
refactor(postgres-operator): scope webhook-ready RBAC to release namespace
Address review feedback from gemini-code-assist on
packages/system/postgres-operator/templates/webhook-ready-hook.yaml:83.

The Job targets the cnpg-webhook-service/services/proxy subresource, which
is namespaced and lives in the release namespace. A namespaced Role and
RoleBinding grant the exact permission needed without creating global
RBAC for a namespaced probe, which is the principle of least privilege.

Also update the kind assertions in tests/webhook-ready-hook_test.yaml so
the unittest suite tracks the new Role/RoleBinding objects.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-04-24 13:31:57 +03:00
Arsolitt
ab7deb2b05
chore(kubernetes): regenerate after HAMi addon integration
Run make generate to update generated types, schema, README,
and CRD definitions for the new HAMi addon.

Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-24 13:03:49 +03:00
Denis Chernosov
3b35404489
fix(cozystack-engine): add managed Kubernetes without visible control-plane nodes support to lineage-controller-webhook (#2417)
Signed-off-by: Denis Chernosov <denis0.ru@gmail.com>
2026-04-24 11:32:20 +04:00
Aleksei Sviridkin
9b87bda06a
fix(postgres-operator): block HelmRelease Ready until the cnpg webhook actually serves
The cnpg admission webhook's controller pod passes readinessProbe as soon
as the local HTTPS server binds to :9443. The HelmRelease marks itself
Ready right after that via helm install --wait. But the Service
cnpg-webhook-service needs its EndpointSlice populated and the data plane
(kube-proxy / cilium) programmed before kube-apiserver can reach the
webhook through the Service ClusterIP. That gap is short but not zero,
and any HelmRelease that depends on postgres-operator (cozy-keycloak,
tenant Postgres apps) can fire its own install inside the window and hit

  Internal error occurred: failed calling webhook "mcluster.cnpg.io":
  failed to call webhook: Post "https://cnpg-webhook-service.cozy-postgres-operator.svc:443/...":
  dial tcp <svc-ip>:443: connect: connection refused

which fails the install of the downstream release. Seen on cozystack/cozystack#2470
E2E run 24862782568.

Add a post-install,post-upgrade Helm hook Job that blocks the release
from reporting Ready until the webhook answers /readyz through the
apiserver service proxy. Apiserver proxy routes the call over the same
Service IP → EndpointSlice → pod path the admission webhook uses, so
once it responds, the webhook admission path is also working.

RBAC is minimal: a dedicated ServiceAccount with a ClusterRole that only
grants get on services/proxy scoped to https:cnpg-webhook-service:webhook-server.
The Job times out after 120s with 60 attempts at 2s intervals — longer
than any data-plane programming delay seen on E2E, but bounded.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-04-24 03:51:54 +03:00
Aleksei Sviridkin
6af74cec0e
fix(ingress): accept pre-CIDR externalIPs in CiliumLoadBalancerIPPool
Address review feedback from gemini-code-assist on
packages/extra/ingress/templates/cilium-lb-pool.yaml:19: if the operator
passes an externalIP already in CIDR form (192.0.2.10/32 or 2001:db8::1/128),
the template appended a second /32 or /128 suffix producing an invalid
CiliumLoadBalancerIPPool block. Guard the suffix append on the absence of
"/" in the input.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-04-23 23:24:38 +03:00
Arsolitt
385ea7a17f
feat(platform): register HAMi as optional system package
Add PackageSource for HAMi with dependency on gpu-operator.
Include HAMi in the iaas bundle as an optional package, allowing
host-level deployment alongside the existing kubernetes addon path.

Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-23 22:16:34 +03:00
Arsolitt
3c5521ee99
fix(hami): remove broken hami-dra subchart
The hami-dra subchart renders resources even when dra.enabled=false
because Helm dependency conditions don't work for vendored subcharts.
The subchart also contains duplicate YAML label keys
(app.kubernetes.io/component, app.kubernetes.io/name) and references
unpublished container images (v0.0.1-dev). DRA support can be
re-added when the upstream project stabilizes it.

Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-23 22:16:30 +03:00
Myasnikov Daniil
9d552d4086
ci(api): broaden codegen drift trigger paths and detect untracked files
- Add generated-output dirs (pkg/generated, internal/crdinstall/manifests,
  packages/system/*/definitions) and Makefile to the workflow paths: filter
  so PRs that modify only generated artifacts still trigger the drift check.
- Mirror the same paths in the root pre-commit hook's files: regex so
  manual edits to generated files or changes to the root generate target
  re-run make generate through pre-commit.
- Switch drift detection in the workflow from `git diff --exit-code` to
  `git status --porcelain` so new untracked files produced by make generate
  (e.g. generated YAML/Go for a new API type) also fail the job; dump
  `git diff --color=always` on failure for easier debugging.

Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-04-23 22:19:35 +05:00
IvanHunters
5b2501db91 test(api): add security tests for TenantNamespace IDOR fix
Added comprehensive unit tests for authorization logic:

- hasAccessToNamespace() tests:
  - User subject access (positive and negative cases)
  - Group subject access
  - ServiceAccount subject access
  - ServiceAccount with empty namespace (defaults to RoleBinding ns)
  - Privileged groups (system:masters, cozystack-cluster-admin)

- Get() handler tests:
  - Returns namespace when user has access
  - Returns NotFound when user lacks access (not Forbidden)
  - Returns NotFound for non-tenant namespaces

All tests verify that authorization correctly enforces RoleBinding-based
access control and prevents IDOR vulnerabilities.

Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>
2026-04-23 18:17:47 +03:00
IvanHunters
64a3edff01 fix(api): prevent IDOR in TenantNamespace Get and Watch handlers
Fixed two IDOR vulnerabilities allowing authenticated users to access
metadata of any tenant namespace without proper authorization.

Changes:
- Added hasAccessToNamespace() for efficient single-namespace access checks
- Get() now verifies access before returning namespace metadata
- Watch() filters events per-namespace with proper authorization
- Returns NotFound (not Forbidden) to prevent tenant enumeration

Performance optimization:
- hasAccessToNamespace() lists RoleBindings only in target namespace
  instead of listing all cluster RoleBindings (order of magnitude faster)
- Watch handler logs authorization errors for security audit

Additional fixes:
- Handle ServiceAccount subjects with empty namespace correctly
- Add klog error logging for failed authorization checks

Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>
2026-04-23 18:13:10 +03:00
Aleksei Sviridkin
adc7abe5c1
feat(ingress): add loadBalancer exposure mode via CiliumLoadBalancerIPPool
Service.spec.externalIPs is deprecated upstream in Kubernetes v1.36
(KEP-5707, kubernetes#137293). The AllowServiceExternalIPs feature gate
is expected to default to off around v1.40 and the implementation to
be removed around v1.43. For bare-metal installs that rely on
externalIPs today, cozystack needs a migration path.

This change adds an opt-in 'loadBalancer' exposure mode for the
ingress-nginx Service:

- New platform value 'publishing.exposure' (enum: externalIPs |
  loadBalancer, default externalIPs). Plumbed through cozystack-values
  into each tenant's ingress HelmRelease via the new 'expose-mode' key.
- Unknown values and loadBalancer with an empty externalIPs list fail
  the chart render with explicit error messages, rather than silently
  producing a broken Service.
- When exposure=loadBalancer and the current namespace matches
  publishing.ingressName, the Service becomes type: LoadBalancer with
  externalTrafficPolicy: Local.
- A new template renders a CiliumLoadBalancerIPPool whose blocks come
  from publishing.externalIPs (IPv4 addresses get /32, IPv6 addresses
  get /128) and whose serviceSelector uses Cilium's synthetic
  io.kubernetes.service.namespace key combined with the standard
  app.kubernetes.io/name: ingress-nginx label. No custom label is
  written to the Service itself, avoiding cross-tenant collisions from
  user-defined labels.

Default behaviour is unchanged: without opting in, the Service is
still ClusterIP + spec.externalIPs as today.

Scope: only ingress-nginx is migrated by this setting. Other cozystack
components that still write Service.spec.externalIPs directly (notably
the vpn app) must be migrated separately before the v1.40 feature gate
flip.

Tests: packages/extra/ingress/tests/exposure_test.yaml adds 13
helm-unittest cases covering both modes, IPv4/IPv6, empty-token
filtering, unknown-mode rejection, and the non-matching-namespace
fallback.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-04-23 17:35:36 +03:00
Myasnikov Daniil
9222b6feda
ci(api): pre-fetch k8s.io/code-generator in codegen drift job
hack/update-codegen.sh sources kube_codegen.sh from the Go module
cache at ~/go/pkg/mod/k8s.io/code-generator@vX.Y.Z/, but the module
is not declared in go.mod so a fresh runner has nothing to source
from. Add a workflow step that parses the pinned version out of the
script and pulls the module into the cache before running
make generate.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-04-23 14:32:20 +05:00
Myasnikov Daniil
c1508940bd
fix(etcd): remove destructive post-upgrade cert-regeneration hook
The etcd chart shipped a `post-upgrade` Helm hook that `kubectl delete`d the
etcd TLS chain (`etcd-{ca,peer-ca,client,peer,server}-tls`) and then deleted
etcd pods on every chart upgrade, gated by a semver compare of an
`etcd-deployed-version` ConfigMap against `2.6.1`.

The hook was added as a one-shot migration for the chart `2.6.0 -> 2.6.1`
transition. Since commit f871fbdb ("Remove versions_map logic") all chart
versions are stamped as `0.0.0+<git-hash>`, which per semver is always
`< 2.6.1`. The gate therefore always resolves to "update certs", firing the
destructive hook on every etcd upgrade. On clusters running Kamaji-managed
tenant control planes this wipes the etcd CA, cert-manager re-issues it, and
tenant kube-apiservers hit `x509: certificate signed by unknown authority`
against `etcd.<ns>.svc:2379` until each tenant DataStore is manually
re-reconciled.

Commit 47d81f70 ("Disabled private key rotation in CA certs") already fixed
the underlying `rotationPolicy: Always` issue the migration was papering
over, so the hook has no remaining use. Remove the hook Job, its RBAC, the
version ConfigMap it read, and add a helm-unittest suite under
`packages/extra/etcd/tests/` that guards against re-introducing the hook
or the version ConfigMap.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-04-23 13:34:51 +05:00
Arsolitt
43035562ef
test(kubernetes): add helm-unittest tests for HAMi integration
Cover HelmRelease rendering, gpuOperator dependency validation,
ExternalArtifact chartRef, namespace targeting, dependency chain,
valuesOverride passthrough, and automatic devicePlugin disable in
GPU Operator when HAMi is active.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-22 13:25:35 +03:00
Arsolitt
273eb7811a
docs(hami): document glibc < 2.34 limitation and upstream issues
HAMi-core relies on _dl_sym (private glibc symbol removed in 2.34)
for CUDA interception. This breaks compute isolation on modern
container images using Ubuntu 22.04+ and makes Alpine/musl completely
incompatible.

Include upstream issue references and a compatibility matrix so users
can make informed decisions about base image selection.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-22 13:25:29 +03:00
Arsolitt
a3e2fbd742
feat(kubernetes): integrate HAMi as optional addon
Add HAMi HelmRelease to the kubernetes app as a toggleable addon.
When enabled, GPU Operator's built-in device plugin is automatically
disabled via a values merge to avoid conflicts with HAMi's own
device plugin.

The HelmRelease fails fast if GPU Operator is not enabled, since HAMi
depends on it for driver management and container toolkit.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-22 13:25:23 +03:00
Arsolitt
51c2ec0ad4
feat(hami): add HAMi GPU virtualization system chart
Add HAMi v2.8.1 (CNCF Sandbox) as a new system package for fractional
GPU sharing in tenant Kubernetes clusters. The chart enables workloads
to request specific amounts of GPU memory and compute cores instead of
claiming entire GPUs.

Vendored upstream chart includes device plugin, scheduler extender,
mutating webhook, and DRA subchart. Wrapper values configure nvidia
RuntimeClass and clear hardcoded node config from upstream defaults.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-22 13:25:16 +03:00
Myasnikov Daniil
860f431187
chore(api): regenerate deepcopy for RestoreJobSpec.Options
The Options field was added to RestoreJobSpec without re-running
'make generate', leaving zz_generated.deepcopy.go out of sync.
Regenerated to include the missing DeepCopyInto handling for the
runtime.RawExtension pointer.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-04-22 12:00:22 +05:00
Myasnikov Daniil
0fdb25df72
ci(api): add codegen drift check
Run root 'make generate' as a pre-commit hook and as a dedicated
CI workflow so missed codegen updates (CRDs, deepcopy, clients, RBAC)
are caught instead of merging stale generated files.

Pre-commit hook is scoped to files that actually affect codegen
(api/, pkg/apis/, hack/update-codegen.sh, hack/boilerplate.go.txt)
so unrelated commits are not slowed down. CI job sets up Go from
go.mod, runs make generate, and fails on drift with a pointer to
the local fix.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-04-22 11:59:50 +05:00
IvanHunters
9f41dc3228 docs(postgres): clarify bootstrap field descriptions
Update oldName and serverName field descriptions based on code review
feedback to avoid confusion about their actual roles:

- oldName: Remove misleading "(matches serverName in backup.info)"
  text. This field represents the Kubernetes cluster resource name,
  not the Barman server name.

- serverName: Provide clearer explanation that it's the S3 path prefix
  (barmanObjectStore.serverName) used by the original cluster, and should
  only be set when it differs from the Kubernetes resource name.

Updated in:
- values.yaml (source of truth for field documentation)
- types.go (Go API type comments)
- values.schema.json (JSON schema for validation)
- postgres.yaml (CRD with embedded OpenAPI schema)

Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>
2026-04-21 14:34:30 +03:00
IvanHunters
531bc00524 feat(postgres): add serverName parameter for backup recovery
Add serverName field to bootstrap configuration to explicitly specify
Barman server name from backup.info. This fixes "no target backup found"
errors when server_name in backup.info differs from Kubernetes cluster name.

Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>
2026-04-21 13:30:28 +03:00
Arsolitt
4d9a61a0ec
docs(gpu-operator): document native-talos service-monitor interval
Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-19 11:10:47 +03:00
Arsolitt
b5232bd15c
feat(gpu-operator): enable NVLINK bandwidth in default DCGM CSV
Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-19 11:10:46 +03:00
Arsolitt
16b2fd008b
docs(monitoring): comment bats regex rule-name convention
Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-19 11:10:43 +03:00
Arsolitt
f5f083e841
feat(monitoring): annotate GPUThrottleFractionOverOne with verified hardware
Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-19 11:09:51 +03:00
Arsolitt
5e8194c850
docs(monitoring): explain cluster-layer filter asymmetry in GPU rules
Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-19 11:09:50 +03:00
Arsolitt
95ea20119e
fix(gpu-operator): drop unused hostPID on driver-compat DaemonSet
Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-19 11:09:01 +03:00
Arsolitt
2cc60f170c
docs(gpu-operator): reflect recording-rule dependency for gpu-quotas
Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-19 11:08:47 +03:00
Arsolitt
a3241bf51b
fix(quotas): apply phase join to GPU limits column
Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-19 11:08:34 +03:00
Arsolitt
f8b9900873
fix(quotas): use allocated recording rules to exclude terminated pods
Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-19 11:08:25 +03:00
Arsolitt
43fe172d2f
docs(gpu-operator): document POWER/THERMAL_VIOLATION and PSS requirements
Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-19 11:02:05 +03:00
Arsolitt
950c5dd669
fix(fleet): guard TDP division and document DCGM dependency
Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-19 11:01:40 +03:00
Arsolitt
14d9188fcd
fix(quotas): exclude terminated pods from GPU request panel
Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-19 11:01:22 +03:00
Arsolitt
eefb3651a0
fix(performance): drop namespace filter on per-GPU physical metrics
Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-19 11:00:53 +03:00
Arsolitt
5e070840d6
fix(fleet): count GPU nodes via DCGM instead of kube_node_labels
Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-19 11:00:02 +03:00
Arsolitt
549b341675
fix(efficiency): drop namespace filter on cluster-level throttle metrics
Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-19 10:59:42 +03:00
Arsolitt
165e175d70
feat(monitoring): alert on DCGM throttle divisor drift
The /1e9 divisor in gpu:{power,thermal}_throttle_fraction:rate5m was
derived empirically against DCGM 3.x on A10 — the counter documents
itself as microseconds but ticks in nanoseconds in practice. If a
future exporter release honors the documented units, pre-clamp values
would exceed 1.0 while clamp_max(..., 1) silently masks the drift,
plateauing every throttle fraction at 100% and making the panels
lie in unison.

Add a validation group that fires when the raw max/1e9 value exceeds
1.0 for 15m, so we notice and rescale to /1e6 before dashboards
silently mislead operators.

Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-18 08:11:57 +03:00
Arsolitt
605bcd338c
fix(monitoring): pin label matching on GPU efficiency and throttle rules
pod:util_per_watt:avg5m divided two DCGM metrics without an explicit
on(...) clause, so the match used the intersection of their label
sets. If dcgm-exporter relabeling ever diverges between
DCGM_FI_DEV_GPU_UTIL and DCGM_FI_DEV_POWER_USAGE (e.g. a pod-mapping
label appears on one but not the other after a config change), the
entire result drops to empty silently. Pin the match to the labels we
group by so divergence becomes a missing side, not a missing rule.

Throttle fractions had a related shape problem: dcgm-exporter emits
one series per GPU for each pod-mapping combination. On a shared GPU
(restart races, MIG/MPS) the same physical counter appears under
multiple pod labels and downstream avg(...) panels get diluted by the
pod count. Fold duplicates with max by (Hostname, gpu, UUID) before
clamp_max so the fraction is tied to the physical GPU.

Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-18 08:03:21 +03:00
Arsolitt
0634654d63
fix(monitoring): exclude terminated pods from GPU allocation count
kube-state-metrics keeps kube_pod_container_resource_requests series
for Failed/Succeeded pods until the apiserver garbage-collects them,
which could inflate :allocated beyond what tenants actually hold and
drive cluster:gpu_count:free negative.

Join the request metric against kube_pod_status_phase filtered to
Pending|Running — the canonical pattern from Kubernetes' own
container_resource recording rules — on both the cluster and namespace
aggregates. Add clamp_min(..., 0) on cluster:gpu_count:free as a
second line of defence against transient label drift between
kube-state-metrics and DCGM.

Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-18 07:53:48 +03:00
Arsolitt
51b0dedd08
chore(monitoring): tidy GPU VMRule top-level structure
Drop the hardcoded metadata.namespace so the rule inherits the chart's
release namespace, and add an explicit empty params field on every
group for schema consistency. No behavior change.

Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-18 07:45:02 +03:00
Arsolitt
b64bfcc414
fix(dashboard): deduplicate pending GPU pods by (namespace, pod)
The Pending GPU pods counter on gpu-quotas joined raw
kube_pod_container_resource_requests (per-container series) against
kube_pod_status_phase (per-pod series). Multi-container pods were
counted once per requesting container instead of once per pod, so the
widget over-reported whenever a Pending pod had more than one GPU
container. Collapse the requests to pod level before the join.

Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-18 07:36:14 +03:00
Arsolitt
5db6ec3e1f
fix(dashboard): scope GPU panels to selected namespace
Pod-level panels on the efficiency dashboard and DCGM-level panels on
the performance dashboard ignored the $namespace template variable, so
changing it left the visualizations unchanged. Add the filter to each
query. Performance-side queries use the `$namespace|` empty-tolerant
form so host-level DCGM series without a namespace label remain
visible when a specific namespace is selected.

Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-18 07:27:33 +03:00
Arsolitt
4e37f64553
docs(gpu-operator): document tolerate-all on compat DaemonSet
Explain why tolerations: [{operator: Exists}] is safe on the driver
compat DaemonSet: the nodeSelector already confines scheduling to GPU
nodes, so the blanket toleration only kicks in when those nodes carry
the dedicated=gpu / nvidia.com/gpu taints that the GPU Operator's
default policy and many deployments apply.

Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-18 07:19:05 +03:00
Arsolitt
5b210ac7fd
docs(monitoring): mark gpu-fleet average utilization as legacy NVML
Clarify that the "Average utilization" panel on gpu-fleet reflects the
legacy NVML view (DCGM_FI_DEV_GPU_UTIL) rather than engine-active
profiling. For AI/LLM workloads the NVML number is optimistic; the
gpu-efficiency dashboard carries the profiling-based view.

Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-18 07:11:28 +03:00
Arsolitt
2518e09d67
refactor(monitoring): store pod:tensor_saturation as unitless ratio
Align pod:tensor_saturation:avg5m with namespace:tensor_active:avg and
DCGM's native 0..1 range by dropping the * 100 from the recording rule
and multiplying at display time in gpu-efficiency.json. Also scope
pod:util_per_watt:avg5m with avg by (Hostname, gpu, UUID, namespace,
pod) so the series mirrors pod:tensor_saturation's grouping and stays
usable in topk queries.

Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-18 07:03:42 +03:00
Arsolitt
ccfec2ef62
fix(monitoring): close DCGM coverage gap for gpu-fleet TDP panel
gpu-fleet.json references DCGM_FI_DEV_POWER_MGMT_LIMIT for its
"TDP vs draw" panel, but the custom DCGM Exporter CSV did not declare
it, so the panel silently rendered "No data" on clusters using that
config. Declare the counter, fix the dashboards table in the
gpu-operator examples README, and add a bats test that cross-checks
every DCGM_FI_* reference in tracked dashboards and recording rules
against the union of the upstream default set (snapshotted under
hack/) and the project's custom CSV.

Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-18 06:55:17 +03:00
Arsolitt
2fa4b3e31c
refactor(monitoring): tighten GPU dashboard queries
- gpu-efficiency: scope Tensor Saturation, Util-per-Watt and Power
  Throttle stats to the $namespace selector. Cluster-wide means were
  misleading when a user had narrowed the dashboard to specific
  tenants — the headline numbers lied relative to the panels below.
- gpu-fleet: show per-node power draw as % of combined TDP cap
  (DCGM_FI_DEV_POWER_MGMT_LIMIT) instead of raw watts. Thresholds
  (60 / 80 %) generalize across GPU SKUs without per-model tuning.
- gpu-quotas: read cluster:gpu_count:allocated from the recording
  rules instead of recomputing sum(kube_pod_container_resource_requests)
  inline. Keeps the dashboard aligned with the canonical definition
  in gpu-recording.rules.yaml so the two can't drift.

Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-18 06:48:33 +03:00
Arsolitt
7eb9fe8ade
refactor(monitoring): drop unused GPU recording rules
- namespace:gpu_count:sum — never consumed by any tracked dashboard;
  the billable view is already covered by namespace:gpu_count:allocated,
  and the admin view by cluster:gpu_count:allocated.
- namespace:energy_joules:sum — no panel integrates joules; kWh
  readings on the tenant dashboard compute their own integrations
  from namespace:power_watts:sum.
- pod:tensor_to_nvml_ratio:avg5m — interesting tenant signal in
  theory, but not wired into any panel and carrying it just burns
  cardinality on large fleets.

Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-18 06:41:45 +03:00
Arsolitt
6d9066f074
feat(monitoring): add GPU fleet and tenants dashboards
- gpu-fleet: cluster-wide admin view — inventory, capacity (total /
  allocated / free), per-node utilization and power, throttling,
  temperatures, XID errors.
- gpu-tenants: per-namespace view — live allocation, utilization,
  tensor saturation, power, and 24h GPU-hours / kWh integrations for
  billing inputs.

Register both under gpu/* in dashboards-infra.list so they ship as
GrafanaDashboard CRs and fall under the bats cross-check introduced
earlier on this branch.

Update examples/README to spell out which DCGM counters each of the
five gpu/* dashboards actually needs on top of the upstream default
CSV — gpu-performance needs profiling and throttling counters,
gpu-efficiency needs profiling, gpu-tenants needs only
DCGM_FI_PROF_PIPE_TENSOR_ACTIVE for its tensor panel, and gpu-fleet
and gpu-quotas work on the default counter set alone.

Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-18 06:35:12 +03:00
Arsolitt
dbed4992b0
fix(monitoring): exclude system namespaces from namespace:gpu_count:allocated
Align namespace:gpu_count:allocated with every other namespace:* rule
by filtering out cozy-*/kube-*. All other per-namespace rules
(gpu_util, tensor_active, fb_used_bytes, power_watts) already exclude
system namespaces, so the label set produced by :allocated diverged
from them — any dashboard variable or join that reads across these
rules could end up with a different namespace list depending on which
rule supplied the :allocated column.

Trade-off: per-namespace GPU accounting for system workloads is no
longer available through this rule. If it's ever needed, add a
dedicated system:gpu_count:allocated rather than widening this one —
the "billable tenant view" invariant is what the filter is protecting.

Cluster-level cluster:gpu_count:allocated intentionally keeps system
pods so it stays aligned with cluster:gpu_count:total and
cluster:gpu_count:free remains meaningful. As a consequence,
sum(namespace:gpu_count:allocated) no longer equals
cluster:gpu_count:allocated; the delta is system-pod GPU usage, which
is fine for the cluster-admin view.

Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-18 06:28:49 +03:00
Arsolitt
49c1d7e7ab
test(monitoring): cross-check GPU dashboards against recording rules
Catch dangling references at PR time: every recording-rule name used
inside a tracked GPU dashboard must exist in
packages/system/monitoring-agents/alerts/gpu-recording.rules.yaml. The
first iteration of gpu-efficiency.json shipped panels keyed on
pod:tensor_saturation:avg5m without the rule defined; the test fails
on exactly that class of bug.

Scoped to dashboards listed under gpu/* in dashboards-infra.list, so
untracked drafts stay out of scope until they are registered. Reverse
direction (rule defined but unused) is intentionally NOT enforced —
some rules exist for ad-hoc PromQL or upcoming dashboards.

Auto-discovered by make bats-unit-tests via hack/cozytest.sh.

Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-18 06:22:31 +03:00
Arsolitt
38c8a37cb5
docs(gpu-operator): clarify minimum required DCGM metrics
The previous wording implied that the entire custom DCGM CSV was
required by the recording rules. In fact only the profiling counters
(DCGM_FI_PROF_*) need to be added on top of the upstream defaults —
everything else the rules consume is already in default-counters.csv.

Add a Verification status block flagging that the minimum-set claim is
derived from the DCGM Exporter version pinned in the currently shipped
gpu-operator package and must be re-checked when that package moves to
a newer release.

Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-18 06:15:58 +03:00
Arsolitt
0e20159bd9
fix(gpu-operator): scope compat DaemonSet to GPU nodes
Restrict the nvidia-driver-compat DaemonSet to nodes labelled
nvidia.com/gpu.present=true (NFD/GPU Operator label). Without the
nodeSelector it was scheduling onto every node — control-plane and
CPU-only workers included — burning a privileged pod slot per host
for no benefit.

Add resource requests and limits to the init and pause containers so
the DaemonSet stays within control-plane budget on small clusters.

Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-18 06:09:44 +03:00
Arsolitt
4e8731b588
refactor(monitoring): rework GPU recording rules
- Drop gpu.recording.30s group: per-GPU 30s aggregates had no consumers
  in tracked dashboards, only burned cardinality.
- Drop namespace:gpu_allocated_count:gauge: identical expression to
  namespace:gpu_count:sum under a different name.
- Reground :allocated on kube_pod_container_resource_requests so it
  reflects what tenants requested (Pending+Running) rather than what
  DCGM currently sees. namespace:gpu_count:sum stays DCGM-based and
  represents actually-running pods; the gap between the two is the
  signal admins want.
- Add namespace:gpu_count:allocated as the per-namespace counterpart.

Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-18 06:02:17 +03:00
Arsolitt
4f8cef47bf
fix(monitoring): restore trailing newline in GPU dashboards
Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-18 05:54:21 +03:00
Arsolitt
11f7d3589b
chore: ignore CLAUDE.local.md
Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-18 05:47:33 +03:00
Arsolitt
7e5f3a7f12
refactor(monitoring): clean up GPU dashboards
Strip Grafana export boilerplate (__inputs, __elements, __requires,
default annotations, embedded datasource inputs) and tighten panel
layouts across the three GPU dashboards. All three continue to use
the $ds_prometheus template variable.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-17 17:46:31 +03:00
Arsolitt
c1b9a06a36
feat(monitoring): expand GPU dashboards — efficiency and quotas
Revise gpu-performance and add two new dashboards, registered in
dashboards-infra.list:

- gpu-efficiency (GPU Efficiency Score) — utilization vs. capacity
  and workload efficiency signals.
- gpu-quotas (GPU Quotas & Allocation) — per-namespace requested vs.
  used GPUs for tenant capacity planning.

All three dashboards use the $ds_prometheus template variable, per
the project convention.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-17 17:08:18 +03:00
Arsolitt
5d6654c6f4
docs(gpu-operator): add native-pod Talos reference manifests
Add reference manifests (not templates) under
packages/system/gpu-operator/examples/ documenting one working
configuration for running CUDA workloads directly in pods on a Talos
cluster, with DCGM metrics that drive the gpu/gpu-performance
dashboard.

- values-native-talos.yaml: Cozystack Package values that disable the
  sandbox path, enable the device plugin, and wire DCGM to the custom
  metrics ConfigMap.
- dcgm-custom-metrics.yaml: ConfigMap extending the default DCGM CSV
  with profiling, ECC, throttling and energy counters used by the
  dashboard and recording rules.
- nvidia-driver-compat.yaml: DaemonSet that stages libnvidia-ml.so.1
  and nvidia-smi from the Talos glibc tree into a location the
  gpu-operator validator inspects. Workaround for
  NVIDIA/gpu-operator#1687.
- README.md: explains why these are shipped as references rather than
  first-class templates (sandbox vs native is a deployment choice),
  and how the pieces connect.

The out-of-the-box values-talos.yaml still targets the sandbox (VFIO
passthrough) scenario. Operators who want native pod GPU workloads can
start from these references.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-17 16:56:03 +03:00
Arsolitt
d1d19e9978
feat(monitoring): add GPU performance Grafana dashboard
Add the gpu/gpu-performance dashboard and register it in the infra
dashboard list. The dashboard provides:

- Cluster overview: total/allocated GPUs, average utilization,
  aggregate power draw.
- Utilization: GPU util (NVML), tensor pipe active (realistic load
  for LLM/AI workloads), graphics engine active, memory copy util.
- Memory: VRAM used/free per GPU.
- Power and temperature per GPU.
- Health: XID errors, power and thermal throttling.

The dashboard relies on DCGM_FI_* metrics plus the cluster:gpu_* and
namespace:gpu_* recording rules added to monitoring-agents.

The JSON follows the cozystack convention — Prometheus data source is
selected via the $ds_prometheus template variable.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-17 16:55:51 +03:00
Arsolitt
cb3e0adf64
feat(monitoring): add GPU recording rules
Add VMRule with recording rules for DCGM metrics at three levels:

- gpu.recording.30s: per-GPU aggregates over 30s windows
- gpu.recording.cluster.1m: cluster-wide totals for overview panels
- gpu.recording.namespace.1m: per-namespace aggregates for tenant
  reporting and GPU-hour calculations

The rules are safe to ship on clusters without DCGM — they evaluate to
empty series when no matching metrics are scraped.

Used by dashboards/gpu/gpu-performance.json.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-04-17 16:55:42 +03:00
121 changed files with 10941 additions and 400 deletions

22
.github/labels.yml vendored
View file

@ -21,7 +21,7 @@
# area/ subsystem; extensible — add when 3+ open issues exist
# do-not-merge/ PR merge blockers
# security/ security-finding severity and status (Cozystack-specific)
# size/ PR size (auto-applied)
# size: PR size (auto-applied)
#
# `aliases:` lets EndBug/label-sync rename existing labels without losing
# references on already-tagged issues and PRs.
@ -295,38 +295,32 @@
description: Indicates a non-member PR is safe to run CI on
# ──────────────────────────────────────────────
# size/ — PR size (auto-applied by .github/workflows/pr-size.yaml)
# size: — PR size (auto-applied by sizing bot)
# ──────────────────────────────────────────────
- name: size/XS
- name: 'size:XS'
color: '00ff00'
description: This PR changes 0-9 lines, ignoring generated files
aliases: ['size:XS']
- name: size/S
- name: 'size:S'
color: '77b800'
description: This PR changes 10-29 lines, ignoring generated files
aliases: ['size:S']
- name: size/M
- name: 'size:M'
color: 'ebb800'
description: This PR changes 30-99 lines, ignoring generated files
aliases: ['size:M']
- name: size/L
- name: 'size:L'
color: 'eb9500'
description: This PR changes 100-499 lines, ignoring generated files
aliases: ['size:L']
- name: size/XL
- name: 'size:XL'
color: 'ff823f'
description: This PR changes 500-999 lines, ignoring generated files
aliases: ['size:XL']
- name: size/XXL
- name: 'size:XXL'
color: 'ffb8b8'
description: This PR changes 1000+ lines, ignoring generated files
aliases: ['size:XXL']
# ──────────────────────────────────────────────
# security/ — security-finding severity and status

61
.github/workflows/codegen-drift.yml vendored Normal file
View file

@ -0,0 +1,61 @@
name: Codegen Drift Check
on:
pull_request:
types: [opened, synchronize, reopened]
paths:
- 'api/**'
- 'pkg/apis/**'
- 'pkg/generated/**'
- 'internal/crdinstall/manifests/**'
- 'packages/system/cozystack-controller/definitions/**'
- 'packages/system/application-definition-crd/definition/**'
- 'packages/system/backup-controller/definitions/**'
- 'packages/system/backupstrategy-controller/definitions/**'
- 'hack/update-codegen.sh'
- 'hack/boilerplate.go.txt'
- 'Makefile'
- 'go.mod'
- 'go.sum'
- '.github/workflows/codegen-drift.yml'
concurrency:
group: codegen-drift-${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
codegen-drift:
name: Verify generated code is up to date
runs-on: ubuntu-22.04
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
cache: true
- name: Pre-fetch k8s.io/code-generator module
# hack/update-codegen.sh sources kube_codegen.sh from the Go module cache.
# The module is not declared in go.mod, so fetch it explicitly at the
# version pinned in the script.
run: |
version=$(grep -oP 'code-generator@\Kv[0-9.]+' hack/update-codegen.sh)
tmpdir=$(mktemp -d)
cd "$tmpdir"
go mod init codegen-fetch
go get "k8s.io/code-generator@${version}"
- name: Run make generate
run: make generate
- name: Fail on drift
run: |
if [ -n "$(git status --porcelain)" ]; then
echo "::error::'make generate' produced changes. Run 'make generate' locally and commit the result."
git status --short
git diff --color=always
exit 1
fi

View file

@ -1,93 +0,0 @@
name: PR size label
on:
pull_request_target:
types: [opened, synchronize, reopened]
permissions:
contents: read
pull-requests: write
concurrency:
group: pr-size-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
size:
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v7
with:
script: |
const pr = context.payload.pull_request;
// Skip files that should not count as "real diff" — they are produced by tools,
// not authored by humans, and including them lies to reviewers about review effort:
// - vendored Go deps,
// - kubebuilder/openapi inline generated files (zz_generated.{deepcopy,conversion,defaults,openapi}.go),
// - whole code-generated trees from client-gen / lister-gen / applyconfiguration-gen
// and protobuf (any path under a `generated/` directory or ending in `.pb.go`),
// - vendored Helm charts,
// - lockfiles.
const isIgnored = (path) =>
path.startsWith('vendor/') ||
/\bzz_generated\.[^/]*\.go$/.test(path) ||
/(^|\/)generated\//.test(path) ||
path.endsWith('.pb.go') ||
/^packages\/system\/[^/]+\/charts\//.test(path) ||
path === 'go.sum' || path.endsWith('/go.sum') ||
path.endsWith('.lock') || path.endsWith('.lockb');
const files = await github.paginate(github.rest.pulls.listFiles, {
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: pr.number,
});
const lines = files
.filter((f) => !isIgnored(f.filename))
.reduce((acc, f) => acc + (f.additions || 0) + (f.deletions || 0), 0);
// Thresholds match the descriptions of size/* labels in .github/labels.yml.
const bucket =
lines <= 9 ? 'XS' :
lines <= 29 ? 'S' :
lines <= 99 ? 'M' :
lines <= 499 ? 'L' :
lines <= 999 ? 'XL' : 'XXL';
const target = `size/${bucket}`;
// Match both legacy "size:" and canonical "size/" during the migration window.
const existing = (pr.labels || []).map((l) => l.name);
const oldSizes = existing.filter((n) => n.startsWith('size/') || n.startsWith('size:'));
const alreadyTarget = oldSizes.includes(target);
// Remove every size/* label that is not the target. Tolerate 404 — concurrent
// runs or manual edits between event payload and execution can race here.
for (const name of oldSizes) {
if (name === target) continue;
try {
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
name,
});
} catch (e) {
if (e.status !== 404) throw e;
core.info(`label ${name} already gone (404)`);
}
}
if (alreadyTarget && oldSizes.length === 1) {
core.info(`PR #${pr.number}: ${lines} lines, label already ${target}`);
return;
}
if (!alreadyTarget) {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
labels: [target],
});
}
core.info(`PR #${pr.number}: ${lines} lines -> ${target}`);

View file

@ -1,56 +0,0 @@
name: Stale issues
on:
schedule:
- cron: '37 4 * * *' # daily at 04:37 UTC
workflow_dispatch:
permissions:
contents: read
issues: write
pull-requests: write
concurrency:
group: stale
cancel-in-progress: false
jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v10
with:
stale-issue-label: lifecycle/stale
stale-pr-label: lifecycle/stale
close-issue-label: lifecycle/rotten
close-pr-label: lifecycle/rotten
# Exempt lists differ on purpose: issues sit longer than PRs by nature.
# An accepted issue (`triage/accepted`) is a known long-tail task, not stale;
# a held PR (`do-not-merge/hold`) is paused intentionally and shouldn't auto-close.
# Priority/security/regression labels protect issues that someone explicitly
# decided to keep visible.
exempt-issue-labels: >-
lifecycle/frozen,epic,
priority/critical-urgent,priority/important-soon,priority/important-longterm,
security/critical,security/high,security/confirmed,
security/triage-needed,security/in-progress,
triage/accepted,kind/regression
exempt-pr-labels: >-
lifecycle/frozen,do-not-merge/hold,
backport,backport-previous
days-before-stale: 60
days-before-close: 14
operations-per-run: 100
remove-stale-when-updated: true
stale-issue-message: |
This issue has had no activity for 60 days and was marked `lifecycle/stale`.
It will be closed in 14 days unless commented or labelled `lifecycle/frozen`.
stale-pr-message: |
This PR has had no activity for 60 days and was marked `lifecycle/stale`.
It will be closed in 14 days unless commented or labelled `lifecycle/frozen`.
close-issue-message: |
Closed because no activity followed the `lifecycle/stale` warning.
Reopen if the issue is still relevant.
close-pr-message: |
Closed because no activity followed the `lifecycle/stale` warning.
Reopen if the change is still wanted.

View file

@ -1,6 +1,17 @@
repos:
- repo: local
hooks:
- id: run-make-generate-root
name: Run 'make generate' at repo root
entry: |
flock -x .git/pre-commit.lock sh -c '
echo "Running make generate at repo root"
make generate || exit $?
git diff --color=always | cat
'
language: system
files: ^(api/|pkg/apis/|pkg/generated/|internal/crdinstall/manifests/|packages/system/cozystack-controller/definitions/|packages/system/application-definition-crd/definition/|packages/system/backup-controller/definitions/|packages/system/backupstrategy-controller/definitions/|hack/update-codegen\.sh$|hack/boilerplate\.go\.txt$|Makefile$)
pass_filenames: false
- id: run-make-generate
name: Run 'make generate' in all app directories
entry: |

View file

@ -22,6 +22,7 @@ build: build-deps
make -C packages/system/lineage-controller-webhook image
make -C packages/system/cilium image
make -C packages/system/linstor image
make -C packages/system/linstor-gui image
make -C packages/system/kubeovn-webhook image
make -C packages/system/kubeovn-plunger image
make -C packages/system/dashboard image

View file

@ -69,6 +69,9 @@ type Addons struct {
// NVIDIA GPU Operator.
// +kubebuilder:default:={}
GpuOperator GPUOperatorAddon `json:"gpuOperator"`
// HAMi GPU virtualization middleware.
// +kubebuilder:default:={}
Hami HAMiAddon `json:"hami"`
// Ingress-NGINX controller.
// +kubebuilder:default:={}
IngressNginx IngressNginxAddon `json:"ingressNginx"`
@ -160,6 +163,15 @@ type GatewayAPIAddon struct {
Enabled bool `json:"enabled"`
}
type HAMiAddon struct {
// Enable HAMi (requires GPU Operator).
// +kubebuilder:default:=false
Enabled bool `json:"enabled"`
// Custom Helm values overrides.
// +kubebuilder:default:={}
ValuesOverride k8sRuntime.RawExtension `json:"valuesOverride"`
}
type Images struct {
// Image used by the wait-for-kubeconfig init container. Empty falls back to images/busybox.tag.
// +kubebuilder:default:=""

View file

@ -49,6 +49,7 @@ func (in *Addons) DeepCopyInto(out *Addons) {
in.Fluxcd.DeepCopyInto(&out.Fluxcd)
out.GatewayAPI = in.GatewayAPI
in.GpuOperator.DeepCopyInto(&out.GpuOperator)
in.Hami.DeepCopyInto(&out.Hami)
in.IngressNginx.DeepCopyInto(&out.IngressNginx)
in.MonitoringAgents.DeepCopyInto(&out.MonitoringAgents)
in.Velero.DeepCopyInto(&out.Velero)
@ -135,6 +136,7 @@ func (in *ConfigSpec) DeepCopyInto(out *ConfigSpec) {
}
in.Addons.DeepCopyInto(&out.Addons)
in.ControlPlane.DeepCopyInto(&out.ControlPlane)
out.Images = in.Images
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConfigSpec.
@ -260,6 +262,37 @@ func (in *GatewayAPIAddon) DeepCopy() *GatewayAPIAddon {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *HAMiAddon) DeepCopyInto(out *HAMiAddon) {
*out = *in
in.ValuesOverride.DeepCopyInto(&out.ValuesOverride)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HAMiAddon.
func (in *HAMiAddon) DeepCopy() *HAMiAddon {
if in == nil {
return nil
}
out := new(HAMiAddon)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Images) DeepCopyInto(out *Images) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Images.
func (in *Images) DeepCopy() *Images {
if in == nil {
return nil
}
out := new(Images)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *IngressNginxAddon) DeepCopyInto(out *IngressNginxAddon) {
*out = *in

View file

@ -92,6 +92,9 @@ type Bootstrap struct {
// Timestamp (RFC3339) for point-in-time recovery; empty means latest.
// +kubebuilder:default:=""
RecoveryTime string `json:"recoveryTime,omitempty"`
// Barman server name (S3 path prefix) used by the original cluster when writing backups. Set this only when the original cluster had an explicit barmanObjectStore.serverName that differed from its Kubernetes resource name.
// +kubebuilder:default:=""
ServerName string `json:"serverName,omitempty"`
}
type Database struct {

View file

@ -26,6 +26,9 @@ type ConfigSpec struct {
// Ports to forward from outside the cluster.
// +kubebuilder:default:={22}
ExternalPorts []int `json:"externalPorts,omitempty"`
// Whether to accept ICMP traffic to the VM in PortList mode (preserves ping and PMTU discovery). No effect in WholeIP mode. Default true so ping behaves as users expect even when port filtering is in effect.
// +kubebuilder:default:=true
ExternalAllowICMP bool `json:"externalAllowICMP"`
// Requested running state of the VirtualMachineInstance
// +kubebuilder:default:="Always"
RunStrategy RunStrategy `json:"runStrategy"`

View file

@ -620,6 +620,11 @@ func (in *RestoreJobSpec) DeepCopyInto(out *RestoreJobSpec) {
*out = new(v1.TypedLocalObjectReference)
(*in).DeepCopyInto(*out)
}
if in.Options != nil {
in, out := &in.Options, &out.Options
*out = new(runtime.RawExtension)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RestoreJobSpec.

View file

@ -0,0 +1,819 @@
{
"uid": "gpu-efficiency",
"title": "GPU Efficiency Score",
"description": "Tensor saturation, util/watt and throttling — reveals inefficient GPU workloads",
"tags": [
"gpu",
"efficiency",
"finops"
],
"timezone": "browser",
"editable": true,
"graphTooltip": 1,
"time": {
"from": "now-1h",
"to": "now"
},
"fiscalYearStartMonth": 0,
"schemaVersion": 42,
"panels": [
{
"type": "row",
"collapsed": false,
"title": "Overall efficiency metrics",
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 0
},
"id": 1,
"panels": []
},
{
"type": "stat",
"id": 2,
"targets": [
{
"expr": "avg(gpu:tensor_saturation:avg5m) * 100",
"refId": "A"
}
],
"title": "Avg Tensor Saturation",
"description": "Mean tensor core saturation across all GPUs. \u003c10% means GPUs are used inefficiently (workloads could move to CPU or optimize their code). Cluster-wide — DCGM metrics cannot be attributed to workload namespaces.",
"transparent": false,
"datasource": {
"type": "prometheus",
"uid": "$ds_prometheus"
},
"gridPos": {
"h": 5,
"w": 8,
"x": 0,
"y": 1
},
"repeatDirection": "h",
"options": {
"graphMode": "area",
"colorMode": "background",
"justifyMode": "auto",
"textMode": "value",
"wideLayout": true,
"showPercentChange": false,
"reduceOptions": {
"calcs": [
"lastNotNull"
]
},
"percentChangeColorMode": "standard",
"orientation": ""
},
"fieldConfig": {
"defaults": {
"unit": "percent",
"min": 0,
"max": 100,
"thresholds": {
"mode": "absolute",
"steps": [
{
"value": null,
"color": "red"
},
{
"value": 10,
"color": "orange"
},
{
"value": 30,
"color": "yellow"
},
{
"value": 60,
"color": "green"
}
]
}
},
"overrides": []
}
},
{
"type": "stat",
"id": 3,
"targets": [
{
"expr": "avg(gpu:util_per_watt:avg5m)",
"refId": "A"
}
],
"title": "Avg Utilization per Watt",
"description": "NVML utilization % per watt across all GPUs. Higher value = more efficient workload. Cluster-wide — DCGM metrics cannot be attributed to workload namespaces.",
"transparent": false,
"datasource": {
"type": "prometheus",
"uid": "$ds_prometheus"
},
"gridPos": {
"h": 5,
"w": 8,
"x": 8,
"y": 1
},
"repeatDirection": "h",
"options": {
"graphMode": "area",
"colorMode": "background",
"justifyMode": "auto",
"textMode": "value",
"wideLayout": true,
"showPercentChange": false,
"reduceOptions": {
"calcs": [
"lastNotNull"
]
},
"percentChangeColorMode": "standard",
"orientation": ""
},
"fieldConfig": {
"defaults": {
"unit": "none",
"decimals": 3,
"thresholds": {
"mode": "absolute",
"steps": [
{
"value": null,
"color": "red"
},
{
"value": 0.5,
"color": "yellow"
},
{
"value": 1,
"color": "green"
}
]
}
},
"overrides": []
}
},
{
"type": "stat",
"id": 4,
"targets": [
{
"expr": "avg(gpu:power_throttle_fraction:rate5m)",
"refId": "A"
}
],
"title": "Avg Power Throttling",
"description": "Fraction of time GPUs hit the TDP cap and lose performance. \u003e5% means tenants underutilize billed FLOPS. Cluster-wide — rule aggregates by (Hostname, gpu, UUID) and drops namespace label.",
"transparent": false,
"datasource": {
"type": "prometheus",
"uid": "$ds_prometheus"
},
"gridPos": {
"h": 5,
"w": 8,
"x": 16,
"y": 1
},
"repeatDirection": "h",
"options": {
"graphMode": "area",
"colorMode": "background",
"justifyMode": "auto",
"textMode": "value",
"wideLayout": true,
"showPercentChange": false,
"reduceOptions": {
"calcs": [
"lastNotNull"
]
},
"percentChangeColorMode": "standard",
"orientation": ""
},
"fieldConfig": {
"defaults": {
"unit": "percentunit",
"decimals": 2,
"thresholds": {
"mode": "absolute",
"steps": [
{
"value": null,
"color": "green"
},
{
"value": 0.05,
"color": "yellow"
},
{
"value": 0.2,
"color": "red"
}
]
}
},
"overrides": []
}
},
{
"type": "row",
"collapsed": false,
"title": "NVML vs Tensor (mismatch detector)",
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 6
},
"id": 10,
"panels": []
},
{
"type": "timeseries",
"id": 11,
"targets": [
{
"expr": "DCGM_FI_DEV_GPU_UTIL",
"legendFormat": "{{Hostname}}/{{gpu}}",
"refId": "A"
}
],
"title": "NVML GPU Utilization",
"description": "Classic utilization metric. Shows activity of any engine (SM, copy, encoder). Cluster-wide — DCGM namespace is the exporter's own namespace, not the workload namespace.",
"transparent": false,
"datasource": {
"type": "prometheus",
"uid": "$ds_prometheus"
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 7
},
"repeatDirection": "h",
"options": {
"legend": {
"displayMode": "table",
"placement": "bottom",
"showLegend": false,
"calcs": [
"mean",
"max"
]
},
"tooltip": {
"mode": "multi",
"sort": ""
}
},
"fieldConfig": {
"defaults": {
"unit": "percent",
"min": 0,
"max": 100,
"custom": {
"drawStyle": "line",
"lineInterpolation": "linear",
"fillOpacity": 10,
"showPoints": "never"
}
},
"overrides": []
}
},
{
"type": "timeseries",
"id": 12,
"targets": [
{
"expr": "DCGM_FI_PROF_PIPE_TENSOR_ACTIVE * 100",
"legendFormat": "{{Hostname}}/{{gpu}}",
"refId": "A"
}
],
"title": "Tensor Pipe Active",
"description": "Real tensor core load (HMMA). For AI/LLM inference it should be ≥20%, otherwise the workload is unoptimized. Cluster-wide — DCGM namespace is the exporter's own namespace, not the workload namespace.",
"transparent": false,
"datasource": {
"type": "prometheus",
"uid": "$ds_prometheus"
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 7
},
"repeatDirection": "h",
"options": {
"legend": {
"displayMode": "table",
"placement": "bottom",
"showLegend": false,
"calcs": [
"mean",
"max"
]
},
"tooltip": {
"mode": "multi",
"sort": ""
}
},
"fieldConfig": {
"defaults": {
"unit": "percent",
"min": 0,
"max": 100,
"custom": {
"drawStyle": "line",
"lineInterpolation": "linear",
"fillOpacity": 10,
"showPoints": "never"
}
},
"overrides": []
}
},
{
"type": "row",
"collapsed": false,
"title": "Per-GPU ranking",
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 15
},
"id": 20,
"panels": []
},
{
"type": "table",
"id": 21,
"targets": [
{
"expr": "topk(20, gpu:tensor_saturation:avg5m * 100)",
"instant": true,
"range": false,
"format": "table",
"refId": "A"
}
],
"title": "Tensor Saturation per GPU (5m avg)",
"description": "Which GPUs are exercising tensor cores and which are not. Sorted descending. Grouped by Hostname/gpu/UUID — DCGM metrics cannot be attributed to workload namespaces.",
"transparent": false,
"datasource": {
"type": "prometheus",
"uid": "$ds_prometheus"
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 16
},
"repeatDirection": "h",
"transformations": [
{
"id": "organize",
"options": {
"excludeByName": {
"DCGM_FI_DRIVER_VERSION": true,
"Time": true,
"__name__": true,
"cluster": true,
"container": true,
"device": true,
"endpoint": true,
"gpu_driver_version": true,
"instance": true,
"job": true,
"modelName": true,
"namespace": true,
"pci_bus_id": true,
"pod": true,
"prometheus": true,
"service": true,
"tenant": true,
"tier": true,
"uid": true,
"unit": true
},
"indexByName": {
"Hostname": 0,
"UUID": 2,
"Value": 3,
"gpu": 1
},
"renameByName": {
"Hostname": "Node",
"UUID": "UUID",
"Value": "Saturation",
"gpu": "GPU"
}
}
}
],
"options": {
"frameIndex": 0,
"showHeader": true,
"showTypeIcons": false,
"sortBy": [
{
"displayName": "Saturation",
"desc": true
}
],
"footer": {
"show": false,
"reducer": []
},
"cellHeight": "sm"
},
"fieldConfig": {
"defaults": {},
"overrides": [
{
"matcher": {
"id": "byName",
"options": "Saturation"
},
"properties": [
{
"id": "unit",
"value": "percent"
},
{
"id": "min",
"value": 0
},
{
"id": "max",
"value": 100
},
{
"id": "custom.cellOptions",
"value": {
"mode": "gradient",
"type": "gauge"
}
},
{
"id": "thresholds",
"value": {
"mode": "absolute",
"steps": [
{
"color": "red"
},
{
"color": "orange",
"value": 10
},
{
"color": "yellow",
"value": 30
},
{
"color": "green",
"value": 60
}
]
}
}
]
}
]
}
},
{
"type": "table",
"id": 22,
"targets": [
{
"expr": "topk(20, gpu:util_per_watt:avg5m)",
"instant": true,
"range": false,
"format": "table",
"refId": "A"
}
],
"title": "Utilization / Watt per GPU (5m avg)",
"description": "How efficiently each GPU spends watts. Low value = poor optimization. Grouped by Hostname/gpu/UUID — DCGM metrics cannot be attributed to workload namespaces.",
"transparent": false,
"datasource": {
"type": "prometheus",
"uid": "$ds_prometheus"
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 16
},
"repeatDirection": "h",
"transformations": [
{
"id": "organize",
"options": {
"excludeByName": {
"DCGM_FI_DRIVER_VERSION": true,
"Time": true,
"__name__": true,
"cluster": true,
"container": true,
"device": true,
"endpoint": true,
"gpu_driver_version": true,
"instance": true,
"job": true,
"modelName": true,
"namespace": true,
"pci_bus_id": true,
"pod": true,
"prometheus": true,
"service": true,
"tenant": true,
"tier": true,
"uid": true,
"unit": true
},
"indexByName": {
"Hostname": 0,
"UUID": 2,
"Value": 3,
"gpu": 1
},
"renameByName": {
"Hostname": "Node",
"UUID": "UUID",
"Value": "Util/Watt",
"gpu": "GPU"
}
}
}
],
"options": {
"frameIndex": 0,
"showHeader": true,
"showTypeIcons": false,
"sortBy": [
{
"displayName": "Util/Watt",
"desc": true
}
],
"footer": {
"show": false,
"reducer": []
},
"cellHeight": "sm"
},
"fieldConfig": {
"defaults": {},
"overrides": [
{
"matcher": {
"id": "byName",
"options": "Util/Watt"
},
"properties": [
{
"id": "unit",
"value": "none"
},
{
"id": "decimals",
"value": 3
},
{
"id": "min",
"value": 0
},
{
"id": "max",
"value": 1.5
},
{
"id": "custom.cellOptions",
"value": {
"mode": "gradient",
"type": "gauge"
}
},
{
"id": "thresholds",
"value": {
"mode": "absolute",
"steps": [
{
"color": "red"
},
{
"color": "yellow",
"value": 0.5
},
{
"color": "green",
"value": 1
}
]
}
}
]
}
]
}
},
{
"type": "row",
"collapsed": false,
"title": "Throttling",
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 24
},
"id": 30,
"panels": []
},
{
"type": "timeseries",
"id": 31,
"targets": [
{
"expr": "gpu:power_throttle_fraction:rate5m",
"legendFormat": "{{Hostname}}/{{gpu}}",
"refId": "A"
}
],
"title": "Power throttle fraction per GPU",
"description": "Fraction of time the GPU was power-throttled. 1.0 = always throttled. Cluster-wide — rule aggregates by (Hostname, gpu, UUID) and drops namespace label.",
"transparent": false,
"datasource": {
"type": "prometheus",
"uid": "$ds_prometheus"
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 25
},
"repeatDirection": "h",
"options": {
"legend": {
"displayMode": "table",
"placement": "bottom",
"showLegend": false,
"calcs": [
"mean",
"max"
]
},
"tooltip": {
"mode": "multi",
"sort": ""
}
},
"fieldConfig": {
"defaults": {
"unit": "percentunit",
"min": 0,
"max": 1,
"thresholds": {
"mode": "absolute",
"steps": [
{
"value": null,
"color": "green"
},
{
"value": 0.05,
"color": "yellow"
},
{
"value": 0.2,
"color": "red"
}
]
},
"custom": {
"drawStyle": "line",
"lineInterpolation": "linear",
"fillOpacity": 25,
"showPoints": "never"
}
},
"overrides": []
}
},
{
"type": "timeseries",
"id": 32,
"targets": [
{
"expr": "gpu:thermal_throttle_fraction:rate5m",
"legendFormat": "{{Hostname}}/{{gpu}}",
"refId": "A"
}
],
"title": "Thermal throttle fraction per GPU",
"description": "Fraction of time the GPU was thermal-throttled. Cluster-wide — rule aggregates by (Hostname, gpu, UUID) and drops namespace label.",
"transparent": false,
"datasource": {
"type": "prometheus",
"uid": "$ds_prometheus"
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 25
},
"repeatDirection": "h",
"options": {
"legend": {
"displayMode": "table",
"placement": "bottom",
"showLegend": false,
"calcs": [
"mean",
"max"
]
},
"tooltip": {
"mode": "multi",
"sort": ""
}
},
"fieldConfig": {
"defaults": {
"unit": "percentunit",
"min": 0,
"max": 1,
"thresholds": {
"mode": "absolute",
"steps": [
{
"value": null,
"color": "green"
},
{
"value": 0.05,
"color": "yellow"
},
{
"value": 0.2,
"color": "red"
}
]
},
"custom": {
"drawStyle": "line",
"lineInterpolation": "linear",
"fillOpacity": 25,
"showPoints": "never"
}
},
"overrides": []
}
}
],
"templating": {
"list": [
{
"type": "datasource",
"name": "ds_prometheus",
"label": "Prometheus",
"skipUrlSync": false,
"query": "prometheus",
"current": {
"selected": false,
"text": "default",
"value": "default"
},
"multi": false,
"allowCustomValue": true,
"includeAll": false,
"regex": "",
"auto": false,
"auto_min": "10s",
"auto_count": 30
}
]
},
"annotations": {}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,957 @@
{
"uid": "gpu-performance",
"title": "GPU Performance",
"tags": [
"gpu",
"dcgm"
],
"timezone": "browser",
"editable": true,
"graphTooltip": 1,
"time": {
"from": "now-1h",
"to": "now"
},
"fiscalYearStartMonth": 0,
"schemaVersion": 42,
"panels": [
{
"type": "row",
"collapsed": false,
"title": "Overview",
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 0
},
"id": 1,
"panels": []
},
{
"type": "stat",
"id": 2,
"targets": [
{
"expr": "cluster:gpu_count:total",
"refId": "A"
}
],
"title": "Total GPUs",
"transparent": false,
"datasource": {
"type": "prometheus",
"uid": "$ds_prometheus"
},
"gridPos": {
"h": 4,
"w": 6,
"x": 0,
"y": 1
},
"repeatDirection": "h",
"options": {
"graphMode": "none",
"colorMode": "value",
"justifyMode": "auto",
"textMode": "value",
"wideLayout": true,
"showPercentChange": false,
"reduceOptions": {
"calcs": [
"lastNotNull"
]
},
"percentChangeColorMode": "standard",
"orientation": ""
},
"fieldConfig": {
"defaults": {
"unit": "short",
"thresholds": {
"mode": "absolute",
"steps": [
{
"value": null,
"color": "blue"
}
]
}
},
"overrides": []
}
},
{
"type": "stat",
"id": 3,
"targets": [
{
"expr": "cluster:gpu_count:allocated",
"refId": "A"
}
],
"title": "Allocated",
"transparent": false,
"datasource": {
"type": "prometheus",
"uid": "$ds_prometheus"
},
"gridPos": {
"h": 4,
"w": 6,
"x": 6,
"y": 1
},
"repeatDirection": "h",
"options": {
"graphMode": "none",
"colorMode": "value",
"justifyMode": "auto",
"textMode": "value",
"wideLayout": true,
"showPercentChange": false,
"reduceOptions": {
"calcs": [
"lastNotNull"
]
},
"percentChangeColorMode": "standard",
"orientation": ""
},
"fieldConfig": {
"defaults": {
"unit": "short",
"thresholds": {
"mode": "absolute",
"steps": [
{
"value": null,
"color": "green"
},
{
"value": 1,
"color": "yellow"
}
]
}
},
"overrides": []
}
},
{
"type": "stat",
"id": 4,
"targets": [
{
"expr": "cluster:gpu_util:avg",
"refId": "A"
}
],
"title": "Average utilization",
"transparent": false,
"datasource": {
"type": "prometheus",
"uid": "$ds_prometheus"
},
"gridPos": {
"h": 4,
"w": 6,
"x": 12,
"y": 1
},
"repeatDirection": "h",
"options": {
"graphMode": "area",
"colorMode": "value",
"justifyMode": "auto",
"textMode": "value",
"wideLayout": true,
"showPercentChange": false,
"reduceOptions": {
"calcs": [
"lastNotNull"
]
},
"percentChangeColorMode": "standard",
"orientation": ""
},
"fieldConfig": {
"defaults": {
"unit": "percent",
"min": 0,
"max": 100,
"thresholds": {
"mode": "absolute",
"steps": [
{
"value": null,
"color": "blue"
},
{
"value": 30,
"color": "green"
},
{
"value": 80,
"color": "orange"
}
]
}
},
"overrides": []
}
},
{
"type": "stat",
"id": 5,
"targets": [
{
"expr": "cluster:gpu_power_watts:sum",
"refId": "A"
}
],
"title": "Power draw",
"transparent": false,
"datasource": {
"type": "prometheus",
"uid": "$ds_prometheus"
},
"gridPos": {
"h": 4,
"w": 6,
"x": 18,
"y": 1
},
"repeatDirection": "h",
"options": {
"graphMode": "area",
"colorMode": "value",
"justifyMode": "auto",
"textMode": "value",
"wideLayout": true,
"showPercentChange": false,
"reduceOptions": {
"calcs": [
"lastNotNull"
]
},
"percentChangeColorMode": "standard",
"orientation": ""
},
"fieldConfig": {
"defaults": {
"unit": "watt",
"decimals": 0,
"thresholds": {
"mode": "absolute",
"steps": [
{
"value": null,
"color": "green"
}
]
}
},
"overrides": []
}
},
{
"type": "row",
"collapsed": false,
"title": "Utilization",
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 5
},
"id": 10,
"panels": []
},
{
"type": "timeseries",
"id": 11,
"targets": [
{
"expr": "DCGM_FI_DEV_GPU_UTIL{Hostname=~\"$Hostname\"}",
"legendFormat": "{{Hostname}}/{{gpu}} {{pod}}",
"refId": "A"
}
],
"title": "GPU Utilization (NVML)",
"transparent": false,
"datasource": {
"type": "prometheus",
"uid": "$ds_prometheus"
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 6
},
"repeatDirection": "h",
"options": {
"legend": {
"displayMode": "table",
"placement": "bottom",
"showLegend": false,
"calcs": [
"mean",
"max"
]
},
"tooltip": {
"mode": "multi",
"sort": ""
}
},
"fieldConfig": {
"defaults": {
"unit": "percent",
"min": 0,
"max": 100,
"custom": {
"drawStyle": "line",
"lineInterpolation": "linear",
"fillOpacity": 10,
"showPoints": "never",
"spanNulls": false
}
},
"overrides": []
}
},
{
"type": "timeseries",
"id": 12,
"targets": [
{
"expr": "DCGM_FI_PROF_PIPE_TENSOR_ACTIVE{Hostname=~\"$Hostname\"} * 100",
"legendFormat": "{{Hostname}}/{{gpu}} {{pod}}",
"refId": "A"
}
],
"title": "Tensor Pipe Active (realistic load for LLM/AI)",
"transparent": false,
"datasource": {
"type": "prometheus",
"uid": "$ds_prometheus"
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 6
},
"repeatDirection": "h",
"options": {
"legend": {
"displayMode": "table",
"placement": "bottom",
"showLegend": false,
"calcs": [
"mean",
"max"
]
},
"tooltip": {
"mode": "multi",
"sort": ""
}
},
"fieldConfig": {
"defaults": {
"unit": "percent",
"min": 0,
"max": 100,
"custom": {
"drawStyle": "line",
"lineInterpolation": "linear",
"fillOpacity": 10,
"showPoints": "never",
"spanNulls": false
}
},
"overrides": []
}
},
{
"type": "timeseries",
"id": 13,
"targets": [
{
"expr": "DCGM_FI_PROF_GR_ENGINE_ACTIVE{Hostname=~\"$Hostname\"} * 100",
"legendFormat": "{{Hostname}}/{{gpu}} {{pod}}",
"refId": "A"
}
],
"title": "Graphics Engine Active",
"transparent": false,
"datasource": {
"type": "prometheus",
"uid": "$ds_prometheus"
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 14
},
"repeatDirection": "h",
"options": {
"legend": {
"displayMode": "table",
"placement": "bottom",
"showLegend": false,
"calcs": [
"mean",
"max"
]
},
"tooltip": {
"mode": "multi",
"sort": ""
}
},
"fieldConfig": {
"defaults": {
"unit": "percent",
"min": 0,
"max": 100,
"custom": {
"drawStyle": "line",
"lineInterpolation": "linear",
"fillOpacity": 10,
"showPoints": "never",
"spanNulls": false
}
},
"overrides": []
}
},
{
"type": "timeseries",
"id": 14,
"targets": [
{
"expr": "DCGM_FI_DEV_MEM_COPY_UTIL{Hostname=~\"$Hostname\"}",
"legendFormat": "{{Hostname}}/{{gpu}} {{pod}}",
"refId": "A"
}
],
"title": "Memory Copy Utilization",
"transparent": false,
"datasource": {
"type": "prometheus",
"uid": "$ds_prometheus"
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 14
},
"repeatDirection": "h",
"options": {
"legend": {
"displayMode": "table",
"placement": "bottom",
"showLegend": false,
"calcs": [
"mean",
"max"
]
},
"tooltip": {
"mode": "multi",
"sort": ""
}
},
"fieldConfig": {
"defaults": {
"unit": "percent",
"min": 0,
"max": 100,
"custom": {
"drawStyle": "line",
"lineInterpolation": "linear",
"fillOpacity": 10,
"showPoints": "never",
"spanNulls": false
}
},
"overrides": []
}
},
{
"type": "row",
"collapsed": false,
"title": "Memory",
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 22
},
"id": 20,
"panels": []
},
{
"type": "timeseries",
"id": 21,
"targets": [
{
"expr": "DCGM_FI_DEV_FB_USED{Hostname=~\"$Hostname\"} * 1048576",
"legendFormat": "{{Hostname}}/{{gpu}} {{pod}}",
"refId": "A"
}
],
"title": "VRAM Used",
"transparent": false,
"datasource": {
"type": "prometheus",
"uid": "$ds_prometheus"
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 23
},
"repeatDirection": "h",
"options": {
"legend": {
"displayMode": "table",
"placement": "bottom",
"showLegend": false,
"calcs": [
"mean",
"max"
]
},
"tooltip": {
"mode": "multi",
"sort": ""
}
},
"fieldConfig": {
"defaults": {
"unit": "bytes",
"custom": {
"drawStyle": "line",
"lineInterpolation": "linear",
"fillOpacity": 25,
"showPoints": "never"
}
},
"overrides": []
}
},
{
"type": "timeseries",
"id": 22,
"targets": [
{
"expr": "DCGM_FI_DEV_FB_FREE{Hostname=~\"$Hostname\"} * 1048576",
"legendFormat": "{{Hostname}}/{{gpu}}",
"refId": "A"
}
],
"title": "VRAM Free",
"transparent": false,
"datasource": {
"type": "prometheus",
"uid": "$ds_prometheus"
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 23
},
"repeatDirection": "h",
"options": {
"legend": {
"displayMode": "table",
"placement": "bottom",
"showLegend": false,
"calcs": [
"mean",
"min"
]
},
"tooltip": {
"mode": "multi",
"sort": ""
}
},
"fieldConfig": {
"defaults": {
"unit": "bytes",
"custom": {
"drawStyle": "line",
"lineInterpolation": "linear",
"fillOpacity": 10,
"showPoints": "never"
}
},
"overrides": []
}
},
{
"type": "row",
"collapsed": false,
"title": "Power \u0026 Temperature",
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 31
},
"id": 30,
"panels": []
},
{
"type": "timeseries",
"id": 31,
"targets": [
{
"expr": "DCGM_FI_DEV_POWER_USAGE{Hostname=~\"$Hostname\"}",
"legendFormat": "{{Hostname}}/{{gpu}}",
"refId": "A"
}
],
"title": "Power Usage per GPU",
"description": "Hardware-level metric — shown cluster-wide regardless of namespace filter.",
"transparent": false,
"datasource": {
"type": "prometheus",
"uid": "$ds_prometheus"
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 32
},
"repeatDirection": "h",
"options": {
"legend": {
"displayMode": "table",
"placement": "bottom",
"showLegend": false,
"calcs": [
"mean",
"max"
]
},
"tooltip": {
"mode": "multi",
"sort": ""
}
},
"fieldConfig": {
"defaults": {
"unit": "watt",
"custom": {
"drawStyle": "line",
"lineInterpolation": "linear",
"fillOpacity": 10,
"showPoints": "never"
}
},
"overrides": []
}
},
{
"type": "timeseries",
"id": 32,
"targets": [
{
"expr": "DCGM_FI_DEV_GPU_TEMP{Hostname=~\"$Hostname\"}",
"legendFormat": "{{Hostname}}/{{gpu}}",
"refId": "A"
}
],
"title": "GPU Temperature",
"description": "Hardware-level metric — shown cluster-wide regardless of namespace filter.",
"transparent": false,
"datasource": {
"type": "prometheus",
"uid": "$ds_prometheus"
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 32
},
"repeatDirection": "h",
"options": {
"legend": {
"displayMode": "table",
"placement": "bottom",
"showLegend": false,
"calcs": [
"mean",
"max"
]
},
"tooltip": {
"mode": "multi",
"sort": ""
}
},
"fieldConfig": {
"defaults": {
"unit": "celsius",
"min": 20,
"max": 100,
"thresholds": {
"mode": "absolute",
"steps": [
{
"value": null,
"color": "green"
},
{
"value": 75,
"color": "yellow"
},
{
"value": 85,
"color": "red"
}
]
},
"custom": {
"drawStyle": "line",
"lineInterpolation": "linear",
"fillOpacity": 10,
"showPoints": "never"
}
},
"overrides": []
}
},
{
"type": "row",
"collapsed": false,
"title": "Health",
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 40
},
"id": 40,
"panels": []
},
{
"type": "stat",
"id": 41,
"targets": [
{
"expr": "max by (Hostname, gpu) (DCGM_FI_DEV_XID_ERRORS{Hostname=~\"$Hostname\"})",
"legendFormat": "{{Hostname}}/{{gpu}}",
"refId": "A"
}
],
"title": "XID errors (latest)",
"description": "Hardware-level metric — shown cluster-wide regardless of namespace filter.",
"transparent": false,
"datasource": {
"type": "prometheus",
"uid": "$ds_prometheus"
},
"gridPos": {
"h": 5,
"w": 8,
"x": 0,
"y": 41
},
"repeatDirection": "h",
"options": {
"graphMode": "none",
"colorMode": "background",
"justifyMode": "auto",
"textMode": "value_and_name",
"wideLayout": true,
"showPercentChange": false,
"reduceOptions": {
"calcs": [
"lastNotNull"
]
},
"percentChangeColorMode": "standard",
"orientation": ""
},
"fieldConfig": {
"defaults": {
"unit": "short",
"thresholds": {
"mode": "absolute",
"steps": [
{
"value": null,
"color": "green"
},
{
"value": 1,
"color": "red"
}
]
}
},
"overrides": []
}
},
{
"type": "timeseries",
"id": 42,
"targets": [
{
"expr": "rate(DCGM_FI_DEV_POWER_VIOLATION{Hostname=~\"$Hostname\"}[5m])",
"legendFormat": "{{Hostname}}/{{gpu}}",
"refId": "A"
}
],
"title": "Power Violation (µs/s throttled due to power)",
"description": "Hardware-level metric — shown cluster-wide regardless of namespace filter.",
"transparent": false,
"datasource": {
"type": "prometheus",
"uid": "$ds_prometheus"
},
"gridPos": {
"h": 5,
"w": 8,
"x": 8,
"y": 41
},
"repeatDirection": "h",
"options": {
"legend": {
"displayMode": "list",
"placement": "bottom",
"showLegend": false,
"calcs": []
},
"tooltip": {
"mode": "multi",
"sort": ""
}
},
"fieldConfig": {
"defaults": {
"unit": "µs",
"custom": {
"drawStyle": "line",
"lineInterpolation": "linear",
"fillOpacity": 10,
"showPoints": "never"
}
},
"overrides": []
}
},
{
"type": "timeseries",
"id": 43,
"targets": [
{
"expr": "rate(DCGM_FI_DEV_THERMAL_VIOLATION{Hostname=~\"$Hostname\"}[5m])",
"legendFormat": "{{Hostname}}/{{gpu}}",
"refId": "A"
}
],
"title": "Thermal Violation (µs/s throttled due to thermals)",
"description": "Hardware-level metric — shown cluster-wide regardless of namespace filter.",
"transparent": false,
"datasource": {
"type": "prometheus",
"uid": "$ds_prometheus"
},
"gridPos": {
"h": 5,
"w": 8,
"x": 16,
"y": 41
},
"repeatDirection": "h",
"options": {
"legend": {
"displayMode": "list",
"placement": "bottom",
"showLegend": false,
"calcs": []
},
"tooltip": {
"mode": "multi",
"sort": ""
}
},
"fieldConfig": {
"defaults": {
"unit": "µs",
"custom": {
"drawStyle": "line",
"lineInterpolation": "linear",
"fillOpacity": 10,
"showPoints": "never"
}
},
"overrides": []
}
}
],
"templating": {
"list": [
{
"type": "datasource",
"name": "ds_prometheus",
"label": "Prometheus",
"skipUrlSync": false,
"query": "prometheus",
"current": {
"selected": false,
"text": "default",
"value": "default"
},
"multi": false,
"allowCustomValue": true,
"includeAll": false,
"regex": "",
"auto": false,
"auto_min": "10s",
"auto_count": 30
},
{
"type": "query",
"name": "Hostname",
"label": "Host",
"skipUrlSync": false,
"query": "label_values(DCGM_FI_DEV_GPU_UTIL, Hostname)",
"datasource": {
"type": "prometheus",
"uid": "$ds_prometheus"
},
"current": {
"selected": true,
"text": "All",
"value": "$__all"
},
"multi": true,
"allowCustomValue": true,
"refresh": 2,
"sort": 1,
"includeAll": true,
"auto": false,
"auto_min": "10s",
"auto_count": 30
}
]
},
"annotations": {}
}

View file

@ -0,0 +1,637 @@
{
"uid": "gpu-quotas",
"title": "GPU Quotas \u0026 Allocation",
"tags": [
"gpu",
"quotas"
],
"timezone": "browser",
"editable": true,
"graphTooltip": 1,
"time": {
"from": "now-6h",
"to": "now"
},
"fiscalYearStartMonth": 0,
"schemaVersion": 42,
"panels": [
{
"type": "row",
"collapsed": false,
"title": "Allocation overview",
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 0
},
"id": 1,
"panels": []
},
{
"type": "stat",
"id": 2,
"targets": [
{
"expr": "sum(kube_node_status_allocatable{resource=\"nvidia_com_gpu\"})",
"refId": "A"
}
],
"title": "GPU allocatable",
"description": "Total GPU capacity the cluster can schedule to pods.",
"transparent": false,
"datasource": {
"type": "prometheus",
"uid": "$ds_prometheus"
},
"gridPos": {
"h": 4,
"w": 6,
"x": 0,
"y": 1
},
"repeatDirection": "h",
"options": {
"graphMode": "none",
"colorMode": "value",
"justifyMode": "auto",
"textMode": "value",
"wideLayout": true,
"showPercentChange": false,
"reduceOptions": {
"calcs": [
"lastNotNull"
]
},
"percentChangeColorMode": "standard",
"orientation": ""
},
"fieldConfig": {
"defaults": {
"unit": "short",
"thresholds": {
"mode": "absolute",
"steps": [
{
"value": null,
"color": "blue"
}
]
}
},
"overrides": []
}
},
{
"type": "stat",
"id": 3,
"targets": [
{
"expr": "cluster:gpu_count:allocated",
"refId": "A"
}
],
"title": "GPU requested",
"description": "Sum of GPU requests across all pods cluster-wide, including system namespaces.",
"transparent": false,
"datasource": {
"type": "prometheus",
"uid": "$ds_prometheus"
},
"gridPos": {
"h": 4,
"w": 6,
"x": 6,
"y": 1
},
"repeatDirection": "h",
"options": {
"graphMode": "none",
"colorMode": "value",
"justifyMode": "auto",
"textMode": "value",
"wideLayout": true,
"showPercentChange": false,
"reduceOptions": {
"calcs": [
"lastNotNull"
]
},
"percentChangeColorMode": "standard",
"orientation": ""
},
"fieldConfig": {
"defaults": {
"unit": "short",
"thresholds": {
"mode": "absolute",
"steps": [
{
"value": null,
"color": "green"
},
{
"value": 1,
"color": "yellow"
}
]
}
},
"overrides": []
}
},
{
"type": "gauge",
"id": 4,
"targets": [
{
"expr": "cluster:gpu_count:allocated / sum(kube_node_status_allocatable{resource=\"nvidia_com_gpu\"}) * 100",
"refId": "A"
}
],
"title": "Allocation ratio",
"description": "Percentage of allocatable GPUs currently requested by pods.",
"transparent": false,
"datasource": {
"type": "prometheus",
"uid": "$ds_prometheus"
},
"gridPos": {
"h": 4,
"w": 6,
"x": 12,
"y": 1
},
"repeatDirection": "h",
"options": {
"showThresholdLabels": false,
"showThresholdMarkers": true,
"sizing": "auto",
"minVizWidth": 75,
"reduceOptions": {
"calcs": [
"lastNotNull"
]
},
"minVizHeight": 75,
"orientation": ""
},
"fieldConfig": {
"defaults": {
"unit": "percent",
"min": 0,
"max": 100,
"thresholds": {
"mode": "absolute",
"steps": [
{
"value": null,
"color": "blue"
},
{
"value": 40,
"color": "green"
},
{
"value": 80,
"color": "yellow"
},
{
"value": 95,
"color": "red"
}
]
}
},
"overrides": []
}
},
{
"type": "stat",
"id": 5,
"targets": [
{
"expr": "count((sum by (namespace, pod) (kube_pod_container_resource_requests{resource=\"nvidia_com_gpu\"}) \u003e 0) * on(namespace, pod) group_left() (kube_pod_status_phase{phase=\"Pending\"} == 1)) or vector(0)",
"refId": "A"
}
],
"title": "Pending pods (GPU)",
"description": "Pods requesting GPUs that are stuck in Pending state — indicates capacity shortage.",
"transparent": false,
"datasource": {
"type": "prometheus",
"uid": "$ds_prometheus"
},
"gridPos": {
"h": 4,
"w": 6,
"x": 18,
"y": 1
},
"repeatDirection": "h",
"options": {
"graphMode": "none",
"colorMode": "background",
"justifyMode": "auto",
"textMode": "value",
"wideLayout": true,
"showPercentChange": false,
"reduceOptions": {
"calcs": [
"lastNotNull"
]
},
"percentChangeColorMode": "standard",
"orientation": ""
},
"fieldConfig": {
"defaults": {
"unit": "short",
"thresholds": {
"mode": "absolute",
"steps": [
{
"value": null,
"color": "green"
},
{
"value": 1,
"color": "red"
}
]
}
},
"overrides": []
}
},
{
"type": "row",
"collapsed": false,
"title": "Per namespace",
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 5
},
"id": 10,
"panels": []
},
{
"type": "bargauge",
"id": 11,
"targets": [
{
"expr": "sum by (namespace) (namespace:gpu_count:allocated{namespace=~\"$namespace\"})",
"legendFormat": "{{namespace}}",
"refId": "A"
}
],
"title": "GPU requested per namespace",
"description": "GPU allocation breakdown by namespace — spot top consumers at a glance.",
"transparent": false,
"datasource": {
"type": "prometheus",
"uid": "$ds_prometheus"
},
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 6
},
"repeatDirection": "h",
"options": {
"displayMode": "gradient",
"valueMode": "color",
"namePlacement": "auto",
"showUnfilled": true,
"sizing": "auto",
"minVizWidth": 8,
"minVizHeight": 16,
"legend": {
"displayMode": "list",
"placement": "bottom",
"showLegend": false,
"calcs": []
},
"reduceOptions": {
"calcs": [
"lastNotNull"
]
},
"maxVizHeight": 300,
"orientation": "horizontal"
},
"fieldConfig": {
"defaults": {
"unit": "short",
"min": 0,
"max": 8,
"thresholds": {
"mode": "absolute",
"steps": [
{
"value": null,
"color": "green"
}
]
}
},
"overrides": []
}
},
{
"type": "timeseries",
"id": 12,
"targets": [
{
"expr": "sum(kube_node_status_allocatable{resource=\"nvidia_com_gpu\"})",
"legendFormat": "Allocatable (total)",
"refId": "A"
},
{
"expr": "sum(namespace:gpu_count:allocated{namespace=~\"$namespace\"})",
"legendFormat": "Requested",
"refId": "B"
}
],
"title": "GPU allocated over time",
"description": "Requested vs allocatable GPUs over time — shows allocation pressure trends.",
"transparent": false,
"datasource": {
"type": "prometheus",
"uid": "$ds_prometheus"
},
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 6
},
"repeatDirection": "h",
"options": {
"legend": {
"displayMode": "list",
"placement": "bottom",
"showLegend": false,
"calcs": []
},
"tooltip": {
"mode": "multi",
"sort": ""
}
},
"fieldConfig": {
"defaults": {
"unit": "short",
"custom": {
"drawStyle": "line",
"lineInterpolation": "stepAfter",
"fillOpacity": 10,
"showPoints": "never"
}
},
"overrides": [
{
"matcher": {
"id": "byName",
"options": "Allocatable (total)"
},
"properties": [
{
"id": "color",
"value": {
"fixedColor": "blue",
"mode": "fixed"
}
}
]
}
]
}
},
{
"type": "row",
"collapsed": false,
"title": "Pods with GPU",
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 14
},
"id": 20,
"panels": []
},
{
"type": "table",
"id": 21,
"targets": [
{
"expr": "kube_pod_container_resource_requests{resource=\"nvidia_com_gpu\", namespace=~\"$namespace\"} * on(namespace, pod) group_left(phase) (kube_pod_status_phase{phase=~\"Running|Pending\"} == 1)",
"instant": true,
"range": false,
"format": "table",
"refId": "requested"
},
{
"expr": "kube_pod_container_resource_limits{resource=\"nvidia_com_gpu\", namespace=~\"$namespace\"} * on(namespace, pod) group_left() (kube_pod_status_phase{phase=~\"Running|Pending\"} == 1)",
"instant": true,
"range": false,
"format": "table",
"refId": "limits"
}
],
"title": "Pods requesting GPU",
"description": "Per-pod GPU requests and limits with scheduling status — Running or Pending.",
"transparent": false,
"datasource": {
"type": "prometheus",
"uid": "$ds_prometheus"
},
"gridPos": {
"h": 10,
"w": 24,
"x": 0,
"y": 15
},
"repeatDirection": "h",
"transformations": [
{
"id": "joinByField",
"options": {
"byField": "pod",
"mode": "outer"
}
},
{
"id": "organize",
"options": {
"excludeByName": {
"Time": true,
"Time 1": true,
"Time 2": true,
"__name__": true,
"__name__ 1": true,
"__name__ 2": true,
"cluster": true,
"cluster 1": true,
"cluster 2": true,
"container 2": true,
"endpoint": true,
"endpoint 1": true,
"endpoint 2": true,
"instance": true,
"instance 1": true,
"instance 2": true,
"job": true,
"job 1": true,
"job 2": true,
"namespace 2": true,
"node 2": true,
"prometheus": true,
"prometheus 1": true,
"prometheus 2": true,
"resource": true,
"resource 1": true,
"resource 2": true,
"service": true,
"service 1": true,
"service 2": true,
"tenant": true,
"tenant 1": true,
"tenant 2": true,
"tier": true,
"tier 1": true,
"tier 2": true,
"uid": true,
"uid 1": true,
"uid 2": true,
"unit": true,
"unit 1": true,
"unit 2": true
},
"indexByName": {
"Value #limits": 5,
"Value #requested": 4,
"container 1": 2,
"namespace 1": 0,
"node 1": 3,
"phase": 6,
"pod": 1
},
"renameByName": {
"Value #limits": "Limit",
"Value #requested": "Req",
"container 1": "Container",
"namespace 1": "Namespace",
"node 1": "Node",
"phase": "Status"
}
}
}
],
"options": {
"frameIndex": 0,
"showHeader": true,
"showTypeIcons": false,
"footer": {
"show": false,
"reducer": []
},
"cellHeight": "sm"
},
"fieldConfig": {
"defaults": {},
"overrides": [
{
"matcher": {
"id": "byName",
"options": "Status"
},
"properties": [
{
"id": "custom.cellOptions",
"value": {
"mode": "basic",
"type": "color-background"
}
},
{
"id": "mappings",
"value": [
{
"options": {
"Failed": {
"color": "red",
"index": 2
},
"Pending": {
"color": "orange",
"index": 1
},
"Running": {
"color": "green",
"index": 0
}
},
"type": "value"
}
]
}
]
}
]
}
}
],
"templating": {
"list": [
{
"type": "datasource",
"name": "ds_prometheus",
"label": "Prometheus",
"skipUrlSync": false,
"query": "prometheus",
"current": {
"selected": false,
"text": "default",
"value": "default"
},
"multi": false,
"allowCustomValue": true,
"includeAll": false,
"regex": "",
"auto": false,
"auto_min": "10s",
"auto_count": 30
},
{
"type": "query",
"name": "namespace",
"label": "Namespace",
"skipUrlSync": false,
"query": "label_values(kube_pod_container_resource_requests{resource=\"nvidia_com_gpu\"}, namespace)",
"datasource": {
"type": "prometheus",
"uid": "$ds_prometheus"
},
"current": {
"selected": true,
"text": "All",
"value": "$__all"
},
"multi": true,
"allowCustomValue": true,
"refresh": 2,
"sort": 1,
"includeAll": true,
"auto": false,
"auto_min": "10s",
"auto_count": 30
}
]
},
"annotations": {}
}

File diff suppressed because it is too large Load diff

37
docs/changelogs/v1.3.1.md Normal file
View file

@ -0,0 +1,37 @@
<!--
https://github.com/cozystack/cozystack/releases/tag/v1.3.1
-->
# v1.3.1 (2026-04-28)
Patch release covering a TenantNamespace IDOR fix in the API, a destructive `post-upgrade` hook removed from the etcd chart, kamaji controller stability, a `linstor-csi` bump that fixes live migration on Protocol-A/B DRBD resources, the missing `linstor-gui` build wiring, and a velero RBAC fix that unblocked installs on bundles without Velero.
## Security
* **fix(api): prevent IDOR in TenantNamespace Get and Watch handlers**: Two IDOR (Insecure Direct Object Reference) vulnerabilities allowed authenticated users to read TenantNamespace metadata they had no RoleBinding for. The `Get` and `Watch` handlers now go through a new `hasAccessToNamespace()` helper that lists RoleBindings scoped only to the target namespace (orders of magnitude faster than the previous all-cluster scan), returns `NotFound` instead of leaking existence on unauthorized access, and applies the same check on the `Watch` filter path. Includes regression tests for the unauthorized paths. ([**@IvanHunters**](https://github.com/IvanHunters) in #2471, backport #2524)
## Features
* **feat(linstor): bump linstor-csi to v1.10.6 with Protocol-C dual-attach fix**: Live migration of KubeVirt VMs on Protocol-A/B (async) DRBD volumes no longer fails with `Protocol C required`. `linstor-csi` v1.10.6 now installs a `Protocol=C` override on the resource-definition during dual-attach and reverts it on detach, so `replicated-async` StorageClasses and other Protocol-A/B resource groups support live migration without manual `drbdadm adjust` intervention. ([**@kvaps**](https://github.com/kvaps) in #2496, backport #2505)
## Fixes
* **fix(backups): move velero-configmap Role to velero chart**: The `backupstrategy-controller` (a default package) declared a Role/RoleBinding scoped to the `cozy-velero` namespace for managing `ResourceModifier` ConfigMaps. On bundles where Velero was not enabled, that namespace did not exist and the HelmRelease failed with `namespaces "cozy-velero" not found`, blocking installation. The Role/RoleBinding now lives in the velero chart, so it is created only when velero is actually deployed. ([**@myasnikovdaniil**](https://github.com/myasnikovdaniil) in #2459, backport #2467)
* **fix(etcd): remove destructive post-upgrade cert-regeneration hook**: The etcd chart ran a `post-upgrade` Helm hook on every upgrade that deleted etcd TLS Secrets (`etcd-ca-tls`, `etcd-peer-ca-tls`, `etcd-client-tls`, `etcd-peer-tls`, `etcd-server-tls`) and then deleted etcd pods, forcing cert-manager to re-issue the entire etcd CA chain. On clusters with Kamaji-managed tenant control planes this put every tenant `kube-apiserver` into CrashLoopBackOff until each DataStore was manually re-reconciled. The hook was a one-shot `2.6.0 → 2.6.1` migration that became a permanent footgun once chart versioning moved to `0.0.0+<git-hash>` (always `< 2.6.1` per semver) and after the underlying `rotationPolicy: Always` issue was fixed in `47d81f70`. The hook is now removed entirely. ([**@myasnikovdaniil**](https://github.com/myasnikovdaniil) in #2462, backport #2511)
* **fix(kamaji): increase memory limits and add startup probe**: The kamaji controller frequently entered CrashLoopBackOff due to OOMKills (exit 137) within ~2025 seconds of startup, with the readiness probe failing while the controller was still finishing initialization. Memory limit raised from 500Mi to 512Mi, request from 100Mi to 256Mi, and a 60-second startup probe (12 attempts × 5s periods) is added so the controller has room to boot before liveness/readiness probes engage. ([**@IvanHunters**](https://github.com/IvanHunters) in #2421, backport #2491)
## Build
* **build(linstor): include linstor-gui in root image build target**: The `linstor-gui` package (added in #2382) was never wired into the root `Makefile`'s `build:` target, so CI never built or published the image. `ghcr.io/cozystack/cozystack/linstor-gui` returned `NAME_UNKNOWN` and `values.yaml` stayed pinned to `tag: 2.3.0` without a digest. The missing build line is added so the next CI run publishes the image and the per-package `Makefile` digest-pins `values.yaml` automatically. ([**@myasnikovdaniil**](https://github.com/myasnikovdaniil) in #2498, backport #2518)
## Contributors
Thanks to everyone who contributed to this patch release:
* [**@IvanHunters**](https://github.com/IvanHunters)
* [**@kvaps**](https://github.com/kvaps)
* [**@myasnikovdaniil**](https://github.com/myasnikovdaniil)
**Full Changelog**: https://github.com/cozystack/cozystack/compare/v1.3.0...v1.3.1

View file

@ -0,0 +1,206 @@
#!/usr/bin/env bats
# -----------------------------------------------------------------------------
# Cross-validation between GPU recording rules, the dashboards that consume
# them, and the DCGM Exporter metric set the cluster actually scrapes. Catches:
#
# 1. dangling references — a dashboard query mentions a recording rule name
# that doesn't exist in gpu-recording.rules.yaml. This is the bug the
# pre-merge review caught: gpu-efficiency.json shipped panels keyed on
# pod:tensor_saturation:avg5m without the rule being defined, so the
# panel showed "No data" everywhere.
#
# 2. typos in rule names — same bug class, manifested as a single-character
# difference between rule and reference.
#
# 3. undeclared DCGM metrics — a dashboard query or recording rule mentions
# a DCGM_FI_* metric that is neither in the upstream default CSV nor in
# the project's custom CSV (dcgm-custom-metrics.yaml), meaning DCGM
# Exporter would never emit it and the panel silently shows "No data".
# Example regression: gpu-fleet.json shipped a TDP panel referencing
# DCGM_FI_DEV_POWER_MGMT_LIMIT before the custom CSV declared it.
#
# Scope: only dashboards listed in packages/system/monitoring/dashboards-infra.list
# under the "gpu/" prefix (i.e. shipped to production), not every JSON file in
# dashboards/gpu/. Untracked drafts stay out of scope on purpose — adding one
# to dashboards-infra.list is what brings it under the test.
#
# Reverse direction (rule defined but never consumed) is intentionally NOT
# enforced: some rules exist for ad-hoc PromQL or upcoming dashboards. Treat
# unused rules as an editorial concern, not a regression.
#
# Title syntax constraints from cozytest.sh's awk parser:
# - Titles delimited by ASCII double quotes; embedded quotes truncate.
# - Only [A-Za-z0-9] from the title survives into the function name; titles
# differing only in punctuation collapse to the same function.
#
# Run with: hack/cozytest.sh hack/check-gpu-recording-rules.bats
# -----------------------------------------------------------------------------
REPO_ROOT="$(cd "$(dirname "${BATS_TEST_FILENAME:-$0}")/.." && pwd)"
RULES_FILE="$REPO_ROOT/packages/system/monitoring-agents/alerts/gpu-recording.rules.yaml"
DASHBOARDS_LIST="$REPO_ROOT/packages/system/monitoring/dashboards-infra.list"
DASHBOARDS_DIR="$REPO_ROOT/dashboards"
DCGM_DEFAULT_CSV="$REPO_ROOT/hack/dcgm-default-counters.csv"
DCGM_CUSTOM_CSV="$REPO_ROOT/packages/system/gpu-operator/examples/dcgm-custom-metrics.yaml"
# Extract the set of "- record: NAME" entries from the rules YAML.
# Outputs one rule name per line, sorted and deduplicated.
extract_rules() {
awk '/^[[:space:]]*-[[:space:]]*record:[[:space:]]/ {
sub(/^[[:space:]]*-[[:space:]]*record:[[:space:]]*/, "")
sub(/[[:space:]]*$/, "")
print
}' "$RULES_FILE" | sort -u
}
# Extract the set of recording-rule references from a dashboard JSON.
# A recording-rule reference is matched by the pattern
# <segment>:<segment>(:<segment>)+
# where each <segment> is [a-z0-9_]. Raw DCGM metrics (DCGM_FI_*),
# kube-state-metrics (kube_*) and similar uppercase / single-word metric
# names do not match because the leading segment must be lowercase and the
# whole expression must contain at least two ':' characters.
extract_refs() {
json_file=$1
# Prometheus convention allows 2-segment rule names (level:metric); this
# regex is tuned to the 3+ segment convention used in this repo
# (level:metric:op — e.g. cluster:gpu_count:total). Update if future
# rules use 2 segments, otherwise they will be silently skipped.
grep -hoE '[a-z][a-z0-9_]*:[a-z0-9_]+:[a-z0-9_]+' "$json_file" | sort -u
}
# Resolve "gpu/foo" -> "$DASHBOARDS_DIR/gpu/foo.json"
list_tracked_gpu_dashboards() {
awk '/^gpu\// { print $0 ".json" }' "$DASHBOARDS_LIST"
}
# Extract the set of DCGM_FI_* metric names declared in a CSV file. Handles
# both the upstream-style default CSV (unindented) and the ConfigMap-style
# custom CSV (YAML-indented). A declaration line starts — after any leading
# whitespace — with "DCGM_FI_<NAME>," ; comment lines begin with "#" and are
# skipped. Uses POSIX awk's match()+RSTART/RLENGTH so no GNU extensions
# are required.
extract_csv_metrics() {
file=$1
awk '
{
line = $0
sub(/^[[:space:]]+/, "", line)
if (line ~ /^#/) next
if (match(line, /^DCGM_FI_[A-Z0-9_]+/)) {
print substr(line, RSTART, RLENGTH)
}
}
' "$file" | sort -u
}
# Extract the set of DCGM_FI_* metric references from a text file (dashboard
# JSON or rules YAML). A DCGM metric name has at least two underscore-delimited
# segments after the "DCGM_FI_" prefix (e.g. DCGM_FI_DEV_GPU_UTIL, DCGM_FI_PROF_
# PIPE_TENSOR_ACTIVE, DCGM_FI_DRIVER_VERSION). Requiring two segments keeps
# the matcher from latching onto glob stubs like "DCGM_FI_DEV_*_VIOLATION" that
# appear in comments.
extract_dcgm_refs() {
file=$1
grep -hoE 'DCGM_FI_[A-Z0-9]+(_[A-Z0-9]+)+' "$file" | sort -u
}
@test "every recording rule reference in tracked GPU dashboards has a matching record" {
TMP=$(mktemp -d)
trap 'rm -rf "$TMP"' EXIT
extract_rules > "$TMP/rules.txt"
[ -s "$TMP/rules.txt" ] || { echo "no recording rules extracted from $RULES_FILE" >&2; exit 1; }
list_tracked_gpu_dashboards > "$TMP/dashboards.txt"
[ -s "$TMP/dashboards.txt" ] || { echo "no gpu/* dashboards listed in $DASHBOARDS_LIST" >&2; exit 1; }
failed=0
while IFS= read -r dashboard_rel; do
dashboard="$DASHBOARDS_DIR/$dashboard_rel"
if [ ! -f "$dashboard" ]; then
echo "ERROR: dashboard listed but file missing: $dashboard" >&2
failed=1
continue
fi
extract_refs "$dashboard" > "$TMP/refs.txt"
# comm -23: lines unique to refs.txt (referenced but not defined)
# Both inputs must be sorted; extract_* helpers already sort.
comm -23 "$TMP/refs.txt" "$TMP/rules.txt" > "$TMP/missing.txt"
if [ -s "$TMP/missing.txt" ]; then
echo "ERROR: $dashboard_rel references undefined recording rules:" >&2
sed 's/^/ - /' "$TMP/missing.txt" >&2
failed=1
fi
done < "$TMP/dashboards.txt"
[ "$failed" -eq 0 ]
}
@test "every DCGM metric referenced in tracked dashboards and rules is declared" {
TMP=$(mktemp -d)
trap 'rm -rf "$TMP"' EXIT
[ -f "$DCGM_DEFAULT_CSV" ] || { echo "missing $DCGM_DEFAULT_CSV" >&2; exit 1; }
[ -f "$DCGM_CUSTOM_CSV" ] || { echo "missing $DCGM_CUSTOM_CSV" >&2; exit 1; }
{
extract_csv_metrics "$DCGM_DEFAULT_CSV"
extract_csv_metrics "$DCGM_CUSTOM_CSV"
} | sort -u > "$TMP/declared.txt"
[ -s "$TMP/declared.txt" ] || { echo "no DCGM metrics extracted from CSVs" >&2; exit 1; }
list_tracked_gpu_dashboards > "$TMP/dashboards.txt"
[ -s "$TMP/dashboards.txt" ] || { echo "no gpu/* dashboards listed in $DASHBOARDS_LIST" >&2; exit 1; }
failed=0
# Dashboard coverage — every dashboard's DCGM references must resolve.
while IFS= read -r dashboard_rel; do
dashboard="$DASHBOARDS_DIR/$dashboard_rel"
[ -f "$dashboard" ] || continue # handled by the existence test
extract_dcgm_refs "$dashboard" > "$TMP/refs.txt"
[ -s "$TMP/refs.txt" ] || continue # dashboard relies entirely on recording rules
comm -23 "$TMP/refs.txt" "$TMP/declared.txt" > "$TMP/missing.txt"
if [ -s "$TMP/missing.txt" ]; then
echo "ERROR: $dashboard_rel references DCGM metrics not declared in any CSV:" >&2
sed 's/^/ - /' "$TMP/missing.txt" >&2
failed=1
fi
done < "$TMP/dashboards.txt"
# Rules coverage — recording rules consume DCGM directly, so their set
# must be declared too, otherwise derived series on every dashboard
# collapse to empty.
extract_dcgm_refs "$RULES_FILE" > "$TMP/rule-refs.txt"
if [ -s "$TMP/rule-refs.txt" ]; then
comm -23 "$TMP/rule-refs.txt" "$TMP/declared.txt" > "$TMP/rule-missing.txt"
if [ -s "$TMP/rule-missing.txt" ]; then
echo "ERROR: gpu-recording.rules.yaml references DCGM metrics not declared in any CSV:" >&2
sed 's/^/ - /' "$TMP/rule-missing.txt" >&2
failed=1
fi
fi
[ "$failed" -eq 0 ]
}
@test "every tracked GPU dashboard listed in dashboards-infra.list exists on disk" {
TMP=$(mktemp -d)
trap 'rm -rf "$TMP"' EXIT
list_tracked_gpu_dashboards > "$TMP/dashboards.txt"
[ -s "$TMP/dashboards.txt" ] || { echo "no gpu/* dashboards listed in $DASHBOARDS_LIST" >&2; exit 1; }
failed=0
while IFS= read -r dashboard_rel; do
dashboard="$DASHBOARDS_DIR/$dashboard_rel"
if [ ! -f "$dashboard" ]; then
echo "ERROR: $dashboard_rel listed in dashboards-infra.list but $dashboard does not exist" >&2
failed=1
fi
done < "$TMP/dashboards.txt"
[ "$failed" -eq 0 ]
}

View file

@ -0,0 +1,104 @@
# Snapshot of the upstream DCGM Exporter default-counters.csv used as a
# fixture by hack/check-gpu-recording-rules.bats. The test verifies that
# every DCGM_FI_* metric referenced by a tracked GPU dashboard is either
# declared here (upstream defaults) or in
# packages/system/gpu-operator/examples/dcgm-custom-metrics.yaml
# (the project's custom CSV).
#
# Source: https://github.com/NVIDIA/dcgm-exporter/blob/4.1.1-4.0.4/etc/default-counters.csv
# Pinned to the DCGM Exporter image tag shipped by the gpu-operator
# chart under packages/system/gpu-operator/charts/gpu-operator/values.yaml
# (dcgmExporter.version = 4.1.1-4.0.4-ubuntu22.04). When that image is
# bumped, refresh this file from the matching tag in the NVIDIA/dcgm-exporter
# repo and re-run `./hack/cozytest.sh hack/check-gpu-recording-rules.bats`.
# Format
# If line starts with a '#' it is considered a comment
# DCGM FIELD, Prometheus metric type, help message
# Clocks
DCGM_FI_DEV_SM_CLOCK, gauge, SM clock frequency (in MHz).
DCGM_FI_DEV_MEM_CLOCK, gauge, Memory clock frequency (in MHz).
# Temperature
DCGM_FI_DEV_MEMORY_TEMP, gauge, Memory temperature (in C).
DCGM_FI_DEV_GPU_TEMP, gauge, GPU temperature (in C).
# Power
DCGM_FI_DEV_POWER_USAGE, gauge, Power draw (in W).
DCGM_FI_DEV_TOTAL_ENERGY_CONSUMPTION, counter, Total energy consumption since boot (in mJ).
# PCIE
# DCGM_FI_PROF_PCIE_TX_BYTES, counter, Total number of bytes transmitted through PCIe TX via NVML.
# DCGM_FI_PROF_PCIE_RX_BYTES, counter, Total number of bytes received through PCIe RX via NVML.
DCGM_FI_DEV_PCIE_REPLAY_COUNTER, counter, Total number of PCIe retries.
# Utilization (the sample period varies depending on the product)
DCGM_FI_DEV_GPU_UTIL, gauge, GPU utilization (in %).
DCGM_FI_DEV_MEM_COPY_UTIL, gauge, Memory utilization (in %).
DCGM_FI_DEV_ENC_UTIL, gauge, Encoder utilization (in %).
DCGM_FI_DEV_DEC_UTIL , gauge, Decoder utilization (in %).
# Errors and violations
DCGM_FI_DEV_XID_ERRORS, gauge, Value of the last XID error encountered.
# DCGM_FI_DEV_POWER_VIOLATION, counter, Throttling duration due to power constraints (in us).
# DCGM_FI_DEV_THERMAL_VIOLATION, counter, Throttling duration due to thermal constraints (in us).
# DCGM_FI_DEV_SYNC_BOOST_VIOLATION, counter, Throttling duration due to sync-boost constraints (in us).
# DCGM_FI_DEV_BOARD_LIMIT_VIOLATION, counter, Throttling duration due to board limit constraints (in us).
# DCGM_FI_DEV_LOW_UTIL_VIOLATION, counter, Throttling duration due to low utilization (in us).
# DCGM_FI_DEV_RELIABILITY_VIOLATION, counter, Throttling duration due to reliability constraints (in us).
# Memory usage
DCGM_FI_DEV_FB_FREE, gauge, Framebuffer memory free (in MiB).
DCGM_FI_DEV_FB_USED, gauge, Framebuffer memory used (in MiB).
# ECC
# DCGM_FI_DEV_ECC_SBE_VOL_TOTAL, counter, Total number of single-bit volatile ECC errors.
# DCGM_FI_DEV_ECC_DBE_VOL_TOTAL, counter, Total number of double-bit volatile ECC errors.
# DCGM_FI_DEV_ECC_SBE_AGG_TOTAL, counter, Total number of single-bit persistent ECC errors.
# DCGM_FI_DEV_ECC_DBE_AGG_TOTAL, counter, Total number of double-bit persistent ECC errors.
# Retired pages
# DCGM_FI_DEV_RETIRED_SBE, counter, Total number of retired pages due to single-bit errors.
# DCGM_FI_DEV_RETIRED_DBE, counter, Total number of retired pages due to double-bit errors.
# DCGM_FI_DEV_RETIRED_PENDING, counter, Total number of pages pending retirement.
# NVLink
# DCGM_FI_DEV_NVLINK_CRC_FLIT_ERROR_COUNT_TOTAL, counter, Total number of NVLink flow-control CRC errors.
# DCGM_FI_DEV_NVLINK_CRC_DATA_ERROR_COUNT_TOTAL, counter, Total number of NVLink data CRC errors.
# DCGM_FI_DEV_NVLINK_REPLAY_ERROR_COUNT_TOTAL, counter, Total number of NVLink retries.
# DCGM_FI_DEV_NVLINK_RECOVERY_ERROR_COUNT_TOTAL, counter, Total number of NVLink recovery errors.
DCGM_FI_DEV_NVLINK_BANDWIDTH_TOTAL, counter, Total number of NVLink bandwidth counters for all lanes.
# DCGM_FI_DEV_NVLINK_BANDWIDTH_L0, counter, The number of bytes of active NVLink rx or tx data including both header and payload.
# VGPU License status
DCGM_FI_DEV_VGPU_LICENSE_STATUS, gauge, vGPU License status
# Remapped rows
DCGM_FI_DEV_UNCORRECTABLE_REMAPPED_ROWS, counter, Number of remapped rows for uncorrectable errors
DCGM_FI_DEV_CORRECTABLE_REMAPPED_ROWS, counter, Number of remapped rows for correctable errors
DCGM_FI_DEV_ROW_REMAP_FAILURE, gauge, Whether remapping of rows has failed
# Static configuration information. These appear as labels on the other metrics
DCGM_FI_DRIVER_VERSION, label, Driver Version
# DCGM_FI_NVML_VERSION, label, NVML Version
# DCGM_FI_DEV_BRAND, label, Device Brand
# DCGM_FI_DEV_SERIAL, label, Device Serial Number
# DCGM_FI_DEV_OEM_INFOROM_VER, label, OEM inforom version
# DCGM_FI_DEV_ECC_INFOROM_VER, label, ECC inforom version
# DCGM_FI_DEV_POWER_INFOROM_VER, label, Power management object inforom version
# DCGM_FI_DEV_INFOROM_IMAGE_VER, label, Inforom image version
# DCGM_FI_DEV_VBIOS_VERSION, label, VBIOS version of the device
# Datacenter Profiling (DCP) metrics
# NOTE: supported on Nvidia datacenter Volta GPUs and newer
DCGM_FI_PROF_GR_ENGINE_ACTIVE, gauge, Ratio of time the graphics engine is active.
# DCGM_FI_PROF_SM_ACTIVE, gauge, The ratio of cycles an SM has at least 1 warp assigned.
# DCGM_FI_PROF_SM_OCCUPANCY, gauge, The ratio of number of warps resident on an SM.
DCGM_FI_PROF_PIPE_TENSOR_ACTIVE, gauge, Ratio of cycles the tensor (HMMA) pipe is active.
DCGM_FI_PROF_DRAM_ACTIVE, gauge, Ratio of cycles the device memory interface is active sending or receiving data.
# DCGM_FI_PROF_PIPE_FP64_ACTIVE, gauge, Ratio of cycles the fp64 pipes are active.
# DCGM_FI_PROF_PIPE_FP32_ACTIVE, gauge, Ratio of cycles the fp32 pipes are active.
# DCGM_FI_PROF_PIPE_FP16_ACTIVE, gauge, Ratio of cycles the fp16 pipes are active.
DCGM_FI_PROF_PCIE_TX_BYTES, gauge, The rate of data transmitted over the PCIe bus - including both protocol headers and data payloads - in bytes per second.
DCGM_FI_PROF_PCIE_RX_BYTES, gauge, The rate of data received over the PCIe bus - including both protocol headers and data payloads - in bytes per second.
Can't render this file because it has a wrong number of fields in line 12.

View file

@ -128,6 +128,9 @@ See the reference for components utilized in this service:
| `addons.gpuOperator` | NVIDIA GPU Operator. | `object` | `{}` |
| `addons.gpuOperator.enabled` | Enable GPU Operator. | `bool` | `false` |
| `addons.gpuOperator.valuesOverride` | Custom Helm values overrides. | `object` | `{}` |
| `addons.hami` | HAMi GPU virtualization middleware. | `object` | `{}` |
| `addons.hami.enabled` | Enable HAMi (requires GPU Operator). | `bool` | `false` |
| `addons.hami.valuesOverride` | Custom Helm values overrides. | `object` | `{}` |
| `addons.fluxcd` | FluxCD GitOps operator. | `object` | `{}` |
| `addons.fluxcd.enabled` | Enable FluxCD. | `bool` | `false` |
| `addons.fluxcd.valuesOverride` | Custom Helm values overrides. | `object` | `{}` |

View file

@ -1,3 +1,11 @@
{{- define "cozystack.defaultGpuOperatorValues" -}}
{{- if .Values.addons.hami.enabled }}
gpu-operator:
devicePlugin:
enabled: false
{{- end }}
{{- end }}
{{- if and .Values.addons.gpuOperator.enabled .Values._namespace.etcd }}
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
@ -29,9 +37,12 @@ spec:
force: true
remediation:
retries: -1
{{- with .Values.addons.gpuOperator.valuesOverride }}
{{- $defaults := fromYaml (include "cozystack.defaultGpuOperatorValues" .) }}
{{- $overrides := deepCopy (default (dict) .Values.addons.gpuOperator.valuesOverride) }}
{{- $merged := mergeOverwrite (default (dict) $defaults) $overrides }}
{{- if $merged }}
values:
{{- toYaml . | nindent 4 }}
{{- toYaml $merged | nindent 4 }}
{{- end }}
dependsOn:

View file

@ -0,0 +1,49 @@
{{- if and .Values.addons.hami.enabled .Values._namespace.etcd }}
{{- if not .Values.addons.gpuOperator.enabled }}
{{- fail "addons.hami requires addons.gpuOperator to be enabled" }}
{{- end }}
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: {{ .Release.Name }}-hami
labels:
cozystack.io/repository: system
cozystack.io/target-cluster-name: {{ .Release.Name }}
sharding.fluxcd.io/key: tenants
spec:
releaseName: hami
chartRef:
kind: ExternalArtifact
name: cozystack-kubernetes-application-kubevirt-kubernetes-hami
namespace: cozy-system
kubeConfig:
secretRef:
name: {{ .Release.Name }}-admin-kubeconfig
key: super-admin.svc
targetNamespace: cozy-hami
storageNamespace: cozy-hami
interval: 5m
timeout: 10m
install:
createNamespace: true
remediation:
retries: -1
upgrade:
force: true
remediation:
retries: -1
{{- with .Values.addons.hami.valuesOverride }}
values:
{{- toYaml . | nindent 4 }}
{{- end }}
dependsOn:
{{- if lookup "helm.toolkit.fluxcd.io/v2" "HelmRelease" .Release.Namespace .Release.Name }}
- name: {{ .Release.Name }}
namespace: {{ .Release.Namespace }}
{{- end }}
- name: {{ .Release.Name }}-cilium
namespace: {{ .Release.Namespace }}
- name: {{ .Release.Name }}-gpu-operator
namespace: {{ .Release.Namespace }}
{{- end }}

View file

@ -0,0 +1,99 @@
suite: GPU Operator HelmRelease HAMi integration tests
templates:
- templates/helmreleases/gpu-operator.yaml
values:
- values-ci.yaml
tests:
- it: should disable devicePlugin when hami is enabled
set:
addons:
gpuOperator:
enabled: true
valuesOverride: {}
hami:
enabled: true
valuesOverride: {}
asserts:
- equal:
path: spec.values.gpu-operator.devicePlugin.enabled
value: false
- it: should not have values when hami is disabled and no overrides
set:
addons:
gpuOperator:
enabled: true
valuesOverride: {}
hami:
enabled: false
valuesOverride: {}
asserts:
- notExists:
path: spec.values
- it: should apply hami defaults when valuesOverride key is omitted
set:
addons:
gpuOperator:
enabled: true
hami:
enabled: true
asserts:
- equal:
path: spec.values.gpu-operator.devicePlugin.enabled
value: false
- it: should allow user overrides to merge with hami defaults
set:
addons:
gpuOperator:
enabled: true
valuesOverride:
gpu-operator:
driver:
enabled: false
hami:
enabled: true
valuesOverride: {}
asserts:
- equal:
path: spec.values.gpu-operator.devicePlugin.enabled
value: false
- equal:
path: spec.values.gpu-operator.driver.enabled
value: false
- it: should let user explicitly override devicePlugin.enabled to true with hami enabled
set:
addons:
gpuOperator:
enabled: true
valuesOverride:
gpu-operator:
devicePlugin:
enabled: true
driver:
enabled: false
hami:
enabled: true
valuesOverride: {}
asserts:
- equal:
path: spec.values.gpu-operator.devicePlugin.enabled
value: true
- equal:
path: spec.values.gpu-operator.driver.enabled
value: false
- it: should not render when gpuOperator is disabled
set:
addons:
gpuOperator:
enabled: false
valuesOverride: {}
hami:
enabled: false
valuesOverride: {}
asserts:
- hasDocuments:
count: 0

View file

@ -0,0 +1,153 @@
suite: HAMi HelmRelease tests
templates:
- templates/helmreleases/hami.yaml
values:
- values-ci.yaml
tests:
- it: should not render when hami is disabled
set:
addons:
hami:
enabled: false
valuesOverride: {}
gpuOperator:
enabled: true
valuesOverride: {}
asserts:
- hasDocuments:
count: 0
- it: should render HelmRelease when hami is enabled
set:
addons:
hami:
enabled: true
valuesOverride: {}
gpuOperator:
enabled: true
valuesOverride: {}
asserts:
- hasDocuments:
count: 1
- isKind:
of: HelmRelease
- it: should fail when gpuOperator is not enabled
set:
addons:
hami:
enabled: true
valuesOverride: {}
gpuOperator:
enabled: false
valuesOverride: {}
asserts:
- failedTemplate:
errorMessage: "addons.hami requires addons.gpuOperator to be enabled"
- it: should have correct metadata labels
set:
addons:
hami:
enabled: true
valuesOverride: {}
gpuOperator:
enabled: true
valuesOverride: {}
asserts:
- equal:
path: metadata.labels["cozystack.io/repository"]
value: system
- equal:
path: metadata.labels["sharding.fluxcd.io/key"]
value: tenants
- it: should use ExternalArtifact chartRef
set:
addons:
hami:
enabled: true
valuesOverride: {}
gpuOperator:
enabled: true
valuesOverride: {}
asserts:
- equal:
path: spec.chartRef.kind
value: ExternalArtifact
- equal:
path: spec.chartRef.namespace
value: cozy-system
- it: should target cozy-hami namespace
set:
addons:
hami:
enabled: true
valuesOverride: {}
gpuOperator:
enabled: true
valuesOverride: {}
asserts:
- equal:
path: spec.targetNamespace
value: cozy-hami
- equal:
path: spec.storageNamespace
value: cozy-hami
- it: should depend on gpu-operator and cilium
release:
name: test
namespace: test-ns
set:
addons:
hami:
enabled: true
valuesOverride: {}
gpuOperator:
enabled: true
valuesOverride: {}
asserts:
- contains:
path: spec.dependsOn
content:
name: test-cilium
namespace: test-ns
- contains:
path: spec.dependsOn
content:
name: test-gpu-operator
namespace: test-ns
- it: should not render spec.values when valuesOverride is empty
set:
addons:
hami:
enabled: true
valuesOverride: {}
gpuOperator:
enabled: true
valuesOverride: {}
asserts:
- hasDocuments:
count: 1
- notExists:
path: spec.values
- it: should pass through valuesOverride
set:
addons:
hami:
enabled: true
valuesOverride:
hami:
devicePlugin:
deviceSplitCount: 5
gpuOperator:
enabled: true
valuesOverride: {}
asserts:
- equal:
path: spec.values.hami.devicePlugin.deviceSplitCount
value: 5

View file

@ -149,6 +149,7 @@
"fluxcd",
"gatewayAPI",
"gpuOperator",
"hami",
"ingressNginx",
"monitoringAgents",
"velero",
@ -268,6 +269,28 @@
}
}
},
"hami": {
"description": "HAMi GPU virtualization middleware.",
"type": "object",
"default": {},
"required": [
"enabled",
"valuesOverride"
],
"properties": {
"enabled": {
"description": "Enable HAMi (requires GPU Operator).",
"type": "boolean",
"default": false
},
"valuesOverride": {
"description": "Custom Helm values overrides.",
"type": "object",
"default": {},
"x-kubernetes-preserve-unknown-fields": true
}
}
},
"ingressNginx": {
"description": "Ingress-NGINX controller.",
"type": "object",

View file

@ -94,6 +94,10 @@ host: ""
## @field {bool} enabled - Enable FluxCD.
## @field {object} valuesOverride - Custom Helm values overrides.
## @typedef {struct} HAMiAddon - HAMi GPU virtualization middleware.
## @field {bool} enabled - Enable HAMi (requires GPU Operator).
## @field {object} valuesOverride - Custom Helm values overrides.
## @typedef {struct} MonitoringAgentsAddon - Monitoring agents (Fluent Bit, VMAgents).
## @field {bool} enabled - Enable monitoring agents.
## @field {object} valuesOverride - Custom Helm values overrides.
@ -114,6 +118,7 @@ host: ""
## @field {GatewayAPIAddon} gatewayAPI - Gateway API addon.
## @field {IngressNginxAddon} ingressNginx - Ingress-NGINX controller.
## @field {GPUOperatorAddon} gpuOperator - NVIDIA GPU Operator.
## @field {HAMiAddon} hami - HAMi GPU virtualization middleware.
## @field {FluxCDAddon} fluxcd - FluxCD GitOps operator.
## @field {MonitoringAgentsAddon} monitoringAgents - Monitoring agents.
## @field {VerticalPodAutoscalerAddon} verticalPodAutoscaler - Vertical Pod Autoscaler.
@ -137,6 +142,9 @@ addons:
gpuOperator:
enabled: false
valuesOverride: {}
hami:
enabled: false
valuesOverride: {}
fluxcd:
enabled: false
valuesOverride: {}

View file

@ -133,12 +133,13 @@ See:
### Bootstrap (recovery) parameters
| Name | Description | Type | Value |
| ------------------------ | ------------------------------------------------------------------- | -------- | ------- |
| `bootstrap` | Bootstrap configuration. | `object` | `{}` |
| `bootstrap.enabled` | Whether to restore from a backup. | `bool` | `false` |
| `bootstrap.recoveryTime` | Timestamp (RFC3339) for point-in-time recovery; empty means latest. | `string` | `""` |
| `bootstrap.oldName` | Previous cluster name before deletion. | `string` | `""` |
| Name | Description | Type | Value |
| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------- |
| `bootstrap` | Bootstrap configuration. | `object` | `{}` |
| `bootstrap.enabled` | Whether to restore from a backup. | `bool` | `false` |
| `bootstrap.recoveryTime` | Timestamp (RFC3339) for point-in-time recovery; empty means latest. | `string` | `""` |
| `bootstrap.oldName` | Previous cluster name before deletion. | `string` | `""` |
| `bootstrap.serverName` | Barman server name (S3 path prefix) used by the original cluster when writing backups. Set this only when the original cluster had an explicit barmanObjectStore.serverName that differed from its Kubernetes resource name. | `string` | `""` |
## Parameter examples and reference

View file

@ -32,6 +32,9 @@ spec:
- name: {{ .Values.bootstrap.oldName }}
barmanObjectStore:
destinationPath: {{ .Values.backup.destinationPath }}
{{- if .Values.bootstrap.serverName }}
serverName: {{ .Values.bootstrap.serverName }}
{{- end }}
endpointURL: {{ .Values.backup.endpointURL }}
s3Credentials:
accessKeyId:

View file

@ -254,6 +254,11 @@
"description": "Timestamp (RFC3339) for point-in-time recovery; empty means latest.",
"type": "string",
"default": ""
},
"serverName": {
"description": "Barman server name (S3 path prefix) used by the original cluster when writing backups. Set this only when the original cluster had an explicit barmanObjectStore.serverName that differed from its Kubernetes resource name.",
"type": "string",
"default": ""
}
}
}

View file

@ -154,6 +154,7 @@ backup:
## @field {bool} enabled - Whether to restore from a backup.
## @field {string} [recoveryTime] - Timestamp (RFC3339) for point-in-time recovery; empty means latest.
## @field {string} oldName - Previous cluster name before deletion.
## @field {string} [serverName] - Barman server name (S3 path prefix) used by the original cluster when writing backups. Set this only when the original cluster had an explicit barmanObjectStore.serverName that differed from its Kubernetes resource name.
## @param {Bootstrap} bootstrap - Bootstrap configuration.
bootstrap:
@ -161,3 +162,4 @@ bootstrap:
# example: 2020-11-26 15:22:00.00000+00
recoveryTime: ""
oldName: ""
serverName: ""

View file

@ -3,3 +3,6 @@ include ../../../hack/package.mk
generate:
cozyvalues-gen -m 'tenant' -v values.yaml -s values.schema.json -r README.md -g ../../../api/apps/v1alpha1/tenant/types.go
../../../hack/update-crd.sh
test:
helm unittest .

View file

@ -0,0 +1,28 @@
{{- $exposeMode := (index .Values._cluster "expose-mode") | default "externalIPs" }}
{{- $exposeIngress := (index .Values._cluster "expose-ingress") | default "tenant-root" }}
{{- $exposeIPs := (index .Values._cluster "expose-external-ips") | default "" | nospace }}
{{- $ipsList := list }}
{{- range splitList "," $exposeIPs }}
{{- $ip := . | trim }}
{{- if $ip }}
{{- $ipsList = append $ipsList $ip }}
{{- end }}
{{- end }}
{{- $isPublishingIngressLB := and
(eq $exposeMode "loadBalancer")
(eq $exposeIngress .Release.Namespace)
.Values.ingress }}
{{- if and $isPublishingIngressLB $ipsList }}
apiVersion: cilium.io/v2
kind: CiliumLoadBalancerIPPool
metadata:
name: {{ trimPrefix "tenant-" .Release.Namespace }}-exposure
spec:
blocks:
{{- range $ipsList }}
- cidr: {{ . }}{{ if not (contains "/" .) }}/{{ if contains ":" . }}128{{ else }}32{{ end }}{{ end }}
{{- end }}
serviceSelector:
matchLabels:
"io.kubernetes.service.namespace": {{ .Release.Namespace | quote }}
{{- end }}

View file

@ -0,0 +1,141 @@
suite: tenant CiliumLoadBalancerIPPool rendering for publishing.exposure=loadBalancer
templates:
- templates/cilium-lb-pool.yaml
release:
name: tenant-root
namespace: tenant-root
tests:
- it: default exposure (externalIPs) renders no pool
set:
ingress: true
_cluster:
expose-ingress: tenant-root
expose-external-ips: "192.0.2.10,192.0.2.11"
asserts:
- hasDocuments:
count: 0
- it: loadBalancer mode in publishing tenant with ingress=true renders v2 pool with namespace-only selector
set:
ingress: true
_cluster:
expose-ingress: tenant-root
expose-external-ips: "192.0.2.10,192.0.2.11"
expose-mode: loadBalancer
asserts:
- hasDocuments:
count: 1
- equal:
path: apiVersion
value: cilium.io/v2
- equal:
path: kind
value: CiliumLoadBalancerIPPool
- equal:
path: metadata.name
value: root-exposure
- equal:
path: spec.blocks
value:
- cidr: 192.0.2.10/32
- cidr: 192.0.2.11/32
- equal:
path: spec.serviceSelector.matchLabels["io.kubernetes.service.namespace"]
value: tenant-root
- notExists:
path: spec.serviceSelector.matchLabels["app.kubernetes.io/name"]
- it: loadBalancer mode with IPv6 emits /128 CIDR
set:
ingress: true
_cluster:
expose-ingress: tenant-root
expose-external-ips: "2001:db8::1"
expose-mode: loadBalancer
asserts:
- equal:
path: spec.blocks
value:
- cidr: 2001:db8::1/128
- it: loadBalancer mode with mixed IPv4 and IPv6 emits correct CIDR per family
set:
ingress: true
_cluster:
expose-ingress: tenant-root
expose-external-ips: "192.0.2.10,2001:db8::1"
expose-mode: loadBalancer
asserts:
- equal:
path: spec.blocks
value:
- cidr: 192.0.2.10/32
- cidr: 2001:db8::1/128
- it: loadBalancer mode accepts pre-CIDR input without double-suffixing
set:
ingress: true
_cluster:
expose-ingress: tenant-root
expose-external-ips: "192.0.2.10/32,2001:db8::1/128"
expose-mode: loadBalancer
asserts:
- equal:
path: spec.blocks
value:
- cidr: 192.0.2.10/32
- cidr: 2001:db8::1/128
- it: loadBalancer mode filters out empty entries from externalIPs (trailing, leading, repeated commas)
set:
ingress: true
_cluster:
expose-ingress: tenant-root
expose-external-ips: "192.0.2.10,,192.0.2.11,"
expose-mode: loadBalancer
asserts:
- hasDocuments:
count: 1
- equal:
path: spec.blocks
value:
- cidr: 192.0.2.10/32
- cidr: 192.0.2.11/32
- it: loadBalancer mode with ingress=false in publishing tenant renders no pool
set:
ingress: false
_cluster:
expose-ingress: tenant-root
expose-external-ips: "192.0.2.10"
expose-mode: loadBalancer
asserts:
- hasDocuments:
count: 0
- it: loadBalancer mode in a non-publishing tenant renders no pool
set:
ingress: true
_cluster:
expose-ingress: tenant-root
expose-external-ips: "192.0.2.10"
expose-mode: loadBalancer
release:
name: tenant-u1
namespace: tenant-u1
asserts:
- hasDocuments:
count: 0
- it: loadBalancer mode in publishing tenant with empty externalIPs renders no pool
set:
ingress: true
_cluster:
expose-ingress: tenant-root
expose-external-ips: ""
expose-mode: loadBalancer
asserts:
- hasDocuments:
count: 0

View file

@ -36,31 +36,32 @@ virtctl ssh <user>@<vm>
### Common parameters
| Name | Description | Type | Value |
| ------------------- | --------------------------------------------------------------------------------------------------------------------------------- | ---------- | ----------- |
| `external` | Enable external access from outside the cluster. | `bool` | `false` |
| `externalMethod` | Method to pass through traffic to the VM. | `string` | `PortList` |
| `externalPorts` | Ports to forward from outside the cluster. | `[]int` | `[22]` |
| `runStrategy` | Requested running state of the VirtualMachineInstance | `string` | `Always` |
| `instanceType` | Virtual Machine instance type. | `string` | `u1.medium` |
| `instanceProfile` | Virtual Machine preferences profile. | `string` | `ubuntu` |
| `disks` | List of disks to attach. | `[]object` | `[]` |
| `disks[i].name` | Disk name. | `string` | `""` |
| `disks[i].bus` | Disk bus type (e.g. "sata"). | `string` | `""` |
| `networks` | Networks to attach the VM to. | `[]object` | `[]` |
| `networks[i].name` | Network attachment name. | `string` | `""` |
| `subnets` | Deprecated: use networks instead. | `[]object` | `[]` |
| `subnets[i].name` | Network attachment name. | `string` | `""` |
| `gpus` | List of GPUs to attach (NVIDIA driver requires at least 4 GiB RAM). | `[]object` | `[]` |
| `gpus[i].name` | The name of the GPU resource to attach. | `string` | `""` |
| `cpuModel` | Model specifies the CPU model inside the VMI. List of available models https://github.com/libvirt/libvirt/tree/master/src/cpu_map | `string` | `""` |
| `resources` | Resource configuration for the virtual machine. | `object` | `{}` |
| `resources.cpu` | Number of CPU cores allocated. | `quantity` | `""` |
| `resources.memory` | Amount of memory allocated. | `quantity` | `""` |
| `resources.sockets` | Number of CPU sockets (vCPU topology). | `quantity` | `""` |
| `sshKeys` | List of SSH public keys for authentication. | `[]string` | `[]` |
| `cloudInit` | Cloud-init user data. | `string` | `""` |
| `cloudInitSeed` | Seed string to generate SMBIOS UUID for the VM. | `string` | `""` |
| Name | Description | Type | Value |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | ----------- |
| `external` | Enable external access from outside the cluster. | `bool` | `false` |
| `externalMethod` | Method to pass through traffic to the VM. | `string` | `PortList` |
| `externalPorts` | Ports to forward from outside the cluster. | `[]int` | `[22]` |
| `externalAllowICMP` | Whether to accept ICMP traffic to the VM in PortList mode (preserves ping and PMTU discovery). No effect in WholeIP mode. Default true so ping behaves as users expect even when port filtering is in effect. | `bool` | `true` |
| `runStrategy` | Requested running state of the VirtualMachineInstance | `string` | `Always` |
| `instanceType` | Virtual Machine instance type. | `string` | `u1.medium` |
| `instanceProfile` | Virtual Machine preferences profile. | `string` | `ubuntu` |
| `disks` | List of disks to attach. | `[]object` | `[]` |
| `disks[i].name` | Disk name. | `string` | `""` |
| `disks[i].bus` | Disk bus type (e.g. "sata"). | `string` | `""` |
| `networks` | Networks to attach the VM to. | `[]object` | `[]` |
| `networks[i].name` | Network attachment name. | `string` | `""` |
| `subnets` | Deprecated: use networks instead. | `[]object` | `[]` |
| `subnets[i].name` | Network attachment name. | `string` | `""` |
| `gpus` | List of GPUs to attach (NVIDIA driver requires at least 4 GiB RAM). | `[]object` | `[]` |
| `gpus[i].name` | The name of the GPU resource to attach. | `string` | `""` |
| `cpuModel` | Model specifies the CPU model inside the VMI. List of available models https://github.com/libvirt/libvirt/tree/master/src/cpu_map | `string` | `""` |
| `resources` | Resource configuration for the virtual machine. | `object` | `{}` |
| `resources.cpu` | Number of CPU cores allocated. | `quantity` | `""` |
| `resources.memory` | Amount of memory allocated. | `quantity` | `""` |
| `resources.sockets` | Number of CPU sockets (vCPU topology). | `quantity` | `""` |
| `sshKeys` | List of SSH public keys for authentication. | `[]string` | `[]` |
| `cloudInit` | Cloud-init user data. | `string` | `""` |
| `cloudInitSeed` | Seed string to generate SMBIOS UUID for the VM. | `string` | `""` |
## U Series

View file

@ -9,7 +9,10 @@ metadata:
{{- if .Values.external }}
service.kubernetes.io/service-proxy-name: "cozy-proxy"
annotations:
networking.cozystack.io/wholeIP: "true"
networking.cozystack.io/wholeIP: {{ ternary "true" "false" (eq .Values.externalMethod "WholeIP") | quote }}
{{- if eq .Values.externalMethod "PortList" }}
networking.cozystack.io/allowICMP: {{ ternary "true" "false" (ne .Values.externalAllowICMP false) | quote }}
{{- end }}
{{- end }}
spec:
type: {{ ternary "LoadBalancer" "ClusterIP" .Values.external }}

View file

@ -26,6 +26,11 @@
"type": "integer"
}
},
"externalAllowICMP": {
"description": "Whether to accept ICMP traffic to the VM in PortList mode (preserves ping and PMTU discovery). No effect in WholeIP mode. Default true so ping behaves as users expect even when port filtering is in effect.",
"type": "boolean",
"default": true
},
"runStrategy": {
"description": "Requested running state of the VirtualMachineInstance",
"type": "string",

View file

@ -31,6 +31,9 @@ externalMethod: PortList
externalPorts:
- 22
## @param {bool} externalAllowICMP - Whether to accept ICMP traffic to the VM in PortList mode (preserves ping and PMTU discovery). No effect in WholeIP mode. Default true so ping behaves as users expect even when port filtering is in effect.
externalAllowICMP: true
## @enum {string} RunStrategy - Requested running state of the VirtualMachineInstance
## @value Always - VMI should always be running
## @value Halted - VMI should never be running

View file

@ -0,0 +1,24 @@
---
apiVersion: cozystack.io/v1alpha1
kind: PackageSource
metadata:
name: cozystack.hami
spec:
sourceRef:
kind: OCIRepository
name: cozystack-packages
namespace: cozy-system
path: /
variants:
- name: default
dependsOn:
- cozystack.gpu-operator
components:
- name: hami
path: system/hami
valuesFiles:
- values.yaml
install:
privileged: true
namespace: cozy-hami
releaseName: hami

View file

@ -52,6 +52,8 @@ spec:
path: system/cilium
- name: kubernetes-gpu-operator
path: system/gpu-operator
- name: kubernetes-hami
path: system/hami
- name: kubernetes-vertical-pod-autoscaler
path: system/vertical-pod-autoscaler
- name: kubernetes-prometheus-operator-crds

View file

@ -30,6 +30,7 @@ stringData:
expose-services: {{ .Values.publishing.exposedServices | join "," | quote }}
expose-ingress: {{ .Values.publishing.ingressName | quote }}
expose-external-ips: {{ .Values.publishing.externalIPs | join "," | quote }}
expose-mode: {{ .Values.publishing.exposure | default "externalIPs" | quote }}
cluster-domain: {{ .Values.networking.clusterDomain | quote }}
api-server-endpoint: {{ .Values.publishing.apiServerEndpoint | quote }}
{{- with .Values.branding }}

View file

@ -10,6 +10,7 @@
{{include "cozystack.platform.package.default" (list "cozystack.kubevirt-cdi" $) }}
{{include "cozystack.platform.package.optional.default" (list "cozystack.vm-default-images" $) }}
{{include "cozystack.platform.package.optional.default" (list "cozystack.gpu-operator" $) }}
{{include "cozystack.platform.package.optional.default" (list "cozystack.hami" $) }}
{{include "cozystack.platform.package.default" (list "cozystack.kamaji" $) }}
{{include "cozystack.platform.package.default" (list "cozystack.capi-operator" $) }}
{{include "cozystack.platform.package.default" (list "cozystack.capi-provider-bootstrap-kubeadm" $) }}

View file

@ -105,9 +105,6 @@
{{- /* cozystack-api DaemonSet */ -}}
{{- $apiValues := dict "cozystackAPI" (dict "nodeSelector" $genericNodeSelector) -}}
{{- $_ := set $cozystackEngineComponents "cozystack-api" (dict "values" $apiValues) -}}
{{- /* lineage-controller-webhook DaemonSet */ -}}
{{- $lineageValues := dict "lineageControllerWebhook" (dict "nodeSelector" $genericNodeSelector) -}}
{{- $_ := set $cozystackEngineComponents "lineage-controller-webhook" (dict "values" $lineageValues) -}}
{{- end -}}
{{- if .Values.authentication.oidc.enabled }}
{{include "cozystack.platform.package" (list "cozystack.cozystack-engine" "oidc" $ $cozystackEngineComponents) }}

View file

@ -45,6 +45,41 @@ publishing:
- cdi-uploadproxy
apiServerEndpoint: "" # example: "https://api.example.org"
externalIPs: []
# Exposure mode for the ingress-nginx Service. When "externalIPs" (current
# default) is selected, the Service is created as ClusterIP with
# Service.spec.externalIPs set from publishing.externalIPs. When
# "loadBalancer" is selected, the Service is type: LoadBalancer and a
# CiliumLoadBalancerIPPool makes those same addresses allocatable via LB IPAM.
#
# Service.spec.externalIPs is deprecated upstream in Kubernetes v1.36
# (KEP-5707). The AllowServiceExternalIPs feature gate is expected to default
# to false around v1.40 and the implementation removed around v1.43 — switch
# to "loadBalancer" before upgrading past v1.40.
#
# Caveats for the "loadBalancer" mode:
# - publishing.externalIPs must contain at least one non-empty address,
# otherwise the chart render fails with an explicit error (a LoadBalancer
# Service without a pool would sit in <pending> forever).
# - The ingress-nginx Service is created with externalTrafficPolicy: Local
# to preserve the client source IP. Traffic arriving on a node that does
# not host an ingress-nginx pod is dropped, so the external IP must be
# routed to a node that runs the ingress pod (floating IP / keepalived /
# upstream router / podAntiAffinity).
# - Cilium does NOT announce the IP on its own unless L2 announcements or
# BGP are enabled in the Cilium values (disabled by default in Cozystack).
# This mode assumes the operator already routes the externalIPs to a
# cluster node; enabling announcements is out of scope for this setting.
# - Switching this value on a running cluster causes the ingress-nginx
# Service to be recreated (the HelmRelease has upgrade.force: true and
# the Service kind changes between ClusterIP and LoadBalancer). Expect a
# brief interruption of ingress traffic during the flip.
#
# Scope: this setting only controls the ingress-nginx Service. Other
# cozystack components that currently write Service.spec.externalIPs directly
# (e.g. the vpn app at packages/apps/vpn/templates/service.yaml) are NOT
# migrated by flipping this value and must be addressed separately before
# the AllowServiceExternalIPs feature gate flips to off in ~v1.40.
exposure: externalIPs # "externalIPs" or "loadBalancer"
certificates:
solver: http01 # "http01" or "dns01"
issuerName: letsencrypt-prod

View file

@ -5,3 +5,6 @@ include ../../../hack/package.mk
generate:
cozyvalues-gen -v values.yaml -s values.schema.json -r README.md
../../../hack/update-crd.sh
test:
helm unittest .

View file

@ -1,39 +0,0 @@
{{- $shouldUpdateCerts := true }}
{{- $configMap := lookup "v1" "ConfigMap" .Release.Namespace "etcd-deployed-version" }}
{{- if $configMap }}
{{- $deployedVersion := index $configMap "data" "version" }}
{{- if $deployedVersion | semverCompare ">= 2.6.1" }}
{{- $shouldUpdateCerts = false }}
{{- end }}
{{- end }}
{{- if $shouldUpdateCerts }}
---
apiVersion: batch/v1
kind: Job
metadata:
name: etcd-hook
annotations:
helm.sh/hook: post-upgrade
helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded
spec:
template:
metadata:
labels:
policy.cozystack.io/allow-to-apiserver: "true"
spec:
serviceAccountName: etcd-hook
containers:
- name: kubectl
image: docker.io/alpine/k8s:1.33.4
command:
- sh
args:
- -exc
- |-
kubectl --namespace={{ .Release.Namespace }} delete secrets etcd-ca-tls etcd-peer-ca-tls
sleep 10
kubectl --namespace={{ .Release.Namespace }} delete secrets etcd-client-tls etcd-peer-tls etcd-server-tls
kubectl --namespace={{ .Release.Namespace }} delete pods --selector=app.kubernetes.io/instance=etcd,app.kubernetes.io/managed-by=etcd-operator,app.kubernetes.io/name=etcd,cozystack.io/service=etcd
restartPolicy: Never
{{- end }}

View file

@ -1,26 +0,0 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
annotations:
helm.sh/hook: post-upgrade
helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded
name: etcd-hook
rules:
- apiGroups:
- ""
resources:
- secrets
- pods
verbs:
- get
- list
- watch
- delete
- apiGroups:
- ""
resources:
- configmaps
verbs:
- get
- list
- watch

View file

@ -1,15 +0,0 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: etcd-hook
annotations:
helm.sh/hook: post-upgrade
helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: etcd-hook
subjects:
- kind: ServiceAccount
name: etcd-hook
namespace: {{ .Release.Namespace | quote }}

View file

@ -1,7 +0,0 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: etcd-hook
annotations:
helm.sh/hook: post-upgrade
helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded

View file

@ -1,6 +0,0 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: etcd-deployed-version
data:
version: {{ .Chart.Version }}

View file

@ -0,0 +1,34 @@
suite: etcd chart does not ship a destructive post-upgrade cert-regeneration hook
release:
name: etcd
namespace: tenant-root
templates:
- templates/check-release-name.yaml
- templates/dashboard-resourcemap.yaml
- templates/datastore.yaml
- templates/etcd-defrag.yaml
- templates/hook/job.yaml
- templates/podscrape.yaml
- templates/prometheus-rules.yaml
- templates/version.yaml
tests:
- it: renders no Job named etcd-hook
documentSelector:
path: metadata.name
value: etcd-hook
skipEmptyTemplates: true
asserts:
- hasDocuments:
count: 0
- it: renders no ConfigMap named etcd-deployed-version
documentSelector:
path: metadata.name
value: etcd-deployed-version
skipEmptyTemplates: true
asserts:
- hasDocuments:
count: 0

View file

@ -10,3 +10,6 @@ get-cloudflare-ips:
generate:
cozyvalues-gen -v values.yaml -s values.schema.json -r README.md
../../../hack/update-crd.sh
test:
helm unittest .

View file

@ -14,3 +14,17 @@
| `resources.memory` | Memory (RAM) available to each replica. | `quantity` | `""` |
| `resourcesPreset` | Default sizing preset used when `resources` is omitted. | `string` | `micro` |
## Exposure mode
The ingress Service type is driven by the cluster-wide `publishing.exposure` value in the platform chart, not by any key in this package. Two modes exist:
- `externalIPs` (default) has three rendered shapes:
- Release namespace matches `publishing.ingressName` AND `publishing.externalIPs` is non-empty → Service is `ClusterIP` with `Service.spec.externalIPs` set from that list and `externalTrafficPolicy: Cluster`.
- Release namespace matches `publishing.ingressName` but `publishing.externalIPs` is empty → Service falls back to `type: LoadBalancer` with `externalTrafficPolicy: Local`.
- Release namespace does not match `publishing.ingressName` (non-root tenants) → Service is `type: LoadBalancer` with `externalTrafficPolicy: Local`.
`Service.spec.externalIPs` is deprecated upstream in Kubernetes v1.36 (KEP-5707); plan migration before v1.40.
- `loadBalancer` — Service is `type: LoadBalancer` with `externalTrafficPolicy: Local`, and a `CiliumLoadBalancerIPPool` makes the addresses in `publishing.externalIPs` allocatable via Cilium LB IPAM. Requires `publishing.externalIPs` to contain at least one non-empty address (render fails otherwise) and assumes the addresses are already routed to a cluster node (floating IP / upstream router). See the inline comment on `publishing.exposure` in the platform chart for full caveats, including the note that switching the value on a running cluster causes the ingress Service to be recreated.
This setting only migrates ingress-nginx away from `Service.spec.externalIPs`. Other cozystack components that use the same deprecated field (e.g. the `vpn` app) must be migrated separately before Kubernetes v1.40 flips the `AllowServiceExternalIPs` feature gate off.

View file

@ -1,5 +1,19 @@
{{- $exposeIngress := (index .Values._cluster "expose-ingress") | default "tenant-root" }}
{{- $exposeExternalIPs := (index .Values._cluster "expose-external-ips") | default "" | nospace }}
{{- $exposeMode := (index .Values._cluster "expose-mode") | default "externalIPs" }}
{{- $exposeIPsList := list }}
{{- range splitList "," $exposeExternalIPs }}
{{- $ip := . | trim }}
{{- if $ip }}
{{- $exposeIPsList = append $exposeIPsList $ip }}
{{- end }}
{{- end }}
{{- if not (has $exposeMode (list "externalIPs" "loadBalancer")) }}
{{- fail (printf "unknown publishing.exposure mode %q: must be \"externalIPs\" or \"loadBalancer\"" $exposeMode) }}
{{- end }}
{{- if and (eq $exposeMode "loadBalancer") (eq $exposeIngress .Release.Namespace) (not $exposeIPsList) }}
{{- fail "publishing.exposure=loadBalancer requires publishing.externalIPs to contain at least one non-empty address: the CiliumLoadBalancerIPPool has nothing to advertise and the Service would stay in <pending>." }}
{{- end }}
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
@ -41,9 +55,12 @@ spec:
enabled: false
{{- end }}
service:
{{- if and (eq $exposeIngress .Release.Namespace) $exposeExternalIPs }}
{{- if and (eq $exposeIngress .Release.Namespace) (eq $exposeMode "loadBalancer") }}
type: LoadBalancer
externalTrafficPolicy: Local
{{- else if and (eq $exposeIngress .Release.Namespace) $exposeIPsList }}
externalIPs:
{{- toYaml (splitList "," $exposeExternalIPs) | nindent 12 }}
{{- toYaml $exposeIPsList | nindent 12 }}
type: ClusterIP
externalTrafficPolicy: Cluster
{{- else }}

View file

@ -0,0 +1,165 @@
suite: ingress exposure modes
templates:
- templates/nginx-ingress.yaml
release:
name: ingress
namespace: tenant-root
tests:
- it: default exposure (externalIPs) renders ClusterIP Service with spec.externalIPs
set:
_cluster:
expose-ingress: tenant-root
expose-external-ips: "192.0.2.10,192.0.2.11"
asserts:
- template: templates/nginx-ingress.yaml
equal:
path: spec.values.ingress-nginx.controller.service.type
value: ClusterIP
- template: templates/nginx-ingress.yaml
equal:
path: spec.values.ingress-nginx.controller.service.externalTrafficPolicy
value: Cluster
- template: templates/nginx-ingress.yaml
equal:
path: spec.values.ingress-nginx.controller.service.externalIPs
value:
- 192.0.2.10
- 192.0.2.11
- template: templates/nginx-ingress.yaml
notExists:
path: spec.values.ingress-nginx.controller.service.labels
- it: legacy config without expose-mode falls back to externalIPs behavior
set:
_cluster:
expose-ingress: tenant-root
expose-external-ips: "192.0.2.10"
asserts:
- template: templates/nginx-ingress.yaml
equal:
path: spec.values.ingress-nginx.controller.service.type
value: ClusterIP
- it: externalIPs mode in a namespace other than publishing.ingressName renders LoadBalancer fallback without externalIPs
set:
_cluster:
expose-ingress: tenant-root
expose-external-ips: "192.0.2.10"
release:
namespace: tenant-other
asserts:
- template: templates/nginx-ingress.yaml
equal:
path: spec.values.ingress-nginx.controller.service.type
value: LoadBalancer
- template: templates/nginx-ingress.yaml
equal:
path: spec.values.ingress-nginx.controller.service.externalTrafficPolicy
value: Local
- template: templates/nginx-ingress.yaml
notExists:
path: spec.values.ingress-nginx.controller.service.externalIPs
- it: loadBalancer mode renders LoadBalancer Service without externalIPs on the Service
set:
_cluster:
expose-ingress: tenant-root
expose-external-ips: "192.0.2.10,192.0.2.11"
expose-mode: loadBalancer
asserts:
- template: templates/nginx-ingress.yaml
equal:
path: spec.values.ingress-nginx.controller.service.type
value: LoadBalancer
- template: templates/nginx-ingress.yaml
equal:
path: spec.values.ingress-nginx.controller.service.externalTrafficPolicy
value: Local
- template: templates/nginx-ingress.yaml
notExists:
path: spec.values.ingress-nginx.controller.service.labels
- template: templates/nginx-ingress.yaml
notExists:
path: spec.values.ingress-nginx.controller.service.externalIPs
- it: loadBalancer mode without externalIPs fails chart render with explicit message
set:
_cluster:
expose-ingress: tenant-root
expose-external-ips: ""
expose-mode: loadBalancer
asserts:
- template: templates/nginx-ingress.yaml
failedTemplate:
errorMessage: "publishing.exposure=loadBalancer requires publishing.externalIPs to contain at least one non-empty address: the CiliumLoadBalancerIPPool has nothing to advertise and the Service would stay in <pending>."
- it: unknown exposure mode is rejected with a clear error (case-sensitive enum)
set:
_cluster:
expose-ingress: tenant-root
expose-external-ips: "192.0.2.10"
expose-mode: LoadBalancer
asserts:
- template: templates/nginx-ingress.yaml
failedTemplate:
errorMessage: "unknown publishing.exposure mode \"LoadBalancer\": must be \"externalIPs\" or \"loadBalancer\""
- it: another typo in exposure mode also fails
set:
_cluster:
expose-ingress: tenant-root
expose-external-ips: "192.0.2.10"
expose-mode: loadbalancer
asserts:
- template: templates/nginx-ingress.yaml
failedTemplate:
errorMessage: "unknown publishing.exposure mode \"loadbalancer\": must be \"externalIPs\" or \"loadBalancer\""
- it: loadBalancer mode in a namespace other than publishing.ingressName falls back to LoadBalancer Service without pool
set:
_cluster:
expose-ingress: tenant-root
expose-external-ips: "192.0.2.10"
expose-mode: loadBalancer
release:
namespace: tenant-other
asserts:
- template: templates/nginx-ingress.yaml
equal:
path: spec.values.ingress-nginx.controller.service.type
value: LoadBalancer
- template: templates/nginx-ingress.yaml
equal:
path: spec.values.ingress-nginx.controller.service.externalTrafficPolicy
value: Local
- template: templates/nginx-ingress.yaml
notExists:
path: spec.values.ingress-nginx.controller.service.labels
- template: templates/nginx-ingress.yaml
notExists:
path: spec.values.ingress-nginx.controller.service.externalIPs
- it: loadBalancer mode with only-empty externalIPs fails chart render (comma-only input)
set:
_cluster:
expose-ingress: tenant-root
expose-external-ips: ",,"
expose-mode: loadBalancer
asserts:
- template: templates/nginx-ingress.yaml
failedTemplate:
errorMessage: "publishing.exposure=loadBalancer requires publishing.externalIPs to contain at least one non-empty address: the CiliumLoadBalancerIPPool has nothing to advertise and the Service would stay in <pending>."
- it: externalIPs mode also filters out empty entries (trailing comma)
set:
_cluster:
expose-ingress: tenant-root
expose-external-ips: "192.0.2.10,"
asserts:
- template: templates/nginx-ingress.yaml
equal:
path: spec.values.ingress-nginx.controller.service.externalIPs
value:
- 192.0.2.10

View file

@ -2,5 +2,5 @@ apiVersion: v2
name: cozy-proxy
description: A simple kube-proxy addon for 1:1 NAT services in Kubernetes using an NFT backend
type: application
version: 0.2.0
appVersion: 0.2.0
version: 0.3.0
appVersion: 0.3.0

View file

@ -1,6 +1,6 @@
image:
repository: ghcr.io/cozystack/cozystack/cozy-proxy
tag: v0.2.0
tag: v0.3.0
pullPolicy: IfNotPresent
daemonset:

View file

@ -0,0 +1,121 @@
# GPU operator — native pod workload on Talos (reference)
The files in this directory are **not** templates. They are reference
artifacts that document one working configuration for running GPU
workloads directly in pods on a Talos-based Cozystack cluster, together
with the DCGM metrics needed by the `gpu/gpu-performance` Grafana
dashboard.
The out-of-the-box `values-talos.yaml` for this package targets the
sandbox (VFIO passthrough to KubeVirt VMs) scenario. The files here
illustrate an alternative — running CUDA workloads in regular pods with
the NVIDIA device plugin — and the workarounds it currently requires on
Talos.
## Files
- [`values-native-talos.yaml`](./values-native-talos.yaml) — Cozystack
`Package` values that disable sandbox workloads, enable the device
plugin, point `hostPaths.driverInstallDir` at the staging location
used by the compat DaemonSet, and wire DCGM to the custom metrics
ConfigMap.
- [`dcgm-custom-metrics.yaml`](./dcgm-custom-metrics.yaml) — `ConfigMap`
with a DCGM metrics CSV that adds profiling, ECC, throttling and
energy counters on top of the upstream defaults. The CSV is a
superset needed for full coverage of the `gpu/gpu-performance`
dashboard. Which parts are actually required depends on which
dashboards you ship — see the table below.
- [`nvidia-driver-compat.yaml`](./nvidia-driver-compat.yaml) — DaemonSet
that stages `libnvidia-ml.so.1` and `nvidia-smi` from the Talos glibc
tree into a path where the NVIDIA GPU Operator validator expects
them. See the "Why the compat DaemonSet exists" section below.
## Why these are reference, not templates
Shipping these as first-class templates would silently impose
assumptions that do not hold for every user:
- Whether the NVIDIA Talos system extension is installed on the nodes.
- Whether GPUs are exposed directly to pods or passed through to VMs.
- The exact path the installed driver ends up at (depends on the
extension version and Talos release).
The sandbox-oriented `values-talos.yaml` remains the default. Operators
who want native pod GPU workloads can start from this directory and
adapt as needed.
## Why the compat DaemonSet exists
The NVIDIA GPU Operator validator checks for `libnvidia-ml.so.1` and
`bin/nvidia-smi` in the path given by `hostPaths.driverInstallDir`.
Talos installs them under `/usr/local/glibc/usr/lib/` and
`/usr/local/bin/`, which the validator does not look at. Until upstream
addresses [NVIDIA/gpu-operator#1687][1], the DaemonSet copies those
files into a directory the validator does inspect and creates the
`.driver-ctr-ready` flag file so the validator proceeds.
[1]: https://github.com/NVIDIA/gpu-operator/issues/1687
The compat DaemonSet runs privileged and bind-mounts host paths, so
the target namespace must allow privileged pods. On clusters that
enforce the Kubernetes Pod Security Standards at `baseline` or
`restricted`, label the namespace with
`pod-security.kubernetes.io/enforce: privileged` (and the matching
`audit`/`warn` labels if the admission webhook is configured to
surface violations) before applying the manifest.
## Dashboards and what DCGM metrics they need
Five GPU dashboards live under `gpu/*` in
`packages/system/monitoring/dashboards-infra.list`. All of them share
`packages/system/monitoring-agents/alerts/gpu-recording.rules.yaml` as
their source of aggregated series. The recording rules are safe to
ship on any cluster — they evaluate to empty series when DCGM is not
scraped, or when optional counters are missing.
What each dashboard needs on top of the upstream DCGM Exporter
[`default-counters.csv`][default-csv]:
| Dashboard | Scope | Needs beyond defaults |
| ----------------- | ---------------------------------- | ----------------------------------------------------------------------- |
| `gpu-performance` | Per-node, per-GPU deep dive | `DCGM_FI_DEV_POWER_VIOLATION`, `DCGM_FI_DEV_THERMAL_VIOLATION` |
| `gpu-efficiency` | Per-workload util vs tensor active | `DCGM_FI_DEV_POWER_VIOLATION`, `DCGM_FI_DEV_THERMAL_VIOLATION` (via `gpu:*_throttle_fraction:rate5m` recording rules) |
| `gpu-fleet` | Cluster-wide admin inventory | `DCGM_FI_DEV_POWER_MGMT_LIMIT` (for the TDP vs draw panel) |
| `gpu-quotas` | Kube-quota vs live usage | `kube_pod_container_resource_requests`, `kube_pod_status_phase`, `kube_node_status_allocatable` (via `namespace:gpu_count:allocated` / `cluster:gpu_count:*` recording rules) |
| `gpu-tenants` | Per-namespace tenant view | nothing (works on default counters) |
`DCGM_FI_PROF_PIPE_TENSOR_ACTIVE` and `DCGM_FI_PROF_GR_ENGINE_ACTIVE`
are already in the upstream default set for the pinned DCGM Exporter
version, so the tensor-saturation and engine-active panels work without
any CSV override. The three counters listed in the table — throttling
violations and the power management limit — are the only extras the
tracked dashboards need. The recording rules in
`gpu-recording.rules.yaml` consume utilization, FB used, power,
temperature and the tensor-active profiling counter from the default
set, plus `DCGM_FI_DEV_POWER_VIOLATION` and
`DCGM_FI_DEV_THERMAL_VIOLATION` — used by the
`gpu.recording.efficiency.1m` group to derive the
`gpu:power_throttle_fraction:rate5m` and
`gpu:thermal_throttle_fraction:rate5m` series consumed by the
throttling panels on the efficiency and fleet dashboards.
The `gpu.recording.throttle.validation.5m` group additionally ships the
`GPUThrottleFractionOverOne` alert (severity `warning`) as a regression
detector: it fires when either throttle-fraction series exceeds 1.0,
which would indicate that DCGM changed the scale/divisor of the
underlying violation counters and the recording rules need to be
re-derived.
## Verification status
The minimum-CSV claims above are verified by
`hack/check-gpu-recording-rules.bats`, which cross-checks every
`DCGM_FI_*` reference in the tracked GPU dashboards and recording rules
against the union of the upstream default set (snapshotted at
`hack/dcgm-default-counters.csv` for the pinned DCGM Exporter version)
and the custom CSV in `dcgm-custom-metrics.yaml`. When the DCGM
Exporter image in `packages/system/gpu-operator/charts/gpu-operator/values.yaml`
is bumped, refresh the snapshot from the matching tag of the
[`NVIDIA/dcgm-exporter`][default-csv] repository and rerun the test.
[default-csv]: https://github.com/NVIDIA/dcgm-exporter/blob/main/etc/default-counters.csv

View file

@ -0,0 +1,88 @@
# Custom DCGM Exporter metrics CSV. Referenced from
# examples/values-native-talos.yaml via dcgmExporter.config.name.
#
# Extends the upstream default set with profiling counters, ECC, page
# retirement, row remap, energy and throttling violations — everything
# the gpu/gpu-performance dashboard and the GPU recording rules in
# monitoring-agents expect.
apiVersion: v1
kind: ConfigMap
metadata:
name: dcgm-custom-metrics
namespace: cozy-gpu-operator
data:
dcgm-metrics.csv: |
# Format
# If line starts with a '#' it is considered a comment
# DCGM FIELD, Prometheus metric type, help message
# Identity
DCGM_FI_DRIVER_VERSION, label, Driver version.
# Clocks
DCGM_FI_DEV_SM_CLOCK, gauge, SM clock frequency (in MHz).
DCGM_FI_DEV_MEM_CLOCK, gauge, Memory clock frequency (in MHz).
# Temperature
DCGM_FI_DEV_MEMORY_TEMP, gauge, Memory temperature (in C).
DCGM_FI_DEV_GPU_TEMP, gauge, GPU temperature (in C).
# Power
DCGM_FI_DEV_POWER_USAGE, gauge, Power draw (in W).
DCGM_FI_DEV_POWER_MGMT_LIMIT, gauge, Current power management limit (in W).
DCGM_FI_DEV_TOTAL_ENERGY_CONSUMPTION, counter, Total energy consumption since boot (in mJ).
# PCIE
DCGM_FI_DEV_PCIE_REPLAY_COUNTER, counter, Total number of PCIe retries.
# Utilization (the sample period varies depending on the product)
DCGM_FI_DEV_GPU_UTIL, gauge, GPU utilization (in %).
DCGM_FI_DEV_MEM_COPY_UTIL, gauge, Memory utilization (in %).
DCGM_FI_DEV_ENC_UTIL, gauge, Encoder utilization (in %).
DCGM_FI_DEV_DEC_UTIL, gauge, Decoder utilization (in %).
# Errors and violations
DCGM_FI_DEV_XID_ERRORS, gauge, Value of the last XID error encountered.
# Memory usage
DCGM_FI_DEV_FB_FREE, gauge, Framebuffer memory free (in MiB).
DCGM_FI_DEV_FB_USED, gauge, Framebuffer memory used (in MiB).
DCGM_FI_DEV_FB_RESERVED, gauge, Framebuffer memory reserved (in MiB).
# ECC (supported on datacenter-class GPUs such as A10)
DCGM_FI_DEV_ECC_SBE_VOL_TOTAL, counter, Total number of single-bit volatile ECC errors.
DCGM_FI_DEV_ECC_DBE_VOL_TOTAL, counter, Total number of double-bit volatile ECC errors.
DCGM_FI_DEV_ECC_SBE_AGG_TOTAL, counter, Total number of single-bit persistent ECC errors.
DCGM_FI_DEV_ECC_DBE_AGG_TOTAL, counter, Total number of double-bit persistent ECC errors.
# Retired pages
DCGM_FI_DEV_RETIRED_SBE, counter, Total number of retired pages due to single-bit errors.
DCGM_FI_DEV_RETIRED_DBE, counter, Total number of retired pages due to double-bit errors.
DCGM_FI_DEV_RETIRED_PENDING, counter, Total number of pages pending retirement.
# Row remapping (not applicable to GDDR6 but left for datacenter GPUs)
DCGM_FI_DEV_UNCORRECTABLE_REMAPPED_ROWS, counter, Number of remapped rows for uncorrectable errors.
DCGM_FI_DEV_CORRECTABLE_REMAPPED_ROWS, counter, Number of remapped rows for correctable errors.
DCGM_FI_DEV_ROW_REMAP_FAILURE, gauge, Whether remapping of rows has failed.
# Throttle / violation counters (crucial for SLA and tenant monitoring)
DCGM_FI_DEV_POWER_VIOLATION, counter, Throttling duration due to power constraints (us per docs; ns on DCGM 3.x).
DCGM_FI_DEV_THERMAL_VIOLATION, counter, Throttling duration due to thermal constraints (us per docs; ns on DCGM 3.x).
DCGM_FI_DEV_SYNC_BOOST_VIOLATION, counter, Throttling duration due to sync-boost constraints (us per docs; ns on DCGM 3.x).
DCGM_FI_DEV_BOARD_LIMIT_VIOLATION, counter, Throttling duration due to board limit constraints (us per docs; ns on DCGM 3.x).
DCGM_FI_DEV_LOW_UTIL_VIOLATION, counter, Throttling duration due to low utilization (us per docs; ns on DCGM 3.x).
DCGM_FI_DEV_RELIABILITY_VIOLATION, counter, Throttling duration due to reliability constraints (us per docs; ns on DCGM 3.x).
# NVLink — DCGM silently drops this metric on GPUs without NVLink, so
# enabling it unconditionally is safe and keeps this file reusable
# across GPU families.
DCGM_FI_DEV_NVLINK_BANDWIDTH_TOTAL, counter, Total number of NVLink bandwidth counters for all lanes.
# DCP (profiling) metrics — supported from Ampere onwards.
DCGM_FI_PROF_GR_ENGINE_ACTIVE, gauge, Ratio of time the graphics engine is active.
DCGM_FI_PROF_SM_ACTIVE, gauge, The ratio of cycles an SM has at least 1 warp assigned.
DCGM_FI_PROF_SM_OCCUPANCY, gauge, The ratio of number of warps resident on an SM.
DCGM_FI_PROF_PIPE_TENSOR_ACTIVE, gauge, Ratio of cycles the tensor (HMMA) pipe is active.
DCGM_FI_PROF_DRAM_ACTIVE, gauge, Ratio of cycles the device memory interface is active sending or receiving data.
DCGM_FI_PROF_PCIE_TX_BYTES, counter, The number of bytes of active PCIe tx (transmit) data including both header and payload.
DCGM_FI_PROF_PCIE_RX_BYTES, counter, The number of bytes of active PCIe rx (read) data including both header and payload.

View file

@ -0,0 +1,109 @@
# Workaround for https://github.com/NVIDIA/gpu-operator/issues/1687
#
# On Talos, the NVIDIA system extension installs driver files under
# /usr/local/glibc/usr/lib/ and /usr/local/bin/. The GPU Operator
# validator only looks for libnvidia-ml.so.1 and bin/nvidia-smi under
# the path configured as hostPaths.driverInstallDir. This DaemonSet
# stages the required files into /var/nvidia-driver on each node and
# creates the .driver-ctr-ready flag so the validator proceeds.
#
# Paired with examples/values-native-talos.yaml, which sets
# hostPaths.driverInstallDir to /var/nvidia-driver.
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: nvidia-driver-compat
namespace: cozy-gpu-operator
labels:
app: nvidia-driver-compat
spec:
selector:
matchLabels:
app: nvidia-driver-compat
template:
metadata:
labels:
app: nvidia-driver-compat
# DaemonSet rather than a one-shot Job: staging is idempotent per-node,
# must re-run after every Talos reboot (hostPath contents are wiped on
# reboot when the system extension re-installs), and the pause container
# keeps the pod visible for operator observability (kubectl get pods).
spec:
priorityClassName: system-node-critical
# Restrict to GPU nodes only. Without this the DaemonSet schedules onto
# every node (including control-plane and CPU-only workers) and burns
# resources on hosts where the compat tree is meaningless.
# The label is published by Node Feature Discovery / GPU Operator's NFD
# subchart; if NFD is disabled, replace this with whatever label your
# cluster uses to mark GPU hosts.
nodeSelector:
nvidia.com/gpu.present: "true"
# Tolerate-all is intentionally broad: the nodeSelector above already
# confines scheduling to GPU nodes, so the "Exists" toleration only
# matters when those nodes carry extra taints (dedicated=gpu:NoSchedule,
# nvidia.com/gpu:NoSchedule from the GPU Operator's default policy,
# etc.). This mirrors the stance of the upstream nvidia-driver-daemonset
# and nvidia-device-plugin DaemonSets — blanket tolerations plus a
# narrow nodeSelector.
tolerations:
- operator: Exists
initContainers:
- name: create-driver-tree
image: busybox:1.37
command:
- sh
- -c
- |
set -e
GLIBC_LIB="/host/usr/local/glibc/usr/lib"
SRC_BIN="/host/usr/local/bin"
DST="/host/var/nvidia-driver"
mkdir -p "$DST/bin"
[ -f "$GLIBC_LIB/libnvidia-ml.so.1" ] || {
echo "missing $GLIBC_LIB/libnvidia-ml.so.1" >&2
exit 1
}
[ -f "$SRC_BIN/nvidia-smi" ] || {
echo "missing $SRC_BIN/nvidia-smi" >&2
exit 1
}
cp -f "$GLIBC_LIB/libnvidia-ml.so.1" "$DST/libnvidia-ml.so.1"
echo "Copied libnvidia-ml.so.1"
cp -f "$SRC_BIN/nvidia-smi" "$DST/bin/nvidia-smi"
chmod +x "$DST/bin/nvidia-smi"
echo "Copied nvidia-smi"
mkdir -p /host/run/nvidia/validations
touch /host/run/nvidia/validations/.driver-ctr-ready
echo "Created driver-ctr-ready flag"
echo "Done"
securityContext:
privileged: true
resources:
requests:
cpu: 10m
memory: 16Mi
limits:
cpu: 100m
memory: 64Mi
volumeMounts:
- name: host-root
mountPath: /host
containers:
- name: pause
image: registry.k8s.io/pause:3.10
resources:
requests:
cpu: 10m
memory: 8Mi
limits:
cpu: 50m
memory: 16Mi
volumes:
- name: host-root
hostPath:
path: /

View file

@ -0,0 +1,52 @@
# Cozystack Package values for running GPU workloads natively in pods
# on Talos. This is the counterpart to values-talos.yaml, which targets
# the sandbox (VFIO passthrough) scenario.
#
# Prerequisites:
# - NVIDIA Talos system extension installed on GPU nodes.
# - examples/nvidia-driver-compat.yaml deployed to stage driver files
# where the gpu-operator validator looks for them.
# - examples/dcgm-custom-metrics.yaml applied so DCGM Exporter exports
# the full set of metrics used by the dashboard and recording rules.
apiVersion: cozystack.io/v1alpha1
kind: Package
metadata:
name: cozystack.gpu-operator
spec:
variant: default
components:
gpu-operator:
values:
gpu-operator:
# The compat DaemonSet stages libnvidia-ml.so.1 and nvidia-smi
# under /var/nvidia-driver. Point the validator at that path.
hostPaths:
driverInstallDir: "/var/nvidia-driver"
# Disable the sandbox path — workloads run in pods, not VMs.
sandboxWorkloads:
enabled: false
vfioManager:
enabled: false
# The Talos extension provides the driver and runtime hooks,
# so the operator's own toolkit and driver components must be
# switched off to avoid conflicting installations.
toolkit:
enabled: false
devicePlugin:
enabled: true
# Export full set of DCGM metrics using the ConfigMap in
# examples/dcgm-custom-metrics.yaml.
dcgmExporter:
serviceMonitor:
enabled: true
# Matches the 1m evaluation cadence of GPU recording rules with
# 4x oversampling headroom. DCGM_FI_PROF_* counters have
# documented overhead concerns below 1s; 15s is safe.
interval: "15s"
honorLabels: true
relabelings:
- sourceLabels: [__meta_kubernetes_pod_node_name]
targetLabel: node
action: replace
config:
name: dcgm-custom-metrics

View file

@ -0,0 +1,3 @@
apiVersion: v2
name: cozy-hami
version: 0.0.0 # Placeholder, the actual version will be automatically set during the build process

View file

@ -0,0 +1,43 @@
export NAME=hami
export NAMESPACE=cozy-$(NAME)
include ../../../hack/common-envs.mk
include ../../../hack/package.mk
# When bumping the HAMi version, run `make update` and then review
# the resulting diff in `charts/hami/`. The recipe below reproduces the
# top-level vendoring overrides automatically:
#
# 1. Removes the broken hami-dra subchart. Upstream's NVIDIA DRA driver
# path requires kubelet DRA support that cozystack does not enable
# and has no upstream fix tracked. See commit 3c5521e.
# 2. Empties Chart.yaml dependencies and drops Chart.lock so Helm does
# not try to re-pull hami-dra at build time. See commit 2734dc0.
# 3. Strips dra/hami-dra/podSecurityPolicy blocks from the upstream
# values.yaml since the corresponding code paths are gone. PSP is
# removed from Kubernetes 1.25+ and is unused by cozystack.
#
# Template-level patches are NOT reproduced automatically:
#
# * Scheduler templates have `{{- if .Values.dra.enabled }}` blocks
# that need to be removed because the dra value is gone (commit
# 2734dc0 stripped them).
# * device-plugin/monitorservice.yaml uses `indent` with leading
# whitespace; it must be rewritten to `nindent` for the labels block
# to render correctly when devicePlugin.service.labels is set
# (commit 3685254).
#
# After `make update`, run `git diff -- charts/hami/templates/` and
# replay these template patches against the new upstream version, then
# verify with `helm unittest`. If upstream restructured the affected
# files, the patches may need to be redesigned rather than reapplied.
update:
rm -rf charts
helm repo add hami-charts https://project-hami.github.io/HAMi/
helm repo update hami-charts
helm pull hami-charts/hami --untar --untardir charts
rm -rf charts/hami/charts/hami-dra
yq --inplace '.dependencies = []' charts/hami/Chart.yaml
rm -f charts/hami/Chart.lock
yq --inplace 'del(.dra) | del(.["hami-dra"]) | del(.podSecurityPolicy)' charts/hami/values.yaml

View file

@ -0,0 +1,82 @@
# HAMi — GPU Virtualization Middleware
[HAMi](https://github.com/Project-HAMi/HAMi) (Heterogeneous AI Computing Virtualization Middleware) is a CNCF Sandbox project that enables fractional GPU sharing in Kubernetes. It allows workloads to request specific amounts of GPU memory and compute cores instead of claiming entire GPUs.
## Architecture
HAMi consists of four components:
- **MutatingWebhook** — intercepts pod creation, injects `schedulerName: hami-scheduler`
- **Scheduler Extender** — extends kube-scheduler with GPU-aware Filter and Bind logic
- **Device Plugin** (DaemonSet) — registers vGPU resources via the Kubernetes Device Plugin API
- **HAMi-core** (`libvgpu.so`) — `LD_PRELOAD` library injected into workload containers, intercepts CUDA API calls to enforce memory and compute isolation
## Prerequisites
- GPU Operator must be enabled (`addons.gpuOperator.enabled: true`)
- NVIDIA driver >= 440 on host nodes
- nvidia-container-toolkit configured as the default container runtime
- GPU nodes labeled with `gpu=on`
## Known Limitations
### glibc < 2.34 requirement for workload containers
HAMi-core uses `LD_PRELOAD` to intercept `dlsym()` for CUDA symbol resolution. The fallback code path relies on `_dl_sym`, a private glibc internal symbol that was removed in glibc 2.34 when libdl and libpthread were merged into libc.so.
**This limitation affects workload containers only**, not the host OS or HAMi's own components.
| Distribution | glibc | Result |
| --------------- | ----- | -------------------------------------------- |
| Ubuntu 18.04 | 2.27 | Full isolation (memory + compute) |
| Ubuntu 20.04 | 2.31 | Full isolation (memory + compute) |
| Ubuntu 22.04 | 2.35 | Memory isolation works, compute breaks |
| Ubuntu 24.04 | 2.39 | Both memory and compute isolation break |
| Alpine (musl) | N/A | Completely incompatible (`dlvsym` absent) |
Most modern ML/AI base images (CUDA 12.x, PyTorch 2.x, TensorFlow 2.x) use Ubuntu 22.04+ with glibc >= 2.35, which means compute isolation will not work with these images until the upstream fix is merged.
**Upstream tracking issues:**
- [HAMi-core#174](https://github.com/Project-HAMi/HAMi-core/issues/174) — `_dl_sym` removal in glibc 2.34 breaks HAMi-core's CUDA symbol resolution at the symbol level
- [HAMi#1190](https://github.com/Project-HAMi/HAMi/issues/1190) — maintainer thread confirming the empirical per-glibc-version isolation behavior shown in the table above
### musl libc (Alpine) incompatibility
HAMi-core is completely incompatible with musl libc. The `dlvsym()` function used by HAMi-core is a glibc extension not available in musl. Only glibc-based container images (Debian, Ubuntu, RHEL, etc.) can use HAMi GPU isolation.
## Usage
Enable HAMi in your tenant Kubernetes cluster values:
```yaml
addons:
gpuOperator:
enabled: true
hami:
enabled: true
```
When HAMi is enabled, GPU Operator's built-in device plugin is automatically disabled to avoid conflicts. This default is preserved by setting `addons.gpuOperator.valuesOverride.gpu-operator.devicePlugin.enabled: false`; advanced topologies that partition GPU pools (e.g. some nodes use HAMi while others run the standard NVIDIA device plugin via node selectors) can re-enable it explicitly through `valuesOverride`.
### Requesting fractional GPU resources
```yaml
resources:
limits:
nvidia.com/gpu: 1
nvidia.com/gpumem: 3000 # 3000 MB of GPU memory
nvidia.com/gpucores: 30 # 30% of GPU compute cores
```
## Parameters
Default values shown below are inherited from the upstream HAMi chart and may change with upstream updates.
| Name | Description | Default |
| --- | --- | --- |
| `hami.devicePlugin.runtimeClassName` | RuntimeClass for device plugin pods | `nvidia` |
| `hami.devicePlugin.deviceSplitCount` | Max virtual GPUs per physical GPU | `10` |
| `hami.devicePlugin.deviceMemoryScaling` | Memory overcommit factor (> 1.0 enables overcommit) | `1` |
| `hami.scheduler.defaultSchedulerPolicy.nodeSchedulerPolicy` | Node packing strategy (`binpack` or `spread`) | `binpack` |
| `hami.scheduler.defaultSchedulerPolicy.gpuSchedulerPolicy` | GPU packing strategy (`binpack` or `spread`) | `spread` |

View file

@ -0,0 +1,18 @@
apiVersion: v2
appVersion: 2.8.1
dependencies: []
description: Heterogeneous AI Computing Virtualization Middleware
keywords:
- vgpu
- gpu
kubeVersion: '>= 1.18.0-0'
maintainers:
- email: archlitchi@gmail.com
name: limengxuan
- email: xiaozhang0210@hotmail.com
name: zhangxiao
name: hami
sources:
- https://github.com/Project-HAMi/HAMi
type: application
version: 2.8.1

View file

@ -0,0 +1,237 @@
# HAMi Helm Chart Values Documentation
This document provides detailed descriptions of all configurable values parameters for the HAMi Helm Chart.
## Global Configuration
| Parameter | Description | Default Value |
|-----------|-------------|---------------|
| `global.imageRegistry` | Global Docker image registry | `""` |
| `global.imagePullSecrets` | Global Docker image pull secrets | `[]` |
| `global.imageTag` | Image tag | `"v2.8.1"` |
| `global.gpuHookPath` | GPU Hook path | `/usr/local` |
| `global.labels` | Global labels | `{}` |
| `global.annotations` | Global annotations | `{}` |
| `global.managedNodeSelectorEnable` | Whether to enable managed node selector | `false` |
| `global.managedNodeSelector.usage` | Managed node selector usage | `"gpu"` |
| `nameOverride` | Name override | `""` |
| `fullnameOverride` | Full name override | `""` |
| `namespaceOverride` | Namespace override | `""` |
## Resource Name Configuration
### NVIDIA GPU Resources
| Parameter | Description | Default Value |
|-----------|-------------|---------------|
| `resourceName` | GPU resource name | `"nvidia.com/gpu"` |
| `resourceMem` | GPU memory resource name | `"nvidia.com/gpumem"` |
| `resourceMemPercentage` | GPU memory percentage resource name | `"nvidia.com/gpumem-percentage"` |
| `resourceCores` | GPU core resource name | `"nvidia.com/gpucores"` |
| `resourcePriority` | GPU priority resource name | `"nvidia.com/priority"` |
### Cambricon MLU Resources
| Parameter | Description | Default Value |
|-----------|-------------|---------------|
| `mluResourceName` | MLU resource name | `"cambricon.com/vmlu"` |
| `mluResourceMem` | MLU memory resource name | `"cambricon.com/mlu.smlu.vmemory"` |
| `mluResourceCores` | MLU core resource name | `"cambricon.com/mlu.smlu.vcore"` |
### Hygon DCU Resources
| Parameter | Description | Default Value |
|-----------|-------------|---------------|
| `dcuResourceName` | DCU resource name | `"hygon.com/dcunum"` |
| `dcuResourceMem` | DCU memory resource name | `"hygon.com/dcumem"` |
| `dcuResourceCores` | DCU core resource name | `"hygon.com/dcucores"` |
### Metax GPU Resources
| Parameter | Description | Default Value |
|-----------|-------------|---------------|
| `metaxResourceName` | GPU resource name | `"metax-tech.com/sgpu"` |
| `metaxResourceCore` | GPU core resource name | `"metax-tech.com/vcore"` |
| `metaxResourceMem` | GPU memory resource name | `"metax-tech.com/vmemory"` |
| `metaxsGPUTopologyAware` | GPU topology awareness | `"false"` |
### Enflame GCU Resources
| Parameter | Description | Default Value |
|-----------|-------------|---------------|
| `enflameResourceNameVGCU` | vGCU resource name | `"enflame.com/vgcu"` |
| `enflameResourceNameVGCUPercentage` | vGCU percentage resource name | `"enflame.com/vgcu-percentage"` |
### Kunlunxin XPU Resources
| Parameter | Description | Default Value |
|-----------|-------------|---------------|
| `kunlunResourceName` | XPU resource name | `"kunlunxin.com/xpu"` |
## Scheduler Configuration
| Parameter | Description | Default Value |
|-----------|-------------|---------------|
| `schedulerName` | Scheduler name | `"hami-scheduler"` |
| `scheduler.nodeName` | Define node name, scheduler will schedule to this node | `""` |
| `scheduler.overwriteEnv` | Whether to overwrite environment variables | `"false"` |
| `scheduler.defaultSchedulerPolicy.nodeSchedulerPolicy` | Node scheduler policy | `binpack` |
| `scheduler.defaultSchedulerPolicy.gpuSchedulerPolicy` | GPU scheduler policy | `spread` |
| `scheduler.metricsBindAddress` | Metrics bind address | `":9395"` |
| `scheduler.forceOverwriteDefaultScheduler` | Whether to force overwrite default scheduler | `true` |
| `scheduler.livenessProbe` | Whether to enable liveness probe | `false` |
| `scheduler.leaderElect` | Whether to enable leader election | `true` |
| `scheduler.replicas` | Number of replicas | `1` |
### Kube Scheduler Configuration
| Parameter | Description | Default Value |
|-----------|-------------|---------------|
| `scheduler.kubeScheduler.enabled` | Whether to run kube-scheduler container in scheduler pod | `true` |
| `scheduler.kubeScheduler.image.registry` | Kube scheduler image registry | `"registry.cn-hangzhou.aliyuncs.com"` |
| `scheduler.kubeScheduler.image.repository` | Kube scheduler image repository | `"google_containers/kube-scheduler"` |
| `scheduler.kubeScheduler.image.tag` | Kube scheduler image tag | `""` |
| `scheduler.kubeScheduler.image.pullPolicy` | Kube scheduler image pull policy | `IfNotPresent` |
| `scheduler.kubeScheduler.image.pullSecrets` | Kube scheduler image pull secrets | `[]` |
| `scheduler.kubeScheduler.extraNewArgs` | Extra new arguments | `["--config=/config/config.yaml", "-v=4"]` |
| `scheduler.kubeScheduler.extraArgs` | Extra arguments | `["--policy-config-file=/config/config.json", "-v=4"]` |
### Extender Configuration
| Parameter | Description | Default Value |
|-----------|-------------|---------------|
| `scheduler.extender.image.registry` | Scheduler extender image registry | `"docker.io"` |
| `scheduler.extender.image.repository` | Scheduler extender image repository | `"projecthami/hami"` |
| `scheduler.extender.image.tag` | Scheduler extender image tag | `""` |
| `scheduler.extender.image.pullPolicy` | Scheduler extender image pull policy | `IfNotPresent` |
| `scheduler.extender.image.pullSecrets` | Scheduler extender image pull secrets | `[]` |
| `scheduler.extender.extraArgs` | Scheduler extender extra arguments | `["--debug", "-v=4"]` |
### Admission Webhook Configuration
| Parameter | Description | Default Value |
|-----------|-------------|---------------|
| `scheduler.admissionWebhook.enabled` | Whether to enable admission webhook | `true` |
| `scheduler.admissionWebhook.customURL.enabled` | Whether to enable custom URL | `false` |
| `scheduler.admissionWebhook.customURL.host` | Custom URL host | `127.0.0.1` |
| `scheduler.admissionWebhook.customURL.port` | Custom URL port | `31998` |
| `scheduler.admissionWebhook.customURL.path` | Custom URL path | `/webhook` |
| `scheduler.admissionWebhook.reinvocationPolicy` | Reinvocation policy | `Never` |
| `scheduler.admissionWebhook.failurePolicy` | Failure policy | `Ignore` |
### TLS Certificate Configuration
| Parameter | Description | Default Value |
|-----------|-------------|---------------|
| `scheduler.certManager.enabled` | Whether to use cert-manager to generate self-signed certificates | `false` |
| `scheduler.patch.enabled` | Whether to use kube-webhook-certgen to generate self-signed certificates | `true` |
| `scheduler.patch.image.registry` | Certgen image registry | `"docker.io"` |
| `scheduler.patch.image.repository` | Certgen image repository | `"jettech/kube-webhook-certgen"` |
| `scheduler.patch.image.tag` | Certgen image tag | `"v1.5.2"` |
| `scheduler.patch.image.pullPolicy` | Certgen image pull policy | `IfNotPresent` |
| `scheduler.patch.imageNew.registry` | New certgen image registry | `"docker.io"` |
| `scheduler.patch.imageNew.repository` | New certgen image repository | `"liangjw/kube-webhook-certgen"` |
| `scheduler.patch.imageNew.tag` | New certgen image tag | `"v1.1.1"` |
### Scheduler Service Configuration
| Parameter | Description | Default Value |
|-----------|-------------|---------------|
| `scheduler.service.type` | Service type | `NodePort` |
| `scheduler.service.httpPort` | HTTP port | `443` |
| `scheduler.service.schedulerPort` | Scheduler NodePort | `31998` |
| `scheduler.service.monitorPort` | Monitor port | `31993` |
| `scheduler.service.monitorTargetPort` | Monitor target port | `9395` |
## Device Plugin Configuration
| Parameter | Description | Default Value |
|-----------|-------------|---------------|
| `devicePlugin.image.registry` | Device plugin image registry | `"docker.io"` |
| `devicePlugin.image.repository` | Device plugin image repository | `"projecthami/hami"` |
| `devicePlugin.image.tag` | Device plugin image tag | `""` |
| `devicePlugin.image.pullPolicy` | Device plugin image pull policy | `IfNotPresent` |
| `devicePlugin.image.pullSecrets` | Device plugin image pull secrets | `[]` |
### Monitor Configuration
| Parameter | Description | Default Value |
|-----------|-------------|---------------|
| `devicePlugin.monitor.image.registry` | Monitor image registry | `"docker.io"` |
| `devicePlugin.monitor.image.repository` | Monitor image repository | `"projecthami/hami"` |
| `devicePlugin.monitor.image.tag` | Monitor image tag | `""` |
| `devicePlugin.monitor.image.pullPolicy` | Monitor image pull policy | `IfNotPresent` |
| `devicePlugin.monitor.image.pullSecrets` | Monitor image pull secrets | `[]` |
| `devicePlugin.monitor.ctrPath` | Container path | `/usr/local/vgpu/containers` |
| `devicePlugin.monitor.extraArgs` | Monitor extra arguments | `["-v=4"]` |
| `devicePlugin.monitor.extraEnvs` | Monitor extra environments | `{}` |
### Device Plugin Other Configuration
| Parameter | Description | Default Value |
|-----------|-------------|---------------|
| `devicePlugin.deviceSplitCount` | Integer type, default value: 10. Maximum number of tasks assigned to a single GPU device | `10` |
| `devicePlugin.deviceMemoryScaling` | Device memory scaling ratio | `1` |
| `devicePlugin.deviceCoreScaling` | Device core scaling ratio | `1` |
| `devicePlugin.runtimeClassName` | Runtime class name | `""` |
| `devicePlugin.createRuntimeClass` | Whether to create runtime class | `false` |
| `devicePlugin.migStrategy` | String type, "none" means ignore MIG functionality, "mixed" means allocate MIG devices through independent resources | `"none"` |
| `devicePlugin.disablecorelimit` | String type, "true" means disable core limit, "false" means enable core limit | `"false"` |
| `devicePlugin.passDeviceSpecsEnabled` | Whether to enable passing device specs | `false` |
| `devicePlugin.extraArgs` | Device plugin extra arguments | `["-v=4"]` |
| `devicePlugin.nodeConfiguration.config` | Node configuration for device plugin by json | An example of default configuration. |
| `devicePlugin.nodeConfiguration.externalConfigName` | Node configuration for device plugin by external congimap | `""` |
| `devicePlugin.extraEnvs` | Device plugin extra environments | `{}` |
### Device Plugin Service Configuration
| Parameter | Description | Default Value |
|-----------|-------------|---------------|
| `devicePlugin.service.type` | Service type | `NodePort` |
| `devicePlugin.service.httpPort` | HTTP port | `31992` |
### Device Plugin Deployment Configuration
| Parameter | Description | Default Value |
|-----------|-------------|---------------|
| `devicePlugin.pluginPath` | Plugin path | `/var/lib/kubelet/device-plugins` |
| `devicePlugin.libPath` | Library path | `/usr/local/vgpu` |
| `devicePlugin.nvidiaNodeSelector` | NVIDIA node selector | `{"gpu": "on"}` |
| `devicePlugin.updateStrategy.type` | Update strategy type | `RollingUpdate` |
| `devicePlugin.updateStrategy.rollingUpdate.maxUnavailable` | Maximum unavailable count | `1` |
## Device Configuration
### AWS Neuron
| Parameter | Description | Default Value |
|-----------|-------------|---------------|
| `devices.awsneuron.customresources` | Custom resources | `["aws.amazon.com/neuron", "aws.amazon.com/neuroncore"]` |
### Kunlunxin
| Parameter | Description | Default Value |
|-----------|-------------|---------------|
| `devices.kunlun.enabled` | Whether to enable | `true` |
| `devices.kunlun.customresources` | Custom resources | `["kunlunxin.com/xpu"]` |
### Mthreads
| Parameter | Description | Default Value |
|-----------|-------------|---------------|
| `devices.mthreads.enabled` | Whether to enable | `true` |
| `devices.mthreads.customresources` | Custom resources | `["mthreads.com/vgpu"]` |
### NVIDIA
| Parameter | Description | Default Value |
|-----------|-------------|---------------|
| `devices.nvidia.gpuCorePolicy` | GPU core policy | `default` |
| `devices.nvidia.libCudaLogLevel` | CUDA library log level | `1` |
### Huawei Ascend
| Parameter | Description | Default Value |
|-----------|-------------|---------------|
| `devices.ascend.enabled` | Whether to enable | `false` |
| `devices.ascend.image` | Image | `""` |
| `devices.ascend.imagePullPolicy` | Image pull policy | `IfNotPresent` |
| `devices.ascend.extraArgs` | Extra arguments | `[]` |
| `devices.ascend.nodeSelector` | Node selector | `{"ascend": "on"}` |
| `devices.ascend.tolerations` | Tolerations | `[]` |
| `devices.ascend.customresources` | Custom resources | `["huawei.com/Ascend910A", "huawei.com/Ascend910A-memory", ...]` |
### Iluvatar
| Parameter | Description | Default Value |
|-----------|-------------|---------------|
| `devices.iluvatar.enabled` | Whether to enable | `false` |
| `devices.iluvatar.customresources` | Custom resources | `["iluvatar.ai/BI-V150-vgpu", "iluvatar.ai/BI-V150.vMem","iluvatar.ai/BI-V150.vCore", ...]` |

View file

@ -0,0 +1,3 @@
** Please be patient while the chart is being deployed **
Resource name: {{ .Values.resourceName }}

View file

@ -0,0 +1,49 @@
{{/*
Return the proper image name
{{ include "common.images.image" ( dict "imageRoot" .Values.path.to.the.image "global" $) }}
*/}}
{{- define "common.images.image" -}}
{{- $registryName := .imageRoot.registry -}}
{{- $repositoryName := .imageRoot.repository -}}
{{- $tag := .imageRoot.tag | toString -}}
{{- if .global }}
{{- if .global.imageRegistry }}
{{- $registryName = .global.imageRegistry -}}
{{- end -}}
{{- end -}}
{{- if .tag }}
{{- $tag = .tag | toString -}}
{{- end -}}
{{- if $registryName }}
{{- printf "%s/%s:%s" $registryName $repositoryName $tag -}}
{{- else -}}
{{- printf "%s:%s" $repositoryName $tag -}}
{{- end -}}
{{- end -}}
{{/*
Return the proper Docker Image Registry Secret Names (deprecated: use common.images.renderPullSecrets instead)
{{ include "common.images.pullSecrets" ( dict "images" (list .Values.path.to.the.image1, .Values.path.to.the.image2) "global" .Values.global) }}
*/}}
{{- define "common.images.pullSecrets" -}}
{{- $pullSecrets := list }}
{{- if .global }}
{{- range .global.imagePullSecrets -}}
{{- $pullSecrets = append $pullSecrets . -}}
{{- end -}}
{{- end -}}
{{- range .images -}}
{{- range .pullSecrets -}}
{{- $pullSecrets = append $pullSecrets . -}}
{{- end -}}
{{- end -}}
{{- if (not (empty $pullSecrets)) }}
imagePullSecrets:
{{- range $pullSecrets }}
- name: {{ . }}
{{- end }}
{{- end }}
{{- end -}}

View file

@ -0,0 +1,163 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "hami-vgpu.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "hami-vgpu.fullname" -}}
{{- if .Values.fullnameOverride -}}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride -}}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{/*
Allow the release namespace to be overridden for multi-namespace deployments in combined charts
*/}}
{{- define "hami-vgpu.namespace" -}}
{{- if .Values.namespaceOverride -}}
{{- .Values.namespaceOverride -}}
{{- else -}}
{{- .Release.Namespace -}}
{{- end -}}
{{- end -}}
{{/*
The app name for Scheduler
*/}}
{{- define "hami-vgpu.scheduler" -}}
{{- printf "%s-scheduler" ( include "hami-vgpu.fullname" . ) | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{/*
The app name for DevicePlugin
*/}}
{{- define "hami-vgpu.device-plugin" -}}
{{- printf "%s-device-plugin" ( include "hami-vgpu.fullname" . ) | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{/*
The app name for MockDevicePlugin
*/}}
{{- define "hami-vgpu.mock-device-plugin" -}}
{{- printf "%s-mock-device-plugin" ( include "hami-vgpu.fullname" . ) | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{/*
The tls secret name for Scheduler
*/}}
{{- define "hami-vgpu.scheduler.tls" -}}
{{- printf "%s-scheduler-tls" ( include "hami-vgpu.fullname" . ) | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{/*
The webhook name
*/}}
{{- define "hami-vgpu.scheduler.webhook" -}}
{{- printf "%s-webhook" ( include "hami-vgpu.fullname" . ) | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "hami-vgpu.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "hami-vgpu.labels" -}}
helm.sh/chart: {{ include "hami-vgpu.chart" . }}
{{ include "hami-vgpu.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "hami-vgpu.selectorLabels" -}}
app.kubernetes.io/name: {{ include "hami-vgpu.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Resolve the tag for kubeScheduler.
*/}}
{{- define "resolvedKubeSchedulerTag" -}}
{{- if .Values.scheduler.kubeScheduler.image.tag }}
{{- .Values.scheduler.kubeScheduler.image.tag | trim -}}
{{- else }}
{{- include "strippedKubeVersion" . | trim -}}
{{- end }}
{{- end }}
{{/*
Return the stripped Kubernetes version string by removing extra parts after semantic version number.
v1.31.1+k3s1 -> v1.31.1
v1.30.8-eks-2d5f260 -> v1.30.8
v1.31.1 -> v1.31.1
*/}}
{{- define "strippedKubeVersion" -}}
{{ regexReplaceAll "^(v[0-9]+\\.[0-9]+\\.[0-9]+)(.*)$" .Capabilities.KubeVersion.Version "$1" }}
{{- end -}}
{{- define "hami.scheduler.kubeScheduler.image" -}}
{{ include "common.images.image" (dict "imageRoot" .Values.scheduler.kubeScheduler.image "global" .Values.global "tag" (include "resolvedKubeSchedulerTag" .)) }}
{{- end -}}
{{- define "hami.scheduler.extender.image" -}}
{{ include "common.images.image" (dict "imageRoot" .Values.scheduler.extender.image "global" .Values.global "tag" .Values.global.imageTag) }}
{{- end -}}
{{- define "hami.devicePlugin.image" -}}
{{ include "common.images.image" (dict "imageRoot" .Values.devicePlugin.image "global" .Values.global "tag" .Values.global.imageTag) }}
{{- end -}}
{{- define "hami.mockDevicePlugin.image" -}}
{{ include "common.images.image" (dict "imageRoot" .Values.mockDevicePlugin.image "global" .Values.global "tag" .Values.mockDevicePlugin.tag) }}
{{- end -}}
{{- define "hami.devicePlugin.monitor.image" -}}
{{ include "common.images.image" (dict "imageRoot" .Values.devicePlugin.monitor.image "global" .Values.global "tag" .Values.global.imageTag) }}
{{- end -}}
{{- define "hami.scheduler.patch.image" -}}
{{ include "common.images.image" (dict "imageRoot" .Values.scheduler.patch.image "global" .Values.global) }}
{{- end -}}
{{- define "hami.scheduler.patch.new.image" -}}
{{ include "common.images.image" (dict "imageRoot" .Values.scheduler.patch.imageNew "global" .Values.global) }}
{{- end -}}
{{- define "hami.scheduler.extender.imagePullSecrets" -}}
{{ include "common.images.pullSecrets" (dict "images" (list .Values.scheduler.extender.image) "global" .Values.global) }}
{{- end -}}
{{- define "hami.devicePlugin.imagePullSecrets" -}}
{{ include "common.images.pullSecrets" (dict "images" (list .Values.devicePlugin.image) "global" .Values.global) }}
{{- end -}}
{{- define "hami.scheduler.patch.imagePullSecrets" -}}
{{ include "common.images.pullSecrets" (dict "images" (list .Values.scheduler.patch.image) "global" .Values.global) }}
{{- end -}}
{{- define "hami.scheduler.patch.new.imagePullSecrets" -}}
{{ include "common.images.pullSecrets" (dict "images" (list .Values.scheduler.patch.imageNew) "global" .Values.global) }}
{{- end -}}

View file

@ -0,0 +1,13 @@
{{- if and .Values.devicePlugin.enabled (not .Values.devicePlugin.nodeConfiguration.externalConfigName) -}}
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "hami-vgpu.device-plugin" . }}
namespace: {{ include "hami-vgpu.namespace" . }}
labels:
app.kubernetes.io/component: hami-device-plugin
{{- include "hami-vgpu.labels" . | nindent 4 }}
data:
config.json: |
{{- .Values.devicePlugin.nodeConfiguration.config | nindent 4 }}
{{- end }}

View file

@ -0,0 +1,55 @@
{{- if .Values.mockDevicePlugin.enabled }}
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: {{ include "hami-vgpu.mock-device-plugin" . }}
namespace: {{ include "hami-vgpu.namespace" . }}
spec:
selector:
matchLabels:
app.kubernetes.io/component: hami-mock-device-plugin
{{- include "hami-vgpu.selectorLabels" . | nindent 6 }}
template:
metadata:
annotations:
scheduler.alpha.kubernetes.io/critical-pod: ""
labels:
app.kubernetes.io/component: hami-mock-device-plugin
{{- include "hami-vgpu.selectorLabels" . | nindent 8 }}
spec:
serviceAccountName: {{ include "hami-vgpu.mock-device-plugin" . }}
tolerations:
- key: CriticalAddonsOnly
operator: Exists
containers:
- image: {{ include "hami.mockDevicePlugin.image" . }}
imagePullPolicy: {{ .Values.mockDevicePlugin.image.pullPolicy }}
name: hami-mock-dp-cntr
env:
- name: NODE_NAME
valueFrom:
fieldRef:
fieldPath: spec.nodeName
command:
- ./k8s-device-plugin
- -v=5
- --device-config-file=/device-config.yaml
volumeMounts:
- name: dp
mountPath: /var/lib/kubelet/device-plugins
- name: sys
mountPath: /sys
- name: device-config
mountPath: /device-config.yaml
subPath: device-config.yaml
volumes:
- name: dp
hostPath:
path: /var/lib/kubelet/device-plugins
- name: sys
hostPath:
path: /sys
- name: device-config
configMap:
name: {{ include "hami-vgpu.scheduler" . }}-device
{{- end -}}

View file

@ -0,0 +1,262 @@
{{- if .Values.devicePlugin.enabled }}
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: {{ include "hami-vgpu.device-plugin" . }}
namespace: {{ include "hami-vgpu.namespace" . }}
labels:
app.kubernetes.io/component: hami-device-plugin
{{- include "hami-vgpu.labels" . | nindent 4 }}
{{- with .Values.global.labels }}
{{- toYaml . | nindent 4 }}
{{- end }}
{{- if .Values.global.annotations }}
annotations: {{ toYaml .Values.global.annotations | nindent 4}}
{{- end }}
spec:
updateStrategy:
{{- with .Values.devicePlugin.updateStrategy }}
{{- toYaml . | nindent 4 }}
{{- end }}
selector:
matchLabels:
app.kubernetes.io/component: hami-device-plugin
{{- include "hami-vgpu.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
app.kubernetes.io/component: hami-device-plugin
hami.io/webhook: ignore
{{- include "hami-vgpu.selectorLabels" . | nindent 8 }}
annotations:
{{- if ge (regexReplaceAll "[^0-9]" .Capabilities.KubeVersion.Minor "" | int) 22 }}
checksum/hami-scheduler-newversion-config: {{ include (print $.Template.BasePath "/scheduler/configmapnew.yaml") . | sha256sum }}
{{- else }}
checksum/hami-scheduler-config: {{ include (print $.Template.BasePath "/scheduler/configmap.yaml") . | sha256sum }}
{{- end }}
checksum/hami-scheduler-device-config: {{ include (print $.Template.BasePath "/scheduler/device-configmap.yaml") . | sha256sum }}
{{- if .Values.devicePlugin.podAnnotations }}
{{- toYaml .Values.devicePlugin.podAnnotations | nindent 8 }}
{{- end }}
spec:
{{- if .Values.devicePlugin.runtimeClassName }}
runtimeClassName: {{ .Values.devicePlugin.runtimeClassName }}
{{- end }}
serviceAccountName: {{ include "hami-vgpu.device-plugin" . }}
priorityClassName: system-node-critical
hostPID: true
hostNetwork: true
{{- include "hami.devicePlugin.imagePullSecrets" . | nindent 6 }}
{{- if .Values.devicePlugin.gpuOperatorToolkitReady.enabled }}
initContainers:
- name: toolkit-validation
image: {{ include "hami.devicePlugin.image" . }}
imagePullPolicy: {{ .Values.devicePlugin.image.pullPolicy }}
securityContext:
privileged: true
runAsUser: 0
command: ["sh", "-c"]
args:
- |
echo "Waiting for NVIDIA Toolkit to be ready..."
until [ -f {{ .Values.devicePlugin.gpuOperatorToolkitReady.hostPath }}/toolkit-ready ]; do
echo "Waiting for {{ .Values.devicePlugin.gpuOperatorToolkitReady.hostPath }}/toolkit-ready..."
sleep 5
done
echo "NVIDIA Toolkit is ready!"
volumeMounts:
- name: nvidia-validations
mountPath: {{ .Values.devicePlugin.gpuOperatorToolkitReady.hostPath | quote }}
mountPropagation: HostToContainer
readOnly: true
{{- end }}
containers:
- name: device-plugin
image: {{ include "hami.devicePlugin.image" . }}
imagePullPolicy: {{ .Values.devicePlugin.image.pullPolicy }}
lifecycle:
postStart:
exec:
command: ["/bin/sh","-c", {{ printf "/k8s-vgpu/bin/vgpu-init.sh %s/vgpu/" .Values.global.gpuHookPath | quote }}]
command:
- nvidia-device-plugin
- --config-file=/device-config.yaml
- --mig-strategy={{ .Values.devicePlugin.migStrategy }}
- --disable-core-limit={{ .Values.devicePlugin.disablecorelimit }}
{{- range .Values.devicePlugin.extraArgs }}
- {{ . }}
{{- end }}
env:
- name: NODE_NAME
valueFrom:
fieldRef:
fieldPath: spec.nodeName
- name: NVIDIA_MIG_MONITOR_DEVICES
value: all
- name: DEVICE_LIST_STRATEGY
value: {{ .Values.devicePlugin.deviceListStrategy }}
- name: HOOK_PATH
value: {{ .Values.global.gpuHookPath }}
{{- if typeIs "bool" .Values.devicePlugin.passDeviceSpecsEnabled }}
- name: PASS_DEVICE_SPECS
value: {{ .Values.devicePlugin.passDeviceSpecsEnabled | quote }}
{{- end }}
{{- if typeIs "string" .Values.devicePlugin.nvidiaDriverRoot }}
- name: NVIDIA_DRIVER_ROOT
value: {{ .Values.devicePlugin.nvidiaDriverRoot }}
{{- end }}
{{- if typeIs "string" .Values.devicePlugin.nvidiaHookPath }}
- name: NVIDIA_CDI_HOOK_PATH
value: {{ .Values.devicePlugin.nvidiaHookPath }}
{{- end }}
{{- if typeIs "bool" .Values.devicePlugin.gdrcopyEnabled }}
- name: GDRCOPY_ENABLED
value: {{ .Values.devicePlugin.gdrcopyEnabled | quote }}
{{- end }}
{{- if typeIs "bool" .Values.devicePlugin.gdsEnabled }}
- name: GDS_ENABLED
value: {{ .Values.devicePlugin.gdsEnabled | quote }}
{{- end }}
{{- if typeIs "bool" .Values.devicePlugin.mofedEnabled }}
- name: MOFED_ENABLED
value: {{ .Values.devicePlugin.mofedEnabled | quote }}
{{- end }}
{{- if eq (.Values.scheduler.defaultSchedulerPolicy.gpuSchedulerPolicy | default "spread") "topology-aware" }}
- name: ENABLE_TOPOLOGY_SCORE
value: "true"
{{- end }}
{{- with .Values.devicePlugin.extraEnvs }}
{{- . | toYaml | nindent 12 }}
{{- end }}
securityContext:
privileged: true
allowPrivilegeEscalation: true
capabilities:
drop: ["ALL"]
add: ["SYS_ADMIN"]
resources:
{{- toYaml .Values.devicePlugin.resources | nindent 12 }}
volumeMounts:
- name: device-plugin
mountPath: /var/lib/kubelet/device-plugins
- name: lib
mountPath: {{ printf "%s%s" .Values.global.gpuHookPath "/vgpu" }}
- name: usrbin
mountPath: /usrbin
- name: deviceconfig
mountPath: /config
- name: hosttmp
mountPath: /tmp
- name: device-config
mountPath: /device-config.yaml
subPath: device-config.yaml
- name: cdi-root
mountPath: /var/run/cdi
{{- if typeIs "string" .Values.devicePlugin.nvidiaDriverRoot }}
# We always mount the driver root at /driver-root in the container.
# This is required for CDI detection to work correctly.
- name: driver-root
mountPath: /driver-root
readOnly: true
{{- end }}
- name: vgpu-monitor
image: {{ include "hami.devicePlugin.monitor.image" . }}
imagePullPolicy: {{ .Values.devicePlugin.monitor.image.pullPolicy }}
command:
- "vGPUmonitor"
{{- range .Values.devicePlugin.monitor.extraArgs }}
- {{ . }}
{{- end }}
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
add: ["SYS_ADMIN"]
env:
- name: NODE_NAME
valueFrom:
fieldRef:
fieldPath: spec.nodeName
- name: NVIDIA_VISIBLE_DEVICES
value: "all"
- name: NVIDIA_MIG_MONITOR_DEVICES
value: "all"
- name: HOOK_PATH
value: "{{ .Values.global.gpuHookPath }}/vgpu"
- name: HAMI_RESYNC_INTERVAL
value: {{ .Values.devicePlugin.monitor.resyncInterval | default "5m" | quote }}
{{- with .Values.devicePlugin.monitor.extraEnvs }}
{{- . | toYaml | nindent 12 }}
{{- end }}
resources:
{{- toYaml .Values.devicePlugin.monitor.resources | nindent 12 }}
volumeMounts:
- name: ctrs
mountPath: {{ .Values.devicePlugin.monitor.ctrPath }}
- name: dockers
mountPath: /run/docker
- name: containerds
mountPath: /run/containerd
- name: sysinfo
mountPath: /sysinfo
- name: hostvar
mountPath: /hostvar
- name: hosttmp
mountPath: /tmp
volumes:
- name: ctrs
hostPath:
path: {{ .Values.devicePlugin.monitor.ctrPath }}
- name: hosttmp
hostPath:
path: /tmp
- name: dockers
hostPath:
path: /run/docker
- name: containerds
hostPath:
path: /run/containerd
- name: device-plugin
hostPath:
path: {{ .Values.devicePlugin.pluginPath }}
- name: lib
hostPath:
path: {{ .Values.devicePlugin.libPath }}
{{- if .Values.devicePlugin.gpuOperatorToolkitReady.enabled }}
- name: nvidia-validations
hostPath:
path: {{ .Values.devicePlugin.gpuOperatorToolkitReady.hostPath }}
type: DirectoryOrCreate
{{- end }}
{{- if typeIs "string" .Values.devicePlugin.nvidiaDriverRoot }}
- name: driver-root
hostPath:
path: {{ .Values.devicePlugin.nvidiaDriverRoot }}
type: Directory
{{- end }}
- name: cdi-root
hostPath:
path: /var/run/cdi
type: DirectoryOrCreate
- name: usrbin
hostPath:
path: /usr/bin
- name: sysinfo
hostPath:
path: /sys
- name: hostvar
hostPath:
path: /var
- name: deviceconfig
configMap:
name: {{ .Values.devicePlugin.nodeConfiguration.externalConfigName | default (include "hami-vgpu.device-plugin" .) }}
- name: device-config
configMap:
name: {{ include "hami-vgpu.scheduler" . }}-device
{{- if .Values.devicePlugin.nvidiaNodeSelector }}
nodeSelector: {{ toYaml .Values.devicePlugin.nvidiaNodeSelector | nindent 8 }}
{{- end }}
{{- if .Values.devicePlugin.tolerations }}
tolerations: {{ toYaml .Values.devicePlugin.tolerations | nindent 8 }}
{{- end }}
{{- end }}

View file

@ -0,0 +1,28 @@
{{- if .Values.devicePlugin.enabled -}}
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: {{ include "hami-vgpu.device-plugin" . }}-monitor
rules:
- apiGroups:
- ""
resources:
- pods
verbs:
- get
- create
- watch
- list
- update
- patch
- apiGroups:
- ""
resources:
- nodes
verbs:
- get
- update
- list
- patch
{{- end -}}

View file

@ -0,0 +1,17 @@
{{- if .Values.devicePlugin.enabled -}}
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: {{ include "hami-vgpu.device-plugin" . }}
labels:
app.kubernetes.io/component: "hami-device-plugin"
{{- include "hami-vgpu.labels" . | nindent 4 }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: {{ include "hami-vgpu.device-plugin" . }}-monitor
subjects:
- kind: ServiceAccount
name: {{ include "hami-vgpu.device-plugin" . }}
namespace: {{ include "hami-vgpu.namespace" . }}
{{- end -}}

View file

@ -0,0 +1,29 @@
{{- if .Values.devicePlugin.enabled -}}
apiVersion: v1
kind: Service
metadata:
name: {{ include "hami-vgpu.device-plugin" . }}-monitor
namespace: {{ include "hami-vgpu.namespace" . }}
labels:
app.kubernetes.io/component: hami-device-plugin
{{- include "hami-vgpu.labels" . | nindent 4 }}
{{- with .Values.devicePlugin.service.labels }}
{{- toYaml . | nindent 4 }}
{{- end }}
{{- if .Values.devicePlugin.service.annotations }} # Use devicePlugin instead of scheduler
annotations: {{ toYaml .Values.devicePlugin.service.annotations | nindent 4 }}
{{- end }}
spec:
type: {{ .Values.devicePlugin.service.type | default "NodePort" }} # Default type is NodePort
ports:
- name: monitorport
port: {{ .Values.devicePlugin.service.httpPort | default 31992 }} # Default HTTP port is 31992
targetPort: 9394
{{- if eq (.Values.devicePlugin.service.type | default "NodePort") "NodePort" }} # If type is NodePort, set nodePort
nodePort: {{ .Values.devicePlugin.service.httpPort | default 31992 }}
{{- end }}
protocol: TCP
selector:
app.kubernetes.io/component: hami-device-plugin
{{- include "hami-vgpu.selectorLabels" . | nindent 4 }}
{{- end -}}

View file

@ -0,0 +1,10 @@
{{- if .Values.devicePlugin.enabled -}}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "hami-vgpu.device-plugin" . }}
namespace: {{ include "hami-vgpu.namespace" . }}
labels:
app.kubernetes.io/component: "hami-device-plugin"
{{- include "hami-vgpu.labels" . | nindent 4 }}
{{- end -}}

View file

@ -0,0 +1,9 @@
{{- if and .Values.devicePlugin.enabled .Values.devicePlugin.createRuntimeClass .Values.devicePlugin.runtimeClassName -}}
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
name: {{ .Values.devicePlugin.runtimeClassName }}
annotations:
helm.sh/hook: pre-install,pre-upgrade
handler: nvidia
{{- end -}}

View file

@ -0,0 +1,31 @@
{{- if .Values.scheduler.admissionWebhook.enabled -}}
{{- if .Values.scheduler.certManager.enabled }}
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: {{ include "hami-vgpu.scheduler" . }}-serving-cert
namespace: {{ include "hami-vgpu.namespace" . }}
labels:
app.kubernetes.io/component: hami-scheduler
{{- include "hami-vgpu.labels" . | nindent 4 }}
spec:
dnsNames:
- {{ include "hami-vgpu.scheduler" . }}.{{ include "hami-vgpu.namespace" . }}.svc
- {{ include "hami-vgpu.scheduler" . }}.{{ include "hami-vgpu.namespace" . }}.svc.cluster.local
issuerRef:
kind: Issuer
name: {{ include "hami-vgpu.scheduler" . }}-selfsigned-issuer
secretName: {{ include "hami-vgpu.scheduler.tls" . }}
---
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
name: {{ include "hami-vgpu.scheduler" . }}-selfsigned-issuer
namespace: {{ include "hami-vgpu.namespace" . }}
labels:
app.kubernetes.io/component: hami-scheduler
{{- include "hami-vgpu.labels" . | nindent 4 }}
spec:
selfSigned: {}
{{- end }}
{{- end }}

View file

@ -0,0 +1,34 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: {{ include "hami-vgpu.scheduler" . }}
labels:
app.kubernetes.io/component: "hami-scheduler"
{{- include "hami-vgpu.labels" . | nindent 4 }}
rules:
- apiGroups: [""]
resources: ["pods", "configmaps"]
verbs: ["get", "list", "watch", "patch"]
- apiGroups: [""]
resources: ["pods/binding"]
verbs: ["create"]
- apiGroups: [""]
resources: ["nodes"]
verbs: ["get", "list", "patch", "watch"]
- apiGroups: [""]
resources: ["events"]
verbs: ["create", "get", "list"]
- apiGroups: [""]
resources: ["resourcequotas"]
verbs: ["get", "list", "watch"]
---
{{- if .Values.mockDevicePlugin.enabled }}
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: {{ include "hami-vgpu.mock-device-plugin" . }}
rules:
- apiGroups: [""]
resources: ["nodes"]
verbs: ["get", "update", "list", "patch"]
{{- end -}}

View file

@ -0,0 +1,47 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: {{ include "hami-vgpu.scheduler" . }}-kube
labels:
app.kubernetes.io/component: "hami-scheduler"
{{- include "hami-vgpu.labels" . | nindent 4 }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: system:kube-scheduler
subjects:
- kind: ServiceAccount
name: {{ include "hami-vgpu.scheduler" . }}
namespace: {{ include "hami-vgpu.namespace" . }}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: {{ include "hami-vgpu.scheduler" . }}-volume
labels:
app.kubernetes.io/component: "hami-scheduler"
{{- include "hami-vgpu.labels" . | nindent 4 }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: system:volume-scheduler
subjects:
- kind: ServiceAccount
name: {{ include "hami-vgpu.scheduler" . }}
namespace: {{ include "hami-vgpu.namespace" . }}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: {{ include "hami-vgpu.scheduler" . }}
labels:
app.kubernetes.io/component: "hami-scheduler"
{{- include "hami-vgpu.labels" . | nindent 4 }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: {{ include "hami-vgpu.scheduler" . }}
subjects:
- kind: ServiceAccount
name: {{ include "hami-vgpu.scheduler" . }}
namespace: {{ include "hami-vgpu.namespace" . }}

View file

@ -0,0 +1,142 @@
{{- if .Values.scheduler.kubeScheduler.enabled -}}
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "hami-vgpu.scheduler" . }}
namespace: {{ include "hami-vgpu.namespace" . }}
labels:
app.kubernetes.io/component: hami-scheduler
{{- include "hami-vgpu.labels" . | nindent 4 }}
data:
config.json: |
{
"kind": "Policy",
"apiVersion": "v1",
"extenders": [
{
{{- if .Values.scheduler.admissionWebhook.enabled }}
"urlPrefix": "https://127.0.0.1:443",
"enableHttps": true,
"tlsConfig": {
"insecure": true
},
{{- else }}
"urlPrefix": "http://127.0.0.1:80",
"enableHttps": false,
{{- end }}
"filterVerb": "filter",
"bindVerb": "bind",
"weight": 1,
"nodeCacheCapable": true,
"httpTimeout": 30000000000,
"managedResources": [
{{- range .Values.devices.amd.customresources }}
{
"name": "{{ . }}",
"ignoredByScheduler": true
},
{{- end }}
{{- if .Values.devices.ascend.enabled }}
{{- range .Values.devices.ascend.customresources }}
{
"name": "{{ . }}",
"ignoredByScheduler": true
},
{{- end }}
{{- end }}
{{- if .Values.devices.mthreads.enabled }}
{{- range .Values.devices.mthreads.customresources }}
{
"name": "{{ . }}",
"ignoredByScheduler": true
},
{{- end }}
{{- end }}
{{- if .Values.devices.enflame.enabled }}
{{- range .Values.devices.enflame.customresources }}
{
"name": "{{ . }}",
"ignoredByScheduler": true
},
{{- end }}
{{- end }}
{{- if .Values.devices.kunlun.enabled }}
{{- range .Values.devices.kunlun.customresources }}
{
"name": "{{ . }}",
"ignoredByScheduler": true
},
{{- end }}
{{- end }}
{{- range .Values.devices.awsneuron.customresources }}
{
"name": "{{ . }}",
"ignoredByScheduler": true
},
{{- end }}
{{- if .Values.devices.iluvatar.enabled }}
{{- range .Values.devices.iluvatar.customresources }}
{
"name": "{{ . }}",
"ignoredByScheduler": true
},
{{- end }}
{{- end }}
{
"name": "{{ .Values.resourceName }}",
"ignoredByScheduler": true
},
{
"name": "{{ .Values.resourceMem }}",
"ignoredByScheduler": true
},
{
"name": "{{ .Values.resourceCores }}",
"ignoredByScheduler": true
},
{
"name": "{{ .Values.resourceMemPercentage }}",
"ignoredByScheduler": true
},
{
"name": "{{ .Values.resourcePriority }}",
"ignoredByScheduler": true
},
{
"name": "{{ .Values.mluResourceName }}",
"ignoredByScheduler": true
},
{
"name": "{{ .Values.dcuResourceName }}",
"ignoredByScheduler": true
},
{
"name": "{{ .Values.dcuResourceMem }}",
"ignoredByScheduler": true
},
{
"name": "{{ .Values.dcuResourceCores }}",
"ignoredByScheduler": true
},
{
"name": "metax-tech.com/gpu",
"ignoredByScheduler": true
},
{
"name": "{{ .Values.metaxResourceName }}",
"ignoredByScheduler": true
},
{
"name": "{{ .Values.metaxResourceCore }}",
"ignoredByScheduler": true
},
{
"name": "{{ .Values.metaxResourceMem }}",
"ignoredByScheduler": true
}
],
"ignoreable": false
}
]
}
{{- end }}

View file

@ -0,0 +1,102 @@
{{- if .Values.scheduler.kubeScheduler.enabled -}}
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "hami-vgpu.scheduler" . }}-newversion
namespace: {{ include "hami-vgpu.namespace" . }}
labels:
app.kubernetes.io/component: hami-scheduler
{{- include "hami-vgpu.labels" . | nindent 4 }}
data:
config.yaml: |
{{- if gt (regexReplaceAll "[^0-9]" .Capabilities.KubeVersion.Minor "" | int) 25}}
apiVersion: kubescheduler.config.k8s.io/v1
{{- else }}
apiVersion: kubescheduler.config.k8s.io/v1beta2
{{- end }}
kind: KubeSchedulerConfiguration
leaderElection:
leaderElect: false
profiles:
- schedulerName: {{ .Values.schedulerName }}
extenders:
{{- if .Values.scheduler.admissionWebhook.enabled }}
- urlPrefix: "https://127.0.0.1:443"
enableHTTPS: true
tlsConfig:
insecure: true
{{- else }}
- urlPrefix: "http://127.0.0.1:80"
enableHTTPS: false
{{- end }}
filterVerb: filter
bindVerb: bind
nodeCacheCapable: true
weight: 1
httpTimeout: 30s
managedResources:
- name: {{ .Values.resourceName }}
ignoredByScheduler: true
- name: {{ .Values.resourceMem }}
ignoredByScheduler: true
- name: {{ .Values.resourceCores }}
ignoredByScheduler: true
- name: {{ .Values.resourceMemPercentage }}
ignoredByScheduler: true
- name: {{ .Values.resourcePriority }}
ignoredByScheduler: true
- name: {{ .Values.mluResourceName }}
ignoredByScheduler: true
- name: {{ .Values.dcuResourceName }}
ignoredByScheduler: true
- name: {{ .Values.dcuResourceMem }}
ignoredByScheduler: true
- name: {{ .Values.dcuResourceCores }}
ignoredByScheduler: true
- name: "metax-tech.com/gpu"
ignoredByScheduler: true
- name: {{ .Values.metaxResourceName }}
ignoredByScheduler: true
- name: {{ .Values.metaxResourceCore }}
ignoredByScheduler: true
- name: {{ .Values.metaxResourceMem }}
ignoredByScheduler: true
{{- if .Values.devices.ascend.enabled }}
{{- range .Values.devices.ascend.customresources }}
- name: {{ . }}
ignoredByScheduler: true
{{- end }}
{{- end }}
{{- if .Values.devices.mthreads.enabled }}
{{- range .Values.devices.mthreads.customresources }}
- name: {{ . }}
ignoredByScheduler: true
{{- end }}
{{- end }}
{{- if .Values.devices.enflame.enabled }}
{{- range .Values.devices.enflame.customresources }}
- name: {{ . }}
ignoredByScheduler: true
{{- end }}
{{- end }}
{{- if .Values.devices.kunlun.enabled }}
{{- range .Values.devices.kunlun.customresources }}
- name: {{ . }}
ignoredByScheduler: true
{{- end }}
{{- end }}
{{- range .Values.devices.awsneuron.customresources }}
- name: {{ . }}
ignoredByScheduler: true
{{- end }}
{{- if .Values.devices.iluvatar.enabled }}
{{- range .Values.devices.iluvatar.customresources }}
- name: {{ . }}
ignoredByScheduler: true
{{- end }}
{{- end }}
{{- range .Values.devices.amd.customresources }}
- name: {{ . }}
ignoredByScheduler: true
{{- end }}
{{- end }}

View file

@ -0,0 +1,225 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "hami-vgpu.scheduler" . }}
namespace: {{ include "hami-vgpu.namespace" . }}
labels:
app.kubernetes.io/component: hami-scheduler
{{- include "hami-vgpu.labels" . | nindent 4 }}
{{- with .Values.global.labels }}
{{- toYaml . | nindent 4 }}
{{- end }}
{{- if .Values.global.annotations }}
annotations: {{ toYaml .Values.global.annotations | nindent 4}}
{{- end }}
spec:
{{- if .Values.scheduler.leaderElect }}
replicas: {{ .Values.scheduler.replicas }}
{{- else }}
replicas: 1
{{- end }}
selector:
matchLabels:
app.kubernetes.io/component: hami-scheduler
{{- include "hami-vgpu.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
app.kubernetes.io/component: hami-scheduler
{{- include "hami-vgpu.selectorLabels" . | nindent 8 }}
hami.io/webhook: ignore
annotations:
{{- if ge (regexReplaceAll "[^0-9]" .Capabilities.KubeVersion.Minor "" | int) 22 }}
checksum/hami-scheduler-newversion-config: {{ include (print $.Template.BasePath "/scheduler/configmapnew.yaml") . | sha256sum }}
{{- else }}
checksum/hami-scheduler-config: {{ include (print $.Template.BasePath "/scheduler/configmap.yaml") . | sha256sum }}
{{- end }}
checksum/hami-scheduler-device-config: {{ include (print $.Template.BasePath "/scheduler/device-configmap.yaml") . | sha256sum }}
{{- if .Values.scheduler.podAnnotations }}
{{- toYaml .Values.scheduler.podAnnotations | nindent 8 }}
{{- end }}
spec:
serviceAccountName: {{ include "hami-vgpu.scheduler" . }}
priorityClassName: system-node-critical
{{- include "hami.scheduler.extender.imagePullSecrets" . | nindent 6 }}
containers:
{{- if .Values.scheduler.kubeScheduler.enabled }}
- name: kube-scheduler
image: {{ include "hami.scheduler.kubeScheduler.image" . }}
imagePullPolicy: {{ .Values.scheduler.kubeScheduler.image.pullPolicy }}
command:
- kube-scheduler
{{- if ge (regexReplaceAll "[^0-9]" .Capabilities.KubeVersion.Minor "" | int) 22 }}
{{- range .Values.scheduler.kubeScheduler.extraNewArgs }}
- {{ . }}
{{- end }}
{{- else }}
- --scheduler-name={{ .Values.schedulerName }}
{{- range .Values.scheduler.kubeScheduler.extraArgs }}
- {{ . }}
{{- end }}
{{- end }}
- --leader-elect={{ .Values.scheduler.leaderElect }}
- --leader-elect-resource-name={{ .Values.schedulerName }}
- --leader-elect-resource-namespace={{ include "hami-vgpu.namespace" . }}
resources:
{{- toYaml .Values.scheduler.kubeScheduler.resources | nindent 12 }}
volumeMounts:
- name: scheduler-config
mountPath: /config
{{- end }}
{{- if .Values.scheduler.livenessProbe }}
livenessProbe:
failureThreshold: 8
httpGet:
path: /healthz
port: 10259
scheme: HTTPS
initialDelaySeconds: 10
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 15
{{- end }}
- name: vgpu-scheduler-extender
image: {{ include "hami.scheduler.extender.image" . }}
imagePullPolicy: {{ .Values.scheduler.extender.image.pullPolicy }}
env:
{{- if .Values.scheduler.nodeLockExpire }}
- name: HAMI_NODELOCK_EXPIRE
value: "{{ .Values.scheduler.nodeLockExpire }}"
{{- end }}
{{- if .Values.global.managedNodeSelectorEnable }}
{{- range $key, $value := .Values.global.managedNodeSelector }}
- name: NODE_SELECTOR_{{ $key | upper | replace "-" "_" }}
value: "{{ $value }}"
{{- end }}
{{- end }}
command:
- scheduler
{{- if .Values.scheduler.admissionWebhook.enabled }}
- --http_bind=0.0.0.0:443
- --cert_file=/tls/tls.crt
- --key_file=/tls/tls.key
{{- else }}
- --http_bind=0.0.0.0:80
{{- end }}
- --scheduler-name={{ .Values.schedulerName }}
- --metrics-bind-address={{ .Values.scheduler.metricsBindAddress }}
- --node-scheduler-policy={{ .Values.scheduler.defaultSchedulerPolicy.nodeSchedulerPolicy }}
- --gpu-scheduler-policy={{ .Values.scheduler.defaultSchedulerPolicy.gpuSchedulerPolicy }}
- --force-overwrite-default-scheduler={{ .Values.scheduler.forceOverwriteDefaultScheduler}}
- --device-config-file=/device-config.yaml
- --leader-elect={{ .Values.scheduler.leaderElect }}
- --leader-elect-resource-name={{ .Values.schedulerName }}
- --leader-elect-resource-namespace={{ include "hami-vgpu.namespace" . }}
{{- if .Values.devices.ascend.enabled }}
- --enable-ascend=true
{{- end }}
{{- if .Values.devices.iluvatar.enabled }}
- --enable-iluvatar=true
{{- end }}
{{- if .Values.scheduler.nodeLabelSelector }}
- --node-label-selector={{- $first := true -}}
{{- range $key, $value := .Values.scheduler.nodeLabelSelector -}}
{{- if not $first }},{{ end -}}
{{- $key }}={{ $value -}}
{{- $first = false -}}
{{- end -}}
{{- end }}
{{- range .Values.scheduler.extender.extraArgs }}
- {{ . }}
{{- end }}
ports:
{{- if .Values.scheduler.admissionWebhook.enabled }}
- name: https
containerPort: 443
protocol: TCP
{{- else }}
- name: http
containerPort: 80
protocol: TCP
{{- end }}
- name: metrics
containerPort: {{ last (splitList ":" .Values.scheduler.metricsBindAddress) | int }}
protocol: TCP
resources:
{{- toYaml .Values.scheduler.extender.resources | nindent 12 }}
volumeMounts:
- name: device-config
mountPath: /device-config.yaml
subPath: device-config.yaml
{{- if .Values.scheduler.admissionWebhook.enabled }}
- name: tls-config
mountPath: /tls
{{- end }}
{{- if .Values.scheduler.livenessProbe }}
livenessProbe:
httpGet:
path: /healthz
{{- if .Values.scheduler.admissionWebhook.enabled }}
port: https
scheme: HTTPS
{{- else }}
port: http
scheme: HTTP
{{- end }}
initialDelaySeconds: 10
periodSeconds: 10
failureThreshold: 3
timeoutSeconds: 5
{{- end }}
{{- if .Values.scheduler.leaderElect }}
readinessProbe:
httpGet:
path: /readyz
{{- if .Values.scheduler.admissionWebhook.enabled }}
port: https
scheme: HTTPS
{{- else }}
port: http
scheme: HTTP
{{- end }}
initialDelaySeconds: 10
periodSeconds: 10
failureThreshold: 3
timeoutSeconds: 5
{{- end }}
{{- if .Values.scheduler.leaderElect }}
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app.kubernetes.io/component
operator: In
values:
- hami-scheduler
topologyKey: "kubernetes.io/hostname"
{{- end }}
volumes:
{{- if .Values.scheduler.admissionWebhook.enabled }}
- name: tls-config
secret:
secretName: {{ template "hami-vgpu.scheduler.tls" . }}
{{- end }}
{{- if .Values.scheduler.kubeScheduler.enabled }}
- name: scheduler-config
configMap:
{{- if ge (regexReplaceAll "[^0-9]" .Capabilities.KubeVersion.Minor "" | int) 22 }}
name: {{ template "hami-vgpu.scheduler" . }}-newversion
{{- else }}
name: {{ template "hami-vgpu.scheduler" . }}
{{- end }}
{{- end }}
- name: device-config
configMap:
name: {{ include "hami-vgpu.scheduler" . }}-device
{{- if .Values.scheduler.nodeSelector }}
nodeSelector: {{ toYaml .Values.scheduler.nodeSelector | nindent 8 }}
{{- end }}
{{- if .Values.scheduler.tolerations }}
tolerations: {{ toYaml .Values.scheduler.tolerations | nindent 8 }}
{{- end }}
{{- if .Values.scheduler.nodeName }}
nodeName: {{ .Values.scheduler.nodeName }}
{{- end }}

View file

@ -0,0 +1,408 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "hami-vgpu.scheduler" . }}-device
namespace: {{ include "hami-vgpu.namespace" . }}
labels:
app.kubernetes.io/component: hami-scheduler
{{- include "hami-vgpu.labels" . | nindent 4 }}
data:
device-config.yaml: |-
{{- if .Files.Glob "files/device-config.yaml" }}
{{- .Files.Get "files/device-config.yaml" | nindent 4}}
{{- else }}
nvidia:
resourceCountName: {{ .Values.resourceName }}
resourceMemoryName: {{ .Values.resourceMem }}
resourceMemoryPercentageName: {{ .Values.resourceMemPercentage }}
resourceCoreName: {{ .Values.resourceCores }}
resourcePriorityName: {{ .Values.resourcePriority }}
overwriteEnv: false
defaultMemory: 0
defaultCores: 0
defaultGPUNum: 1
memoryFactor: 1
deviceSplitCount: {{ .Values.devicePlugin.deviceSplitCount }}
deviceMemoryScaling: {{ .Values.devicePlugin.deviceMemoryScaling }}
deviceCoreScaling: {{ .Values.devicePlugin.deviceCoreScaling }}
gpuCorePolicy: {{ .Values.devices.nvidia.gpuCorePolicy }}
libCudaLogLevel: {{ .Values.devices.nvidia.libCudaLogLevel }}
runtimeClassName: "{{ .Values.devicePlugin.runtimeClassName }}"
knownMigGeometries:
- models: [ "A30" ]
allowedGeometries:
-
- name: 1g.6gb
core: 25
memory: 6144
count: 4
-
- name: 2g.12gb
core: 50
memory: 12288
count: 2
-
- name: 4g.24gb
core: 100
memory: 24576
count: 1
- models: [ "A100-SXM4-40GB", "A100-40GB-PCIe", "A100-PCIE-40GB"]
allowedGeometries:
-
- name: 1g.5gb
core: 14
memory: 5120
count: 7
-
- name: 1g.5gb
core: 14
memory: 5120
count: 1
- name: 2g.10gb
core: 28
memory: 10240
count: 3
-
- name: 3g.20gb
core: 42
memory: 20480
count: 2
-
- name: 7g.40gb
core: 100
memory: 40960
count: 1
- models: [ "A100-SXM4-80GB", "A100-80GB-PCIe", "A100-PCIE-80GB"]
allowedGeometries:
-
- name: 1g.10gb
core: 14
memory: 10240
count: 7
-
- name: 1g.10gb
core: 14
memory: 10240
count: 1
- name: 2g.20gb
core: 28
memory: 20480
count: 3
-
- name: 3g.40gb
core: 42
memory: 40960
count: 2
-
- name: 7g.79gb
core: 100
memory: 80896
count: 1
- models: [ "H100-PCIE-80GB", "H100-SXM5-80GB"]
allowedGeometries:
-
- name: 1g.10gb
core: 14
memory: 10240
count: 7
-
- name: 1g.10gb
core: 14
memory: 10240
count: 1
- name: 2g.20gb
core: 28
memory: 20480
count: 3
-
- name: 3g.40gb
core: 42
memory: 40960
count: 2
-
- name: 7g.80gb
core: 100
memory: 81920
count: 1
- models: [ "H100-PCIE-94GB", "H100-SXM5-94GB"]
allowedGeometries:
-
- name: 1g.12gb
core: 14
memory: 12288
count: 7
-
- name: 1g.12gb
core: 14
memory: 12288
count: 1
- name: 2g.24gb
core: 28
memory: 24576
count: 3
-
- name: 3g.47gb
core: 42
memory: 48128
count: 2
-
- name: 7g.94gb
core: 100
memory: 96256
count: 1
- models: [ "H20", "H100 on GH200"]
allowedGeometries:
-
- name: 1g.12gb
core: 14
memory: 12288
count: 7
-
- name: 1g.12gb
core: 14
memory: 12288
count: 1
- name: 2g.24gb
core: 28
memory: 24576
count: 3
-
- name: 3g.48gb
core: 42
memory: 49152
count: 2
-
- name: 7g.96gb
core: 100
memory: 98304
count: 1
- models: [ "H200 NVL", "H200-SXM5"]
allowedGeometries:
-
- name: 1g.18gb
core: 14
memory: 18432
count: 7
-
- name: 1g.18gb
core: 14
memory: 18432
count: 1
- name: 2g.35gb
core: 28
memory: 35840
count: 3
-
- name: 3g.71gb
core: 42
memory: 72704
count: 2
-
- name: 7g.141gb
core: 100
memory: 144384
count: 1
- models: [ "B200" ]
allowedGeometries:
-
- name: 1g.23gb
core: 14
memory: 23552
count: 7
-
- name: 1g.23gb
core: 14
memory: 23552
count: 1
- name: 2g.45gb
core: 28
memory: 46080
count: 3
-
- name: 3g.90gb
core: 42
memory: 92160
count: 2
-
- name: 7g.180gb
core: 100
memory: 184320
count: 1
cambricon:
resourceCountName: {{ .Values.mluResourceName }}
resourceMemoryName: {{ .Values.mluResourceMem }}
resourceCoreName: {{ .Values.mluResourceCores }}
hygon:
resourceCountName: {{ .Values.dcuResourceName }}
resourceMemoryName: {{ .Values.dcuResourceMem }}
resourceCoreName: {{ .Values.dcuResourceCores }}
memoryFactor: 1
metax:
resourceCountName: "metax-tech.com/gpu"
resourceVCountName: {{ .Values.metaxResourceName }}
resourceVMemoryName: {{ .Values.metaxResourceMem }}
resourceVCoreName: {{ .Values.metaxResourceCore }}
sgpuTopologyAware: {{ .Values.metaxsGPUTopologyAware }}
enflame:
resourceNameGCU: "enflame.com/gcu"
resourceNameVGCU: {{ .Values.enflameResourceNameVGCU }}
resourceNameVGCUPercentage: {{ .Values.enflameResourceNameVGCUPercentage }}
mthreads:
resourceCountName: "mthreads.com/vgpu"
resourceMemoryName: "mthreads.com/sgpu-memory"
resourceCoreName: "mthreads.com/sgpu-core"
iluvatars:
- chipName: MR-V100
commonWord: MR-V100
resourceCountName: iluvatar.ai/MR-V100-vgpu
resourceMemoryName: iluvatar.ai/MR-V100.vMem
resourceCoreName: iluvatar.ai/MR-V100.vCore
- chipName: MR-V50
commonWord: MR-V50
resourceCountName: iluvatar.ai/MR-V50-vgpu
resourceMemoryName: iluvatar.ai/MR-V50.vMem
resourceCoreName: iluvatar.ai/MR-V50.vCore
- chipName: BI-V150
commonWord: BI-V150
resourceCountName: iluvatar.ai/BI-V150-vgpu
resourceMemoryName: iluvatar.ai/BI-V150.vMem
resourceCoreName: iluvatar.ai/BI-V150.vCore
- chipName: BI-V100
commonWord: BI-V100
resourceCountName: iluvatar.ai/BI-V100-vgpu
resourceMemoryName: iluvatar.ai/BI-V100.vMem
resourceCoreName: iluvatar.ai/BI-V100.vCore
kunlun:
resourceCountName: {{ .Values.kunlunResourceName }}
resourceVCountName: {{ .Values.kunlunResourceVCountName }}
resourceVMemoryName: {{ .Values.kunlunResourceVMemoryName }}
awsneuron:
resourceCountName: "aws.amazon.com/neuron"
resourceCoreName: "aws.amazon.com/neuroncore"
amd:
resourceCountName: "amd.com/gpu"
vnpus:
- chipName: 910A
commonWord: Ascend910A
resourceName: huawei.com/Ascend910A
resourceMemoryName: huawei.com/Ascend910A-memory
memoryAllocatable: 32768
memoryCapacity: 32768
memoryFactor: 1
aiCore: 30
templates:
- name: vir02
memory: 2184
aiCore: 2
- name: vir04
memory: 4369
aiCore: 4
- name: vir08
memory: 8738
aiCore: 8
- name: vir16
memory: 17476
aiCore: 16
- chipName: 910B2
commonWord: Ascend910B2
resourceName: huawei.com/Ascend910B2
resourceMemoryName: huawei.com/Ascend910B2-memory
memoryAllocatable: 65536
memoryCapacity: 65536
memoryFactor: 1
aiCore: 24
aiCPU: 6
templates:
- name: vir03_1c_8g
memory: 8192
aiCore: 3
aiCPU: 1
- name: vir06_1c_16g
memory: 16384
aiCore: 6
aiCPU: 1
- name: vir12_3c_32g
memory: 32768
aiCore: 12
aiCPU: 3
- chipName: 910B3
commonWord: Ascend910B3
resourceName: huawei.com/Ascend910B3
resourceMemoryName: huawei.com/Ascend910B3-memory
memoryAllocatable: 65536
memoryCapacity: 65536
memoryFactor: 1
aiCore: 20
aiCPU: 7
templates:
- name: vir05_1c_16g
memory: 16384
aiCore: 5
aiCPU: 1
- name: vir10_3c_32g
memory: 32768
aiCore: 10
aiCPU: 3
- chipName: 910B4-1
commonWord: Ascend910B4-1
resourceName: huawei.com/Ascend910B4-1
resourceMemoryName: huawei.com/Ascend910B4-1-memory
memoryAllocatable: 65536
memoryCapacity: 65536
memoryFactor: 1
aiCore: 20
aiCPU: 7
templates:
# NOTE: Names of vnpu template for 910B4-1 are fixed by Ascend runtime and must not be changed.
# The memory is used for scheduling so the correct values must be set.
# Template vir05_1c_8g actually provides 16GB memory,
- name: vir05_1c_8g
memory: 16384
aiCore: 5
aiCPU: 1
# Template vir10_3c_16g actually provides 32GB memory
- name: vir10_3c_16g
memory: 32768
aiCore: 10
aiCPU: 3
- chipName: 910B4
commonWord: Ascend910B4
resourceName: huawei.com/Ascend910B4
resourceMemoryName: huawei.com/Ascend910B4-memory
memoryAllocatable: 32768
memoryCapacity: 32768
memoryFactor: 1
aiCore: 20
aiCPU: 7
templates:
- name: vir05_1c_8g
memory: 8192
aiCore: 5
aiCPU: 1
- name: vir10_3c_16g
memory: 16384
aiCore: 10
aiCPU: 3
- chipName: 310P3
commonWord: Ascend310P
resourceName: huawei.com/Ascend310P
resourceMemoryName: huawei.com/Ascend310P-memory
memoryAllocatable: 21527
memoryCapacity: 24576
memoryFactor: 1
aiCore: 8
aiCPU: 7
templates:
- name: vir01
memory: 3072
aiCore: 1
aiCPU: 1
- name: vir02
memory: 6144
aiCore: 2
aiCPU: 2
- name: vir04
memory: 12288
aiCore: 4
aiCPU: 4
{{ end }}

View file

@ -0,0 +1,21 @@
{{- if and .Values.scheduler.admissionWebhook.enabled (.Values.scheduler.patch.enabled) (not .Values.scheduler.certManager.enabled) -}}
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: {{ include "hami-vgpu.fullname" . }}-admission
annotations:
"helm.sh/hook": pre-install,pre-upgrade,post-install,post-upgrade
"helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
labels:
{{- include "hami-vgpu.labels" . | nindent 4 }}
app.kubernetes.io/component: admission-webhook
rules:
- apiGroups:
- admissionregistration.k8s.io
resources:
#- validatingwebhookconfigurations
- mutatingwebhookconfigurations
verbs:
- get
- update
{{- end }}

View file

@ -0,0 +1,20 @@
{{- if and .Values.scheduler.admissionWebhook.enabled (.Values.scheduler.patch.enabled) (not .Values.scheduler.certManager.enabled) -}}
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: {{ include "hami-vgpu.fullname" . }}-admission
annotations:
"helm.sh/hook": pre-install,pre-upgrade,post-install,post-upgrade
"helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
labels:
{{- include "hami-vgpu.labels" . | nindent 4 }}
app.kubernetes.io/component: admission-webhook
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: {{ include "hami-vgpu.fullname" . }}-admission
subjects:
- kind: ServiceAccount
name: {{ include "hami-vgpu.fullname" . }}-admission
namespace: {{ include "hami-vgpu.namespace" . }}
{{- end }}

View file

@ -0,0 +1,68 @@
{{- if and .Values.scheduler.admissionWebhook.enabled (.Values.scheduler.patch.enabled) (not .Values.scheduler.certManager.enabled) -}}
apiVersion: batch/v1
kind: Job
metadata:
name: {{ include "hami-vgpu.fullname" . }}-admission-create
namespace: {{ include "hami-vgpu.namespace" . }}
annotations:
"helm.sh/hook": pre-install,pre-upgrade
"helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
labels:
{{- include "hami-vgpu.labels" . | nindent 4 }}
app.kubernetes.io/component: admission-webhook
spec:
{{- if .Capabilities.APIVersions.Has "batch/v1alpha1" }}
# Alpha feature since k8s 1.12
ttlSecondsAfterFinished: 0
{{- end }}
template:
metadata:
name: {{ include "hami-vgpu.fullname" . }}-admission-create
{{- if .Values.scheduler.patch.podAnnotations }}
annotations: {{ toYaml .Values.scheduler.patch.podAnnotations | nindent 8 }}
{{- end }}
labels:
{{- include "hami-vgpu.labels" . | nindent 8 }}
app.kubernetes.io/component: admission-webhook
hami.io/webhook: ignore
spec:
{{- if .Values.scheduler.patch.priorityClassName }}
priorityClassName: {{ .Values.scheduler.patch.priorityClassName }}
{{- end }}
{{- if ge (regexReplaceAll "[^0-9]" .Capabilities.KubeVersion.Minor "" | int) 22 }}
{{- include "hami.scheduler.patch.new.imagePullSecrets" . | nindent 6 }}
{{- else }}
{{- include "hami.scheduler.patch.imagePullSecrets" . | nindent 6 }}
{{- end }}
containers:
- name: create
{{- if ge (regexReplaceAll "[^0-9]" .Capabilities.KubeVersion.Minor "" | int) 22 }}
image: {{ include "hami.scheduler.patch.new.image" . }}
imagePullPolicy: {{ .Values.scheduler.patch.imageNew.pullPolicy }}
{{- else }}
image: {{ include "hami.scheduler.patch.image" . }}
imagePullPolicy: {{ .Values.scheduler.patch.image.pullPolicy }}
{{- end }}
args:
- create
- --cert-name=tls.crt
- --key-name=tls.key
{{- if .Values.scheduler.admissionWebhook.customURL.enabled }}
- --host={{ printf "%s.%s.svc,127.0.0.1,%s" (include "hami-vgpu.scheduler" .) (include "hami-vgpu.namespace" .) .Values.scheduler.admissionWebhook.customURL.host}}
{{- else }}
- --host={{ printf "%s.%s.svc,127.0.0.1" (include "hami-vgpu.scheduler" .) (include "hami-vgpu.namespace" .) }}
{{- end }}
- --namespace={{ include "hami-vgpu.namespace" . }}
- --secret-name={{ include "hami-vgpu.scheduler.tls" . }}
restartPolicy: OnFailure
serviceAccountName: {{ include "hami-vgpu.fullname" . }}-admission
{{- if .Values.scheduler.patch.nodeSelector }}
nodeSelector: {{ toYaml .Values.scheduler.patch.nodeSelector | nindent 8 }}
{{- end }}
{{- if .Values.scheduler.patch.tolerations }}
tolerations: {{ toYaml .Values.scheduler.patch.tolerations | nindent 8 }}
{{- end }}
securityContext:
runAsNonRoot: true
runAsUser: {{ .Values.scheduler.patch.runAsUser }}
{{- end }}

View file

@ -0,0 +1,63 @@
{{- if and .Values.scheduler.admissionWebhook.enabled (.Values.scheduler.patch.enabled) (not .Values.scheduler.certManager.enabled) -}}
apiVersion: batch/v1
kind: Job
metadata:
name: {{ include "hami-vgpu.fullname" . }}-admission-patch
namespace: {{ include "hami-vgpu.namespace" . }}
annotations:
"helm.sh/hook": post-install,post-upgrade
"helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
labels:
{{- include "hami-vgpu.labels" . | nindent 4 }}
app.kubernetes.io/component: admission-webhook
spec:
{{- if .Capabilities.APIVersions.Has "batch/v1alpha1" }}
# Alpha feature since k8s 1.12
ttlSecondsAfterFinished: 0
{{- end }}
template:
metadata:
name: {{ include "hami-vgpu.fullname" . }}-admission-patch
{{- if .Values.scheduler.patch.podAnnotations }}
annotations: {{ toYaml .Values.scheduler.patch.podAnnotations | nindent 8 }}
{{- end }}
labels:
{{- include "hami-vgpu.labels" . | nindent 8 }}
app.kubernetes.io/component: admission-webhook
hami.io/webhook: ignore
spec:
{{- if .Values.scheduler.patch.priorityClassName }}
priorityClassName: {{ .Values.scheduler.patch.priorityClassName }}
{{- end }}
{{- if ge (regexReplaceAll "[^0-9]" .Capabilities.KubeVersion.Minor "" | int) 22 }}
{{- include "hami.scheduler.patch.new.imagePullSecrets" . | nindent 6 }}
{{- else }}
{{- include "hami.scheduler.patch.imagePullSecrets" . | nindent 6 }}
{{- end }}
containers:
- name: patch
{{- if ge (regexReplaceAll "[^0-9]" .Capabilities.KubeVersion.Minor "" | int) 22 }}
image: {{ include "hami.scheduler.patch.new.image" . }}
imagePullPolicy: {{ .Values.scheduler.patch.imageNew.pullPolicy }}
{{- else }}
image: {{ include "hami.scheduler.patch.image" . }}
imagePullPolicy: {{ .Values.scheduler.patch.image.pullPolicy }}
{{- end }}
args:
- patch
- --webhook-name={{ include "hami-vgpu.scheduler.webhook" . }}
- --namespace={{ include "hami-vgpu.namespace" . }}
- --patch-validating=false
- --secret-name={{ include "hami-vgpu.scheduler.tls" . }}
restartPolicy: OnFailure
serviceAccountName: {{ include "hami-vgpu.fullname" . }}-admission
{{- if .Values.scheduler.patch.nodeSelector }}
nodeSelector: {{ toYaml .Values.scheduler.patch.nodeSelector | nindent 8 }}
{{- end }}
{{- if .Values.scheduler.patch.tolerations }}
tolerations: {{ toYaml .Values.scheduler.patch.tolerations | nindent 8 }}
{{- end }}
securityContext:
runAsNonRoot: true
runAsUser: {{ .Values.scheduler.patch.runAsUser }}
{{- end }}

View file

@ -0,0 +1,21 @@
{{- if and .Values.scheduler.admissionWebhook.enabled (.Values.scheduler.patch.enabled) (not .Values.scheduler.certManager.enabled) -}}
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: {{ include "hami-vgpu.fullname" . }}-admission
namespace: {{ include "hami-vgpu.namespace" . }}
annotations:
"helm.sh/hook": pre-install,pre-upgrade,post-install,post-upgrade
"helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
labels:
{{- include "hami-vgpu.labels" . | nindent 4 }}
app.kubernetes.io/component: admission-webhook
rules:
- apiGroups:
- ""
resources:
- secrets
verbs:
- get
- create
{{- end }}

View file

@ -0,0 +1,21 @@
{{- if and .Values.scheduler.admissionWebhook.enabled (.Values.scheduler.patch.enabled) (not .Values.scheduler.certManager.enabled) -}}
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: {{ include "hami-vgpu.fullname" . }}-admission
namespace: {{ include "hami-vgpu.namespace" . }}
annotations:
"helm.sh/hook": pre-install,pre-upgrade,post-install,post-upgrade
"helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
labels:
{{- include "hami-vgpu.labels" . | nindent 4 }}
app.kubernetes.io/component: admission-webhook
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: {{ include "hami-vgpu.fullname" . }}-admission
subjects:
- kind: ServiceAccount
name: {{ include "hami-vgpu.fullname" . }}-admission
namespace: {{ include "hami-vgpu.namespace" . }}
{{- end }}

View file

@ -0,0 +1,13 @@
{{- if and .Values.scheduler.admissionWebhook.enabled (.Values.scheduler.patch.enabled) (not .Values.scheduler.certManager.enabled) }}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "hami-vgpu.fullname" . }}-admission
namespace: {{ include "hami-vgpu.namespace" . }}
annotations:
"helm.sh/hook": pre-install,pre-upgrade,post-install,post-upgrade
"helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
labels:
{{- include "hami-vgpu.labels" . | nindent 4 }}
app.kubernetes.io/component: admission-webhook
{{- end }}

View file

@ -0,0 +1,12 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: {{ include "hami-vgpu.scheduler" . }}
namespace: {{ include "hami-vgpu.namespace" . }}
labels:
app.kubernetes.io/component: "hami-scheduler"
{{- include "hami-vgpu.labels" . | nindent 4 }}
rules:
- apiGroups: ["coordination.k8s.io"]
resources: ["leases"]
verbs: ["create", "list", "watch", "get", "update", "patch"]

View file

@ -0,0 +1,31 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: {{ include "hami-vgpu.scheduler" . }}
namespace: {{ include "hami-vgpu.namespace" . }}
labels:
app.kubernetes.io/component: "hami-scheduler"
{{- include "hami-vgpu.labels" . | nindent 4 }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: {{ include "hami-vgpu.scheduler" . }}
subjects:
- kind: ServiceAccount
name: {{ include "hami-vgpu.scheduler" . }}
namespace: {{ include "hami-vgpu.namespace" . }}
---
{{- if .Values.mockDevicePlugin.enabled }}
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: {{ include "hami-vgpu.mock-device-plugin" . }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: {{ include "hami-vgpu.mock-device-plugin" . }}
subjects:
- kind: ServiceAccount
name: {{ include "hami-vgpu.mock-device-plugin" . }}
namespace: {{ include "hami-vgpu.namespace" . }}
{{- end -}}

View file

@ -0,0 +1,34 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "hami-vgpu.scheduler" . }}
namespace: {{ include "hami-vgpu.namespace" . }}
labels:
app.kubernetes.io/component: hami-scheduler
{{- include "hami-vgpu.labels" . | nindent 4 }}
{{- if .Values.scheduler.service.labels }}
{{ toYaml .Values.scheduler.service.labels | indent 4 }}
{{- end }}
{{- if .Values.scheduler.service.annotations }}
annotations: {{ toYaml .Values.scheduler.service.annotations | nindent 4 }}
{{- end }}
spec:
type: {{ .Values.scheduler.service.type | default "NodePort" }} # Default type is NodePort
ports:
- name: http
port: {{ .Values.scheduler.service.httpPort | default 443 }} # Default HTTP port is 443
targetPort: {{ .Values.scheduler.service.httpTargetPort | default 443 }}
{{- if eq (.Values.scheduler.service.type | default "NodePort") "NodePort" }} # If type is NodePort, set nodePort
nodePort: {{ .Values.scheduler.service.schedulerPort | default 31998 }}
{{- end }}
protocol: TCP
- name: monitor
port: {{ .Values.scheduler.service.monitorPort | default 31993 }} # Default monitoring port is 31993
targetPort: {{ .Values.scheduler.service.monitorTargetPort | default 9395 }}
{{- if eq (.Values.scheduler.service.type | default "NodePort") "NodePort" }} # If type is NodePort, set nodePort
nodePort: {{ .Values.scheduler.service.monitorPort | default 31993 }}
{{- end }}
protocol: TCP
selector:
app.kubernetes.io/component: hami-scheduler
{{- include "hami-vgpu.selectorLabels" . | nindent 4 }}

View file

@ -0,0 +1,16 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "hami-vgpu.scheduler" . }}
namespace: {{ include "hami-vgpu.namespace" . }}
labels:
app.kubernetes.io/component: "hami-scheduler"
{{- include "hami-vgpu.labels" . | nindent 4 }}
---
{{- if .Values.mockDevicePlugin.enabled }}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "hami-vgpu.mock-device-plugin" . }}
namespace: {{ include "hami-vgpu.namespace" . }}
{{- end -}}

View file

@ -0,0 +1,57 @@
{{- if .Values.scheduler.admissionWebhook.enabled -}}
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
metadata:
{{- if .Values.scheduler.certManager.enabled }}
annotations:
cert-manager.io/inject-ca-from: {{ include "hami-vgpu.namespace" . }}/{{ include "hami-vgpu.scheduler" . }}-serving-cert
{{- end }}
name: {{ include "hami-vgpu.scheduler.webhook" . }}
webhooks:
- admissionReviewVersions:
- v1beta1
clientConfig:
{{- if .Values.scheduler.admissionWebhook.customURL.enabled }}
url: https://{{ .Values.scheduler.admissionWebhook.customURL.host}}:{{.Values.scheduler.admissionWebhook.customURL.port}}{{.Values.scheduler.admissionWebhook.customURL.path}}
{{- else }}
service:
name: {{ include "hami-vgpu.scheduler" . }}
namespace: {{ include "hami-vgpu.namespace" . }}
path: /webhook
port: {{ .Values.scheduler.service.httpPort }}
{{- end }}
failurePolicy: {{ .Values.scheduler.admissionWebhook.failurePolicy }}
matchPolicy: Equivalent
name: vgpu.hami.io
namespaceSelector:
matchExpressions:
- key: hami.io/webhook
operator: NotIn
values:
- ignore
{{- if .Values.scheduler.admissionWebhook.whitelistNamespaces }}
- key: kubernetes.io/metadata.name
operator: NotIn
values:
{{- toYaml .Values.scheduler.admissionWebhook.whitelistNamespaces | nindent 10 }}
{{- end }}
objectSelector:
matchExpressions:
- key: hami.io/webhook
operator: NotIn
values:
- ignore
reinvocationPolicy: {{ .Values.scheduler.admissionWebhook.reinvocationPolicy }}
rules:
- apiGroups:
- ""
apiVersions:
- v1
operations:
- CREATE
resources:
- pods
scope: '*'
sideEffects: None
timeoutSeconds: 10
{{- end }}

View file

@ -0,0 +1,455 @@
## @section Global configuration
## @param global.imageRegistry Global Docker image registry
## @param global.imagePullSecrets Global Docker image pull secrets
global:
## @param global.imageRegistry Global Docker image registry
imageRegistry: ""
## E.g.
## imagePullSecrets:
## - myRegistryKeySecretName
## @param global.imagePullSecrets Global Docker image pull secrets
imagePullSecrets: []
imageTag: "v2.8.1"
gpuHookPath: /usr/local
labels: {}
annotations: {}
managedNodeSelectorEnable: false
managedNodeSelector:
usage: "gpu"
nameOverride: ""
fullnameOverride: ""
namespaceOverride: ""
#Nvidia GPU Parameters
resourceName: "nvidia.com/gpu"
resourceMem: "nvidia.com/gpumem"
resourceMemPercentage: "nvidia.com/gpumem-percentage"
resourceCores: "nvidia.com/gpucores"
resourcePriority: "nvidia.com/priority"
#MLU Parameters
mluResourceName: "cambricon.com/vmlu"
mluResourceMem: "cambricon.com/mlu.smlu.vmemory"
mluResourceCores: "cambricon.com/mlu.smlu.vcore"
#Hygon DCU Parameters
dcuResourceName: "hygon.com/dcunum"
dcuResourceMem: "hygon.com/dcumem"
dcuResourceCores: "hygon.com/dcucores"
#Metax sGPU Parameters
metaxResourceName: "metax-tech.com/sgpu"
metaxResourceCore: "metax-tech.com/vcore"
metaxResourceMem: "metax-tech.com/vmemory"
metaxsGPUTopologyAware: "false"
#Enflame VGCU Parameters
enflameResourceNameVGCU: "enflame.com/vgcu"
enflameResourceNameVGCUPercentage: "enflame.com/vgcu-percentage"
#Kunlun XPU Parameters
kunlunResourceName: "kunlunxin.com/xpu"
kunlunResourceVCountName: "kunlunxin.com/vxpu"
kunlunResourceVMemoryName: "kunlunxin.com/vxpu-memory"
schedulerName: "hami-scheduler"
scheduler:
# @param nodeName defines the node name and the nvidia-vgpu-scheduler-scheduler will schedule to the node.
# if we install the nvidia-vgpu-scheduler-scheduler as default scheduler, we need to remove the k8s default
# scheduler pod from the cluster first, we must specify node name to skip the schedule workflow.
nodeName: ""
#nodeLabelSelector:
# "gpu": "on"
overwriteEnv: "false"
defaultSchedulerPolicy:
nodeSchedulerPolicy: binpack
gpuSchedulerPolicy: spread
metricsBindAddress: ":9395"
# If set to false, When Pod.Spec.SchedulerName equals to the const DefaultSchedulerName in k8s.io/api/core/v1 package, webhook will not overwrite it
forceOverwriteDefaultScheduler: true
livenessProbe: false
leaderElect: true
# when leaderElect is true, replicas is available, otherwise replicas is 1.
replicas: 1
kubeScheduler:
# @param enabled indicate whether to run kube-scheduler container in the scheduler pod, it's true by default.
enabled: true
## @param image.registry kube scheduler image registry
## @param image.repository kube scheduler image repository
## @param image.tag kube scheduler image tag (immutable tags are recommended)
## @param image.pullPolicy kube scheduler image pull policy
## @param image.pullSecrets Specify docker-registry secret names as an array
image:
registry: "registry.cn-hangzhou.aliyuncs.com"
repository: "google_containers/kube-scheduler"
tag: ""
## Specify a imagePullPolicy
## Defaults to 'Always' if image tag is 'latest', else set to 'IfNotPresent'
## ref: https://kubernetes.io/docs/user-guide/images/#pre-pulling-images
##
pullPolicy: IfNotPresent
## Optionally specify an array of imagePullSecrets.
## Secrets must be manually created in the namespace.
## Example:
## pullSecrets:
## - myRegistryKeySecretName
##
pullSecrets: []
resources: {}
# If you do want to specify resources, uncomment the following lines, adjust them as necessary.
# and remove the curly braces after 'resources:'.
# limits:
# cpu: 1000m
# memory: 1000Mi
# requests:
# cpu: 100m
# memory: 100Mi
extraNewArgs:
- --config=/config/config.yaml
- -v=4
extraArgs:
- --policy-config-file=/config/config.json
- -v=4
extender:
## @param image.registry scheduler extender image registry
## @param image.repository scheduler extender image repository
## @param image.tag scheduler extender image tag (immutable tags are recommended)
## @param image.pullPolicy scheduler extender image pull policy
## @param image.pullSecrets Specify docker-registry secret names as an array
image:
registry: "docker.io"
repository: "projecthami/hami"
tag: ""
## Specify a imagePullPolicy
## Defaults to 'Always' if image tag is 'latest', else set to 'IfNotPresent'
## ref: https://kubernetes.io/docs/user-guide/images/#pre-pulling-images
##
pullPolicy: IfNotPresent
## Optionally specify an array of imagePullSecrets.
## Secrets must be manually created in the namespace.
## Example:
## pullSecrets:
## - myRegistryKeySecretName
##
pullSecrets: []
resources: {}
# If you do want to specify resources, uncomment the following lines, adjust them as necessary,
# and remove the curly braces after 'resources:'.
# limits:
# cpu: 1000m
# memory: 1000Mi
# requests:
# cpu: 100m
# memory: 100Mi
extraArgs:
- --debug
- -v=4
nodeLockExpire: "5m"
podAnnotations: {}
tolerations: []
#serviceAccountName: "hami-vgpu-scheduler-sa"
admissionWebhook:
# If set to false, the admission webhook is not installed and any pods that should use HAMi must be
# configured to use the right 'schedulerName' and other device-specific configurations.
enabled: true
customURL:
enabled: false
# must be an endpoint using https.
# should generate host certs here
host: 127.0.0.1 # hostname or ip, can be your node'IP if you want to use https://<nodeIP>:<schedulerPort>/<path>
port: 31998
path: /webhook
whitelistNamespaces:
# Specify the namespaces that the webhook will not be applied to.
# - default
# - kube-system
# - istio-system
reinvocationPolicy: Never
failurePolicy: Ignore
## TLS Certificate Option 1: Use cert-manager to generate self-signed certificate.
## If enabled, always takes precedence over options 2.
certManager:
enabled: false
## TLS Certificate Option 2: Use kube-webhook-certgen to generate self-signed certificate.
## If true and certManager.enabled is false, Helm will automatically create a self-signed cert and secret for you.
patch:
enabled: true
## @param image.registry scheduler extender image registry
## @param image.repository scheduler extender image repository
## @param image.tag scheduler extender image tag (immutable tags are recommended)
## @param image.pullPolicy scheduler extender image pull policy
## @param image.pullSecrets Specify docker-registry secret names as an array
image:
registry: "docker.io"
repository: "jettech/kube-webhook-certgen"
tag: "v1.5.2"
## Specify a imagePullPolicy
## Defaults to 'Always' if image tag is 'latest', else set to 'IfNotPresent'
## ref: https://kubernetes.io/docs/user-guide/images/#pre-pulling-images
##
pullPolicy: IfNotPresent
## Optionally specify an array of imagePullSecrets.
## Secrets must be manually created in the namespace.
## Example:
## pullSecrets:
## - myRegistryKeySecretName
##
pullSecrets: []
## @param image.registry scheduler extender image registry
## @param image.repository scheduler extender image repository
## @param image.tag scheduler extender image tag (immutable tags are recommended)
## @param image.pullPolicy scheduler extender image pull policy
## @param image.pullSecrets Specify docker-registry secret names as an array
imageNew:
registry: "docker.io"
repository: "liangjw/kube-webhook-certgen"
tag: "v1.1.1"
## Specify a imagePullPolicy
## Defaults to 'Always' if image tag is 'latest', else set to 'IfNotPresent'
## ref: https://kubernetes.io/docs/user-guide/images/#pre-pulling-images
##
pullPolicy: IfNotPresent
## Optionally specify an array of imagePullSecrets.
## Secrets must be manually created in the namespace.
## Example:
## pullSecrets:
## - myRegistryKeySecretName
##
pullSecrets: []
priorityClassName: ""
podAnnotations: {}
nodeSelector: {}
tolerations: []
runAsUser: 2000
service:
type: NodePort # Default type is NodePort, can be changed to ClusterIP
httpPort: 443 # HTTP port
schedulerPort: 31998 # NodePort for HTTP
monitorPort: 31993 # Monitoring port
monitorTargetPort: 9395
httpTargetPort: 443
labels: {}
annotations: {}
devicePlugin:
enabled: true
gpuOperatorToolkitReady:
enabled: false
hostPath: "/run/nvidia/validations"
## @param image.registry devicePlugin image registry
## @param image.repository devicePlugin image repository
## @param image.tag devicePlugin image tag (immutable tags are recommended)
## @param image.pullPolicy devicePlugin image pull policy
## @param image.pullSecrets Specify docker-registry secret names as an array
image:
registry: "docker.io"
repository: "projecthami/hami"
tag: ""
## Specify a imagePullPolicy
## Defaults to 'Always' if image tag is 'latest', else set to 'IfNotPresent'
## ref: https://kubernetes.io/docs/user-guide/images/#pre-pulling-images
##
pullPolicy: IfNotPresent
## Optionally specify an array of imagePullSecrets.
## Secrets must be manually created in the namespace.
## Example:
## pullSecrets:
## - myRegistryKeySecretName
##
pullSecrets: []
monitor:
## @param image.registry monitor image registry
## @param image.repository monitor image repository
## @param image.tag monitor image tag (immutable tags are recommended)
## @param image.pullPolicy monitor image pull policy
## @param image.pullSecrets Specify docker-registry secret names as an array
image:
registry: "docker.io"
repository: "projecthami/hami"
tag: ""
## Specify a imagePullPolicy
## Defaults to 'Always' if image tag is 'latest', else set to 'IfNotPresent'
## ref: https://kubernetes.io/docs/user-guide/images/#pre-pulling-images
##
pullPolicy: IfNotPresent
## Optionally specify an array of imagePullSecrets.
## Secrets must be manually created in the namespace.
## Example:
## pullSecrets:
## - myRegistryKeySecretName
##
pullSecrets: []
ctrPath: /usr/local/vgpu/containers
resyncInterval: "5m"
extraArgs:
- -v=4
extraEnvs: {}
resources: {}
# If you do want to specify resources, uncomment the following lines, adjust them as necessary.
# and remove the curly braces after 'resources:'.
# limits:
# cpu: 1000m
# memory: 1000Mi
# requests:
# cpu: 100m
# memory: 100Mi
deviceSplitCount: 10
deviceMemoryScaling: 1
deviceCoreScaling: 1
# Node configuration for device plugin, Priority: externalConfigName > config > default config
nodeConfiguration:
# If you want to use a custom config.json, you can set the content here.
# If this is set, it will override the default config.json(An example is as follows).
config: |
{
"nodeconfig": [
{
"name": "your-node-name",
"operatingmode": "hami-core",
"devicememoryscaling": 1,
"devicesplitcount": 10,
"migstrategy": "none",
"filterdevices": {
"uuid": [],
"index": []
}
}
]
}
# If you want to use an existing ConfigMap, you can set the name here.
# If this is set, the chart will not create the ConfigMap and will use the existing one.
externalConfigName: ""
# The runtime class name to be used by the device plugin, and added to the pod.spec.runtimeClassName of applications utilizing NVIDIA GPUs
runtimeClassName: ""
# Whether to create runtime class, name comes from runtimeClassName when it is set
createRuntimeClass: false
migStrategy: "none"
disablecorelimit: "false"
passDeviceSpecsEnabled: false
deviceListStrategy: "envvar"
nvidiaHookPath: null
nvidiaDriverRoot: null
gdrcopyEnabled: null
gdsEnabled: null
mofedEnabled: null
extraArgs:
- -v=4
extraEnvs: {}
service:
type: NodePort # Default type is NodePort, can be changed to ClusterIP
httpPort: 31992
labels: {}
annotations: {}
pluginPath: /var/lib/kubelet/device-plugins
libPath: /usr/local/vgpu
podAnnotations: {}
nvidiaNodeSelector:
gpu: "on"
tolerations: []
# The updateStrategy for DevicePlugin DaemonSet.
# If you want to update the DaemonSet by manual, set type as "OnDelete".
# We recommend use OnDelete update strategy because DevicePlugin pod restart will cause business pod restart, this behavior is destructive.
# Otherwise, you can use RollingUpdate update strategy to rolling update DevicePlugin pod.
updateStrategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1
resources: {}
# If you do want to specify resources, uncomment the following lines, adjust them as necessary.
# and remove the curly braces after 'resources:'.
# limits:
# cpu: 1000m
# memory: 1000Mi
# requests:
# cpu: 100m
# memory: 100Mi
mockDevicePlugin:
enabled: false
image:
registry: "docker.io"
repository: "projecthami/mock-device-plugin"
tag: "1.0.1"
## Specify a imagePullPolicy
## Defaults to 'Always' if image tag is 'latest', else set to 'IfNotPresent'
## ref: https://kubernetes.io/docs/user-guide/images/#pre-pulling-images
##
pullPolicy: IfNotPresent
## Optionally specify an array of imagePullSecrets.
## Secrets must be manually created in the namespace.
## Example:
## pullSecrets:
## - myRegistryKeySecretName
##
pullSecrets: []
devices:
amd:
customresources:
- amd.com/gpu
- amd.com/gpu-memory
awsneuron:
customresources:
- aws.amazon.com/neuron
- aws.amazon.com/neuroncore
kunlun:
enabled: true
customresources:
- kunlunxin.com/xpu
- kunlunxin.com/vxpu
- kunlunxin.com/vxpu-memory
enflame:
enabled: true
customresources:
- enflame.com/vgcu
- enflame.com/vgcu-percentage
- enflame.com/gcu
mthreads:
enabled: true
customresources:
- mthreads.com/vgpu
nvidia:
gpuCorePolicy: default
libCudaLogLevel: 1
ascend:
enabled: false
image: ""
imagePullPolicy: IfNotPresent
extraArgs: []
nodeSelector:
ascend: "on"
tolerations: []
customresources:
- huawei.com/Ascend910A
- huawei.com/Ascend910A-memory
- huawei.com/Ascend910B2
- huawei.com/Ascend910B2-memory
- huawei.com/Ascend910B3
- huawei.com/Ascend910B3-memory
- huawei.com/Ascend910B4
- huawei.com/Ascend910B4-memory
- huawei.com/Ascend910B4-1
- huawei.com/Ascend910B4-1-memory
- huawei.com/Ascend310P
- huawei.com/Ascend310P-memory
iluvatar:
enabled: false
customresources:
- iluvatar.ai/BI-V100-vgpu
- iluvatar.ai/BI-V100.vCore
- iluvatar.ai/BI-V100.vMem
- iluvatar.ai/BI-V150-vgpu
- iluvatar.ai/BI-V150.vCore
- iluvatar.ai/BI-V150.vMem
- iluvatar.ai/MR-V100-vgpu
- iluvatar.ai/MR-V100.vCore
- iluvatar.ai/MR-V100.vMem
- iluvatar.ai/MR-V50-vgpu
- iluvatar.ai/MR-V50.vCore
- iluvatar.ai/MR-V50.vMem

View file

@ -0,0 +1,17 @@
hami:
scheduler:
kubeScheduler:
image:
registry: registry.k8s.io
repository: kube-scheduler
devicePlugin:
runtimeClassName: nvidia
updateStrategy:
type: RollingUpdate
# See https://github.com/Project-HAMi/HAMi/blob/v2.8.1/deployments/charts/hami/values.yaml
# Each entry: {"name": "node-name", "operatingmode": "hami-core", "devicesplitcount": N, ...}
nodeConfiguration:
config: |
{
"nodeconfig": []
}

File diff suppressed because one or more lines are too long

View file

@ -4,8 +4,13 @@ NAMESPACE=cozy-system
include ../../../hack/common-envs.mk
include ../../../hack/package.mk
.PHONY: test
image: image-lineage-controller-webhook
test:
helm unittest .
image-lineage-controller-webhook:
docker buildx build -f images/lineage-controller-webhook/Dockerfile ../../.. \
--tag $(REGISTRY)/lineage-controller-webhook:$(call settag,$(TAG)) \

View file

@ -0,0 +1,67 @@
# lineage-controller-webhook
Cozystack system package for the **lineage controller webhook** — a mutating
admission webhook that stamps "lineage" labels onto tenant workloads, linking
each resource back to the Cozystack `Application` that ultimately owns it.
The webhook intercepts CREATE and UPDATE on `pods`, `secrets`, `services`,
`persistentvolumeclaims`, `ingresses.networking.k8s.io`, and
`workloadmonitors.cozystack.io` outside system namespaces. For each request it
walks the ownership graph upward (Kubernetes `ownerReferences`, then the
`HelmRelease` Flux installed the resource with, then the Cozystack
`Application`-derived CRD that produced the HelmRelease) and writes the
discovered application's group, kind and name as labels
(`apps.cozystack.io/application.{group,kind,name}`) on the incoming object.
Those labels let the aggregated API server, the Cozystack dashboard, and other
lineage-aware consumers reason about which application a resource belongs to.
The webhook serves TLS on port 9443 with a cert-manager issued certificate, and
runs with `failurePolicy: Fail` — every CREATE/UPDATE on the resources above
is gated on the webhook being reachable.
## Topology
The chart ships a single shape, modelled on `cozystack-api`:
- **Deployment** with two replicas (override via `replicas`).
- **Soft `nodeAffinity`** preferring `node-role.kubernetes.io/control-plane`
(`Exists`, so it matches both Talos's empty value and k3s/kubeadm's `"true"`).
Soft means: the pod lands on a control-plane node when one is reachable, and
on any worker otherwise — no override needed for managed Kubernetes,
Cozy-in-Cozy tenants, or any other cluster where control-plane nodes aren't
visible.
- **Permissive `tolerations`** (`[{operator: Exists}]`) so the pod can land on
tainted control-plane nodes when the soft affinity is satisfiable.
- **Soft `podAntiAffinity`** on `kubernetes.io/hostname` so replicas spread
across nodes when possible (best-effort).
- **`PodDisruptionBudget`** with `maxUnavailable: 1`, unconditional. At
`replicas: 1` it's a useful no-op (allows full drain); at `replicas >= 2`
it caps disruption to one pod.
- **`Service` with `spec.trafficDistribution: PreferClose`** so the apiserver
prefers a webhook endpoint on its own node when one exists, and transparently
falls over to a remote endpoint otherwise. Requires Kubernetes ≥ 1.31; on
older clusters the field is silently ignored and traffic uses default
cluster-wide distribution.
## Parameters
All keys live under the top-level `lineageControllerWebhook:` map.
| Name | Description | Value |
| ----------------------------- | --------------------------------------------------------- | -------------------------------------------------------------------------------- |
| `image` | Container image (digest-pinned) | `ghcr.io/cozystack/cozystack/lineage-controller-webhook:v1.3.0@sha256:e898…6fb0` |
| `debug` | Enable `--zap-log-level=debug` instead of `info` | `false` |
| `replicas` | Deployment replica count | `2` |
| `localK8sAPIEndpoint.enabled` | **Deprecated.** See note below. | `false` |
### `localK8sAPIEndpoint.enabled` (deprecated)
When enabled, this injects `KUBERNETES_SERVICE_HOST=status.hostIP` and
`KUBERNETES_SERVICE_PORT=6443` so the webhook talks to the kube-apiserver on
its own node. It was originally added to avoid latency on the
webhook-to-apiserver path, but it is only valid when the pod is actually
scheduled on an apiserver-bearing node — which the chart's soft control-plane
affinity no longer guarantees. With this flag enabled and the pod scheduled
off a control-plane node, the controller will crash-loop dialing a non-
apiserver hostIP. Slated for removal once the latency motivation is addressed
in the webhook itself; **leave disabled**.

Some files were not shown because too many files have changed in this diff Show more