Compare commits

...
Sign in to create a new pull request.

56 commits

Author SHA1 Message Date
cozystack-ci[bot]
a8cb7e49c1 Prepare release v1.2.4
Signed-off-by: cozystack-ci[bot] <274107086+cozystack-ci[bot]@users.noreply.github.com>
2026-04-30 01:39:35 +00:00
myasnikovdaniil
c611aa3de7
[Backport release-1.2] fix(platform): migrate ACME HTTP-01 to ingressClassName API (#2438)
ClusterIssuer solver referenced IngressClass "nginx" which does not
exist on cozystack clusters — real classes are named after tenant
namespaces (e.g. tenant-root). Cert issuance only worked because every
requesting Ingress overrode the ClusterIssuer via the legacy
acme.cert-manager.io/http01-ingress-class annotation.

Switch both sides to the modern cert-manager API (available since
cert-manager 1.12; cozystack ships 1.19.3):

- ClusterIssuer: http01.ingress.ingressClassName, value parameterized
from _cluster.expose-ingress (default "tenant-root")
- Ingress annotation: http01-ingress-ingressclassname

These must migrate together — mixing ingressClassName (ClusterIssuer)
with the old http01-ingress-class annotation triggers cert-manager's
"fields ingressClassName and class cannot be set at the same time"
validation and breaks issuance.

Assisted-By: Claude <noreply@anthropic.com>

(cherry picked from commit 2b6e20cc3f)

<!-- Thank you for making a contribution! Here are some tips for you:
- Use Conventional Commits for the PR title: `type(scope): description`
- Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore
- Scopes for system components: dashboard, platform, cilium, kube-ovn,
linstor, fluxcd, cluster-api
- Scopes for managed apps: postgres, mariadb, redis, kafka, clickhouse,
virtual-machine, kubernetes
- Scopes for development and maintenance: api, hack, tests, ci, docs,
maintenance
- Breaking changes: append `!` after type/scope (`feat(api)!: ...`) or
add a `BREAKING CHANGE:` footer
- If it's a work in progress, consider creating this PR as a draft.
- Don't hesistate to ask for opinion and review in the community chats,
even if it's still a draft.
- Add the label `backport` if it's a bugfix that needs to be backported
to a previous version.
-->

## 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
- Follow the guidelines at
https://github.com/kubernetes/community/blob/master/contributors/guide/release-notes.md.
-->

```release-note

```
2026-04-21 19:34:36 +05:00
Myasnikov Daniil
f7ca9880a4
fix(platform): migrate ACME HTTP-01 to ingressClassName API
ClusterIssuer solver referenced IngressClass "nginx" which does not
exist on cozystack clusters — real classes are named after tenant
namespaces (e.g. tenant-root). Cert issuance only worked because every
requesting Ingress overrode the ClusterIssuer via the legacy
acme.cert-manager.io/http01-ingress-class annotation.

Switch both sides to the modern cert-manager API (available since
cert-manager 1.12; cozystack ships 1.19.3):

- ClusterIssuer: http01.ingress.ingressClassName, value parameterized
  from _cluster.expose-ingress (default "tenant-root")
- Ingress annotation: http01-ingress-ingressclassname

These must migrate together — mixing ingressClassName (ClusterIssuer)
with the old http01-ingress-class annotation triggers cert-manager's
"fields ingressClassName and class cannot be set at the same time"
validation and breaks issuance.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
(cherry picked from commit 2b6e20cc3f)
2026-04-21 18:43:28 +05:00
myasnikovdaniil
d59a691eb7
Release v1.2.3 (#2424)
This PR prepares the release `v1.2.3`.
2026-04-21 16:46:09 +05:00
cozystack-ci[bot]
4e32be953c Prepare release v1.2.3
Signed-off-by: cozystack-ci[bot] <274107086+cozystack-ci[bot]@users.noreply.github.com>
2026-04-21 01:36:49 +00:00
Andrei Kvapil
4d60e5ffda
[Backport release-1.2] fix(kubernetes): set explicit ephemeral-storage on virt-launcher pods (#2423)
# Description
Backport of #2317 to `release-1.2`.
2026-04-20 10:24:11 +02:00
Andrei Kvapil
9accd4a7c9 fix(kubernetes): set explicit ephemeral-storage on virt-launcher pods
Without explicit ephemeral-storage resources on the VirtualMachine spec,
virt-launcher pods inherit default limits from the namespace LimitRange,
which can be much lower than the emptyDisk capacity. This causes kubelet
to evict the pod when the VM's containerd unpacks images exceeding the
limit.

Set domain.resources with ephemeral-storage calculated via cozy-lib
allocation ratio based on the configured ephemeralStorage value.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
(cherry picked from commit 94d42749a7)
2026-04-20 08:18:29 +00:00
Andrei Kvapil
93ad9366e2
Release v1.2.2 (#2379)
This PR prepares the release `v1.2.2`.
2026-04-14 09:53:15 +02:00
cozystack-ci[bot]
6ccdcf8d55 Prepare release v1.2.2
Signed-off-by: cozystack-ci[bot] <274107086+cozystack-ci[bot]@users.noreply.github.com>
2026-04-14 01:37:02 +00:00
Andrei Kvapil
161d4fe17e
[Backport release-1.2] ci: use cozystack org noreply email for bot commits (#2393)
# Description
Backport of #2392 to `release-1.2`.
2026-04-13 16:36:37 +02:00
Andrei Kvapil
14c652c8eb ci: use cozystack org noreply email for bot commits
The cozystack-ci[bot] noreply email with brackets is rejected by DCO
probot as an invalid email address, causing release PRs to fail DCO
checks.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
(cherry picked from commit ce0e709be8)
2026-04-13 14:31:28 +00:00
Andrei Kvapil
626ba21122
[Backport release-1.2] fix(build): filter git describe to match only v* tags (#2389)
# Description
Backport of #2386 to `release-1.2`.
2026-04-13 15:27:12 +02:00
Andrei Kvapil
a8b11eea7b fix(build): filter git describe to match only v* tags
The api/apps/v1alpha1/* subtags share the same commit as the release
tags. git describe --exact-match picks the first match alphabetically,
returning api/apps/v1alpha1/vX.Y.Z instead of vX.Y.Z, which produces
invalid Docker image tags.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
(cherry picked from commit fbbccdbb7b)
2026-04-13 13:26:45 +00:00
myasnikovdaniil
6ba8309617
Fix system postgresql images to 17.7-standard-trixie (#2369)
(cherry picked from commit a3f50ba2bd)

<!-- Thank you for making a contribution! Here are some tips for you:
- 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.
-->

## What this PR does


### Release note

<!--  Write a release note:
- Explain what has changed internally and for users.
- 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
[]
```
2026-04-13 15:52:25 +05:00
Andrei Kvapil
82a6b48314
[Backport release-1.2] ci(pull-requests): replace GH_PAT with cozystack-ci GitHub App token (#2384)
# Description
Backport of #2383 to `release-1.2`.
2026-04-13 10:07:37 +02:00
Andrei Kvapil
1c78614bbf ci(pull-requests): replace GH_PAT with cozystack-ci GitHub App token
The GH_PAT secret tied to cozystack-bot is no longer valid after
migrating release workflows to the cozystack-ci GitHub App in #2351.
This broke the resolve_assets and e2e jobs for release PRs.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
(cherry picked from commit 1144211a85)
2026-04-13 08:02:26 +00:00
Andrei Kvapil
8b1f29975c
Replace cozystack-bot PAT with cozystack-ci GitHub App (#2351)
## Summary

Replaces the `cozystack-bot` user account + personal access token
(`GH_PAT`) with the `cozystack-ci` GitHub App across all CI/CD release
workflows.

### Why

- **Security**: GitHub App tokens are short-lived (1h) and scoped, vs. a
long-lived PAT tied to a user account
- **Auditability**: App actions are clearly attributed to
`cozystack-ci[bot]`
- **No 2FA issue**: The bot account lacked 2FA; GitHub Apps don't
require it
- **Best practice**: GitHub recommends Apps over PATs for automation

### Changes

| Workflow | What changed |
|---|---|
| `tags.yaml` | App token in all 3 jobs: prepare-release,
generate-changelog, update-website-docs |
| `auto-release.yaml` | App token for daily auto-patch tagging |
| `pull-requests-release.yaml` | App token for release PR finalization |

All `cozystack-bot` git identity replaced with `cozystack-ci[bot]`.

### New secrets (already configured at org level)

- `COZYSTACK_CI_APP_ID` — GitHub App ID
- `COZYSTACK_CI_PRIVATE_KEY` — GitHub App private key

### After merge

- [ ] Delete `GH_PAT` repo-level secret
- [ ] Remove `cozystack-bot` from the cozystack org

🤖 Generated with [Claude Code](https://claude.com/claude-code)

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

## Summary by CodeRabbit

* **Chores**
* Updated CI/CD automation authentication mechanisms across release and
tagging workflows for improved security practices.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2026-04-12 12:08:09 +02:00
Andrei Kvapil
94f5b79390
[Backport release-1.2] [cilium] Opt-out of cri-containerd.apparmor.d for nsenter init containers (#2378)
# Description
Backport of #2370 to `release-1.2`.
2026-04-12 11:40:54 +02:00
Aleksei Sviridkin
5fc12d44b7 [cilium] Move AppArmor podAnnotations to a non-Talos-only values file
E2E revealed that the kube-apiserver rejects a DaemonSet carrying a
per-container AppArmor annotation for a container that is not present
in the pod spec:

    DaemonSet.apps 'cilium' is invalid:
    spec.template.annotations[container.apparmor.security.beta.kubernetes.io/mount-cgroup]:
    Invalid value: 'mount-cgroup': container not found

On Talos (isp-full bundle) values-talos.yaml sets
cgroup.autoMount.enabled=false, so the upstream chart does not render
the mount-cgroup init container, and our unconditional podAnnotations
entry for it becomes an invalid reference.

Move the podAnnotations block from the shared values.yaml into a new
values-apparmor.yaml and include it only in the cilium-generic and
kubeovn-cilium-generic PackageSource variants, where
cgroup.autoMount.enabled is explicitly true. Talos variants
(cilium, cilium-kilo, kubeovn-cilium) get no AppArmor annotations,
which is safe because the Talos kernel does not load the AppArmor
LSM anyway.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
(cherry picked from commit abd6667eb3)
2026-04-12 09:32:15 +00:00
Aleksei Sviridkin
b0ca7ce577 [cilium] Harden vendoring guard and clarify Talos annotation comment
- Broaden the grep guard in the Makefile update: target to match any
  container.apparmor.security.beta.kubernetes.io key, not only the
  quoted cilium-agent line. The previous pattern would silently pass
  if upstream switched to an unquoted annotation value, masking a
  failed perl patch; grepping for the prefix catches any residual
  hardcoded AppArmor annotation regardless of format.

- Rewrite the values.yaml comment to distinguish the two Talos-safe
  reasons: mount-cgroup is simply not rendered when
  cgroup.autoMount.enabled is false (Talos default), while the other
  annotations are harmless because kubelet ignores AppArmor metadata
  on nodes without the LSM loaded.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
(cherry picked from commit fc9ef55c42)
2026-04-12 09:32:15 +00:00
Aleksei Sviridkin
caacf92507 [cilium] Clarify nsenter/AppArmor comment wording
Replace the misleading 'nsenter --ptrace' shorthand with an accurate
description: nsenter joins the host cgroup/mount namespace, and the
cri-containerd.apparmor.d profile reports the denial as a blocked
'ptrace' operation (operation name, not an nsenter CLI flag).

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
(cherry picked from commit 0b0d3de99d)
2026-04-12 09:32:15 +00:00
Aleksei Sviridkin
016fe585b5 [cilium] Add fail-fast guard for the vendored daemonset patch
The perl multi-line delete in the update: target silently becomes a
no-op if the upstream template is ever reformatted. Verify the patch
applied by grepping for one of the stripped annotation keys after the
perl runs; fail the update: target loudly if it still exists, so a
future upstream reformat is caught instead of silently reintroducing
the duplicate-key rendering on k8s < 1.30.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
(cherry picked from commit 39cd2658b5)
2026-04-12 09:32:15 +00:00
Aleksei Sviridkin
b23775dc7b [cilium] Strip the entire k8s<1.30 AppArmor block from vendored daemonset
The previous sed patch removed only the four annotation lines and
left an empty nested '{{- if ... }}{{- end }}{{- end }}{{- end }}'
skeleton behind. That skeleton was dead code, but it also meant that
the next make update would re-fetch the upstream chart with the
annotations back inside the same block, and the sed would strip them
again — leaving the skeleton behind forever.

Replace the per-line sed patch with a single perl -i -0pe multi-line
delete that removes the entire <1.30 AppArmor block in one step, and
apply the same patch to the currently vendored copy for consistency.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
(cherry picked from commit 5a50df800d)
2026-04-12 09:32:15 +00:00
Aleksei Sviridkin
d093ec2eb0 [cilium] Strip hardcoded AppArmor annotations from vendored daemonset
The upstream chart emits per-container AppArmor unconfined annotations
inside a k8s<1.30 branch of cilium-agent/daemonset.yaml. Cozystack now
owns these annotations via cilium.podAnnotations in values.yaml so they
also take effect on k8s>=1.30 (where upstream drops them in favour of
the containerd-broken podSecurityContext.appArmorProfile). Having both
sources emit the same keys produced a duplicate mapping on k8s<1.30.

Add a new SED_INPLACE invocation to the package Makefile that removes
the four hardcoded annotation lines from the vendored template, and
apply the patch to the currently vendored copy so make update will
reproduce the same state on future refreshes.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
(cherry picked from commit 182fc0b52d)
2026-04-12 09:32:15 +00:00
Aleksei Sviridkin
1433adf88f [cilium] Document duplicate-key rendering on unsupported k8s < 1.30
On Kubernetes versions below 1.30 the upstream cilium chart still emits
the deprecated per-container AppArmor annotations from its own
semverCompare '<1.30.0' branch. Our podAnnotations override then emits
the same keys again, yielding a YAML mapping with duplicate entries
(identical values, last-wins by kubernetes API server JSON semantics).

Cozystack's supported Kubernetes matrix starts at 1.30
(packages/apps/kubernetes/files/versions.yaml), so this rendering
quirk never affects any shipped version. Document the behaviour in a
comment so future readers are not surprised.

No functional change.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
(cherry picked from commit 703dbca734)
2026-04-12 09:32:15 +00:00
Aleksei Sviridkin
5bf0112bed [cilium] Drop mount-bpf-fs from AppArmor unconfined annotations
mount-bpf-fs runs as privileged in the vendored chart, and the Linux
kernel does not apply AppArmor profiles to privileged containers. The
per-container unconfined annotation for this container is a no-op and
only widens the pod's admission-policy surface for no reason.

Keep the four meaningful annotations (cilium-agent, clean-cilium-state,
mount-cgroup, apply-sysctl-overwrites) which target unprivileged agent
and init containers actually affected by the cri-containerd.apparmor.d
profile.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
(cherry picked from commit 2531ab661a)
2026-04-12 09:32:15 +00:00
Aleksei Sviridkin
06dce029c9 [cilium] Opt-out of cri-containerd.apparmor.d for nsenter init containers
Cilium 1.19 init containers mount-cgroup, apply-sysctl-overwrites,
mount-bpf-fs and clean-cilium-state call nsenter --ptrace, which is
denied by the cri-containerd.apparmor.d profile on Ubuntu 22.04+ and
any other distro loading this AppArmor profile. As a result cilium
agent pods land in Init:CrashLoopBackOff and downstream HelmReleases
cascade into "dependency not ready".

The modern k8s >= 1.30 pod-level appArmorProfile: Unconfined field is
accepted into the DaemonSet spec but not honoured by containerd CRI
at runtime (kubernetes/kubernetes#125069), so the deprecated
per-container AppArmor annotations remain the only reliable opt-out.

Add the annotations via podAnnotations in the wrapper values.yaml.
The change is harmless on systems without AppArmor (Talos,
RHEL/Rocky/AlmaLinux with SELinux) because kubelet silently ignores
unknown AppArmor annotations when the LSM is not loaded.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
(cherry picked from commit 6c0ae2570e)
2026-04-12 09:32:15 +00:00
Andrei Kvapil
5674ce73e6
[Backport release-1.2] linstor: update piraeus-server to v1.33.2 with selected backports (#2377)
# Description
Backport of #2331 to `release-1.2`.
2026-04-12 11:22:13 +02:00
Andrei Kvapil
69ccaa772e linstor: bump piraeus-server from v1.33.1 to v1.33.2
All existing patches (allow-toggle-disk-retry, fix-duplicate-tcp-ports,
fix-luks-header-size, retry-adjust-after-stale-bitmap) apply cleanly on
the new version without modifications.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
(cherry picked from commit 7fb0c575c0)
2026-04-12 09:20:47 +00:00
Andrei Kvapil
198c57c1ea linstor: add stale bitmap adjust retry patch
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
(cherry picked from commit fd6bae62b2)
2026-04-12 09:20:47 +00:00
Andrei Kvapil
8d1628df49 linstor: update piraeus-server patches for v1.33.1
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
(cherry picked from commit e4468148f6)
2026-04-12 09:20:47 +00:00
Myasnikov Daniil
0726e1aa51
Fix system postgresql images to 17.7-standard-trixie
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
(cherry picked from commit a3f50ba2bd)
2026-04-10 19:05:10 +05:00
Andrei Kvapil
168ebe1a3e
[Backport release-1.2] [monitoring] Fix infra dashboards missing in default variant (#2367)
# Description
Backport of #2365 to `release-1.2`.
2026-04-10 02:12:39 +02:00
mattia-eleuteri
3b2d6a6325 [monitoring] Fix infra dashboards missing in default variant
The default variant deploys monitoring to the cozy-monitoring namespace,
but the infra dashboards condition only checked for tenant-root. This
adds cozy-monitoring to the condition, consistent with the existing
pattern in vmagent.yaml.

Fixes cozystack/cozystack#2349

Signed-off-by: mattia-eleuteri <mattia@hidora.io>
(cherry picked from commit b8d48ad711)
2026-04-09 23:23:18 +00:00
Andrei Kvapil
ec480b0617
[Backport release-1.2] [virtual-machine] Exclude external VM services from Cilium BPF LB (#2361)
# Description
Backport of #2357 to `release-1.2`.
2026-04-08 16:54:49 +02:00
mattia-eleuteri
a0209327b4 [virtual-machine] Exclude external VM services from Cilium BPF LB
Add service.kubernetes.io/service-proxy-name label to LoadBalancer
services when external: true. This prevents Cilium from adding the
service to its BPF service map, fixing two issues:

1. Inter-tenant connectivity via public LB IPs: Cilium's kube-proxy
   replacement DNATs traffic to LB IPs before policy evaluation,
   causing the CiliumClusterwideNetworkPolicy to block legitimate
   cross-tenant traffic through public IPs.

2. WholeIP broken on Cilium 1.19+ (#2327): wildcard service drop
   entries block all ports not declared in the Service spec before
   traffic reaches cozy-proxy's nftables rules.

With this label, Cilium completely ignores the Service. Routing is
handled by kube-ovn (via the wholeIP annotation) and MetalLB
continues to advertise the IP via L2 unaffected.

Tested on Cilium 1.18.6 and 1.19.1: inter-tenant via LB IP works,
pod IP isolation preserved, external access unaffected.

Signed-off-by: mattia-eleuteri <mattia@hidora.io>
(cherry picked from commit 026b1c7811)
2026-04-08 14:44:34 +00:00
Andrei Kvapil
1b32142ce3
Release v1.2.1 (#2306)
This PR prepares the release `v1.2.1`.
2026-03-31 14:59:27 +02:00
cozystack-bot
27638a14b5 Prepare release v1.2.1
Signed-off-by: cozystack-bot <217169706+cozystack-bot@users.noreply.github.com>
2026-03-31 11:32:30 +00:00
Andrei Kvapil
52a16dc518
fix(ci): force-update API subtag on re-runs
Always force-create and force-push the API submodule tag so it stays
in sync with the main version tag, even when re-tagging.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2026-03-31 13:25:26 +02:00
Andrei Kvapil
d68f56ae19
fix(ci): make tags workflow idempotent on re-runs
- Remove broken `git push origin HEAD` in detached HEAD state (commit
  artifacts are already pushed via the "Create release branch" step)
- Make subtag push idempotent: use explicit refs/tags/ prefix and
  tolerate already-existing remote tags after verifying they point to
  the correct commit

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2026-03-31 13:25:26 +02:00
Andrei Kvapil
6de6ca1abf
[Backport release-1.2] fix(multus): pin master CNI to 05-cilium.conflist (#2316)
# Description
Backport of #2315 to `release-1.2`.
2026-03-31 12:03:09 +02:00
Andrei Kvapil
aefd8b45ff fix(multus): pin master CNI to 05-cilium.conflist
Set multusMasterCNI to explicitly use 05-cilium.conflist instead of
auto-detecting the first available conflist at boot. This prevents a
race condition where multus picks 10-kube-ovn.conflist if it appears
before 05-cilium.conflist during node startup, bypassing the Cilium
CNI chain entirely.

Upstream issue: https://github.com/k8snetworkplumbingwg/multus-cni/issues/1499
Tracking issue: https://github.com/cozystack/cozystack/issues/2310

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
(cherry picked from commit e09cd0f37f)
2026-03-31 10:02:01 +00:00
Andrei Kvapil
f16ef08a98
[Backport release-1.2] fix(multus): build custom image with DEL cache fix (#2314)
# Description
Backport of #2313 to `release-1.2`.
2026-03-31 11:22:52 +02:00
Andrei Kvapil
3ab03d3bce chore: remove build artifact
Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
(cherry picked from commit 1cd564330c)
2026-03-31 09:22:35 +00:00
Andrei Kvapil
56f0484eb0 fix(multus): build custom image with DEL cache fix
When CNI ADD never completes, multus DEL fails because delegate plugins
require runtime state that only exists after a successful ADD. This
creates a deadlock where containerd cannot release sandbox name
reservations, permanently blocking pod creation on the affected node.

Build a custom multus-cni image with a patch that returns success on
DEL when no cache file exists (ADD was never completed).

Upstream PR: https://github.com/k8snetworkplumbingwg/multus-cni/pull/1498
Tracking issue: https://github.com/cozystack/cozystack/issues/2310

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
(cherry picked from commit 094b80ae16)
2026-03-31 09:22:35 +00:00
Andrei Kvapil
9a1b45b49d
[Backport release-1.2] fix(linstor): set verify-alg to crc32c to avoid crct10dif unavailability (#2312)
# Description
Backport of #2303 to `release-1.2`.
2026-03-31 10:48:26 +02:00
Andrei Kvapil
e3336bf6f5 fix(linstor): set verify-alg to crc32c to avoid crct10dif unavailability
The crct10dif kernel crypto module is no longer available in Talos
v1.12.6 (kernel 6.18.18). DRBD auto-verify selects crct10dif by
default, causing peer connections to fail with VERIFYAlgNotAvail.
Setting verify-alg explicitly to crc32c at the controller level
ensures all resources use an available algorithm.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
(cherry picked from commit 30616b73f3)
2026-03-31 08:48:01 +00:00
Andrei Kvapil
064294d870
[Backport release-1.2] feat(postgres): hardcode PostgreSQL 17 for monitoring and add migration (#2309)
# Description
Backport of #2304 to `release-1.2`.
2026-03-31 10:16:13 +02:00
IvanHunters
f6ce580126 feat(postgres): extend v17 hardcode to all system components
Add explicit PostgreSQL 17.7 image to Harbor, SeaweedFS, and Keycloak
databases to ensure consistent version across all system components.

Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>
(cherry picked from commit 620c8fb3c0)
2026-03-31 08:15:51 +00:00
IvanHunters
091f21fbd2 feat(postgres): hardcode PostgreSQL 17 for monitoring and add migration
Add migration 37 to backfill spec.version=v17 for existing PostgreSQL
resources without a version set.

Hardcode PostgreSQL 17.7 image in monitoring databases (Grafana and Alerta)
to ensure compatibility with monitoring queries that expect PostgreSQL 17
features (pg_stat_checkpointer, updated pg_stat_bgwriter).

Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>
(cherry picked from commit fd1714442e)
2026-03-31 08:15:50 +00:00
Andrei Kvapil
02e200372b
[Backport release-1.2] fix(platform): propagate resource allocation ratios to packages (#2301)
# Description
Backport of #2296 to `release-1.2`.
2026-03-30 20:16:15 +02:00
Kirill Ilin
982a6508a9 fix(platform): propagate resource allocation ratios to packages
Since the bundle restructure in 2d022e38, the cpuAllocationRatio,
memoryAllocationRatio and ephemeralStorageAllocationRatio values from
platform values.yaml were no longer propagated to child packages.

The old monolithic bundles read these values from the cozystack
ConfigMap via lookup and passed them explicitly. After migration to
the Package CRD model, the bridge was lost: cozy-lib expects the
ratios in _cluster config (via cozystack-values Secret), but platform
never wrote them there, so the hardcoded defaults were always used.

This commit restores the propagation by:
- Adding cpu/memory/ephemeral-storage allocation ratios to the
  _cluster section in the cozystack-values Secret, so cozy-lib in
  all managed applications picks them up.
- Passing cpuAllocationRatio to the kubevirt Package component, so
  the KubeVirt CR gets the configured value.

Assisted-By: Claude AI
Signed-off-by: Kirill Ilin <stitch14@yandex.ru>
(cherry picked from commit b42a8ed7e4)
2026-03-30 18:15:24 +00:00
Andrei Kvapil
0eefd0b236
[Backport release-1.2] [linstor] Preserve TCP ports during toggle-disk operations (#2299)
# Description
Backport of #2292 to `release-1.2`.
2026-03-30 20:08:26 +02:00
Andrei Kvapil
56e6178685 fix(linstor): preserve TCP ports during toggle-disk operations
Update fix-duplicate-tcp-ports patch to preserve existing TCP ports when
DrbdRscData is recreated during toggle-disk operations. Without this,
removeLayerData() frees ports and ensureStackDataExists() may allocate
different ones, causing port mismatches between controller and satellites
if the satellite misses the update.

Also add dh_strip_nondeterminism override in Dockerfile to fix build
failures on some JAR files.

Upstream: https://github.com/LINBIT/linstor-server/pull/476#issuecomment-4147527442

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
(cherry picked from commit 812d4138bb)
2026-03-30 18:06:55 +00:00
Andrei Kvapil
4d92b6362b
[Backport release-1.2] [platform] Prevent installed packages deletion (#2297)
# Description
Backport of #2273 to `release-1.2`.
2026-03-30 20:04:36 +02:00
Myasnikov Daniil
7eca74675a [platform] Added resource-policy to keep installed packages
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
(cherry picked from commit a243f3d72a)
2026-03-30 16:37:01 +00:00
76 changed files with 1464 additions and 664 deletions

View file

@ -20,6 +20,14 @@ jobs:
pull-requests: read
steps:
- name: Generate GitHub App token
id: app-token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.COZYSTACK_CI_APP_ID }}
private-key: ${{ secrets.COZYSTACK_CI_PRIVATE_KEY }}
owner: cozystack
- name: Checkout code
uses: actions/checkout@v4
with:
@ -28,27 +36,27 @@ jobs:
- name: Configure git
env:
GH_PAT: ${{ secrets.GH_PAT }}
APP_TOKEN: ${{ steps.app-token.outputs.token }}
run: |
git config user.name "cozystack-bot"
git config user.email "217169706+cozystack-bot@users.noreply.github.com"
git remote set-url origin https://cozystack-bot:${GH_PAT}@github.com/${GITHUB_REPOSITORY}
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 config --unset-all http.https://github.com/.extraheader || true
- name: Process release branches
uses: actions/github-script@v7
env:
GH_PAT: ${{ secrets.GH_PAT }}
APP_TOKEN: ${{ steps.app-token.outputs.token }}
with:
github-token: ${{ secrets.GH_PAT }}
github-token: ${{ steps.app-token.outputs.token }}
script: |
const { execSync } = require('child_process');
// Configure git to use PAT for authentication
execSync('git config user.name "cozystack-bot"', { encoding: 'utf8' });
execSync('git config user.email "217169706+cozystack-bot@users.noreply.github.com"', { encoding: 'utf8' });
execSync(`git remote set-url origin https://cozystack-bot:${process.env.GH_PAT}@github.com/${process.env.GITHUB_REPOSITORY}`, { encoding: 'utf8' });
// Remove GITHUB_TOKEN extraheader to ensure PAT is used (needed to trigger other workflows)
// Configure git to use GitHub App token for authentication
execSync('git config user.name "cozystack-ci[bot]"', { encoding: 'utf8' });
execSync('git config user.email "274107086+cozystack-ci[bot]@users.noreply.github.com"', { encoding: 'utf8' });
execSync(`git remote set-url origin https://x-access-token:${process.env.APP_TOKEN}@github.com/${process.env.GITHUB_REPOSITORY}`, { encoding: 'utf8' });
// Remove GITHUB_TOKEN extraheader to ensure App token is used (needed to trigger other workflows)
execSync('git config --unset-all http.https://github.com/.extraheader || true', { encoding: 'utf8' });
// Get all release-X.Y branches

View file

@ -23,6 +23,14 @@ jobs:
contains(github.event.pull_request.labels.*.name, 'release')
steps:
- name: Generate GitHub App token
id: app-token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.COZYSTACK_CI_APP_ID }}
private-key: ${{ secrets.COZYSTACK_CI_PRIVATE_KEY }}
owner: cozystack
# Extract tag from branch name (branch = release-X.Y.Z*)
- name: Extract tag from branch name
id: get_tag
@ -47,11 +55,11 @@ jobs:
- name: Create tag on merge commit
env:
GH_PAT: ${{ secrets.GH_PAT }}
APP_TOKEN: ${{ steps.app-token.outputs.token }}
run: |
git config user.name "cozystack-bot"
git config user.email "217169706+cozystack-bot@users.noreply.github.com"
git remote set-url origin https://cozystack-bot:${GH_PAT}@github.com/${GITHUB_REPOSITORY}
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 tag -f ${{ steps.get_tag.outputs.tag }} ${{ github.sha }}
git push -f origin ${{ steps.get_tag.outputs.tag }}
@ -59,7 +67,7 @@ jobs:
- name: Ensure maintenance branch release-X.Y
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GH_PAT }}
github-token: ${{ steps.app-token.outputs.token }}
script: |
const tag = '${{ steps.get_tag.outputs.tag }}'; // e.g. v0.1.3 or v0.1.3-rc3
const match = tag.match(/^v(\d+)\.(\d+)\.\d+(?:[-\w\.]+)?$/);

View file

@ -99,6 +99,14 @@ jobs:
disk_id: ${{ steps.fetch_assets.outputs.disk_id }}
steps:
- name: Generate GitHub App token
id: app-token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.COZYSTACK_CI_APP_ID }}
private-key: ${{ secrets.COZYSTACK_CI_PRIVATE_KEY }}
owner: cozystack
- name: Checkout code
if: contains(github.event.pull_request.labels.*.name, 'release')
uses: actions/checkout@v4
@ -125,7 +133,7 @@ jobs:
id: fetch_assets
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GH_PAT }}
github-token: ${{ steps.app-token.outputs.token }}
script: |
const tag = '${{ steps.get_tag.outputs.tag }}';
const releases = await github.rest.repos.listReleases({
@ -159,6 +167,15 @@ jobs:
if: ${{ always() && (needs.build.result == 'success' || needs.resolve_assets.result == 'success') }}
steps:
- name: Generate GitHub App token
if: contains(github.event.pull_request.labels.*.name, 'release')
id: app-token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.COZYSTACK_CI_APP_ID }}
private-key: ${{ secrets.COZYSTACK_CI_PRIVATE_KEY }}
owner: cozystack
# ▸ Checkout and prepare the codebase
- name: Checkout code
uses: actions/checkout@v4
@ -188,11 +205,11 @@ jobs:
if: contains(github.event.pull_request.labels.*.name, 'release')
run: |
mkdir -p _out/assets
curl -sSL -H "Authorization: token ${GH_PAT}" -H "Accept: application/octet-stream" \
curl -sSL -H "Authorization: token ${APP_TOKEN}" -H "Accept: application/octet-stream" \
-o _out/assets/nocloud-amd64.raw.xz \
"https://api.github.com/repos/${GITHUB_REPOSITORY}/releases/assets/${{ needs.resolve_assets.outputs.disk_id }}"
env:
GH_PAT: ${{ secrets.GH_PAT }}
APP_TOKEN: ${{ steps.app-token.outputs.token }}
- name: Set sandbox ID
run: echo "SANDBOX_NAME=cozy-e2e-sandbox-$(echo "${GITHUB_REPOSITORY}:${GITHUB_WORKFLOW}:${GITHUB_REF}" | sha256sum | cut -c1-10)" >> $GITHUB_ENV

View file

@ -25,6 +25,14 @@ jobs:
actions: write
steps:
- name: Generate GitHub App token
id: app-token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.COZYSTACK_CI_APP_ID }}
private-key: ${{ secrets.COZYSTACK_CI_PRIVATE_KEY }}
owner: cozystack
# Check if a non-draft release with this tag already exists
- name: Check if release already exists
id: check_release
@ -115,34 +123,29 @@ jobs:
- name: Commit release artifacts
if: steps.check_release.outputs.skip == 'false'
env:
GH_PAT: ${{ secrets.GH_PAT }}
APP_TOKEN: ${{ steps.app-token.outputs.token }}
run: |
git config user.name "cozystack-bot"
git config user.email "217169706+cozystack-bot@users.noreply.github.com"
git remote set-url origin https://cozystack-bot:${GH_PAT}@github.com/${GITHUB_REPOSITORY}
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 config --unset-all http.https://github.com/.extraheader || true
git add .
git commit -m "Prepare release ${GITHUB_REF#refs/tags/}" -s || echo "No changes to commit"
git push origin HEAD || true
# Tag the api/apps/v1alpha1 submodule for pkg.go.dev
- name: Tag API submodule
if: steps.check_release.outputs.skip == 'false'
env:
GH_PAT: ${{ secrets.GH_PAT }}
APP_TOKEN: ${{ steps.app-token.outputs.token }}
run: |
VTAG="${{ steps.tag.outputs.tag }}"
SUBTAG="api/apps/v1alpha1/${VTAG}"
git config user.name "cozystack-bot"
git config user.email "217169706+cozystack-bot@users.noreply.github.com"
git remote set-url origin https://cozystack-bot:${GH_PAT}@github.com/${GITHUB_REPOSITORY}
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}
TARGET="$(git rev-parse "${VTAG}^{}")"
if git rev-parse -q --verify "refs/tags/${SUBTAG}" >/dev/null; then
test "$(git rev-list -n1 "${SUBTAG}")" = "$TARGET"
else
git tag "${SUBTAG}" "$TARGET"
fi
git push origin "${SUBTAG}"
git tag -f "${SUBTAG}" "$TARGET"
git push -f origin "refs/tags/${SUBTAG}"
# Create or reuse draft release
- name: Create / reuse draft release
@ -188,11 +191,11 @@ jobs:
- name: Create release branch
if: steps.check_release.outputs.skip == 'false'
env:
GH_PAT: ${{ secrets.GH_PAT }}
APP_TOKEN: ${{ steps.app-token.outputs.token }}
run: |
git config user.name "cozystack-bot"
git config user.email "217169706+cozystack-bot@users.noreply.github.com"
git remote set-url origin https://cozystack-bot:${GH_PAT}@github.com/${GITHUB_REPOSITORY}
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}
BRANCH="release-${GITHUB_REF#refs/tags/v}"
git branch -f "$BRANCH"
git push -f origin "$BRANCH"
@ -202,7 +205,7 @@ jobs:
if: steps.check_release.outputs.skip == 'false'
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GH_PAT }}
github-token: ${{ steps.app-token.outputs.token }}
script: |
const version = context.ref.replace('refs/tags/v', '');
const base = '${{ steps.get_base.outputs.branch }}';
@ -244,6 +247,14 @@ jobs:
pull-requests: write
if: needs.prepare-release.result == 'success'
steps:
- name: Generate GitHub App token
id: app-token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.COZYSTACK_CI_APP_ID }}
private-key: ${{ secrets.COZYSTACK_CI_PRIVATE_KEY }}
owner: cozystack
- name: Parse tag
id: tag
uses: actions/github-script@v7
@ -266,7 +277,7 @@ jobs:
ref: main
fetch-depth: 0
fetch-tags: true
token: ${{ secrets.GH_PAT }}
token: ${{ steps.app-token.outputs.token }}
- name: Check if changelog already exists
id: check_changelog
@ -294,7 +305,7 @@ jobs:
if: steps.check_changelog.outputs.exists == 'false'
env:
COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}
GH_TOKEN: ${{ secrets.GH_PAT }}
GH_TOKEN: ${{ steps.app-token.outputs.token }}
run: |
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
@ -302,11 +313,11 @@ jobs:
- name: Create changelog branch and commit
if: steps.check_changelog.outputs.exists == 'false'
env:
GH_PAT: ${{ secrets.GH_PAT }}
APP_TOKEN: ${{ steps.app-token.outputs.token }}
run: |
git config user.name "cozystack-bot"
git config user.email "217169706+cozystack-bot@users.noreply.github.com"
git remote set-url origin https://cozystack-bot:${GH_PAT}@github.com/${GITHUB_REPOSITORY}
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}
CHANGELOG_FILE="docs/changelogs/v${{ steps.tag.outputs.version }}.md"
CHANGELOG_BRANCH="changelog-v${{ steps.tag.outputs.version }}"
@ -341,7 +352,7 @@ jobs:
if: steps.check_changelog.outputs.exists == 'false'
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GH_PAT }}
github-token: ${{ steps.app-token.outputs.token }}
script: |
const version = '${{ steps.tag.outputs.version }}';
const changelogBranch = `changelog-v${version}`;
@ -400,6 +411,14 @@ jobs:
permissions:
contents: read
steps:
- name: Generate GitHub App token
id: app-token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.COZYSTACK_CI_APP_ID }}
private-key: ${{ secrets.COZYSTACK_CI_PRIVATE_KEY }}
owner: cozystack
- name: Parse tag
id: tag
uses: actions/github-script@v7
@ -419,19 +438,19 @@ jobs:
uses: actions/checkout@v4
with:
repository: cozystack/website
token: ${{ secrets.GH_PAT }}
token: ${{ steps.app-token.outputs.token }}
ref: main
- name: Update docs from release branch
env:
GH_TOKEN: ${{ secrets.GH_PAT }}
GH_TOKEN: ${{ steps.app-token.outputs.token }}
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-bot"
git config user.email "217169706+cozystack-bot@users.noreply.github.com"
git config user.name "cozystack-ci[bot]"
git config user.email "274107086+cozystack-ci[bot]@users.noreply.github.com"
git add content
if git diff --cached --quiet; then
echo "No changes to commit"
@ -448,7 +467,7 @@ jobs:
- name: Open pull request
if: steps.commit.outputs.changed == 'true'
env:
GH_TOKEN: ${{ secrets.GH_PAT }}
GH_TOKEN: ${{ steps.app-token.outputs.token }}
run: |
BRANCH="update-docs-v${{ steps.tag.outputs.version }}"
pr_state=$(gh pr view "$BRANCH" --repo cozystack/website --json state --jq .state 2>/dev/null || echo "")

View file

@ -27,6 +27,7 @@ build: build-deps
make -C packages/system/dashboard image
make -C packages/system/metallb image
make -C packages/system/kamaji image
make -C packages/system/multus image
make -C packages/system/bucket image
make -C packages/system/objectstorage-controller image
make -C packages/system/grafana-operator image

View file

@ -6,13 +6,13 @@ else
endif
REGISTRY ?= ghcr.io/cozystack/cozystack
TAG = $(shell git describe --tags --exact-match 2>/dev/null || echo latest)
TAG = $(shell git describe --tags --exact-match --match 'v*' 2>/dev/null || echo latest)
PUSH := 1
LOAD := 0
BUILDER ?=
PLATFORM ?=
BUILDX_EXTRA_ARGS ?=
COZYSTACK_VERSION = $(patsubst v%,%,$(shell git describe --tags))
COZYSTACK_VERSION = $(patsubst v%,%,$(shell git describe --tags --match 'v*'))
BUILDX_ARGS := --provenance=false --push=$(PUSH) --load=$(LOAD) \
--label org.opencontainers.image.source=https://github.com/cozystack/cozystack \
@ -28,6 +28,6 @@ endef
ifeq ($(COZYSTACK_VERSION),)
$(shell git remote add upstream https://github.com/cozystack/cozystack.git || true)
$(shell git fetch upstream --tags)
COZYSTACK_VERSION = $(patsubst v%,%,$(shell git describe --tags))
COZYSTACK_VERSION = $(patsubst v%,%,$(shell git describe --tags --match 'v*'))
endif

View file

@ -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-class: {{ $ingress }}
acme.cert-manager.io/http01-ingress-ingressclassname: {{ $ingress }}
{{- end }}
cert-manager.io/cluster-issuer: {{ $clusterIssuer }}
spec:

View file

@ -1 +1 @@
ghcr.io/cozystack/cozystack/nginx-cache:0.0.0@sha256:2f987017ff95d3c782e16ef0d99928831f520daf154d08a2fa38c2d68363e036
ghcr.io/cozystack/cozystack/nginx-cache:0.0.0@sha256:d397781152ab9123b11b8191d92eba7a0d2faa376aa2c15ddeb67842a9b59bab

View file

@ -1 +1 @@
ghcr.io/cozystack/cozystack/kubevirt-csi-driver:0.0.0@sha256:5b209248acba3d8cf0d999edd8a915915a2e565a1c8ac1f5dde0dffec20fa02e
ghcr.io/cozystack/cozystack/kubevirt-csi-driver:0.0.0@sha256:0e70f4391b344c71a9567e1ed22ca24910116a619cb681381059b8c39f382219

View file

@ -1 +1 @@
ghcr.io/cozystack/cozystack/ubuntu-container-disk:v1.30@sha256:51e272db23d64eb20bc44e38cdbb70199fd00a49a958b76c2a89d4a46fec9256
ghcr.io/cozystack/cozystack/ubuntu-container-disk:v1.30@sha256:f460ed1fd5a721aed423e6c3b8be0105d7f693cd86ad992d9c15fd4b27e58cec

View file

@ -1 +1 @@
ghcr.io/cozystack/cozystack/ubuntu-container-disk:v1.31@sha256:31c3290305159fe484caf6c5780960cf071dc3939528295336da4abfa731a81b
ghcr.io/cozystack/cozystack/ubuntu-container-disk:v1.31@sha256:aaa2dfa8ee53ae26295f44d2491330f51412457bbe4aa6ba256297cc0bd8a0da

View file

@ -1 +1 @@
ghcr.io/cozystack/cozystack/ubuntu-container-disk:v1.32@sha256:9efb1fe18cffb045530b43ac4b39910f1802d3c16798d733165dcd1c67de9237
ghcr.io/cozystack/cozystack/ubuntu-container-disk:v1.32@sha256:7acb4728ef6b9c0ff3344bf486bb00f2a083f2098452344a362242235013db01

View file

@ -1 +1 @@
ghcr.io/cozystack/cozystack/ubuntu-container-disk:v1.33@sha256:a0afca1d6f720cfff764a1bc912bfe404b5e97ff731f6c126a7abdb51ed23f85
ghcr.io/cozystack/cozystack/ubuntu-container-disk:v1.33@sha256:a2024339ab9edb980a96b43ad2744b7aa31afaef0983ddbc6a546068b4cfa87a

View file

@ -1 +1 @@
ghcr.io/cozystack/cozystack/ubuntu-container-disk:v1.34@sha256:51dfe5ce3e2f8765fde9422ae7e9fba677016ec5e2be41559c5f11a56958b0e6
ghcr.io/cozystack/cozystack/ubuntu-container-disk:v1.34@sha256:97bcf946a5687889c6a421d14e5adece11f0978151b2165dc87b28f77f7607db

View file

@ -1 +1 @@
ghcr.io/cozystack/cozystack/ubuntu-container-disk:v1.35@sha256:c4ae418b2a2c139794cd9630c3b2c2171870cc7748ac200344d2c4ed4283cf0b
ghcr.io/cozystack/cozystack/ubuntu-container-disk:v1.35@sha256:364c6d454891f1eb1a598fddb69cf328a14dbc451a8ac65812b038a7756da60a

View file

@ -70,6 +70,8 @@ spec:
memory:
guest: {{ .group.resources.memory }}
{{- end }}
resources:
{{- include "cozy-lib.resources.sanitize" (list (dict "ephemeral-storage" .group.ephemeralStorage) $) | nindent 14 }}
evictionStrategy: External
volumes:
- name: system

View file

@ -1 +1 @@
ghcr.io/cozystack/cozystack/mariadb-backup:0.0.0@sha256:0ddbbec0568dcb9fbc317cd9cc654e826dbe88ba3f184fa9b6b58aacb93b4570
ghcr.io/cozystack/cozystack/mariadb-backup:0.0.0@sha256:3841eb171416711977dea0cf8cd45d32344caac9727af760c37d5e1dd41ee4bb

View file

@ -7,6 +7,7 @@ metadata:
apps.cozystack.io/user-service: "true"
{{- include "virtual-machine.labels" . | nindent 4 }}
{{- if .Values.external }}
service.kubernetes.io/service-proxy-name: "cozy-proxy"
annotations:
networking.cozystack.io/wholeIP: "true"
{{- end }}

View file

@ -1,9 +1,9 @@
cozystackOperator:
# Deployment variant: talos, generic, hosted
variant: talos
image: ghcr.io/cozystack/cozystack/cozystack-operator:v1.2.0@sha256:dbc0cbd99dbc2405769bed0d33d5e5d3f53e79e0269ad9ddaa26d9eebbcc6da2
image: ghcr.io/cozystack/cozystack/cozystack-operator:v1.2.4@sha256:1cd36a4643ecf4f06782f050ff9544b316ea52af67d738bf593f40e60c4acbec
platformSourceUrl: 'oci://ghcr.io/cozystack/cozystack/cozystack-packages'
platformSourceRef: 'digest=sha256:c659905000e8279d32965169cde0817e16a5b46f81d305a3606defcd5446abae'
platformSourceRef: 'digest=sha256:98d595ded030ee0b71ff6570419b81ae6cd1bcc8fb7a31c5e29556fbdfa5ec4a'
# Generic variant configuration (only used when cozystackOperator.variant=generic)
cozystack:
# Kubernetes API server host (IP only, no protocol/port)

View file

@ -0,0 +1,78 @@
#!/bin/bash
# Migration 37 --> 38
# Pin PostgreSQL image to 17.7-standard-trixie for system databases.
#
# This migration updates the imageName for all CNPG clusters belonging to
# system components (keycloak-db, grafana-db, alerta-db, seaweedfs-db, and
# harbor's dynamically-named DB):
# - If imageName is not set: pins to 17.7-standard-trixie
# - If imageName has any PG 17 tag: forces 17.7-standard-trixie
# - If imageName has a bare version tag (e.g. :18.1): appends -standard-trixie
#
# NOTE: This migration ONLY updates system CNPG Cluster resources (clusters.postgresql.cnpg.io),
# NOT user-created Postgres applications.
set -euo pipefail
TARGET_IMAGE="ghcr.io/cloudnative-pg/postgresql:17.7-standard-trixie"
# Static system database names
STATIC_DB_NAMES="keycloak-db grafana-db alerta-db seaweedfs-db"
echo "=== Updating PostgreSQL images for system databases ==="
echo "Target image: $TARGET_IMAGE"
# Fetch all CNPG clusters with their name, namespace, imageName, and Helm release annotation in one call
ALL_CLUSTERS=$(kubectl get clusters.postgresql.cnpg.io -A \
-o jsonpath='{range .items[*]}{.metadata.namespace}{"\t"}{.metadata.name}{"\t"}{.spec.imageName}{"\t"}{.metadata.annotations.meta\.helm\.sh/release-name}{"\n"}{end}' 2>/dev/null || true)
while IFS=$'\t' read -r NAMESPACE CLUSTER_NAME CURRENT_IMAGE HELM_RELEASE; do
[ -z "$NAMESPACE" ] && continue
# Check if cluster name matches one of the static system databases
MATCH=false
for db_name in $STATIC_DB_NAMES; do
if [ "$CLUSTER_NAME" = "$db_name" ]; then
MATCH=true
break
fi
done
# For harbor: match clusters ending with -db whose Helm release ends with -system
if [ "$MATCH" = false ] && [[ "$CLUSTER_NAME" == *-db ]] && [[ "$HELM_RELEASE" == *-system ]]; then
MATCH=true
fi
if [ "$MATCH" = false ]; then
continue
fi
PATCH_IMAGE=""
if [ -z "$CURRENT_IMAGE" ]; then
# imageName not set — pin to target
PATCH_IMAGE="$TARGET_IMAGE"
echo "PATCH $NAMESPACE/$CLUSTER_NAME: imageName not set, setting to $PATCH_IMAGE"
elif [[ "$CURRENT_IMAGE" =~ :17\. ]]; then
# Any PG 17 image — force to the pinned 17.7-standard-trixie
PATCH_IMAGE="$TARGET_IMAGE"
echo "PATCH $NAMESPACE/$CLUSTER_NAME: PG 17 detected, $CURRENT_IMAGE -> $PATCH_IMAGE"
elif [[ "$CURRENT_IMAGE" =~ :[0-9]+\.[0-9]+$ ]]; then
# Bare version tag for other majors (e.g. :18.1) — append -standard-trixie suffix
PATCH_IMAGE="${CURRENT_IMAGE}-standard-trixie"
echo "PATCH $NAMESPACE/$CLUSTER_NAME: bare tag detected, $CURRENT_IMAGE -> $PATCH_IMAGE"
else
echo "SKIP $NAMESPACE/$CLUSTER_NAME: imageName already set to $CURRENT_IMAGE"
continue
fi
kubectl patch clusters.postgresql.cnpg.io -n "$NAMESPACE" "$CLUSTER_NAME" \
--type=merge \
--patch "{\"spec\":{\"imageName\":\"${PATCH_IMAGE}\"}}"
done <<< "$ALL_CLUSTERS"
echo "=== PostgreSQL image update completed ==="
# Stamp version
kubectl create configmap -n cozy-system cozystack-version \
--from-literal=version=38 --dry-run=client -o yaml | kubectl apply -f-

View file

@ -66,6 +66,7 @@ spec:
path: system/cilium
valuesFiles:
- values.yaml
- values-apparmor.yaml
install:
privileged: true
namespace: cozy-cilium
@ -117,6 +118,7 @@ spec:
path: system/cilium
valuesFiles:
- values.yaml
- values-apparmor.yaml
- values-kubeovn.yaml
install:
privileged: true

View file

@ -13,6 +13,8 @@ apiVersion: cozystack.io/v1alpha1
kind: Package
metadata:
name: {{ $name }}
annotations:
helm.sh/resource-policy: keep
spec:
variant: {{ $variant }}
{{- if $components }}
@ -40,6 +42,8 @@ apiVersion: cozystack.io/v1alpha1
kind: Package
metadata:
name: {{ $name }}
annotations:
helm.sh/resource-policy: keep
spec:
variant: {{ $variant }}
{{- end }}

View file

@ -36,6 +36,9 @@ stringData:
branding:
{{- . | toYaml | nindent 8 }}
{{- end }}
cpu-allocation-ratio: {{ .Values.resources.cpuAllocationRatio | quote }}
memory-allocation-ratio: {{ .Values.resources.memoryAllocationRatio | quote }}
ephemeral-storage-allocation-ratio: {{ .Values.resources.ephemeralStorageAllocationRatio | quote }}
{{- with .Values.scheduling }}
scheduling:
{{- . | toYaml | nindent 8 }}

View file

@ -2,7 +2,11 @@
{{- fail "bundles.iaas.enabled can only be true when bundles.system.variant is 'isp-full' or 'isp-full-generic'" }}
{{- end }}
{{- if and .Values.bundles.iaas.enabled (or (eq .Values.bundles.system.variant "isp-full") (eq .Values.bundles.system.variant "isp-full-generic")) }}
{{include "cozystack.platform.package.default" (list "cozystack.kubevirt" $) }}
{{- $kubevirtComponents := dict -}}
{{- if .Values.resources.cpuAllocationRatio -}}
{{- $_ := set $kubevirtComponents "kubevirt" (dict "values" (dict "cpuAllocationRatio" (.Values.resources.cpuAllocationRatio | int))) -}}
{{- end -}}
{{include "cozystack.platform.package" (list "cozystack.kubevirt" "default" $ $kubevirtComponents) }}
{{include "cozystack.platform.package.default" (list "cozystack.kubevirt-cdi" $) }}
{{include "cozystack.platform.package.optional.default" (list "cozystack.gpu-operator" $) }}
{{include "cozystack.platform.package.default" (list "cozystack.kamaji" $) }}

View file

@ -5,8 +5,8 @@ sourceRef:
path: /
migrations:
enabled: false
image: ghcr.io/cozystack/cozystack/platform-migrations:v1.2.0@sha256:3a3d8cfa4323d8023b7c7800d8b552b250bc2de01ca7550fd024a10b12324f6d
targetVersion: 36
image: ghcr.io/cozystack/cozystack/platform-migrations:v1.2.4@sha256:72af209576eb42793dbf241c024d713fd23cc1435673dc2f9a81f70c237df124
targetVersion: 38
# Bundle deployment configuration
bundles:
system:

View file

@ -1,2 +1,2 @@
e2e:
image: ghcr.io/cozystack/cozystack/e2e-sandbox:v1.2.0@sha256:7964a3e4b11053887be201d62f94138c3a7836c861036c26fb833aa1fcb934e1
image: ghcr.io/cozystack/cozystack/e2e-sandbox:v1.2.4@sha256:0367a03b981df2a3ea13f411d4cb7869c2bf2c89c07d3d5c8971b9a28921ccef

View file

@ -1 +1 @@
ghcr.io/cozystack/cozystack/matchbox:v1.2.0@sha256:313f36db2e8b9dea0cd1a80d73f27a0694e996631d430bbd1b8391fbb9859020
ghcr.io/cozystack/cozystack/matchbox:v1.2.4@sha256:a7bfd50d2b18eaf644c5b84e95bb115b07479feed8fe2664ecf2810766dc322b

View file

@ -10,7 +10,7 @@ metadata:
app: bootbox
annotations:
{{- if eq $solver "http01" }}
acme.cert-manager.io/http01-ingress-class: {{ $ingress }}
acme.cert-manager.io/http01-ingress-ingressclassname: {{ $ingress }}
{{- end }}
cert-manager.io/cluster-issuer: {{ $clusterIssuer }}
{{- if .Values.whitelistHTTP }}

View file

@ -1 +1 @@
ghcr.io/cozystack/cozystack/objectstorage-sidecar:v1.2.0@sha256:b508ca451c6051b64e71cedd6209c80ac0e0bea8aa6bb47e838ec9d99bf4796a
ghcr.io/cozystack/cozystack/objectstorage-sidecar:v1.2.4@sha256:cc1f60ef2cb40ab61c4787e234f83e57a7b8aca0ffe7b17f4c4fa6e3de1669f4

View file

@ -243,7 +243,7 @@ spec:
nginx.ingress.kubernetes.io/proxy-body-size: "0"
nginx.ingress.kubernetes.io/backend-protocol: "HTTPS"
{{- if eq $solver "http01" }}
acme.cert-manager.io/http01-ingress-class: {{ $ingress }}
acme.cert-manager.io/http01-ingress-ingressclassname: {{ $ingress }}
{{- end }}
cert-manager.io/cluster-issuer: {{ $clusterIssuer }}
tls:

View file

@ -1,5 +1,5 @@
backupController:
image: "ghcr.io/cozystack/cozystack/backup-controller:v1.2.0@sha256:07bcd89bd725951801870d819f0fc536705069608f0fe5c20617607af6f8a61a"
image: "ghcr.io/cozystack/cozystack/backup-controller:v1.2.4@sha256:318bd6986190142b1a46de2df63975931d6669c138831c9da51a324439097689"
replicas: 2
debug: false
metrics:

View file

@ -1,5 +1,5 @@
backupStrategyController:
image: "ghcr.io/cozystack/cozystack/backupstrategy-controller:v1.2.0@sha256:0039ec58f531faf5a5ba3f70993c2c71d74dae4cc8e8e83bd21d2cc2adf10d8a"
image: "ghcr.io/cozystack/cozystack/backupstrategy-controller:v1.2.4@sha256:e620387a891d7834d77d09d7106f7ab7d4c0dc9f288cafb484b6bfe1a7c47f28"
replicas: 2
debug: false
metrics:

View file

@ -1 +1 @@
ghcr.io/cozystack/cozystack/s3manager:v0.5.0@sha256:73382d7911274e6528cc2272ca9bfdb095c59e78845a3cdffd39d6c8ac29d920
ghcr.io/cozystack/cozystack/s3manager:v0.5.0@sha256:be364c6ebc27b8dc51f41965ed56523a6a03bb66e95d25cc5b6aaaf4e203bde4

View file

@ -12,7 +12,7 @@ metadata:
nginx.ingress.kubernetes.io/proxy-read-timeout: "99999"
nginx.ingress.kubernetes.io/proxy-send-timeout: "99999"
{{- if eq $solver "http01" }}
acme.cert-manager.io/http01-ingress-class: {{ $ingress }}
acme.cert-manager.io/http01-ingress-ingressclassname: {{ $ingress }}
{{- end }}
cert-manager.io/cluster-issuer: {{ $clusterIssuer }}
spec:

View file

@ -1,6 +1,7 @@
{{- $solver := (index .Values._cluster "solver") | default "http01" }}
{{- $exposeIngress := (index .Values._cluster "expose-ingress") | default "tenant-root" }}
apiVersion: cert-manager.io/v1
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
@ -17,9 +18,9 @@ spec:
name: cloudflare-api-token-secret
key: api-token
{{- else }}
http01:
ingress:
class: nginx
http01:
ingress:
ingressClassName: {{ $exposeIngress }}
{{- end }}
---
@ -41,9 +42,9 @@ spec:
name: cloudflare-api-token-secret
key: api-token
{{- else }}
http01:
ingress:
class: nginx
http01:
ingress:
ingressClassName: {{ $exposeIngress }}
{{- end }}
---

View file

@ -12,6 +12,15 @@ update:
helm repo update cilium
helm pull cilium/cilium --untar --untardir charts --version 1.19
$(SED_INPLACE) -e '/Used in iptables/d' -e '/SYS_MODULE/d' charts/cilium/values.yaml
# Drop the whole k8s<1.30 AppArmor annotations block from the cilium-agent
# DaemonSet. Cozystack manages these annotations through cilium.podAnnotations
# in values.yaml on every k8s version, so keeping the upstream block would
# produce duplicate mapping keys on k8s<1.30.
perl -i -0pe 's|\n \{\{- if not \.Values\.securityContext\.privileged \}\}\n \{\{- if semverCompare "<1\.30\.0".*?\n \{\{- end \}\}\n \{\{- end \}\}\n \{\{- end \}\}||s' \
charts/cilium/templates/cilium-agent/daemonset.yaml
@! grep -q 'container\.apparmor\.security\.beta\.kubernetes\.io' \
charts/cilium/templates/cilium-agent/daemonset.yaml || \
{ echo 'ERROR: perl patch did not remove the upstream AppArmor block from cilium-agent/daemonset.yaml (upstream template format may have changed)' >&2; exit 1; }
version=$$(awk '$$1 == "version:" {print $$2}' charts/cilium/Chart.yaml) && \
$(SED_INPLACE) "s/ARG VERSION=.*/ARG VERSION=v$${version}/" images/cilium/Dockerfile

View file

@ -57,19 +57,6 @@ spec:
# ensure pods roll when configmap updates
cilium.io/cilium-configmap-checksum: {{ include (print $.Template.BasePath "/cilium-configmap.yaml") . | sha256sum | quote }}
{{- end }}
{{- if not .Values.securityContext.privileged }}
{{- if semverCompare "<1.30.0" (printf "%d.%d.0" (semver .Capabilities.KubeVersion.Version).Major (semver .Capabilities.KubeVersion.Version).Minor) }}
# Set app AppArmor's profile to "unconfined". The value of this annotation
# can be modified as long users know which profiles they have available
# in AppArmor.
container.apparmor.security.beta.kubernetes.io/cilium-agent: "unconfined"
container.apparmor.security.beta.kubernetes.io/clean-cilium-state: "unconfined"
{{- if .Values.cgroup.autoMount.enabled }}
container.apparmor.security.beta.kubernetes.io/mount-cgroup: "unconfined"
container.apparmor.security.beta.kubernetes.io/apply-sysctl-overwrites: "unconfined"
{{- end }}
{{- end }}
{{- end }}
{{- with .Values.podAnnotations }}
{{- toYaml . | nindent 8 }}
{{- end }}

View file

@ -0,0 +1,20 @@
cilium:
# Opt out of the cri-containerd.apparmor.d profile for containers that invoke
# nsenter to join the host cgroup/mount namespace. The kernel reports the
# denial as a blocked "ptrace" operation. Required on Ubuntu 22.04+ and
# other distros that load this AppArmor profile by default, where the
# denial otherwise puts cilium-agent into Init:CrashLoopBackOff.
# The deprecated annotations are used because k8s >= 1.30 pod-level
# appArmorProfile: Unconfined is not honoured by containerd CRI today
# (kubernetes/kubernetes#125069).
# Only applied on non-Talos cilium variants where cgroup.autoMount.enabled is
# true — on Talos mount-cgroup is not rendered and the kube-apiserver would
# reject an annotation referencing a non-existent container.
# mount-bpf-fs is intentionally excluded: it renders with its own
# securityContext.privileged=true, and the kernel does not apply AppArmor
# profiles to privileged containers.
podAnnotations:
container.apparmor.security.beta.kubernetes.io/cilium-agent: unconfined
container.apparmor.security.beta.kubernetes.io/clean-cilium-state: unconfined
container.apparmor.security.beta.kubernetes.io/mount-cgroup: unconfined
container.apparmor.security.beta.kubernetes.io/apply-sysctl-overwrites: unconfined

View file

@ -1,3 +1,3 @@
cozystackAPI:
image: ghcr.io/cozystack/cozystack/cozystack-api:v1.2.0@sha256:b40ba5c87bb625d646dd69075d21dcc1aca18775b3de8e63d04466d60d42cee5
image: ghcr.io/cozystack/cozystack/cozystack-api:v1.2.4@sha256:daf48b09d79df112953a43d6cfdb10a4440dd63779ecbfa62436805076dcf977
replicas: 2

View file

@ -1,4 +1,4 @@
cozystackController:
image: ghcr.io/cozystack/cozystack/cozystack-controller:v1.2.0@sha256:f56c8222411fed890331502939c8d4c5700489e697da39e8e46896cf674cce71
image: ghcr.io/cozystack/cozystack/cozystack-controller:v1.2.4@sha256:e2472fd4f12efd3a7763f76b276e5a5f465031fa8bb113df585ce4b8ebede717
debug: false
disableTelemetry: false

View file

@ -1,6 +1,6 @@
{{- $brandingConfig := .Values._cluster.branding | default dict }}
{{- $tenantText := "v1.2.0" }}
{{- $tenantText := "v1.2.4" }}
{{- $footerText := "Cozystack" }}
{{- $titleText := "Cozystack Dashboard" }}
{{- $logoText := "" }}

View file

@ -11,7 +11,7 @@ metadata:
annotations:
cert-manager.io/cluster-issuer: {{ $clusterIssuer }}
{{- if eq $solver "http01" }}
acme.cert-manager.io/http01-ingress-class: {{ $exposeIngress }}
acme.cert-manager.io/http01-ingress-ingressclassname: {{ $exposeIngress }}
{{- end }}
nginx.ingress.kubernetes.io/rewrite-target: /
nginx.ingress.kubernetes.io/client-max-body-size: 100m

View file

@ -1,6 +1,6 @@
openapiUI:
image: ghcr.io/cozystack/cozystack/openapi-ui:v1.2.0@sha256:bf68676a809be8b37ae3ab7416b2ae3185a4efa216b8b09358d916df081d0a3e
image: ghcr.io/cozystack/cozystack/openapi-ui:v1.2.4@sha256:ee273aaacc6fcb58c180cbdc58c047fd85068724c6478ea0cdfe7b8396877078
openapiUIK8sBff:
image: ghcr.io/cozystack/cozystack/openapi-ui-k8s-bff:v1.2.0@sha256:89b5c62df0ed9a1984023ba909765e4b60af3f9a61e42ebdd67437d71ebbb645
image: ghcr.io/cozystack/cozystack/openapi-ui-k8s-bff:v1.2.4@sha256:7dd82ebe1d0c1925bbc440bc65a86852f3dd2bc0f6f12856904df847f15913d3
tokenProxy:
image: ghcr.io/cozystack/cozystack/token-proxy:v1.2.0@sha256:2e280991e07853ea48f97b0a42946afffa10d03d6a83d41099ed83e6ffc94fdc
image: ghcr.io/cozystack/cozystack/token-proxy:v1.2.4@sha256:2e280991e07853ea48f97b0a42946afffa10d03d6a83d41099ed83e6ffc94fdc

View file

@ -1 +1 @@
ghcr.io/cozystack/cozystack/grafana-dashboards:v1.2.0@sha256:2c9aa0b48e2bf6167db198f4d15882bfe51700108edf2e9f6d0942940a2c1204
ghcr.io/cozystack/cozystack/grafana-dashboards:v1.2.4@sha256:2c9aa0b48e2bf6167db198f4d15882bfe51700108edf2e9f6d0942940a2c1204

View file

@ -5,6 +5,9 @@ metadata:
name: {{ .Values.harbor.fullnameOverride }}-db
spec:
instances: {{ .Values.db.replicas }}
{{- $existingCluster := lookup "postgresql.cnpg.io/v1" "Cluster" .Release.Namespace (printf "%s-db" .Values.harbor.fullnameOverride) }}
{{- $image := dig "spec" "imageName" "ghcr.io/cloudnative-pg/postgresql:17.7-standard-trixie" $existingCluster }}
imageName: {{ if regexMatch ":17\\." $image }}ghcr.io/cloudnative-pg/postgresql:17.7-standard-trixie{{ else if regexMatch ":[0-9]+\\.[0-9]+$" $image }}{{ printf "%s-standard-trixie" $image }}{{ else }}{{ $image }}{{ end }}
storage:
size: {{ .Values.db.size }}
{{- with .Values.db.storageClass }}

View file

@ -3,7 +3,7 @@ kamaji:
deploy: false
image:
pullPolicy: IfNotPresent
tag: v1.2.0@sha256:071f5f893f16918e6f03c3cf42315a3bce1c7624f10c155f3b3e6dfcd4d60c2a
tag: v1.2.4@sha256:441ead7178dd429a16cebe506a9df489a3cd38c8fb8255437f6927b2dd0a7a04
repository: ghcr.io/cozystack/cozystack/kamaji
resources:
limits:
@ -13,4 +13,4 @@ kamaji:
cpu: 100m
memory: 100Mi
extraArgs:
- --migrate-image=ghcr.io/cozystack/cozystack/kamaji:v1.2.0@sha256:071f5f893f16918e6f03c3cf42315a3bce1c7624f10c155f3b3e6dfcd4d60c2a
- --migrate-image=ghcr.io/cozystack/cozystack/kamaji:v1.2.4@sha256:441ead7178dd429a16cebe506a9df489a3cd38c8fb8255437f6927b2dd0a7a04

View file

@ -4,6 +4,9 @@ metadata:
name: keycloak-db
spec:
instances: 2
{{- $existingCluster := lookup "postgresql.cnpg.io/v1" "Cluster" .Release.Namespace "keycloak-db" }}
{{- $image := dig "spec" "imageName" "ghcr.io/cloudnative-pg/postgresql:17.7-standard-trixie" $existingCluster }}
imageName: {{ if regexMatch ":17\\." $image }}ghcr.io/cloudnative-pg/postgresql:17.7-standard-trixie{{ else if regexMatch ":[0-9]+\\.[0-9]+$" $image }}{{ printf "%s-standard-trixie" $image }}{{ else }}{{ $image }}{{ end }}
storage:
size: 20Gi
{{- if .Values._cluster.scheduling }}

View file

@ -11,7 +11,7 @@ metadata:
{{- with .Values.ingress.annotations }}
annotations:
{{- if eq $solver "http01" }}
acme.cert-manager.io/http01-ingress-class: {{ $exposeIngress }}
acme.cert-manager.io/http01-ingress-ingressclassname: {{ $exposeIngress }}
{{- end }}
cert-manager.io/cluster-issuer: {{ $clusterIssuer }}
{{- toYaml . | nindent 4 }}

View file

@ -1,4 +1,4 @@
portSecurity: true
routes: ""
image: ghcr.io/cozystack/cozystack/kubeovn-plunger:v1.2.0@sha256:39f9f0e6ec42afdd91082f7c025699425096244bb10f4e2f7a1d17155ac0cebb
image: ghcr.io/cozystack/cozystack/kubeovn-plunger:v1.2.4@sha256:174768084b30b070aaa411fd55f461439449a4e5fe0e31298986066c4fb2e104
ovnCentralName: ovn-central

View file

@ -1,3 +1,3 @@
portSecurity: true
routes: ""
image: ghcr.io/cozystack/cozystack/kubeovn-webhook:v1.2.0@sha256:e6334c29d3aaf0dea766c88e3e05b53ad623d1bb497b3c836e6f76adade45b29
image: ghcr.io/cozystack/cozystack/kubeovn-webhook:v1.2.4@sha256:e18f9fd679e38f65362a8d0042f25468272f6d081136ad47027168d8e7e07a4a

View file

@ -1,3 +1,3 @@
storageClass: replicated
csiDriver:
image: ghcr.io/cozystack/cozystack/kubevirt-csi-driver:0.0.0@sha256:5b209248acba3d8cf0d999edd8a915915a2e565a1c8ac1f5dde0dffec20fa02e
image: ghcr.io/cozystack/cozystack/kubevirt-csi-driver:0.0.0@sha256:0e70f4391b344c71a9567e1ed22ca24910116a619cb681381059b8c39f382219

View file

@ -1,5 +1,5 @@
lineageControllerWebhook:
image: ghcr.io/cozystack/cozystack/lineage-controller-webhook:v1.2.0@sha256:fdbd4ed09c2e825880e84b8668d8bb6e0795da5c51d7c20b9d5eb4abe31d3ae0
image: ghcr.io/cozystack/cozystack/lineage-controller-webhook:v1.2.4@sha256:b7bd8300a06c87ba4f5815e240e6f9843d80baaddd1f549004ddbfe9858e9310
debug: false
localK8sAPIEndpoint:
enabled: true

View file

@ -4,7 +4,7 @@ export NAMESPACE=cozy-$(NAME)
include ../../../hack/common-envs.mk
include ../../../hack/package.mk
LINSTOR_VERSION ?= 1.32.3
LINSTOR_VERSION ?= 1.33.2
LINSTOR_CSI_VERSION ?= v1.10.5
image: image-piraeus-server image-linstor-csi

View file

@ -61,6 +61,8 @@ RUN test -d .gradlehome && echo ".gradlehome found in tarball" || (echo ".gradle
# Build DEB packages from tarball
# Override GRADLE_FLAGS to remove --offline flag, allowing Gradle to download missing dependencies
RUN sed -i 's/GRADLE_FLAGS = --offline/GRADLE_FLAGS =/' debian/rules || true
# Skip dh_strip_nondeterminism to avoid failures on some JAR files (logback-core)
RUN printf '\noverride_dh_strip_nondeterminism:\n\ttrue\n' >> debian/rules
RUN LD_LIBRARY_PATH='' dpkg-buildpackage -rfakeroot -b -uc
# Copy built .deb packages to a location accessible from final image

View file

@ -1,14 +1,17 @@
# LINSTOR Server Patches
Custom patches for piraeus-server (linstor-server) v1.32.3.
Custom patches for piraeus-server (linstor-server) v1.33.2.
- **adjust-on-resfile-change.diff** — Use actual device path in res file during toggle-disk; fix LUKS data offset
- Upstream: [#473](https://github.com/LINBIT/linstor-server/pull/473), [#472](https://github.com/LINBIT/linstor-server/pull/472)
- **allow-toggle-disk-retry.diff** — Allow retry and cancellation of failed toggle-disk operations
- Upstream: [#475](https://github.com/LINBIT/linstor-server/pull/475)
- **force-metadata-check-on-disk-add.diff** — Create metadata during toggle-disk from diskless to diskful
- Upstream: [#474](https://github.com/LINBIT/linstor-server/pull/474)
- **fix-duplicate-tcp-ports.diff** — Prevent duplicate TCP ports after toggle-disk operations
- Upstream: [#476](https://github.com/LINBIT/linstor-server/pull/476)
- **skip-adjust-when-device-inaccessible.diff** — Fix resources stuck in StandAlone after reboot, Unknown state race condition, and encrypted resource deletion
- Upstream: [#477](https://github.com/LINBIT/linstor-server/pull/477)
- **allow-toggle-disk-retry.diff** — Backport maintainer implementation of toggle-disk retry/abort
- Source PR/comment: [#475](https://github.com/LINBIT/linstor-server/pull/475), [maintainer note](https://github.com/LINBIT/linstor-server/pull/475#issuecomment-3949630419)
- Backported from upstream commit: [`3d97f71c9`](https://github.com/LINBIT/linstor-server/commit/3d97f71c95a493588d3d521c63eac4d846935fb3)
- **fix-duplicate-tcp-ports.diff** — Preserve DRBD TCP ports during toggle-disk and avoid redundant `ensureStackDataExists()`
- Source PR/review: [#476](https://github.com/LINBIT/linstor-server/pull/476), [review suggestion](https://github.com/LINBIT/linstor-server/pull/476#discussion_r3007725079)
- Backported from commits: [`79d6375c5`](https://github.com/kvaps/linstor-server/commit/79d6375c55d6181b35a7b7f0fe8dbdfb86e126cd), [`bcc89902f`](https://github.com/kvaps/linstor-server/commit/bcc89902f4f61ac1589dd07ebb7f5aae1935370d)
- **fix-luks-header-size.diff** — Account for LUKS2 `--offset`, metadata/keyslots sizing, and device `optimal_io_size`
- Source PR/comment: [#472](https://github.com/LINBIT/linstor-server/pull/472), [maintainer note](https://github.com/LINBIT/linstor-server/pull/472#issuecomment-3949687603)
- Backported from commits: [`ccc85fbd2`](https://github.com/LINBIT/linstor-server/commit/ccc85fbd2c65f0b97c52403fa80f1efdb886ec4e), [`71b601554`](https://github.com/LINBIT/linstor-server/commit/71b601554f41bcb50cd5bd06989c5b0d3a814acd)
- Note: upstream commit [`3d0402a0c`](https://github.com/LINBIT/linstor-server/commit/3d0402a0c25f0a4b57b380321f10e89982f26e7a) is already included in `v1.33.1`
- **retry-adjust-after-stale-bitmap.diff** — Retry `drbdadm adjust` after detaching a stale local bitmap state
- Source PR: [#491](https://github.com/LINBIT/linstor-server/pull/491)
- Backported from commit: [`51ae50a84`](https://github.com/kvaps/linstor-server/commit/51ae50a84dcb98093f543b819652c750a94d96c9)

View file

@ -1,48 +0,0 @@
diff --git a/satellite/src/main/java/com/linbit/linstor/layer/drbd/utils/ConfFileBuilder.java b/satellite/src/main/java/com/linbit/linstor/layer/drbd/utils/ConfFileBuilder.java
index 36c52ccf8..c0bb7b967 100644
--- a/satellite/src/main/java/com/linbit/linstor/layer/drbd/utils/ConfFileBuilder.java
+++ b/satellite/src/main/java/com/linbit/linstor/layer/drbd/utils/ConfFileBuilder.java
@@ -894,12 +894,16 @@ public class ConfFileBuilder
if (((Volume) vlmData.getVolume()).getFlags().isUnset(localAccCtx, Volume.Flags.DELETE))
{
final String disk;
+ // Check if we're in toggle-disk operation (adding disk to diskless resource)
+ boolean isDiskAdding = vlmData.getVolume().getAbsResource().getStateFlags().isSomeSet(
+ localAccCtx, Resource.Flags.DISK_ADD_REQUESTED, Resource.Flags.DISK_ADDING);
if ((!isPeerRsc && vlmData.getDataDevice() == null) ||
(isPeerRsc &&
// FIXME: vlmData.getRscLayerObject().getFlags should be used here
vlmData.getVolume().getAbsResource().disklessForDrbdPeers(accCtx)
) ||
- (!isPeerRsc &&
+ // For toggle-disk: if dataDevice is set and we're adding disk, use the actual device
+ (!isPeerRsc && !isDiskAdding &&
// FIXME: vlmData.getRscLayerObject().getFlags should be used here
vlmData.getVolume().getAbsResource().isDrbdDiskless(accCtx)
)
diff --git a/satellite/src/main/java/com/linbit/linstor/layer/luks/CryptSetupCommands.java b/satellite/src/main/java/com/linbit/linstor/layer/luks/CryptSetupCommands.java
index 54dd5c19f..018de58cf 100644
--- a/satellite/src/main/java/com/linbit/linstor/layer/luks/CryptSetupCommands.java
+++ b/satellite/src/main/java/com/linbit/linstor/layer/luks/CryptSetupCommands.java
@@ -34,6 +34,9 @@ public class CryptSetupCommands implements Luks
private static final Version V2_1_0 = new Version(2, 1, 0);
private static final Version V2_0_0 = new Version(2, 0, 0);
private static final String PBDKF_MAX_MEMORY_KIB = "262144"; // 256 MiB
+ // Fixed LUKS2 data offset in 512-byte sectors (16 MiB = 32768 sectors)
+ // This ensures consistent LUKS header size across all nodes regardless of system defaults
+ private static final String LUKS2_DATA_OFFSET_SECTORS = "32768";
@SuppressWarnings("unused")
private final ErrorReporter errorReporter;
@@ -78,6 +81,11 @@ public class CryptSetupCommands implements Luks
command.add(CRYPTSETUP);
command.add("-q");
command.add("luksFormat");
+ // Always specify explicit offset to ensure consistent LUKS header size across all nodes
+ // Without this, different systems may create LUKS with different header sizes (16MiB vs 32MiB)
+ // which causes "Low.dev. smaller than requested DRBD-dev. size" errors during toggle-disk
+ command.add("--offset");
+ command.add(LUKS2_DATA_OFFSET_SECTORS);
if (version.greaterOrEqual(V2_0_0))
{
command.add("--pbkdf-memory");

View file

@ -1,168 +1,232 @@
diff --git a/controller/src/main/java/com/linbit/linstor/core/apicallhandler/controller/CtrlRscToggleDiskApiCallHandler.java b/controller/src/main/java/com/linbit/linstor/core/apicallhandler/controller/CtrlRscToggleDiskApiCallHandler.java
index d93a18014..cc8ce4f04 100644
index d93a18014..a944cb809 100644
--- a/controller/src/main/java/com/linbit/linstor/core/apicallhandler/controller/CtrlRscToggleDiskApiCallHandler.java
+++ b/controller/src/main/java/com/linbit/linstor/core/apicallhandler/controller/CtrlRscToggleDiskApiCallHandler.java
@@ -57,7 +57,9 @@ import com.linbit.linstor.stateflags.StateFlags;
import com.linbit.linstor.storage.StorageException;
import com.linbit.linstor.storage.data.adapter.drbd.DrbdRscData;
import com.linbit.linstor.storage.interfaces.categories.resource.AbsRscLayerObject;
+import com.linbit.linstor.storage.interfaces.categories.resource.VlmProviderObject;
import com.linbit.linstor.storage.kinds.DeviceLayerKind;
+import com.linbit.linstor.storage.kinds.DeviceProviderKind;
import com.linbit.linstor.storage.utils.LayerUtils;
import com.linbit.linstor.tasks.AutoDiskfulTask;
import com.linbit.linstor.utils.layer.LayerRscUtils;
@@ -387,21 +389,84 @@ public class CtrlRscToggleDiskApiCallHandler implements CtrlSatelliteConnectionL
@@ -111,6 +111,14 @@ import reactor.util.function.Tuple2;
@Singleton
public class CtrlRscToggleDiskApiCallHandler implements CtrlSatelliteConnectionListener
{
+ private enum ToggleDiskAction
+ {
+ NOOP,
+ NORMAL,
+ ABORT,
+ RETRY
+ }
+
private final AccessContext apiCtx;
private final ScopeRunner scopeRunner;
private final BackgroundRunner backgroundRunner;
@@ -386,69 +394,33 @@ public class CtrlRscToggleDiskApiCallHandler implements CtrlSatelliteConnectionL
"Toggle Disk on %s/%s %s", nodeNameStr, rscNameStr, removeDisk ? "removing disk" : "adding disk");
Resource rsc = ctrlApiDataLoader.loadRsc(nodeName, rscName, true);
+ ResourceDefinition rscDfn = rsc.getResourceDefinition();
+ // Allow retry of the same operation if the previous attempt failed
+ // (the requested flag remains set for retry on reconnection, but we should also allow manual retry)
+ // Also allow cancellation of a failed operation by requesting the opposite operation
if (hasDiskAddRequested(rsc))
{
- if (hasDiskAddRequested(rsc))
- {
- throw new ApiRcException(ApiCallRcImpl.simpleEntry(
- ApiConsts.FAIL_RSC_BUSY,
- "Addition of disk to resource already requested",
- true
- ));
+ if (removeDisk)
+ {
+ // User wants to cancel the failed add-disk operation and go back to diskless
+ // Use the existing disk removal flow to properly cleanup storage on satellite
+ errorReporter.logInfo(
+ "Toggle Disk cancel on %s/%s - cancelling failed DISK_ADD_REQUESTED, reverting to diskless",
+ nodeNameStr, rscNameStr);
+ unmarkDiskAddRequested(rsc);
+ // Also clear DISK_ADDING if it was set
+ unmarkDiskAdding(rsc);
+
+ // Set storage pool to diskless pool (overwrite the diskful pool that was set)
+ Props rscProps = ctrlPropsHelper.getProps(rsc);
+ rscProps.map().put(ApiConsts.KEY_STOR_POOL_NAME, LinStor.DISKLESS_STOR_POOL_NAME);
+
+ // Set DISK_REMOVE_REQUESTED to use the existing disk removal flow
+ // This will:
+ // 1. updateAndAdjustDisk sets DISK_REMOVING flag
+ // 2. Satellite sees DISK_REMOVING and deletes LUKS/storage devices
+ // 3. finishOperation rebuilds layer stack as diskless
+ // We keep the existing layer data so satellite can properly cleanup
+ markDiskRemoveRequested(rsc);
+
+ ctrlTransactionHelper.commit();
+
+ // Use existing disk removal flow - this will properly cleanup storage on satellite
+ return Flux
+ .<ApiCallRc>just(ApiCallRcImpl.singleApiCallRc(
+ ApiConsts.MODIFIED,
+ "Cancelling disk addition, reverting to diskless"
+ ))
+ .concatWith(updateAndAdjustDisk(nodeName, rscName, true, toggleIntoTiebreakerRef, context))
+ .concatWith(ctrlRscDfnApiCallHandler.get().updateProps(rsc.getResourceDefinition()));
+ }
+ // If adding disk and DISK_ADD_REQUESTED is already set, treat as retry
+ // Simply retry the operation with existing layer data - satellite will handle it idempotently
+ // NOTE: We don't remove/recreate layer data here because removeLayerData() only deletes
+ // from controller DB without calling drbdadm down on satellite, leaving orphaned DRBD devices
+ errorReporter.logInfo(
+ "Toggle Disk retry on %s/%s - DISK_ADD_REQUESTED already set, retrying operation",
+ nodeNameStr, rscNameStr);
+ ctrlTransactionHelper.commit();
+ return Flux
+ .<ApiCallRc>just(new ApiCallRcImpl())
+ .concatWith(updateAndAdjustDisk(nodeName, rscName, false, toggleIntoTiebreakerRef, context))
+ .concatWith(ctrlRscDfnApiCallHandler.get().updateProps(rsc.getResourceDefinition()));
}
if (hasDiskRemoveRequested(rsc))
{
- }
- if (hasDiskRemoveRequested(rsc))
- {
- throw new ApiRcException(ApiCallRcImpl.simpleEntry(
- ApiConsts.FAIL_RSC_BUSY,
- "Removal of disk from resource already requested",
- true
- ));
+ if (!removeDisk)
+ {
+ // User wants to cancel the failed remove-disk operation
- }
+ ToggleDiskAction action = determineToggleDiskAction(rsc, removeDisk);
+ errorReporter.logDebug("Toggle Disk action: %s", action);
- if (!removeDisk && !ctrlVlmCrtApiHelper.isDiskless(rsc))
+ switch (action)
{
- throw new ApiRcException(ApiCallRcImpl.simpleEntry(
- ApiConsts.WARN_RSC_ALREADY_HAS_DISK,
- "Resource already has disk",
- true
- ));
- }
- if (removeDisk && ctrlVlmCrtApiHelper.isDiskless(rsc))
- {
- throw new ApiRcException(ApiCallRcImpl.simpleEntry(
- ApiConsts.WARN_RSC_ALREADY_DISKLESS,
- "Resource already diskless",
- true
- ));
+ case NOOP:
+ return handleNoopAction(rsc, removeDisk);
+ case RETRY:
+ return handleRetryAction(rsc, removeDisk, toggleIntoTiebreakerRef, context);
+ case ABORT:
+ clearToggleDiskFlags(rsc);
+ errorReporter.logInfo(
+ "Toggle Disk cancel on %s/%s - cancelling failed DISK_REMOVE_REQUESTED",
+ nodeNameStr, rscNameStr);
+ unmarkDiskRemoveRequested(rsc);
+ ctrlTransactionHelper.commit();
+ return Flux.<ApiCallRc>just(
+ ApiCallRcImpl.singleApiCallRc(
+ ApiConsts.MODIFIED,
+ "Cancelled disk removal request"
+ )
+ "Aborting previous toggle disk transition, starting new transition to %s",
+ removeDisk ? "diskless" : "diskful"
+ );
+ }
+ // If removing disk and DISK_REMOVE_REQUESTED is already set, treat as retry
+ errorReporter.logInfo(
+ "Toggle Disk retry on %s/%s - DISK_REMOVE_REQUESTED already set, continuing operation",
+ nodeNameStr, rscNameStr);
+ ctrlTransactionHelper.commit();
+ return Flux
+ .<ApiCallRc>just(new ApiCallRcImpl())
+ .concatWith(updateAndAdjustDisk(nodeName, rscName, true, toggleIntoTiebreakerRef, context))
+ .concatWith(ctrlRscDfnApiCallHandler.get().updateProps(rsc.getResourceDefinition()));
+ break;
+ case NORMAL:
+ break;
+ default:
+ throw new ImplementationError("Unhandled case: " + action);
}
if (!removeDisk && !ctrlVlmCrtApiHelper.isDiskless(rsc))
@@ -412,17 +477,43 @@ public class CtrlRscToggleDiskApiCallHandler implements CtrlSatelliteConnectionL
true
));
}
+ ResourceDefinition rscDfn = rsc.getResourceDefinition();
+ AccessContext peerCtx = peerAccCtx.get();
+
if (removeDisk && ctrlVlmCrtApiHelper.isDiskless(rsc))
{
+ // Resource is marked as diskless - check if it has orphaned storage layers that need cleanup
+ AbsRscLayerObject<Resource> layerData = getLayerData(peerCtx, rsc);
+ if (layerData != null && (LayerUtils.hasLayer(layerData, DeviceLayerKind.LUKS) ||
+ hasNonDisklessStorageLayer(layerData)))
+ {
+ // Resource is marked as diskless but has orphaned storage layers - need cleanup
+ // Use the existing disk removal flow to properly cleanup storage on satellite
+ errorReporter.logInfo(
+ "Toggle Disk cleanup on %s/%s - resource is diskless but has orphaned storage layers, cleaning up",
+ nodeNameStr, rscNameStr);
+
+ // Set DISK_REMOVE_REQUESTED to use the existing disk removal flow
+ // This will trigger proper satellite cleanup via DISK_REMOVING flag
+ markDiskRemoveRequested(rsc);
+
+ ctrlTransactionHelper.commit();
+
+ // Use existing disk removal flow - this will properly cleanup storage on satellite
+ return Flux
+ .<ApiCallRc>just(ApiCallRcImpl.singleApiCallRc(
+ ApiConsts.MODIFIED,
+ "Cleaning up orphaned storage layers"
+ ))
+ .concatWith(updateAndAdjustDisk(nodeName, rscName, true, toggleIntoTiebreakerRef, context))
+ .concatWith(ctrlRscDfnApiCallHandler.get().updateProps(rsc.getResourceDefinition()));
+ }
throw new ApiRcException(ApiCallRcImpl.simpleEntry(
ApiConsts.WARN_RSC_ALREADY_DISKLESS,
"Resource already diskless",
true
));
}
-
- ResourceDefinition rscDfn = rsc.getResourceDefinition();
- AccessContext peerCtx = peerAccCtx.get();
if (removeDisk)
{
// Prevent removal of the last disk
@@ -1446,6 +1537,30 @@ public class CtrlRscToggleDiskApiCallHandler implements CtrlSatelliteConnectionL
- if (removeDisk)
- {
- // Prevent removal of the last disk
- int haveDiskCount = countDisksAndIsOnline(rscDfn);
- if (haveDiskCount <= 1)
- {
- throw new ApiRcException(ApiCallRcImpl.simpleEntry(
- ApiConsts.FAIL_INSUFFICIENT_REPLICA_COUNT,
- "Cannot remove the disk from the only online resource with a disk",
- true
- ));
- }
+ validateToggleDiskPreconditions(rsc, removeDisk);
- if (!LayerUtils.hasLayer(getLayerData(peerCtx, rsc), DeviceLayerKind.DRBD))
- {
- throw new ApiRcException(ApiCallRcImpl.simpleEntry(
- ApiConsts.FAIL_INVLD_LAYER_STACK,
- "Toggle disk is only supported in combination with DRBD",
- true
- ));
- }
- }
- else
- {
- ensureAllPeersHavePeerSlotLeft(rscDfn);
- }
+ AccessContext peerCtx = peerAccCtx.get();
// Save the requested storage pool in the resource properties.
// This does not cause the storage pool to be used automatically.
@@ -628,10 +600,10 @@ public class CtrlRscToggleDiskApiCallHandler implements CtrlSatelliteConnectionL
ctrlTransactionHelper.commit();
- String action = removeDisk ? "Removal of disk from" : "Addition of disk to";
+ String actionStr = removeDisk ? "Removal of disk from" : "Addition of disk to";
responses.addEntry(ApiCallRcImpl.simpleEntry(
ApiConsts.MODIFIED,
- action + " resource '" + rscDfn.getName().displayValue + "' " +
+ actionStr + " resource '" + rscDfn.getName().displayValue + "' " +
"on node '" + rsc.getNode().getName().displayValue + "' registered"
));
@@ -651,6 +623,40 @@ public class CtrlRscToggleDiskApiCallHandler implements CtrlSatelliteConnectionL
);
}
+ private Flux<ApiCallRc> handleNoopAction(Resource rscRef, boolean removeDiskRef)
+ {
+ String state = removeDiskRef ? "diskless" : "diskful";
+ ApiCallRcImpl responses = new ApiCallRcImpl();
+ responses.addEntry(ApiCallRcImpl.simpleEntry(
+ ApiConsts.INFO_NOOP,
+ "Resource '" + rscRef.getResourceDefinition().getName().displayValue + "' on node '" +
+ rscRef.getNode().getName().displayValue + "' is already " + state
+ ));
+ return Flux.just(responses);
+ }
+
+ private Flux<ApiCallRc> handleRetryAction(
+ Resource rscRef,
+ boolean removeDisk,
+ boolean toggleIntoTiebreakerRef,
+ ResponseContext context
+ )
+ {
+ String direction = removeDisk ? "diskless" : "diskful";
+ ApiCallRcImpl responses = new ApiCallRcImpl();
+ NodeName nodeName = rscRef.getNode().getName();
+ ResourceName rscName = rscRef.getResourceDefinition().getName();
+ responses.addEntry(ApiCallRcImpl.simpleEntry(
+ ApiConsts.INFO_NOOP,
+ "Retrying toggle disk to " + direction + " for resource '" + rscName.displayValue +
+ "' on node '" + nodeName.displayValue + "'"
+ ));
+
+ return Flux
+ .<ApiCallRc>just(responses)
+ .concatWith(updateAndAdjustDisk(nodeName, rscName, removeDisk, toggleIntoTiebreakerRef, context));
+ }
+
private long getVlmDfnSizePrivileged(VolumeDefinition vlmDfnRef)
{
try
@@ -781,6 +787,96 @@ public class CtrlRscToggleDiskApiCallHandler implements CtrlSatelliteConnectionL
}
}
+ private void unmarkDiskAddRequested(Resource rsc)
+ private ToggleDiskAction determineToggleDiskAction(Resource rsc, boolean removeDisk)
+ {
+ boolean isDiskless = ctrlVlmCrtApiHelper.isDiskless(rsc);
+ boolean diskAddRequested = hasDiskAddRequested(rsc);
+ boolean diskRemoveRequested = hasDiskRemoveRequested(rsc);
+
+ ToggleDiskAction action;
+ if (isDiskless)
+ {
+ if (diskAddRequested)
+ {
+ action = removeDisk ? ToggleDiskAction.ABORT : ToggleDiskAction.RETRY;
+ }
+ else if (diskRemoveRequested)
+ {
+ action = removeDisk ? ToggleDiskAction.RETRY : ToggleDiskAction.ABORT;
+ }
+ else
+ {
+ action = removeDisk ? ToggleDiskAction.NOOP : ToggleDiskAction.NORMAL;
+ }
+ }
+ else
+ {
+ if (diskRemoveRequested)
+ {
+ action = removeDisk ? ToggleDiskAction.RETRY : ToggleDiskAction.ABORT;
+ }
+ else
+ {
+ action = removeDisk ? ToggleDiskAction.NORMAL : ToggleDiskAction.NOOP;
+ }
+ }
+
+ return action;
+ }
+
+ private void validateToggleDiskPreconditions(Resource rsc, boolean removeDisk)
+ {
+ ResourceDefinition rscDfn = rsc.getResourceDefinition();
+ if (removeDisk)
+ {
+ validateDiskRemovalAllowed(rsc, rscDfn);
+ }
+ else
+ {
+ ensureAllPeersHavePeerSlotLeft(rscDfn);
+ }
+ }
+
+ private void clearToggleDiskFlags(Resource rsc)
+ {
+ try
+ {
+ rsc.getStateFlags().disableFlags(apiCtx, Resource.Flags.DISK_ADD_REQUESTED);
+ rsc.getStateFlags().disableFlags(
+ apiCtx,
+ Resource.Flags.DISK_ADD_REQUESTED,
+ Resource.Flags.DISK_ADDING,
+ Resource.Flags.DISK_REMOVE_REQUESTED,
+ Resource.Flags.DISK_REMOVING
+ );
+ }
+ catch (AccessDeniedException | DatabaseException exc)
+ {
@ -170,60 +234,28 @@ index d93a18014..cc8ce4f04 100644
+ }
+ }
+
+ private void unmarkDiskRemoveRequested(Resource rsc)
+ private void validateDiskRemovalAllowed(Resource rsc, ResourceDefinition rscDfn)
+ {
+ try
+ int haveDiskCount = countDisksAndIsOnline(rscDfn);
+ if (haveDiskCount <= 1)
+ {
+ rsc.getStateFlags().disableFlags(apiCtx, Resource.Flags.DISK_REMOVE_REQUESTED);
+ throw new ApiRcException(ApiCallRcImpl.simpleEntry(
+ ApiConsts.FAIL_INSUFFICIENT_REPLICA_COUNT,
+ "Cannot remove the disk from the only online resource with a disk",
+ true
+ ));
+ }
+ catch (AccessDeniedException | DatabaseException exc)
+
+ if (!LayerUtils.hasLayer(getLayerData(peerAccCtx.get(), rsc), DeviceLayerKind.DRBD))
+ {
+ throw new ImplementationError(exc);
+ throw new ApiRcException(ApiCallRcImpl.simpleEntry(
+ ApiConsts.FAIL_INVLD_LAYER_STACK,
+ "Toggle disk is only supported in combination with DRBD",
+ true
+ ));
+ }
+ }
+
private void markDiskAdded(Resource rscData)
{
try
@@ -1511,6 +1626,41 @@ public class CtrlRscToggleDiskApiCallHandler implements CtrlSatelliteConnectionL
return layerData;
}
+ /**
+ * Check if the layer stack has a non-diskless STORAGE layer.
+ * This is used to detect orphaned storage layers that need cleanup.
+ */
+ private boolean hasNonDisklessStorageLayer(AbsRscLayerObject<Resource> layerDataRef)
+ {
+ boolean hasNonDiskless = false;
+ if (layerDataRef != null)
+ {
+ if (layerDataRef.getLayerKind() == DeviceLayerKind.STORAGE)
+ {
+ for (VlmProviderObject<Resource> vlmData : layerDataRef.getVlmLayerObjects().values())
+ {
+ if (vlmData.getProviderKind() != DeviceProviderKind.DISKLESS)
+ {
+ hasNonDiskless = true;
+ break;
+ }
+ }
+ }
+ if (!hasNonDiskless)
+ {
+ for (AbsRscLayerObject<Resource> child : layerDataRef.getChildren())
+ {
+ if (hasNonDisklessStorageLayer(child))
+ {
+ hasNonDiskless = true;
+ break;
+ }
+ }
+ }
+ }
+ return hasNonDiskless;
+ }
+
private LockGuard createLockGuard()
{
return lockGuardFactory.buildDeferred(LockType.WRITE, LockObj.NODES_MAP, LockObj.RSC_DFN_MAP);
/**
* Although we need to rebuild the layerData as the layerList might have changed, if we do not
* deactivate (i.e. down) the current resource, we need to make sure that deleting DrbdRscData

View file

@ -1,87 +1,137 @@
From 1250abe99d64a0501795e37d3b6af62410002239 Mon Sep 17 00:00:00 2001
From: Andrei Kvapil <kvapss@gmail.com>
Date: Mon, 12 Jan 2026 13:44:46 +0100
Subject: [PATCH] fix(drbd): prevent duplicate TCP ports after toggle-disk
operations
Remove redundant ensureStackDataExists() call with empty payload from
resetStoragePools() method that was causing TCP port conflicts after
toggle-disk operations.
Root Cause:
-----------
The resetStoragePools() method, introduced in 2019 (commit 95cc17d0b8),
calls ensureStackDataExists() with an empty LayerPayload. This worked
correctly when TCP ports were stored at RscDfn level.
After the TCP port migration to per-node level (commit f754943463, May
2025), this empty payload results in DrbdRscData being created without
TCP ports assigned. The controller then sends a Pojo with an empty port
Set to satellites.
On satellites, when DrbdRscData is initialized with an empty port list,
initPorts() uses preferredNewPortsRef from peer resources. Since
SatelliteDynamicNumberPool.tryAllocate() always returns true (no-op),
any port from preferredNewPortsRef is accepted without conflict checking,
leading to duplicate TCP port assignments.
Impact:
-------
This regression affects toggle-disk operations, particularly:
- Snapshot creation/restore operations
- Manual toggle-disk operations
- Any operation calling resetStoragePools()
Symptoms include:
- DRBD resources failing to adjust with "port is also used" errors
- Resources stuck in StandAlone or Connecting states
- Multiple resources on the same node using identical TCP ports
Solution:
---------
Remove the ensureStackDataExists() call from resetStoragePools() as it
is redundant. The calling code (e.g., CtrlRscToggleDiskApiCallHandler
line 1071) already invokes ensureStackDataExists() with the correct
payload immediately after resetStoragePools().
This fix ensures:
1. resetStoragePools() only resets storage pool assignments
2. Layer data creation with proper TCP ports happens via the caller's
ensureStackDataExists() with correct payload
3. No DrbdRscData objects are created without TCP port assignments
Related Issues:
---------------
Fixes #454 - Duplicate TCP ports after backup/restore operations
Related to user reports of resources stuck in StandAlone after node
reboots when toggle-disk or backup operations were in progress.
Testing:
--------
Verified that:
- Toggle-disk operations no longer create resources without TCP ports
- Backup/restore operations complete without TCP port conflicts
- Resources maintain unique TCP ports across toggle-disk cycles
Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
---
.../linbit/linstor/layer/resource/CtrlRscLayerDataFactory.java | 2 --
1 file changed, 2 deletions(-)
diff --git a/controller/src/main/java/com/linbit/linstor/core/apicallhandler/controller/CtrlRscToggleDiskApiCallHandler.java b/controller/src/main/java/com/linbit/linstor/core/apicallhandler/controller/CtrlRscToggleDiskApiCallHandler.java
index d93a18014..01cfbbacf 100644
--- a/controller/src/main/java/com/linbit/linstor/core/apicallhandler/controller/CtrlRscToggleDiskApiCallHandler.java
+++ b/controller/src/main/java/com/linbit/linstor/core/apicallhandler/controller/CtrlRscToggleDiskApiCallHandler.java
@@ -37,6 +37,7 @@ import com.linbit.linstor.core.objects.StorPool;
import com.linbit.linstor.core.objects.Volume;
import com.linbit.linstor.core.objects.VolumeDefinition;
import com.linbit.linstor.core.objects.utils.MixedStorPoolHelper;
+import com.linbit.linstor.core.types.TcpPortNumber;
import com.linbit.linstor.dbdrivers.DatabaseException;
import com.linbit.linstor.event.EventWaiter;
import com.linbit.linstor.event.ObjectIdentifier;
@@ -85,6 +86,7 @@ import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
+import java.util.TreeSet;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
@@ -575,12 +577,13 @@ public class CtrlRscToggleDiskApiCallHandler implements CtrlSatelliteConnectionL
/*
* We also have to remove the currently diskless DrbdRscData and free up the node-id as now we must
- * use the shared resource's node-id
+ * use the shared resource's node-id. We still need to preserve TCP ports though.
*/
+ copyDrbdTcpPortsIfExists(rsc, payload);
}
else
{
- copyDrbdNodeIdIfExists(rsc, payload);
+ copyDrbdSettings(rsc, payload);
}
/*
* rebuilds the layerdata in case we just removed it..
@@ -782,10 +785,20 @@ public class CtrlRscToggleDiskApiCallHandler implements CtrlSatelliteConnectionL
}
/**
- * Although we need to rebuild the layerData as the layerList might have changed, if we do not
- * deactivate (i.e. down) the current resource, we need to make sure that deleting DrbdRscData
- * and recreating a new DrbdRscData ends up with the same node-id as before.
+ * Copies DRBD settings (node-id and TCP ports) from the existing DrbdRscData into the payload
+ * before removeLayerData() deletes them. This ensures that recreated DrbdRscData ends up with
+ * the same node-id and TCP ports as before.
+ *
+ * TCP ports must be preserved because if the satellite misses the update (e.g. due to controller
+ * restart or connectivity issues), it will keep the old ports while peers receive the new ones,
+ * causing DRBD connections to fail with StandAlone state.
*/
+ private void copyDrbdSettings(Resource rsc, LayerPayload payload) throws ImplementationError
+ {
+ copyDrbdNodeIdIfExists(rsc, payload);
+ copyDrbdTcpPortsIfExists(rsc, payload);
+ }
+
private void copyDrbdNodeIdIfExists(Resource rsc, LayerPayload payload) throws ImplementationError
{
Set<AbsRscLayerObject<Resource>> drbdRscDataSet = LayerRscUtils.getRscDataByLayer(
@@ -804,6 +817,28 @@ public class CtrlRscToggleDiskApiCallHandler implements CtrlSatelliteConnectionL
}
}
+ private void copyDrbdTcpPortsIfExists(Resource rsc, LayerPayload payload) throws ImplementationError
+ {
+ Set<AbsRscLayerObject<Resource>> drbdRscDataSet = LayerRscUtils.getRscDataByLayer(
+ getLayerData(apiCtx, rsc),
+ DeviceLayerKind.DRBD
+ );
+ if (!drbdRscDataSet.isEmpty())
+ {
+ DrbdRscData<Resource> drbdRscData = (DrbdRscData<Resource>) drbdRscDataSet.iterator().next();
+ Collection<TcpPortNumber> tcpPorts = drbdRscData.getTcpPortList();
+ if (tcpPorts != null && !tcpPorts.isEmpty())
+ {
+ Set<Integer> portInts = new TreeSet<>();
+ for (TcpPortNumber port : tcpPorts)
+ {
+ portInts.add(port.value);
+ }
+ payload.drbdRsc.tcpPorts = portInts;
+ }
+ }
+ }
+
private List<DeviceLayerKind> removeLayerData(Resource rscRef)
{
List<DeviceLayerKind> layerList;
@@ -1058,15 +1093,15 @@ public class CtrlRscToggleDiskApiCallHandler implements CtrlSatelliteConnectionL
/*
* We also have to remove the possible meta-children of previous StorageRscData.
* LayerData will be recreated with ensureStackDataExists.
- * However, we still need to remember our node-id if we had / have DRBD in the list
+ * However, we still need to remember our DRBD settings if we had / have DRBD in the list
*/
- copyDrbdNodeIdIfExists(rsc, payload);
+ copyDrbdSettings(rsc, payload);
layerList = removeLayerData(rsc);
}
else
{
markDiskAdded(rsc);
- ctrlLayerStackHelper.resetStoragePools(rsc);
+ ctrlLayerStackHelper.resetStoragePools(rsc, false);
}
ctrlLayerStackHelper.ensureStackDataExists(rsc, layerList, payload);
diff --git a/controller/src/main/java/com/linbit/linstor/layer/resource/CtrlRscLayerDataFactory.java b/controller/src/main/java/com/linbit/linstor/layer/resource/CtrlRscLayerDataFactory.java
index 3538b380c..4f589145e 100644
index 3538b380c..f9733b6f1 100644
--- a/controller/src/main/java/com/linbit/linstor/layer/resource/CtrlRscLayerDataFactory.java
+++ b/controller/src/main/java/com/linbit/linstor/layer/resource/CtrlRscLayerDataFactory.java
@@ -276,8 +276,6 @@ public class CtrlRscLayerDataFactory
@@ -263,6 +263,11 @@ public class CtrlRscLayerDataFactory
}
public void resetStoragePools(Resource rscRef)
+ {
+ resetStoragePools(rscRef, true);
+ }
+
+ public void resetStoragePools(Resource rscRef, boolean callEnsureStackDataExistsRef)
{
try
{
@@ -276,8 +281,10 @@ public class CtrlRscLayerDataFactory
rscDataToProcess.addAll(rscData.getChildren());
}
-
- ensureStackDataExists(rscRef, null, new LayerPayload());
+ if (callEnsureStackDataExistsRef)
+ {
+ ensureStackDataExists(rscRef, null, new LayerPayload());
+ }
}
catch (AccessDeniedException exc)
{
--
2.39.5 (Apple Git-154)

View file

@ -0,0 +1,570 @@
diff --git a/satellite/src/main/java/com/linbit/linstor/layer/storage/AbsStorageProvider.java b/satellite/src/main/java/com/linbit/linstor/layer/storage/AbsStorageProvider.java
--- a/satellite/src/main/java/com/linbit/linstor/layer/storage/AbsStorageProvider.java
+++ b/satellite/src/main/java/com/linbit/linstor/layer/storage/AbsStorageProvider.java
@@ -2081,7 +2081,6 @@ public abstract class AbsStorageProvider<
)
throws IOException, AccessDeniedException
{
- final StorPoolName storPoolObjName = storPoolObj.getName();
if (storDevicePath != null)
{
errorReporter.logDebug("updateMinIoSize: Have storDevicePath \"%s\"", storDevicePath);
@@ -2094,35 +2093,18 @@ public abstract class AbsStorageProvider<
"updateMinIoSize: Block device path is \"%s\"",
blockDevicePath.toString()
);
- final long minIoSize = BlockSizeInfo.getBlockSize(blockDevicePath);
-
- boolean updateValue = true;
-
- final String propKey = StorageConstants.NAMESPACE_INTERNAL + '/' +
- StorageConstants.BLK_DEV_MIN_IO_SIZE;
- final Props storPoolProps = storPoolObj.getProps(storDriverAccCtx);
- final @Nullable String currentPropValue = storPoolProps.getProp(propKey);
- if (currentPropValue != null)
- {
- try
- {
- final long currentPropMinIoSize = Long.parseLong(currentPropValue);
- updateValue = currentPropMinIoSize != minIoSize;
- }
- catch (NumberFormatException ignored)
- {
- }
- }
-
- if (updateValue)
- {
- final String propValue = Long.toString(minIoSize);
- errorReporter.logDebug(
- "Storage pool \"%s\": Set property \"%s\" = \"%s\"",
- storPoolObjName.displayValue, propKey, propValue
- );
- propsChange.changeStorPoolProp(storPoolObj, propKey, propValue);
- }
+ updatePropIfNeeded(
+ propsChange,
+ storPoolObj,
+ StorageConstants.NAMESPACE_INTERNAL + '/' + StorageConstants.BLK_DEV_MIN_IO_SIZE,
+ Long.toString(BlockSizeInfo.getBlockSize(blockDevicePath))
+ );
+ updatePropIfNeeded(
+ propsChange,
+ storPoolObj,
+ StorageConstants.NAMESPACE_INTERNAL + '/' + StorageConstants.BLK_DEV_OPT_IO_SIZE,
+ Long.toString(BlockSizeInfo.getOptimalIoSize(blockDevicePath))
+ );
}
else
{
@@ -2130,6 +2112,28 @@ public abstract class AbsStorageProvider<
}
}
+ private void updatePropIfNeeded(
+ LocalPropsChangePojo propsChangeRef,
+ StorPool storPoolObjRef,
+ String propKeyRef,
+ String propValueRef
+ )
+ throws AccessDeniedException
+ {
+ final Props storPoolProps = storPoolObjRef.getProps(storDriverAccCtx);
+ final @Nullable String currentPropValue = storPoolProps.getProp(propKeyRef);
+ if (currentPropValue == null || !currentPropValue.equals(propValueRef))
+ {
+ errorReporter.logDebug(
+ "Storage pool \"%s\": Set property \"%s\" = \"%s\"",
+ storPoolObjRef.getName().displayValue,
+ propKeyRef,
+ propValueRef
+ );
+ propsChangeRef.changeStorPoolProp(storPoolObjRef, propKeyRef, propValueRef);
+ }
+ }
+
@SuppressWarnings("unchecked")
@Override
public void updateAllocatedSize(VlmProviderObject<Resource> vlmDataRef)
diff --git a/satellite/src/main/java/com/linbit/linstor/layer/storage/utils/BlockSizeInfo.java b/satellite/src/main/java/com/linbit/linstor/layer/storage/utils/BlockSizeInfo.java
--- a/satellite/src/main/java/com/linbit/linstor/layer/storage/utils/BlockSizeInfo.java
+++ b/satellite/src/main/java/com/linbit/linstor/layer/storage/utils/BlockSizeInfo.java
@@ -3,8 +3,11 @@ package com.linbit.linstor.layer.storage.utils;
import com.linbit.utils.MathUtils;
import com.linbit.utils.SymbolicLinkResolver;
+import static com.linbit.linstor.layer.storage.BlockSizeConsts.DFLT_OPT_IO_SIZE;
import static com.linbit.linstor.layer.storage.BlockSizeConsts.DFLT_IO_SIZE;
+import static com.linbit.linstor.layer.storage.BlockSizeConsts.MAX_OPT_IO_SIZE;
import static com.linbit.linstor.layer.storage.BlockSizeConsts.MAX_IO_SIZE;
+import static com.linbit.linstor.layer.storage.BlockSizeConsts.MIN_OPT_IO_SIZE;
import static com.linbit.linstor.layer.storage.BlockSizeConsts.MIN_IO_SIZE;
import java.io.FileInputStream;
@@ -13,6 +16,8 @@ import java.nio.file.Path;
public class BlockSizeInfo
{
+ private static final int NUMBER_BUFFER_SIZE = 32;
+
/**
* Determines the blocksize, aka minimum I/O size, for the specified backing storage path.
*
@@ -51,7 +56,7 @@ public class BlockSizeInfo
final Path infoSourceName = blockDevice.getFileName();
final Path infoSource = Path.of("/sys/block", infoSourceName.toString(), "queue/physical_block_size");
- final byte[] data = new byte[32];
+ final byte[] data = new byte[NUMBER_BUFFER_SIZE];
try (final FileInputStream fileIn = new FileInputStream(infoSource.toString()))
{
final int readCount = fileIn.read(data);
@@ -75,4 +80,38 @@ public class BlockSizeInfo
}
return blockSize;
}
+
+ public static long getOptimalIoSize(final Path storageObj)
+ {
+ long optIoSize = DFLT_OPT_IO_SIZE;
+ try
+ {
+ final Path blockDevice = SymbolicLinkResolver.resolveSymLink(storageObj);
+ final Path infoSourceName = blockDevice.getFileName();
+ final Path infoSource = Path.of("/sys/block", infoSourceName.toString(), "queue/optimal_io_size");
+
+ final byte[] data = new byte[NUMBER_BUFFER_SIZE];
+ try (final FileInputStream fileIn = new FileInputStream(infoSource.toString()))
+ {
+ final int readCount = fileIn.read(data);
+ if (readCount > 0)
+ {
+ String numberStr = new String(data, 0, readCount);
+ numberStr = numberStr.trim();
+ try
+ {
+ final long unboundedOptIoSize = Long.parseLong(numberStr);
+ optIoSize = MathUtils.bounds(MIN_OPT_IO_SIZE, unboundedOptIoSize, MAX_OPT_IO_SIZE);
+ }
+ catch (NumberFormatException ignored)
+ {
+ }
+ }
+ }
+ }
+ catch (IOException ignored)
+ {
+ }
+ return optIoSize;
+ }
}
diff --git a/server/src/main/java/com/linbit/SizeConv.java b/server/src/main/java/com/linbit/SizeConv.java
--- a/server/src/main/java/com/linbit/SizeConv.java
+++ b/server/src/main/java/com/linbit/SizeConv.java
@@ -16,6 +16,7 @@ public class SizeConv
public enum SizeUnit
{
UNIT_B,
+ UNIT_SECTORS,
UNIT_KiB,
UNIT_MiB,
UNIT_GiB,
@@ -41,6 +42,9 @@ public class SizeConv
case UNIT_B:
factor = FACTOR_B;
break;
+ case UNIT_SECTORS:
+ factor = FACTOR_SECTORS;
+ break;
case UNIT_KiB:
factor = FACTOR_KiB;
break;
@@ -111,6 +115,9 @@ public class SizeConv
case "b":
unit = SizeUnit.UNIT_B;
break;
+ case "s":
+ unit = SizeUnit.UNIT_SECTORS;
+ break;
case "k":
// fall-through
case "kb":
@@ -217,6 +224,9 @@ public class SizeConv
// Factor 1
public static final BigInteger FACTOR_B = BigInteger.valueOf(1L);
+ // Factor 512
+ public static final BigInteger FACTOR_SECTORS = BigInteger.valueOf(512L);
+
// Factor 1,024
// Naming convention exception: SI unit capitalization rules
@SuppressWarnings("checkstyle:constantname")
diff --git a/server/src/main/java/com/linbit/linstor/layer/luks/LuksLayerSizeCalculator.java b/server/src/main/java/com/linbit/linstor/layer/luks/LuksLayerSizeCalculator.java
--- a/server/src/main/java/com/linbit/linstor/layer/luks/LuksLayerSizeCalculator.java
+++ b/server/src/main/java/com/linbit/linstor/layer/luks/LuksLayerSizeCalculator.java
@@ -1,27 +1,62 @@
package com.linbit.linstor.layer.luks;
+import com.linbit.SizeConv;
+import com.linbit.SizeConv.SizeUnit;
import com.linbit.exceptions.InvalidSizeException;
+import com.linbit.linstor.PriorityProps;
+import com.linbit.linstor.annotation.Nullable;
+import com.linbit.linstor.api.ApiConsts;
+import com.linbit.linstor.core.objects.AbsResource;
+import com.linbit.linstor.core.objects.AbsVolume;
+import com.linbit.linstor.core.objects.Resource;
+import com.linbit.linstor.core.objects.ResourceDefinition;
+import com.linbit.linstor.core.objects.ResourceGroup;
+import com.linbit.linstor.core.objects.Snapshot;
+import com.linbit.linstor.core.objects.SnapshotVolume;
+import com.linbit.linstor.core.objects.StorPool;
+import com.linbit.linstor.core.objects.Volume;
+import com.linbit.linstor.core.objects.VolumeDefinition;
import com.linbit.linstor.dbdrivers.DatabaseException;
import com.linbit.linstor.layer.AbsLayerSizeCalculator;
+import com.linbit.linstor.netcom.Peer;
+import com.linbit.linstor.propscon.InvalidKeyException;
+import com.linbit.linstor.propscon.ReadOnlyProps;
import com.linbit.linstor.security.AccessDeniedException;
+import com.linbit.linstor.storage.StorageConstants;
import com.linbit.linstor.storage.data.adapter.luks.LuksVlmData;
import com.linbit.linstor.storage.interfaces.categories.resource.VlmProviderObject;
import com.linbit.linstor.storage.kinds.DeviceLayerKind;
import com.linbit.linstor.storage.kinds.ExtTools;
import com.linbit.linstor.storage.kinds.ExtToolsInfo;
import com.linbit.linstor.storage.kinds.ExtToolsInfo.Version;
+import com.linbit.linstor.utils.layer.LayerVlmUtils;
+import com.linbit.utils.ShellUtils;
+import com.linbit.utils.SignedAlign;
import javax.inject.Inject;
import javax.inject.Singleton;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
@Singleton
public class LuksLayerSizeCalculator extends AbsLayerSizeCalculator<LuksVlmData<?>>
{
+ public static final String LUKS2_OPT_METADATA_SIZE = "--luks2-metadata-size";
+ public static final String LUKS2_OPT_KEYSLOTS_SIZE = "--luks2-keyslots-size";
+ public static final String LUKS2_OPT_OFFSET = "--offset";
+ private static final String LUKS2_OPT_ALIGN_PAYLOAD = "--align-payload";
+ private static final long DFLT_LUKS2_METADATA_SIZE_IN_BYTES = 16L << 10;
+ private static final long DFLT_ALIGNMENT_1MIB_IN_BYTES = 1L << 20;
+ private static final long LUKS1_HEADER_SIZE_IN_KIB = 2L << 10;
+ private static final long LUKS2_HEADER_SIZE_IN_KIB = 16L << 10;
+ private static final long LUKS2_HEADER_SIZE_IN_BYTES = LUKS2_HEADER_SIZE_IN_KIB << 10;
- // linstor calculates in KiB
- private static final int MIB = 1024;
- private static final int LUKS1_HEADER_SIZE = 2 * MIB;
- private static final int LUKS2_HEADER_SIZE = 16 * MIB;
+ private static final Pattern PATTERN_SIZE = Pattern.compile("(\\d+)([kKmMgGtT]i?[bB]?|[sS]|)");
@Inject
public LuksLayerSizeCalculator(AbsLayerSizeCalculatorInit initRef)
@@ -74,30 +109,266 @@ public class LuksLayerSizeCalculator extends AbsLayerSizeCalculator<LuksVlmData<
}
private long getLuksHeaderSize(VlmProviderObject<?> vlmDataRef)
- throws AccessDeniedException
+ throws AccessDeniedException, InvalidSizeException
{
- ExtToolsInfo cryptSetupInfo = vlmDataRef.getRscLayerObject()
+ @Nullable Peer peer = vlmDataRef.getRscLayerObject()
.getAbsResource()
.getNode()
- .getPeer(sysCtx)
- .getExtToolsManager()
+ .getPeer(sysCtx);
+ if (peer == null)
+ {
+ throw new InvalidSizeException(
+ "Could not calculate size of LUKS volume, since cryptsetup's version could not be determined",
+ null
+ );
+ }
+ ExtToolsInfo cryptSetupInfo = peer.getExtToolsManager()
.getExtToolInfo(ExtTools.CRYPT_SETUP);
- long luksHeaderSize;
+
+ final long luksHeaderSize;
if (cryptSetupInfo != null && cryptSetupInfo.isSupported())
{
if (cryptSetupInfo.hasVersionOrHigher(new Version(2, 1)))
{
- luksHeaderSize = LUKS2_HEADER_SIZE;
+ luksHeaderSize = calcLuks2HeaderSize(vlmDataRef);
}
else
{
- luksHeaderSize = LUKS1_HEADER_SIZE;
+ luksHeaderSize = LUKS1_HEADER_SIZE_IN_KIB;
}
}
else
{
- luksHeaderSize = -1;
+ throw new InvalidSizeException(
+ "Could not calculate size of LUKS volume, since cryptsetup's version could not be determined",
+ null
+ );
}
return luksHeaderSize;
}
+
+ private long calcLuks2HeaderSize(VlmProviderObject<?> vlmDataRef) throws AccessDeniedException
+ {
+ PriorityProps prioProps = getPrioProps(vlmDataRef);
+
+ final @Nullable String userOptProp = prioProps.getProp(
+ ApiConsts.KEY_STOR_DRIVER_LUKS_FORMAT_OPTIONS,
+ ApiConsts.NAMESPC_STORAGE_DRIVER
+ );
+ List<String> userOptions = userOptProp != null ?
+ ShellUtils.shellSplit(userOptProp) :
+ new LinkedList<>();
+
+ final long alignedHeaderSizeInBytes;
+ final long unalignedHeaderSizeInBytes;
+ @Nullable Long cryptsetupOffsetInBytes = getLongOptionValue(
+ userOptions,
+ LUKS2_OPT_OFFSET,
+ SizeUnit.UNIT_SECTORS
+ );
+ if (cryptsetupOffsetInBytes != null)
+ {
+ alignedHeaderSizeInBytes = cryptsetupOffsetInBytes;
+ }
+ else
+ {
+ @Nullable Long cryptsetupLuks2KeyslotsSize = getLongOptionValue(
+ userOptions,
+ LUKS2_OPT_KEYSLOTS_SIZE
+ );
+ if (cryptsetupLuks2KeyslotsSize == null)
+ {
+ unalignedHeaderSizeInBytes = LUKS2_HEADER_SIZE_IN_BYTES;
+ }
+ else
+ {
+ @Nullable Long cryptsetupLuks2MetadataSize = getLongOptionValue(
+ userOptions,
+ LUKS2_OPT_METADATA_SIZE
+ );
+ cryptsetupLuks2MetadataSize = cryptsetupLuks2MetadataSize == null ?
+ DFLT_LUKS2_METADATA_SIZE_IN_BYTES :
+ cryptsetupLuks2MetadataSize;
+
+ unalignedHeaderSizeInBytes = 2 * cryptsetupLuks2MetadataSize + cryptsetupLuks2KeyslotsSize;
+ }
+
+ long alignment = getAlignment(vlmDataRef, userOptions);
+ alignedHeaderSizeInBytes = new SignedAlign(alignment).ceiling(unalignedHeaderSizeInBytes);
+ }
+ return SizeConv.convert(
+ alignedHeaderSizeInBytes,
+ SizeUnit.UNIT_B,
+ SizeUnit.UNIT_KiB
+ );
+ }
+
+ private long getAlignment(VlmProviderObject<?> vlmDataRef, List<String> userOptions) throws AccessDeniedException
+ {
+ @Nullable Long cryptsetupAlignPayloadInBytes = getLongOptionValue(
+ userOptions,
+ LUKS2_OPT_ALIGN_PAYLOAD,
+ SizeUnit.UNIT_SECTORS
+ );
+
+ long alignment = DFLT_ALIGNMENT_1MIB_IN_BYTES;
+ if (cryptsetupAlignPayloadInBytes != null)
+ {
+ alignment = cryptsetupAlignPayloadInBytes;
+ }
+ else
+ {
+ final long maxOptIoSize = getMaxOptIoSize(vlmDataRef);
+ alignment = Math.max(alignment, maxOptIoSize);
+ }
+ return alignment;
+ }
+
+ private long getMaxOptIoSize(VlmProviderObject<?> vlmDataRef) throws InvalidKeyException, AccessDeniedException
+ {
+ long ret = 0;
+ Set<StorPool> storPoolSet = LayerVlmUtils.getStorPoolSet(vlmDataRef, sysCtx);
+ for (StorPool sp : storPoolSet)
+ {
+ @Nullable String strValue = sp.getProps(sysCtx)
+ .getProp(
+ StorageConstants.BLK_DEV_OPT_IO_SIZE,
+ StorageConstants.NAMESPACE_INTERNAL
+ );
+ if (strValue != null)
+ {
+ try
+ {
+ long parsed = Long.parseLong(strValue);
+ ret = Math.max(parsed, ret);
+ }
+ catch (NumberFormatException ignored)
+ {
+ errorReporter.logWarning(
+ "LuksHeaderSize: Failed to parse '%s' from prop %s. Defaulting to 0 opt_io_size " +
+ "(no recommendation/hint)",
+ strValue,
+ StorageConstants.NAMESPACE_INTERNAL + "/" + StorageConstants.BLK_DEV_OPT_IO_SIZE
+ );
+ }
+ }
+ }
+ return ret;
+ }
+
+ private @Nullable Long getLongOptionValue(List<String> userOptionsRef, String optRef)
+ {
+ return getLongOptionValue(userOptionsRef, optRef, SizeUnit.UNIT_B);
+ }
+
+ @SuppressWarnings("checkstyle:magicnumber")
+ private @Nullable Long getLongOptionValue(List<String> userOptionsRef, String optRef, SizeUnit dfltSizeUnit)
+ {
+ @Nullable Long ret = null;
+ @Nullable String val = findLastValue(userOptionsRef, optRef);
+ if (val != null && !val.isBlank())
+ {
+ Matcher matcher = PATTERN_SIZE.matcher(val);
+ if (matcher.matches())
+ {
+ try
+ {
+ ret = Long.parseLong(matcher.group(1));
+ String unit = matcher.group(2);
+ SizeUnit sizeUnit;
+ if (unit.isBlank())
+ {
+ sizeUnit = dfltSizeUnit;
+ }
+ else
+ {
+ boolean forcePowerOfTwo = unit.length() == 1 || unit.length() == 3;
+ sizeUnit = SizeUnit.parse(unit, forcePowerOfTwo);
+ }
+
+ ret = SizeConv.convert(ret, sizeUnit, SizeUnit.UNIT_B);
+ }
+ catch (NumberFormatException ignored)
+ {
+ errorReporter.logWarning(
+ "LuksHeaderSize: Failed to parse '%s' from option '%s %s'.",
+ matcher.group(1),
+ optRef,
+ val
+ );
+ }
+ }
+ }
+ return ret;
+ }
+
+ private @Nullable String findLastValue(List<String> userOptionsRef, String optRef)
+ {
+ Iterator<String> it = userOptionsRef.iterator();
+ @Nullable String val = null;
+ while (it.hasNext())
+ {
+ String opt = it.next();
+ if (opt.equals(optRef))
+ {
+ if (it.hasNext())
+ {
+ val = it.next();
+ }
+ else
+ {
+ val = null;
+ }
+ }
+ else if (opt.startsWith(optRef + "="))
+ {
+ val = opt.substring(optRef.length() + 1);
+ if (val.isBlank())
+ {
+ val = null;
+ }
+ }
+ }
+ return val;
+ }
+
+ private PriorityProps getPrioProps(VlmProviderObject<?> vlmDataRef) throws AccessDeniedException
+ {
+ final AbsVolume<?> vlm = vlmDataRef.getVolume();
+ final AbsResource<?> rsc = vlm.getAbsResource();
+ final ResourceDefinition rscDfn = vlm.getResourceDefinition();
+ final VolumeDefinition vlmDfn = vlm.getVolumeDefinition();
+ final ResourceGroup rscGrp = rscDfn.getResourceGroup();
+
+ final ReadOnlyProps vlmProps;
+ final ReadOnlyProps rscProps;
+ if (vlm instanceof Volume)
+ {
+ vlmProps = ((Volume) vlm).getProps(sysCtx);
+ rscProps = ((Resource) rsc).getProps(sysCtx);
+ }
+ else
+ {
+ vlmProps = ((SnapshotVolume) vlm).getVlmProps(sysCtx);
+ rscProps = ((Snapshot) rsc).getRscProps(sysCtx);
+ }
+
+ final PriorityProps prioProps = new PriorityProps(
+ vlmProps,
+ rscProps
+ );
+ for (StorPool storPool : LayerVlmUtils.getStorPoolSet(vlmDataRef, sysCtx))
+ {
+ prioProps.addProps(storPool.getProps(sysCtx));
+ }
+ prioProps.addProps(
+ rsc.getNode().getProps(sysCtx),
+ vlmDfn.getProps(sysCtx),
+ rscDfn.getProps(sysCtx),
+ rscGrp.getVolumeGroupProps(sysCtx, vlmDfn.getVolumeNumber()),
+ rscGrp.getProps(sysCtx),
+ stltProps
+ );
+ return prioProps;
+ }
}
diff --git a/server/src/main/java/com/linbit/linstor/layer/storage/BlockSizeConsts.java b/server/src/main/java/com/linbit/linstor/layer/storage/BlockSizeConsts.java
--- a/server/src/main/java/com/linbit/linstor/layer/storage/BlockSizeConsts.java
+++ b/server/src/main/java/com/linbit/linstor/layer/storage/BlockSizeConsts.java
@@ -13,4 +13,9 @@ public class BlockSizeConsts
// Default value for the minimum_io_size value of non-storage layers
public static final long DFLT_SPECIAL_IO_SIZE = (1L << 12);
+
+ // optimal_io_size may be 0 to indicate no recommendation.
+ public static final long MIN_OPT_IO_SIZE = 0;
+ public static final long DFLT_OPT_IO_SIZE = 0;
+ public static final long MAX_OPT_IO_SIZE = Long.MAX_VALUE;
}
diff --git a/server/src/main/java/com/linbit/linstor/storage/StorageConstants.java b/server/src/main/java/com/linbit/linstor/storage/StorageConstants.java
--- a/server/src/main/java/com/linbit/linstor/storage/StorageConstants.java
+++ b/server/src/main/java/com/linbit/linstor/storage/StorageConstants.java
@@ -11,6 +11,7 @@ public class StorageConstants
public static final String NAMESPACE_NVME = ApiConsts.NAMESPC_STORAGE_DRIVER + "/NVME";
public static final String NAMESPACE_INTERNAL = NAMESPACE_STOR_DRIVER + "/internal/";
+ public static final String BLK_DEV_OPT_IO_SIZE = "optIoSize";
public static final String BLK_DEV_MIN_IO_SIZE = "minIoSize";
public static final String BLK_DEV_MIN_IO_SIZE_AUTO = "minIoSizeAuto";
public static final String BLK_DEV_MAX_BIO_SIZE = "maxBioSize";

View file

@ -1,63 +0,0 @@
diff --git a/satellite/src/main/java/com/linbit/linstor/layer/drbd/DrbdLayer.java b/satellite/src/main/java/com/linbit/linstor/layer/drbd/DrbdLayer.java
index a302ee835..01967a31f 100644
--- a/satellite/src/main/java/com/linbit/linstor/layer/drbd/DrbdLayer.java
+++ b/satellite/src/main/java/com/linbit/linstor/layer/drbd/DrbdLayer.java
@@ -371,10 +371,13 @@ public class DrbdLayer implements DeviceLayer
boolean isDiskless = drbdRscData.getAbsResource().isDrbdDiskless(workerCtx);
StateFlags<Flags> rscFlags = drbdRscData.getAbsResource().getStateFlags();
boolean isDiskRemoving = rscFlags.isSet(workerCtx, Resource.Flags.DISK_REMOVING);
+ // Check if we're in toggle-disk operation (adding disk to diskless resource)
+ boolean isDiskAdding = rscFlags.isSomeSet(workerCtx, Resource.Flags.DISK_ADD_REQUESTED, Resource.Flags.DISK_ADDING);
boolean contProcess = isDiskless;
- boolean processChildren = !isDiskless || isDiskRemoving;
+ // Process children when: has disk, removing disk, OR adding disk (toggle-disk)
+ boolean processChildren = !isDiskless || isDiskRemoving || isDiskAdding;
// do not process children when ONLY DRBD_DELETE flag is set (DELETE flag is still unset)
processChildren &= (!rscFlags.isSet(workerCtx, Resource.Flags.DRBD_DELETE) ||
rscFlags.isSet(workerCtx, Resource.Flags.DELETE));
@@ -570,7 +573,11 @@ public class DrbdLayer implements DeviceLayer
{
// hasMetaData needs to be run after child-resource processed
List<DrbdVlmData<Resource>> createMetaData = new ArrayList<>();
- if (!drbdRscData.getAbsResource().isDrbdDiskless(workerCtx) && !skipDisk)
+ // Check if we're in toggle-disk operation (adding disk to diskless resource)
+ boolean isDiskAddingForMd = drbdRscData.getAbsResource().getStateFlags()
+ .isSomeSet(workerCtx, Resource.Flags.DISK_ADD_REQUESTED, Resource.Flags.DISK_ADDING);
+ // Create metadata when: has disk OR adding disk (toggle-disk), and skipDisk is disabled
+ if ((!drbdRscData.getAbsResource().isDrbdDiskless(workerCtx) || isDiskAddingForMd) && !skipDisk)
{
// do not try to create meta data while the resource is diskless or skipDisk is enabled
for (DrbdVlmData<Resource> drbdVlmData : checkMetaData)
@@ -988,8 +995,10 @@ public class DrbdLayer implements DeviceLayer
{
List<DrbdVlmData<Resource>> checkMetaData = new ArrayList<>();
Resource rsc = drbdRscData.getAbsResource();
+ // Include DISK_ADD_REQUESTED/DISK_ADDING for toggle-disk scenario where we need to check/create metadata
if (!rsc.isDrbdDiskless(workerCtx) ||
- rsc.getStateFlags().isSet(workerCtx, Resource.Flags.DISK_REMOVING)
+ rsc.getStateFlags().isSet(workerCtx, Resource.Flags.DISK_REMOVING) ||
+ rsc.getStateFlags().isSomeSet(workerCtx, Resource.Flags.DISK_ADD_REQUESTED, Resource.Flags.DISK_ADDING)
)
{
// using a dedicated list to prevent concurrentModificationException
@@ -1177,9 +1186,16 @@ public class DrbdLayer implements DeviceLayer
boolean hasMetaData;
+ // Check if we need to verify/create metadata
+ // Force metadata check when:
+ // 1. checkMetaData is enabled
+ // 2. volume doesn't have disk yet (diskless -> diskful transition)
+ // 3. DISK_ADD_REQUESTED/DISK_ADDING flag is set (retry scenario where storage exists but no metadata)
+ boolean isDiskAddingState = drbdVlmData.getRscLayerObject().getAbsResource().getStateFlags()
+ .isSomeSet(workerCtx, Resource.Flags.DISK_ADD_REQUESTED, Resource.Flags.DISK_ADDING);
if (drbdVlmData.checkMetaData() ||
- // when adding a disk, DRBD believes that it is diskless but we still need to create metadata
- !drbdVlmData.hasDisk())
+ !drbdVlmData.hasDisk() ||
+ isDiskAddingState)
{
if (mdUtils.hasMetaData())
{

View file

@ -0,0 +1,141 @@
diff --git a/satellite/src/main/java/com/linbit/linstor/layer/drbd/utils/DrbdAdm.java b/satellite/src/main/java/com/linbit/linstor/layer/drbd/utils/DrbdAdm.java
index 5627d1be8..ece191292 100644
--- a/satellite/src/main/java/com/linbit/linstor/layer/drbd/utils/DrbdAdm.java
+++ b/satellite/src/main/java/com/linbit/linstor/layer/drbd/utils/DrbdAdm.java
@@ -42,6 +42,9 @@ import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.TimeUnit;
+import java.nio.charset.StandardCharsets;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
import java.util.stream.Collectors;
@Singleton
@@ -56,6 +58,9 @@ public class DrbdAdm
public static final int WAIT_CONNECT_RES_TIME = 10;
private static final long DOWN_WAIT_TIMEOUT_SEC = 5;
+ private static final long FORCE_DETACH_RETRY_WAIT_MS = 250;
+ private static final String BITMAP_LEAK_ERR_MSG = "already has a bitmap, this should not happen";
+ private static final Pattern BITMAP_LEAK_MINOR_PATTERN = Pattern.compile("\\bminor\\s+(\\d+)\\b");
private final ExtCmdFactory extCmdFactory;
private final AccessContext sysCtx;
@@ -131,8 +136,38 @@ public class DrbdAdm
// command.add(resName);
command.add(drbdRscData.getSuffixedResourceName());
// execute(Arrays.asList("drbdsetup", "show", drbdRscData.getSuffixedResourceName()));
- execute(command);
- // execute(Arrays.asList("drbdsetup", "show", drbdRscData.getSuffixedResourceName()));
+ String[] commandArr = command.toArray(new String[0]);
+ try
+ {
+ File nullDevice = new File(Platform.nullDevice());
+ ExtCmd extCmd = extCmdFactory.create();
+ if (Platform.isWindows())
+ {
+ extCmd.setTimeout(TimeoutType.WAIT, 5 * 60 * 1000);
+ }
+
+ OutputData outputData = extCmd.pipeExec(ProcessBuilder.Redirect.from(nullDevice), commandArr);
+ if (
+ outputData.exitCode != 0 &&
+ isBitmapLeakOnAttach(outputData) &&
+ cleanupStaleBitmapAndRetry(extCmd, nullDevice, outputData)
+ )
+ {
+ outputData = extCmd.pipeExec(ProcessBuilder.Redirect.from(nullDevice), commandArr);
+ }
+ if (outputData.exitCode != 0)
+ {
+ throw new ExtCmdFailedException(commandArr, outputData);
+ }
+ }
+ catch (ChildProcessTimeoutException timeoutExc)
+ {
+ throw new ExtCmdFailedException(commandArr, timeoutExc);
+ }
+ catch (IOException ioExc)
+ {
+ throw new ExtCmdFailedException(commandArr, ioExc);
+ }
drbdRscData.setAdjustRequired(false);
}
@@ -805,6 +840,75 @@ public class DrbdAdm
}
}
+ static boolean isBitmapLeakOnAttach(OutputData outputData)
+ {
+ return extractBitmapLeakMinor(outputData) != null;
+ }
+
+ static @Nullable Integer extractBitmapLeakMinor(OutputData outputData)
+ {
+ String stderr = new String(outputData.stderrData, StandardCharsets.UTF_8);
+ if (!stderr.contains(BITMAP_LEAK_ERR_MSG))
+ {
+ return null;
+ }
+
+ Matcher matcher = BITMAP_LEAK_MINOR_PATTERN.matcher(stderr);
+ if (!matcher.find())
+ {
+ return null;
+ }
+
+ return Integer.parseInt(matcher.group(1));
+ }
+
+ private boolean cleanupStaleBitmapAndRetry(
+ ExtCmd extCmd,
+ File nullDevice,
+ OutputData outputData
+ )
+ throws IOException, ChildProcessTimeoutException, ExtCmdFailedException
+ {
+ @Nullable Integer minor = extractBitmapLeakMinor(outputData);
+ if (minor == null)
+ {
+ return false;
+ }
+
+ OutputData detachOut = extCmd.pipeExec(
+ ProcessBuilder.Redirect.from(nullDevice),
+ DRBDSETUP_UTIL,
+ "detach",
+ Integer.toString(minor)
+ );
+ if (detachOut.exitCode == 0)
+ {
+ return true;
+ }
+
+ OutputData forceDetachOut = extCmd.pipeExec(
+ ProcessBuilder.Redirect.from(nullDevice),
+ DRBDSETUP_UTIL,
+ "detach",
+ Integer.toString(minor),
+ "--force"
+ );
+ if (forceDetachOut.exitCode != 0)
+ {
+ throw new ExtCmdFailedException(forceDetachOut.executedCommand, forceDetachOut);
+ }
+
+ try
+ {
+ Thread.sleep(FORCE_DETACH_RETRY_WAIT_MS);
+ }
+ catch (InterruptedException ignored)
+ {
+ Thread.currentThread().interrupt();
+ }
+ return true;
+ }
+
public static class DrbdPrimary implements AutoCloseable
{
private final DrbdAdm drbdAdm;

View file

@ -1,155 +0,0 @@
diff --git a/satellite/src/main/java/com/linbit/linstor/core/devmgr/DeviceHandlerImpl.java b/satellite/src/main/java/com/linbit/linstor/core/devmgr/DeviceHandlerImpl.java
index 49138a8fd..2f768ca0d 100644
--- a/satellite/src/main/java/com/linbit/linstor/core/devmgr/DeviceHandlerImpl.java
+++ b/satellite/src/main/java/com/linbit/linstor/core/devmgr/DeviceHandlerImpl.java
@@ -83,6 +83,8 @@ import java.util.TreeMap;
import java.util.TreeSet;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Function;
+import java.nio.file.Files;
+import java.nio.file.Paths;
@Singleton
public class DeviceHandlerImpl implements DeviceHandler
@@ -1646,7 +1648,10 @@ public class DeviceHandlerImpl implements DeviceHandler
private void updateDiscGran(VlmProviderObject<Resource> vlmData) throws DatabaseException, StorageException
{
String devicePath = vlmData.getDevicePath();
- if (devicePath != null && vlmData.exists())
+ // Check if device path physically exists before calling lsblk
+ // This is important for DRBD devices which might be temporarily unavailable during adjust
+ // (drbdadm adjust brings devices down/up, and kernel might not have created the device node yet)
+ if (devicePath != null && vlmData.exists() && Files.exists(Paths.get(devicePath)))
{
if (vlmData.getDiscGran() == VlmProviderObject.UNINITIALIZED_SIZE)
{
diff --git a/satellite/src/main/java/com/linbit/linstor/layer/drbd/DrbdLayer.java b/satellite/src/main/java/com/linbit/linstor/layer/drbd/DrbdLayer.java
index 01967a31f..78b8195a4 100644
--- a/satellite/src/main/java/com/linbit/linstor/layer/drbd/DrbdLayer.java
+++ b/satellite/src/main/java/com/linbit/linstor/layer/drbd/DrbdLayer.java
@@ -592,7 +592,29 @@ public class DrbdLayer implements DeviceLayer
// The .res file might not have been generated in the prepare method since it was
// missing information from the child-layers. Now that we have processed them, we
// need to make sure the .res file exists in all circumstances.
- regenerateResFile(drbdRscData);
+ // However, if the underlying devices are not accessible (e.g., LUKS device is closed
+ // during resource deletion), we skip regenerating the res file to avoid errors
+ boolean canRegenerateResFile = true;
+ if (!skipDisk && !drbdRscData.getAbsResource().isDrbdDiskless(workerCtx))
+ {
+ AbsRscLayerObject<Resource> dataChild = drbdRscData.getChildBySuffix(RscLayerSuffixes.SUFFIX_DATA);
+ if (dataChild != null)
+ {
+ for (DrbdVlmData<Resource> drbdVlmData : drbdRscData.getVlmLayerObjects().values())
+ {
+ VlmProviderObject<Resource> childVlm = dataChild.getVlmProviderObject(drbdVlmData.getVlmNr());
+ if (childVlm == null || !childVlm.exists() || childVlm.getDevicePath() == null)
+ {
+ canRegenerateResFile = false;
+ break;
+ }
+ }
+ }
+ }
+ if (canRegenerateResFile)
+ {
+ regenerateResFile(drbdRscData);
+ }
// createMetaData needs rendered resFile
for (DrbdVlmData<Resource> drbdVlmData : createMetaData)
@@ -766,19 +788,72 @@ public class DrbdLayer implements DeviceLayer
if (drbdRscData.isAdjustRequired())
{
- try
+ // Check if underlying devices are accessible before adjusting
+ // This is important for encrypted resources (LUKS) where the device
+ // might be closed during deletion
+ boolean canAdjust = true;
+
+ // IMPORTANT: Check child volumes only when disk access is actually needed.
+ // For network reconnect (StandAlone -> Connected), disk access is not required.
+ boolean needsDiskAccess = false;
+
+ // Check if there are pending operations that require disk access
+ for (DrbdVlmData<Resource> drbdVlmData : drbdRscData.getVlmLayerObjects().values())
{
- drbdUtils.adjust(
- drbdRscData,
- false,
- skipDisk,
- false
- );
+ Volume vlm = (Volume) drbdVlmData.getVolume();
+ StateFlags<Volume.Flags> vlmFlags = vlm.getFlags();
+
+ // Disk access is needed if:
+ // - creating a new volume
+ // - resizing
+ // - checking/creating metadata
+ if (!drbdVlmData.exists() ||
+ drbdVlmData.checkMetaData() ||
+ vlmFlags.isSomeSet(workerCtx, Volume.Flags.RESIZE, Volume.Flags.DRBD_RESIZE))
+ {
+ needsDiskAccess = true;
+ break;
+ }
+ }
+
+ // Check child volumes only if disk access is actually needed
+ if (needsDiskAccess && !skipDisk && !drbdRscData.getAbsResource().isDrbdDiskless(workerCtx))
+ {
+ AbsRscLayerObject<Resource> dataChild = drbdRscData.getChildBySuffix(RscLayerSuffixes.SUFFIX_DATA);
+ if (dataChild != null)
+ {
+ for (DrbdVlmData<Resource> drbdVlmData : drbdRscData.getVlmLayerObjects().values())
+ {
+ VlmProviderObject<Resource> childVlm = dataChild.getVlmProviderObject(drbdVlmData.getVlmNr());
+ if (childVlm == null || !childVlm.exists() || childVlm.getDevicePath() == null)
+ {
+ canAdjust = false;
+ break;
+ }
+ }
+ }
}
- catch (ExtCmdFailedException extCmdExc)
+
+ if (canAdjust)
+ {
+ try
+ {
+ drbdUtils.adjust(
+ drbdRscData,
+ false,
+ skipDisk,
+ false
+ );
+ }
+ catch (ExtCmdFailedException extCmdExc)
+ {
+ restoreBackupResFile(drbdRscData);
+ throw extCmdExc;
+ }
+ }
+ else
{
- restoreBackupResFile(drbdRscData);
- throw extCmdExc;
+ drbdRscData.setAdjustRequired(false);
}
}
diff --git a/satellite/src/main/java/com/linbit/linstor/layer/luks/LuksLayer.java b/satellite/src/main/java/com/linbit/linstor/layer/luks/LuksLayer.java
index cdca0b6d2..89c8be9da 100644
--- a/satellite/src/main/java/com/linbit/linstor/layer/luks/LuksLayer.java
+++ b/satellite/src/main/java/com/linbit/linstor/layer/luks/LuksLayer.java
@@ -383,6 +383,7 @@ public class LuksLayer implements DeviceLayer
vlmData.setSizeState(Size.AS_EXPECTED);
vlmData.setOpened(true);
+ vlmData.setExists(true);
vlmData.setFailed(false);
}
}

View file

@ -20,6 +20,8 @@ spec:
- name: DrbdOptions/auto-diskful-allow-cleanup
value: {{ .Values.linstor.autoDiskful.allowCleanup | quote }}
{{- end }}
- name: DrbdOptions/Net/verify-alg
value: "crc32c"
- name: DrbdOptions/Net/connect-int
value: "15"
- name: DrbdOptions/Net/ping-int

View file

@ -1,7 +1,7 @@
piraeusServer:
image:
repository: ghcr.io/cozystack/cozystack/piraeus-server
tag: 1.32.3@sha256:64c91357affc6317d9544fc35b3ff8b2fcb065ad8fc5ed26f7a2fa8ac991a1c1
tag: 1.33.2@sha256:553f313ab35dc2e345ef3683156d29e75c23177e2750e9af3a83aa9e23941cbb
# Talos-specific workarounds (disable for generic Linux like Ubuntu/Debian)
talos:
enabled: true
@ -13,4 +13,4 @@ linstor:
linstorCSI:
image:
repository: ghcr.io/cozystack/cozystack/linstor-csi
tag: v1.10.5@sha256:585ee6336036cb9e0b2183a6ab3f67e1ccb6d35a1df484d36e734444ee09eb4f
tag: v1.10.5@sha256:44154467c2f13c30ad09d932f51a935b143028bce54f877f00a51d1e43979f61

View file

@ -5,6 +5,9 @@ metadata:
name: alerta-db
spec:
instances: 2
{{- $existingCluster := lookup "postgresql.cnpg.io/v1" "Cluster" .Release.Namespace "alerta-db" }}
{{- $image := dig "spec" "imageName" "ghcr.io/cloudnative-pg/postgresql:17.7-standard-trixie" $existingCluster }}
imageName: {{ if regexMatch ":17\\." $image }}ghcr.io/cloudnative-pg/postgresql:17.7-standard-trixie{{ else if regexMatch ":[0-9]+\\.[0-9]+$" $image }}{{ printf "%s-standard-trixie" $image }}{{ else }}{{ $image }}{{ end }}
{{- if .Values._cluster.scheduling }}
{{- $rawConstraints := get .Values._cluster.scheduling "globalAppTopologySpreadConstraints" }}
{{- if $rawConstraints }}

View file

@ -181,7 +181,7 @@ metadata:
app: alerta
annotations:
{{- if eq $solver "http01" }}
acme.cert-manager.io/http01-ingress-class: {{ $ingress }}
acme.cert-manager.io/http01-ingress-ingressclassname: {{ $ingress }}
{{- end }}
cert-manager.io/cluster-issuer: {{ $clusterIssuer }}
spec:

View file

@ -14,7 +14,7 @@ spec:
url: http://grafana-dashboards.cozy-grafana-operator.svc/{{ . }}.json
{{- end }}
{{- end }}
{{- if eq .Release.Namespace "tenant-root" }}
{{- if or (eq .Release.Namespace "tenant-root") (eq .Release.Namespace "cozy-monitoring") }}
{{- range (split "\n" (.Files.Get "dashboards-infra.list")) }}
{{- $parts := split "/" . }}
{{- if eq (len $parts) 2 }}

View file

@ -4,6 +4,9 @@ metadata:
name: grafana-db
spec:
instances: 2
{{- $existingCluster := lookup "postgresql.cnpg.io/v1" "Cluster" .Release.Namespace "grafana-db" }}
{{- $image := dig "spec" "imageName" "ghcr.io/cloudnative-pg/postgresql:17.7-standard-trixie" $existingCluster }}
imageName: {{ if regexMatch ":17\\." $image }}ghcr.io/cloudnative-pg/postgresql:17.7-standard-trixie{{ else if regexMatch ":[0-9]+\\.[0-9]+$" $image }}{{ printf "%s-standard-trixie" $image }}{{ else }}{{ $image }}{{ end }}
storage:
size: {{ .Values.grafana.db.size }}
{{- if .Values._cluster.scheduling }}

View file

@ -74,7 +74,7 @@ spec:
metadata:
annotations:
{{- if eq $solver "http01" }}
acme.cert-manager.io/http01-ingress-class: "{{ $ingress }}"
acme.cert-manager.io/http01-ingress-ingressclassname: "{{ $ingress }}"
{{- end }}
cert-manager.io/cluster-issuer: {{ $clusterIssuer }}
spec:

View file

@ -11,3 +11,15 @@ update:
wget -q https://raw.githubusercontent.com/k8snetworkplumbingwg/multus-cni/refs/tags/$(RELEASE)/deployments/multus-daemonset-thick.yml -O templates/multus-daemonset-thick.yml
sed -i '/multus-cni/s/snapshot-thick/$(RELEASE)-thick/' templates/multus-daemonset-thick.yml && \
patch --no-backup-if-mismatch -p4 < patches/customize-deployment.patch
$(SED_INPLACE) "/ARG VERSION/ s|=.*|=$(RELEASE)|g" images/multus-cni/Dockerfile
image:
docker buildx build images/multus-cni \
--tag $(REGISTRY)/multus-cni:$(call settag,$(TAG)) \
--cache-from type=registry,ref=$(REGISTRY)/multus-cni:latest \
--cache-to type=inline \
--metadata-file images/multus-cni.json \
$(BUILDX_ARGS)
DIGEST=$$(yq e '."containerimage.digest"' images/multus-cni.json -o json -r) && \
sed -i "s|image: .*multus-cni.*|image: $(REGISTRY)/multus-cni:$(TAG)@$${DIGEST}|g" templates/multus-daemonset-thick.yml
rm -f images/multus-cni.json

View file

@ -0,0 +1,25 @@
FROM --platform=$BUILDPLATFORM golang:1.24 AS build
ARG VERSION=v4.2.3
ARG TARGETPLATFORM
WORKDIR /usr/src/multus-cni
RUN curl -sSL https://github.com/k8snetworkplumbingwg/multus-cni/archive/refs/tags/${VERSION}.tar.gz | \
tar -xzf - --strip=1
COPY patches /patches
RUN git init && git config user.email "build@cozystack" && git config user.name "build" && \
git add -A && git commit -m "init" && git tag ${VERSION} && \
git apply /patches/*.diff
RUN ./hack/build-go.sh
FROM debian:stable-slim
LABEL org.opencontainers.image.source=https://github.com/cozystack/cozystack
COPY --from=build /usr/src/multus-cni/bin /usr/src/multus-cni/bin
COPY --from=build /usr/src/multus-cni/LICENSE /usr/src/multus-cni/LICENSE
COPY --from=build /usr/src/multus-cni/bin/cert-approver /
WORKDIR /
ENTRYPOINT ["/usr/src/multus-cni/bin/multus-daemon"]

View file

@ -0,0 +1,49 @@
diff --git a/pkg/multus/multus.go b/pkg/multus/multus.go
index a7941fe..eff0406 100644
--- a/pkg/multus/multus.go
+++ b/pkg/multus/multus.go
@@ -954,33 +954,19 @@ func CmdDel(args *skel.CmdArgs, exec invoke.Exec, kubeClient *k8s.ClientInfo) er
}
if !useCacheConf {
- // Fetch delegates again if cache is not exist and pod info can be read
- if os.IsNotExist(err) && pod != nil {
- if in.ClusterNetwork != "" {
- _, err = k8s.GetDefaultNetworks(pod, in, kubeClient, nil)
- if err != nil {
- return cmdErr(k8sArgs, "failed to get clusterNetwork/defaultNetworks: %v", err)
- }
- // First delegate is always the master plugin
- in.Delegates[0].MasterPlugin = true
- }
-
- // Get pod annotation and so on
- _, _, err := k8s.TryLoadPodDelegates(pod, in, kubeClient, nil)
- if err != nil {
- if len(in.Delegates) == 0 {
- // No delegate available so send error
- return cmdErr(k8sArgs, "failed to get delegates: %v", err)
- }
- // Get clusterNetwork before, so continue to delete
- logging.Errorf("Multus: failed to get delegates: %v, but continue to delete clusterNetwork", err)
- }
- } else {
- // The options to continue with a delete have been exhausted (cachefile + API query didn't work)
- // We cannot exit with an error as this may cause a sandbox to never get deleted.
- logging.Errorf("Multus: failed to get the cached delegates file: %v, cannot properly delete", err)
+ // If cache does not exist, ADD was never completed for this sandbox.
+ // Attempting DEL with a config reconstructed from the API will fail because
+ // delegate plugins (e.g. kube-ovn) require runtime state that only exists
+ // after a successful ADD (socket paths, cached results, etc.).
+ // Return success to allow containerd to release the sandbox name reservation.
+ if os.IsNotExist(err) {
+ logging.Verbosef("Multus: no cache found for container %s (ADD was never completed), skip DEL", args.ContainerID)
return nil
}
+ // The options to continue with a delete have been exhausted
+ // We cannot exit with an error as this may cause a sandbox to never get deleted.
+ logging.Errorf("Multus: failed to get the cached delegates file: %v, cannot properly delete", err)
+ return nil
}
// set CNIVersion in delegate CNI config if there is no CNIVersion and multus conf have CNIVersion.

View file

@ -119,6 +119,7 @@ data:
"cniConfigDir": "/host/etc/cni/net.d",
"multusAutoconfigDir": "/host/etc/cni/net.d",
"multusConfigFile": "auto",
"multusMasterCNI": "05-cilium.conflist",
"socketDir": "/host/run/multus/"
}
---
@ -154,7 +155,7 @@ spec:
serviceAccountName: multus
containers:
- name: kube-multus
image: ghcr.io/k8snetworkplumbingwg/multus-cni:v4.2.3-thick
image: ghcr.io/cozystack/cozystack/multus-cni:v1.2.4@sha256:6735ffc12e5e660951f5a42943b38c308f33774a55836e94c191001405b58ec0
command: [ "/usr/src/multus-cni/bin/multus-daemon" ]
resources:
requests:
@ -200,7 +201,7 @@ spec:
fieldPath: spec.nodeName
initContainers:
- name: install-multus-binary
image: ghcr.io/k8snetworkplumbingwg/multus-cni:v4.2.3-thick
image: ghcr.io/cozystack/cozystack/multus-cni:v1.2.4@sha256:6735ffc12e5e660951f5a42943b38c308f33774a55836e94c191001405b58ec0
command:
- "/usr/src/multus-cni/bin/install_multus"
- "-d"

View file

@ -1,3 +1,3 @@
objectstorage:
controller:
image: "ghcr.io/cozystack/cozystack/objectstorage-controller:v1.2.0@sha256:05aee8bf8b292150090868163c6bb49962d874ac63858c93f8e130f53dd40b85"
image: "ghcr.io/cozystack/cozystack/objectstorage-controller:v1.2.4@sha256:e176668b8daecc5d462b843285c907c873edeb2d888af31254b30bb02320c394"

View file

@ -5,6 +5,9 @@ metadata:
name: seaweedfs-db
spec:
instances: {{ .Values.db.replicas }}
{{- $existingCluster := lookup "postgresql.cnpg.io/v1" "Cluster" .Release.Namespace "seaweedfs-db" }}
{{- $image := dig "spec" "imageName" "ghcr.io/cloudnative-pg/postgresql:17.7-standard-trixie" $existingCluster }}
imageName: {{ if regexMatch ":17\\." $image }}ghcr.io/cloudnative-pg/postgresql:17.7-standard-trixie{{ else if regexMatch ":[0-9]+\\.[0-9]+$" $image }}{{ printf "%s-standard-trixie" $image }}{{ else }}{{ $image }}{{ end }}
storage:
size: {{ .Values.db.size }}
{{- with .Values.db.storageClass }}

View file

@ -111,7 +111,7 @@ seaweedfs:
nginx.ingress.kubernetes.io/client-body-timeout: "3600"
nginx.ingress.kubernetes.io/client-header-timeout: "120"
nginx.ingress.kubernetes.io/service-upstream: "true"
acme.cert-manager.io/http01-ingress-class: tenant-root
acme.cert-manager.io/http01-ingress-ingressclassname: tenant-root
cert-manager.io/cluster-issuer: letsencrypt-prod
tls:
- hosts:
@ -177,7 +177,7 @@ seaweedfs:
bucketClassName: "seaweedfs"
region: ""
sidecar:
image: "ghcr.io/cozystack/cozystack/objectstorage-sidecar:v1.2.0@sha256:b508ca451c6051b64e71cedd6209c80ac0e0bea8aa6bb47e838ec9d99bf4796a"
image: "ghcr.io/cozystack/cozystack/objectstorage-sidecar:v1.2.4@sha256:cc1f60ef2cb40ab61c4787e234f83e57a7b8aca0ffe7b17f4c4fa6e3de1669f4"
certificates:
commonName: "SeaweedFS CA"
ipAddresses: []