Compare commits
11 commits
main
...
feature/de
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2157452da3 | ||
|
|
4a4354a7e1 | ||
|
|
5153c1599b | ||
|
|
4576435558 | ||
|
|
14a909dba3 | ||
|
|
facfc90666 | ||
|
|
6654ca7a67 | ||
|
|
0ce56ae338 | ||
|
|
f36b7d64d5 | ||
|
|
b1b6cf40f7 | ||
|
|
1cbe69661a |
344 changed files with 1181 additions and 19286 deletions
|
|
@ -33,18 +33,14 @@ Similarly, never propose edits to:
|
|||
|
||||
## Commit and PR Requirements
|
||||
|
||||
Each commit must follow [Conventional Commits](https://www.conventionalcommits.org/) format: `type(scope): brief description`.
|
||||
Each commit must start with a component prefix: `[component] Brief description`.
|
||||
|
||||
Valid types: `feat`, `fix`, `docs`, `style`, `refactor`, `perf`, `test`, `build`, `ci`, `chore`.
|
||||
Valid prefixes:
|
||||
|
||||
Valid scopes:
|
||||
|
||||
- System: `dashboard`, `platform`, `cilium`, `kube-ovn`, `linstor`, `fluxcd`, `cluster-api`
|
||||
- Apps: `postgres`, `mariadb`, `redis`, `kafka`, `clickhouse`, `kubernetes`, `virtual-machine`
|
||||
- Meta: `api`, `hack`, `tests`, `ci`, `docs`
|
||||
- Package-specific: any `<package-name>` matching a directory under `packages/`
|
||||
|
||||
Breaking changes: append `!` after type/scope (`feat(api)!: ...`) or add a `BREAKING CHANGE:` footer.
|
||||
- System: `[dashboard]`, `[platform]`, `[cilium]`, `[kube-ovn]`, `[linstor]`, `[fluxcd]`, `[cluster-api]`
|
||||
- Apps: `[postgres]`, `[mariadb]`, `[redis]`, `[kafka]`, `[clickhouse]`, `[kubernetes]`, `[virtual-machine]`
|
||||
- Meta: `[tests]`, `[ci]`, `[docs]`, `[maintenance]`
|
||||
- Package-specific: any `[<package-name>]` matching a directory under `packages/`
|
||||
|
||||
Each commit must have a `Signed-off-by:` trailer (produced by `git commit --signoff`).
|
||||
|
||||
|
|
@ -52,11 +48,11 @@ PR body must contain a release note block:
|
|||
|
||||
````text
|
||||
```release-note
|
||||
type(scope): human-readable changelog entry
|
||||
[component] Human-readable changelog entry
|
||||
```
|
||||
````
|
||||
|
||||
Flag any PR whose commits lack the Conventional Commits format or signoff, or whose body has no release-note block.
|
||||
Flag any PR whose commits lack the prefix or signoff, or whose body has no release-note block.
|
||||
|
||||
## Helm Chart Conventions
|
||||
|
||||
|
|
|
|||
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
|
|
@ -1 +1 @@
|
|||
* @kvaps @lllamnyp @lexfrei @androndo @IvanHunters @sircthulhu @myasnikovdaniil
|
||||
* @kvaps @lllamnyp @lexfrei @androndo @IvanHunters @sircthulhu
|
||||
|
|
|
|||
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
labels: 'kind/bug'
|
||||
labels: 'bug'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
|
|
|||
20
.github/PULL_REQUEST_TEMPLATE.md
vendored
20
.github/PULL_REQUEST_TEMPLATE.md
vendored
|
|
@ -1,11 +1,8 @@
|
|||
<!-- 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 are not an exhaustive list — pick the most specific scope for the change and extend the list when a genuinely new area appears. Examples:
|
||||
- System components: dashboard, platform, operator, cilium, kube-ovn, linstor, fluxcd, cluster-api
|
||||
- Managed apps: postgres, mariadb, redis, kafka, clickhouse, virtual-machine, kubernetes
|
||||
- Development and maintenance: api, hack, tests, ci, docs, maintenance
|
||||
- Breaking changes: append `!` after type/scope (`feat(api)!: ...`) or add a `BREAKING CHANGE:` footer
|
||||
- Start the PR title with the [label] of Cozystack component:
|
||||
- For system components: [platform], [system], [linstor], [cilium], [kube-ovn], [dashboard], [cluster-api], etc.
|
||||
- For managed apps: [apps], [tenant], [kubernetes], [postgres], [virtual-machine] etc.
|
||||
- For development and maintenance: [tests], [ci], [docs], [maintenance].
|
||||
- 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.
|
||||
|
|
@ -14,19 +11,14 @@
|
|||
## What this PR does
|
||||
|
||||
|
||||
### Screenshots
|
||||
|
||||
<!-- REQUIRED for UI changes: attach screenshots or screen recordings demonstrating
|
||||
the visual impact of your changes. PRs with UI changes without screenshots will not be merged. -->
|
||||
|
||||
### Release note
|
||||
|
||||
<!-- Write a release note:
|
||||
- Explain what has changed internally and for users.
|
||||
- Start with the same `type(scope):` prefix as in the PR title
|
||||
- Start with the same [label] as in the PR title
|
||||
- Follow the guidelines at https://github.com/kubernetes/community/blob/master/contributors/guide/release-notes.md.
|
||||
-->
|
||||
|
||||
```release-note
|
||||
|
||||
[]
|
||||
```
|
||||
371
.github/labels.yml
vendored
371
.github/labels.yml
vendored
|
|
@ -1,371 +0,0 @@
|
|||
# Cozystack repository labels
|
||||
#
|
||||
# Label conventions follow the Kubernetes scheme:
|
||||
# https://github.com/kubernetes/test-infra/blob/master/label_sync/labels.md
|
||||
#
|
||||
# Synced into the repository by .github/workflows/labels.yaml
|
||||
# (EndBug/label-sync@v2). Edit this file via pull request — UI changes
|
||||
# will be overwritten on the next sync.
|
||||
#
|
||||
# Constraints (enforced by the validate job in labels.yaml):
|
||||
# - description ≤ 100 characters (GitHub REST API limit)
|
||||
# - color is a 6-character hex string (no leading #)
|
||||
# - label names are unique
|
||||
# - aliases do not collide with top-level names
|
||||
#
|
||||
# Categories:
|
||||
# kind/ issue or PR type
|
||||
# priority/ urgency
|
||||
# triage/ review state
|
||||
# lifecycle/ issue or PR lifecycle
|
||||
# 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)
|
||||
#
|
||||
# `aliases:` lets EndBug/label-sync rename existing labels without losing
|
||||
# references on already-tagged issues and PRs.
|
||||
#
|
||||
# GitHub-default labels not migrated here (`wontfix`, `invalid`) currently
|
||||
# carry zero issues/PRs in this repo and will be removed in a follow-up
|
||||
# cleanup PR rather than aliased to a different namespace.
|
||||
|
||||
# ──────────────────────────────────────────────
|
||||
# kind/ — issue or PR type
|
||||
# ──────────────────────────────────────────────
|
||||
|
||||
- name: kind/bug
|
||||
color: 'd73a4a'
|
||||
description: Categorizes issue or PR as related to a bug
|
||||
aliases: ['bug']
|
||||
|
||||
- name: kind/feature
|
||||
color: 'a2eeef'
|
||||
description: Categorizes issue or PR as related to a new feature
|
||||
aliases: ['enhancement']
|
||||
|
||||
- name: kind/documentation
|
||||
color: '0075ca'
|
||||
description: Categorizes issue or PR as related to documentation
|
||||
aliases: ['documentation']
|
||||
|
||||
- name: kind/support
|
||||
color: 'd876e3'
|
||||
description: Categorizes issue as a support question
|
||||
aliases: ['question']
|
||||
|
||||
- name: kind/cleanup
|
||||
color: 'c7def8'
|
||||
description: Categorizes issue or PR as related to cleanup of code, process, or technical debt
|
||||
|
||||
- name: kind/regression
|
||||
color: 'e11d21'
|
||||
description: Categorizes issue or PR as related to a regression from a prior release
|
||||
|
||||
- name: kind/flake
|
||||
color: 'f7c6c7'
|
||||
description: Categorizes issue or PR as related to a flaky test
|
||||
|
||||
- name: kind/failing-test
|
||||
color: 'e11d21'
|
||||
description: Categorizes issue or PR as related to a consistently or frequently failing test
|
||||
|
||||
- name: kind/api-change
|
||||
color: 'c7def8'
|
||||
description: Categorizes issue or PR as related to adding, removing, or otherwise changing an API
|
||||
|
||||
- name: kind/breaking-change
|
||||
color: 'e11d21'
|
||||
description: Indicates the change introduces a breaking API or behaviour change
|
||||
|
||||
# ──────────────────────────────────────────────
|
||||
# priority/ — urgency
|
||||
# ──────────────────────────────────────────────
|
||||
|
||||
- name: priority/critical-urgent
|
||||
color: 'e11d21'
|
||||
description: Highest priority. Must be actively worked on as someone's top priority right now
|
||||
|
||||
- name: priority/important-soon
|
||||
color: 'eb6420'
|
||||
description: Must be staffed and worked on either currently, or very soon, ideally in time for the next release
|
||||
|
||||
- name: priority/important-longterm
|
||||
color: 'fbca04'
|
||||
description: Important over the long term, but may not be staffed and/or may need multiple releases to complete
|
||||
|
||||
- name: priority/backlog
|
||||
color: 'fef2c0'
|
||||
description: General backlog priority. Lower than priority/important-longterm
|
||||
|
||||
# ──────────────────────────────────────────────
|
||||
# triage/ — review state
|
||||
# ──────────────────────────────────────────────
|
||||
|
||||
- name: triage/needs-triage
|
||||
color: 'ededed'
|
||||
description: Indicates an issue needs triage by a maintainer
|
||||
|
||||
- name: triage/accepted
|
||||
color: '0e8a16'
|
||||
description: Indicates an issue is ready to be actively worked on
|
||||
|
||||
- name: triage/needs-information
|
||||
color: 'fbca04'
|
||||
description: Indicates an issue needs more information in order to work on it
|
||||
|
||||
- name: triage/not-reproducible
|
||||
color: 'fbca04'
|
||||
description: Indicates an issue can not be reproduced as described
|
||||
|
||||
- name: triage/duplicate
|
||||
color: 'cfd3d7'
|
||||
description: Indicates an issue is a duplicate of another issue
|
||||
aliases: ['duplicate']
|
||||
|
||||
- name: triage/unresolved
|
||||
color: 'cfd3d7'
|
||||
description: Indicates an issue that can not or will not be resolved
|
||||
|
||||
# ──────────────────────────────────────────────
|
||||
# lifecycle/ — issue or PR lifecycle
|
||||
# ──────────────────────────────────────────────
|
||||
|
||||
- name: lifecycle/active
|
||||
color: '1d76db'
|
||||
description: Indicates that an issue or PR is actively being worked on by a contributor
|
||||
|
||||
- name: lifecycle/frozen
|
||||
color: 'db5dd6'
|
||||
description: Indicates that an issue or PR should not be auto-closed due to staleness
|
||||
aliases: ['frozen']
|
||||
|
||||
- name: lifecycle/stale
|
||||
color: 'dadada'
|
||||
description: Denotes an issue or PR has remained open with no activity and has become stale
|
||||
aliases: ['stale']
|
||||
|
||||
- name: lifecycle/rotten
|
||||
color: '795548'
|
||||
description: Denotes an issue or PR that has aged beyond stale and will be auto-closed
|
||||
|
||||
# ──────────────────────────────────────────────
|
||||
# area/ — subsystem (extensible)
|
||||
# Add a new area/* when there are 3+ open issues on the topic.
|
||||
# ──────────────────────────────────────────────
|
||||
|
||||
- name: area/api
|
||||
color: 'bfd4f2'
|
||||
description: Issues or PRs related to the cozystack-api aggregated API server
|
||||
|
||||
- name: area/ai
|
||||
color: 'bfd4f2'
|
||||
description: Issues or PRs related to AI agent guides, AGENTS.md, docs/agents/
|
||||
|
||||
- name: area/build
|
||||
color: 'bfd4f2'
|
||||
description: Issues or PRs related to image build infrastructure, multi-arch support
|
||||
|
||||
- name: area/ci
|
||||
color: 'bfd4f2'
|
||||
description: Issues or PRs related to CI workflows, GitHub Actions, automation
|
||||
|
||||
- name: area/dashboard
|
||||
color: 'bfd4f2'
|
||||
description: Issues or PRs related to the dashboard / UI
|
||||
|
||||
- name: area/extra
|
||||
color: 'bfd4f2'
|
||||
description: Issues or PRs related to tenant-specific modules (packages/extra/)
|
||||
|
||||
- name: area/database
|
||||
color: 'bfd4f2'
|
||||
description: Issues or PRs related to managed databases (postgres, mariadb, redis, etcd, kafka, clickhouse)
|
||||
|
||||
- name: area/kubernetes
|
||||
color: 'bfd4f2'
|
||||
description: Issues or PRs related to the tenant Kubernetes app
|
||||
|
||||
- name: area/monitoring
|
||||
color: 'bfd4f2'
|
||||
description: Issues or PRs related to the monitoring stack (vlogs, vmstack, grafana, workloadmonitor)
|
||||
|
||||
- name: area/networking
|
||||
color: 'bfd4f2'
|
||||
description: Issues or PRs related to networking (ingress, gateway, vpn, metallb, cilium, kube-ovn)
|
||||
|
||||
- name: area/platform
|
||||
color: 'bfd4f2'
|
||||
description: Issues or PRs related to platform infrastructure (bundle, flux, talos, installer)
|
||||
|
||||
- name: area/release
|
||||
color: 'bfd4f2'
|
||||
description: Issues or PRs related to release tooling (changelog, backport, release pipeline)
|
||||
|
||||
- name: area/storage
|
||||
color: 'bfd4f2'
|
||||
description: Issues or PRs related to storage (linstor, seaweedfs, bucket, velero, harbor)
|
||||
|
||||
- name: area/testing
|
||||
color: 'bfd4f2'
|
||||
description: Issues or PRs related to testing (e2e, bats, unit tests)
|
||||
|
||||
- name: area/virtualization
|
||||
color: 'bfd4f2'
|
||||
description: Issues or PRs related to virtualization (kubevirt, cdi, vmi, vm-import)
|
||||
|
||||
- name: area/uncategorized
|
||||
color: 'fbca04'
|
||||
description: PR auto-labeler could not map title scope to a known area/*; please review
|
||||
|
||||
# ──────────────────────────────────────────────
|
||||
# do-not-merge/ — PR merge blockers (Prow convention)
|
||||
# ──────────────────────────────────────────────
|
||||
|
||||
- name: do-not-merge/work-in-progress
|
||||
color: 'e11d21'
|
||||
description: Indicates that a PR should not merge because it is a work in progress
|
||||
# Both legacy spellings collapse here. EndBug processes aliases sequentially;
|
||||
# the second rename hits a name collision and logs a warning — the legacy
|
||||
# label survives and gets cleaned up in the follow-up dedup PR.
|
||||
aliases: ['do-not-merge', 'do not merge']
|
||||
|
||||
- name: do-not-merge/hold
|
||||
color: 'e11d21'
|
||||
description: Indicates that a PR should not merge because someone has issued /hold
|
||||
|
||||
# ──────────────────────────────────────────────
|
||||
# Cozystack-specific (preserved)
|
||||
# ──────────────────────────────────────────────
|
||||
|
||||
- name: epic
|
||||
color: 'a335ee'
|
||||
description: A large development increment that brings definite value to Cozystack users
|
||||
|
||||
- name: community
|
||||
color: '97458a'
|
||||
description: Community contributions are welcome in this issue
|
||||
|
||||
- name: help wanted
|
||||
color: '008672'
|
||||
description: Extra attention is needed
|
||||
|
||||
- name: good first issue
|
||||
color: '7057ff'
|
||||
description: Good for newcomers
|
||||
|
||||
- name: quality-of-life
|
||||
color: 'aaaaaa'
|
||||
description: QoL improvements
|
||||
|
||||
- name: upstream-issue
|
||||
color: 'aaaaaa'
|
||||
description: Requires resolving an issue in an upstream project
|
||||
|
||||
- name: backport
|
||||
color: 'fbca04'
|
||||
description: Should change be backported on previous release
|
||||
|
||||
- name: backport-previous
|
||||
color: 'fbd876'
|
||||
description: Backport target — previous release line
|
||||
|
||||
- name: release
|
||||
color: 'aaaaaa'
|
||||
description: Releasing a new Cozystack version
|
||||
|
||||
- name: automated
|
||||
color: 'ededed'
|
||||
description: Created by automation
|
||||
|
||||
- name: debug
|
||||
color: '704479'
|
||||
description: Debugging in progress
|
||||
|
||||
- name: sponsored
|
||||
color: '00ff00'
|
||||
description: Sponsored work
|
||||
|
||||
- name: lgtm
|
||||
color: '238636'
|
||||
description: This PR has been approved by a maintainer
|
||||
|
||||
- name: ok-to-test
|
||||
color: '00ff00'
|
||||
description: Indicates a non-member PR is safe to run CI on
|
||||
|
||||
# ──────────────────────────────────────────────
|
||||
# size: — PR size (auto-applied by sizing bot)
|
||||
# ──────────────────────────────────────────────
|
||||
|
||||
- name: 'size:XS'
|
||||
color: '00ff00'
|
||||
description: This PR changes 0-9 lines, ignoring generated files
|
||||
|
||||
- name: 'size:S'
|
||||
color: '77b800'
|
||||
description: This PR changes 10-29 lines, ignoring generated files
|
||||
|
||||
- name: 'size:M'
|
||||
color: 'ebb800'
|
||||
description: This PR changes 30-99 lines, ignoring generated files
|
||||
|
||||
- name: 'size:L'
|
||||
color: 'eb9500'
|
||||
description: This PR changes 100-499 lines, ignoring generated files
|
||||
|
||||
- name: 'size:XL'
|
||||
color: 'ff823f'
|
||||
description: This PR changes 500-999 lines, ignoring generated files
|
||||
|
||||
- name: 'size:XXL'
|
||||
color: 'ffb8b8'
|
||||
description: This PR changes 1000+ lines, ignoring generated files
|
||||
|
||||
# ──────────────────────────────────────────────
|
||||
# security/ — security-finding severity and status
|
||||
# ──────────────────────────────────────────────
|
||||
|
||||
- name: security
|
||||
color: 'aaaaaa'
|
||||
description: Security-related issues and features
|
||||
|
||||
- name: security/critical
|
||||
color: 'd73a4a'
|
||||
description: Critical security vulnerability
|
||||
|
||||
- name: security/high
|
||||
color: 'e99695'
|
||||
description: High severity security finding
|
||||
|
||||
- name: security/medium
|
||||
color: 'f9c513'
|
||||
description: Medium severity security finding
|
||||
|
||||
- name: security/low
|
||||
color: '0e8a16'
|
||||
description: Low severity security finding
|
||||
|
||||
- name: security/triage-needed
|
||||
color: 'fbca04'
|
||||
description: Needs security triage
|
||||
|
||||
- name: security/confirmed
|
||||
color: '1d76db'
|
||||
description: Confirmed vulnerability
|
||||
|
||||
- name: security/false-positive
|
||||
color: 'c5def5'
|
||||
description: Triaged as false positive
|
||||
|
||||
- name: security/accepted-risk
|
||||
color: 'bfd4f2'
|
||||
description: Risk accepted with justification
|
||||
|
||||
- name: security/in-progress
|
||||
color: '0075ca'
|
||||
description: Fix in progress
|
||||
|
||||
- name: security/fixed
|
||||
color: '0e8a16'
|
||||
description: Fix released
|
||||
61
.github/workflows/codegen-drift.yml
vendored
61
.github/workflows/codegen-drift.yml
vendored
|
|
@ -1,61 +0,0 @@
|
|||
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
|
||||
84
.github/workflows/labels.yaml
vendored
84
.github/workflows/labels.yaml
vendored
|
|
@ -1,84 +0,0 @@
|
|||
name: Labels
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- .github/labels.yml
|
||||
- .github/workflows/labels.yaml
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- .github/labels.yml
|
||||
- .github/workflows/labels.yaml
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '17 4 * * 1' # Mondays at 04:17 UTC
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: labels-sync
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Validate labels.yml schema
|
||||
run: |
|
||||
python3 - <<'PY'
|
||||
import re, sys, yaml
|
||||
|
||||
path = '.github/labels.yml'
|
||||
data = yaml.safe_load(open(path))
|
||||
errors = []
|
||||
|
||||
# 1. description ≤ 100 chars (GitHub REST API limit)
|
||||
for label in data:
|
||||
desc = label.get('description', '') or ''
|
||||
if len(desc) > 100:
|
||||
errors.append(f"{label['name']}: description {len(desc)} chars (max 100)")
|
||||
|
||||
# 2. color is 6-char hex without leading #
|
||||
for label in data:
|
||||
color = label.get('color', '') or ''
|
||||
if not re.match(r'^[0-9A-Fa-f]{6}$', color):
|
||||
errors.append(f"{label['name']}: bad color {color!r} (must be 6-char hex without #)")
|
||||
|
||||
# 3. unique top-level names
|
||||
names = [label['name'] for label in data]
|
||||
dups = sorted({n for n in names if names.count(n) > 1})
|
||||
for n in dups:
|
||||
errors.append(f"duplicate name: {n}")
|
||||
|
||||
# 4. aliases do not collide with any top-level name
|
||||
name_set = set(names)
|
||||
for label in data:
|
||||
for alias in (label.get('aliases') or []):
|
||||
if alias in name_set:
|
||||
errors.append(f"alias {alias!r} (under {label['name']!r}) collides with a top-level name")
|
||||
|
||||
if errors:
|
||||
for err in errors:
|
||||
print(f"::error::{err}")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"labels.yml schema OK ({len(data)} labels)")
|
||||
PY
|
||||
|
||||
sync:
|
||||
needs: validate
|
||||
if: github.event_name != 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: EndBug/label-sync@v2
|
||||
with:
|
||||
config-file: .github/labels.yml
|
||||
delete-other-labels: false
|
||||
206
.github/workflows/pr-labeler.yaml
vendored
206
.github/workflows/pr-labeler.yaml
vendored
|
|
@ -1,206 +0,0 @@
|
|||
name: PR Auto-Label
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, edited, reopened, synchronize]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
label:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Apply labels from PR title
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
// Conventional Commits types accepted by Cozystack (per docs/agents/contributing.md):
|
||||
// feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert
|
||||
// Mapping below maps a subset to kind/* — types not listed do not produce a kind/*.
|
||||
const typeToKind = {
|
||||
feat: 'kind/feature',
|
||||
fix: 'kind/bug',
|
||||
docs: 'kind/documentation',
|
||||
chore: 'kind/cleanup',
|
||||
refactor: 'kind/cleanup',
|
||||
// style, perf, test, build, ci, revert — no kind mapping
|
||||
};
|
||||
|
||||
// scope -> area/* mapping. Keys are the scopes observed in cozystack issues
|
||||
// and PRs. Add new entries when a scope recurs (3+ times).
|
||||
const scopeToArea = {
|
||||
// area/api
|
||||
'api': 'area/api',
|
||||
'cozystack-api': 'area/api',
|
||||
|
||||
// area/ai
|
||||
'agents': 'area/ai',
|
||||
'ai': 'area/ai',
|
||||
|
||||
// area/build
|
||||
'build': 'area/build',
|
||||
|
||||
// area/ci
|
||||
'ci': 'area/ci',
|
||||
|
||||
// area/dashboard
|
||||
'dashboard': 'area/dashboard',
|
||||
|
||||
// area/database
|
||||
'postgres': 'area/database',
|
||||
'postgres-operator': 'area/database',
|
||||
'mariadb': 'area/database',
|
||||
'mariadb-operator': 'area/database',
|
||||
'redis': 'area/database',
|
||||
'etcd': 'area/database',
|
||||
'kafka': 'area/database',
|
||||
'clickhouse': 'area/database',
|
||||
|
||||
// area/extra
|
||||
'extra': 'area/extra',
|
||||
|
||||
// area/kubernetes
|
||||
'kubernetes': 'area/kubernetes',
|
||||
'apps/kubernetes': 'area/kubernetes',
|
||||
|
||||
// area/monitoring
|
||||
'monitoring': 'area/monitoring',
|
||||
'vlogs': 'area/monitoring',
|
||||
'vmstack': 'area/monitoring',
|
||||
'grafana': 'area/monitoring',
|
||||
'workloadmonitor': 'area/monitoring',
|
||||
|
||||
// area/networking
|
||||
'ingress': 'area/networking',
|
||||
'ingress-nginx': 'area/networking',
|
||||
'gateway': 'area/networking',
|
||||
'vpn': 'area/networking',
|
||||
'metallb': 'area/networking',
|
||||
'cilium': 'area/networking',
|
||||
'kube-ovn': 'area/networking',
|
||||
'tcp-balancer': 'area/networking',
|
||||
'securitygroups': 'area/networking',
|
||||
'cozy-proxy': 'area/networking',
|
||||
|
||||
// area/platform
|
||||
'platform': 'area/platform',
|
||||
'bundle': 'area/platform',
|
||||
'flux': 'area/platform',
|
||||
'fluxcd': 'area/platform',
|
||||
'cluster-api': 'area/platform',
|
||||
'talos': 'area/platform',
|
||||
'installer': 'area/platform',
|
||||
'cozyctl': 'area/platform',
|
||||
'cozystack-engine': 'area/platform',
|
||||
'cozy-lib': 'area/platform',
|
||||
|
||||
// area/release
|
||||
'backport': 'area/release',
|
||||
'release': 'area/release',
|
||||
|
||||
// area/storage
|
||||
'seaweedfs': 'area/storage',
|
||||
'seaweedfs-cosi-driver': 'area/storage',
|
||||
'bucket': 'area/storage',
|
||||
'linstor': 'area/storage',
|
||||
'velero': 'area/storage',
|
||||
'harbor': 'area/storage',
|
||||
'backups': 'area/storage',
|
||||
|
||||
// area/testing
|
||||
'tests': 'area/testing',
|
||||
'e2e': 'area/testing',
|
||||
|
||||
// area/virtualization
|
||||
'kubevirt': 'area/virtualization',
|
||||
'cdi': 'area/virtualization',
|
||||
'vmi': 'area/virtualization',
|
||||
'vm-import': 'area/virtualization',
|
||||
'virtual-machine': 'area/virtualization',
|
||||
'hami': 'area/virtualization',
|
||||
'gpu-operator': 'area/virtualization',
|
||||
};
|
||||
|
||||
const pr = context.payload.pull_request;
|
||||
const title = pr.title || '';
|
||||
const body = pr.body || '';
|
||||
const existing = new Set((pr.labels || []).map(l => l.name));
|
||||
const toAdd = new Set();
|
||||
|
||||
// 1. Strip "[Backport release-1.x]" prefix if present.
|
||||
const backportMatch = title.match(/^\[Backport ([^\]]+)\]\s+(.+)$/);
|
||||
const cleanTitle = backportMatch ? backportMatch[2] : title;
|
||||
if (backportMatch) {
|
||||
toAdd.add('area/release');
|
||||
toAdd.add('backport');
|
||||
}
|
||||
|
||||
// 2. Try Conventional Commits form: type(scope)?(!)?: description
|
||||
const conv = cleanTitle.match(/^([a-z]+)(?:\(([^)]+)\))?(!)?:\s*.+$/);
|
||||
// 3. Fall back to bracket form: [scope] description
|
||||
const bracket = !conv && cleanTitle.match(/^\[([^\]]+)\]\s+.+$/);
|
||||
|
||||
let type = null, scopeStr = null, breaking = false;
|
||||
if (conv) {
|
||||
type = conv[1];
|
||||
scopeStr = conv[2] || null;
|
||||
breaking = !!conv[3];
|
||||
} else if (bracket) {
|
||||
scopeStr = bracket[1];
|
||||
}
|
||||
|
||||
// 4. Detect BREAKING CHANGE: or BREAKING-CHANGE: footer in body.
|
||||
// Conventional Commits 1.0 spec item 16 treats them as synonymous.
|
||||
if (/^BREAKING[ -]CHANGE:/m.test(body)) {
|
||||
breaking = true;
|
||||
}
|
||||
|
||||
// 5. Apply kind/* from type.
|
||||
if (type) {
|
||||
if (typeToKind[type]) {
|
||||
toAdd.add(typeToKind[type]);
|
||||
} else {
|
||||
core.warning(`type "${type}" has no kind/* mapping — typo or new type? See .github/workflows/pr-labeler.yaml typeToKind`);
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Apply area/* from scope. Composite scopes split on comma.
|
||||
const scopes = (scopeStr || '')
|
||||
.split(/,\s*/)
|
||||
.map(s => s.trim())
|
||||
.filter(Boolean);
|
||||
for (const s of scopes) {
|
||||
if (scopeToArea[s]) {
|
||||
toAdd.add(scopeToArea[s]);
|
||||
} else {
|
||||
core.warning(`scope "${s}" has no area/* mapping — consider extending scopeToArea in .github/workflows/pr-labeler.yaml if it recurs`);
|
||||
}
|
||||
}
|
||||
|
||||
// 7. kind/breaking-change.
|
||||
if (breaking) {
|
||||
toAdd.add('kind/breaking-change');
|
||||
}
|
||||
|
||||
// 8. Fallback: no area/* applied -> area/uncategorized.
|
||||
const hasArea = [...toAdd].some(l => l.startsWith('area/'));
|
||||
if (!hasArea) {
|
||||
toAdd.add('area/uncategorized');
|
||||
}
|
||||
|
||||
// 9. Additive only — never remove existing labels.
|
||||
const newLabels = [...toAdd].filter(l => !existing.has(l));
|
||||
if (newLabels.length === 0) {
|
||||
core.info('No new labels to apply');
|
||||
return;
|
||||
}
|
||||
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: pr.number,
|
||||
labels: newLabels,
|
||||
});
|
||||
core.info(`Applied labels: ${newLabels.join(', ')}`);
|
||||
139
.github/workflows/tags.yaml
vendored
139
.github/workflows/tags.yaml
vendored
|
|
@ -223,7 +223,7 @@ jobs:
|
|||
repo: context.repo.repo,
|
||||
head,
|
||||
base,
|
||||
title: `chore(release): cut v${version}`,
|
||||
title: `Release v${version}`,
|
||||
body: `This PR prepares the release \`v${version}\`.`,
|
||||
draft: false
|
||||
});
|
||||
|
|
@ -255,21 +255,6 @@ jobs:
|
|||
private-key: ${{ secrets.COZYSTACK_CI_PRIVATE_KEY }}
|
||||
owner: cozystack
|
||||
|
||||
# Read-only token for the AI step. Minting a separate scoped token
|
||||
# means the Generate changelog using AI step cannot push branches,
|
||||
# open PRs, or mutate any repository even with --allow-all-tools,
|
||||
# regardless of whether the agent follows the prompt's instructions.
|
||||
- name: Generate read-only GitHub App token
|
||||
id: app-token-read
|
||||
uses: actions/create-github-app-token@v1
|
||||
with:
|
||||
app-id: ${{ secrets.COZYSTACK_CI_APP_ID }}
|
||||
private-key: ${{ secrets.COZYSTACK_CI_PRIVATE_KEY }}
|
||||
owner: cozystack
|
||||
permission-contents: read
|
||||
permission-pull-requests: read
|
||||
permission-metadata: read
|
||||
|
||||
- name: Parse tag
|
||||
id: tag
|
||||
uses: actions/github-script@v7
|
||||
|
|
@ -318,59 +303,50 @@ jobs:
|
|||
|
||||
- name: Generate changelog using AI
|
||||
if: steps.check_changelog.outputs.exists == 'false'
|
||||
timeout-minutes: 30
|
||||
env:
|
||||
COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
|
||||
GH_TOKEN: ${{ steps.app-token-read.outputs.token }}
|
||||
VERSION: ${{ steps.tag.outputs.version }}
|
||||
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
run: |
|
||||
copilot \
|
||||
--prompt "Generate the release changelog for tag v${VERSION}. Follow the instructions in @docs/agents/changelog.md exactly, including the 'Scope and boundaries' section at the top. Your deliverable is the single file docs/changelogs/v${VERSION}.md — write it and exit; this workflow handles branching, committing, pushing, and opening the PR." \
|
||||
copilot --prompt "prepare changelog file for tagged release v${{ steps.tag.outputs.version }}, use @docs/agents/changelog.md for it. Create the changelog file at docs/changelogs/v${{ steps.tag.outputs.version }}.md" \
|
||||
--allow-all-tools --allow-all-paths < /dev/null
|
||||
|
||||
- name: Create changelog branch and commit
|
||||
if: steps.check_changelog.outputs.exists == 'false'
|
||||
env:
|
||||
APP_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
VERSION: ${{ steps.tag.outputs.version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
CHANGELOG_FILE="docs/changelogs/v${VERSION}.md"
|
||||
CHANGELOG_BRANCH="changelog-v${VERSION}"
|
||||
|
||||
if [ ! -f "$CHANGELOG_FILE" ]; then
|
||||
echo "::error::Changelog file $CHANGELOG_FILE was not produced by the Generate changelog using AI step"
|
||||
exit 1
|
||||
fi
|
||||
if [ ! -s "$CHANGELOG_FILE" ]; then
|
||||
echo "::error::Changelog file $CHANGELOG_FILE is empty"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Snapshot the file across the branch switch — the checkout below
|
||||
# resets tracked files to match origin/main.
|
||||
TEMP_FILE="$(mktemp)"
|
||||
trap 'rm -f "$TEMP_FILE"' EXIT
|
||||
cp "$CHANGELOG_FILE" "$TEMP_FILE"
|
||||
|
||||
git config user.name "cozystack-ci[bot]"
|
||||
git config user.email "274107086+cozystack-ci[bot]@users.noreply.github.com"
|
||||
git remote set-url origin "https://x-access-token:${APP_TOKEN}@github.com/${GITHUB_REPOSITORY}"
|
||||
|
||||
git fetch origin main
|
||||
git branch -D "$CHANGELOG_BRANCH" 2>/dev/null || true
|
||||
git checkout -b "$CHANGELOG_BRANCH" origin/main
|
||||
|
||||
mkdir -p "$(dirname "$CHANGELOG_FILE")"
|
||||
cp "$TEMP_FILE" "$CHANGELOG_FILE"
|
||||
|
||||
# The `check_changelog` step gated this job on the file being absent
|
||||
# from origin/main, so `git add` + `git commit` must produce a diff.
|
||||
# If they don't, something is wrong (e.g. empty file) — fail loud.
|
||||
git add "$CHANGELOG_FILE"
|
||||
git commit -m "docs: add changelog for v${VERSION}" -s
|
||||
git push -f origin "$CHANGELOG_BRANCH"
|
||||
git remote set-url origin https://x-access-token:${APP_TOKEN}@github.com/${GITHUB_REPOSITORY}
|
||||
|
||||
CHANGELOG_FILE="docs/changelogs/v${{ steps.tag.outputs.version }}.md"
|
||||
CHANGELOG_BRANCH="changelog-v${{ steps.tag.outputs.version }}"
|
||||
|
||||
if [ -f "$CHANGELOG_FILE" ]; then
|
||||
# Fetch latest main branch
|
||||
git fetch origin main
|
||||
|
||||
# Delete local branch if it exists
|
||||
git branch -D "$CHANGELOG_BRANCH" 2>/dev/null || true
|
||||
|
||||
# Create and checkout new branch from main
|
||||
git checkout -b "$CHANGELOG_BRANCH" origin/main
|
||||
|
||||
# Add and commit changelog
|
||||
git add "$CHANGELOG_FILE"
|
||||
if git diff --staged --quiet; then
|
||||
echo "⚠️ No changes to commit (file may already be committed)"
|
||||
else
|
||||
git commit -m "docs: add changelog for v${{ steps.tag.outputs.version }}" -s
|
||||
echo "✅ Changelog committed to branch $CHANGELOG_BRANCH"
|
||||
fi
|
||||
|
||||
# Push the branch (force push to update if it exists)
|
||||
git push -f origin "$CHANGELOG_BRANCH"
|
||||
else
|
||||
echo "⚠️ Changelog file was not generated"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Create PR for changelog
|
||||
if: steps.check_changelog.outputs.exists == 'false'
|
||||
|
|
@ -411,7 +387,7 @@ jobs:
|
|||
repo: context.repo.repo,
|
||||
head: changelogBranch,
|
||||
base: baseBranch,
|
||||
title: `docs(release): add changelog for v${version}`,
|
||||
title: `docs: add changelog for v${version}`,
|
||||
body: `This PR adds the changelog for release \`v${version}\`.\n\n✅ Changelog has been automatically generated in \`docs/changelogs/v${version}.md\`.`,
|
||||
draft: false
|
||||
});
|
||||
|
|
@ -421,7 +397,7 @@ jobs:
|
|||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: pr.data.number,
|
||||
labels: ['kind/documentation', 'automated']
|
||||
labels: ['documentation', 'automated']
|
||||
});
|
||||
|
||||
console.log(`Created PR #${pr.data.number} for changelog`);
|
||||
|
|
@ -465,58 +441,17 @@ jobs:
|
|||
token: ${{ steps.app-token.outputs.token }}
|
||||
ref: main
|
||||
|
||||
# Decide whether this release promotes the `next/` trunk to a new released
|
||||
# version directory. Per the website repo's contract (see website#495):
|
||||
# - `make release-next` runs only for new minor/major final releases
|
||||
# (tag matches `vX.Y.Z` with no prerelease suffix AND `vX.Y/` does
|
||||
# not yet exist on disk).
|
||||
# - Prereleases and patch releases skip this step and let
|
||||
# `make update-all` handle routing on its own.
|
||||
- name: Determine if this release promotes next/
|
||||
id: promote
|
||||
env:
|
||||
TAG: ${{ steps.tag.outputs.tag }}
|
||||
run: |
|
||||
if [[ ! "$TAG" =~ ^v([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
|
||||
echo "promote=false" >> "$GITHUB_OUTPUT"
|
||||
echo "Prerelease tag '$TAG' — skipping release-next."
|
||||
exit 0
|
||||
fi
|
||||
MAJOR="${BASH_REMATCH[1]}"
|
||||
MINOR="${BASH_REMATCH[2]}"
|
||||
if [[ "$MAJOR" == "0" ]]; then
|
||||
DOC_VERSION="v0"
|
||||
else
|
||||
DOC_VERSION="v${MAJOR}.${MINOR}"
|
||||
fi
|
||||
if [[ -d "content/en/docs/${DOC_VERSION}" ]]; then
|
||||
echo "promote=false" >> "$GITHUB_OUTPUT"
|
||||
echo "content/en/docs/${DOC_VERSION}/ already exists — patch release, skipping release-next."
|
||||
else
|
||||
echo "promote=true" >> "$GITHUB_OUTPUT"
|
||||
echo "New minor/major release ${DOC_VERSION} — will promote next/ → ${DOC_VERSION}/."
|
||||
fi
|
||||
|
||||
- name: Promote next/ to released version
|
||||
if: steps.promote.outputs.promote == 'true'
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
TAG: ${{ steps.tag.outputs.tag }}
|
||||
run: make release-next RELEASE_TAG="$TAG"
|
||||
|
||||
- name: Update docs from release branch
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
TAG: ${{ steps.tag.outputs.tag }}
|
||||
VERSION: ${{ steps.tag.outputs.version }}
|
||||
run: make update-all BRANCH="release-$VERSION" RELEASE_TAG="$TAG"
|
||||
run: make update-all BRANCH=release-${{ steps.tag.outputs.version }} RELEASE_TAG=${{ steps.tag.outputs.tag }}
|
||||
|
||||
- name: Commit and push
|
||||
id: commit
|
||||
run: |
|
||||
git config user.name "cozystack-ci[bot]"
|
||||
git config user.email "274107086+cozystack-ci[bot]@users.noreply.github.com"
|
||||
git add content hugo.yaml
|
||||
git add content
|
||||
if git diff --cached --quiet; then
|
||||
echo "No changes to commit"
|
||||
echo "changed=false" >> $GITHUB_OUTPUT
|
||||
|
|
|
|||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -83,4 +83,3 @@ tmp/
|
|||
|
||||
# build revision marker (generated by make image-packages)
|
||||
packages/core/platform/.build-revision
|
||||
.claude/
|
||||
|
|
|
|||
|
|
@ -1,17 +1,6 @@
|
|||
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: |
|
||||
|
|
|
|||
|
|
@ -27,12 +27,6 @@ working with the **Cozystack** project.
|
|||
- Read: [`contributing.md`](./docs/agents/contributing.md)
|
||||
- Action: Read the file to understand git workflow, commit format, PR process
|
||||
|
||||
- **Issue and PR labeling, triage** (e.g., "label this issue", "what label should I use", "triage this", "categorize")
|
||||
- Read: [`.github/labels.yml`](./.github/labels.yml)
|
||||
- Action: Use labels defined there. Conventions follow the Kubernetes scheme — `kind/*` (type), `area/*` (subsystem), `priority/*` (urgency), `triage/*` (review state), `lifecycle/*` (auto-close), `do-not-merge/*` (PR blockers), `security/*` (severity)
|
||||
- For `area/*`: accuracy outweighs reuse. If no existing `area/*` truly fits the change, propose a new one via PR (extend `.github/labels.yml` and the scope mapping in `.github/workflows/pr-labeler.yaml`) — do not shoehorn the change into a wrong area. `area/uncategorized` is the auto-labeler fallback; treat it as a signal to pick a fit, create a new area, or correct the PR title
|
||||
- PR titles: a Conventional Commits header (`type(scope): description`, types from [`contributing.md`](./docs/agents/contributing.md)) auto-applies `kind/*` and `area/*` via `.github/workflows/pr-labeler.yaml`. Append `!` (or add a `BREAKING CHANGE:` footer) to apply `kind/breaking-change`
|
||||
|
||||
**Important rules:**
|
||||
- ✅ **ONLY read the file if the task matches the documented process scope** - do not read files for tasks that don't match their purpose
|
||||
- ✅ **ALWAYS read the file FIRST** before starting the task (when applicable)
|
||||
|
|
@ -59,7 +53,7 @@ working with the **Cozystack** project.
|
|||
### Conventions
|
||||
- **Helm Charts**: Umbrella pattern, vendored upstream charts in `charts/`
|
||||
- **Go Code**: Controller-runtime patterns, kubebuilder style
|
||||
- **Git Commits**: Conventional Commits (`type(scope): description`) with `--signoff`
|
||||
- **Git Commits**: `[component] Description` format with `--signoff`
|
||||
|
||||
### What NOT to Do
|
||||
- ❌ Edit `/vendor/`, `zz_generated.*.go`, upstream charts directly
|
||||
|
|
|
|||
|
|
@ -10,5 +10,3 @@
|
|||
| Timur Tukaev | [@tym83](https://github.com/tym83) | Ænix | Cozystack Website, Marketing, Community Management |
|
||||
| Kirill Klinchenkov | [@klinch0](https://github.com/klinch0) | Ænix | Core Maintainer |
|
||||
| Nikita Bykov | [@nbykov0](https://github.com/nbykov0) | Ænix | Maintainer of ARM and stuff |
|
||||
| Matthieu Robin | [@matthieu-robin](https://github.com/matthieu-robin) | Hidora | Managed Applications, Platform Quality & Benchmarking |
|
||||
| Mattia Eleuteri | [@mattia-eleuteri](https://github.com/mattia-eleuteri) | Hidora | CSI, Storage, Networking & Security |
|
||||
|
|
|
|||
11
Makefile
11
Makefile
|
|
@ -22,7 +22,6 @@ 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
|
||||
|
|
@ -83,19 +82,11 @@ test:
|
|||
make -C packages/core/testing apply
|
||||
make -C packages/core/testing test
|
||||
|
||||
unit-tests: helm-unit-tests bats-unit-tests go-unit-tests
|
||||
unit-tests: helm-unit-tests bats-unit-tests
|
||||
|
||||
helm-unit-tests:
|
||||
hack/helm-unit-tests.sh
|
||||
|
||||
# Scoped go test over the cozystack-api surface that this repo owns. Kept
|
||||
# narrow intentionally - running `go test ./...` pulls in generated code
|
||||
# round-trip suites whose behavior depends on tool versions outside this
|
||||
# repo's control (kubebuilder, openapi-gen, etc.) and is better exercised
|
||||
# from their generator workflows.
|
||||
go-unit-tests:
|
||||
go test ./pkg/registry/... ./pkg/config/... ./pkg/cmd/server/...
|
||||
|
||||
# Discover every hack/*.bats file that is NOT an e2e test and run it
|
||||
# through cozytest.sh. Drop a new *.bats file in hack/ and it is picked
|
||||
# up automatically on the next `make unit-tests` run.
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
|
||||
# Cozystack
|
||||
|
||||
**Cozystack** is a free platform and framework for building clouds.
|
||||
**Cozystack** is a free PaaS platform and framework for building clouds.
|
||||
|
||||
Cozystack is a [CNCF Sandbox Level Project](https://www.cncf.io/sandbox-projects/) that was originally built and sponsored by [Ænix](https://aenix.io/).
|
||||
|
||||
|
|
|
|||
|
|
@ -36,9 +36,6 @@ type ConfigSpec struct {
|
|||
// Kubernetes control-plane configuration.
|
||||
// +kubebuilder:default:={}
|
||||
ControlPlane ControlPlane `json:"controlPlane"`
|
||||
// Optional image overrides for air-gapped or rate-limited registries.
|
||||
// +kubebuilder:default:={}
|
||||
Images Images `json:"images"`
|
||||
}
|
||||
|
||||
type APIServer struct {
|
||||
|
|
@ -69,9 +66,6 @@ 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"`
|
||||
|
|
@ -163,21 +157,6 @@ 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:=""
|
||||
WaitForKubeconfig string `json:"waitForKubeconfig,omitempty"`
|
||||
}
|
||||
|
||||
type IngressNginxAddon struct {
|
||||
// Enable the controller (requires nodes labeled `ingress-nginx`).
|
||||
// +kubebuilder:default:=false
|
||||
|
|
|
|||
|
|
@ -49,7 +49,6 @@ 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)
|
||||
|
|
@ -136,7 +135,6 @@ 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.
|
||||
|
|
@ -262,37 +260,6 @@ 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
|
||||
|
|
|
|||
|
|
@ -92,9 +92,6 @@ 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 {
|
||||
|
|
|
|||
|
|
@ -32,28 +32,21 @@ type ConfigSpec struct {
|
|||
}
|
||||
|
||||
type Source struct {
|
||||
// Clone an existing vm-disk.
|
||||
Disk *SourceDisk `json:"disk,omitempty"`
|
||||
// Download image from an HTTP source.
|
||||
Http *SourceHTTP `json:"http,omitempty"`
|
||||
// Use image by name from default collection.
|
||||
// Use image by name.
|
||||
Image *SourceImage `json:"image,omitempty"`
|
||||
// Upload local image.
|
||||
Upload *SourceUpload `json:"upload,omitempty"`
|
||||
}
|
||||
|
||||
type SourceDisk struct {
|
||||
// Name of the vm-disk to clone.
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type SourceHTTP struct {
|
||||
// URL to download the image.
|
||||
Url string `json:"url"`
|
||||
}
|
||||
|
||||
type SourceImage struct {
|
||||
// Name of the image to use.
|
||||
// Name of the image to use (uploaded as "golden image" or from the list: `ubuntu`, `fedora`, `cirros`, `alpine`, `talos`).
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -70,11 +70,6 @@ func (in *ConfigSpec) DeepCopy() *ConfigSpec {
|
|||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Source) DeepCopyInto(out *Source) {
|
||||
*out = *in
|
||||
if in.Disk != nil {
|
||||
in, out := &in.Disk, &out.Disk
|
||||
*out = new(SourceDisk)
|
||||
**out = **in
|
||||
}
|
||||
if in.Http != nil {
|
||||
in, out := &in.Http, &out.Http
|
||||
*out = new(SourceHTTP)
|
||||
|
|
@ -102,21 +97,6 @@ func (in *Source) DeepCopy() *Source {
|
|||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *SourceDisk) DeepCopyInto(out *SourceDisk) {
|
||||
*out = *in
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SourceDisk.
|
||||
func (in *SourceDisk) DeepCopy() *SourceDisk {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(SourceDisk)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *SourceHTTP) DeepCopyInto(out *SourceHTTP) {
|
||||
*out = *in
|
||||
|
|
|
|||
|
|
@ -26,9 +26,6 @@ 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"`
|
||||
|
|
|
|||
|
|
@ -63,14 +63,9 @@ func (in *ConfigSpec) DeepCopyInto(out *ConfigSpec) {
|
|||
*out = make([]Disk, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.Networks != nil {
|
||||
in, out := &in.Networks, &out.Networks
|
||||
*out = make([]Network, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.Subnets != nil {
|
||||
in, out := &in.Subnets, &out.Subnets
|
||||
*out = make([]Network, len(*in))
|
||||
*out = make([]Subnet, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.Gpus != nil {
|
||||
|
|
@ -126,21 +121,6 @@ func (in *GPU) DeepCopy() *GPU {
|
|||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Network) DeepCopyInto(out *Network) {
|
||||
*out = *in
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Network.
|
||||
func (in *Network) DeepCopy() *Network {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(Network)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Resources) DeepCopyInto(out *Resources) {
|
||||
*out = *in
|
||||
|
|
@ -158,3 +138,18 @@ func (in *Resources) DeepCopy() *Resources {
|
|||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Subnet) DeepCopyInto(out *Subnet) {
|
||||
*out = *in
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Subnet.
|
||||
func (in *Subnet) DeepCopy() *Subnet {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(Subnet)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,16 +58,6 @@ func (in *ConfigSpec) DeepCopyInto(out *ConfigSpec) {
|
|||
*out = make([]Subnet, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.Peers != nil {
|
||||
in, out := &in.Peers, &out.Peers
|
||||
*out = make([]Peer, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.Routes != nil {
|
||||
in, out := &in.Routes, &out.Routes
|
||||
*out = make([]Route, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ConfigSpec.
|
||||
|
|
@ -80,36 +70,6 @@ func (in *ConfigSpec) DeepCopy() *ConfigSpec {
|
|||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Peer) DeepCopyInto(out *Peer) {
|
||||
*out = *in
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Peer.
|
||||
func (in *Peer) DeepCopy() *Peer {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(Peer)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Route) DeepCopyInto(out *Route) {
|
||||
*out = *in
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Route.
|
||||
func (in *Route) DeepCopy() *Route {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(Route)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Subnet) DeepCopyInto(out *Subnet) {
|
||||
*out = *in
|
||||
|
|
|
|||
|
|
@ -1,21 +1,5 @@
|
|||
//go:build !ignore_autogenerated
|
||||
|
||||
/*
|
||||
Copyright 2025 The Cozystack Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Code generated by controller-gen. DO NOT EDIT.
|
||||
|
||||
package v1alpha1
|
||||
|
|
@ -620,11 +604,6 @@ 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.
|
||||
|
|
@ -666,3 +645,4 @@ func (in *RestoreJobStatus) DeepCopy() *RestoreJobStatus {
|
|||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -132,16 +132,6 @@ type ComponentInstall struct {
|
|||
// DependsOn is a list of component names that must be installed before this component
|
||||
// +optional
|
||||
DependsOn []string `json:"dependsOn,omitempty"`
|
||||
|
||||
// UpgradeCRDs controls how CRDs from the chart's crds/ directory are
|
||||
// handled on HelmRelease upgrades. Maps to HelmRelease.Spec.Upgrade.CRDs.
|
||||
// Empty string (default) preserves the helm-controller default (Skip).
|
||||
// Use "CreateReplace" for operators that evolve their CRD set between
|
||||
// versions. Warning: CreateReplace overwrites CRDs and may cause data
|
||||
// loss if upstream drops fields from a CRD with live objects.
|
||||
// +optional
|
||||
// +kubebuilder:validation:Enum=Skip;Create;CreateReplace
|
||||
UpgradeCRDs string `json:"upgradeCRDs,omitempty"`
|
||||
}
|
||||
|
||||
// Component defines a single Helm release component within a package source
|
||||
|
|
|
|||
|
|
@ -42,7 +42,6 @@ import (
|
|||
"github.com/cozystack/cozystack/internal/telemetry"
|
||||
|
||||
helmv2 "github.com/fluxcd/helm-controller/api/v2"
|
||||
cosiv1alpha1 "sigs.k8s.io/container-object-storage-interface-api/apis/objectstorage/v1alpha1"
|
||||
// +kubebuilder:scaffold:imports
|
||||
)
|
||||
|
||||
|
|
@ -57,7 +56,6 @@ func init() {
|
|||
utilruntime.Must(cozystackiov1alpha1.AddToScheme(scheme))
|
||||
utilruntime.Must(dashboard.AddToScheme(scheme))
|
||||
utilruntime.Must(helmv2.AddToScheme(scheme))
|
||||
utilruntime.Must(cosiv1alpha1.AddToScheme(scheme))
|
||||
// +kubebuilder:scaffold:scheme
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -29,7 +29,6 @@ import (
|
|||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/cache"
|
||||
"sigs.k8s.io/controller-runtime/pkg/healthz"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log/zap"
|
||||
"sigs.k8s.io/controller-runtime/pkg/metrics"
|
||||
|
|
@ -132,11 +131,6 @@ func main() {
|
|||
HealthProbeBindAddress: probeAddr,
|
||||
LeaderElection: enableLeaderElection,
|
||||
LeaderElectionID: "29a0338b.cozystack.io",
|
||||
Cache: cache.Options{
|
||||
DefaultNamespaces: map[string]cache.Config{
|
||||
kubeOVNNamespace: {},
|
||||
},
|
||||
},
|
||||
// LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily
|
||||
// when the Manager ends. This requires the binary to immediately end when the
|
||||
// Manager is stopped, otherwise, this setting is unsafe. Setting this significantly
|
||||
|
|
|
|||
|
|
@ -160,6 +160,10 @@ func main() {
|
|||
os.Exit(1)
|
||||
}
|
||||
|
||||
deletionProtectionWebhook := &lcw.DeletionProtectionWebhook{}
|
||||
mgr.GetWebhookServer().Register("/validate-deletion", &webhook.Admission{Handler: deletionProtectionWebhook})
|
||||
setupLog.Info("registered deletion protection webhook at /validate-deletion")
|
||||
|
||||
// +kubebuilder:scaffold:builder
|
||||
|
||||
if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
|
||||
|
|
|
|||
|
|
@ -1,819 +0,0 @@
|
|||
{
|
||||
"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
|
|
@ -1,957 +0,0 @@
|
|||
{
|
||||
"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": {}
|
||||
}
|
||||
|
|
@ -1,637 +0,0 @@
|
|||
{
|
||||
"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
|
|
@ -6,20 +6,6 @@ This file contains detailed instructions for AI-powered IDE on how to generate c
|
|||
|
||||
Follow these instructions when the user explicitly asks to generate a changelog.
|
||||
|
||||
## Scope and boundaries
|
||||
|
||||
**Your single deliverable is the file `docs/changelogs/v<version>.md`.** Write the complete, verified changelog to that path. That is the entire task. Exit as soon as the file is written and verified against the checklist in Step 9.
|
||||
|
||||
Unless the caller explicitly instructs otherwise:
|
||||
|
||||
- **In the cozystack working tree**, do not run `git commit`, `git push`, `git checkout` (to switch branches), `git branch`, `git tag`, `git reset`, `git merge`, or `git rebase`. Do not write to local branches, tags, or HEAD. `git fetch` is expected and fine (see the read-only analysis list below).
|
||||
- **Do not** push to any remote, open pull requests, or issue GitHub API write calls (POST / PATCH / DELETE) for any repository.
|
||||
- In the cozystack working tree, the **only** file you create or modify is `docs/changelogs/v<version>.md`. Cloning auxiliary repositories under `_repos/` for cross-repo analysis (see Step 6) is fine; local git operations inside those disposable clones (`git checkout`, `git pull`, etc.) are allowed — just never push from them or open PRs against them.
|
||||
|
||||
The caller — a GitHub Actions workflow in CI, or a developer running you interactively — owns branching, committing, pushing, and PR creation. They will perform those actions after you exit. Do not pre-empt them even if the working tree looks ready.
|
||||
|
||||
Read-only analysis is expected and encouraged: `git log`, `git show`, `git fetch`, `git diff`, `gh pr view`, `gh api` GET requests, and reading any file in the repository.
|
||||
|
||||
## Required Tools
|
||||
|
||||
Before generating changelogs, ensure you have access to `gh` (GitHub CLI) tool, which is used to fetch commit and PR author information. The GitHub CLI is used to correctly identify PR authors from commits and pull requests.
|
||||
|
|
@ -36,7 +22,7 @@ When the user asks to generate a changelog, follow these steps in the specified
|
|||
- [ ] Step 5: Get the list of commits for the release period
|
||||
- [ ] Step 6: Check additional repositories (website is REQUIRED, optional repos if tags exist)
|
||||
- [ ] **MANDATORY**: Check website repository for documentation changes WITH authors and PR links via GitHub CLI
|
||||
- [ ] **MANDATORY**: Check ALL optional repositories (talm, boot-to-talos, cozyhr, cozy-proxy, external-apps-example, ansible-cozystack) for tags during release period
|
||||
- [ ] **MANDATORY**: Check ALL optional repositories (talm, boot-to-talos, cozyhr, cozy-proxy) for tags during release period
|
||||
- [ ] **MANDATORY**: For ALL commits from additional repos, get GitHub username via CLI, prioritizing PR author over commit author.
|
||||
- [ ] Step 7: Analyze commits (extract PR numbers, authors, user impact)
|
||||
- [ ] **MANDATORY**: For EVERY PR in main repo, get PR author via `gh pr view <PR_NUMBER> --json author --jq .author.login` (do NOT skip this step)
|
||||
|
|
@ -162,8 +148,6 @@ Cozystack release may include changes from related repositories. Check and inclu
|
|||
- [https://github.com/cozystack/boot-to-talos](https://github.com/cozystack/boot-to-talos)
|
||||
- [https://github.com/cozystack/cozyhr](https://github.com/cozystack/cozyhr)
|
||||
- [https://github.com/cozystack/cozy-proxy](https://github.com/cozystack/cozy-proxy)
|
||||
- [https://github.com/cozystack/external-apps-example](https://github.com/cozystack/external-apps-example)
|
||||
- [https://github.com/cozystack/ansible-cozystack](https://github.com/cozystack/ansible-cozystack)
|
||||
|
||||
**⚠️ IMPORTANT**: You MUST check ALL optional repositories for tags created during the release period. Do NOT skip this step even if you think there might not be any tags. Use the process below to verify.
|
||||
|
||||
|
|
@ -211,7 +195,7 @@ Cozystack release may include changes from related repositories. Check and inclu
|
|||
|
||||
3. **For optional repositories, check if tags exist during release period:**
|
||||
|
||||
**⚠️ MANDATORY: You MUST check ALL optional repositories (talm, boot-to-talos, cozyhr, cozy-proxy, external-apps-example, ansible-cozystack). Do NOT skip any repository!**
|
||||
**⚠️ MANDATORY: You MUST check ALL optional repositories (talm, boot-to-talos, cozyhr, cozy-proxy). Do NOT skip any repository!**
|
||||
|
||||
**Use the helper script:**
|
||||
```bash
|
||||
|
|
@ -224,7 +208,7 @@ Cozystack release may include changes from related repositories. Check and inclu
|
|||
```
|
||||
|
||||
The script will:
|
||||
- Check ALL optional repositories (talm, boot-to-talos, cozyhr, cozy-proxy, external-apps-example, ansible-cozystack)
|
||||
- Check ALL optional repositories (talm, boot-to-talos, cozyhr, cozy-proxy)
|
||||
- Look for tags created during the release period
|
||||
- Get commits between tags (if tags exist) or by date range (if no tags)
|
||||
- Extract PR numbers from commit messages
|
||||
|
|
@ -585,7 +569,7 @@ Create a new changelog file in the format matching previous versions:
|
|||
- [ ] Step 5 completed: **ALL commits included** (including merge commits and backports) - do not skip any commits
|
||||
- [ ] Step 5 completed: **Backports identified and handled correctly** - original PR author used, both original and backport PR numbers included
|
||||
- [ ] Step 6 completed: Website repository checked for documentation changes WITH authors and PR links via GitHub CLI
|
||||
- [ ] Step 6 completed: **ALL** optional repositories (talm, boot-to-talos, cozyhr, cozy-proxy, external-apps-example, ansible-cozystack) checked for tags during release period
|
||||
- [ ] Step 6 completed: **ALL** optional repositories (talm, boot-to-talos, cozyhr, cozy-proxy) checked for tags during release period
|
||||
- [ ] Step 6 completed: For ALL commits from additional repos, GitHub username obtained via GitHub CLI (not skipped). For commits with PR numbers, PR author used via `gh pr view` (not commit author)
|
||||
- [ ] Step 7 completed: For EVERY PR in main repo (including backports), PR author obtained via `gh pr view <PR_NUMBER> --json author --jq .author.login` (not skipped or assumed). Commit author NOT used - always use PR author
|
||||
- [ ] Step 7 completed: **Backports verified** - for each backport PR, original PR found and original PR author used in changelog
|
||||
|
|
@ -622,8 +606,6 @@ Create a new changelog file in the format matching previous versions:
|
|||
**Save the changelog:**
|
||||
Save the changelog to file `docs/changelogs/v<version>.md` according to the version for which the changelog is being generated.
|
||||
|
||||
**Then exit.** Do not commit, push, create a branch, or open a pull request — the caller handles all git and GitHub operations after you return. See the "Scope and boundaries" section at the top of this document.
|
||||
|
||||
### Important notes
|
||||
|
||||
- **After fetch with --force** local tags are up-to-date, use them for work
|
||||
|
|
@ -646,7 +628,7 @@ Save the changelog to file `docs/changelogs/v<version>.md` according to the vers
|
|||
|
||||
- **Additional repositories (Step 6) - MANDATORY**:
|
||||
- **⚠️ CRITICAL**: Always check the **website** repository for documentation changes during the release period. This is a required step and MUST NOT be skipped.
|
||||
- **⚠️ CRITICAL**: You MUST check ALL optional repositories (talm, boot-to-talos, cozyhr, cozy-proxy, external-apps-example, ansible-cozystack) for tags during the release period. Do NOT skip any repository even if you think there might not be tags.
|
||||
- **⚠️ CRITICAL**: You MUST check ALL optional repositories (talm, boot-to-talos, cozyhr, cozy-proxy) for tags during the release period. Do NOT skip any repository even if you think there might not be tags.
|
||||
- **CRITICAL**: For ALL entries from additional repositories (website and optional), you MUST:
|
||||
- **MANDATORY**: Extract PR number from commit message first
|
||||
- **MANDATORY**: For commits with PR numbers, ALWAYS use `gh pr view <PR_NUMBER> --repo cozystack/<repo> --json author --jq .author.login` to get PR author (not commit author)
|
||||
|
|
@ -655,7 +637,7 @@ Save the changelog to file `docs/changelogs/v<version>.md` according to the vers
|
|||
- **MANDATORY**: Do NOT use commit author for PRs - always use PR author
|
||||
- Include PR link or commit hash reference
|
||||
- Format: `* **[repo] Description**: details ([**@username**](https://github.com/username) in cozystack/repo#123)`
|
||||
- For **optional repositories** (talm, boot-to-talos, cozyhr, cozy-proxy, external-apps-example, ansible-cozystack), you MUST check ALL of them for tags during the release period. Use the loop provided in Step 6 to check each repository systematically.
|
||||
- For **optional repositories** (talm, boot-to-talos, cozyhr, cozy-proxy), you MUST check ALL of them for tags during the release period. Use the loop provided in Step 6 to check each repository systematically.
|
||||
- When including changes from additional repositories, use the format: `[repo-name] Description` and link to the repository's PR/issue if available
|
||||
- **Prefer PR numbers over commit hashes**: For commits from additional repositories, extract PR number from commit message using GitHub API. Use PR format (`cozystack/website#123`) instead of commit hash (`cozystack/website@abc1234`) when available
|
||||
- **Never add entries without author and PR/commit reference**: Every entry from additional repositories must have both author and link
|
||||
|
|
|
|||
|
|
@ -1,140 +1,167 @@
|
|||
# Contributing Conventions for AI Agents
|
||||
# Instructions for AI Agents
|
||||
|
||||
Project-side conventions for commits, branches, and pull requests in Cozystack.
|
||||
Guidelines for AI agents contributing to Cozystack.
|
||||
|
||||
## Checklist for Creating a Pull Request
|
||||
|
||||
- [ ] Commit message follows Conventional Commits format
|
||||
- [ ] Changes are made and tested
|
||||
- [ ] Commit message uses correct `[component]` prefix
|
||||
- [ ] Commit is signed off with `--signoff`
|
||||
- [ ] Branch is rebased on `upstream/main` (no extra commits)
|
||||
- [ ] PR body includes description and release note
|
||||
- [ ] Ran `make generate` in every package whose `values.yaml`, `values.schema.json`, `Chart.yaml`, or `README.md` was touched, and committed the regenerated files
|
||||
- [ ] PR is pushed and created with `gh pr create`
|
||||
|
||||
## Regenerate Artifacts Before Committing
|
||||
## How to Commit and Create Pull Requests
|
||||
|
||||
Several files in each package are produced by `make generate` from `values.yaml` + `values.schema.json` and must stay in sync with the hand-edited sources:
|
||||
### 1. Make Your Changes
|
||||
|
||||
- `packages/(apps|extra)/<name>/README.md` — regenerated by `cozyvalues-gen` (parameter table, formatting).
|
||||
- `packages/(apps|extra)/<name>/values.schema.json` — `cozyvalues-gen` rewrites ordering and derived fields.
|
||||
- `packages/system/<name>-rd/cozyrds/<name>.yaml` — produced by `hack/update-crd.sh`, which `make generate` invokes.
|
||||
Edit the necessary files in the codebase.
|
||||
|
||||
**Before committing edits to any of those sources**, run `make generate` inside the package and stage the full diff:
|
||||
### 2. Commit with Proper Format
|
||||
|
||||
Use the `[component]` prefix and `--signoff` flag:
|
||||
|
||||
```bash
|
||||
make -C packages/<apps-or-extra>/<name> generate
|
||||
git add packages/<apps-or-extra>/<name>/ packages/system/<name>-rd/
|
||||
git commit --signoff -m "[component] Brief description of changes"
|
||||
```
|
||||
|
||||
The repo's pre-commit CI job runs `make generate` in every package and then `git diff --exit-code`. Any unstaged generator output fails the job with exit code 123 and blocks the PR. Also rerun `make generate` after a `git commit --amend` if the amended change touched any of the sources above.
|
||||
|
||||
To locate packages a WIP branch likely needs to be regenerated:
|
||||
|
||||
```bash
|
||||
git diff --name-only | xargs -n1 dirname | sort -u | grep ^packages/
|
||||
```
|
||||
|
||||
## Commit Format
|
||||
|
||||
Follow [Conventional Commits](https://www.conventionalcommits.org/) with `--signoff`:
|
||||
|
||||
```bash
|
||||
git commit --signoff -m "type(scope): brief description"
|
||||
```
|
||||
|
||||
**Types:** `feat`, `fix`, `docs`, `style`, `refactor`, `perf`, `test`, `build`, `ci`, `chore`
|
||||
|
||||
**Scopes** (examples — not an exhaustive list; pick the most specific scope that describes the change, and introduce a new one if a genuinely new area needs its own):
|
||||
|
||||
- System, e.g.: `dashboard`, `platform`, `operator`, `cilium`, `kube-ovn`, `linstor`, `fluxcd`, `cluster-api`
|
||||
- Apps, e.g.: `postgres`, `mariadb`, `redis`, `kafka`, `clickhouse`, `virtual-machine`, `kubernetes`
|
||||
- Other, e.g.: `api`, `hack`, `tests`, `ci`, `docs`, `agents`, `maintenance`
|
||||
|
||||
Breaking changes: append `!` after type/scope (`feat(api)!: ...`) or add a `BREAKING CHANGE:` footer.
|
||||
**Component prefixes:**
|
||||
- System: `[dashboard]`, `[platform]`, `[cilium]`, `[kube-ovn]`, `[linstor]`, `[fluxcd]`, `[cluster-api]`
|
||||
- Apps: `[postgres]`, `[mariadb]`, `[redis]`, `[kafka]`, `[clickhouse]`, `[virtual-machine]`, `[kubernetes]`
|
||||
- Other: `[tests]`, `[ci]`, `[docs]`, `[maintenance]`
|
||||
|
||||
**Examples:**
|
||||
```bash
|
||||
git commit --signoff -m "feat(dashboard): add config hash annotations to restart pods on config changes"
|
||||
git commit --signoff -m "fix(postgres): update operator to version 1.2.3"
|
||||
git commit --signoff -m "docs(contributing): add installation guide"
|
||||
git commit --signoff -m "[dashboard] Add config hash annotations to restart pods on config changes"
|
||||
git commit --signoff -m "[postgres] Update operator to version 1.2.3"
|
||||
git commit --signoff -m "[docs] Add installation guide"
|
||||
```
|
||||
|
||||
## PR Title Auto-Labeling
|
||||
### 3. Rebase on upstream/main (if needed)
|
||||
|
||||
`.github/workflows/pr-labeler.yaml` parses the PR title on `opened`, `edited`, `reopened`, and `synchronize` events and applies labels additively (never removes). The title is expected to follow Conventional Commits — same format as commit messages above.
|
||||
|
||||
**Type → `kind/*`:**
|
||||
|
||||
| type | label |
|
||||
| --------- | ------------------ |
|
||||
| feat | kind/feature |
|
||||
| fix | kind/bug |
|
||||
| docs | kind/documentation |
|
||||
| chore | kind/cleanup |
|
||||
| refactor | kind/cleanup |
|
||||
| style, perf, test, build, ci, revert | (no kind label) |
|
||||
|
||||
**Scope → `area/*`** (full mapping in `.github/workflows/pr-labeler.yaml`):
|
||||
|
||||
| scope (examples) | label |
|
||||
| --- | --- |
|
||||
| agents, ai | area/ai |
|
||||
| api, cozystack-api | area/api |
|
||||
| build | area/build |
|
||||
| ci | area/ci |
|
||||
| dashboard | area/dashboard |
|
||||
| postgres, mariadb, redis, etcd, kafka, clickhouse, postgres-operator, mariadb-operator | area/database |
|
||||
| extra | area/extra |
|
||||
| kubernetes | area/kubernetes |
|
||||
| monitoring, vlogs, vmstack, grafana, workloadmonitor | area/monitoring |
|
||||
| ingress, gateway, vpn, metallb, cilium, kube-ovn, cozy-proxy, ... | area/networking |
|
||||
| platform, bundle, flux, fluxcd, cluster-api, talos, installer, cozyctl, cozystack-engine, cozy-lib | area/platform |
|
||||
| backport, release | area/release |
|
||||
| seaweedfs, bucket, linstor, velero, harbor, backups | area/storage |
|
||||
| tests, e2e | area/testing |
|
||||
| kubevirt, cdi, vmi, vm-import, virtual-machine, hami, gpu-operator | area/virtualization |
|
||||
|
||||
**Special handling:**
|
||||
|
||||
- `[Backport release-1.x]` prefix is stripped before parsing; `area/release` and `backport` labels are added.
|
||||
- Composite scope (`feat(platform, system, apps): ...`) — each comma-separated part is mapped independently.
|
||||
- `!` after type or `BREAKING CHANGE:` footer in the body → `kind/breaking-change`.
|
||||
- Unmapped scope or non-conventional title → `area/uncategorized` (signals the PR needs manual area selection).
|
||||
- Bracket-style fallback (`[scope] description`) maps `scope` → `area/*` but cannot infer `kind/*`.
|
||||
|
||||
### AI Agent Attribution
|
||||
|
||||
When an AI agent authors or materially assists with a commit, add an `Assisted-By:` trailer naming the model:
|
||||
|
||||
```text
|
||||
Assisted-By: Claude <noreply@anthropic.com>
|
||||
Assisted-By: GPT-5 <noreply@openai.com>
|
||||
Assisted-By: Gemini <noreply@google.com>
|
||||
```
|
||||
|
||||
This sits alongside the `Signed-off-by:` trailer produced by `--signoff`. Use one trailer per model if multiple contributed.
|
||||
|
||||
## Rebasing on upstream/main
|
||||
|
||||
If the branch has extra commits, clean it up:
|
||||
If your branch has extra commits, clean it up:
|
||||
|
||||
```bash
|
||||
# Fetch latest
|
||||
git fetch upstream
|
||||
|
||||
# Create clean branch from upstream/main
|
||||
git checkout -b my-feature upstream/main
|
||||
|
||||
# Cherry-pick only your commit
|
||||
git cherry-pick <your-commit-hash>
|
||||
git push -f origin my-feature
|
||||
|
||||
# Force push to your branch
|
||||
git push -f origin my-feature:my-branch-name
|
||||
```
|
||||
|
||||
## Pull Request Body
|
||||
### 4. Push Your Branch
|
||||
|
||||
Fill in the template at [`.github/PULL_REQUEST_TEMPLATE.md`](../../.github/PULL_REQUEST_TEMPLATE.md). It includes the required `release-note` block.
|
||||
```bash
|
||||
git push origin <branch-name>
|
||||
```
|
||||
|
||||
Create the PR with `gh pr create --title "type(scope): brief description" --body-file <file>`.
|
||||
### 5. Create Pull Request
|
||||
|
||||
## Fetching Unresolved Review Comments
|
||||
Write the PR body to a temporary file:
|
||||
|
||||
Cozystack uses GitHub review threads with resolution status. Only unresolved threads are actionable — resolved threads are already handled.
|
||||
```bash
|
||||
cat > /tmp/pr_body.md << 'EOF'
|
||||
## What this PR does
|
||||
|
||||
The REST endpoint `/pulls/{pr}/reviews` returns review summaries, not individual review comments. Use the GraphQL API to access `reviewThreads` with `isResolved` status:
|
||||
Brief description of the changes.
|
||||
|
||||
Changes:
|
||||
- Change 1
|
||||
- Change 2
|
||||
|
||||
### Release note
|
||||
|
||||
```release-note
|
||||
[component] Description for changelog
|
||||
```
|
||||
EOF
|
||||
```
|
||||
|
||||
Create the PR:
|
||||
|
||||
```bash
|
||||
gh pr create --title "[component] Brief description" --body-file /tmp/pr_body.md
|
||||
```
|
||||
|
||||
Clean up:
|
||||
|
||||
```bash
|
||||
rm /tmp/pr_body.md
|
||||
```
|
||||
|
||||
## Addressing AI Bot Reviewer Comments
|
||||
|
||||
When the user asks to fix comments from AI bot reviewers (like Qodo, Copilot, etc.):
|
||||
|
||||
### 1. Get PR Comments
|
||||
|
||||
View all comments on the pull request:
|
||||
|
||||
```bash
|
||||
gh pr view <PR-number> --comments
|
||||
```
|
||||
|
||||
Or for the current branch:
|
||||
|
||||
```bash
|
||||
gh pr view --comments
|
||||
```
|
||||
|
||||
### 2. Review Each Comment Carefully
|
||||
|
||||
**Important**: Do NOT blindly apply all suggestions. Each comment should be evaluated:
|
||||
|
||||
- **Consider context** - Does the suggestion make sense for this specific case?
|
||||
- **Check project conventions** - Does it align with Cozystack patterns?
|
||||
- **Evaluate impact** - Will this improve code quality or introduce issues?
|
||||
- **Question validity** - AI bots can be wrong or miss context
|
||||
|
||||
**When to apply:**
|
||||
- ✅ Legitimate bugs or security issues
|
||||
- ✅ Clear improvements to code quality
|
||||
- ✅ Better error handling or edge cases
|
||||
- ✅ Conformance to project conventions
|
||||
|
||||
**When to skip:**
|
||||
- ❌ Stylistic preferences that don't match project style
|
||||
- ❌ Over-engineering simple code
|
||||
- ❌ Changes that break existing patterns
|
||||
- ❌ Suggestions that show misunderstanding of the code
|
||||
|
||||
### 3. Apply Valid Fixes
|
||||
|
||||
Make changes addressing the valid comments. Use your judgment.
|
||||
|
||||
### 4. Leave Changes Uncommitted
|
||||
|
||||
**Critical**: Do NOT commit or push the changes automatically.
|
||||
|
||||
Leave the changes in the working directory so the user can:
|
||||
- Review the fixes
|
||||
- Decide whether to commit them
|
||||
- Make additional adjustments if needed
|
||||
|
||||
```bash
|
||||
# After making changes, show status but DON'T commit
|
||||
git status
|
||||
git diff
|
||||
```
|
||||
|
||||
The user will commit and push when ready.
|
||||
|
||||
## Code Review Comments
|
||||
|
||||
When asked to fix code review comments, **always work only with unresolved (open) comments**. Resolved comments should be ignored as they have already been addressed.
|
||||
|
||||
### Getting Unresolved Review Comments
|
||||
|
||||
Use GitHub GraphQL API to fetch only unresolved review comments from a pull request:
|
||||
|
||||
```bash
|
||||
gh api graphql -F owner=cozystack -F repo=cozystack -F pr=<PR_NUMBER> -f query='
|
||||
|
|
@ -162,7 +189,27 @@ query($owner: String!, $repo: String!, $pr: Int!) {
|
|||
}' --jq '.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved == false) | .comments.nodes[]'
|
||||
```
|
||||
|
||||
Compact one-line variant:
|
||||
### Filtering for Unresolved Comments
|
||||
|
||||
The key filter is `select(.isResolved == false)` which ensures only unresolved review threads are processed. Each thread can contain multiple comments, but if the thread is resolved, all its comments should be ignored.
|
||||
|
||||
### Working with Review Comments
|
||||
|
||||
1. **Fetch unresolved comments** using the GraphQL query above
|
||||
2. **Parse the results** to identify:
|
||||
- File path (`path`)
|
||||
- Line number (`line` or `originalLine`)
|
||||
- Comment text (`bodyText`)
|
||||
- Author (`author.login`)
|
||||
3. **Address each unresolved comment** by:
|
||||
- Locating the relevant code section
|
||||
- Making the requested changes
|
||||
- Ensuring the fix addresses the concern raised
|
||||
4. **Do NOT process resolved comments** - they have already been handled
|
||||
|
||||
### Example: Compact List of Unresolved Comments
|
||||
|
||||
For a quick overview of unresolved comments:
|
||||
|
||||
```bash
|
||||
gh api graphql -F owner=cozystack -F repo=cozystack -F pr=<PR_NUMBER> -f query='
|
||||
|
|
@ -186,3 +233,43 @@ query($owner: String!, $repo: String!, $pr: Int!) {
|
|||
}
|
||||
}' --jq '.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved == false) | .comments.nodes[] | "\(.path):\(.line // "N/A") - \(.author.login): \(.bodyText[:150])"'
|
||||
```
|
||||
|
||||
### Important Notes
|
||||
|
||||
- **REST API limitation**: The REST endpoint `/pulls/{pr}/reviews` returns review summaries, not individual review comments. Use GraphQL API for accessing `reviewThreads` with `isResolved` status.
|
||||
- **Thread-based resolution**: Comments are organized in threads. If a thread is resolved (`isResolved: true`), ignore all comments in that thread.
|
||||
- **Always filter**: Never process comments from resolved threads, even if they appear in the results.
|
||||
|
||||
### Example Workflow
|
||||
|
||||
```bash
|
||||
# Get PR comments
|
||||
gh pr view 1234 --comments
|
||||
|
||||
# Review comments and identify valid ones
|
||||
# Make necessary changes to address valid comments
|
||||
# ... edit files ...
|
||||
|
||||
# Show what was changed (but don't commit)
|
||||
git status
|
||||
git diff
|
||||
|
||||
# Tell the user what was fixed and what was skipped
|
||||
```
|
||||
|
||||
## Git Permissions
|
||||
|
||||
Request these permissions when needed:
|
||||
- `git_write` - For commit, rebase, cherry-pick, branch operations
|
||||
- `network` - For push, fetch, pull operations
|
||||
|
||||
## Common Issues
|
||||
|
||||
**PR has extra commits?**
|
||||
→ Rebase on `upstream/main` and cherry-pick only your commits
|
||||
|
||||
**Wrong commit message?**
|
||||
→ `git commit --amend --signoff -m "[correct] message"` then `git push -f`
|
||||
|
||||
**Need to update PR?**
|
||||
→ `gh pr edit <number> --body "new description"`
|
||||
|
|
|
|||
|
|
@ -78,18 +78,11 @@ packages/<category>/<package-name>/
|
|||
- Add proper error handling and structured logging
|
||||
|
||||
### Git Commits
|
||||
- Follow [Conventional Commits](https://www.conventionalcommits.org/) format: `type(scope): description`
|
||||
- Use format: `[component] Description`
|
||||
- Always use `--signoff` flag
|
||||
- Reference PR numbers when available
|
||||
- Keep commits atomic and focused
|
||||
|
||||
### PackageSource CRD upgrade policy
|
||||
|
||||
Each component in a `PackageSource` may set `install.upgradeCRDs` to control how CRDs from the chart's `crds/` directory are handled on `HelmRelease` upgrades. Allowed values: `Skip` (default — helm-controller does not touch CRDs on upgrade), `Create` (create new CRDs only), `CreateReplace` (create new and overwrite existing).
|
||||
|
||||
Set `upgradeCRDs: CreateReplace` for operators whose upstream regularly adds new CRDs between versions (etcd-operator, cnpg, kubevirt, kamaji). Without it, new CRDs from a chart bump do not land on existing clusters — only fresh installs get them.
|
||||
|
||||
Do **not** set `CreateReplace` blindly: it overwrites every CRD in `crds/` and can cause silent data loss if upstream drops a field from a CRD that has live objects. Only enable it for operators whose schema evolution is additive-only. When in doubt, leave it unset and apply new CRDs manually.
|
||||
- Follow conventional commit format for changelogs
|
||||
|
||||
### Documentation
|
||||
|
||||
|
|
|
|||
|
|
@ -1,19 +0,0 @@
|
|||
<!--
|
||||
https://github.com/cozystack/cozystack/releases/tag/v1.1.6
|
||||
-->
|
||||
|
||||
## Fixes
|
||||
|
||||
* **[build] Filter git describe to match only v* tags**: Adds `--match 'v*'` to all `git describe` calls in `hack/common-envs.mk`. The `api/apps/v1alpha1/*` subtags share the same commit as release tags, causing `git describe --exact-match` to pick `api/apps/v1alpha1/vX.Y.Z` instead of `vX.Y.Z`, producing invalid Docker image tags ([**@kvaps**](https://github.com/kvaps) in #2386, #2388).
|
||||
|
||||
## Development, Testing, and CI/CD
|
||||
|
||||
* **[ci] Replace cozystack-bot PAT with cozystack-ci GitHub App**: Replaces the long-lived `cozystack-bot` personal access token with short-lived, scoped tokens from the `cozystack-ci` GitHub App across all release workflows. Improves security and auditability of CI operations ([**@tym83**](https://github.com/tym83) in #2351).
|
||||
|
||||
* **[ci] Replace GH_PAT with cozystack-ci GitHub App token in pull-requests workflow**: Switches the pull-requests release workflow to use the cozystack-ci GitHub App token instead of the personal access token ([**@kvaps**](https://github.com/kvaps) in #2383).
|
||||
|
||||
* **[ci] Use cozystack org noreply email for bot commits**: Updates CI workflows to use the cozystack organization noreply email for bot commits ([**@kvaps**](https://github.com/kvaps) in #2392).
|
||||
|
||||
---
|
||||
|
||||
**Full Changelog**: https://github.com/cozystack/cozystack/compare/v1.1.5...v1.1.6
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
<!--
|
||||
https://github.com/cozystack/cozystack/releases/tag/v1.2.2
|
||||
-->
|
||||
|
||||
## Features and Improvements
|
||||
|
||||
* **[linstor] Update piraeus-server to v1.33.2 with selected backports**: Bumps LINSTOR server from v1.33.1 to v1.33.2 and adds backported patches for improved storage reliability: a stale bitmap adjust retry mechanism for automatic recovery after bitmap attach errors, LUKS2 header sizing and optimal I/O size detection improvements for more reliable disk formatting, and the maintainer implementation backport. All patches verified against upstream v1.33.2 with `git apply --check` and `gradlew compileJava` ([**@kvaps**](https://github.com/kvaps) in #2331, #2377).
|
||||
|
||||
## Fixes
|
||||
|
||||
* **[postgres] Fix system PostgreSQL images to 17.7-standard-trixie**: Hardcodes PostgreSQL 17.7-standard-trixie images for system PostgreSQL instances. This ensures system databases use the correct image variant consistent with the monitoring stack requirements introduced in v1.2.1 ([**@myasnikovdaniil**](https://github.com/myasnikovdaniil) in #2364, #2369).
|
||||
|
||||
* **[cilium] Opt-out of cri-containerd.apparmor.d for nsenter init containers**: On Ubuntu 22.04+, Debian, and other distributions that load the `cri-containerd.apparmor.d` AppArmor profile by default for containerd workloads, the kernel denied `nsenter` namespace entry in cilium-agent init containers (`mount-cgroup`, `apply-sysctl-overwrites`, `clean-cilium-state`), causing the agent to land in `Init:CrashLoopBackOff` and cascading platform failures. Per-container `container.apparmor.security.beta.kubernetes.io` annotations now opt the affected containers out of this profile, applied only on non-Talos cilium variants (`cilium-generic`, `kubeovn-cilium-generic`). The vendored daemonset template is also patched to strip the upstream `semverCompare "<1.30.0"` AppArmor block, preventing duplicate annotation keys. Talos variants are untouched as Talos does not load the AppArmor LSM ([**@lexfrei**](https://github.com/lexfrei) in #2370, #2378).
|
||||
|
||||
* **[virtual-machine] Exclude external VM services from Cilium BPF LB**: Adds the `service.kubernetes.io/service-proxy-name: "cozy-proxy"` label to VM LoadBalancer services when `external: true`, telling Cilium to skip BPF processing entirely for these services. This fixes two issues: inter-tenant connectivity via public LB IPs (Cilium's DNAT caused cross-tenant pod-to-pod flow classification, triggering CiliumClusterwideNetworkPolicy blocks) and WholeIP broken on Cilium 1.19+ (wildcard service drop entries blocked traffic to LB IPs on undeclared ports before it reached netfilter/cozy-proxy). MetalLB L2 advertisement and kube-ovn routing remain unaffected ([**@mattia-eleuteri**](https://github.com/mattia-eleuteri) in #2357, #2361).
|
||||
|
||||
* **[monitoring] Fix infra dashboards missing in default variant**: The default platform variant deploys the monitoring chart to the `cozy-monitoring` namespace, but the dashboard rendering condition introduced in #2197 only checked for `tenant-root`. Infrastructure dashboards were not rendered in the default variant. The `cozy-monitoring` namespace is now included in the rendering condition, consistent with the existing pattern in `vmagent.yaml` ([**@mattia-eleuteri**](https://github.com/mattia-eleuteri) in #2365, #2367).
|
||||
|
||||
* **[build] Filter git describe to match only v* tags**: Adds `--match 'v*'` to all `git describe` calls in `hack/common-envs.mk`. The `api/apps/v1alpha1/*` subtags share the same commit as release tags, causing `git describe --exact-match` to pick `api/apps/v1alpha1/vX.Y.Z` instead of `vX.Y.Z`, producing invalid Docker image tags ([**@kvaps**](https://github.com/kvaps) in #2386, #2389).
|
||||
|
||||
## Development, Testing, and CI/CD
|
||||
|
||||
* **[ci] Replace cozystack-bot PAT with cozystack-ci GitHub App**: Replaces the long-lived `cozystack-bot` personal access token with short-lived, scoped tokens from the `cozystack-ci` GitHub App across all release workflows (`tags.yaml`, `auto-release.yaml`, `pull-requests-release.yaml`). Improves security and auditability of CI operations ([**@tym83**](https://github.com/tym83) in #2351).
|
||||
|
||||
* **[ci] Use cozystack org noreply email for bot commits**: Updates CI workflows to use the cozystack organization noreply email for bot commits ([**@kvaps**](https://github.com/kvaps) in #2392, #2393).
|
||||
|
||||
* **[ci] Replace GH_PAT with cozystack-ci GitHub App token in pull-requests workflow**: Switches the pull-requests release workflow to use the cozystack-ci GitHub App token instead of the personal access token ([**@kvaps**](https://github.com/kvaps) in #2383, #2384).
|
||||
|
||||
## Documentation
|
||||
|
||||
* **[website] Add ApplicationDefinition naming convention reference**: Added reference documentation on ApplicationDefinition naming conventions and how `cozystack-api` resolves kinds to their backing definitions ([**@lexfrei**](https://github.com/lexfrei) in cozystack/website#478).
|
||||
|
||||
* **[website] Document Talos / talosctl / Cozystack version pairing**: Added documentation covering Talos, talosctl, and Cozystack version compatibility matrix for installation ([**@lexfrei**](https://github.com/lexfrei) in cozystack/website#484).
|
||||
|
||||
* **[website] Fix KubeOVN MASTER_NODES example path and key in troubleshooting**: Corrected the MASTER_NODES example path and key in the KubeOVN troubleshooting guide ([**@lexfrei**](https://github.com/lexfrei) in cozystack/website#483).
|
||||
|
||||
* **[website] Prefix bundle package names with cozystack. in v1 examples**: Updated documentation examples to use the correct `cozystack.` prefix for bundle package names in enabled/disabledPackages ([**@lexfrei**](https://github.com/lexfrei) in cozystack/website#482).
|
||||
|
||||
* **[website] Finish isolated-field removal and document opt-in policy labels**: Removed the obsolete `isolated` field from tenant documentation and documented the new opt-in policy labels approach ([**@lexfrei**](https://github.com/lexfrei) in cozystack/website#481).
|
||||
|
||||
* **[website] Add --take-ownership flag and describe networking.* fields**: Added documentation for the `--take-ownership` flag and described the `networking.*` fields in the installation guide ([**@lexfrei**](https://github.com/lexfrei) in cozystack/website#480).
|
||||
|
||||
* **[website] Add bonding (LACP) configuration how-to guide**: Added a guide for configuring network bonding with LACP on Cozystack installations ([**@sircthulhu**](https://github.com/sircthulhu) in cozystack/website#459).
|
||||
|
||||
* **[website] Improve registry mirrors for tenant Kubernetes in air-gapped guide**: Improved documentation for configuring registry mirrors in tenant Kubernetes clusters for air-gapped environments ([**@sircthulhu**](https://github.com/sircthulhu) in cozystack/website#461).
|
||||
|
||||
* **[website] Update backup/restore documentation for VMI/VMDisk**: Updated backup documentation with information related to VM instance and VM disk restore improvements ([**@androndo**](https://github.com/androndo) in cozystack/website#466).
|
||||
|
||||
* **[website] Add updated OpenAPI spec**: Updated the OpenAPI specification for managed applications reference ([**@myasnikovdaniil**](https://github.com/myasnikovdaniil) in cozystack/website#469).
|
||||
|
||||
* **[website] Add OSS Health pages and OpenSSF badge**: Added OSS Health section with OpenSSF Scorecard and Best Practices badge to the website footer ([**@tym83**](https://github.com/tym83) in cozystack/website#470).
|
||||
|
||||
* **[website] Add CozySummit Virtual 2026 program announcement**: Published the CozySummit Virtual 2026 program announcement blog post ([**@tym83**](https://github.com/tym83) in cozystack/website#472).
|
||||
|
||||
* **[website] Add missing release announcements for v0.1–v0.41**: Backfilled missing release announcement blog posts for Cozystack versions v0.1 through v0.41 ([**@tym83**](https://github.com/tym83) in cozystack/website#468).
|
||||
|
||||
* **[talm] Render templates online in apply to resolve lookups**: Fixed talm `apply` command to render templates online, resolving template lookup failures when using modeline templates ([**@myasnikovdaniil**](https://github.com/myasnikovdaniil) in cozystack/talm#119).
|
||||
|
||||
* **[talm] Update default Talos image to v1.12.6**: Updated the default Talos image version to v1.12.6 in talm ([**@kvaps**](https://github.com/kvaps) in cozystack/talm@03e9b6e).
|
||||
|
||||
---
|
||||
|
||||
**Full Changelog**: https://github.com/cozystack/cozystack/compare/v1.2.1...v1.2.2
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
<!--
|
||||
https://github.com/cozystack/cozystack/releases/tag/v1.2.3
|
||||
-->
|
||||
|
||||
# v1.2.3 (2026-04-20)
|
||||
|
||||
A patch release with bug fixes and documentation updates.
|
||||
|
||||
## Features and Improvements
|
||||
|
||||
_No notable features in this patch release._
|
||||
|
||||
## Fixes
|
||||
|
||||
* **fix(kubernetes): set explicit ephemeral-storage on virt-launcher pods**: Prevents VM crashes caused by ephemeral-storage eviction by setting explicit `domain.resources` ephemeral-storage on the VirtualMachine spec. Uses sanitized limits and requests so virt-launcher pods do not inherit too-small namespace defaults. ([**@kvaps**](https://github.com/kvaps) in #2317, backport #2423).
|
||||
|
||||
## Documentation
|
||||
|
||||
* **[website] feat: add Telemetry page under OSS Health section**: Add Telemetry page and initial data seeding to OSS Health docs ([**@tym83**](https://github.com/tym83) in cozystack/website#471).
|
||||
* **[website] Refactor docs versions to major.minor variants**: Move docs to major.minor versioning for v1.x series ([**@myasnikovdaniil**](https://github.com/myasnikovdaniil) in cozystack/website#477).
|
||||
* **[website] docs(tenants): document namespace layout and parent/child derivation** ([**@lexfrei**](https://github.com/lexfrei) in cozystack/website#479).
|
||||
* **[website] docs(tenants): document the checkbox-then-edit-CR customization pattern** ([**@lexfrei**](https://github.com/lexfrei) in cozystack/website#485).
|
||||
* **[website] docs: fix 14 broken links and stale talm anchor across v1 docs** ([**@lexfrei**](https://github.com/lexfrei) in cozystack/website#486).
|
||||
* **[website] fix(og): update social badge image and title** ([**@tym83**](https://github.com/tym83) in cozystack/website#487).
|
||||
* **[website] docs(external-apps): rewrite guide for ApplicationDefinition API** ([**@kitsunoff**](https://github.com/kitsunoff) in cozystack/website#488).
|
||||
* **[website] docs: add CLAUDE.md for AI agent guidance** ([**@myasnikovdaniil**](https://github.com/myasnikovdaniil) in cozystack/website#489).
|
||||
* **[website] fix: update /docs/v1/ redirect to latest v1.2** ([**@myasnikovdaniil**](https://github.com/myasnikovdaniil) in cozystack/website#492).
|
||||
* **[website] fix(ci): add OpenAPI spec download to GitHub Pages build** ([**@myasnikovdaniil**](https://github.com/myasnikovdaniil) in cozystack/website#494).
|
||||
* **[website] feat(blog): add managed PostgreSQL with synchronous replication post** ([**@tym83**](https://github.com/tym83) in cozystack/website#497).
|
||||
* **[website] chore(blog): add images frontmatter for social preview on existing posts** ([**@tym83**](https://github.com/tym83) in cozystack/website#498).
|
||||
* **[website] feat(blog): taxonomies and client-side filter UI** ([**@tym83**](https://github.com/tym83) in cozystack/website#499).
|
||||
* **[website] style(oss-health): add breathing room between navbar and hero** ([**@tym83**](https://github.com/tym83) in cozystack/website#500).
|
||||
|
||||
## Other repositories
|
||||
|
||||
* **[talm] feat(config): migrate to Talos v1.12 multi-document config format**: Upgrade Talos config format and modernize configuration handling ([**@lexfrei**](https://github.com/lexfrei) in cozystack/talm#116).
|
||||
* **[talm] chore(deps): bump dependencies and modernize codebase** ([**@lexfrei**](https://github.com/lexfrei) in cozystack/talm#124).
|
||||
* **[external-apps-example] feat: replace MongoDB example with Minecraft apps from cozylex** ([**@lexfrei**](https://github.com/lexfrei) in cozystack/external-apps-example#2).
|
||||
* **[ansible-cozystack] fix(examples): add v prefix to collection version in requirements.yml** ([**@lexfrei**](https://github.com/lexfrei) in cozystack/ansible-cozystack#23).
|
||||
* **[ansible-cozystack] fix(plugins): replace ansible.utils.ipaddr with stdlib-based test plugin** ([**@lexfrei**](https://github.com/lexfrei) in cozystack/ansible-cozystack#24).
|
||||
* **[ansible-cozystack] feat(examples): comprehensive node prerequisites audit (fixes #19)** ([**@lexfrei**](https://github.com/lexfrei) in cozystack/ansible-cozystack#27).
|
||||
* **[ansible-cozystack] chore(deps): update dependency cozystack.installer to v1.2.3** ([**@app/renovate**](https://github.com/apps/renovate) in cozystack/ansible-cozystack#29).
|
||||
* **[ansible-cozystack] feat(role): expose publishing.externalIPs and tenant-root ingress via role variables** ([**@lexfrei**](https://github.com/lexfrei) in cozystack/ansible-cozystack#30).
|
||||
|
||||
## Contributors
|
||||
|
||||
Thanks to everyone who contributed to this patch release:
|
||||
|
||||
* [**@app/github-actions**](https://github.com/apps/github-actions)
|
||||
* [**@app/renovate**](https://github.com/apps/renovate)
|
||||
* [**@kitsunoff**](https://github.com/kitsunoff)
|
||||
* [**@kvaps**](https://github.com/kvaps)
|
||||
* [**@lexfrei**](https://github.com/lexfrei)
|
||||
* [**@myasnikovdaniil**](https://github.com/myasnikovdaniil)
|
||||
* [**@tym83**](https://github.com/tym83)
|
||||
|
||||
|
||||
**Full Changelog**: https://github.com/cozystack/cozystack/compare/v1.2.2...v1.2.3
|
||||
|
|
@ -1,173 +0,0 @@
|
|||
<!--
|
||||
https://github.com/cozystack/cozystack/releases/tag/v1.3.0-rc.1
|
||||
-->
|
||||
|
||||
# Cozystack v1.3.0-rc.1
|
||||
|
||||
Cozystack v1.3.0-rc.1 is the first release candidate for v1.3.0, bringing **storage-aware scheduling** via the LINSTOR scheduler extender, a managed **LINSTOR GUI** web UI with Keycloak SSO, a **VM Default Images** catalog for out-of-the-box virtual machine provisioning, **WorkloadsReady conditions** with a real-time Events tab in the dashboard, and **cross-namespace VM backup restore** capabilities. Additional highlights include stricter tenant name validation, VM network selector improvements, Keycloak theme injection and SMTP configuration, and a comprehensive host runtime preflight check.
|
||||
|
||||
> **Note:** Fixes marked with *(backported to v1.2.x)* were also included in v1.2.1 or v1.2.2 patch releases.
|
||||
|
||||
## Feature Highlights
|
||||
|
||||
### Storage-Aware Scheduling via LINSTOR Extender
|
||||
|
||||
The `cozystack-scheduler` now calls the **LINSTOR scheduler extender** for storage-locality-aware pod placement. When a pod declares both a `SchedulingClass` and LINSTOR-backed PVCs, the scheduler consults LINSTOR to prefer nodes where volume replicas already exist — reducing cross-node replication traffic and improving I/O latency for storage-heavy workloads ([**@lllamnyp**](https://github.com/lllamnyp) in #2330).
|
||||
|
||||
### LINSTOR GUI: Managed Web UI for Storage Administration
|
||||
|
||||
A new opt-in `linstor-gui` system package deploys **LINBIT's linstor-gui web UI** alongside the LINSTOR controller with mTLS client authentication, non-root security context, and ClusterIP-only service. An optional **Keycloak-protected Ingress** (via oauth2-proxy) can be enabled for SSO-authenticated browser access when OIDC is configured on the platform ([**@myasnikovdaniil**](https://github.com/myasnikovdaniil) in #2382, #2390).
|
||||
|
||||
### VM Default Images: Out-of-the-Box VM Provisioning
|
||||
|
||||
The new `vm-default-images` package provides a curated set of **cluster-wide virtual machine images** (Ubuntu, Debian, CentOS Stream, and others) as pre-populated DataVolumes. The package is opt-in via the `iaas` bundle and defaults to replicated storage for high availability. A companion migration (migration 38) renames legacy `vm-image-*` DataVolumes to the new `vm-default-images-*` naming scheme. The `vm-disk` chart also gains a new "disk" source type for cloning from existing vm-disks in the same namespace ([**@myasnikovdaniil**](https://github.com/myasnikovdaniil) in #2258).
|
||||
|
||||
### WorkloadsReady Condition and Events Tab
|
||||
|
||||
Applications now expose a **WorkloadsReady** condition on their status by querying associated WorkloadMonitor resources, giving operators a single place to check whether all underlying workloads (Deployments, StatefulSets, DaemonSets) are healthy. The dashboard gains a new **Events tab** showing namespace-scoped Kubernetes events for each application, with fallback to `.firstTimestamp` when `.eventTime` is absent. A bug where WorkloadMonitor's `Operational` status was never persisted is also fixed ([**@lexfrei**](https://github.com/lexfrei) in #2356).
|
||||
|
||||
### Cross-Namespace VM Backup Restore
|
||||
|
||||
The backup system now supports **restoring VMInstance backups into a different namespace** (cross-namespace copy restores), with IP/MAC preservation and safe rename semantics. In-place backup/restores for VMDisk and VMInstance are improved: HelmReleases and DataVolumes are properly handled, and Velero failure messages are propagated to the Application status. The backup status structure has been refactored to store underlying resources as a generic opaque JSON object, enabling arbitrary application-specific metadata ([**@androndo**](https://github.com/androndo) in #2251, #2329, #2319).
|
||||
|
||||
## Major Features and Improvements
|
||||
|
||||
* **[api] Reject tenant names with dashes at Create time**: Enforces alphanumeric-only naming for Tenants at the API level, preventing names with hyphens that would silently fail during Helm reconciliation. A corresponding regex tightening and regression test suite hardens the validation ([**@lexfrei**](https://github.com/lexfrei) in #2380).
|
||||
|
||||
* **[platform] Validate computed tenant namespace length**: Rejects Tenant creation when the computed ancestor-chain namespace would exceed the 63-character Kubernetes namespace limit, preventing opaque HelmRelease reconcile errors downstream ([**@lexfrei**](https://github.com/lexfrei) in #2376).
|
||||
|
||||
* **[vm-instance] Rename subnets to networks and add dropdown selector**: Renames the misleading `subnets` field to `networks` in VMInstance for clarity, adds a dropdown selector for available networks in the dashboard form, and includes a migration to copy existing `subnets` values. The old field remains supported for backward compatibility ([**@sircthulhu**](https://github.com/sircthulhu) in #2263).
|
||||
|
||||
* **[keycloak] Enable injecting themes**: Cozystack administrators can now inject custom Keycloak themes via `initContainers` for UI white-labeling and customization ([**@lllamnyp**](https://github.com/lllamnyp) in #2142).
|
||||
|
||||
* **[keycloak-configure] Add email verification and SMTP configuration**: Adds configurable Keycloak settings for user self-registration, email verification, and SMTP server configuration, enabling automated user onboarding flows ([**@BROngineer**](https://github.com/BROngineer) in #2318).
|
||||
|
||||
* **[postgres] Hardcode PostgreSQL 17 for monitoring databases**: Pins PostgreSQL 17.7 images for system databases (Grafana, Alerta, Harbor, Keycloak, SeaweedFS) and adds migration 37 to backfill `spec.version=v17` for existing PostgreSQL resources, preventing CNPG from defaulting to PostgreSQL 18 *(backported to v1.2.1)* ([**@IvanHunters**](https://github.com/IvanHunters) in #2304).
|
||||
|
||||
* **[hack] Add host runtime preflight check**: New `check-host-runtime.sh` script and `make preflight` target that warns operators when a standalone containerd or docker runtime is running alongside the embedded k3s runtime, helping diagnose container runtime conflicts ([**@lexfrei**](https://github.com/lexfrei) in #2371).
|
||||
|
||||
* **[hack] Add check-readiness.sh diagnostic script**: A new diagnostic script for tracking platform reconciliation by checking readiness of Packages, ArtifactGenerators, ExternalArtifacts, and HelmReleases, with support for watch mode and continuous monitoring ([**@myasnikovdaniil**](https://github.com/myasnikovdaniil) in #2294).
|
||||
|
||||
* **[mariadb] Always enable replication for consistent service naming**: MariaDB now always enables replication, creating `-primary`/`-secondary` services even for single-replica instances. This fixes dashboard visibility and backup functionality for single-replica setups ([**@sircthulhu**](https://github.com/sircthulhu) in #2279).
|
||||
|
||||
* **[platform] Prevent installed packages deletion**: Adds `helm.sh/resource-policy: keep` annotation to packages, preventing automatic deletion when packages are disabled and restoring documented behavior *(backported to v1.2.1)* ([**@kvaps**](https://github.com/kvaps) in #2273).
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
* **[cilium] Opt-out of cri-containerd.apparmor.d for nsenter init containers**: Opts cilium-agent init containers out of the `cri-containerd.apparmor.d` AppArmor profile on non-Talos variants, fixing `Init:CrashLoopBackOff` on Ubuntu 22.04+ and Debian *(backported to v1.2.2)* ([**@lexfrei**](https://github.com/lexfrei) in #2370).
|
||||
|
||||
* **[virtual-machine] Exclude external VM services from Cilium BPF LB**: Adds `service-proxy-name: cozy-proxy` label to VM LoadBalancer services, telling Cilium to skip BPF processing. Fixes inter-tenant connectivity via public LB IPs and WholeIP functionality on Cilium 1.19+ *(backported to v1.2.2)* ([**@mattia-eleuteri**](https://github.com/mattia-eleuteri) in #2357).
|
||||
|
||||
* **[monitoring] Fix infra dashboards missing in default variant**: Includes `cozy-monitoring` namespace in the dashboard rendering condition, fixing infrastructure Grafana dashboards not rendering in the default platform variant *(backported to v1.2.2)* ([**@mattia-eleuteri**](https://github.com/mattia-eleuteri) in #2365).
|
||||
|
||||
* **[postgres] Fix system PostgreSQL images to 17.7-standard-trixie**: Normalizes system PostgreSQL image tags to use `17.7-standard-trixie` variant with migration logic for existing CNPG clusters *(backported to v1.2.2)* ([**@myasnikovdaniil**](https://github.com/myasnikovdaniil) in #2364).
|
||||
|
||||
* **[build] Filter git describe to match only v\* tags**: Adds `--match 'v*'` to `git describe` calls, preventing API subtags from being picked up instead of release tags and producing invalid Docker image tags *(backported to v1.2.2)* ([**@kvaps**](https://github.com/kvaps) in #2386).
|
||||
|
||||
* **[platform] Fix resource allocation ratios not propagated to packages**: Restores propagation of CPU, memory, and ephemeral-storage allocation ratios to managed applications and KubeVirt, which were silently ignored since the bundle restructure *(backported to v1.2.1)* ([**@sircthulhu**](https://github.com/sircthulhu) in #2296).
|
||||
|
||||
* **[kubernetes] Set explicit ephemeral-storage on virt-launcher pods**: Sets explicit `domain.resources` with ephemeral-storage on VirtualMachine spec to prevent virt-launcher pods from being evicted due to LimitRange defaults being too low for actual emptyDisk capacity ([**@kvaps**](https://github.com/kvaps) in #2317).
|
||||
|
||||
* **[multus] Pin master CNI to 05-cilium.conflist**: Prevents a boot-time race condition where multus could auto-detect kube-ovn's conflist instead of Cilium's *(backported to v1.2.1)* ([**@kvaps**](https://github.com/kvaps) in #2315).
|
||||
|
||||
* **[multus] Build custom image with DEL cache fix**: Fixes sandbox cleanup deadlock when CNI ADD never completes, preventing stale sandbox name reservations from permanently blocking pod creation *(backported to v1.2.1)* ([**@kvaps**](https://github.com/kvaps) in #2313).
|
||||
|
||||
* **[linstor] Set verify-alg to crc32c**: Prevents DRBD connection failures on kernels where `crct10dif` is unavailable (e.g., Talos v1.12.6 with kernel 6.18.18) *(backported to v1.2.1)* ([**@kvaps**](https://github.com/kvaps) in #2303).
|
||||
|
||||
* **[linstor] Preserve TCP ports during toggle-disk operations**: Fixes TCP port mismatches after toggle-disk operations that could cause DRBD resources to enter StandAlone state *(backported to v1.2.1)* ([**@kvaps**](https://github.com/kvaps) in #2292).
|
||||
|
||||
## Dependencies & Version Updates
|
||||
|
||||
* **[linstor] Update piraeus-server to v1.33.2 with selected backports**: Bumps LINSTOR server from v1.33.1 to v1.33.2 with backported patches for stale bitmap adjust retry, LUKS2 header sizing, and optimal I/O size detection *(backported to v1.2.2)* ([**@kvaps**](https://github.com/kvaps) in #2331).
|
||||
|
||||
* **[kamaji] Update to 26.3.5-edge, drop upstreamed patches**: Updates Kamaji from edge-26.2.4 to 26.3.5-edge and removes two patches accepted upstream. Adds configurable probe tuning and DataStore readiness conditions ([**@myasnikovdaniil**](https://github.com/myasnikovdaniil) in #2260).
|
||||
|
||||
* **[talm] Release v0.23.0, v0.23.1, v0.24.0** (github.com/cozystack/talm): Migrates to the Talos v1.12 multi-document machine config format ([**@lexfrei**](https://github.com/lexfrei) in cozystack/talm#116); fixes template rendering in `apply` command to resolve lookups ([**@myasnikovdaniil**](https://github.com/myasnikovdaniil) in cozystack/talm#119); bumps dependencies and modernizes codebase ([**@lexfrei**](https://github.com/lexfrei) in cozystack/talm#124).
|
||||
|
||||
* **[ansible-cozystack] Release v1.2.1, v1.2.2** (github.com/cozystack/ansible-cozystack): Exposes `publishing.externalIPs` and tenant-root ingress via role variables ([**@lexfrei**](https://github.com/lexfrei) in cozystack/ansible-cozystack#30); adds comprehensive node prerequisites audit ([**@lexfrei**](https://github.com/lexfrei) in cozystack/ansible-cozystack#27); replaces `ansible.utils.ipaddr` with a stdlib-based test plugin ([**@lexfrei**](https://github.com/lexfrei) in cozystack/ansible-cozystack#24).
|
||||
|
||||
## Security
|
||||
|
||||
* **docs: add SECURITY.md**: Adds vulnerability reporting procedures, disclosure expectations, and supported release lines ([**@kvaps**](https://github.com/kvaps) in #2230).
|
||||
|
||||
* **docs: add OpenSSF Best Practices badge to README**: Adds the OpenSSF Best Practices passing badge to the project README ([**@lexfrei**](https://github.com/lexfrei) in #2320).
|
||||
|
||||
## Development, Testing, and CI/CD
|
||||
|
||||
* **[ci] Replace cozystack-bot PAT with cozystack-ci GitHub App**: Replaces the long-lived cozystack-bot personal access token with short-lived, scoped tokens from the cozystack-ci GitHub App across all CI release workflows ([**@tym83**](https://github.com/tym83) in #2351; [**@kvaps**](https://github.com/kvaps) in #2383, #2392).
|
||||
|
||||
* **[ci] Add Gemini Code Assist and CodeRabbit configuration**: Adds repository-level configuration for AI code reviewers with ignore patterns for vendored/generated code and incremental review settings ([**@lexfrei**](https://github.com/lexfrei) in #2385).
|
||||
|
||||
* **[ci] Make tags workflow idempotent on re-runs**: Fixes CI to force-update API subtags and handle re-runs gracefully ([**@kvaps**](https://github.com/kvaps)).
|
||||
|
||||
* **[tests] Fix Kafka E2E test timeout and retry race condition**: Increases Kafka E2E test timeout from 60s to 300s and fixes a retry race condition where `kubectl apply` could hit a still-deleting resource ([**@lexfrei**](https://github.com/lexfrei) in #2358).
|
||||
|
||||
* **docs: adopt Conventional Commits for commit and PR titles**: Standardizes commit and PR title format to `type(scope): description` across all contributing docs and the PR template ([**@lexfrei**](https://github.com/lexfrei) in #2395).
|
||||
|
||||
* **docs(ci): require screenshots for UI changes in PR template**: Adds a mandatory screenshots section to the PR template for UI-related changes ([**@kitsunoff**](https://github.com/kitsunoff) in #2407).
|
||||
|
||||
## Documentation
|
||||
|
||||
* **[website] Add ApplicationDefinition naming convention reference**: Documents how `cozystack-api` resolves kinds to their backing definitions ([**@lexfrei**](https://github.com/lexfrei) in cozystack/website#478).
|
||||
|
||||
* **[website] Document Talos / talosctl / Cozystack version pairing**: Adds version compatibility matrix for installation ([**@lexfrei**](https://github.com/lexfrei) in cozystack/website#484).
|
||||
|
||||
* **[website] Document namespace layout and parent/child derivation**: Explains tenant namespace hierarchy and parent/child namespace derivation rules ([**@lexfrei**](https://github.com/lexfrei) in cozystack/website#479).
|
||||
|
||||
* **[website] Document the checkbox-then-edit-CR customization pattern for tenants**: Describes the workflow for customizing tenant settings via the CR after initial checkbox-based creation ([**@lexfrei**](https://github.com/lexfrei) in cozystack/website#485).
|
||||
|
||||
* **[website] Add custom Keycloak themes documentation**: Covers the theme image contract, configuration, `imagePullSecrets`, and theme activation in the Keycloak admin console ([**@lexfrei**](https://github.com/lexfrei) in cozystack/website#463).
|
||||
|
||||
* **[website] Add bonding (LACP) configuration how-to guide**: Covers network bonding configuration for Cozystack installations ([**@sircthulhu**](https://github.com/sircthulhu) in cozystack/website#459).
|
||||
|
||||
* **[website] Improve registry mirrors for tenant Kubernetes in air-gapped guide**: Improved documentation for configuring registry mirrors in air-gapped environments ([**@sircthulhu**](https://github.com/sircthulhu) in cozystack/website#461).
|
||||
|
||||
* **[website] Rewrite guide for ApplicationDefinition API (external-apps)**: Comprehensive rewrite of the external apps guide using the ApplicationDefinition API ([**@kitsunoff**](https://github.com/kitsunoff) in cozystack/website#488).
|
||||
|
||||
* **[website] Add documentation for Go types usage**: Guide for using generated Go types for Cozystack managed applications as a Go module ([**@myasnikovdaniil**](https://github.com/myasnikovdaniil) in cozystack/website#465).
|
||||
|
||||
* **[website] Update backup/restore documentation for VMI/VMDisk**: Updated backup documentation with VM instance and VM disk restore improvements ([**@androndo**](https://github.com/androndo) in cozystack/website#466).
|
||||
|
||||
* **[website] Add OSS Health pages and OpenSSF badge**: Added OSS Health section with OpenSSF Scorecard and Best Practices badge to the website ([**@tym83**](https://github.com/tym83) in cozystack/website#470).
|
||||
|
||||
* **[website] Add CozySummit Virtual 2026 program announcement**: Published the CozySummit Virtual 2026 program announcement blog post ([**@tym83**](https://github.com/tym83) in cozystack/website#472).
|
||||
|
||||
* **[website] Add missing release announcements for v0.1–v0.41**: Backfilled missing release announcement blog posts for historical Cozystack versions ([**@tym83**](https://github.com/tym83) in cozystack/website#468).
|
||||
|
||||
* **[website] Fix broken links and stale anchors across v1 docs**: Fixes 14 broken links and stale talm anchors ([**@lexfrei**](https://github.com/lexfrei) in cozystack/website#486).
|
||||
|
||||
* **[website] Prefix bundle package names with cozystack. in v1 examples**: Corrects package naming in documentation examples ([**@lexfrei**](https://github.com/lexfrei) in cozystack/website#482).
|
||||
|
||||
* **[website] Finish isolated-field removal and document opt-in policy labels**: Removes obsolete `isolated` field from tenant documentation and documents the new approach ([**@lexfrei**](https://github.com/lexfrei) in cozystack/website#481).
|
||||
|
||||
* **[website] Add --take-ownership flag and describe networking.* fields**: Documents the `--take-ownership` flag and `networking.*` fields in the installation guide ([**@lexfrei**](https://github.com/lexfrei) in cozystack/website#480).
|
||||
|
||||
* **[website] Fix KubeOVN MASTER_NODES example path and key in troubleshooting**: Corrects the MASTER_NODES example path ([**@lexfrei**](https://github.com/lexfrei) in cozystack/website#483).
|
||||
|
||||
* **[external-apps-example] Replace MongoDB example with Minecraft apps**: Refactors the external apps example to use ApplicationDefinition API with Minecraft server applications ([**@lexfrei**](https://github.com/lexfrei) in cozystack/external-apps-example#2).
|
||||
|
||||
## Governance
|
||||
|
||||
* **Add Mattia Eleuteri ([@mattia-eleuteri](https://github.com/mattia-eleuteri)) as Maintainer**: CSI, Storage, Networking & Security ([**@tym83**](https://github.com/tym83) in #2345).
|
||||
|
||||
* **Add Matthieu Robin ([@matthieu-robin](https://github.com/matthieu-robin)) as Maintainer**: Managed applications, platform quality, and benchmarking ([**@tym83**](https://github.com/tym83) in #2346).
|
||||
|
||||
## Contributors
|
||||
|
||||
We'd like to thank all contributors who made this release possible:
|
||||
|
||||
* [**@androndo**](https://github.com/androndo)
|
||||
* [**@BROngineer**](https://github.com/BROngineer)
|
||||
* [**@IvanHunters**](https://github.com/IvanHunters)
|
||||
* [**@kitsunoff**](https://github.com/kitsunoff)
|
||||
* [**@kvaps**](https://github.com/kvaps)
|
||||
* [**@lexfrei**](https://github.com/lexfrei)
|
||||
* [**@lllamnyp**](https://github.com/lllamnyp)
|
||||
* [**@mattia-eleuteri**](https://github.com/mattia-eleuteri)
|
||||
* [**@myasnikovdaniil**](https://github.com/myasnikovdaniil)
|
||||
* [**@sircthulhu**](https://github.com/sircthulhu)
|
||||
* [**@tym83**](https://github.com/tym83)
|
||||
|
||||
---
|
||||
|
||||
**Full Changelog**: https://github.com/cozystack/cozystack/compare/v1.2.0...v1.3.0-rc.1
|
||||
|
|
@ -1,238 +0,0 @@
|
|||
<!--
|
||||
https://github.com/cozystack/cozystack/releases/tag/v1.3.0
|
||||
-->
|
||||
|
||||
# Cozystack v1.3.0
|
||||
|
||||
Cozystack v1.3.0 brings **storage-aware pod scheduling** via a LINSTOR scheduler extender, a managed **LINSTOR GUI** web console with Keycloak SSO, a curated **VM Default Images** catalog for out-of-the-box virtual-machine provisioning, a new **WorkloadsReady / Events** observability surface with S3 bucket metering, and **cross-namespace VMInstance backup restore** with a full **RestoreJob dashboard** flow. The release also ships stricter tenant-name validation, VMInstance network-selector improvements, Keycloak theme injection and SMTP configuration, a host-runtime preflight check, and rolls up every fix from the v1.2.1 → v1.2.4 patch line.
|
||||
|
||||
> **Note:** Items marked *(backported to v1.2.x)* were also shipped in v1.2.1, v1.2.2, v1.2.3, or v1.2.4 patch releases.
|
||||
|
||||
## Feature Highlights
|
||||
|
||||
### Storage-Aware Scheduling via the LINSTOR Extender
|
||||
|
||||
The `cozystack-scheduler` now calls a **LINSTOR scheduler extender** for storage-locality-aware pod placement. When a pod declares both a `SchedulingClass` and LINSTOR-backed PVCs, the scheduler consults LINSTOR to prefer nodes where volume replicas already exist — reducing cross-node replication traffic and improving I/O latency for storage-heavy workloads such as databases, object stores, and VMs.
|
||||
|
||||
The integration builds on the existing `SchedulingClass` tenant workload placement system introduced in v1.2.0 and requires no tenant-side configuration — workloads simply benefit once a SchedulingClass is assigned. Administrators can mix storage locality with the existing data-center / hardware-generation constraints defined on SchedulingClass CRs ([**@lllamnyp**](https://github.com/lllamnyp) in #2330).
|
||||
|
||||
### LINSTOR GUI: Managed Web Console for Storage Administration
|
||||
|
||||
A new opt-in `linstor-gui` system package deploys **LINBIT's linstor-gui web UI** alongside the LINSTOR controller with mTLS client authentication, non-root security context, and a ClusterIP-only service by default. When OIDC is configured on the platform, an optional **Keycloak-protected Ingress** (via oauth2-proxy) exposes the UI for browser access. Access is restricted to members of the `cozystack-cluster-admin` Keycloak group, consistent with host-cluster admin RBAC, and the gatekeeper blocks in-app LINSTOR authentication setup at the nginx proxy layer so the managed configuration cannot be subverted through the UI.
|
||||
|
||||
Operators who prefer CLI access keep the existing `linstor` command; the GUI is strictly additive and stays disabled by default ([**@myasnikovdaniil**](https://github.com/myasnikovdaniil) in #2382, #2390, #2415, #2419).
|
||||
|
||||
### VM Default Images: Out-of-the-Box VM Provisioning
|
||||
|
||||
The new `vm-default-images` package provides a curated set of **cluster-wide virtual-machine images** (Ubuntu, Debian, CentOS Stream, and others) as pre-populated DataVolumes, so tenants can provision VMs against well-known base images without first having to upload them. The package is opt-in via the `iaas` bundle and defaults to replicated storage for high availability. Migration 38 renames legacy `vm-image-*` DataVolumes to the new `vm-default-images-*` naming scheme, and the `vm-disk` chart gains a new "disk" source type for cloning from existing vm-disks in the same namespace ([**@myasnikovdaniil**](https://github.com/myasnikovdaniil) in #2258).
|
||||
|
||||
### Application Observability: WorkloadsReady, Events, and S3 Bucket Metering
|
||||
|
||||
Applications now expose a **WorkloadsReady** condition on their status by querying associated WorkloadMonitor resources, giving operators a single place to check whether all underlying workloads (Deployments, StatefulSets, DaemonSets, PVCs) are healthy. The dashboard gains a new **Events tab** showing namespace-scoped Kubernetes events per application, with fallback to `.firstTimestamp` when `.eventTime` is absent. A long-standing bug where WorkloadMonitor's `Operational` status was never persisted is fixed in the same change ([**@lexfrei**](https://github.com/lexfrei) in #2356).
|
||||
|
||||
The WorkloadMonitor reconciler is extended to track **COSI BucketClaim** objects as first-class Workloads, and the bucket controller now queries SeaweedFS logical and physical bucket-size metrics from VictoriaMetrics via a namespace-scoped monitoring endpoint, enabling S3 billing integration on par with Pods and PVCs ([**@kitsunoff**](https://github.com/kitsunoff) in #2391). Workloads are also enriched with `workloads.cozystack.io/resource-preset` and source-object labels so downstream billing pipelines can correlate monitors with the tenant preset that produced them ([**@androndo**](https://github.com/androndo) in #2416).
|
||||
|
||||
### Cross-Namespace VM Backup Restore and RestoreJob Dashboard
|
||||
|
||||
The backup system now supports **restoring VMInstance backups into a different namespace** (cross-namespace copy restores) with IP/MAC preservation and safe rename semantics. In-place backup and restore flows for VMDisk and VMInstance are improved: HelmReleases and DataVolumes are properly handled, and Velero failure messages are propagated to the Application status. The backup status structure has been refactored to store underlying resources as a generic opaque JSON object, enabling arbitrary application-specific metadata without status-schema churn ([**@androndo**](https://github.com/androndo) in #2251, #2319, #2329).
|
||||
|
||||
The dashboard now ships a complete **RestoreJob experience**: list view, details page, create form, and sidebar entry, with a "Same as backup" fallback rendering when `spec.targetApplicationRef` is omitted. Non-CRD-backed sidebar factories (`kube-*`, `plan`, `backupjob`, `backup`, `restorejob`) are marked static so they pick up consistent managed-by labels across reconciles ([**@myasnikovdaniil**](https://github.com/myasnikovdaniil) in #2437).
|
||||
|
||||
## Major Features and Improvements
|
||||
|
||||
* **[api] Reject tenant names with dashes at Create time**: Enforces alphanumeric-only naming for Tenants at the API level, preventing names with hyphens that would silently fail during Helm reconciliation. A corresponding regex tightening and regression test suite hardens the validation ([**@lexfrei**](https://github.com/lexfrei) in #2380).
|
||||
|
||||
* **[platform] Validate computed tenant namespace length**: Rejects Tenant creation when the computed ancestor-chain namespace would exceed the 63-character Kubernetes namespace limit, preventing opaque HelmRelease reconcile errors downstream ([**@lexfrei**](https://github.com/lexfrei) in #2376).
|
||||
|
||||
* **[vm-instance] Rename subnets to networks and add dropdown selector**: Renames the misleading `subnets` field to `networks` in VMInstance for clarity, adds a dropdown selector for available networks in the dashboard form, and includes migration 36 to copy existing `subnets` values. The old field remains supported for backward compatibility ([**@sircthulhu**](https://github.com/sircthulhu) in #2263).
|
||||
|
||||
* **[keycloak] Enable injecting themes**: Cozystack administrators can now inject custom Keycloak themes via `initContainers` for UI white-labeling and customization ([**@lllamnyp**](https://github.com/lllamnyp) in #2142).
|
||||
|
||||
* **[keycloak-configure] Add email verification and SMTP configuration**: Adds configurable Keycloak settings for user self-registration, email verification, and SMTP server configuration, enabling automated user onboarding flows ([**@BROngineer**](https://github.com/BROngineer) in #2318).
|
||||
|
||||
* **[postgres] Pin system PostgreSQL to 17.7-standard-trixie**: Pins the PostgreSQL image for system databases (Grafana, Alerta, Harbor, Keycloak, SeaweedFS) to `17.7-standard-trixie` across chart templates and `values.yaml`, and ships migration 37 to patch existing CNPG Cluster `imageName` fields to the same variant (handling unset, any PG 17 tag, and bare-version tags). This prevents CNPG from defaulting to PostgreSQL 18 and locks system databases to the trixie variant consistent with the monitoring stack requirements *(related backports shipped in v1.2.1 via #2309 and v1.2.2 via #2364)* ([**@myasnikovdaniil**](https://github.com/myasnikovdaniil) in #2369).
|
||||
|
||||
* **[platform] Prevent installed packages deletion**: Adds the `helm.sh/resource-policy: keep` annotation to platform packages so disabling a package no longer triggers automatic Helm deletion, restoring the documented behavior where operators must explicitly delete a package *(backported to v1.2.1)* ([**@kvaps**](https://github.com/kvaps) in #2273).
|
||||
|
||||
* **[mariadb] Always enable replication for consistent service naming**: MariaDB now always enables replication, creating `-primary`/`-secondary` services even for single-replica instances. This fixes dashboard visibility and backup functionality for single-replica setups ([**@sircthulhu**](https://github.com/sircthulhu) in #2279).
|
||||
|
||||
* **[hack] Add host runtime preflight check**: New `check-host-runtime.sh` script and `make preflight` target that warns operators when a standalone containerd or docker runtime is running alongside the embedded k3s runtime, helping diagnose container-runtime conflicts early in an installation ([**@lexfrei**](https://github.com/lexfrei) in #2371).
|
||||
|
||||
* **[hack] Add check-readiness.sh diagnostic script**: A new diagnostic script for tracking platform reconciliation by checking readiness of Packages, ArtifactGenerators, ExternalArtifacts, and HelmReleases, with support for watch mode and continuous monitoring ([**@myasnikovdaniil**](https://github.com/myasnikovdaniil) in #2294).
|
||||
|
||||
* **[platform] Add resourcePreset labels to WorkloadMonitor labels**: WorkloadMonitor labels with the `workloads.cozystack.io/` prefix are now propagated onto created Workloads; created Workloads always include the reserved `workloads.cozystack.io/monitor` label, and Helm app charts add `workloads.cozystack.io/resource-preset` metadata to WorkloadMonitor manifests, enabling downstream billing pipelines to correlate monitors with the tenant preset that produced them ([**@androndo**](https://github.com/androndo) in #2416).
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
* **[platform] Migrate ACME HTTP-01 to ingressClassName API**: Switches ACME HTTP-01 issuance from the deprecated `acme.cert-manager.io/http01-ingress-class` annotation to the modern `ingressClassName` field on `ClusterIssuer` and solver pods. Previously, ClusterIssuers referenced a non-existent `nginx` class while each Ingress individually overrode it via annotation — producing `ingressClassName and class cannot be set at the same time` errors when tenants attempted to migrate to the modern field. The migration is atomic: both the ClusterIssuer and consuming Ingresses are updated together *(backported to v1.2.4)* ([**@myasnikovdaniil**](https://github.com/myasnikovdaniil) in #2436).
|
||||
|
||||
* **[harbor] Remove incorrect tenant module flags**: Harbor is a PaaS service, not a tenant module. Incorrect `spec.dashboard.module: true` and `internal.cozystack.io/tenantmodule` flags caused Harbor to appear in the sidebar "Modules" section and be misclassified by controllers handling tenant modules. The flags are now removed so Harbor is displayed in its proper PaaS category and is no longer treated as a tenant-scoped HelmRelease ([**@kvaps**](https://github.com/kvaps) in #2444).
|
||||
|
||||
* **[kube-ovn] Resolve kubeovn-plunger RBAC forbidden on deployments**: Grants `kube-ovn-plunger` the RBAC needed to list Deployments so it can reconcile `ovn-central`, fixing `deployments.apps is forbidden` errors in `cozy-kubeovn` ([**@kvaps**](https://github.com/kvaps) in #2441).
|
||||
|
||||
* **[cilium] Opt-out of cri-containerd.apparmor.d for nsenter init containers**: Opts cilium-agent init containers out of the `cri-containerd.apparmor.d` AppArmor profile on non-Talos variants (`cilium-generic`, `kubeovn-cilium-generic`), fixing `Init:CrashLoopBackOff` on Ubuntu 22.04+ and Debian where the profile denies `nsenter` namespace entry. Talos variants are untouched as Talos does not load the AppArmor LSM *(backported to v1.2.2)* ([**@lexfrei**](https://github.com/lexfrei) in #2370).
|
||||
|
||||
* **[virtual-machine] Exclude external VM services from Cilium BPF LB**: Adds the `service.kubernetes.io/service-proxy-name: cozy-proxy` label to VM LoadBalancer services with `external: true`, telling Cilium to skip BPF processing entirely. Fixes inter-tenant connectivity via public LB IPs (Cilium's DNAT caused cross-tenant pod-to-pod flow classification, triggering CiliumClusterwideNetworkPolicy blocks) and restores WholeIP behavior on Cilium 1.19+ where wildcard service drop entries previously blocked traffic to LB IPs on undeclared ports *(backported to v1.2.2)* ([**@mattia-eleuteri**](https://github.com/mattia-eleuteri) in #2357).
|
||||
|
||||
* **[monitoring] Fix infra dashboards missing in default variant**: Includes the `cozy-monitoring` namespace in the dashboard rendering condition, fixing infrastructure Grafana dashboards not rendering in the default platform variant (only the `tenant-root` namespace was previously checked) *(backported to v1.2.2)* ([**@mattia-eleuteri**](https://github.com/mattia-eleuteri) in #2365).
|
||||
|
||||
* **[build] Filter git describe to match only v* tags**: Adds `--match 'v*'` to all `git describe` calls in `hack/common-envs.mk`, preventing the `api/apps/v1alpha1/vX.Y.Z` subtag from being picked up instead of the release tag and producing invalid Docker image tags *(backported to v1.2.2)* ([**@kvaps**](https://github.com/kvaps) in #2386).
|
||||
|
||||
* **[platform] Fix resource allocation ratios not propagated to packages**: Restores propagation of `cpuAllocationRatio`, `memoryAllocationRatio`, and `ephemeralStorageAllocationRatio` from `platform/values.yaml` to the `cozystack-values` Secret that managed applications and KubeVirt read, fixing a regression introduced in the bundle restructure that silently ignored operator-configured ratios *(backported to v1.2.1)* ([**@sircthulhu**](https://github.com/sircthulhu) in #2296).
|
||||
|
||||
* **[kubernetes] Set explicit ephemeral-storage on virt-launcher pods**: Sets explicit `domain.resources` ephemeral-storage on the VirtualMachine spec to prevent virt-launcher pods from being evicted because LimitRange defaults were too small for the actual emptyDisk capacity *(backported to v1.2.3)* ([**@kvaps**](https://github.com/kvaps) in #2317).
|
||||
|
||||
* **[multus] Pin master CNI to 05-cilium.conflist**: Prevents a boot-time race where multus could auto-detect kube-ovn's conflist instead of Cilium's, which would cause pods to bypass the Cilium chain entirely and lose their endpoint *(backported to v1.2.1)* ([**@kvaps**](https://github.com/kvaps) in #2315).
|
||||
|
||||
* **[multus] Build custom image with DEL cache fix**: Fixes sandbox cleanup deadlock when CNI ADD never completes, preventing stale sandbox name reservations from permanently blocking pod creation after a node disruption *(backported to v1.2.1)* ([**@kvaps**](https://github.com/kvaps) in #2313).
|
||||
|
||||
* **[linstor] Set verify-alg to crc32c**: Prevents DRBD connection failures on kernels where `crct10dif` is unavailable (e.g., Talos v1.12.6 with kernel 6.18.18) by setting the LINSTOR verify-alg controller default to `crc32c` *(backported to v1.2.1)* ([**@kvaps**](https://github.com/kvaps) in #2303).
|
||||
|
||||
* **[linstor] Preserve TCP ports during toggle-disk operations**: Saves existing TCP ports into the `LayerPayload` before `removeLayerData()` deletes them, preventing DRBD resources from entering StandAlone state when a satellite misses the resulting update *(backported to v1.2.1)* ([**@kvaps**](https://github.com/kvaps) in #2292).
|
||||
|
||||
* **[linstor] Increase satellite startup probe failure threshold**: Raises the LINSTOR satellite `startupProbe` `failureThreshold` from 3 to 30 (30s → 300s) in the `LinstorSatelliteConfiguration` pod template, giving satellites with slow storage initialization enough time to come up without being killed and restarted ([**@Arsolitt**](https://github.com/Arsolitt) in #2425).
|
||||
|
||||
## Security
|
||||
|
||||
* **docs: add SECURITY.md**: Adds vulnerability reporting procedures, disclosure expectations, and supported release lines ([**@kvaps**](https://github.com/kvaps) in #2230).
|
||||
|
||||
* **docs: add OpenSSF Best Practices badge to README**: Adds the OpenSSF Best Practices passing badge to the project README ([**@lexfrei**](https://github.com/lexfrei) in #2320).
|
||||
|
||||
## Dependencies & Version Updates
|
||||
|
||||
* **[kube-ovn] Bump kube-ovn to v1.15.10 with port-group regression fix**: Updates `packages/system/kubeovn` to upstream v1.15.10 (from v1.15.3) and carries a patch for `pkg/controller/pod.go` that preserves a VM LSP's port-group memberships when Kubernetes GCs a completed virt-launcher pod while another virt-launcher pod of the same VM is still running. Without the patch, the destination pod of a successful live migration lost its security groups, network policies, and node-scoped routing until `kube-ovn-controller` was restarted ([**@kvaps**](https://github.com/kvaps) in #2443).
|
||||
|
||||
* **[monitoring] Upgrade victoria-metrics-operator to v0.68.4**: Bumps the vendored `victoria-metrics-operator` Helm chart from 0.59.1 to 0.61.0 (operator appVersion v0.68.1 → v0.68.4), picking up upstream fixes for `VMPodScrape` port routing on VMAgent/VLAgent and `StatefulSet` pod deletion (not eviction) when `maxUnavailable=100%` ([**@lexfrei**](https://github.com/lexfrei) in #2426).
|
||||
|
||||
* **[linstor] Update piraeus-server to v1.33.2 with selected backports**: Bumps LINSTOR server from v1.33.1 to v1.33.2 with backported patches for stale bitmap adjust retry, LUKS2 header sizing, optimal I/O size detection, and the maintainer implementation. All patches verified against upstream v1.33.2 with `git apply --check` and `gradlew compileJava` *(backported to v1.2.2)* ([**@kvaps**](https://github.com/kvaps) in #2331).
|
||||
|
||||
* **[kamaji] Update to 26.3.5-edge, drop upstreamed patches**: Updates Kamaji from edge-26.2.4 to 26.3.5-edge and removes two patches accepted upstream. Adds configurable probe tuning and DataStore readiness conditions ([**@myasnikovdaniil**](https://github.com/myasnikovdaniil) in #2260).
|
||||
|
||||
* **[talm] Release v0.23.0, v0.23.1, v0.24.0** (github.com/cozystack/talm): Migrates to the Talos v1.12 multi-document machine config format ([**@lexfrei**](https://github.com/lexfrei) in cozystack/talm#116); renders templates online in `apply` to resolve lookups ([**@myasnikovdaniil**](https://github.com/myasnikovdaniil) in cozystack/talm#119); bumps dependencies and modernizes the codebase ([**@lexfrei**](https://github.com/lexfrei) in cozystack/talm#124).
|
||||
|
||||
* **[ansible-cozystack] Release v1.2.1, v1.2.2, v1.2.4** (github.com/cozystack/ansible-cozystack): Exposes `publishing.externalIPs` and tenant-root ingress via role variables ([**@lexfrei**](https://github.com/lexfrei) in cozystack/ansible-cozystack#30); adds a comprehensive node prerequisites audit ([**@lexfrei**](https://github.com/lexfrei) in cozystack/ansible-cozystack#27); replaces `ansible.utils.ipaddr` with a stdlib-based test plugin ([**@lexfrei**](https://github.com/lexfrei) in cozystack/ansible-cozystack#24); adds `v` prefix to collection version in requirements.yml examples ([**@lexfrei**](https://github.com/lexfrei) in cozystack/ansible-cozystack#23); tracks installer releases v1.2.1 through v1.2.4 ([**@app/renovate**](https://github.com/apps/renovate) in cozystack/ansible-cozystack#20, #22, #29, #31, #32).
|
||||
|
||||
## Development, Testing, and CI/CD
|
||||
|
||||
* **[ci] Replace cozystack-bot PAT with cozystack-ci GitHub App**: Replaces the long-lived `cozystack-bot` personal access token with short-lived, scoped tokens from the `cozystack-ci` GitHub App across all release workflows (`tags.yaml`, `auto-release.yaml`, `pull-requests-release.yaml`), improving security and auditability of CI operations ([**@tym83**](https://github.com/tym83) in #2351; [**@kvaps**](https://github.com/kvaps) in #2383, #2392).
|
||||
|
||||
* **[ci] Add Gemini Code Assist and CodeRabbit configuration**: Adds repository-level configuration for AI code reviewers with ignore patterns for vendored/generated code and incremental review settings ([**@lexfrei**](https://github.com/lexfrei) in #2385).
|
||||
|
||||
* **[ci] Promote next/ trunk on new minor/major releases**: Updates `update-website-docs` in `tags.yaml` to match the new docs-versioning contract — the website repo replaces the old "pre-create `vX.Y/` draft directory" scheme with a permanent `content/en/docs/next/` trunk, and released version directories are promoted explicitly by the release workflow ([**@myasnikovdaniil**](https://github.com/myasnikovdaniil) in #2433).
|
||||
|
||||
* **[tests] Fix Kafka E2E test timeout and retry race condition**: Increases Kafka E2E test timeout from 60s to 300s and fixes a retry race where `kubectl apply` could hit a still-deleting resource ([**@lexfrei**](https://github.com/lexfrei) in #2358).
|
||||
|
||||
* **docs: adopt Conventional Commits for commit and PR titles**: Standardizes commit and PR title format to `type(scope): description` across all contributing docs and the PR template ([**@lexfrei**](https://github.com/lexfrei) in #2395).
|
||||
|
||||
* **docs(ci): require screenshots for UI changes in PR template**: Adds a mandatory screenshots section to the PR template for UI-related changes ([**@kitsunoff**](https://github.com/kitsunoff) in #2407).
|
||||
|
||||
* **chore(maintenance): add @myasnikovdaniil to CODEOWNERS**: Adds @myasnikovdaniil to the default owners in `.github/CODEOWNERS` for automatic review requests ([**@myasnikovdaniil**](https://github.com/myasnikovdaniil) in #2434).
|
||||
|
||||
## Documentation
|
||||
|
||||
* **[website] Add ApplicationDefinition naming convention reference**: Documents how `cozystack-api` resolves kinds to their backing definitions ([**@lexfrei**](https://github.com/lexfrei) in cozystack/website#478).
|
||||
|
||||
* **[website] Document Talos / talosctl / Cozystack version pairing**: Adds a version compatibility matrix for installation ([**@lexfrei**](https://github.com/lexfrei) in cozystack/website#484).
|
||||
|
||||
* **[website] Document namespace layout and parent/child derivation**: Explains tenant namespace hierarchy and parent/child namespace derivation rules ([**@lexfrei**](https://github.com/lexfrei) in cozystack/website#479).
|
||||
|
||||
* **[website] Document the checkbox-then-edit-CR customization pattern for tenants**: Describes the workflow for customizing tenant settings via the CR after initial checkbox-based creation ([**@lexfrei**](https://github.com/lexfrei) in cozystack/website#485).
|
||||
|
||||
* **[website] Add custom Keycloak themes documentation**: Covers the theme image contract, configuration, `imagePullSecrets`, and theme activation in the Keycloak admin console ([**@lexfrei**](https://github.com/lexfrei) in cozystack/website#463).
|
||||
|
||||
* **[website] Add bonding (LACP) configuration how-to guide**: Covers network bonding configuration for Cozystack installations ([**@sircthulhu**](https://github.com/sircthulhu) in cozystack/website#459).
|
||||
|
||||
* **[website] Improve registry mirrors for tenant Kubernetes in air-gapped guide**: Improves documentation for configuring registry mirrors in air-gapped environments ([**@sircthulhu**](https://github.com/sircthulhu) in cozystack/website#461).
|
||||
|
||||
* **[website] Rewrite guide for ApplicationDefinition API (external-apps)**: Comprehensive rewrite of the external apps guide using the ApplicationDefinition API with Minecraft server examples ([**@kitsunoff**](https://github.com/kitsunoff) in cozystack/website#488).
|
||||
|
||||
* **[website] Add documentation for Go types usage**: Guide for using generated Go types for Cozystack managed applications as a Go module ([**@myasnikovdaniil**](https://github.com/myasnikovdaniil) in cozystack/website#465).
|
||||
|
||||
* **[website] Update backup/restore documentation for VMI/VMDisk**: Updates backup documentation with VM instance and VM disk restore improvements ([**@androndo**](https://github.com/androndo) in cozystack/website#466).
|
||||
|
||||
* **[website] Refactor docs versions to major.minor variants**: Moves docs to major.minor versioning for the v1.x series ([**@myasnikovdaniil**](https://github.com/myasnikovdaniil) in cozystack/website#477).
|
||||
|
||||
* **[website] Trunk-based versioning with permanent next/ directory**: Replaces the old "pre-create `vX.Y/` draft directory" scheme with a permanent `content/en/docs/next/` trunk; released version directories are promoted explicitly by `hack/release_next.sh` on new minor/major releases, and routing between `next/` and `vX.Y/` is Makefile-driven ([**@myasnikovdaniil**](https://github.com/myasnikovdaniil) in cozystack/website#495).
|
||||
|
||||
* **[website] Add updated OpenAPI spec**: Updates the OpenAPI specification for managed applications reference ([**@myasnikovdaniil**](https://github.com/myasnikovdaniil) in cozystack/website#469).
|
||||
|
||||
* **[website] Add OpenAPI spec download to GitHub Pages build**: Fixes the GitHub Pages build to include the OpenAPI spec download ([**@myasnikovdaniil**](https://github.com/myasnikovdaniil) in cozystack/website#494).
|
||||
|
||||
* **[website] Add OSS Health pages and OpenSSF badge**: Adds OSS Health section with OpenSSF Scorecard and Best Practices badges to the website ([**@tym83**](https://github.com/tym83) in cozystack/website#470).
|
||||
|
||||
* **[website] Add Telemetry page under OSS Health section**: Adds the Telemetry page with initial data seeding to the OSS Health docs ([**@tym83**](https://github.com/tym83) in cozystack/website#471, cozystack/website#504).
|
||||
|
||||
* **[website] Blog: OSS Health section launch announcement**: Publishes the announcement blog post for the OSS Health section ([**@tym83**](https://github.com/tym83) in cozystack/website#474).
|
||||
|
||||
* **[website] Fix OpenSSF canonical status URL**: Changes the OpenSSF canonical status URL from pt-BR to en ([**@tym83**](https://github.com/tym83) in cozystack/website#475).
|
||||
|
||||
* **[website] Add CozySummit Virtual 2026 program announcement**: Publishes the CozySummit Virtual 2026 program announcement blog post ([**@tym83**](https://github.com/tym83) in cozystack/website#472).
|
||||
|
||||
* **[website] Add missing release announcements for v0.1–v0.41**: Backfills missing release announcement blog posts for historical Cozystack versions ([**@tym83**](https://github.com/tym83) in cozystack/website#468).
|
||||
|
||||
* **[website] Blog: managed PostgreSQL with synchronous replication**: Adds a post covering the managed PostgreSQL synchronous-replication feature ([**@tym83**](https://github.com/tym83) in cozystack/website#497).
|
||||
|
||||
* **[website] Blog taxonomies and client-side filter UI**: Registers article-type and topic taxonomies and adds a client-side filter on the blog list page ([**@tym83**](https://github.com/tym83) in cozystack/website#499).
|
||||
|
||||
* **[website] Add images frontmatter for social preview on existing posts**: Adds images frontmatter for social preview on existing blog posts ([**@tym83**](https://github.com/tym83) in cozystack/website#498).
|
||||
|
||||
* **[website] Fix broken links and stale anchors across v1 docs**: Fixes 14 broken links and stale talm anchors ([**@lexfrei**](https://github.com/lexfrei) in cozystack/website#486).
|
||||
|
||||
* **[website] Prefix bundle package names with cozystack. in v1 examples**: Corrects package naming in documentation examples ([**@lexfrei**](https://github.com/lexfrei) in cozystack/website#482).
|
||||
|
||||
* **[website] Finish isolated-field removal and document opt-in policy labels**: Removes the obsolete `isolated` field from tenant documentation and documents the new opt-in policy labels approach ([**@lexfrei**](https://github.com/lexfrei) in cozystack/website#481).
|
||||
|
||||
* **[website] Add --take-ownership flag and describe networking.* fields**: Documents the `--take-ownership` flag and `networking.*` fields in the installation guide ([**@lexfrei**](https://github.com/lexfrei) in cozystack/website#480).
|
||||
|
||||
* **[website] Fix KubeOVN MASTER_NODES example path and key in troubleshooting**: Corrects the MASTER_NODES example path and key ([**@lexfrei**](https://github.com/lexfrei) in cozystack/website#483).
|
||||
|
||||
* **[website] Add CLAUDE.md for AI agent guidance**: Adds a CLAUDE.md file describing the trunk-based docs architecture for AI agent guidance ([**@myasnikovdaniil**](https://github.com/myasnikovdaniil) in cozystack/website#489).
|
||||
|
||||
* **[website] Update /docs/v1/ redirect to latest v1.2**: Updates the `/docs/v1/` redirect target to point to the latest v1.2 docs on GitHub Pages ([**@myasnikovdaniil**](https://github.com/myasnikovdaniil) in cozystack/website#492).
|
||||
|
||||
* **[website] Remove nbykov from CODEOWNERS and CLAUDE.md**: Cleans up CODEOWNERS and CLAUDE.md entries ([**@myasnikovdaniil**](https://github.com/myasnikovdaniil) in cozystack/website#491).
|
||||
|
||||
* **[website] Add Ahrefs Analytics tracker**: Adds the Ahrefs Analytics tracker to the website ([**@tym83**](https://github.com/tym83) in cozystack/website#503).
|
||||
|
||||
* **[website] Add breathing room between navbar and hero on OSS Health**: Minor styling fix for the OSS Health section ([**@tym83**](https://github.com/tym83) in cozystack/website#500).
|
||||
|
||||
* **[website] Fix og social badge image and title**: Updates the social badge image and title ([**@tym83**](https://github.com/tym83) in cozystack/website#487).
|
||||
|
||||
* **[website] Update managed apps reference for v1.2.1**: Automated managed-apps reference update ([**@cozystack-bot**](https://github.com/cozystack-bot) in cozystack/website#464).
|
||||
|
||||
* **[external-apps-example] Replace MongoDB example with Minecraft apps**: Refactors the external apps example to use the ApplicationDefinition API with Minecraft server applications ([**@lexfrei**](https://github.com/lexfrei) in cozystack/external-apps-example#2).
|
||||
|
||||
* **docs: update README introductory description**: Refines the platform positioning and improves clarity on core capabilities in the main README ([**@tym83**](https://github.com/tym83) in #2409).
|
||||
|
||||
## Governance
|
||||
|
||||
* **Add Mattia Eleuteri ([@mattia-eleuteri](https://github.com/mattia-eleuteri)) as Maintainer**: CSI, Storage, Networking & Security ([**@tym83**](https://github.com/tym83) in #2345).
|
||||
|
||||
* **Add Matthieu Robin ([@matthieu-robin](https://github.com/matthieu-robin)) as Maintainer**: Managed applications, platform quality, and benchmarking ([**@tym83**](https://github.com/tym83) in #2346).
|
||||
|
||||
## Contributors
|
||||
|
||||
We'd like to thank all contributors who made this release possible:
|
||||
|
||||
* [**@androndo**](https://github.com/androndo)
|
||||
* [**@Arsolitt**](https://github.com/Arsolitt)
|
||||
* [**@BROngineer**](https://github.com/BROngineer)
|
||||
* [**@IvanHunters**](https://github.com/IvanHunters)
|
||||
* [**@kitsunoff**](https://github.com/kitsunoff)
|
||||
* [**@kvaps**](https://github.com/kvaps)
|
||||
* [**@lexfrei**](https://github.com/lexfrei)
|
||||
* [**@lllamnyp**](https://github.com/lllamnyp)
|
||||
* [**@mattia-eleuteri**](https://github.com/mattia-eleuteri)
|
||||
* [**@myasnikovdaniil**](https://github.com/myasnikovdaniil)
|
||||
* [**@sircthulhu**](https://github.com/sircthulhu)
|
||||
* [**@tym83**](https://github.com/tym83)
|
||||
|
||||
### New Contributors
|
||||
|
||||
We're excited to welcome our first-time contributors:
|
||||
|
||||
* [**@Arsolitt**](https://github.com/Arsolitt) — First contribution!
|
||||
|
||||
---
|
||||
|
||||
**Full Changelog**: https://github.com/cozystack/cozystack/compare/v1.2.0...v1.3.0
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
<!--
|
||||
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 ~20–25 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
|
||||
6
go.mod
6
go.mod
|
|
@ -5,7 +5,6 @@ module github.com/cozystack/cozystack
|
|||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/cozystack/cozystack-scheduler/pkg/apis v0.1.1
|
||||
github.com/emicklei/dot v1.10.0
|
||||
github.com/fluxcd/helm-controller/api v1.4.3
|
||||
github.com/fluxcd/source-controller/api v1.7.4
|
||||
|
|
@ -29,10 +28,8 @@ require (
|
|||
k8s.io/klog/v2 v2.130.1
|
||||
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b
|
||||
k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d
|
||||
sigs.k8s.io/container-object-storage-interface-api v0.1.0
|
||||
sigs.k8s.io/controller-runtime v0.22.4
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.3.0
|
||||
sigs.k8s.io/yaml v1.6.0
|
||||
)
|
||||
|
||||
require (
|
||||
|
|
@ -45,8 +42,10 @@ require (
|
|||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/coreos/go-semver v0.3.1 // indirect
|
||||
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
|
||||
github.com/cozystack/cozystack-scheduler/pkg/apis v0.1.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.12.2 // indirect
|
||||
github.com/evanphx/json-patch v4.12.0+incompatible // indirect
|
||||
github.com/evanphx/json-patch/v5 v5.9.11 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fluxcd/pkg/apis/acl v0.9.0 // indirect
|
||||
|
|
@ -127,6 +126,7 @@ require (
|
|||
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
|
||||
sigs.k8s.io/randfill v1.0.0 // indirect
|
||||
sigs.k8s.io/yaml v1.6.0 // indirect
|
||||
)
|
||||
|
||||
// See: issues.k8s.io/135537
|
||||
|
|
|
|||
2
go.sum
2
go.sum
|
|
@ -321,8 +321,6 @@ k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d h1:wAhiDyZ4Tdtt7e46e9M5ZSAJ/MnPG
|
|||
k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 h1:jpcvIRr3GLoUoEKRkHKSmGjxb6lWwrBlJsXc+eUYQHM=
|
||||
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw=
|
||||
sigs.k8s.io/container-object-storage-interface-api v0.1.0 h1:8tB6JFQhbQIC1hwGQ+q4+tmSSNfjKemb7bFI6C0CK/4=
|
||||
sigs.k8s.io/container-object-storage-interface-api v0.1.0/go.mod h1:YiB+i/UGkzqgODDhRG3u7jkbWkQcoUeLEJ7hwOT/2Qk=
|
||||
sigs.k8s.io/controller-runtime v0.22.4 h1:GEjV7KV3TY8e+tJ2LCTxUTanW4z/FmNB7l327UfMq9A=
|
||||
sigs.k8s.io/controller-runtime v0.22.4/go.mod h1:+QX1XUpTXN4mLoblf4tqr5CQcyHPAki2HLXqQMY6vh8=
|
||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
|
||||
|
|
|
|||
|
|
@ -1,145 +0,0 @@
|
|||
#!/usr/bin/env bats
|
||||
# -----------------------------------------------------------------------------
|
||||
# Chart-wide invariant for packages/apps/kubernetes:
|
||||
#
|
||||
# Every Deployment in this chart that mounts <release>-admin-kubeconfig as a
|
||||
# Secret volume MUST:
|
||||
# - declare that volume optional: true (so kubelet does not FailedMount
|
||||
# while Kamaji is still provisioning the Secret), AND
|
||||
# - include the wait-for-kubeconfig init container (so the pod becomes
|
||||
# Ready only after Kamaji publishes the Secret).
|
||||
#
|
||||
# The per-template unittests in packages/apps/kubernetes/tests/ lock in
|
||||
# today's three Deployments (cluster-autoscaler, kccm, csi controller) by
|
||||
# name. This invariant is stricter: any future Deployment added to this
|
||||
# chart that mounts the same Secret but forgets the guard will fail here.
|
||||
#
|
||||
# Requires: helm, yq (mikefarah v4+), jq. All three are available on the
|
||||
# project's CI runners and on the maintainer workstation.
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@test "every Deployment mounting admin-kubeconfig has optional:true and wait-for-kubeconfig init" {
|
||||
values_file="packages/apps/kubernetes/tests/values-ci.yaml"
|
||||
[ -f "$values_file" ]
|
||||
|
||||
tmp=$(mktemp -d)
|
||||
trap 'rm -rf "$tmp"' EXIT
|
||||
|
||||
helm template invariant packages/apps/kubernetes \
|
||||
--namespace tenant-root \
|
||||
--values "$values_file" \
|
||||
2>/dev/null > "$tmp/rendered.yaml"
|
||||
|
||||
# yq streams one JSON object per input document. jq -s slurps the stream
|
||||
# into an array so we can treat all Deployments as a single collection.
|
||||
yq --output-format=json eval-all '.' "$tmp/rendered.yaml" \
|
||||
| jq -s --raw-output '
|
||||
map(select(.kind == "Deployment")) |
|
||||
map({
|
||||
name: .metadata.name,
|
||||
volumes: (.spec.template.spec.volumes // []),
|
||||
initNames: ((.spec.template.spec.initContainers // []) | map(.name)),
|
||||
}) |
|
||||
map(
|
||||
.name as $n |
|
||||
.initNames as $ins |
|
||||
(.volumes[] | select(.secret.secretName | test("-admin-kubeconfig$")?))
|
||||
| {
|
||||
name: $n,
|
||||
optional: (.secret.optional == true),
|
||||
hasInit: ($ins | index("wait-for-kubeconfig") != null),
|
||||
}
|
||||
)
|
||||
' > "$tmp/summary.json"
|
||||
|
||||
# At least one Deployment must match; if a refactor removes every
|
||||
# admin-kubeconfig volume from this chart, the test must be updated
|
||||
# deliberately rather than silently passing.
|
||||
matched=$(jq 'length' "$tmp/summary.json")
|
||||
[ "$matched" -ge 1 ]
|
||||
|
||||
offenders=$(jq --raw-output '.[] | select(.optional != true or .hasInit != true) | .name' "$tmp/summary.json")
|
||||
|
||||
if [ -n "$offenders" ]; then
|
||||
echo "Deployments mounting *-admin-kubeconfig without optional:true + wait-for-kubeconfig init:" >&2
|
||||
echo "$offenders" >&2
|
||||
echo "Full summary:" >&2
|
||||
cat "$tmp/summary.json" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Invariant holds for $matched Deployment(s)"
|
||||
}
|
||||
|
||||
@test "chart emits zero admin-kubeconfig Deployments when tenant has no etcd DataStore" {
|
||||
# Without a DataStore (parent Tenant has not populated _namespace.etcd yet)
|
||||
# the control-plane-side Deployments must NOT render at all. If they did,
|
||||
# the wait-for-kubeconfig init would CrashLoopBackOff indefinitely - there
|
||||
# would be no KamajiControlPlane to provision the Secret - consuming the
|
||||
# HelmRelease wait budget and triggering exactly the remediation cycle the
|
||||
# rest of this chart tries to avoid. This test renders the whole chart
|
||||
# with etcd empty and asserts no Deployment references the admin-kubeconfig
|
||||
# Secret.
|
||||
tmp=$(mktemp -d)
|
||||
trap 'rm -rf "$tmp"' EXIT
|
||||
|
||||
helm template invariant packages/apps/kubernetes \
|
||||
--namespace tenant-root \
|
||||
--values packages/apps/kubernetes/tests/values-ci-no-etcd.yaml \
|
||||
2>/dev/null > "$tmp/rendered.yaml"
|
||||
|
||||
matched=$(
|
||||
yq --output-format=json eval-all '.' "$tmp/rendered.yaml" \
|
||||
| jq -s '
|
||||
map(select(.kind == "Deployment")) |
|
||||
map(select(
|
||||
(.spec.template.spec.volumes // [])
|
||||
| any(.secret.secretName | test("-admin-kubeconfig$")?)
|
||||
)) |
|
||||
length
|
||||
'
|
||||
)
|
||||
|
||||
if [ "$matched" -ne 0 ]; then
|
||||
echo "Expected zero Deployments mounting *-admin-kubeconfig when etcd is empty, got $matched:" >&2
|
||||
yq --output-format=json eval-all '.' "$tmp/rendered.yaml" \
|
||||
| jq -s 'map(select(.kind == "Deployment") | .metadata.name)' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "No admin-kubeconfig Deployments rendered for empty etcd (as expected)"
|
||||
}
|
||||
|
||||
@test "chart emits zero admin-kubeconfig HelmReleases when tenant has no etcd DataStore" {
|
||||
# Same principle as the Deployment variant above, extended to every child
|
||||
# HelmRelease (cilium, coredns, csi, cert-manager, ...). They reference
|
||||
# *-admin-kubeconfig via kubeConfig.secretRef and would otherwise sit in
|
||||
# NotReady forever on an etcd-less tenant, polluting the HelmRelease list
|
||||
# the operator sees and contradicting the "awaiting-etcd beacon only"
|
||||
# contract of the soft-skip path.
|
||||
tmp=$(mktemp -d)
|
||||
trap 'rm -rf "$tmp"' EXIT
|
||||
|
||||
helm template invariant packages/apps/kubernetes \
|
||||
--namespace tenant-root \
|
||||
--values packages/apps/kubernetes/tests/values-ci-no-etcd.yaml \
|
||||
2>/dev/null > "$tmp/rendered.yaml"
|
||||
|
||||
matched=$(
|
||||
yq --output-format=json eval-all '.' "$tmp/rendered.yaml" \
|
||||
| jq -s '
|
||||
map(select(.kind == "HelmRelease")) |
|
||||
map(select(.spec.kubeConfig.secretRef.name | test("-admin-kubeconfig$")?)) |
|
||||
length
|
||||
'
|
||||
)
|
||||
|
||||
if [ "$matched" -ne 0 ]; then
|
||||
echo "Expected zero HelmReleases referencing *-admin-kubeconfig when etcd is empty, got $matched:" >&2
|
||||
yq --output-format=json eval-all '.' "$tmp/rendered.yaml" \
|
||||
| jq -s 'map(select(.kind == "HelmRelease") | .metadata.name)' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "No admin-kubeconfig HelmReleases rendered for empty etcd (as expected)"
|
||||
}
|
||||
|
|
@ -16,7 +16,7 @@ kubectl create -f - <<EOF
|
|||
apiVersion: cdi.kubevirt.io/v1beta1
|
||||
kind: DataVolume
|
||||
metadata:
|
||||
name: "vm-default-images-$name"
|
||||
name: "vm-image-$name"
|
||||
namespace: cozy-public
|
||||
annotations:
|
||||
cdi.kubevirt.io/storage.bind.immediate.requested: "true"
|
||||
|
|
|
|||
|
|
@ -1,206 +0,0 @@
|
|||
#!/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 ]
|
||||
}
|
||||
|
|
@ -1,104 +0,0 @@
|
|||
# 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.
|
|
|
@ -1,39 +0,0 @@
|
|||
# Helpers for asserting that a Flux HelmRelease did not fall into an
|
||||
# install/upgrade remediation cycle during an e2e run.
|
||||
#
|
||||
# Background: Flux helm-controller's ClearFailures() zeroes
|
||||
# .status.installFailures / .status.upgradeFailures on every successful
|
||||
# reconciliation (see the upstream ClearFailures method on
|
||||
# HelmReleaseStatus). That makes those counters useless for a guard that
|
||||
# runs after the HelmRelease has reached Ready - the values are always 0.
|
||||
#
|
||||
# What survives a successful reconciliation is .status.history, a bounded
|
||||
# list of release Snapshots. Each Snapshot carries a status field that
|
||||
# tracks the Helm release state: deployed, superseded, failed, uninstalled,
|
||||
# and so on. A remediation cycle leaves the footprint behind: a snapshot
|
||||
# with status "uninstalled" (from install/upgrade remediation) or "failed"
|
||||
# (Helm release failure that remediation then uninstalled). Those stay in
|
||||
# history even after a subsequent successful reinstall.
|
||||
#
|
||||
# helmrelease_has_remediation_cycle takes a newline-delimited list of
|
||||
# snapshot statuses (whatever the caller extracted via kubectl -o jsonpath
|
||||
# or equivalent) and returns 0 (detected) when any entry is "failed" or
|
||||
# "uninstalled", 1 otherwise. Empty input is treated as "no history yet,
|
||||
# no cycle observed".
|
||||
|
||||
helmrelease_has_remediation_cycle() {
|
||||
statuses="$1"
|
||||
if [ -z "${statuses}" ]; then
|
||||
return 1
|
||||
fi
|
||||
# printf + grep over the pipe, rather than a heredoc plus while read.
|
||||
# printf %s treats the status string as a literal payload, so any stray
|
||||
# $ in a future caller's input does not trigger shell expansion. grep
|
||||
# returns 0 iff at least one line matches the allowlist, which is
|
||||
# exactly the contract the caller wants, so we can return its exit
|
||||
# status directly.
|
||||
if printf '%s\n' "${statuses}" | grep --extended-regexp --quiet '^(failed|uninstalled)$'; then
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
|
@ -1,5 +1,3 @@
|
|||
. hack/e2e-apps/remediation-guard.sh
|
||||
|
||||
run_kubernetes_test() {
|
||||
local version_expr="$1"
|
||||
local test_name="$2"
|
||||
|
|
@ -322,35 +320,6 @@ EOF
|
|||
done
|
||||
kubectl wait hr kubernetes-${test_name}-ingress-nginx -n tenant-test --timeout=5m --for=condition=ready
|
||||
|
||||
# Guard: parent HelmRelease must not have entered an install/upgrade remediation cycle.
|
||||
# A non-zero installFailures/upgradeFailures indicates the helm-wait budget expired while
|
||||
# admin-kubeconfig was still being provisioned, which would trigger uninstall remediation
|
||||
# and churn the Cluster CR.
|
||||
# Flux helm-controller v2 retains per-revision release Snapshots in
|
||||
# .status.history; each Snapshot's .status reflects the Helm release
|
||||
# state (deployed/superseded/failed/uninstalled). A remediation cycle
|
||||
# leaves a "failed" or "uninstalled" entry behind that survives a later
|
||||
# successful reinstall, unlike the installFailures/upgradeFailures
|
||||
# counters (which ClearFailures zeroes on every successful reconcile).
|
||||
# The shape is pinned by hack/remediation-guard.bats; the upstream
|
||||
# types are github.com/fluxcd/helm-controller/api v2 Snapshot.
|
||||
history_statuses=$(kubectl get hr -n tenant-test "kubernetes-${test_name}" \
|
||||
-ojsonpath='{range .status.history[*]}{.status}{"\n"}{end}')
|
||||
# Always emit the raw value so a silent future-Flux field rename shows
|
||||
# up as "empty history on a Ready HR" in CI logs rather than vanishing.
|
||||
echo "Parent HelmRelease history statuses:"
|
||||
printf '%s\n' "${history_statuses:-<empty>}"
|
||||
if [ -z "${history_statuses}" ]; then
|
||||
echo "Unexpected empty .status.history on a Ready HelmRelease - Flux API shape may have changed." >&2
|
||||
kubectl -n tenant-test describe hr "kubernetes-${test_name}" >&2
|
||||
exit 1
|
||||
fi
|
||||
if helmrelease_has_remediation_cycle "${history_statuses}"; then
|
||||
echo "Parent HelmRelease entered remediation cycle." >&2
|
||||
kubectl -n tenant-test describe hr "kubernetes-${test_name}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Clean up
|
||||
pkill -f "port-forward.*${port}:" 2>/dev/null || true
|
||||
rm -f "tenantkubeconfig-${test_name}"
|
||||
|
|
|
|||
|
|
@ -1,127 +0,0 @@
|
|||
#!/usr/bin/env bats
|
||||
# -----------------------------------------------------------------------------
|
||||
# Unit tests for hack/e2e-apps/remediation-guard.sh
|
||||
#
|
||||
# helmrelease_has_remediation_cycle takes a newline-delimited list of
|
||||
# HelmRelease history snapshot status values (deployed/superseded/failed/
|
||||
# uninstalled/...) and returns 0 when any entry is "failed" or "uninstalled"
|
||||
# (meaning flux helm-controller performed install/upgrade remediation).
|
||||
#
|
||||
# This is used by the e2e script after the HelmRelease reaches Ready. The
|
||||
# failure/upgrade counters (.status.installFailures / .status.upgradeFailures)
|
||||
# are useless there because flux's ClearFailures zeroes them on successful
|
||||
# reconciliation; .status.history retains the snapshot trail.
|
||||
#
|
||||
# cozytest.sh's awk parser recognizes only @test blocks and a bare `}` on
|
||||
# its own line; there is no bats `run` or `$status`. Assertions are
|
||||
# expressed as direct shell tests that exit non-zero on failure.
|
||||
#
|
||||
# Run with: hack/cozytest.sh hack/remediation-guard.bats
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@test "empty history returns not-detected" {
|
||||
. hack/e2e-apps/remediation-guard.sh
|
||||
if helmrelease_has_remediation_cycle ""; then
|
||||
echo "expected not-detected for empty history" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
@test "single deployed snapshot returns not-detected" {
|
||||
. hack/e2e-apps/remediation-guard.sh
|
||||
if helmrelease_has_remediation_cycle "deployed"; then
|
||||
echo "expected not-detected for deployed-only history" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
@test "deployed then superseded returns not-detected" {
|
||||
. hack/e2e-apps/remediation-guard.sh
|
||||
statuses=$(printf 'deployed\nsuperseded\n')
|
||||
if helmrelease_has_remediation_cycle "${statuses}"; then
|
||||
echo "expected not-detected for deployed+superseded history" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
@test "single failed snapshot returns detected" {
|
||||
. hack/e2e-apps/remediation-guard.sh
|
||||
if ! helmrelease_has_remediation_cycle "failed"; then
|
||||
echo "expected detected when history contains failed snapshot" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
@test "single uninstalled snapshot returns detected" {
|
||||
# The exact signature of the install-remediation race: the first install
|
||||
# exceeded flux's wait budget, remediation uninstalled, the next retry
|
||||
# eventually succeeded. History still carries the uninstalled snapshot.
|
||||
. hack/e2e-apps/remediation-guard.sh
|
||||
if ! helmrelease_has_remediation_cycle "uninstalled"; then
|
||||
echo "expected detected when history contains uninstalled snapshot" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
@test "uninstalled then deployed still returns detected" {
|
||||
. hack/e2e-apps/remediation-guard.sh
|
||||
statuses=$(printf 'uninstalled\ndeployed\n')
|
||||
if ! helmrelease_has_remediation_cycle "${statuses}"; then
|
||||
echo "expected detected despite later successful deploy" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
@test "deployed then failed still returns detected" {
|
||||
. hack/e2e-apps/remediation-guard.sh
|
||||
statuses=$(printf 'deployed\nfailed\n')
|
||||
if ! helmrelease_has_remediation_cycle "${statuses}"; then
|
||||
echo "expected detected when any entry is failed" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
@test "status.history extraction pins HR v2 status.history shape" {
|
||||
# Pins the Flux HelmRelease v2 .status.history[].status shape that
|
||||
# run-kubernetes.sh relies on. If a future flux release renames the
|
||||
# field, the jsonpath returns nothing, the guard reports no cycle,
|
||||
# and real remediation loops slip past the e2e assertion. This test
|
||||
# uses yq to read the exact path used in the e2e script; the upstream
|
||||
# Snapshot type lives at
|
||||
# github.com/fluxcd/helm-controller/api/v2.Snapshot (via go.mod).
|
||||
tmp=$(mktemp -d)
|
||||
trap 'rm -rf "$tmp"' EXIT
|
||||
|
||||
cat > "$tmp/hr.yaml" <<'YAML'
|
||||
apiVersion: helm.toolkit.fluxcd.io/v2
|
||||
kind: HelmRelease
|
||||
metadata:
|
||||
name: kubernetes-test
|
||||
namespace: tenant-test
|
||||
status:
|
||||
history:
|
||||
- name: kubernetes-test
|
||||
namespace: tenant-test
|
||||
version: 1
|
||||
status: uninstalled
|
||||
- name: kubernetes-test
|
||||
namespace: tenant-test
|
||||
version: 2
|
||||
status: deployed
|
||||
YAML
|
||||
|
||||
# Default yq output is yaml scalar format, which for string values emits
|
||||
# bare unquoted tokens - matching what kubectl -o jsonpath produces in
|
||||
# e2e. Do not switch to JSON output here; that would quote the values
|
||||
# and break the loop in helmrelease_has_remediation_cycle.
|
||||
statuses=$(yq '.status.history[].status' "$tmp/hr.yaml")
|
||||
|
||||
[ -n "$statuses" ]
|
||||
echo "$statuses" | grep --quiet '^uninstalled$'
|
||||
|
||||
. hack/e2e-apps/remediation-guard.sh
|
||||
if ! helmrelease_has_remediation_cycle "$statuses"; then
|
||||
echo "expected detected for pinned HR snippet with uninstalled + deployed history" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
|
@ -141,10 +141,7 @@ func (m *Manager) ensureCFOMapping(ctx context.Context, crd *cozyv1alpha1.Applic
|
|||
}
|
||||
|
||||
// buildMultilineStringSchema parses OpenAPI schema and creates schema with multilineString
|
||||
// for all string fields inside spec that don't have enum.
|
||||
// It handles two structures:
|
||||
// - properties.spec.properties (most resources)
|
||||
// - properties.properties (VMDisk and similar resources without spec wrapper)
|
||||
// for all string fields inside spec that don't have enum
|
||||
func buildMultilineStringSchema(openAPISchema string) (map[string]any, error) {
|
||||
if openAPISchema == "" {
|
||||
return map[string]any{}, nil
|
||||
|
|
@ -164,25 +161,15 @@ func buildMultilineStringSchema(openAPISchema string) (map[string]any, error) {
|
|||
"properties": map[string]any{},
|
||||
}
|
||||
|
||||
var specProps map[string]any
|
||||
var hasSpec bool
|
||||
|
||||
// First try to find properties under spec
|
||||
if specProp, ok := props["spec"].(map[string]any); ok {
|
||||
specProps, hasSpec = specProp["properties"].(map[string]any)
|
||||
// Check if there's a spec property
|
||||
specProp, ok := props["spec"].(map[string]any)
|
||||
if !ok {
|
||||
return map[string]any{}, nil
|
||||
}
|
||||
|
||||
// If no spec wrapper, use top-level properties directly (VMDisk pattern)
|
||||
if !hasSpec {
|
||||
specProps = props
|
||||
// Still wrap in spec for consistency with applyListInputOverrides
|
||||
schemaProps := schema["properties"].(map[string]any)
|
||||
specSchema := map[string]any{
|
||||
"properties": map[string]any{},
|
||||
}
|
||||
schemaProps["spec"] = specSchema
|
||||
processSpecProperties(specProps, specSchema["properties"].(map[string]any))
|
||||
return schema, nil
|
||||
specProps, ok := specProp["properties"].(map[string]any)
|
||||
if !ok {
|
||||
return map[string]any{}, nil
|
||||
}
|
||||
|
||||
// Create spec.properties structure in schema
|
||||
|
|
@ -244,45 +231,10 @@ func applyListInputOverrides(schema map[string]any, kind string, openAPIProps ma
|
|||
}
|
||||
|
||||
case "ClickHouse", "Harbor", "HTTPCache", "Kubernetes", "MariaDB", "MongoDB",
|
||||
"NATS", "OpenBAO", "Postgres", "Qdrant", "RabbitMQ", "Redis":
|
||||
"NATS", "OpenBAO", "Postgres", "Qdrant", "RabbitMQ", "Redis", "VMDisk":
|
||||
specProps := ensureSchemaPath(schema, "spec")
|
||||
specProps["storageClass"] = storageClassListInput()
|
||||
|
||||
case "VMDisk":
|
||||
specProps := ensureSchemaPath(schema, "spec")
|
||||
specProps["storageClass"] = storageClassListInput()
|
||||
|
||||
// Override source.image.name to be an API-backed dropdown listing default images
|
||||
if sourceObj, ok := specProps["source"].(map[string]any); ok {
|
||||
if imgProps, ok := sourceObj["properties"].(map[string]any); ok {
|
||||
if imgName, ok := imgProps["image"].(map[string]any); ok {
|
||||
if imgNameProps, ok := imgName["properties"].(map[string]any); ok {
|
||||
imgNameProps["name"] = map[string]any{
|
||||
"type": "listInput",
|
||||
"customProps": map[string]any{
|
||||
"valueUri": "/api/clusters/{cluster}/k8s/apis/cdi.kubevirt.io/v1beta1/namespaces/cozy-public/datavolumes",
|
||||
"keysToValue": []any{"metadata", "annotations", "vm-default-images.cozystack.io/name"},
|
||||
"keysToLabel": []any{"metadata", "annotations", "vm-default-images.cozystack.io/description"},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
// Override source.disk.name to be an API-backed dropdown listing VMDisk resources
|
||||
if diskName, ok := imgProps["disk"].(map[string]any); ok {
|
||||
if diskNameProps, ok := diskName["properties"].(map[string]any); ok {
|
||||
diskNameProps["name"] = map[string]any{
|
||||
"type": "listInput",
|
||||
"customProps": map[string]any{
|
||||
"valueUri": "/api/clusters/{cluster}/k8s/apis/apps.cozystack.io/v1alpha1/namespaces/{namespace}/vmdisks",
|
||||
"keysToValue": []any{"metadata", "name"},
|
||||
"keysToLabel": []any{"metadata", "name"},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case "FoundationDB":
|
||||
storageProps := ensureSchemaPath(schema, "spec", "storage")
|
||||
storageProps["storageClass"] = storageClassListInput()
|
||||
|
|
|
|||
|
|
@ -317,108 +317,6 @@ func TestApplyListInputOverrides_StorageClassFoundationDB(t *testing.T) {
|
|||
assertStorageClassListInput(t, sc)
|
||||
}
|
||||
|
||||
func TestApplyListInputOverrides_VMDisk_SourceFields(t *testing.T) {
|
||||
openAPISchema := `{
|
||||
"properties":{
|
||||
"optical":{"type":"boolean"},
|
||||
"source":{
|
||||
"type":"object",
|
||||
"properties":{
|
||||
"image":{
|
||||
"type":"object",
|
||||
"properties":{
|
||||
"name":{"type":"string"}
|
||||
}
|
||||
},
|
||||
"disk":{
|
||||
"type":"object",
|
||||
"properties":{
|
||||
"name":{"type":"string"}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"storage":{"type":"string"},
|
||||
"storageClass":{"type":"string"}
|
||||
}
|
||||
}`
|
||||
|
||||
schema, err := buildMultilineStringSchema(openAPISchema)
|
||||
if err != nil {
|
||||
t.Fatalf("buildMultilineStringSchema failed: %v", err)
|
||||
}
|
||||
|
||||
applyListInputOverrides(schema, "VMDisk", map[string]any{})
|
||||
|
||||
specProps := schema["properties"].(map[string]any)["spec"].(map[string]any)["properties"].(map[string]any)
|
||||
|
||||
// Check storageClass
|
||||
sc, ok := specProps["storageClass"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatal("storageClass not found in spec.properties")
|
||||
}
|
||||
assertStorageClassListInput(t, sc)
|
||||
|
||||
// Check source.image.name listInput
|
||||
// Structure: specProps["source"]["properties"]["image"]["properties"]["name"]
|
||||
sourceObj, ok := specProps["source"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatal("source not found in spec.properties")
|
||||
}
|
||||
sourceObjProps, ok := sourceObj["properties"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatal("source.properties not found")
|
||||
}
|
||||
imageObj, ok := sourceObjProps["image"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatal("image not found in source.properties")
|
||||
}
|
||||
imageObjProps, ok := imageObj["properties"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatal("image.properties not found")
|
||||
}
|
||||
imgName, ok := imageObjProps["name"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatal("name not found in image.properties")
|
||||
}
|
||||
if imgName["type"] != "listInput" {
|
||||
t.Errorf("expected type listInput, got %v", imgName["type"])
|
||||
}
|
||||
imgNameCustomProps, ok := imgName["customProps"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatal("name.customProps not found")
|
||||
}
|
||||
expectedImageURI := "/api/clusters/{cluster}/k8s/apis/cdi.kubevirt.io/v1beta1/namespaces/cozy-public/datavolumes"
|
||||
if imgNameCustomProps["valueUri"] != expectedImageURI {
|
||||
t.Errorf("expected valueUri %s, got %v", expectedImageURI, imgNameCustomProps["valueUri"])
|
||||
}
|
||||
|
||||
// Check source.disk.name listInput
|
||||
diskObj, ok := sourceObjProps["disk"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatal("disk not found in source.properties")
|
||||
}
|
||||
diskObjProps, ok := diskObj["properties"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatal("disk.properties not found")
|
||||
}
|
||||
diskName, ok := diskObjProps["name"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatal("name not found in disk.properties")
|
||||
}
|
||||
if diskName["type"] != "listInput" {
|
||||
t.Errorf("expected type listInput, got %v", diskName["type"])
|
||||
}
|
||||
diskNameCustomProps, ok := diskName["customProps"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatal("disk name.customProps not found")
|
||||
}
|
||||
expectedDiskURI := "/api/clusters/{cluster}/k8s/apis/apps.cozystack.io/v1alpha1/namespaces/{namespace}/vmdisks"
|
||||
if diskNameCustomProps["valueUri"] != expectedDiskURI {
|
||||
t.Errorf("expected valueUri %s, got %v", expectedDiskURI, diskNameCustomProps["valueUri"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyListInputOverrides_StorageClassKafka(t *testing.T) {
|
||||
schema := map[string]any{}
|
||||
applyListInputOverrides(schema, "Kafka", map[string]any{})
|
||||
|
|
|
|||
|
|
@ -47,7 +47,6 @@ func (m *Manager) ensureFactory(ctx context.Context, crd *cozyv1alpha1.Applicati
|
|||
if prefix, ok := vncTabPrefix(kind); ok {
|
||||
tabs = append(tabs, vncTab(prefix))
|
||||
}
|
||||
tabs = append(tabs, eventsTab(kind))
|
||||
tabs = append(tabs, yamlTab(g, v, plural))
|
||||
|
||||
// Use unified factory creation
|
||||
|
|
@ -359,37 +358,6 @@ func secretsTab(kind string) map[string]any {
|
|||
}
|
||||
}
|
||||
|
||||
// eventsTab shows Kubernetes Events scoped to the application's namespace.
|
||||
// Events are namespace-scoped because Kubernetes Events don't carry application
|
||||
// labels and cannot be filtered by label selector. In Cozystack's multi-tenancy
|
||||
// model, each tenant namespace maps to a single application scope, so namespace
|
||||
// filtering provides the correct event scope.
|
||||
// For Tenant applications, events are fetched from status.namespace (the tenant's
|
||||
// own namespace) instead of the parent namespace where the Tenant object lives.
|
||||
func eventsTab(kind string) map[string]any {
|
||||
nsPlaceholder := "{3}"
|
||||
if kind == "Tenant" {
|
||||
nsPlaceholder = "{reqsJsonPath[0]['.status.namespace']}"
|
||||
}
|
||||
return map[string]any{
|
||||
"key": "events",
|
||||
"label": "Events",
|
||||
"children": []any{
|
||||
map[string]any{
|
||||
"type": "EnrichedTable",
|
||||
"data": map[string]any{
|
||||
"id": "events-table",
|
||||
"fetchUrl": "/api/clusters/{2}/k8s/api/v1/namespaces/" + nsPlaceholder + "/events",
|
||||
"cluster": "{2}",
|
||||
"baseprefix": "/openapi-ui",
|
||||
"customizationId": "factory-details-events",
|
||||
"pathToItems": []any{"items"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func yamlTab(group, version, plural string) map[string]any {
|
||||
return map[string]any{
|
||||
"key": "yaml",
|
||||
|
|
|
|||
|
|
@ -1,60 +0,0 @@
|
|||
package dashboard
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEventsTab_Structure(t *testing.T) {
|
||||
tab := eventsTab("PostgreSQL")
|
||||
|
||||
if tab["key"] != "events" {
|
||||
t.Errorf("expected key=events, got %v", tab["key"])
|
||||
}
|
||||
if tab["label"] != "Events" {
|
||||
t.Errorf("expected label=Events, got %v", tab["label"])
|
||||
}
|
||||
|
||||
children, ok := tab["children"].([]any)
|
||||
if !ok || len(children) != 1 {
|
||||
t.Fatal("expected exactly 1 child in events tab")
|
||||
}
|
||||
|
||||
table, ok := children[0].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatal("child is not a map")
|
||||
}
|
||||
if table["type"] != "EnrichedTable" {
|
||||
t.Errorf("expected type=EnrichedTable, got %v", table["type"])
|
||||
}
|
||||
|
||||
data, ok := table["data"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatal("table data is not a map")
|
||||
}
|
||||
if data["id"] != "events-table" {
|
||||
t.Errorf("expected id=events-table, got %v", data["id"])
|
||||
}
|
||||
if data["fetchUrl"] != "/api/clusters/{2}/k8s/api/v1/namespaces/{3}/events" {
|
||||
t.Errorf("unexpected fetchUrl for non-Tenant: %v", data["fetchUrl"])
|
||||
}
|
||||
if data["customizationId"] != "factory-details-events" {
|
||||
t.Errorf("expected customizationId=factory-details-events, got %v", data["customizationId"])
|
||||
}
|
||||
|
||||
pathToItems, ok := data["pathToItems"].([]any)
|
||||
if !ok || len(pathToItems) != 1 || pathToItems[0] != "items" {
|
||||
t.Errorf("expected pathToItems=[items], got %v", data["pathToItems"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestEventsTab_TenantUsesStatusNamespace(t *testing.T) {
|
||||
tab := eventsTab("Tenant")
|
||||
children := tab["children"].([]any)
|
||||
table := children[0].(map[string]any)
|
||||
data := table["data"].(map[string]any)
|
||||
|
||||
expectedURL := "/api/clusters/{2}/k8s/api/v1/namespaces/{reqsJsonPath[0]['.status.namespace']}/events"
|
||||
if data["fetchUrl"] != expectedURL {
|
||||
t.Errorf("expected Tenant fetchUrl to use status.namespace, got %v", data["fetchUrl"])
|
||||
}
|
||||
}
|
||||
|
|
@ -144,9 +144,6 @@ func (m *Manager) ensureSidebar(ctx context.Context, crd *cozyv1alpha1.Applicati
|
|||
// Add sidebar for backups.cozystack.io Backup resource
|
||||
keysAndTags["backups"] = []any{"backup-sidebar"}
|
||||
|
||||
// Add sidebar for backups.cozystack.io RestoreJob resource
|
||||
keysAndTags["restorejobs"] = []any{"restorejob-sidebar"}
|
||||
|
||||
// 3) Sort items within each category by Weight (desc), then Label (A→Z)
|
||||
for cat := range categories {
|
||||
sort.Slice(categories[cat], func(i, j int) bool {
|
||||
|
|
@ -218,11 +215,6 @@ func (m *Manager) ensureSidebar(ctx context.Context, crd *cozyv1alpha1.Applicati
|
|||
"label": "Backups",
|
||||
"link": "/openapi-ui/{cluster}/{namespace}/api-table/backups.cozystack.io/v1alpha1/backups",
|
||||
},
|
||||
map[string]any{
|
||||
"key": "restorejobs",
|
||||
"label": "RestoreJobs",
|
||||
"link": "/openapi-ui/{cluster}/{namespace}/api-table/backups.cozystack.io/v1alpha1/restorejobs",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
|
|
@ -266,7 +258,6 @@ func (m *Manager) ensureSidebar(ctx context.Context, crd *cozyv1alpha1.Applicati
|
|||
"stock-project-factory-plan-details",
|
||||
"stock-project-factory-backupjob-details",
|
||||
"stock-project-factory-backup-details",
|
||||
"stock-project-factory-restorejob-details",
|
||||
"stock-project-factory-external-ips",
|
||||
"stock-project-api-form",
|
||||
"stock-project-api-table",
|
||||
|
|
@ -281,12 +272,7 @@ func (m *Manager) ensureSidebar(ctx context.Context, crd *cozyv1alpha1.Applicati
|
|||
"stock-instance-builtin-table",
|
||||
}
|
||||
|
||||
// Add details sidebars for all CRDs with dashboard config, and collect
|
||||
// the set of IDs that are genuinely CRD-backed (dynamic). The hardcoded
|
||||
// `-details` IDs above (e.g. kube-* and backup/backupjob/plan/restorejob)
|
||||
// are not tied to an ApplicationDefinition and must be treated as static
|
||||
// so they receive consistent labels via upsertMultipleSidebars().
|
||||
dynamicDetailsIDs := map[string]bool{}
|
||||
// Add details sidebars for all CRDs with dashboard config
|
||||
for i := range all {
|
||||
def := &all[i]
|
||||
if def.Spec.Dashboard == nil {
|
||||
|
|
@ -296,22 +282,17 @@ func (m *Manager) ensureSidebar(ctx context.Context, crd *cozyv1alpha1.Applicati
|
|||
lowerKind := strings.ToLower(kind)
|
||||
detailsID := fmt.Sprintf("stock-project-factory-%s-details", lowerKind)
|
||||
targetIDs = append(targetIDs, detailsID)
|
||||
dynamicDetailsIDs[detailsID] = true
|
||||
}
|
||||
|
||||
// 7) Upsert all target sidebars with identical menuItems and keysAndTags
|
||||
return m.upsertMultipleSidebars(ctx, crd, targetIDs, dynamicDetailsIDs, keysAndTags, menuItems)
|
||||
return m.upsertMultipleSidebars(ctx, crd, targetIDs, keysAndTags, menuItems)
|
||||
}
|
||||
|
||||
// upsertMultipleSidebars creates/updates several Sidebar resources with the same menu spec.
|
||||
// dynamicDetailsIDs identifies `stock-project-factory-<kind>-details` sidebars that are
|
||||
// backed by an ApplicationDefinition and should therefore be owned by that CRD.
|
||||
// Any other ID is treated as a static sidebar (managed-by labels, no owner ref).
|
||||
func (m *Manager) upsertMultipleSidebars(
|
||||
ctx context.Context,
|
||||
crd *cozyv1alpha1.ApplicationDefinition,
|
||||
ids []string,
|
||||
dynamicDetailsIDs map[string]bool,
|
||||
keysAndTags map[string]any,
|
||||
menuItems []any,
|
||||
) error {
|
||||
|
|
@ -327,10 +308,8 @@ func (m *Manager) upsertMultipleSidebars(
|
|||
|
||||
if _, err := controllerutil.CreateOrUpdate(ctx, m.Client, obj, func() error {
|
||||
// Only set owner reference for dynamic sidebars (stock-project-factory-{kind}-details)
|
||||
// that are actually backed by an ApplicationDefinition. Static sidebars — including
|
||||
// hardcoded details sidebars for built-in/backup resources — must fall through to the
|
||||
// static-label branch so they're managed consistently.
|
||||
if strings.HasPrefix(id, "stock-project-factory-") && strings.HasSuffix(id, "-details") && dynamicDetailsIDs[id] {
|
||||
// Static sidebars (stock-instance-*, stock-project-*) should not have owner references
|
||||
if strings.HasPrefix(id, "stock-project-factory-") && strings.HasSuffix(id, "-details") {
|
||||
// This is a dynamic sidebar, set owner reference only if it matches the current CRD
|
||||
_, _, kind := pickGVK(crd)
|
||||
lowerKind := strings.ToLower(kind)
|
||||
|
|
|
|||
|
|
@ -650,31 +650,12 @@ func createStringColumn(name, jsonPath string) map[string]any {
|
|||
}
|
||||
}
|
||||
|
||||
// createTimestampColumn creates a timestamp column with custom formatting.
|
||||
// Extra jsonPaths act as ordered fallbacks: the first non-null path wins,
|
||||
// and `-` is used only when every path is null. Depth is capped at two
|
||||
// because the template parser's non-greedy matcher cannot track more than
|
||||
// one level of alternating quotes in a nested reqsJsonPath fallback.
|
||||
func createTimestampColumn(name string, jsonPaths ...string) map[string]any {
|
||||
if len(jsonPaths) == 0 {
|
||||
panic("createTimestampColumn requires at least one jsonPath")
|
||||
}
|
||||
if len(jsonPaths) > 2 {
|
||||
panic("createTimestampColumn supports at most two jsonPaths (primary + one fallback)")
|
||||
}
|
||||
|
||||
var text string
|
||||
switch len(jsonPaths) {
|
||||
case 1:
|
||||
text = "{reqsJsonPath[0]['" + jsonPaths[0] + "']['-']}"
|
||||
case 2:
|
||||
text = "{reqsJsonPath[0]['" + jsonPaths[0] + "'][\"{reqsJsonPath[0]['" + jsonPaths[1] + "']['-']}\"]}"
|
||||
}
|
||||
|
||||
// createTimestampColumn creates a timestamp column with custom formatting
|
||||
func createTimestampColumn(name, jsonPath string) map[string]any {
|
||||
return map[string]any{
|
||||
"name": name,
|
||||
"type": "factory",
|
||||
"jsonPath": jsonPaths[0],
|
||||
"jsonPath": jsonPath,
|
||||
"customProps": map[string]any{
|
||||
"disableEventBubbling": true,
|
||||
"items": []any{
|
||||
|
|
@ -692,7 +673,7 @@ func createTimestampColumn(name string, jsonPaths ...string) map[string]any {
|
|||
"data": map[string]any{
|
||||
"formatter": "timestamp",
|
||||
"id": "time-value",
|
||||
"text": text,
|
||||
"text": "{reqsJsonPath[0]['" + jsonPath + "']['-']}",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -224,20 +224,6 @@ func CreateAllCustomColumnsOverrides() []*dashboardv1alpha1.CustomColumnsOverrid
|
|||
createStringColumn("Name", ".name"),
|
||||
}),
|
||||
|
||||
// Factory details events. Event Time falls back to `.firstTimestamp`
|
||||
// because core/v1 Events (Helm, controller-runtime) populate only the
|
||||
// legacy timestamps while events.k8s.io/v1 Events populate only
|
||||
// `.eventTime`; taking the first non-null of the two covers both.
|
||||
createCustomColumnsOverride("factory-details-events", []any{
|
||||
createTimestampColumn("Last Seen", ".lastTimestamp", ".eventTime"),
|
||||
createTimestampColumn("Event Time", ".eventTime", ".firstTimestamp"),
|
||||
createStringColumn("Type", ".type"),
|
||||
createStringColumn("Reason", ".reason"),
|
||||
createStringColumn("Object", ".involvedObject.kind"),
|
||||
createStringColumn("Name", ".involvedObject.name"),
|
||||
createStringColumn("Message", ".message"),
|
||||
}),
|
||||
|
||||
// Factory status conditions
|
||||
createCustomColumnsOverride("factory-status-conditions", []any{
|
||||
createStringColumn("Type", ".type"),
|
||||
|
|
@ -425,14 +411,6 @@ func CreateAllCustomColumnsOverrides() []*dashboardv1alpha1.CustomColumnsOverrid
|
|||
createTimestampColumn("Taken At", ".spec.takenAt"),
|
||||
createTimestampColumn("Created", ".metadata.creationTimestamp"),
|
||||
}),
|
||||
|
||||
// Stock namespace backups cozystack io v1alpha1 restorejobs
|
||||
createCustomColumnsOverride("stock-namespace-/backups.cozystack.io/v1alpha1/restorejobs", []any{
|
||||
createCustomColumnWithJsonPath("Name", ".metadata.name", "RestoreJob", "", "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/restorejob-details/{reqsJsonPath[0]['.metadata.name']['-']}"),
|
||||
createStringColumn("Phase", ".status.phase"),
|
||||
createStringColumn("Backup", ".spec.backupRef.name"),
|
||||
createTimestampColumn("Created", ".metadata.creationTimestamp"),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -555,31 +533,6 @@ func CreateAllCustomFormsOverrides() []*dashboardv1alpha1.CustomFormsOverride {
|
|||
"backupClassName": listInputScemaItemBackupClass(),
|
||||
}),
|
||||
}),
|
||||
|
||||
// RestoreJobs form override - backups.cozystack.io/v1alpha1
|
||||
createCustomFormsOverride("default-/backups.cozystack.io/v1alpha1/restorejobs", map[string]any{
|
||||
"formItems": []any{
|
||||
createFormItem("metadata.name", "Name", "text"),
|
||||
createFormItem("metadata.namespace", "Namespace", "text"),
|
||||
createFormItem("spec.backupRef.name", "Backup", "text"),
|
||||
// Target application: leave empty to restore into the same application
|
||||
// as referenced by the selected Backup. Fill all three to restore into
|
||||
// a different application (e.g. rename, or restore into a new target).
|
||||
createFormItem("spec.targetApplicationRef.apiGroup", "Target Application API Group (optional, used to restore into a different application)", "text"),
|
||||
createFormItem("spec.targetApplicationRef.kind", "Target Application Kind (optional, used to restore into a different application)", "text"),
|
||||
createFormItem("spec.targetApplicationRef.name", "Target Application Name (optional, used to restore into a different application)", "text"),
|
||||
// Driver-specific options (key-value editor). Refer to the backup driver
|
||||
// documentation for supported keys.
|
||||
createFormItem("spec.options", "Options (driver-specific key/value pairs)", "object"),
|
||||
},
|
||||
"schema": createSchema(map[string]any{
|
||||
"backupRef": map[string]any{
|
||||
"properties": map[string]any{
|
||||
"name": listInputSchemaItemBackup(),
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1970,177 +1923,6 @@ func CreateAllFactories() []*dashboardv1alpha1.Factory {
|
|||
}
|
||||
backupSpec := createUnifiedFactory(backupConfig, backupTabs, []any{"/api/clusters/{2}/k8s/apis/backups.cozystack.io/v1alpha1/namespaces/{3}/backups/{6}"})
|
||||
|
||||
// RestoreJob details factory using unified approach
|
||||
restoreJobConfig := UnifiedResourceConfig{
|
||||
Name: "restorejob-details",
|
||||
ResourceType: "factory",
|
||||
Kind: "RestoreJob",
|
||||
Plural: "restorejobs",
|
||||
Title: "restorejob",
|
||||
}
|
||||
restoreJobTabs := []any{
|
||||
map[string]any{
|
||||
"key": "details",
|
||||
"label": "Details",
|
||||
"children": []any{
|
||||
contentCard("details-card", map[string]any{
|
||||
"marginBottom": "24px",
|
||||
}, []any{
|
||||
antdText("details-title", true, "RestoreJob details", map[string]any{
|
||||
"fontSize": 20,
|
||||
"marginBottom": "12px",
|
||||
}),
|
||||
spacer("details-spacer", 16),
|
||||
antdRow("details-grid", []any{48, 12}, []any{
|
||||
antdCol("col-left", 12, []any{
|
||||
antdFlexVertical("col-left-stack", 24, []any{
|
||||
antdFlexVertical("meta-name-block", 4, []any{
|
||||
antdText("meta-name-label", true, "Name", nil),
|
||||
parsedText("meta-name-value", "{reqsJsonPath[0]['.metadata.name']['-']}", nil),
|
||||
}),
|
||||
antdFlexVertical("meta-namespace-block", 8, []any{
|
||||
antdText("meta-namespace-label", true, "Namespace", nil),
|
||||
antdFlex("header-row", 6, []any{
|
||||
map[string]any{
|
||||
"type": "antdText",
|
||||
"data": map[string]any{
|
||||
"id": "header-badge",
|
||||
"text": "NS",
|
||||
"title": "namespace",
|
||||
"style": map[string]any{
|
||||
"backgroundColor": "#a25792ff",
|
||||
"borderRadius": "20px",
|
||||
"color": "#fff",
|
||||
"display": "inline-block",
|
||||
"fontFamily": "RedHatDisplay, Overpass, overpass, helvetica, arial, sans-serif",
|
||||
"fontSize": "15px",
|
||||
"fontWeight": 400,
|
||||
"lineHeight": "24px",
|
||||
"minWidth": 24,
|
||||
"padding": "0 9px",
|
||||
"textAlign": "center",
|
||||
"whiteSpace": "nowrap",
|
||||
},
|
||||
},
|
||||
},
|
||||
map[string]any{
|
||||
"type": "antdLink",
|
||||
"data": map[string]any{
|
||||
"id": "namespace-link",
|
||||
"text": "{reqsJsonPath[0]['.metadata.namespace']['-']}",
|
||||
"href": "/openapi-ui/{2}/{reqsJsonPath[0]['.metadata.namespace']['-']}/factory/marketplace",
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
antdFlexVertical("meta-created-block", 4, []any{
|
||||
antdText("created-time-label", true, "Created", nil),
|
||||
antdFlex("created-time-block", 6, []any{
|
||||
map[string]any{
|
||||
"type": "antdText",
|
||||
"data": map[string]any{
|
||||
"id": "created-time-icon",
|
||||
"text": "🌐",
|
||||
},
|
||||
},
|
||||
map[string]any{
|
||||
"type": "parsedText",
|
||||
"data": map[string]any{
|
||||
"formatter": "timestamp",
|
||||
"id": "created-time-value",
|
||||
"text": "{reqsJsonPath[0]['.metadata.creationTimestamp']['-']}",
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
antdCol("col-right", 12, []any{
|
||||
antdFlexVertical("col-right-stack", 24, []any{
|
||||
antdFlexVertical("status-phase-block", 4, []any{
|
||||
antdText("phase-label", true, "Phase", nil),
|
||||
parsedText("phase-value", "{reqsJsonPath[0]['.status.phase']['-']}", nil),
|
||||
}),
|
||||
antdFlexVertical("spec-backup-ref-block", 4, []any{
|
||||
antdText("backup-ref-label", true, "Backup Ref", nil),
|
||||
parsedText("backup-ref-value", "{reqsJsonPath[0]['.spec.backupRef.name']['-']}", nil),
|
||||
}),
|
||||
antdFlexVertical("spec-target-application-ref-block", 4, []any{
|
||||
antdText("target-application-ref-label", true, "Target Application", nil),
|
||||
// targetApplicationRef is optional — when absent, the restore targets
|
||||
// the same application as the selected Backup. Show a single friendly
|
||||
// fallback in that case instead of rendering "-.-/-".
|
||||
parsedText("target-application-ref-value", "{reqsJsonPath[0]['.spec.targetApplicationRef.name']['Same as backup']}", nil),
|
||||
}),
|
||||
antdFlexVertical("spec-options-target-namespace-block", 4, []any{
|
||||
antdText("options-target-namespace-label", true, "Target Namespace", nil),
|
||||
parsedText("options-target-namespace-value", "{reqsJsonPath[0]['.spec.options.targetNamespace']['-']}", nil),
|
||||
}),
|
||||
antdFlexVertical("spec-options-fail-if-target-exists-block", 4, []any{
|
||||
antdText("options-fail-if-target-exists-label", true, "Fail If Target Exists", nil),
|
||||
parsedText("options-fail-if-target-exists-value", "{reqsJsonPath[0]['.spec.options.failIfTargetExists']['-']}", nil),
|
||||
}),
|
||||
antdFlexVertical("spec-options-keep-original-pvc-block", 4, []any{
|
||||
antdText("options-keep-original-pvc-label", true, "Keep Original PVC", nil),
|
||||
parsedText("options-keep-original-pvc-value", "{reqsJsonPath[0]['.spec.options.keepOriginalPVC']['-']}", nil),
|
||||
}),
|
||||
antdFlexVertical("spec-options-keep-original-ip-mac-block", 4, []any{
|
||||
antdText("options-keep-original-ip-mac-label", true, "Keep Original IP/MAC", nil),
|
||||
parsedText("options-keep-original-ip-mac-value", "{reqsJsonPath[0]['.spec.options.keepOriginalIpAndMac']['-']}", nil),
|
||||
}),
|
||||
antdFlexVertical("status-started-at-block", 4, []any{
|
||||
antdText("started-at-label", true, "Started At", nil),
|
||||
antdFlex("started-at-time-block", 6, []any{
|
||||
map[string]any{
|
||||
"type": "antdText",
|
||||
"data": map[string]any{
|
||||
"id": "started-at-time-icon",
|
||||
"text": "🌐",
|
||||
},
|
||||
},
|
||||
map[string]any{
|
||||
"type": "parsedText",
|
||||
"data": map[string]any{
|
||||
"formatter": "timestamp",
|
||||
"id": "started-at-time-value",
|
||||
"text": "{reqsJsonPath[0]['.status.startedAt']['-']}",
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
antdFlexVertical("status-completed-at-block", 4, []any{
|
||||
antdText("completed-at-label", true, "Completed At", nil),
|
||||
antdFlex("completed-at-time-block", 6, []any{
|
||||
map[string]any{
|
||||
"type": "antdText",
|
||||
"data": map[string]any{
|
||||
"id": "completed-at-time-icon",
|
||||
"text": "🌐",
|
||||
},
|
||||
},
|
||||
map[string]any{
|
||||
"type": "parsedText",
|
||||
"data": map[string]any{
|
||||
"formatter": "timestamp",
|
||||
"id": "completed-at-time-value",
|
||||
"text": "{reqsJsonPath[0]['.status.completedAt']['-']}",
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
antdFlexVertical("status-message-block", 4, []any{
|
||||
antdText("message-label", true, "Message", nil),
|
||||
parsedText("message-value", "{reqsJsonPath[0]['.status.message']['-']}", nil),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
},
|
||||
}
|
||||
restoreJobSpec := createUnifiedFactory(restoreJobConfig, restoreJobTabs, []any{"/api/clusters/{2}/k8s/apis/backups.cozystack.io/v1alpha1/namespaces/{3}/restorejobs/{6}"})
|
||||
|
||||
// External IPs factory (filtered services)
|
||||
externalIPsTabs := []any{
|
||||
map[string]any{
|
||||
|
|
@ -2193,7 +1975,6 @@ func CreateAllFactories() []*dashboardv1alpha1.Factory {
|
|||
createFactory("plan-details", planSpec),
|
||||
createFactory("backupjob-details", backupJobSpec),
|
||||
createFactory("backup-details", backupSpec),
|
||||
createFactory("restorejob-details", restoreJobSpec),
|
||||
createFactory("external-ips", externalIPsSpec),
|
||||
}
|
||||
}
|
||||
|
|
@ -2213,10 +1994,9 @@ func CreateAllNavigations() []*dashboardv1alpha1.Navigation {
|
|||
"base-factory-namespaced-api-networking.k8s.io-v1-ingresses": "kube-ingress-details",
|
||||
"base-factory-namespaced-api-cozystack.io-v1alpha1-workloadmonitors": "workloadmonitor-details",
|
||||
// Backup resources (not ApplicationDefinitions, so ensureNavigation doesn't cover them)
|
||||
"base-factory-namespaced-api-backups.cozystack.io-v1alpha1-plans": "plan-details",
|
||||
"base-factory-namespaced-api-backups.cozystack.io-v1alpha1-backupjobs": "backupjob-details",
|
||||
"base-factory-namespaced-api-backups.cozystack.io-v1alpha1-backups": "backup-details",
|
||||
"base-factory-namespaced-api-backups.cozystack.io-v1alpha1-restorejobs": "restorejob-details",
|
||||
"base-factory-namespaced-api-backups.cozystack.io-v1alpha1-plans": "plan-details",
|
||||
"base-factory-namespaced-api-backups.cozystack.io-v1alpha1-backupjobs": "backupjob-details",
|
||||
"base-factory-namespaced-api-backups.cozystack.io-v1alpha1-backups": "backup-details",
|
||||
}
|
||||
|
||||
return []*dashboardv1alpha1.Navigation{
|
||||
|
|
@ -2341,19 +2121,6 @@ func listInputScemaItemBackupClass() map[string]any {
|
|||
}
|
||||
}
|
||||
|
||||
// listInputSchemaItemBackup returns a listInput schema overlay for selecting a Backup
|
||||
// from the current namespace (used by RestoreJob form for spec.backupRef.name).
|
||||
func listInputSchemaItemBackup() map[string]any {
|
||||
return map[string]any{
|
||||
"type": "listInput",
|
||||
"customProps": map[string]any{
|
||||
"valueUri": "/api/clusters/{cluster}/k8s/apis/backups.cozystack.io/v1alpha1/namespaces/{namespace}/backups",
|
||||
"keysToValue": []any{"metadata", "name"},
|
||||
"keysToLabel": []any{"metadata", "name"},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// backupClassSchema returns the schema for spec.backupClassName as listInput (BackupJob/Plan).
|
||||
func createSchema(customProps map[string]any) map[string]any {
|
||||
return map[string]any{
|
||||
|
|
|
|||
|
|
@ -4,13 +4,7 @@ import (
|
|||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
|
|
@ -28,29 +22,8 @@ import (
|
|||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
|
||||
cosiv1alpha1 "sigs.k8s.io/container-object-storage-interface-api/apis/objectstorage/v1alpha1"
|
||||
)
|
||||
|
||||
const (
|
||||
// namespaceMonitoringLabel is the namespace label that indicates which tenant
|
||||
// namespace hosts the monitoring stack (VictoriaMetrics/Prometheus).
|
||||
namespaceMonitoringLabel = "namespace.cozystack.io/monitoring"
|
||||
workloadLabelPrefix = "workloads.cozystack.io/"
|
||||
// workloadMonitorLabel is reserved: it names the WorkloadMonitor that owns
|
||||
// the Workload and is always set by the reconciler, so it is never copied
|
||||
// from monitor labels.
|
||||
workloadMonitorLabel = workloadLabelPrefix + "monitor"
|
||||
// vmSelectService is the well-known service name for VictoriaMetrics vmselect
|
||||
// within a monitoring namespace. Port 8481, path /select/0/prometheus.
|
||||
vmSelectService = "vmselect-shortterm"
|
||||
vmSelectPort = "8481"
|
||||
vmSelectPath = "/select/0/prometheus"
|
||||
)
|
||||
|
||||
// prometheusHTTPClient is a dedicated HTTP client for Prometheus queries,
|
||||
// avoiding the shared http.DefaultClient global.
|
||||
var prometheusHTTPClient = &http.Client{Timeout: 10 * time.Second}
|
||||
|
||||
// WorkloadMonitorReconciler reconciles a WorkloadMonitor object
|
||||
type WorkloadMonitorReconciler struct {
|
||||
client.Client
|
||||
|
|
@ -63,13 +36,6 @@ type WorkloadMonitorReconciler struct {
|
|||
// +kubebuilder:rbac:groups=cozystack.io,resources=workloads/status,verbs=get;update;patch
|
||||
// +kubebuilder:rbac:groups=core,resources=pods,verbs=get;list;watch
|
||||
// +kubebuilder:rbac:groups=core,resources=persistentvolumeclaims,verbs=get;list;watch
|
||||
// +kubebuilder:rbac:groups=core,resources=namespaces,verbs=get
|
||||
// +kubebuilder:rbac:groups=objectstorage.k8s.io,resources=bucketclaims,verbs=get;list;watch
|
||||
|
||||
// isBucketClaimReady checks if the BucketClaim has been provisioned.
|
||||
func (r *WorkloadMonitorReconciler) isBucketClaimReady(bc *cosiv1alpha1.BucketClaim) bool {
|
||||
return bc.Status.BucketReady
|
||||
}
|
||||
|
||||
// isServiceReady checks if the service has an external IP bound
|
||||
func (r *WorkloadMonitorReconciler) isServiceReady(svc *corev1.Service) bool {
|
||||
|
|
@ -135,190 +101,6 @@ func updateOwnerReferences(obj metav1.Object, monitor client.Object) {
|
|||
obj.SetOwnerReferences(owners)
|
||||
}
|
||||
|
||||
// resolvePrometheusURL returns the Prometheus-compatible API base URL for the given namespace.
|
||||
// It reads the namespace.cozystack.io/monitoring label to find the monitoring namespace,
|
||||
// then constructs the vmselect URL. Returns empty string if monitoring is not configured.
|
||||
func (r *WorkloadMonitorReconciler) resolvePrometheusURL(ctx context.Context, namespace string) string {
|
||||
logger := log.FromContext(ctx)
|
||||
ns := &corev1.Namespace{}
|
||||
if err := r.Get(ctx, types.NamespacedName{Name: namespace}, ns); err != nil {
|
||||
logger.V(1).Info("Failed to read namespace for monitoring resolution", "namespace", namespace, "error", err)
|
||||
return ""
|
||||
}
|
||||
monitoringNS := ns.Labels[namespaceMonitoringLabel]
|
||||
if monitoringNS == "" {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("http://%s.%s.svc:%s%s", vmSelectService, monitoringNS, vmSelectPort, vmSelectPath)
|
||||
}
|
||||
|
||||
// bucketMetrics holds size metrics for a single bucket, keyed by metric name.
|
||||
type bucketMetrics struct {
|
||||
LogicalSize int64
|
||||
PhysicalSize int64
|
||||
HasLogical bool
|
||||
HasPhysical bool
|
||||
}
|
||||
|
||||
// queryAllBucketMetrics fetches SeaweedFS bucket size metrics for the given
|
||||
// bucket names in a single Prometheus query and returns them keyed by bucket
|
||||
// name. The query is scoped to only the requested buckets to avoid fetching
|
||||
// metrics for buckets belonging to other WorkloadMonitors.
|
||||
func (r *WorkloadMonitorReconciler) queryAllBucketMetrics(ctx context.Context, prometheusBaseURL string, bucketNames []string) map[string]*bucketMetrics {
|
||||
result := make(map[string]*bucketMetrics)
|
||||
if prometheusBaseURL == "" || len(bucketNames) == 0 {
|
||||
return result
|
||||
}
|
||||
logger := log.FromContext(ctx)
|
||||
|
||||
query := fmt.Sprintf(`{__name__=~"SeaweedFS_s3_bucket_(size|physical_size)_bytes",bucket=~"%s"}`, strings.Join(bucketNames, "|"))
|
||||
u, err := url.Parse(strings.TrimRight(prometheusBaseURL, "/") + "/api/v1/query")
|
||||
if err != nil {
|
||||
logger.Error(err, "Failed to parse Prometheus URL")
|
||||
return result
|
||||
}
|
||||
u.RawQuery = url.Values{"query": {query}}.Encode()
|
||||
|
||||
httpCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequestWithContext(httpCtx, http.MethodGet, u.String(), nil)
|
||||
if err != nil {
|
||||
logger.Error(err, "Failed to create Prometheus request")
|
||||
return result
|
||||
}
|
||||
|
||||
resp, err := prometheusHTTPClient.Do(req)
|
||||
if err != nil {
|
||||
logger.V(1).Info("Failed to query Prometheus for bucket metrics", "error", err)
|
||||
return result
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
logger.V(1).Info("Prometheus returned non-OK status for bucket metrics", "status", resp.StatusCode)
|
||||
return result
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(io.LimitReader(resp.Body, 4<<20))
|
||||
if err != nil {
|
||||
logger.Error(err, "Failed to read Prometheus response")
|
||||
return result
|
||||
}
|
||||
|
||||
var promResp struct {
|
||||
Status string `json:"status"`
|
||||
Data struct {
|
||||
Result []struct {
|
||||
Metric map[string]string `json:"metric"`
|
||||
Value [2]json.RawMessage `json:"value"`
|
||||
} `json:"result"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &promResp); err != nil {
|
||||
logger.Error(err, "Failed to parse Prometheus response")
|
||||
return result
|
||||
}
|
||||
if promResp.Status != "success" {
|
||||
return result
|
||||
}
|
||||
|
||||
for _, r := range promResp.Data.Result {
|
||||
bucket := r.Metric["bucket"]
|
||||
metricName := r.Metric["__name__"]
|
||||
if bucket == "" || metricName == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
var valueStr string
|
||||
if err := json.Unmarshal(r.Value[1], &valueStr); err != nil {
|
||||
continue
|
||||
}
|
||||
val, err := strconv.ParseFloat(valueStr, 64)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
bm, ok := result[bucket]
|
||||
if !ok {
|
||||
bm = &bucketMetrics{}
|
||||
result[bucket] = bm
|
||||
}
|
||||
|
||||
switch metricName {
|
||||
case "SeaweedFS_s3_bucket_size_bytes":
|
||||
bm.LogicalSize = int64(val)
|
||||
bm.HasLogical = true
|
||||
case "SeaweedFS_s3_bucket_physical_size_bytes":
|
||||
bm.PhysicalSize = int64(val)
|
||||
bm.HasPhysical = true
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// reconcileBucketClaimForMonitor creates or updates a Workload object for the given BucketClaim and WorkloadMonitor.
|
||||
func (r *WorkloadMonitorReconciler) reconcileBucketClaimForMonitor(
|
||||
ctx context.Context,
|
||||
monitor *cozyv1alpha1.WorkloadMonitor,
|
||||
bc cosiv1alpha1.BucketClaim,
|
||||
allMetrics map[string]*bucketMetrics,
|
||||
) error {
|
||||
logger := log.FromContext(ctx)
|
||||
workload := &cozyv1alpha1.Workload{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: fmt.Sprintf("bucket-%s", bc.Name),
|
||||
Namespace: bc.Namespace,
|
||||
Labels: make(map[string]string, len(bc.Labels)),
|
||||
},
|
||||
}
|
||||
|
||||
resources := make(map[string]resource.Quantity)
|
||||
|
||||
// Look up pre-fetched bucket metrics by the SeaweedFS bucket name.
|
||||
// bc.Status.BucketName is the COSI Bucket name, which the COSI driver
|
||||
// uses directly as the SeaweedFS bucket name.
|
||||
if bm, ok := allMetrics[bc.Status.BucketName]; ok {
|
||||
if bm.HasLogical {
|
||||
resources["s3-storage-bytes"] = *resource.NewQuantity(bm.LogicalSize, resource.BinarySI)
|
||||
}
|
||||
if bm.HasPhysical {
|
||||
resources["s3-physical-storage-bytes"] = *resource.NewQuantity(bm.PhysicalSize, resource.BinarySI)
|
||||
}
|
||||
}
|
||||
|
||||
monitorLabels := r.getMonitorLabels(monitor)
|
||||
_, err := ctrl.CreateOrUpdate(ctx, r.Client, workload, func() error {
|
||||
updateOwnerReferences(workload.GetObjectMeta(), &bc)
|
||||
|
||||
if workload.Labels == nil {
|
||||
workload.Labels = make(map[string]string)
|
||||
}
|
||||
// Apply monitor-level labels first so source-object labels can override on conflict
|
||||
for k, v := range monitorLabels {
|
||||
workload.Labels[k] = v
|
||||
}
|
||||
for k, v := range bc.Labels {
|
||||
workload.Labels[k] = v
|
||||
}
|
||||
workload.Labels[workloadMonitorLabel] = monitor.Name
|
||||
|
||||
workload.Status.Kind = monitor.Spec.Kind
|
||||
workload.Status.Type = monitor.Spec.Type
|
||||
workload.Status.Resources = resources
|
||||
workload.Status.Operational = r.isBucketClaimReady(&bc)
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
logger.Error(err, "Failed to CreateOrUpdate Workload", "workload", workload.Name)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// reconcileServiceForMonitor creates or updates a Workload object for the given Service and WorkloadMonitor.
|
||||
func (r *WorkloadMonitorReconciler) reconcileServiceForMonitor(
|
||||
ctx context.Context,
|
||||
|
|
@ -355,22 +137,14 @@ func (r *WorkloadMonitorReconciler) reconcileServiceForMonitor(
|
|||
resourceLabel = fmt.Sprintf("%s.ipaddresspool.metallb.io/requests.ipaddresses", resourceLabel)
|
||||
resources[resourceLabel] = quantity
|
||||
|
||||
monitorLabels := r.getMonitorLabels(monitor)
|
||||
_, err := ctrl.CreateOrUpdate(ctx, r.Client, workload, func() error {
|
||||
// Update owner references with the new monitor
|
||||
updateOwnerReferences(workload.GetObjectMeta(), &svc)
|
||||
|
||||
// Apply monitor-level labels first so source-object labels can override on conflict
|
||||
if workload.Labels == nil {
|
||||
workload.Labels = make(map[string]string)
|
||||
}
|
||||
for k, v := range monitorLabels {
|
||||
workload.Labels[k] = v
|
||||
}
|
||||
for k, v := range svc.Labels {
|
||||
workload.Labels[k] = v
|
||||
}
|
||||
workload.Labels[workloadMonitorLabel] = monitor.Name
|
||||
workload.Labels["workloads.cozystack.io/monitor"] = monitor.Name
|
||||
|
||||
// Fill Workload status fields:
|
||||
workload.Status.Kind = monitor.Spec.Kind
|
||||
|
|
@ -414,22 +188,14 @@ func (r *WorkloadMonitorReconciler) reconcilePVCForMonitor(
|
|||
resources[resourceLabel] = resourceQuantity
|
||||
}
|
||||
|
||||
monitorLabels := r.getMonitorLabels(monitor)
|
||||
_, err := ctrl.CreateOrUpdate(ctx, r.Client, workload, func() error {
|
||||
// Update owner references with the new monitor
|
||||
updateOwnerReferences(workload.GetObjectMeta(), &pvc)
|
||||
|
||||
// Apply monitor-level labels first so source-object labels can override on conflict
|
||||
if workload.Labels == nil {
|
||||
workload.Labels = make(map[string]string)
|
||||
}
|
||||
for k, v := range monitorLabels {
|
||||
workload.Labels[k] = v
|
||||
}
|
||||
for k, v := range pvc.Labels {
|
||||
workload.Labels[k] = v
|
||||
}
|
||||
workload.Labels[workloadMonitorLabel] = monitor.Name
|
||||
workload.Labels["workloads.cozystack.io/monitor"] = monitor.Name
|
||||
|
||||
// Fill Workload status fields:
|
||||
workload.Status.Kind = monitor.Spec.Kind
|
||||
|
|
@ -496,22 +262,14 @@ func (r *WorkloadMonitorReconciler) reconcilePodForMonitor(
|
|||
}
|
||||
|
||||
metaLabels := r.getWorkloadMetadata(&pod)
|
||||
monitorLabels := r.getMonitorLabels(monitor)
|
||||
_, err := ctrl.CreateOrUpdate(ctx, r.Client, workload, func() error {
|
||||
// Update owner references with the new monitor
|
||||
updateOwnerReferences(workload.GetObjectMeta(), &pod)
|
||||
|
||||
// Apply monitor-level labels first so source-object labels can override on conflict
|
||||
if workload.Labels == nil {
|
||||
workload.Labels = make(map[string]string)
|
||||
}
|
||||
for k, v := range monitorLabels {
|
||||
workload.Labels[k] = v
|
||||
}
|
||||
for k, v := range pod.Labels {
|
||||
workload.Labels[k] = v
|
||||
}
|
||||
workload.Labels[workloadMonitorLabel] = monitor.Name
|
||||
workload.Labels["workloads.cozystack.io/monitor"] = monitor.Name
|
||||
|
||||
// Add workload meta to labels
|
||||
for k, v := range metaLabels {
|
||||
|
|
@ -617,34 +375,6 @@ func (r *WorkloadMonitorReconciler) Reconcile(ctx context.Context, req ctrl.Requ
|
|||
}
|
||||
}
|
||||
|
||||
bucketClaimList := &cosiv1alpha1.BucketClaimList{}
|
||||
if err := r.List(
|
||||
ctx,
|
||||
bucketClaimList,
|
||||
client.InNamespace(monitor.Namespace),
|
||||
client.MatchingLabels(monitor.Spec.Selector),
|
||||
); err != nil {
|
||||
logger.Error(err, "Unable to list BucketClaims for WorkloadMonitor", "monitor", monitor.Name)
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
if len(bucketClaimList.Items) > 0 {
|
||||
bucketPromURL := r.resolvePrometheusURL(ctx, monitor.Namespace)
|
||||
var bucketNames []string
|
||||
for _, bc := range bucketClaimList.Items {
|
||||
if bc.Status.BucketName != "" {
|
||||
bucketNames = append(bucketNames, bc.Status.BucketName)
|
||||
}
|
||||
}
|
||||
allBucketMetrics := r.queryAllBucketMetrics(ctx, bucketPromURL, bucketNames)
|
||||
for _, bc := range bucketClaimList.Items {
|
||||
if err := r.reconcileBucketClaimForMonitor(ctx, monitor, bc, allBucketMetrics); err != nil {
|
||||
logger.Error(err, "Failed to reconcile Workload for BucketClaim", "BucketClaim", bc.Name)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update WorkloadMonitor status based on observed pods
|
||||
monitor.Status.ObservedReplicas = observedReplicas
|
||||
monitor.Status.AvailableReplicas = availableReplicas
|
||||
|
|
@ -658,12 +388,10 @@ func (r *WorkloadMonitorReconciler) Reconcile(ctx context.Context, req ctrl.Requ
|
|||
fresh.Status.ObservedReplicas = observedReplicas
|
||||
fresh.Status.AvailableReplicas = availableReplicas
|
||||
|
||||
// Default to operational = true, but check MinReplicas if set.
|
||||
// Use fresh.Spec to avoid making decisions based on a stale cached copy
|
||||
// when the spec was updated between the initial read and this retry.
|
||||
fresh.Status.Operational = pointer.Bool(true)
|
||||
if fresh.Spec.MinReplicas != nil && availableReplicas < *fresh.Spec.MinReplicas {
|
||||
fresh.Status.Operational = pointer.Bool(false)
|
||||
// Default to operational = true, but check MinReplicas if set
|
||||
monitor.Status.Operational = pointer.Bool(true)
|
||||
if monitor.Spec.MinReplicas != nil && availableReplicas < *monitor.Spec.MinReplicas {
|
||||
monitor.Status.Operational = pointer.Bool(false)
|
||||
}
|
||||
return r.Status().Update(ctx, fresh)
|
||||
})
|
||||
|
|
@ -672,12 +400,7 @@ func (r *WorkloadMonitorReconciler) Reconcile(ctx context.Context, req ctrl.Requ
|
|||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
// Requeue periodically if there are BucketClaims to keep sizes up to date.
|
||||
// Bucket sizes come from Prometheus metrics that update every 60s.
|
||||
if len(bucketClaimList.Items) > 0 {
|
||||
return ctrl.Result{RequeueAfter: 60 * time.Second}, nil
|
||||
}
|
||||
|
||||
// Return without requeue if we want purely event-driven reconciliations
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
|
|
@ -696,11 +419,6 @@ func (r *WorkloadMonitorReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
|||
&corev1.PersistentVolumeClaim{},
|
||||
handler.EnqueueRequestsFromMapFunc(mapObjectToMonitor(&corev1.PersistentVolumeClaim{}, r.Client)),
|
||||
).
|
||||
// Watch BucketClaims for S3 bucket billing
|
||||
Watches(
|
||||
&cosiv1alpha1.BucketClaim{},
|
||||
handler.EnqueueRequestsFromMapFunc(mapObjectToMonitor(&cosiv1alpha1.BucketClaim{}, r.Client)),
|
||||
).
|
||||
// Watch for changes to Workload objects we create (owned by WorkloadMonitor)
|
||||
Owns(&cozyv1alpha1.Workload{}).
|
||||
Complete(r)
|
||||
|
|
@ -754,21 +472,3 @@ func (r *WorkloadMonitorReconciler) getWorkloadMetadata(obj client.Object) map[s
|
|||
}
|
||||
return labels
|
||||
}
|
||||
|
||||
// getMonitorLabels extracts workloads.cozystack.io/* labels from a WorkloadMonitor
|
||||
// so they can be propagated onto Workload objects created for pods, PVCs, services,
|
||||
// or bucket claims. The monitor label "workloads.cozystack.io/monitor" is reserved
|
||||
// and set separately per Workload, so it is excluded here.
|
||||
func (r *WorkloadMonitorReconciler) getMonitorLabels(monitor *cozyv1alpha1.WorkloadMonitor) map[string]string {
|
||||
labels := make(map[string]string)
|
||||
for k, v := range monitor.GetLabels() {
|
||||
if !strings.HasPrefix(k, workloadLabelPrefix) {
|
||||
continue
|
||||
}
|
||||
if k == workloadMonitorLabel {
|
||||
continue
|
||||
}
|
||||
labels[k] = v
|
||||
}
|
||||
return labels
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,846 +0,0 @@
|
|||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
cosiv1alpha1 "sigs.k8s.io/container-object-storage-interface-api/apis/objectstorage/v1alpha1"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
)
|
||||
|
||||
func TestReconcile_OperationalStatusPersisted(t *testing.T) {
|
||||
scheme := runtime.NewScheme()
|
||||
_ = cozyv1alpha1.AddToScheme(scheme)
|
||||
_ = corev1.AddToScheme(scheme)
|
||||
_ = cosiv1alpha1.AddToScheme(scheme)
|
||||
|
||||
minReplicas := int32(2)
|
||||
monitor := &cozyv1alpha1.WorkloadMonitor{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-monitor",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: cozyv1alpha1.WorkloadMonitorSpec{
|
||||
Selector: map[string]string{"app": "test"},
|
||||
MinReplicas: &minReplicas,
|
||||
},
|
||||
}
|
||||
|
||||
// Create one pod that is ready — availableReplicas=1 < minReplicas=2, so Operational should be false
|
||||
pod := &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-pod-1",
|
||||
Namespace: "default",
|
||||
Labels: map[string]string{"app": "test"},
|
||||
},
|
||||
Status: corev1.PodStatus{
|
||||
Conditions: []corev1.PodCondition{
|
||||
{Type: corev1.PodReady, Status: corev1.ConditionTrue},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
fakeClient := fake.NewClientBuilder().
|
||||
WithScheme(scheme).
|
||||
WithObjects(monitor, pod).
|
||||
WithStatusSubresource(monitor).
|
||||
Build()
|
||||
|
||||
reconciler := &WorkloadMonitorReconciler{Client: fakeClient, Scheme: scheme}
|
||||
req := reconcile.Request{NamespacedName: types.NamespacedName{Name: "test-monitor", Namespace: "default"}}
|
||||
|
||||
_, err := reconciler.Reconcile(context.TODO(), req)
|
||||
if err != nil {
|
||||
t.Fatalf("Reconcile returned error: %v", err)
|
||||
}
|
||||
|
||||
// Fetch the monitor back from fake client and check Operational is persisted
|
||||
updated := &cozyv1alpha1.WorkloadMonitor{}
|
||||
if err := fakeClient.Get(context.TODO(), req.NamespacedName, updated); err != nil {
|
||||
t.Fatalf("Failed to get updated WorkloadMonitor: %v", err)
|
||||
}
|
||||
|
||||
if updated.Status.Operational == nil {
|
||||
t.Fatal("Expected Operational to be set, got nil")
|
||||
}
|
||||
if *updated.Status.Operational {
|
||||
t.Error("Expected Operational=false (1 available < 2 minReplicas), got true")
|
||||
}
|
||||
if updated.Status.ObservedReplicas != 1 {
|
||||
t.Errorf("Expected ObservedReplicas=1, got %d", updated.Status.ObservedReplicas)
|
||||
}
|
||||
if updated.Status.AvailableReplicas != 1 {
|
||||
t.Errorf("Expected AvailableReplicas=1, got %d", updated.Status.AvailableReplicas)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReconcile_OperationalTrue_WhenEnoughReplicas(t *testing.T) {
|
||||
scheme := runtime.NewScheme()
|
||||
_ = cozyv1alpha1.AddToScheme(scheme)
|
||||
_ = corev1.AddToScheme(scheme)
|
||||
_ = cosiv1alpha1.AddToScheme(scheme)
|
||||
|
||||
minReplicas := int32(1)
|
||||
monitor := &cozyv1alpha1.WorkloadMonitor{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-monitor",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: cozyv1alpha1.WorkloadMonitorSpec{
|
||||
Selector: map[string]string{"app": "test"},
|
||||
MinReplicas: &minReplicas,
|
||||
},
|
||||
}
|
||||
|
||||
pod := &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-pod-1",
|
||||
Namespace: "default",
|
||||
Labels: map[string]string{"app": "test"},
|
||||
},
|
||||
Status: corev1.PodStatus{
|
||||
Conditions: []corev1.PodCondition{
|
||||
{Type: corev1.PodReady, Status: corev1.ConditionTrue},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
fakeClient := fake.NewClientBuilder().
|
||||
WithScheme(scheme).
|
||||
WithObjects(monitor, pod).
|
||||
WithStatusSubresource(monitor).
|
||||
Build()
|
||||
|
||||
reconciler := &WorkloadMonitorReconciler{Client: fakeClient, Scheme: scheme}
|
||||
req := reconcile.Request{NamespacedName: types.NamespacedName{Name: "test-monitor", Namespace: "default"}}
|
||||
|
||||
_, err := reconciler.Reconcile(context.TODO(), req)
|
||||
if err != nil {
|
||||
t.Fatalf("Reconcile returned error: %v", err)
|
||||
}
|
||||
|
||||
updated := &cozyv1alpha1.WorkloadMonitor{}
|
||||
if err := fakeClient.Get(context.TODO(), req.NamespacedName, updated); err != nil {
|
||||
t.Fatalf("Failed to get updated WorkloadMonitor: %v", err)
|
||||
}
|
||||
|
||||
if updated.Status.Operational == nil {
|
||||
t.Fatal("Expected Operational to be set, got nil")
|
||||
}
|
||||
if !*updated.Status.Operational {
|
||||
t.Error("Expected Operational=true (1 available >= 1 minReplicas), got false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetMonitorLabels(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
labels map[string]string
|
||||
expected map[string]string
|
||||
}{
|
||||
{
|
||||
name: "nil labels",
|
||||
labels: nil,
|
||||
expected: map[string]string{},
|
||||
},
|
||||
{
|
||||
name: "only workloads.cozystack.io/* labels are propagated",
|
||||
labels: map[string]string{
|
||||
"workloads.cozystack.io/resource-preset": "medium",
|
||||
"app.kubernetes.io/name": "postgres",
|
||||
"custom.example.com/team": "platform",
|
||||
},
|
||||
expected: map[string]string{
|
||||
"workloads.cozystack.io/resource-preset": "medium",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "monitor label is reserved and excluded",
|
||||
labels: map[string]string{
|
||||
"workloads.cozystack.io/resource-preset": "small",
|
||||
"workloads.cozystack.io/monitor": "should-be-dropped",
|
||||
},
|
||||
expected: map[string]string{
|
||||
"workloads.cozystack.io/resource-preset": "small",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multiple workloads.cozystack.io labels propagate",
|
||||
labels: map[string]string{
|
||||
"workloads.cozystack.io/resource-preset": "large",
|
||||
"workloads.cozystack.io/tier": "db",
|
||||
},
|
||||
expected: map[string]string{
|
||||
"workloads.cozystack.io/resource-preset": "large",
|
||||
"workloads.cozystack.io/tier": "db",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no matching labels returns empty map",
|
||||
labels: map[string]string{
|
||||
"app.kubernetes.io/name": "postgres",
|
||||
},
|
||||
expected: map[string]string{},
|
||||
},
|
||||
}
|
||||
|
||||
r := &WorkloadMonitorReconciler{}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
monitor := &cozyv1alpha1.WorkloadMonitor{
|
||||
ObjectMeta: metav1.ObjectMeta{Labels: tc.labels},
|
||||
}
|
||||
got := r.getMonitorLabels(monitor)
|
||||
if len(got) != len(tc.expected) {
|
||||
t.Fatalf("expected %d labels, got %d (%v)", len(tc.expected), len(got), got)
|
||||
}
|
||||
for k, v := range tc.expected {
|
||||
if gv, ok := got[k]; !ok || gv != v {
|
||||
t.Errorf("expected label %q=%q, got %q", k, v, gv)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestReconcile_MonitorLabelsPropagatedToPodWorkload(t *testing.T) {
|
||||
scheme := runtime.NewScheme()
|
||||
_ = cozyv1alpha1.AddToScheme(scheme)
|
||||
_ = corev1.AddToScheme(scheme)
|
||||
|
||||
monitor := &cozyv1alpha1.WorkloadMonitor{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-monitor",
|
||||
Namespace: "default",
|
||||
Labels: map[string]string{
|
||||
"workloads.cozystack.io/resource-preset": "medium",
|
||||
"app.kubernetes.io/name": "ignored-not-propagated",
|
||||
},
|
||||
},
|
||||
Spec: cozyv1alpha1.WorkloadMonitorSpec{
|
||||
Selector: map[string]string{"app": "test"},
|
||||
Kind: "postgres",
|
||||
Type: "postgres",
|
||||
},
|
||||
}
|
||||
|
||||
pod := &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-pod-1",
|
||||
Namespace: "default",
|
||||
Labels: map[string]string{
|
||||
"app": "test",
|
||||
"app.kubernetes.io/name": "pod-wins-on-conflict",
|
||||
},
|
||||
},
|
||||
Status: corev1.PodStatus{
|
||||
Conditions: []corev1.PodCondition{
|
||||
{Type: corev1.PodReady, Status: corev1.ConditionTrue},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
fakeClient := fake.NewClientBuilder().
|
||||
WithScheme(scheme).
|
||||
WithObjects(monitor, pod).
|
||||
WithStatusSubresource(monitor).
|
||||
Build()
|
||||
|
||||
reconciler := &WorkloadMonitorReconciler{Client: fakeClient, Scheme: scheme}
|
||||
req := reconcile.Request{NamespacedName: types.NamespacedName{Name: "test-monitor", Namespace: "default"}}
|
||||
if _, err := reconciler.Reconcile(context.TODO(), req); err != nil {
|
||||
t.Fatalf("Reconcile returned error: %v", err)
|
||||
}
|
||||
|
||||
workload := &cozyv1alpha1.Workload{}
|
||||
if err := fakeClient.Get(context.TODO(), types.NamespacedName{Name: "pod-test-pod-1", Namespace: "default"}, workload); err != nil {
|
||||
t.Fatalf("Failed to get Workload: %v", err)
|
||||
}
|
||||
|
||||
if got := workload.Labels["workloads.cozystack.io/resource-preset"]; got != "medium" {
|
||||
t.Errorf("expected monitor label propagated, got %q", got)
|
||||
}
|
||||
// Non-workloads.cozystack.io monitor labels must not be copied
|
||||
if _, ok := workload.Labels["app.kubernetes.io/name"]; !ok {
|
||||
t.Error("expected pod label to be present on Workload")
|
||||
}
|
||||
// Source-object label takes precedence on conflict
|
||||
if got := workload.Labels["app.kubernetes.io/name"]; got != "pod-wins-on-conflict" {
|
||||
t.Errorf("expected pod label to win on conflict, got %q", got)
|
||||
}
|
||||
// Reserved monitor label is always set from the monitor name
|
||||
if got := workload.Labels["workloads.cozystack.io/monitor"]; got != "test-monitor" {
|
||||
t.Errorf("expected monitor-name label, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReconcile_BackwardCompat_NoMonitorLabels(t *testing.T) {
|
||||
scheme := runtime.NewScheme()
|
||||
_ = cozyv1alpha1.AddToScheme(scheme)
|
||||
_ = corev1.AddToScheme(scheme)
|
||||
|
||||
monitor := &cozyv1alpha1.WorkloadMonitor{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-monitor",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: cozyv1alpha1.WorkloadMonitorSpec{
|
||||
Selector: map[string]string{"app": "test"},
|
||||
},
|
||||
}
|
||||
|
||||
pod := &corev1.Pod{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-pod-1",
|
||||
Namespace: "default",
|
||||
Labels: map[string]string{"app": "test"},
|
||||
},
|
||||
Status: corev1.PodStatus{
|
||||
Conditions: []corev1.PodCondition{
|
||||
{Type: corev1.PodReady, Status: corev1.ConditionTrue},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
fakeClient := fake.NewClientBuilder().
|
||||
WithScheme(scheme).
|
||||
WithObjects(monitor, pod).
|
||||
WithStatusSubresource(monitor).
|
||||
Build()
|
||||
|
||||
reconciler := &WorkloadMonitorReconciler{Client: fakeClient, Scheme: scheme}
|
||||
req := reconcile.Request{NamespacedName: types.NamespacedName{Name: "test-monitor", Namespace: "default"}}
|
||||
if _, err := reconciler.Reconcile(context.TODO(), req); err != nil {
|
||||
t.Fatalf("Reconcile returned error: %v", err)
|
||||
}
|
||||
|
||||
workload := &cozyv1alpha1.Workload{}
|
||||
if err := fakeClient.Get(context.TODO(), types.NamespacedName{Name: "pod-test-pod-1", Namespace: "default"}, workload); err != nil {
|
||||
t.Fatalf("Failed to get Workload: %v", err)
|
||||
}
|
||||
for k := range workload.Labels {
|
||||
if strings.HasPrefix(k, "workloads.cozystack.io/") && k != "workloads.cozystack.io/monitor" {
|
||||
t.Errorf("unexpected workload label present: %q", k)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestReconcile_OperationalTrue_WhenNoMinReplicas(t *testing.T) {
|
||||
scheme := runtime.NewScheme()
|
||||
_ = cozyv1alpha1.AddToScheme(scheme)
|
||||
_ = corev1.AddToScheme(scheme)
|
||||
_ = cosiv1alpha1.AddToScheme(scheme)
|
||||
|
||||
monitor := &cozyv1alpha1.WorkloadMonitor{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-monitor",
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: cozyv1alpha1.WorkloadMonitorSpec{
|
||||
Selector: map[string]string{"app": "test"},
|
||||
// No MinReplicas — should default to operational=true
|
||||
},
|
||||
}
|
||||
|
||||
fakeClient := fake.NewClientBuilder().
|
||||
WithScheme(scheme).
|
||||
WithObjects(monitor).
|
||||
WithStatusSubresource(monitor).
|
||||
Build()
|
||||
|
||||
reconciler := &WorkloadMonitorReconciler{Client: fakeClient, Scheme: scheme}
|
||||
req := reconcile.Request{NamespacedName: types.NamespacedName{Name: "test-monitor", Namespace: "default"}}
|
||||
|
||||
_, err := reconciler.Reconcile(context.TODO(), req)
|
||||
if err != nil {
|
||||
t.Fatalf("Reconcile returned error: %v", err)
|
||||
}
|
||||
|
||||
updated := &cozyv1alpha1.WorkloadMonitor{}
|
||||
if err := fakeClient.Get(context.TODO(), req.NamespacedName, updated); err != nil {
|
||||
t.Fatalf("Failed to get updated WorkloadMonitor: %v", err)
|
||||
}
|
||||
|
||||
if updated.Status.Operational == nil {
|
||||
t.Fatal("Expected Operational to be set, got nil")
|
||||
}
|
||||
if !*updated.Status.Operational {
|
||||
t.Error("Expected Operational=true (no MinReplicas constraint), got false")
|
||||
}
|
||||
}
|
||||
|
||||
func newTestScheme() *runtime.Scheme {
|
||||
s := runtime.NewScheme()
|
||||
_ = cozyv1alpha1.AddToScheme(s)
|
||||
_ = corev1.AddToScheme(s)
|
||||
_ = cosiv1alpha1.AddToScheme(s)
|
||||
return s
|
||||
}
|
||||
|
||||
func TestReconcileBucketClaimCreatesWorkload(t *testing.T) {
|
||||
s := newTestScheme()
|
||||
|
||||
monitor := &cozyv1alpha1.WorkloadMonitor{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "my-bucket",
|
||||
Namespace: "tenant-demo",
|
||||
},
|
||||
Spec: cozyv1alpha1.WorkloadMonitorSpec{
|
||||
Kind: "bucket",
|
||||
Type: "s3",
|
||||
Selector: map[string]string{
|
||||
"app.kubernetes.io/instance": "my-bucket",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
bc := &cosiv1alpha1.BucketClaim{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "my-bucket",
|
||||
Namespace: "tenant-demo",
|
||||
Labels: map[string]string{
|
||||
"app.kubernetes.io/instance": "my-bucket",
|
||||
},
|
||||
},
|
||||
Spec: cosiv1alpha1.BucketClaimSpec{
|
||||
BucketClassName: "seaweedfs",
|
||||
Protocols: []cosiv1alpha1.Protocol{cosiv1alpha1.ProtocolS3},
|
||||
},
|
||||
Status: cosiv1alpha1.BucketClaimStatus{
|
||||
BucketReady: true,
|
||||
BucketName: "cosi-abc123",
|
||||
},
|
||||
}
|
||||
|
||||
fakeClient := fake.NewClientBuilder().
|
||||
WithScheme(s).
|
||||
WithObjects(monitor, bc).
|
||||
WithStatusSubresource(monitor).
|
||||
Build()
|
||||
|
||||
reconciler := &WorkloadMonitorReconciler{Client: fakeClient, Scheme: s}
|
||||
req := reconcile.Request{NamespacedName: types.NamespacedName{
|
||||
Name: "my-bucket",
|
||||
Namespace: "tenant-demo",
|
||||
}}
|
||||
|
||||
_, err := reconciler.Reconcile(context.TODO(), req)
|
||||
if err != nil {
|
||||
t.Fatalf("Reconcile returned error: %v", err)
|
||||
}
|
||||
|
||||
workload := &cozyv1alpha1.Workload{}
|
||||
err = fakeClient.Get(context.TODO(), types.NamespacedName{
|
||||
Name: "bucket-my-bucket",
|
||||
Namespace: "tenant-demo",
|
||||
}, workload)
|
||||
if err != nil {
|
||||
t.Fatalf("expected Workload to be created, got error: %v", err)
|
||||
}
|
||||
|
||||
if workload.Status.Kind != "bucket" {
|
||||
t.Errorf("expected Kind=bucket, got %q", workload.Status.Kind)
|
||||
}
|
||||
if workload.Status.Type != "s3" {
|
||||
t.Errorf("expected Type=s3, got %q", workload.Status.Type)
|
||||
}
|
||||
if !workload.Status.Operational {
|
||||
t.Error("expected Operational=true for ready BucketClaim")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReconcileBucketClaimNotReady(t *testing.T) {
|
||||
s := newTestScheme()
|
||||
|
||||
monitor := &cozyv1alpha1.WorkloadMonitor{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "my-bucket",
|
||||
Namespace: "tenant-demo",
|
||||
},
|
||||
Spec: cozyv1alpha1.WorkloadMonitorSpec{
|
||||
Kind: "bucket",
|
||||
Type: "s3",
|
||||
Selector: map[string]string{
|
||||
"app.kubernetes.io/instance": "my-bucket",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
bc := &cosiv1alpha1.BucketClaim{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "my-bucket",
|
||||
Namespace: "tenant-demo",
|
||||
Labels: map[string]string{
|
||||
"app.kubernetes.io/instance": "my-bucket",
|
||||
},
|
||||
},
|
||||
Spec: cosiv1alpha1.BucketClaimSpec{
|
||||
BucketClassName: "seaweedfs",
|
||||
Protocols: []cosiv1alpha1.Protocol{cosiv1alpha1.ProtocolS3},
|
||||
},
|
||||
Status: cosiv1alpha1.BucketClaimStatus{
|
||||
BucketReady: false,
|
||||
},
|
||||
}
|
||||
|
||||
fakeClient := fake.NewClientBuilder().
|
||||
WithScheme(s).
|
||||
WithObjects(monitor, bc).
|
||||
WithStatusSubresource(monitor).
|
||||
Build()
|
||||
|
||||
reconciler := &WorkloadMonitorReconciler{Client: fakeClient, Scheme: s}
|
||||
req := reconcile.Request{NamespacedName: types.NamespacedName{
|
||||
Name: "my-bucket",
|
||||
Namespace: "tenant-demo",
|
||||
}}
|
||||
|
||||
_, err := reconciler.Reconcile(context.TODO(), req)
|
||||
if err != nil {
|
||||
t.Fatalf("Reconcile returned error: %v", err)
|
||||
}
|
||||
|
||||
workload := &cozyv1alpha1.Workload{}
|
||||
err = fakeClient.Get(context.TODO(), types.NamespacedName{
|
||||
Name: "bucket-my-bucket",
|
||||
Namespace: "tenant-demo",
|
||||
}, workload)
|
||||
if err != nil {
|
||||
t.Fatalf("expected Workload to be created, got error: %v", err)
|
||||
}
|
||||
|
||||
if workload.Status.Operational {
|
||||
t.Error("expected Operational=false for not-ready BucketClaim")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReconcile_MonitorLabelsPropagatedToBucketClaimWorkload(t *testing.T) {
|
||||
s := newTestScheme()
|
||||
|
||||
monitor := &cozyv1alpha1.WorkloadMonitor{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "my-bucket",
|
||||
Namespace: "tenant-demo",
|
||||
Labels: map[string]string{
|
||||
"workloads.cozystack.io/resource-preset": "medium",
|
||||
"app.kubernetes.io/name": "ignored-not-propagated",
|
||||
},
|
||||
},
|
||||
Spec: cozyv1alpha1.WorkloadMonitorSpec{
|
||||
Kind: "bucket",
|
||||
Type: "s3",
|
||||
Selector: map[string]string{
|
||||
"app.kubernetes.io/instance": "my-bucket",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
bc := &cosiv1alpha1.BucketClaim{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "my-bucket",
|
||||
Namespace: "tenant-demo",
|
||||
Labels: map[string]string{
|
||||
"app.kubernetes.io/instance": "my-bucket",
|
||||
"app.kubernetes.io/name": "bucket-wins-on-conflict",
|
||||
},
|
||||
},
|
||||
Spec: cosiv1alpha1.BucketClaimSpec{
|
||||
BucketClassName: "seaweedfs",
|
||||
Protocols: []cosiv1alpha1.Protocol{cosiv1alpha1.ProtocolS3},
|
||||
},
|
||||
Status: cosiv1alpha1.BucketClaimStatus{
|
||||
BucketReady: true,
|
||||
BucketName: "cosi-abc123",
|
||||
},
|
||||
}
|
||||
|
||||
fakeClient := fake.NewClientBuilder().
|
||||
WithScheme(s).
|
||||
WithObjects(monitor, bc).
|
||||
WithStatusSubresource(monitor).
|
||||
Build()
|
||||
|
||||
reconciler := &WorkloadMonitorReconciler{Client: fakeClient, Scheme: s}
|
||||
req := reconcile.Request{NamespacedName: types.NamespacedName{
|
||||
Name: "my-bucket",
|
||||
Namespace: "tenant-demo",
|
||||
}}
|
||||
if _, err := reconciler.Reconcile(context.TODO(), req); err != nil {
|
||||
t.Fatalf("Reconcile returned error: %v", err)
|
||||
}
|
||||
|
||||
workload := &cozyv1alpha1.Workload{}
|
||||
if err := fakeClient.Get(context.TODO(), types.NamespacedName{
|
||||
Name: "bucket-my-bucket",
|
||||
Namespace: "tenant-demo",
|
||||
}, workload); err != nil {
|
||||
t.Fatalf("Failed to get Workload: %v", err)
|
||||
}
|
||||
|
||||
if got := workload.Labels["workloads.cozystack.io/resource-preset"]; got != "medium" {
|
||||
t.Errorf("expected monitor label propagated, got %q", got)
|
||||
}
|
||||
// Source-object label takes precedence on conflict
|
||||
if got := workload.Labels["app.kubernetes.io/name"]; got != "bucket-wins-on-conflict" {
|
||||
t.Errorf("expected bucket claim label to win on conflict, got %q", got)
|
||||
}
|
||||
// Reserved monitor label is always set from the monitor name
|
||||
if got := workload.Labels["workloads.cozystack.io/monitor"]; got != "my-bucket" {
|
||||
t.Errorf("expected monitor-name label, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReconcileNoBucketClaimSkips(t *testing.T) {
|
||||
s := newTestScheme()
|
||||
|
||||
monitor := &cozyv1alpha1.WorkloadMonitor{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "my-postgres",
|
||||
Namespace: "tenant-demo",
|
||||
},
|
||||
Spec: cozyv1alpha1.WorkloadMonitorSpec{
|
||||
Kind: "postgres",
|
||||
Type: "postgres",
|
||||
Selector: map[string]string{
|
||||
"app.kubernetes.io/instance": "my-postgres",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
fakeClient := fake.NewClientBuilder().
|
||||
WithScheme(s).
|
||||
WithObjects(monitor).
|
||||
WithStatusSubresource(monitor).
|
||||
Build()
|
||||
|
||||
reconciler := &WorkloadMonitorReconciler{Client: fakeClient, Scheme: s}
|
||||
req := reconcile.Request{NamespacedName: types.NamespacedName{
|
||||
Name: "my-postgres",
|
||||
Namespace: "tenant-demo",
|
||||
}}
|
||||
|
||||
_, err := reconciler.Reconcile(context.TODO(), req)
|
||||
if err != nil {
|
||||
t.Fatalf("Reconcile returned error: %v", err)
|
||||
}
|
||||
|
||||
workloadList := &cozyv1alpha1.WorkloadList{}
|
||||
err = fakeClient.List(context.TODO(), workloadList)
|
||||
if err != nil {
|
||||
t.Fatalf("List returned error: %v", err)
|
||||
}
|
||||
|
||||
for _, w := range workloadList.Items {
|
||||
if w.Status.Kind == "bucket" {
|
||||
t.Error("expected no bucket workloads to be created for postgres monitor")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryAllBucketMetrics(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprint(w, `{"status":"success","data":{"resultType":"vector","result":[
|
||||
{"metric":{"__name__":"SeaweedFS_s3_bucket_size_bytes","bucket":"bucket-aaa"},"value":[1713000000,"10485864"]},
|
||||
{"metric":{"__name__":"SeaweedFS_s3_bucket_physical_size_bytes","bucket":"bucket-aaa"},"value":[1713000000,"20971728"]},
|
||||
{"metric":{"__name__":"SeaweedFS_s3_bucket_size_bytes","bucket":"bucket-bbb"},"value":[1713000000,"0"]}
|
||||
]}}`)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
reconciler := &WorkloadMonitorReconciler{}
|
||||
metrics := reconciler.queryAllBucketMetrics(context.TODO(), srv.URL, []string{"bucket-aaa", "bucket-bbb"})
|
||||
|
||||
bm, ok := metrics["bucket-aaa"]
|
||||
if !ok {
|
||||
t.Fatal("expected bucket-aaa in metrics")
|
||||
}
|
||||
if !bm.HasLogical || bm.LogicalSize != 10485864 {
|
||||
t.Errorf("expected logical=10485864, got %d", bm.LogicalSize)
|
||||
}
|
||||
if !bm.HasPhysical || bm.PhysicalSize != 20971728 {
|
||||
t.Errorf("expected physical=20971728, got %d", bm.PhysicalSize)
|
||||
}
|
||||
|
||||
bm2, ok := metrics["bucket-bbb"]
|
||||
if !ok {
|
||||
t.Fatal("expected bucket-bbb in metrics")
|
||||
}
|
||||
if !bm2.HasLogical || bm2.LogicalSize != 0 {
|
||||
t.Errorf("expected logical=0 for empty bucket, got %d", bm2.LogicalSize)
|
||||
}
|
||||
if bm2.HasPhysical {
|
||||
t.Error("expected no physical size for bucket-bbb")
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryAllBucketMetricsEmpty(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprint(w, `{"status":"success","data":{"resultType":"vector","result":[]}}`)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
reconciler := &WorkloadMonitorReconciler{}
|
||||
metrics := reconciler.queryAllBucketMetrics(context.TODO(), srv.URL, []string{"bucket-aaa", "bucket-bbb"})
|
||||
if len(metrics) != 0 {
|
||||
t.Errorf("expected empty metrics, got %d", len(metrics))
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryAllBucketMetricsServerError(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
reconciler := &WorkloadMonitorReconciler{}
|
||||
metrics := reconciler.queryAllBucketMetrics(context.TODO(), srv.URL, []string{"bucket-aaa", "bucket-bbb"})
|
||||
if len(metrics) != 0 {
|
||||
t.Errorf("expected empty metrics on error, got %d", len(metrics))
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueryAllBucketMetricsNoURL(t *testing.T) {
|
||||
reconciler := &WorkloadMonitorReconciler{}
|
||||
metrics := reconciler.queryAllBucketMetrics(context.TODO(), "", nil)
|
||||
if len(metrics) != 0 {
|
||||
t.Errorf("expected empty metrics when URL is empty, got %d", len(metrics))
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolvePrometheusURL(t *testing.T) {
|
||||
s := newTestScheme()
|
||||
|
||||
ns := &corev1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "tenant-demo",
|
||||
Labels: map[string]string{
|
||||
"namespace.cozystack.io/monitoring": "tenant-root",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
fakeClient := fake.NewClientBuilder().
|
||||
WithScheme(s).
|
||||
WithObjects(ns).
|
||||
Build()
|
||||
|
||||
reconciler := &WorkloadMonitorReconciler{Client: fakeClient, Scheme: s}
|
||||
url := reconciler.resolvePrometheusURL(context.TODO(), "tenant-demo")
|
||||
|
||||
expected := "http://vmselect-shortterm.tenant-root.svc:8481/select/0/prometheus"
|
||||
if url != expected {
|
||||
t.Errorf("expected %q, got %q", expected, url)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolvePrometheusURLNoLabel(t *testing.T) {
|
||||
s := newTestScheme()
|
||||
|
||||
ns := &corev1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "tenant-demo",
|
||||
},
|
||||
}
|
||||
|
||||
fakeClient := fake.NewClientBuilder().
|
||||
WithScheme(s).
|
||||
WithObjects(ns).
|
||||
Build()
|
||||
|
||||
reconciler := &WorkloadMonitorReconciler{Client: fakeClient, Scheme: s}
|
||||
url := reconciler.resolvePrometheusURL(context.TODO(), "tenant-demo")
|
||||
|
||||
if url != "" {
|
||||
t.Errorf("expected empty URL when no monitoring label, got %q", url)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReconcileBucketClaimRequeuesWhenBucketsExist(t *testing.T) {
|
||||
s := newTestScheme()
|
||||
|
||||
ns := &corev1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "tenant-demo",
|
||||
},
|
||||
}
|
||||
|
||||
monitor := &cozyv1alpha1.WorkloadMonitor{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "my-bucket",
|
||||
Namespace: "tenant-demo",
|
||||
},
|
||||
Spec: cozyv1alpha1.WorkloadMonitorSpec{
|
||||
Kind: "bucket",
|
||||
Type: "s3",
|
||||
Selector: map[string]string{
|
||||
"app.kubernetes.io/instance": "my-bucket",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
bc := &cosiv1alpha1.BucketClaim{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "my-bucket",
|
||||
Namespace: "tenant-demo",
|
||||
Labels: map[string]string{
|
||||
"app.kubernetes.io/instance": "my-bucket",
|
||||
},
|
||||
},
|
||||
Spec: cosiv1alpha1.BucketClaimSpec{
|
||||
BucketClassName: "seaweedfs",
|
||||
Protocols: []cosiv1alpha1.Protocol{cosiv1alpha1.ProtocolS3},
|
||||
},
|
||||
Status: cosiv1alpha1.BucketClaimStatus{
|
||||
BucketReady: true,
|
||||
BucketName: "cosi-abc123",
|
||||
},
|
||||
}
|
||||
|
||||
fakeClient := fake.NewClientBuilder().
|
||||
WithScheme(s).
|
||||
WithObjects(ns, monitor, bc).
|
||||
WithStatusSubresource(monitor).
|
||||
Build()
|
||||
|
||||
reconciler := &WorkloadMonitorReconciler{Client: fakeClient, Scheme: s}
|
||||
req := reconcile.Request{NamespacedName: types.NamespacedName{
|
||||
Name: "my-bucket",
|
||||
Namespace: "tenant-demo",
|
||||
}}
|
||||
|
||||
result, err := reconciler.Reconcile(context.TODO(), req)
|
||||
if err != nil {
|
||||
t.Fatalf("Reconcile returned error: %v", err)
|
||||
}
|
||||
|
||||
if result.RequeueAfter == 0 {
|
||||
t.Error("expected RequeueAfter > 0 when buckets exist")
|
||||
}
|
||||
|
||||
workload := &cozyv1alpha1.Workload{}
|
||||
err = fakeClient.Get(context.TODO(), types.NamespacedName{
|
||||
Name: "bucket-my-bucket",
|
||||
Namespace: "tenant-demo",
|
||||
}, workload)
|
||||
if err != nil {
|
||||
t.Fatalf("expected Workload to be created, got error: %v", err)
|
||||
}
|
||||
|
||||
// Without monitoring label on namespace, no size metrics should be set
|
||||
if _, ok := workload.Status.Resources["s3-storage-bytes"]; ok {
|
||||
t.Error("expected no s3-storage-bytes when monitoring is not configured")
|
||||
}
|
||||
if len(workload.Status.Resources) != 0 {
|
||||
t.Errorf("expected empty resources without monitoring, got %v", workload.Status.Resources)
|
||||
}
|
||||
}
|
||||
|
|
@ -89,6 +89,16 @@ func Install(ctx context.Context, k8sClient client.Client, writeEmbeddedManifest
|
|||
}
|
||||
}
|
||||
|
||||
// Add deletion protection label to all CRDs
|
||||
for _, obj := range objects {
|
||||
labels := obj.GetLabels()
|
||||
if labels == nil {
|
||||
labels = make(map[string]string)
|
||||
}
|
||||
labels["cozystack.io/deletion-protected"] = "true"
|
||||
obj.SetLabels(labels)
|
||||
}
|
||||
|
||||
logger.Info("Applying Cozystack CRDs", "count", len(objects))
|
||||
for _, obj := range objects {
|
||||
patchOptions := &client.PatchOptions{
|
||||
|
|
|
|||
|
|
@ -118,19 +118,6 @@ spec:
|
|||
ReleaseName is the name of the HelmRelease resource that will be created
|
||||
If not specified, defaults to the component Name field
|
||||
type: string
|
||||
upgradeCRDs:
|
||||
description: |-
|
||||
UpgradeCRDs controls how CRDs from the chart's crds/ directory are
|
||||
handled on HelmRelease upgrades. Maps to HelmRelease.Spec.Upgrade.CRDs.
|
||||
Empty string (default) preserves the helm-controller default (Skip).
|
||||
Use "CreateReplace" for operators that evolve their CRD set between
|
||||
versions. Warning: CreateReplace overwrites CRDs and may cause data
|
||||
loss if upstream drops fields from a CRD with live objects.
|
||||
enum:
|
||||
- Skip
|
||||
- Create
|
||||
- CreateReplace
|
||||
type: string
|
||||
type: object
|
||||
libraries:
|
||||
description: |-
|
||||
|
|
|
|||
106
internal/lineagecontrollerwebhook/validate_deletion.go
Normal file
106
internal/lineagecontrollerwebhook/validate_deletion.go
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
/*
|
||||
Copyright 2025 The Cozystack Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package lineagecontrollerwebhook
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
admissionv1 "k8s.io/api/admission/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
|
||||
)
|
||||
|
||||
const (
|
||||
deletionProtectedLabel = "cozystack.io/deletion-protected"
|
||||
)
|
||||
|
||||
// DeletionProtectionWebhook denies DELETE requests on resources that carry
|
||||
// the cozystack.io/deletion-protected label. The objectSelector on the
|
||||
// ValidatingWebhookConfiguration ensures this handler is only called for
|
||||
// labeled resources.
|
||||
type DeletionProtectionWebhook struct{}
|
||||
|
||||
func (h *DeletionProtectionWebhook) Handle(ctx context.Context, req admission.Request) admission.Response {
|
||||
logger := log.FromContext(ctx).WithValues(
|
||||
"kind", req.Kind.Kind,
|
||||
"namespace", req.Namespace,
|
||||
"name", req.Name,
|
||||
"operation", req.Operation,
|
||||
)
|
||||
|
||||
if req.Operation != admissionv1.Delete {
|
||||
return admission.Allowed("not a DELETE operation")
|
||||
}
|
||||
|
||||
var identifier string
|
||||
if req.Namespace != "" {
|
||||
identifier = fmt.Sprintf("%s %s/%s", req.Kind.Kind, req.Namespace, req.Name)
|
||||
} else {
|
||||
identifier = fmt.Sprintf("%s %s", req.Kind.Kind, req.Name)
|
||||
}
|
||||
|
||||
msg := fmt.Sprintf(
|
||||
"deletion of %s is protected by cozystack. "+
|
||||
"To force-delete, first remove the label: "+
|
||||
"kubectl label %s %s %s-",
|
||||
identifier,
|
||||
kindToResourceArg(req.Kind.Kind, req.Namespace),
|
||||
req.Name,
|
||||
deletionProtectedLabel,
|
||||
)
|
||||
|
||||
logger.Info("DENIED deletion of protected resource", "resource", identifier)
|
||||
|
||||
return admission.Response{
|
||||
AdmissionResponse: admissionv1.AdmissionResponse{
|
||||
Allowed: false,
|
||||
Result: &metav1.Status{
|
||||
Status: metav1.StatusFailure,
|
||||
Message: msg,
|
||||
Reason: metav1.StatusReasonForbidden,
|
||||
Code: http.StatusForbidden,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func kindToResourceArg(kind, namespace string) string {
|
||||
switch kind {
|
||||
case "Namespace":
|
||||
return "namespace"
|
||||
case "ConfigMap":
|
||||
return "configmap -n " + namespace
|
||||
case "HelmRelease":
|
||||
return "helmrelease.helm.toolkit.fluxcd.io -n " + namespace
|
||||
case "CustomResourceDefinition":
|
||||
return "crd"
|
||||
case "LinstorCluster":
|
||||
return "linstorcluster.piraeus.io"
|
||||
case "ClusterIssuer":
|
||||
return "clusterissuer.cert-manager.io"
|
||||
case "OCIRepository":
|
||||
return "ocirepository.source.toolkit.fluxcd.io -n " + namespace
|
||||
default:
|
||||
if namespace != "" {
|
||||
return kind + " -n " + namespace
|
||||
}
|
||||
return kind
|
||||
}
|
||||
}
|
||||
164
internal/lineagecontrollerwebhook/validate_deletion_test.go
Normal file
164
internal/lineagecontrollerwebhook/validate_deletion_test.go
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
/*
|
||||
Copyright 2025 The Cozystack Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package lineagecontrollerwebhook
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
admissionv1 "k8s.io/api/admission/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log/zap"
|
||||
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
|
||||
)
|
||||
|
||||
func TestDeletionProtectionWebhook_Handle(t *testing.T) {
|
||||
log.SetLogger(zap.New(zap.UseDevMode(true)))
|
||||
ctx := context.Background()
|
||||
|
||||
handler := &DeletionProtectionWebhook{}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
req admission.Request
|
||||
wantAllowed bool
|
||||
wantCode int32
|
||||
}{
|
||||
{
|
||||
name: "deny DELETE on protected namespace",
|
||||
req: admission.Request{
|
||||
AdmissionRequest: admissionv1.AdmissionRequest{
|
||||
Operation: admissionv1.Delete,
|
||||
Name: "tenant-root",
|
||||
Kind: metav1.GroupVersionKind{
|
||||
Group: "",
|
||||
Version: "v1",
|
||||
Kind: "Namespace",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantAllowed: false,
|
||||
wantCode: http.StatusForbidden,
|
||||
},
|
||||
{
|
||||
name: "deny DELETE on protected HelmRelease",
|
||||
req: admission.Request{
|
||||
AdmissionRequest: admissionv1.AdmissionRequest{
|
||||
Operation: admissionv1.Delete,
|
||||
Name: "tenant-root",
|
||||
Namespace: "tenant-root",
|
||||
Kind: metav1.GroupVersionKind{
|
||||
Group: "helm.toolkit.fluxcd.io",
|
||||
Version: "v2",
|
||||
Kind: "HelmRelease",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantAllowed: false,
|
||||
wantCode: http.StatusForbidden,
|
||||
},
|
||||
{
|
||||
name: "deny DELETE on protected CRD",
|
||||
req: admission.Request{
|
||||
AdmissionRequest: admissionv1.AdmissionRequest{
|
||||
Operation: admissionv1.Delete,
|
||||
Name: "packages.cozystack.io",
|
||||
Kind: metav1.GroupVersionKind{
|
||||
Group: "apiextensions.k8s.io",
|
||||
Version: "v1",
|
||||
Kind: "CustomResourceDefinition",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantAllowed: false,
|
||||
wantCode: http.StatusForbidden,
|
||||
},
|
||||
{
|
||||
name: "deny DELETE on protected LinstorCluster",
|
||||
req: admission.Request{
|
||||
AdmissionRequest: admissionv1.AdmissionRequest{
|
||||
Operation: admissionv1.Delete,
|
||||
Name: "linstorcluster",
|
||||
Kind: metav1.GroupVersionKind{
|
||||
Group: "piraeus.io",
|
||||
Version: "v1",
|
||||
Kind: "LinstorCluster",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantAllowed: false,
|
||||
wantCode: http.StatusForbidden,
|
||||
},
|
||||
{
|
||||
name: "allow non-DELETE operation",
|
||||
req: admission.Request{
|
||||
AdmissionRequest: admissionv1.AdmissionRequest{
|
||||
Operation: admissionv1.Update,
|
||||
Name: "tenant-root",
|
||||
Kind: metav1.GroupVersionKind{
|
||||
Group: "",
|
||||
Version: "v1",
|
||||
Kind: "Namespace",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantAllowed: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
resp := handler.Handle(ctx, tt.req)
|
||||
|
||||
if resp.Allowed != tt.wantAllowed {
|
||||
t.Errorf("Allowed = %v, want %v", resp.Allowed, tt.wantAllowed)
|
||||
}
|
||||
if !tt.wantAllowed && resp.Result != nil && resp.Result.Code != tt.wantCode {
|
||||
t.Errorf("Code = %d, want %d", resp.Result.Code, tt.wantCode)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestKindToResourceArg(t *testing.T) {
|
||||
tests := []struct {
|
||||
kind string
|
||||
namespace string
|
||||
want string
|
||||
}{
|
||||
{"Namespace", "", "namespace"},
|
||||
{"ConfigMap", "cozy-system", "configmap -n cozy-system"},
|
||||
{"HelmRelease", "tenant-root", "helmrelease.helm.toolkit.fluxcd.io -n tenant-root"},
|
||||
{"CustomResourceDefinition", "", "crd"},
|
||||
{"LinstorCluster", "", "linstorcluster.piraeus.io"},
|
||||
{"ClusterIssuer", "", "clusterissuer.cert-manager.io"},
|
||||
{"OCIRepository", "cozy-system", "ocirepository.source.toolkit.fluxcd.io -n cozy-system"},
|
||||
{"Unknown", "ns", "Unknown -n ns"},
|
||||
{"Unknown", "", "Unknown"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.kind, func(t *testing.T) {
|
||||
got := kindToResourceArg(tt.kind, tt.namespace)
|
||||
if got != tt.want {
|
||||
t.Errorf("kindToResourceArg(%q, %q) = %q, want %q", tt.kind, tt.namespace, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -45,16 +45,6 @@ const (
|
|||
SecretCozystackValues = "cozystack-values"
|
||||
)
|
||||
|
||||
// parseCRDPolicy maps ComponentInstall.UpgradeCRDs to a helmv2.CRDsPolicy.
|
||||
// Empty / nil preserves the helm-controller default (Skip on upgrade);
|
||||
// the CRD enum marker restricts the string to Skip/Create/CreateReplace.
|
||||
func parseCRDPolicy(install *cozyv1alpha1.ComponentInstall) helmv2.CRDsPolicy {
|
||||
if install == nil || install.UpgradeCRDs == "" {
|
||||
return ""
|
||||
}
|
||||
return helmv2.CRDsPolicy(install.UpgradeCRDs)
|
||||
}
|
||||
|
||||
// PackageReconciler reconciles Package resources
|
||||
type PackageReconciler struct {
|
||||
client.Client
|
||||
|
|
@ -231,7 +221,6 @@ func (r *PackageReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
|
|||
Remediation: &helmv2.UpgradeRemediation{
|
||||
Retries: -1,
|
||||
},
|
||||
CRDs: parseCRDPolicy(component.Install),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,138 +0,0 @@
|
|||
/*
|
||||
Copyright 2025 The Cozystack Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package operator
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
|
||||
helmv2 "github.com/fluxcd/helm-controller/api/v2"
|
||||
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||
"sigs.k8s.io/yaml"
|
||||
)
|
||||
|
||||
func TestParseCRDPolicy(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
install *cozyv1alpha1.ComponentInstall
|
||||
want helmv2.CRDsPolicy
|
||||
}{
|
||||
{
|
||||
name: "nil install leaves flux default",
|
||||
install: nil,
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "empty upgradeCRDs leaves flux default",
|
||||
install: &cozyv1alpha1.ComponentInstall{},
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "Skip is passed through",
|
||||
install: &cozyv1alpha1.ComponentInstall{UpgradeCRDs: "Skip"},
|
||||
want: helmv2.Skip,
|
||||
},
|
||||
{
|
||||
name: "Create is passed through",
|
||||
install: &cozyv1alpha1.ComponentInstall{UpgradeCRDs: "Create"},
|
||||
want: helmv2.Create,
|
||||
},
|
||||
{
|
||||
name: "CreateReplace is passed through",
|
||||
install: &cozyv1alpha1.ComponentInstall{UpgradeCRDs: "CreateReplace"},
|
||||
want: helmv2.CreateReplace,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got := parseCRDPolicy(tc.install)
|
||||
if got != tc.want {
|
||||
t.Errorf("parseCRDPolicy() = %q, want %q", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestPackageSourceCRDHasUpgradeCRDsEnum guards the generated CRD schema: the
|
||||
// invalid-value case from the spec is enforced at the API server via a
|
||||
// kubebuilder enum marker, not in the reconciler. If someone drops the marker
|
||||
// and forgets to regenerate, this test catches it.
|
||||
func TestPackageSourceCRDHasUpgradeCRDsEnum(t *testing.T) {
|
||||
path := filepath.Join("..", "crdinstall", "manifests", "cozystack.io_packagesources.yaml")
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("read %s: %v", path, err)
|
||||
}
|
||||
|
||||
var crd apiextensionsv1.CustomResourceDefinition
|
||||
if err := yaml.Unmarshal(data, &crd); err != nil {
|
||||
t.Fatalf("unmarshal CRD: %v", err)
|
||||
}
|
||||
|
||||
var field *apiextensionsv1.JSONSchemaProps
|
||||
for i := range crd.Spec.Versions {
|
||||
v := &crd.Spec.Versions[i]
|
||||
if v.Schema == nil || v.Schema.OpenAPIV3Schema == nil {
|
||||
continue
|
||||
}
|
||||
spec, ok := v.Schema.OpenAPIV3Schema.Properties["spec"]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
variants, ok := spec.Properties["variants"]
|
||||
if !ok || variants.Items == nil || variants.Items.Schema == nil {
|
||||
continue
|
||||
}
|
||||
components, ok := variants.Items.Schema.Properties["components"]
|
||||
if !ok || components.Items == nil || components.Items.Schema == nil {
|
||||
continue
|
||||
}
|
||||
install, ok := components.Items.Schema.Properties["install"]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
f, ok := install.Properties["upgradeCRDs"]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
field = &f
|
||||
break
|
||||
}
|
||||
|
||||
if field == nil {
|
||||
t.Fatal("upgradeCRDs field not found in PackageSource CRD schema")
|
||||
}
|
||||
|
||||
got := map[string]bool{}
|
||||
for _, e := range field.Enum {
|
||||
var s string
|
||||
if err := json.Unmarshal(e.Raw, &s); err != nil {
|
||||
t.Fatalf("unmarshal enum value %q: %v", e.Raw, err)
|
||||
}
|
||||
got[s] = true
|
||||
}
|
||||
|
||||
for _, want := range []string{"Skip", "Create", "CreateReplace"} {
|
||||
if !got[want] {
|
||||
t.Errorf("enum value %q missing from upgradeCRDs; got %v", want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,8 +4,6 @@ apiVersion: objectstorage.k8s.io/v1alpha1
|
|||
kind: BucketClaim
|
||||
metadata:
|
||||
name: {{ .Release.Name }}
|
||||
labels:
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
spec:
|
||||
bucketClassName: {{ $seaweedfs }}{{- if $pool }}-{{ $pool }}{{- end }}{{- if .Values.locking }}-lock{{- end }}
|
||||
protocols:
|
||||
|
|
|
|||
|
|
@ -1,13 +0,0 @@
|
|||
---
|
||||
apiVersion: cozystack.io/v1alpha1
|
||||
kind: WorkloadMonitor
|
||||
metadata:
|
||||
name: {{ $.Release.Name }}
|
||||
spec:
|
||||
replicas: 0
|
||||
minReplicas: 0
|
||||
kind: bucket
|
||||
type: s3
|
||||
selector:
|
||||
app.kubernetes.io/instance: {{ $.Release.Name }}
|
||||
version: {{ $.Chart.Version }}
|
||||
|
|
@ -3,8 +3,6 @@ apiVersion: cozystack.io/v1alpha1
|
|||
kind: WorkloadMonitor
|
||||
metadata:
|
||||
name: {{ $.Release.Name }}
|
||||
labels:
|
||||
workloads.cozystack.io/resource-preset: {{ .Values.resourcesPreset | quote }}
|
||||
spec:
|
||||
replicas: {{ .Values.replicas }}
|
||||
minReplicas: 1
|
||||
|
|
@ -19,8 +17,6 @@ apiVersion: cozystack.io/v1alpha1
|
|||
kind: WorkloadMonitor
|
||||
metadata:
|
||||
name: {{ $.Release.Name }}-keeper
|
||||
labels:
|
||||
workloads.cozystack.io/resource-preset: {{ .Values.clickhouseKeeper.resourcesPreset | quote }}
|
||||
spec:
|
||||
replicas: {{ .Values.clickhouseKeeper.replicas }}
|
||||
minReplicas: 1
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ metadata:
|
|||
app.kubernetes.io/name: foundationdb
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
workloads.cozystack.io/resource-preset: {{ .Values.resourcesPreset | quote }}
|
||||
spec:
|
||||
replicas: {{ .Values.cluster.processCounts.storage }}
|
||||
minReplicas: {{ include "foundationdb.minReplicas" . }}
|
||||
|
|
|
|||
|
|
@ -158,8 +158,6 @@ apiVersion: cozystack.io/v1alpha1
|
|||
kind: WorkloadMonitor
|
||||
metadata:
|
||||
name: {{ $.Release.Name }}-core
|
||||
labels:
|
||||
workloads.cozystack.io/resource-preset: {{ .Values.core.resourcesPreset | quote }}
|
||||
spec:
|
||||
replicas: 1
|
||||
minReplicas: 1
|
||||
|
|
@ -176,8 +174,6 @@ apiVersion: cozystack.io/v1alpha1
|
|||
kind: WorkloadMonitor
|
||||
metadata:
|
||||
name: {{ $.Release.Name }}-registry
|
||||
labels:
|
||||
workloads.cozystack.io/resource-preset: {{ .Values.registry.resourcesPreset | quote }}
|
||||
spec:
|
||||
replicas: 1
|
||||
minReplicas: 1
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ metadata:
|
|||
nginx.ingress.kubernetes.io/ssl-redirect: "true"
|
||||
nginx.ingress.kubernetes.io/backend-protocol: "HTTP"
|
||||
{{- if eq $solver "http01" }}
|
||||
acme.cert-manager.io/http01-ingress-ingressclassname: {{ $ingress }}
|
||||
acme.cert-manager.io/http01-ingress-class: {{ $ingress }}
|
||||
{{- end }}
|
||||
cert-manager.io/cluster-issuer: {{ $clusterIssuer }}
|
||||
spec:
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
ghcr.io/cozystack/cozystack/nginx-cache:0.0.0@sha256:d397781152ab9123b11b8191d92eba7a0d2faa376aa2c15ddeb67842a9b59bab
|
||||
ghcr.io/cozystack/cozystack/nginx-cache:0.0.0@sha256:2f987017ff95d3c782e16ef0d99928831f520daf154d08a2fa38c2d68363e036
|
||||
|
|
|
|||
|
|
@ -3,8 +3,6 @@ apiVersion: cozystack.io/v1alpha1
|
|||
kind: WorkloadMonitor
|
||||
metadata:
|
||||
name: {{ $.Release.Name }}-haproxy
|
||||
labels:
|
||||
workloads.cozystack.io/resource-preset: {{ .Values.haproxy.resourcesPreset | quote }}
|
||||
spec:
|
||||
replicas: {{ .Values.haproxy.replicas }}
|
||||
minReplicas: 1
|
||||
|
|
@ -18,8 +16,6 @@ apiVersion: cozystack.io/v1alpha1
|
|||
kind: WorkloadMonitor
|
||||
metadata:
|
||||
name: {{ $.Release.Name }}-nginx
|
||||
labels:
|
||||
workloads.cozystack.io/resource-preset: {{ .Values.nginx.resourcesPreset | quote }}
|
||||
spec:
|
||||
replicas: {{ .Values.nginx.replicas }}
|
||||
minReplicas: 1
|
||||
|
|
|
|||
|
|
@ -3,8 +3,6 @@ apiVersion: cozystack.io/v1alpha1
|
|||
kind: WorkloadMonitor
|
||||
metadata:
|
||||
name: {{ $.Release.Name }}
|
||||
labels:
|
||||
workloads.cozystack.io/resource-preset: {{ .Values.kafka.resourcesPreset | quote }}
|
||||
spec:
|
||||
replicas: {{ .Values.replicas }}
|
||||
minReplicas: 1
|
||||
|
|
@ -21,8 +19,6 @@ apiVersion: cozystack.io/v1alpha1
|
|||
kind: WorkloadMonitor
|
||||
metadata:
|
||||
name: {{ $.Release.Name }}-zookeeper
|
||||
labels:
|
||||
workloads.cozystack.io/resource-preset: {{ .Values.zookeeper.resourcesPreset | quote }}
|
||||
spec:
|
||||
replicas: {{ .Values.replicas }}
|
||||
minReplicas: 1
|
||||
|
|
|
|||
|
|
@ -4,9 +4,6 @@ KUBERNETES_PKG_TAG = $(shell awk '$$1 == "version:" {print $$2}' Chart.yaml)
|
|||
include ../../../hack/common-envs.mk
|
||||
include ../../../hack/package.mk
|
||||
|
||||
test:
|
||||
helm unittest .
|
||||
|
||||
generate:
|
||||
cozyvalues-gen -m 'kubernetes' -v values.yaml -s values.schema.json -r README.md -g ../../../api/apps/v1alpha1/kubernetes/types.go
|
||||
../../../hack/update-crd.sh
|
||||
|
|
@ -70,4 +67,3 @@ image-cluster-autoscaler:
|
|||
echo "$(REGISTRY)/cluster-autoscaler:$(call settag,$(KUBERNETES_PKG_TAG))@$$(yq e '."containerimage.digest"' images/cluster-autoscaler.json -o json -r)" \
|
||||
> images/cluster-autoscaler.tag
|
||||
rm -f images/cluster-autoscaler.json
|
||||
|
||||
|
|
|
|||
|
|
@ -128,9 +128,6 @@ 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` | `{}` |
|
||||
|
|
@ -148,33 +145,31 @@ See the reference for components utilized in this service:
|
|||
|
||||
### Kubernetes Control Plane Configuration
|
||||
|
||||
| Name | Description | Type | Value |
|
||||
| --------------------------------------------------- | --------------------------------------------------------------------------------------------- | ---------- | ------- |
|
||||
| `controlPlane` | Kubernetes control-plane configuration. | `object` | `{}` |
|
||||
| `controlPlane.replicas` | Number of control-plane replicas. | `int` | `2` |
|
||||
| `controlPlane.apiServer` | API Server configuration. | `object` | `{}` |
|
||||
| `controlPlane.apiServer.resources` | CPU and memory resources for API Server. | `object` | `{}` |
|
||||
| `controlPlane.apiServer.resources.cpu` | CPU available. | `quantity` | `""` |
|
||||
| `controlPlane.apiServer.resources.memory` | Memory (RAM) available. | `quantity` | `""` |
|
||||
| `controlPlane.apiServer.resourcesPreset` | Preset if `resources` omitted. | `string` | `large` |
|
||||
| `controlPlane.controllerManager` | Controller Manager configuration. | `object` | `{}` |
|
||||
| `controlPlane.controllerManager.resources` | CPU and memory resources for Controller Manager. | `object` | `{}` |
|
||||
| `controlPlane.controllerManager.resources.cpu` | CPU available. | `quantity` | `""` |
|
||||
| `controlPlane.controllerManager.resources.memory` | Memory (RAM) available. | `quantity` | `""` |
|
||||
| `controlPlane.controllerManager.resourcesPreset` | Preset if `resources` omitted. | `string` | `micro` |
|
||||
| `controlPlane.scheduler` | Scheduler configuration. | `object` | `{}` |
|
||||
| `controlPlane.scheduler.resources` | CPU and memory resources for Scheduler. | `object` | `{}` |
|
||||
| `controlPlane.scheduler.resources.cpu` | CPU available. | `quantity` | `""` |
|
||||
| `controlPlane.scheduler.resources.memory` | Memory (RAM) available. | `quantity` | `""` |
|
||||
| `controlPlane.scheduler.resourcesPreset` | Preset if `resources` omitted. | `string` | `micro` |
|
||||
| `controlPlane.konnectivity` | Konnectivity configuration. | `object` | `{}` |
|
||||
| `controlPlane.konnectivity.server` | Konnectivity Server configuration. | `object` | `{}` |
|
||||
| `controlPlane.konnectivity.server.resources` | CPU and memory resources for Konnectivity. | `object` | `{}` |
|
||||
| `controlPlane.konnectivity.server.resources.cpu` | CPU available. | `quantity` | `""` |
|
||||
| `controlPlane.konnectivity.server.resources.memory` | Memory (RAM) available. | `quantity` | `""` |
|
||||
| `controlPlane.konnectivity.server.resourcesPreset` | Preset if `resources` omitted. | `string` | `micro` |
|
||||
| `images` | Optional image overrides for air-gapped or rate-limited registries. | `object` | `{}` |
|
||||
| `images.waitForKubeconfig` | Image used by the wait-for-kubeconfig init container. Empty falls back to images/busybox.tag. | `string` | `""` |
|
||||
| Name | Description | Type | Value |
|
||||
| --------------------------------------------------- | ------------------------------------------------ | ---------- | ------- |
|
||||
| `controlPlane` | Kubernetes control-plane configuration. | `object` | `{}` |
|
||||
| `controlPlane.replicas` | Number of control-plane replicas. | `int` | `2` |
|
||||
| `controlPlane.apiServer` | API Server configuration. | `object` | `{}` |
|
||||
| `controlPlane.apiServer.resources` | CPU and memory resources for API Server. | `object` | `{}` |
|
||||
| `controlPlane.apiServer.resources.cpu` | CPU available. | `quantity` | `""` |
|
||||
| `controlPlane.apiServer.resources.memory` | Memory (RAM) available. | `quantity` | `""` |
|
||||
| `controlPlane.apiServer.resourcesPreset` | Preset if `resources` omitted. | `string` | `large` |
|
||||
| `controlPlane.controllerManager` | Controller Manager configuration. | `object` | `{}` |
|
||||
| `controlPlane.controllerManager.resources` | CPU and memory resources for Controller Manager. | `object` | `{}` |
|
||||
| `controlPlane.controllerManager.resources.cpu` | CPU available. | `quantity` | `""` |
|
||||
| `controlPlane.controllerManager.resources.memory` | Memory (RAM) available. | `quantity` | `""` |
|
||||
| `controlPlane.controllerManager.resourcesPreset` | Preset if `resources` omitted. | `string` | `micro` |
|
||||
| `controlPlane.scheduler` | Scheduler configuration. | `object` | `{}` |
|
||||
| `controlPlane.scheduler.resources` | CPU and memory resources for Scheduler. | `object` | `{}` |
|
||||
| `controlPlane.scheduler.resources.cpu` | CPU available. | `quantity` | `""` |
|
||||
| `controlPlane.scheduler.resources.memory` | Memory (RAM) available. | `quantity` | `""` |
|
||||
| `controlPlane.scheduler.resourcesPreset` | Preset if `resources` omitted. | `string` | `micro` |
|
||||
| `controlPlane.konnectivity` | Konnectivity configuration. | `object` | `{}` |
|
||||
| `controlPlane.konnectivity.server` | Konnectivity Server configuration. | `object` | `{}` |
|
||||
| `controlPlane.konnectivity.server.resources` | CPU and memory resources for Konnectivity. | `object` | `{}` |
|
||||
| `controlPlane.konnectivity.server.resources.cpu` | CPU available. | `quantity` | `""` |
|
||||
| `controlPlane.konnectivity.server.resources.memory` | Memory (RAM) available. | `quantity` | `""` |
|
||||
| `controlPlane.konnectivity.server.resourcesPreset` | Preset if `resources` omitted. | `string` | `micro` |
|
||||
|
||||
|
||||
## Parameter examples and reference
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
docker.io/library/busybox:1.37.0@sha256:1487d0af5f52b4ba31c7e465126ee2123fe3f2305d638e7827681e7cf6c83d5e
|
||||
|
|
@ -1 +1 @@
|
|||
ghcr.io/cozystack/cozystack/kubevirt-csi-driver:0.0.0@sha256:72154a97054e16cdf3dea6129d962b8d7e86b55cf9386095e8ac2ce7c8b69172
|
||||
ghcr.io/cozystack/cozystack/kubevirt-csi-driver:0.0.0@sha256:5b209248acba3d8cf0d999edd8a915915a2e565a1c8ac1f5dde0dffec20fa02e
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
ghcr.io/cozystack/cozystack/ubuntu-container-disk:v1.30@sha256:f460ed1fd5a721aed423e6c3b8be0105d7f693cd86ad992d9c15fd4b27e58cec
|
||||
ghcr.io/cozystack/cozystack/ubuntu-container-disk:v1.30@sha256:51e272db23d64eb20bc44e38cdbb70199fd00a49a958b76c2a89d4a46fec9256
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
ghcr.io/cozystack/cozystack/ubuntu-container-disk:v1.31@sha256:aaa2dfa8ee53ae26295f44d2491330f51412457bbe4aa6ba256297cc0bd8a0da
|
||||
ghcr.io/cozystack/cozystack/ubuntu-container-disk:v1.31@sha256:31c3290305159fe484caf6c5780960cf071dc3939528295336da4abfa731a81b
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
ghcr.io/cozystack/cozystack/ubuntu-container-disk:v1.32@sha256:7acb4728ef6b9c0ff3344bf486bb00f2a083f2098452344a362242235013db01
|
||||
ghcr.io/cozystack/cozystack/ubuntu-container-disk:v1.32@sha256:9efb1fe18cffb045530b43ac4b39910f1802d3c16798d733165dcd1c67de9237
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
ghcr.io/cozystack/cozystack/ubuntu-container-disk:v1.33@sha256:a2024339ab9edb980a96b43ad2744b7aa31afaef0983ddbc6a546068b4cfa87a
|
||||
ghcr.io/cozystack/cozystack/ubuntu-container-disk:v1.33@sha256:a0afca1d6f720cfff764a1bc912bfe404b5e97ff731f6c126a7abdb51ed23f85
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
ghcr.io/cozystack/cozystack/ubuntu-container-disk:v1.34@sha256:97bcf946a5687889c6a421d14e5adece11f0978151b2165dc87b28f77f7607db
|
||||
ghcr.io/cozystack/cozystack/ubuntu-container-disk:v1.34@sha256:51dfe5ce3e2f8765fde9422ae7e9fba677016ec5e2be41559c5f11a56958b0e6
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
ghcr.io/cozystack/cozystack/ubuntu-container-disk:v1.35@sha256:364c6d454891f1eb1a598fddb69cf328a14dbc451a8ac65812b038a7756da60a
|
||||
ghcr.io/cozystack/cozystack/ubuntu-container-disk:v1.35@sha256:c4ae418b2a2c139794cd9630c3b2c2171870cc7748ac200344d2c4ed4283cf0b
|
||||
|
|
|
|||
|
|
@ -49,52 +49,3 @@ Selector labels
|
|||
app.kubernetes.io/name: {{ include "kubernetes.name" . }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
wait-for-kubeconfig init container shared by the control-plane-side
|
||||
Deployments (cluster-autoscaler, kccm, kcsi-controller) that mount the
|
||||
*-admin-kubeconfig Secret provisioned asynchronously by Kamaji. The
|
||||
Secret volume is declared optional so kubelet does not FailedMount while
|
||||
Kamaji is still bootstrapping; this container polls the mounted path and
|
||||
exits only when super-admin.svc appears, which happens after kubelet's
|
||||
optional-Secret refresh cycle.
|
||||
|
||||
The 10m deadline stays strictly below the 15m HelmRelease
|
||||
Install.Timeout set by cozystack-api for the Kubernetes kind (via the
|
||||
release.cozystack.io/helm-install-timeout annotation) so the
|
||||
CrashLoopBackOff surfaces before flux remediation fires and uninstalls
|
||||
the Cluster CR.
|
||||
|
||||
The default image lives in images/busybox.tag and points directly at
|
||||
docker.io by digest (not mirrored to ghcr.io like the other .tag files
|
||||
here): the payload is a one-shot sh loop and the digest pin makes the
|
||||
pull immutable. Operators in air-gapped or rate-limited environments
|
||||
can override it via .Values.images.waitForKubeconfig (any registry
|
||||
reference kubelet can pull). When the value is empty the chart falls
|
||||
back to the bundled digest pin, preserving the prior default.
|
||||
|
||||
Call site owns the surrounding volumes block; the kubeconfig volume
|
||||
must exist on the pod and mount at /etc/kubernetes/kubeconfig.
|
||||
*/}}
|
||||
{{- define "kubernetes.waitForAdminKubeconfig" -}}
|
||||
- name: wait-for-kubeconfig
|
||||
image: "{{ default (.Files.Get "images/busybox.tag" | trim) .Values.images.waitForKubeconfig }}"
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- |
|
||||
set -eu
|
||||
deadline=$(( $(date +%s) + 600 ))
|
||||
until [ -s /etc/kubernetes/kubeconfig/super-admin.svc ]; do
|
||||
if [ "$(date +%s)" -ge "$deadline" ]; then
|
||||
echo "admin kubeconfig was not provisioned within 10m; exiting so the pod goes CrashLoopBackOff and surfaces in dashboards" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "waiting for admin kubeconfig (provisioned by Kamaji, visible after kubelet Secret refresh)..."
|
||||
sleep 5
|
||||
done
|
||||
volumeMounts:
|
||||
- name: kubeconfig
|
||||
mountPath: /etc/kubernetes/kubeconfig
|
||||
readOnly: true
|
||||
{{- end }}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,3 @@
|
|||
{{- /*
|
||||
Gate the control-plane-side workloads on the parent tenant having an etcd
|
||||
DataStore. Without it no KamajiControlPlane is ever created, Kamaji never
|
||||
provisions -admin-kubeconfig, and rendering these Deployments would cause
|
||||
the wait-for-kubeconfig init to CrashLoopBackOff indefinitely, consuming
|
||||
the parent HelmRelease install timeout and triggering the very uninstall
|
||||
remediation cycle this chart is supposed to avoid. Rendering them only
|
||||
when $etcd is set keeps the HelmRelease Ready while flux retries on its
|
||||
interval and picks up the DataStore as soon as the Tenant chart finishes.
|
||||
*/}}
|
||||
{{- if .Values._namespace.etcd }}
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
|
|
@ -34,8 +23,6 @@ spec:
|
|||
- key: node-role.kubernetes.io/control-plane
|
||||
operator: Exists
|
||||
effect: "NoSchedule"
|
||||
initContainers:
|
||||
{{- include "kubernetes.waitForAdminKubeconfig" $ | nindent 6 }}
|
||||
containers:
|
||||
- image: "{{ $.Files.Get "images/cluster-autoscaler.tag" | trim }}"
|
||||
name: cluster-autoscaler
|
||||
|
|
@ -69,7 +56,6 @@ spec:
|
|||
name: cloud-config
|
||||
- secret:
|
||||
secretName: {{ .Release.Name }}-admin-kubeconfig
|
||||
optional: true
|
||||
name: kubeconfig
|
||||
serviceAccountName: {{ .Release.Name }}-cluster-autoscaler
|
||||
terminationGracePeriodSeconds: 10
|
||||
|
|
@ -119,4 +105,3 @@ rules:
|
|||
- list
|
||||
- update
|
||||
- watch
|
||||
{{- end }}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,4 @@
|
|||
{{- $etcd := .Values._namespace.etcd }}
|
||||
{{- /*
|
||||
When $etcd is empty, the parent Tenant application has not populated
|
||||
_namespace.etcd in cozystack-values yet - either the operator forgot to
|
||||
set etcd: true on an ancestor Tenant, or the Tenant HelmRelease is still
|
||||
reconciling. Either way, rendering a KamajiControlPlane with an empty
|
||||
dataStoreName would be rejected by Kamaji's admission webhook and the
|
||||
HelmRelease would fail to install, triggering remediation. Instead, emit
|
||||
a single ConfigMap as a user-visible status beacon and skip the rest so
|
||||
flux marks the HelmRelease Ready and retries its 5m reconcile loop until
|
||||
the Tenant chart catches up.
|
||||
*/}}
|
||||
{{- $ingress := .Values._namespace.ingress }}
|
||||
{{- $host := .Values._namespace.host }}
|
||||
{{- $kubevirtmachinetemplateNames := list }}
|
||||
|
|
@ -95,26 +84,6 @@ spec:
|
|||
- name: default
|
||||
pod: {}
|
||||
{{- end }}
|
||||
{{- if not $etcd }}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: {{ .Release.Name }}-awaiting-etcd
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
app.kubernetes.io/name: kubernetes
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
data:
|
||||
status: "awaiting-etcd"
|
||||
message: |
|
||||
No DataStore is available for this tenant Kubernetes cluster. The parent
|
||||
Tenant application has not populated _namespace.etcd. Set spec.etcd: true
|
||||
on an ancestor Tenant (usually tenant-root) and wait for its HelmRelease
|
||||
to reconcile - this HelmRelease will pick up the DataStore on its next
|
||||
5m reconcile loop and provision the cluster.
|
||||
{{- else }}
|
||||
---
|
||||
apiVersion: cluster.x-k8s.io/v1beta1
|
||||
kind: Cluster
|
||||
|
|
@ -435,4 +404,3 @@ metadata:
|
|||
spec:
|
||||
{{- .spec | toYaml | nindent 2 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
{{- if .Values._namespace.etcd }}
|
||||
kind: Deployment
|
||||
apiVersion: apps/v1
|
||||
metadata:
|
||||
|
|
@ -25,8 +24,6 @@ spec:
|
|||
- key: node-role.kubernetes.io/control-plane
|
||||
operator: Exists
|
||||
effect: "NoSchedule"
|
||||
initContainers:
|
||||
{{- include "kubernetes.waitForAdminKubeconfig" $ | nindent 6 }}
|
||||
containers:
|
||||
- name: csi-driver
|
||||
imagePullPolicy: Always
|
||||
|
|
@ -237,6 +234,4 @@ spec:
|
|||
emptyDir: {}
|
||||
- secret:
|
||||
secretName: {{ .Release.Name }}-admin-kubeconfig
|
||||
optional: true
|
||||
name: kubeconfig
|
||||
{{- end }}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{{- if and .Values.addons.certManager.enabled .Values._namespace.etcd }}
|
||||
{{- if .Values.addons.certManager.enabled }}
|
||||
apiVersion: helm.toolkit.fluxcd.io/v2
|
||||
kind: HelmRelease
|
||||
metadata:
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ cert-manager:
|
|||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- if and .Values.addons.certManager.enabled .Values._namespace.etcd }}
|
||||
{{- if .Values.addons.certManager.enabled }}
|
||||
apiVersion: helm.toolkit.fluxcd.io/v2
|
||||
kind: HelmRelease
|
||||
metadata:
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ cilium:
|
|||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- if .Values._namespace.etcd }}
|
||||
apiVersion: helm.toolkit.fluxcd.io/v2
|
||||
kind: HelmRelease
|
||||
metadata:
|
||||
|
|
@ -56,4 +55,3 @@ spec:
|
|||
- name: {{ .Release.Name }}-gateway-api-crds
|
||||
namespace: {{ .Release.Namespace }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ coredns:
|
|||
clusterIP: "10.95.0.10"
|
||||
{{- end }}
|
||||
|
||||
{{- if .Values._namespace.etcd }}
|
||||
apiVersion: helm.toolkit.fluxcd.io/v2
|
||||
kind: HelmRelease
|
||||
metadata:
|
||||
|
|
@ -43,4 +42,3 @@ spec:
|
|||
{{- end }}
|
||||
- name: {{ .Release.Name }}-cilium
|
||||
namespace: {{ .Release.Namespace }}
|
||||
{{- end }}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
{{- if .Values._namespace.etcd }}
|
||||
apiVersion: helm.toolkit.fluxcd.io/v2
|
||||
kind: HelmRelease
|
||||
metadata:
|
||||
|
|
@ -40,4 +39,3 @@ spec:
|
|||
- name: {{ .Release.Name }}
|
||||
namespace: {{ .Release.Namespace }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{{- if and .Values.addons.fluxcd.enabled .Values._namespace.etcd }}
|
||||
{{- if .Values.addons.fluxcd.enabled }}
|
||||
apiVersion: helm.toolkit.fluxcd.io/v2
|
||||
kind: HelmRelease
|
||||
metadata:
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
{{- if and $.Values.addons.gatewayAPI.enabled $.Values._namespace.etcd }}
|
||||
{{- if $.Values.addons.gatewayAPI.enabled }}
|
||||
apiVersion: helm.toolkit.fluxcd.io/v2
|
||||
kind: HelmRelease
|
||||
metadata:
|
||||
|
|
|
|||
|
|
@ -1,12 +1,4 @@
|
|||
{{- define "cozystack.defaultGpuOperatorValues" -}}
|
||||
{{- if .Values.addons.hami.enabled }}
|
||||
gpu-operator:
|
||||
devicePlugin:
|
||||
enabled: false
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- if and .Values.addons.gpuOperator.enabled .Values._namespace.etcd }}
|
||||
{{- if .Values.addons.gpuOperator.enabled }}
|
||||
apiVersion: helm.toolkit.fluxcd.io/v2
|
||||
kind: HelmRelease
|
||||
metadata:
|
||||
|
|
@ -37,12 +29,9 @@ spec:
|
|||
force: true
|
||||
remediation:
|
||||
retries: -1
|
||||
{{- $defaults := fromYaml (include "cozystack.defaultGpuOperatorValues" .) }}
|
||||
{{- $overrides := deepCopy (default (dict) .Values.addons.gpuOperator.valuesOverride) }}
|
||||
{{- $merged := mergeOverwrite (default (dict) $defaults) $overrides }}
|
||||
{{- if $merged }}
|
||||
{{- with .Values.addons.gpuOperator.valuesOverride }}
|
||||
values:
|
||||
{{- toYaml $merged | nindent 4 }}
|
||||
{{- toYaml . | nindent 4 }}
|
||||
{{- end }}
|
||||
|
||||
dependsOn:
|
||||
|
|
|
|||
|
|
@ -1,49 +0,0 @@
|
|||
{{- 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 }}
|
||||
|
|
@ -20,7 +20,7 @@ ingress-nginx:
|
|||
node-role.kubernetes.io/ingress-nginx: ""
|
||||
{{- end }}
|
||||
|
||||
{{- if and .Values.addons.ingressNginx.enabled .Values._namespace.etcd }}
|
||||
{{- if .Values.addons.ingressNginx.enabled }}
|
||||
apiVersion: helm.toolkit.fluxcd.io/v2
|
||||
kind: HelmRelease
|
||||
metadata:
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
{{- if .Values._namespace.etcd }}
|
||||
apiVersion: helm.toolkit.fluxcd.io/v2
|
||||
kind: HelmRelease
|
||||
metadata:
|
||||
|
|
@ -37,4 +36,3 @@ spec:
|
|||
namespace: {{ .Release.Namespace }}
|
||||
- name: {{ .Release.Name }}-prometheus-operator-crds
|
||||
namespace: {{ .Release.Namespace }}
|
||||
{{- end }}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue