Compare commits

...

77 Commits

Author SHA1 Message Date
cozystack-bot
d4c69751ae Prepare release v1.1.2
Signed-off-by: cozystack-bot <217169706+cozystack-bot@users.noreply.github.com>
2026-03-15 01:36:40 +00:00
Andrei Kvapil
a18047fc16 [Backport release-1.1] fix(api): skip OpenAPI post-processor for non-apps group versions (#2217)
# Description
Backport of #2212 to `release-1.1`.
2026-03-13 16:24:27 +01:00
Andrei Kvapil
803d6b15ba [Backport release-1.1] [bucket] Fix s3manager endpoint mismatch with COSI credentials (#2215)
# Description
Backport of #2211 to `release-1.1`.
2026-03-13 16:23:58 +01:00
Andrei Kvapil
c94768c64b Revert "fix(operator): requeue packages when dependencies are not ready"
This reverts commit f906a0d8ad.

Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
(cherry picked from commit 2b60c010dd)
2026-03-13 15:23:34 +00:00
Andrei Kvapil
093329cdce fix(operator): requeue packages when dependencies are not ready
When dependencies are not ready the reconciler returned without
requeueing, relying solely on watch events to re-trigger. If a watch
event was missed (controller restart, race condition, dependency already
ready before watch setup), the package would stay stuck in
DependenciesNotReady forever.

Add RequeueAfter: 30s so dependencies are periodically rechecked.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
(cherry picked from commit f906a0d8ad)
2026-03-13 15:23:34 +00:00
Andrei Kvapil
0e3c7fabff fix(api): skip OpenAPI post-processor for non-apps group versions
The OpenAPI PostProcessSpec callback is invoked for every group-version
(apps, core, version, etc.), but the Application schema cloning logic
only applies to apps.cozystack.io. When called for other GVs the base
Application schemas are absent, causing a spurious error log on every
API server start.

Return early instead of erroring when the base schemas are not found.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
(cherry picked from commit ee83aaa82e)
2026-03-13 15:23:34 +00:00
IvanHunters
0ba129b4b7 [bucket] Fix s3manager endpoint to use actual S3 endpoint from BucketInfo
The deployment template was constructing the S3 endpoint from the tenant's
namespace host (e.g. s3.freedom.infra.example.com), while COSI credentials
are issued for the actual SeaweedFS endpoint (e.g. s3.infra.example.com).
This mismatch caused 'invalid credentials' errors when users tried to log
in with valid credentials from the bucket secret.

Now the endpoint is resolved from BucketInfo (same source as credentials),
with a fallback to the constructed namespace host for first-time deploys
before BucketAccess secrets are created.

Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>
(cherry picked from commit f647cfd7b9)
2026-03-13 15:23:24 +00:00
Andrei Kvapil
095d3ab150 Release v1.1.1 (#2184)
This PR prepares the release `v1.1.1`.
2026-03-11 00:12:15 +01:00
cozystack-bot
b06e2cecd5 Prepare release v1.1.1
Signed-off-by: cozystack-bot <217169706+cozystack-bot@users.noreply.github.com>
2026-03-10 20:31:40 +00:00
Andrei Kvapil
b255214da0 [Backport release-1.1] fix(dashboard): exclude hidden MarketplacePanel resources from sidebar menu (#2203)
# Description
Backport of #2177 to `release-1.1`.
2026-03-10 17:50:15 +01:00
Andrei Kvapil
4c9c68b7f5 [Backport release-1.1] fix(dashboard): preserve disabled/hidden state on MarketplacePanel reconciliation (#2201)
# Description
Backport of #2176 to `release-1.1`.
2026-03-10 17:50:04 +01:00
IvanHunters
002bd20f19 fix(dashboard): exclude hidden MarketplacePanel resources from sidebar menu
The sidebar was generated independently from MarketplacePanels, always
showing all resources regardless of their hidden state. Fetch
MarketplacePanels during sidebar reconciliation and skip resources
where hidden=true, so hiding a resource from the marketplace also
removes it from the sidebar navigation.

Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>
(cherry picked from commit 318079bf66)
2026-03-10 16:49:44 +00:00
IvanHunters
22c46d7271 fix(dashboard): preserve disabled/hidden state on MarketplacePanel reconciliation
The controller was hardcoding disabled=false and hidden=false on every
reconciliation, overwriting any user changes made through the dashboard
UI. Move spec building inside the CreateOrUpdate mutate function to read
and preserve current disabled/hidden values from the existing resource.

Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>
(cherry picked from commit e69efd80c4)
2026-03-10 16:48:33 +00:00
Andrei Kvapil
dce757c884 [Backport release-1.1] fix(dashboard): fix External IPs factory EnrichedTable rendering (#2193)
# Description
Backport of #2175 to `release-1.1`.
2026-03-10 15:19:42 +01:00
Andrei Kvapil
109b4a333e [Backport release-1.1] [platform] Fix VM MAC address not preserved during migration (#2190)
# Description
Backport of #2169 to `release-1.1`.
2026-03-10 15:19:15 +01:00
IvanHunters
4dada99a92 fix(dashboard): fix External IPs factory EnrichedTable rendering
The external-ips factory used incorrect EnrichedTable properties causing
empty rows in the dashboard. Replace `clusterNamePartOfUrl` with
`cluster` and change `pathToItems` from array to dot-path string format
to match the convention used by all other working EnrichedTable instances.

Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>
(cherry picked from commit 49601b166d)
2026-03-10 14:19:13 +00:00
Kirill Ilin
fd18a699b9 fix(migration): preserve VM MAC address during virtual-machine to vm-instance migration
Kube-OVN reads MAC address exclusively from the pod annotation
ovn.kubernetes.io/mac_address, not from the IP resource spec.macAddress.
Without pod-level annotations, migrated VMs receive a new random MAC,
breaking OS-level network config that matches by MAC (e.g. netplan).

Add a Helm lookup for the Kube-OVN IP resource in the vm-instance chart
template. When the IP resource exists, its macAddress and ipAddress are
automatically injected as pod annotations. This removes the need for
fragile Flux postRenderers on the HelmRelease — the chart itself handles
MAC/IP preservation based on actual cluster state.

Remove the postRenderers approach from migration 29 since the chart now
handles this natively.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Kirill Ilin <stitch14@yandex.ru>
(cherry picked from commit 9a4f49238c)
2026-03-10 14:18:35 +00:00
Andrei Kvapil
532669ad61 [Backport release-1.1] fix(etcd-operator): replace deprecated kube-rbac-proxy image (#2182)
# Description
Backport of #2181 to `release-1.1`.
2026-03-10 12:39:16 +01:00
Andrei Kvapil
116e1baf63 fix(etcd-operator): replace deprecated kube-rbac-proxy image
The gcr.io/kubebuilder/kube-rbac-proxy image is no longer available
since GCR was deprecated. Replace it with quay.io/brancz/kube-rbac-proxy
from the original upstream author.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
(cherry picked from commit 4946383cf1)
2026-03-10 11:37:08 +00:00
Andrei Kvapil
52b4a0a7c6 [Backport release-1.1] fix(migrations): handle missing rabbitmq CRD in migration 34 (#2180)
# Description
Backport of #2168 to `release-1.1`.
2026-03-10 09:03:24 +01:00
IvanHunters
0b4f3c7d30 fix(migrations): handle missing rabbitmq CRD in migration 34
Migration 34 fails when rabbitmqs.apps.cozystack.io CRD does not exist,
which happens when RabbitMQ was never installed on the cluster. Add a
check for CRD presence before attempting to list resources.

Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>
(cherry picked from commit 21f293ace5)
2026-03-10 07:19:01 +00:00
Andrei Kvapil
ccb37d3fac [Backport release-1.1] fix(keycloak): use management port health endpoints for probes (#2179)
# Description
Backport of #2162 to `release-1.1`.
2026-03-10 08:17:32 +01:00
mattia-eleuteri
89d90cac2d fix(keycloak): add startupProbe, remove initialDelaySeconds
Use a startupProbe to defer liveness/readiness checks until Keycloak
has fully started, instead of relying on initialDelaySeconds. This is
more robust for applications with variable startup times.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: mattia-eleuteri <mattia@hidora.io>
(cherry picked from commit d18ed79382)
2026-03-10 07:15:33 +00:00
mattia-eleuteri
4f9a035c5b fix(keycloak): use management port health endpoints for probes
Keycloak 26.x exposes dedicated health endpoints on the management
port (9000) via /health/live and /health/ready. The previous probes
used GET / on port 8080 which redirects to the configured KC_HOSTNAME
(HTTPS), causing kubelet to fail the probe with "Probe terminated
redirects" and eventually kill the pod in a crashloop.

Changes:
- Add KC_HEALTH_ENABLED=true to activate health endpoints
- Expose management port 9000 in container ports
- Switch liveness probe to /health/live on port 9000
- Switch readiness probe to /health/ready on port 9000
- Increase failure thresholds for more tolerance during startup

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: mattia-eleuteri <mattia@hidora.io>
(cherry picked from commit 0873691913)
2026-03-10 07:15:33 +00:00
IvanHunters
3736dd4285 Release v1.1.0 (#2163)
This PR prepares the release `v1.1.0`.
2026-03-06 16:21:32 +03:00
cozystack-bot
3c971d0e1b Prepare release v1.1.0
Signed-off-by: cozystack-bot <217169706+cozystack-bot@users.noreply.github.com>
2026-03-06 12:03:56 +00:00
IvanHunters
8166df23de [system] Add MongoDB Grafana dashboards (#2158)
## What this PR does

Adds two Grafana dashboards for MongoDB monitoring, adapted to cozystack
conventions:

- **MongoDB Overview** — command operations, connections, cursors,
document operations, queued operations, query efficiency, scanned
objects, write time/operations, asserts, page faults
- **MongoDB InMemory Details** — WiredTiger cache size/capacity, dirty
pages, transactions, sessions, pages, concurrency tickets, cache
eviction, document changes, scanned objects, page faults

Key adaptations from the original PMM dashboards:
- Replaced hardcoded datasource UIDs with `${ds_prometheus}` variable
- Replaced PMM-specific variables with standard `job`-based service
filtering
- Removed Node Summary section (covered by existing node dashboards)
- Registered both dashboards in `dashboards.list` for automatic
GrafanaDashboard CRD generation

### Release note

```release-note
[system] Add MongoDB Overview and MongoDB InMemory Details Grafana dashboards to the monitoring stack
```

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

## Summary by CodeRabbit

* **New Features**
* Added MongoDB InMemory dashboard providing detailed visibility into
in-memory metrics, cache performance, and data management.
* Added MongoDB Overview dashboard for monitoring operations,
connections, cursors, document activity, and query efficiency.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-03-06 14:53:40 +03:00
Timofei Larkin
c30a3cff13 docs: add changelog for v1.0.3 (#2160)
This PR adds the changelog for release `v1.0.3`.

 Changelog has been automatically generated in
`docs/changelogs/v1.0.3.md`.

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

## Summary by CodeRabbit

* **Bug Fixes**
* Fixed migration script to correctly apply configuration prefixes
during v0.41 to v1.0 upgrade.

* **Documentation**
* Added white labeling guide covering branding customization and SVG
handling.
* Updated backup and recovery documentation with improved operator and
tenant workflow guidance and administration resources.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-03-06 15:45:12 +04:00
Andrei Kvapil
d8c96ecf50 [apps][system] Add -lock BucketClass, -readonly BucketAccessClass, and bucket user model (#2119)
## What this PR does

Combines and unifies COSI enhancements across seaweedfs and bucket
charts:

**SeaweedFS (extra + system charts):**
- Rename storage pool BucketClass suffix from `-worm` to `-lock`
- Rename parameter `disk` to `diskType` for consistency with COSI driver
- Reduce default object lock retention from 36500 to 365 days
- Add `-lock` BucketClass (COMPLIANCE mode, 365 days) for client and
system topologies
- Add `-readonly` BucketAccessClass with explicit `accessPolicy` for all
topologies
- Add explicit `accessPolicy: readwrite` on default BucketAccessClass
- Update pool name validation to reject `-lock` suffix (was `-worm`)

**Bucket app:**
- Add `locking` parameter: provisions from `-lock` BucketClass
- Add `storagePool` parameter: selects pool-specific BucketClass
- Replace hardcoded BucketAccess with `users` map — each entry creates a
BucketAccess with optional `readonly` flag
- Update dashboard RBAC to dynamically list user credential secrets
- Update ApplicationDefinition schema with new properties

**Breaking change:** empty `users: {}` (default) produces zero
BucketAccess resources. Existing buckets that relied on the implicit
default BucketAccess will need to define users explicitly.

### Release note

```release-note
[apps] Add locking, storagePool, and users configuration to bucket app; rename COSI BucketClass suffix from -worm to -lock
```

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

* **New Features**
* Bucket locking with a shorter retention option, storage-pool
selectable bucket classes, and per-user access (per-user BucketAccess
and readonly controls)
* S3 Manager login mode: user login/logout, per-session credentials, and
new login UI

* **Behavior Changes**
* Credential handling changed to per-user secrets/label selection;
previously generated secrets removed; Ingress basic auth annotations
removed

* **Documentation**
* Added parameters: locking, storagePool, users (including per-user
readonly)

* **Updates**
  * Updated COSI driver and S3 manager images
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-03-06 09:17:53 +01:00
Andrei Kvapil
2731972129 fix(kubernetes): set explicit MTU for Cilium in tenant clusters (#2147)
## Summary

- Set explicit MTU 1350 for Cilium in KubeVirt-based tenant Kubernetes
clusters to prevent packet drops caused by VXLAN encapsulation overhead

## Problem

Cilium's MTU auto-detection does not account for VXLAN overhead when
running inside KubeVirt VMs. The VM network interface inherits MTU 1400
from the parent cluster's OVN/Geneve overlay (1500 - 100 Geneve
overhead). Cilium detects this MTU and applies it to all tunnel
interfaces without subtracting the 50-byte VXLAN encapsulation overhead.

This results in:
- Large packets (> 1350 bytes) being silently dropped when crossing
VXLAN tunnels between nodes
- Intermittent connectivity issues for services in tenant clusters (TLS
handshakes, HTTP responses with data)
- HTTP 499 errors and timeouts observed under load

## Fix

Explicitly set `MTU: 1350` (1400 - 50 VXLAN overhead) in the default
Cilium values for tenant clusters. This value can still be overridden
via `addons.cilium.valuesOverride` if needed.

## Test plan

- [ ] Deploy a tenant Kubernetes cluster and verify Cilium interfaces
use MTU 1350
- [ ] Verify large packet connectivity from pods inside the tenant
cluster
2026-03-06 09:10:37 +01:00
cozystack-bot
133a071d6b docs: add changelog for v1.0.3
Signed-off-by: cozystack-bot <217169706+cozystack-bot@users.noreply.github.com>
2026-03-06 01:42:51 +00:00
IvanHunters
c838358c26 feat(monitoring): register MongoDB dashboards in dashboards.list
Add mongodb/mongodb-overview and mongodb/mongodb-inmemory entries
to the monitoring dashboards list so GrafanaDashboard CRDs are
generated and dashboards are served by the grafana-dashboards
HTTP service.

Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>
2026-03-05 22:48:05 +03:00
IvanHunters
6408988115 feat(monitoring): add MongoDB Grafana dashboards
Add two MongoDB dashboards adapted for cozystack monitoring:
- mongodb-overview: command operations, connections, cursors,
  document operations, queued operations, query efficiency,
  scanned objects, write time, asserts, page faults
- mongodb-inmemory: WiredTiger InMemory cache details including
  data size, capacity, transactions, sessions, pages, concurrency
  tickets, cache eviction, document changes

Dashboards use ds_prometheus variable for datasource selection and
job-based service filtering compatible with Percona MongoDB exporter
metrics.

Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>
2026-03-05 22:47:53 +03:00
IvanHunters
e1b169d6a7 fix(test): replace bats-specific run command with shell negation
cozytest.sh executes .bats files as plain shell functions where bats
builtins like `run` are not available. Use `!` negation to assert that
readonly user upload fails.

Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>
2026-03-05 08:45:57 +03:00
IvanHunters
4e8733091d fix(test): add --insecure flag to all mc commands in bucket E2E test
The mc client requires --insecure on each command when connecting to
SeaweedFS S3 with self-signed certificates via port-forward.

Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>
2026-03-04 22:37:37 +03:00
IvanHunters
15ed534c25 test(bucket): update E2E test for user model and readonly access
Update bucket E2E test to match the new per-user access model:
- Create bucket with admin (readwrite) and viewer (readonly) users
- Test that readwrite user can upload, list, and download objects
- Test that readonly user can list and download but cannot upload
- Use per-user BucketAccess and credential secret names

Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>
2026-03-04 21:13:46 +03:00
IvanHunters
72a91c7780 fix(seaweedfs): update COSI driver to v0.3.0
v0.1.2 ignores accessPolicy parameter from BucketAccessClass,
granting readwrite access to all users regardless of the
readonly flag. v0.3.0 includes support for readonly bucket
access, Object Lock, and improved error handling.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>
2026-03-04 20:15:03 +03:00
IvanHunters
fdb015458c build(bucket): rebuild s3manager image with login mode
Rebuild s3manager with auth.go login page support and push
to 999669/s3manager registry for testing.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>
2026-03-04 19:01:27 +03:00
IvanHunters
84da47a2ce refactor(bucket): replace basic auth with s3manager login page
Remove nginx basic auth and credential secret injection from the
bucket Helm chart. s3manager now always starts in login mode and
handles authentication via its own login page with encrypted
session cookies. This eliminates the dependency on the -credentials
and -ui-auth secrets for the UI layer.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>
2026-03-04 18:47:51 +03:00
IvanHunters
175b3badd2 feat(bucket): make credentials and basic auth conditional on users
deployment.yaml: use s3._namespace.host for ENDPOINT instead of
secret ref, inject ACCESS_KEY_ID/SECRET_ACCESS_KEY only when users
exist. Without users, s3manager starts in login mode.

ingress.yaml: nginx basic auth annotations only when users exist.
Without users, s3manager handles authentication via its login form.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>
2026-03-04 18:30:49 +03:00
IvanHunters
f777f659d5 feat(bucket): add login screen to s3manager for credentialless mode
When a bucket has no users configured, s3manager previously crashed
due to missing ACCESS_KEY_ID/SECRET_ACCESS_KEY env vars. This adds
a login mode where users enter their S3 credentials via a web form.

New Go code (via cozystack.patch):
- auth.go: session-based auth middleware, login/logout handlers,
  per-request S3 client from encrypted cookie session
- login.html.tmpl: Materialize CSS login form
- main.go: LoginMode toggle, conditional route setup
- Dependency: gorilla/sessions for AES-256 encrypted cookies

Dockerfile: add go mod tidy step for new dependency resolution.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>
2026-03-04 18:30:40 +03:00
IvanHunters
69e0320e3a fix(bucket): remove duplicate credentials from dashboard
Show only per-user credential secrets in the dashboard instead of
both the internal UI secret and per-user secrets.

Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>
2026-03-04 17:02:36 +03:00
Andrei Kvapil
f5b29e1182 Fixed packages name conversion in migration script (#2144)
<!-- 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
Fixed migrate-to-version-1.0.sh script to properly convert packages names.
```

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

## Summary by CodeRabbit

* **Chores**
* Updated migration tooling to improve package configuration handling
during version upgrades.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-03-04 00:22:42 +01:00
IvanHunters
9e6c240d6e fix(bucket): use per-user COSI secret for UI credentials
The default BucketAccess was removed in favor of per-user access.
Update secret.yaml to look up the first user's COSI secret instead
of the non-existent default one, with nil-check for race conditions.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>
2026-03-04 00:34:33 +03:00
IvanHunters
79b2546e67 fix(kubernetes): set explicit MTU for Cilium in tenant clusters
Cilium's MTU auto-detection does not account for VXLAN overhead when
running inside KubeVirt VMs. The VM interface inherits MTU 1400 from
the parent OVN/Geneve overlay, and Cilium sets all interfaces
(cilium_vxlan, lxc*, cilium_host/net) to 1400 without subtracting
the 50-byte VXLAN encapsulation overhead.

This causes intermittent packet drops for large packets (TLS
handshakes, HTTP responses with data), resulting in timeouts and
499 errors for services running in tenant clusters.

Set MTU to 1350 (1400 - 50 VXLAN overhead) explicitly in the default
Cilium values for tenant Kubernetes clusters.

Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>
2026-03-03 21:35:39 +03:00
IvanHunters
1d41e270b9 fix(bucket): regenerate schema and docs from values.yaml annotations
Remove the yq strip of properties from Makefile that was clearing
the schema, and run make generate to sync all generated files.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>
2026-03-03 19:32:32 +03:00
IvanHunters
b4f1e96b98 feat(bucket): include COSI user credential secrets in dashboard
Create labeled secrets in the -system chart using lookup to copy
credentials from COSI-created secrets. The ApplicationDefinition
matchLabels selector exposes them in the dashboard.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>
2026-03-03 19:32:32 +03:00
IvanHunters
757f8b5699 Revert "fix(bucket): include COSI user credential secrets in dashboard"
This reverts commit 87b89d90e6.

Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>
2026-03-03 19:32:32 +03:00
IvanHunters
70c6dfd704 fix(bucket): include COSI user credential secrets in dashboard
Add a catch-all include selector so that COSI-created user credential
secrets (dynamically named per user) are visible in the dashboard.

The lineage webhook already verifies ownership via the graph walk
(Secret -> BucketAccess -> HelmRelease -> Bucket), so an empty selector
safely matches only secrets belonging to this application.

This is needed because COSI sidecar creates secrets without custom
labels, making the matchLabels pattern (used by rabbitmq) inapplicable.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>
2026-03-03 19:32:32 +03:00
IvanHunters
d4043bd71c fix(seaweedfs): use correct COSI driver parameter name 'disk'
The seaweedfs-cosi-driver v0.3.0 expects the parameter key 'disk',
not 'diskType'. Restore the correct key to match the driver's
paramDisk constant.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>
2026-03-03 19:32:32 +03:00
IvanHunters
2dbc27075a feat(bucket): add locking, storagePool, and users configuration
Replace hardcoded BucketAccess with user-driven model:
- locking: provisions from -lock BucketClass (object lock enabled)
- storagePool: selects pool-specific BucketClass
- users: map of named users, each creating a BucketAccess with
  optional readonly flag

Empty users map produces zero BucketAccess resources (breaking change).
Update ApplicationDefinition schema and dashboard RBAC accordingly.

Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>
2026-03-03 19:32:32 +03:00
IvanHunters
96e27cbcab feat(seaweedfs): add -lock BucketClass and -readonly BucketAccessClass
Add COSI resources for object locking and read-only access to both
client topology and system chart:
- BucketClass with -lock suffix (COMPLIANCE mode, 365 days retention)
- BucketAccessClass with -readonly suffix
- Explicit accessPolicy: readwrite on default BucketAccessClass

Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>
2026-03-03 19:32:32 +03:00
IvanHunters
e822768fcf feat(seaweedfs): rename -worm to -lock BucketClass and update pool COSI parameters
Rename storage pool COSI resources to use consistent naming:
- BucketClass suffix: -worm -> -lock
- Parameter name: disk -> diskType
- Retention days: 36500 -> 365
- Validation suffix check updated accordingly

Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>
2026-03-03 19:32:31 +03:00
Andrey Kolkov
1429b94f5d fix(backups): rbac actualized (#2145)
<!-- 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
- fixed rbac for backup controllers 
```

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

## Summary by CodeRabbit

* **Chores**
* Updated backup controller permissions to focus on core backup
operations.
* Expanded backup strategy controller permissions to support enhanced
backup and restore capabilities, including Velero integration and status
management.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-03-03 19:36:06 +04:00
Andrey Kolkov
9ac8b2d291 fix(backups): rbac actualized
Signed-off-by: Andrey Kolkov <androndo@gmail.com>
2026-03-03 18:23:40 +04:00
Myasnikov Daniil
780af33ee1 Fixed packages name conversion in migration script
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-03-03 19:10:39 +05:00
Andrei Kvapil
772fb4363a docs: add changelog for v1.0.2 (#2141)
This PR adds the changelog for release `v1.0.2`.

 Changelog has been automatically generated in
`docs/changelogs/v1.0.2.md`.

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

## Summary by CodeRabbit

* **Documentation**
  * Published v1.0.2 release notes.

* **Bug Fixes**
  * Fixed migration script to ensure all upgrade steps execute.
* Improved dashboard functionality for field clearing and secret
copying.
  * Restored sidebar navigation on namespace-level pages.
  * Updated proxy configurations for enhanced TLS handling.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-03-02 21:50:58 +01:00
Kirill Ilin
cb919f4c85 [dashboard] Add storageClass dropdown for stateful apps (#2131)
## What this PR does

Replaces the plain text input for `storageClass` fields with an
API-backed dropdown listing available StorageClasses from the cluster.
Follows the same pattern as the `instanceType` dropdown for VMInstance.

Affected applications:
- **Top-level `spec.storageClass`**: ClickHouse, Harbor, HTTPCache,
  Kubernetes, MariaDB, MongoDB, NATS, OpenBAO, Postgres, Qdrant,
  RabbitMQ, Redis, VMDisk
- **Nested `spec.storage.storageClass`**: FoundationDB
- **Nested `spec.kafka.storageClass` / `spec.zookeeper.storageClass`**:
Kafka

### Release note

```release-note
[dashboard] storageClass fields in stateful app forms now render as a
dropdown populated with available StorageClasses from the cluster,
instead of a free-text input.
```

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

## Summary by CodeRabbit

## Release Notes

* **New Features**
* Storage class selection dropdowns now available in configuration forms
for multiple database, messaging, and storage services.

* **Tests**
* Added comprehensive test coverage for storage class configuration
handling.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-03-02 23:55:12 +05:00
cozystack-bot
aed46a8819 docs: add changelog for v1.0.2
Signed-off-by: cozystack-bot <217169706+cozystack-bot@users.noreply.github.com>
2026-03-02 18:45:19 +00:00
Andrei Kvapil
35e3eddeff [dashboard] fix: restore stock-instance sidebars for namespace-level pages (#2136)
## What this PR does

Restores `stock-instance-api-form`, `stock-instance-api-table`,
`stock-instance-builtin-form`, and `stock-instance-builtin-table`
sidebar
resources that were removed in #2106, and adds them back to the orphan
cleanup allowlist.

PR #2106 removed these sidebars to fix broken URLs on the main page
before
namespace selection (`default//api-table/...`). However,
`stock-instance-*`
sidebars are required by the frontend for namespace-level
api-table/api-form
pages. Without them and with `CUSTOMIZATION_SIDEBAR_FALLBACK_ID=""`, the
frontend cannot find a sidebar for pages like Backup Plans and renders
an
empty page where no interaction is possible.

The broken-URL bug is already fully fixed by
`CUSTOMIZATION_SIDEBAR_FALLBACK_ID=""`
in `web.yaml`. Re-adding `stock-instance-*` does not reintroduce it,
since
these sidebars are only shown when the user is on a namespace-level page
where the `{namespace}` placeholder is filled.

### Release note

```release-note
[dashboard] fix empty page on Backup Plans and other namespace-level api-table pages
```

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

## Summary by CodeRabbit

* **New Features**
* Added four new dashboard sidebar resources for stock instances: API
form, API table, built-in form, and built-in table views. These enable
expanded dashboard customization options for managing stock instance
configurations and data.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-03-02 19:28:18 +01:00
Andrei Kvapil
8d566a27ed [dashboard] fix: allow clearing instanceType and preserve secret copy newlines (#2135)
## What this PR does

Updates the openapi-k8s-toolkit integration in the dashboard to fix two
UX issues:

**1. Allow clearing the instanceType field in VMInstance form**

When `instanceType` has a default value, clearing the field in the form
UI would
silently revert to the default, making it impossible to explicitly send
an empty
value. This blocked use of custom KubeVirt resources without a named
instance type.

Adds `allowEmpty: true` to the instanceType listInput field so the BFF
preserves
an explicit empty value. Also introduces a generic `persistType` prop
(`'str' | 'number' | 'arr' | 'obj'`) to the listInput component, so the
allow-empty behaviour works correctly for any field type, not just
strings.

Updates openapi-k8s-toolkit to release/1.4.0 (d6b9e4ad), which already
includes
the FormListInput layout refactor — the previous
formlistinput-value-binding.diff
patch is no longer needed.

Upstream PR:
https://github.com/PRO-Robotech/openapi-k8s-toolkit/pull/340

**2. Preserve newlines when copying secrets with CMD+C**

Native `input[type=text]` strips newlines on copy. Adds an `onCopy`
handler to
the SecretBase64Plain component that intercepts the copy event and
writes the full
decoded value (including newlines) to the clipboard.

Upstream PR:
https://github.com/PRO-Robotech/openapi-k8s-toolkit/pull/339

### Release note

```release-note
[dashboard] Fix clearing instanceType in VMInstance form: explicit empty value
is now correctly sent to the API instead of falling back to the schema default.
Fix CMD+C copying of secrets stripping newlines.
```

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

## Summary by CodeRabbit

* **New Features**
  * Dropdown fields now support configuration to allow empty selections
* Enhanced empty value handling for form fields across multiple data
types (string, number, array, object)

* **Bug Fixes**
* Fixed secret field copy functionality to preserve plain-text format
when visible

* **Chores**
  * Updated base image dependencies for dashboard build

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-03-02 19:27:35 +01:00
Kirill Ilin
45b61f812d fix(dashboard): restore stock-instance sidebars for namespace-level pages
PR #2106 removed stock-instance-* sidebar resources to fix broken URLs
on the main page before namespace selection. However, these sidebars are
required for rendering namespace-level pages (api-table, api-form, etc.)
such as the Backup Plans page.

Without stock-instance-api-table, the frontend cannot find the sidebar
for namespace-scoped api-table pages and renders an empty page instead.

The original bug (broken URLs with empty namespace placeholder) is already
fixed by CUSTOMIZATION_SIDEBAR_FALLBACK_ID="" in web.yaml, so re-adding
stock-instance-* sidebars does not reintroduce it.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Kirill Ilin <stitch14@yandex.ru>
2026-03-02 23:13:48 +05:00
Kirill Ilin
99ee0e34bf fix(dashboard): preserve newlines when copying secrets with CMD+C
Add onCopy handler to SecretBase64Plain inputs to intercept native browser
copy events and explicitly write the full decoded text (including newlines)
to the clipboard. Without this, input[type=text] strips newlines on copy.

Upstream PR: https://github.com/PRO-Robotech/openapi-k8s-toolkit/pull/339

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Kirill Ilin <stitch14@yandex.ru>
2026-03-02 22:36:01 +05:00
Andrei Kvapil
0a461958ca [system] Fix Keycloak proxy configuration for v26.x (#2125)
## What this PR does

Replace deprecated `KC_PROXY=edge` with `KC_PROXY_HEADERS=xforwarded`
and `KC_HTTP_ENABLED=true` in the Keycloak StatefulSet template.

`KC_PROXY` was removed in Keycloak 26.x, causing "Non-secure context
detected" warnings and broken cookie handling when running behind a
reverse proxy with TLS termination.

### Release note

```release-note
[system] Fix Keycloak proxy headers configuration for compatibility with Keycloak 26.x
```

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

## Summary by CodeRabbit

## Release Notes

* **Chores**
* Updated system configuration to improve proxy header handling and
enable direct HTTP support for enhanced compatibility with reverse proxy
environments.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-03-02 18:29:35 +01:00
Kirill Ilin
6e8ce65e49 feat(dashboard): set allowEmpty on instanceType and update openapi-ui toolkit
Update openapi-k8s-toolkit commit to d6b9e4ad (release/1.4.0) which
includes the FormListInput layout refactor, making formlistinput-value-binding.diff
obsolete.

Set allowEmpty: true on the VMInstance instanceType field so users can
explicitly clear the selection and override the default instance type.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Kirill Ilin <stitch14@yandex.ru>
2026-03-02 22:19:54 +05:00
Kirill Ilin
a3ccb4f87d feat(dashboard): allow clearing instanceType field in VMInstance form
Update openapi-k8s-toolkit to release/1.4.0 (d6b9e4ad). The previous
value-binding layout refactor is already included upstream, so drop the
formlistinput-value-binding.diff patch.

Add formlistinput-allow-empty.diff patch which introduces two props to
the listInput component:
- allowEmpty: when set, auto-persists the field so BFF sends an empty
  value instead of falling back to the schema default
- persistType: controls the type of empty value ('str' | 'number' | 'arr'
  | 'obj'), allowing the feature to work correctly for any field type

Set allowEmpty: true on the VMInstance instanceType field so users can
explicitly clear the selection and override the default instance type.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Kirill Ilin <stitch14@yandex.ru>
2026-03-02 22:18:53 +05:00
Andrei Kvapil
20c91f25da [platform] Fixed run-migrations script (#2126)
<!-- 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
[platform] Fixed off-by-one error where the first required migration was always skipped.
```

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

* **Bug Fixes**
* Corrected migration range handling so upgrade steps run for the
intended version window, preventing skipped or duplicated migrations.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-03-02 17:53:41 +01:00
Andrei Kvapil
ee36c50d69 Disable private key rotation in CA certs (#2113)
<!-- 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
Disabled private key rotation in every CA cert in cozystack system packages to prevent trustchain problems when CA cert reissued
```

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

* **Chores**
* Disabled private-key rotation (set rotationPolicy: Never) for CA/root
certificates used by multiple system components (ingress-nginx, linstor,
linstor-scheduler, seaweedfs, victoria-metrics-operator,
kubeovn-webhook, lineage-controller-webhook, cozystack-api, etcd,
linstor API/internal, seaweedfs).
* Added patch application steps to relevant update workflows to ensure
the certificate template changes are applied during chart/update
operations.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-03-02 17:48:21 +01:00
Andrei Kvapil
1de4bb39a8 [rabbitmq] update version selection (#2092)
<!-- 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
[rabbitmq] Added version selection to newly created RabbitMQ instances.
```

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

* **New Features**
* Configurable RabbitMQ major.minor version selector (v4.2, v4.1, v4.0,
v3.13), default v4.2; chart validates selection and uses it to pick the
runtime image.

* **Chores**
  * Default RabbitMQ image updated to 4.2.4.
* Added an automated version-update helper and a Makefile target to
refresh available versions and regenerate manifests.

* **Migration**
* Migration added to backfill the version field on existing RabbitMQ
resources.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-03-02 17:46:22 +01:00
Kirill Ilin
c2bf8cf56f feat(dashboard): add storageClass dropdown for all stateful apps
Replace plain text input with an API-backed listInput dropdown for
storageClass fields across all applications that expose them.

The dropdown fetches available StorageClasses from the cluster via
/api/clusters/{cluster}/k8s/apis/storage.k8s.io/v1/storageclasses,
following the same pattern as the instanceType dropdown for VMInstance.

Top-level spec.storageClass: ClickHouse, Harbor, HTTPCache, Kubernetes,
MariaDB, MongoDB, NATS, OpenBAO, Postgres, Qdrant, RabbitMQ, Redis, VMDisk.

Nested paths: FoundationDB (spec.storage.storageClass),
Kafka (spec.kafka.storageClass and spec.zookeeper.storageClass).

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Kirill Ilin <stitch14@yandex.ru>
2026-03-02 21:26:51 +05:00
Andrei Kvapil
b455405402 fix(migration): suspend cozy-proxy if it conflicts with installer release (#2128)
## What this PR does

Adds a check in the migration script to detect and suspend the
`cozy-proxy`
HelmRelease if it has `releaseName: cozystack`, which conflicts with the
installer
release and causes cozystack-operator deletion during upgrade from v0.41
to v1.0.

### Release note

```release-note
[platform] Fix migration script to handle cozy-proxy releaseName conflict during v0.41→v1.0 upgrade.
```

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

## Summary by CodeRabbit

* **Chores**
* Enhanced the version 1.0 migration process with automatic conflict
detection and interactive guidance, prompting users to resolve issues
during the upgrade for a smoother migration experience.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-03-02 16:31:03 +01:00
Andrei Kvapil
14a9017932 fix(migration): suspend cozy-proxy if it conflicts with installer release
In v0.41.x, cozy-proxy HelmRelease was configured with
releaseName: cozystack, which collides with the installer helm release.
If not suspended before upgrade, the cozy-proxy HR reconciles and
overwrites the installer release, deleting cozystack-operator.

Add a check in the migration script that detects this conflict and
suspends the cozy-proxy HelmRelease before proceeding.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
2026-03-02 12:59:36 +01:00
Myasnikov Daniil
dc5c3dc9bc [rabbitmq] Added app version selection
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-03-02 15:36:20 +05:00
Myasnikov Daniil
79c57874bb [platform] Fixed run-migrations script
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-03-02 15:34:13 +05:00
Kirill Ilin
14228aa0d7 fix(keycloak): replace deprecated KC_PROXY with KC_PROXY_HEADERS
KC_PROXY=edge was deprecated and removed in Keycloak 26.x, causing
"Non-secure context detected" warnings and broken cookie handling
behind reverse proxy. Replace with KC_PROXY_HEADERS=xforwarded and
KC_HTTP_ENABLED=true.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Kirill Ilin <stitch14@yandex.ru>
2026-03-02 14:35:04 +05:00
Myasnikov Daniil
013b5b0873 Replaced direct chart edits with patch files
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-03-02 12:35:14 +05:00
Myasnikov Daniil
47d81f70d7 Disabled private key rotation in CA certs
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-02-27 19:09:10 +05:00
97 changed files with 4500 additions and 239 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

19
docs/changelogs/v1.0.2.md Normal file
View File

@@ -0,0 +1,19 @@
<!--
https://github.com/cozystack/cozystack/releases/tag/v1.0.2
-->
## Fixes
* **[platform] Suspend cozy-proxy if it conflicts with installer release during migration**: Added a check in the v0.41→v1.0 migration script to detect and automatically suspend the `cozy-proxy` HelmRelease when its `releaseName` is set to `cozystack`, which conflicts with the installer release and would cause `cozystack-operator` deletion during the upgrade ([**@kvaps**](https://github.com/kvaps) in #2128, #2130).
* **[platform] Fix off-by-one error in run-migrations script**: Fixed a bug in the migration runner where the first required migration was always skipped due to an off-by-one error in the migration range calculation, ensuring all upgrade steps execute correctly ([**@myasnikovdaniil**](https://github.com/myasnikovdaniil) in #2126, #2132).
* **[system] Fix Keycloak proxy configuration for v26.x**: Replaced the deprecated `KC_PROXY=edge` environment variable with `KC_PROXY_HEADERS=xforwarded` and `KC_HTTP_ENABLED=true` in the Keycloak StatefulSet template. `KC_PROXY` was removed in Keycloak 26.x, previously causing "Non-secure context detected" warnings and broken cookie handling when running behind a reverse proxy with TLS termination ([**@sircthulhu**](https://github.com/sircthulhu) in #2125, #2134).
* **[dashboard] Allow clearing instanceType field and preserve newlines in secret copy**: Added `allowEmpty: true` to the `instanceType` field in the VMInstance form so users can explicitly clear it to use custom KubeVirt resources without a named instance type. Also fixed newline preservation when copying secrets with CMD+C ([**@sircthulhu**](https://github.com/sircthulhu) in #2135, #2137).
* **[dashboard] Restore stock-instance sidebars for namespace-level pages**: Restored `stock-instance-api-form`, `stock-instance-api-table`, `stock-instance-builtin-form`, and `stock-instance-builtin-table` sidebar resources that were inadvertently removed in #2106. Without these sidebars, namespace-level pages such as Backup Plans rendered as empty pages with no interactive content ([**@sircthulhu**](https://github.com/sircthulhu) in #2136, #2138).
---
**Full Changelog**: https://github.com/cozystack/cozystack/compare/v1.0.1...v1.0.2

17
docs/changelogs/v1.0.3.md Normal file
View File

@@ -0,0 +1,17 @@
<!--
https://github.com/cozystack/cozystack/releases/tag/v1.0.3
-->
## Fixes
* **[platform] Fix package name conversion in migration script**: Fixed the `migrate-to-version-1.0.sh` script to correctly prepend the `cozystack.` prefix when converting `BUNDLE_DISABLE` and `BUNDLE_ENABLE` package name lists, ensuring packages are properly identified during the v0.41→v1.0 upgrade ([**@myasnikovdaniil**](https://github.com/myasnikovdaniil) in #2144, #2148).
## Documentation
* **[website] Add white labeling guide**: Added a comprehensive guide for configuring white labeling (branding) in Cozystack v1, covering Dashboard fields (`titleText`, `footerText`, `tenantText`, `logoText`, `logoSvg`, `iconSvg`) and Keycloak fields (`brandName`, `brandHtmlName`). Includes SVG preparation workflow with theme-aware template variables, portable base64 encoding, and migration notes from the v0 ConfigMap approach ([**@lexfrei**](https://github.com/lexfrei) in cozystack/website#441).
* **[website] Actualize backup and recovery documentation**: Reworked the backup and recovery docs to be user-focused, separating operator and tenant workflows. Added tenant-facing documentation for `BackupJob` and `Plan` resources and status inspection commands, and added a new Velero administration guide for operators covering storage credentials and backup storage configuration ([**@androndo**](https://github.com/androndo) in cozystack/website#434).
---
**Full Changelog**: https://github.com/cozystack/cozystack/compare/v1.0.2...v1.0.3

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env bats
@test "Create and Verify Seeweedfs Bucket" {
# Create the bucket resource
# Create the bucket resource with readwrite and readonly users
name='test'
kubectl apply -f - <<EOF
apiVersion: apps.cozystack.io/v1alpha1
@@ -9,21 +9,29 @@ kind: Bucket
metadata:
name: ${name}
namespace: tenant-test
spec: {}
spec:
users:
admin: {}
viewer:
readonly: true
EOF
# Wait for the bucket to be ready
kubectl -n tenant-test wait hr bucket-${name} --timeout=100s --for=condition=ready
kubectl -n tenant-test wait bucketclaims.objectstorage.k8s.io bucket-${name} --timeout=300s --for=jsonpath='{.status.bucketReady}'
kubectl -n tenant-test wait bucketaccesses.objectstorage.k8s.io bucket-${name} --timeout=300s --for=jsonpath='{.status.accessGranted}'
kubectl -n tenant-test wait bucketaccesses.objectstorage.k8s.io bucket-${name}-admin --timeout=300s --for=jsonpath='{.status.accessGranted}'
kubectl -n tenant-test wait bucketaccesses.objectstorage.k8s.io bucket-${name}-viewer --timeout=300s --for=jsonpath='{.status.accessGranted}'
# Get and decode credentials
kubectl -n tenant-test get secret bucket-${name} -ojsonpath='{.data.BucketInfo}' | base64 -d > bucket-test-credentials.json
# Get admin (readwrite) credentials
kubectl -n tenant-test get secret bucket-${name}-admin -ojsonpath='{.data.BucketInfo}' | base64 -d > bucket-admin-credentials.json
ADMIN_ACCESS_KEY=$(jq -r '.spec.secretS3.accessKeyID' bucket-admin-credentials.json)
ADMIN_SECRET_KEY=$(jq -r '.spec.secretS3.accessSecretKey' bucket-admin-credentials.json)
BUCKET_NAME=$(jq -r '.spec.bucketName' bucket-admin-credentials.json)
# Get credentials from the secret
ACCESS_KEY=$(jq -r '.spec.secretS3.accessKeyID' bucket-test-credentials.json)
SECRET_KEY=$(jq -r '.spec.secretS3.accessSecretKey' bucket-test-credentials.json)
BUCKET_NAME=$(jq -r '.spec.bucketName' bucket-test-credentials.json)
# Get viewer (readonly) credentials
kubectl -n tenant-test get secret bucket-${name}-viewer -ojsonpath='{.data.BucketInfo}' | base64 -d > bucket-viewer-credentials.json
VIEWER_ACCESS_KEY=$(jq -r '.spec.secretS3.accessKeyID' bucket-viewer-credentials.json)
VIEWER_SECRET_KEY=$(jq -r '.spec.secretS3.accessSecretKey' bucket-viewer-credentials.json)
# Start port-forwarding
bash -c 'timeout 100s kubectl port-forward service/seaweedfs-s3 -n tenant-root 8333:8333 > /dev/null 2>&1 &'
@@ -31,17 +39,33 @@ EOF
# Wait for port-forward to be ready
timeout 30 sh -ec 'until nc -z localhost 8333; do sleep 1; done'
# Set up MinIO alias with error handling
mc alias set local https://localhost:8333 $ACCESS_KEY $SECRET_KEY --insecure
# --- Test readwrite user (admin) ---
mc alias set rw-user https://localhost:8333 $ADMIN_ACCESS_KEY $ADMIN_SECRET_KEY --insecure
# Upload file to bucket
mc cp bucket-test-credentials.json $BUCKET_NAME/bucket-test-credentials.json
# Admin can upload
echo "readwrite test" > /tmp/rw-test.txt
mc cp --insecure /tmp/rw-test.txt rw-user/$BUCKET_NAME/rw-test.txt
# Verify file was uploaded
mc ls $BUCKET_NAME/bucket-test-credentials.json
# Admin can list
mc ls --insecure rw-user/$BUCKET_NAME/rw-test.txt
# Clean up uploaded file
mc rm $BUCKET_NAME/bucket-test-credentials.json
# Admin can download
mc cp --insecure rw-user/$BUCKET_NAME/rw-test.txt /tmp/rw-test-download.txt
# --- Test readonly user (viewer) ---
mc alias set ro-user https://localhost:8333 $VIEWER_ACCESS_KEY $VIEWER_SECRET_KEY --insecure
# Viewer can list
mc ls --insecure ro-user/$BUCKET_NAME/rw-test.txt
# Viewer can download
mc cp --insecure ro-user/$BUCKET_NAME/rw-test.txt /tmp/ro-test-download.txt
# Viewer cannot upload (must fail with Access Denied)
echo "readonly test" > /tmp/ro-test.txt
! mc cp --insecure /tmp/ro-test.txt ro-user/$BUCKET_NAME/ro-test.txt
# --- Cleanup ---
mc rm --insecure rw-user/$BUCKET_NAME/rw-test.txt
kubectl -n tenant-test delete bucket.apps.cozystack.io ${name}
}

View File

@@ -56,6 +56,30 @@ else
fi
echo ""
# Step 1: Check for cozy-proxy HelmRelease with conflicting releaseName
# In v0.41.x, cozy-proxy was incorrectly configured with releaseName "cozystack",
# which conflicts with the installer helm release name. If not suspended, cozy-proxy
# HelmRelease will overwrite the installer release and delete cozystack-operator.
COZY_PROXY_RELEASE_NAME=$(kubectl get hr -n "$NAMESPACE" cozy-proxy -o jsonpath='{.spec.releaseName}' 2>/dev/null || true)
if [ "$COZY_PROXY_RELEASE_NAME" = "cozystack" ]; then
echo "WARNING: HelmRelease cozy-proxy has releaseName 'cozystack', which conflicts"
echo "with the installer release. It must be suspended before proceeding, otherwise"
echo "it will overwrite the installer and delete cozystack-operator."
echo ""
read -p "Suspend HelmRelease cozy-proxy? (y/N) " -n 1 -r
echo ""
if [[ $REPLY =~ ^[Yy]$ ]]; then
kubectl -n "$NAMESPACE" patch hr cozy-proxy --type=merge --field-manager=flux-client-side-apply -p '{"spec":{"suspend":true}}'
echo "HelmRelease cozy-proxy suspended."
else
echo "ERROR: Cannot proceed with conflicting cozy-proxy HelmRelease active."
echo "Please suspend it manually:"
echo " kubectl -n $NAMESPACE patch hr cozy-proxy --type=merge -p '{\"spec\":{\"suspend\":true}}'"
exit 1
fi
echo ""
fi
# Read ConfigMap cozystack
echo "Reading ConfigMap cozystack..."
COZYSTACK_CM=$(kubectl get configmap -n "$NAMESPACE" cozystack -o json 2>/dev/null || echo "{}")
@@ -131,13 +155,13 @@ fi
if [ -z "$BUNDLE_DISABLE" ]; then
DISABLED_PACKAGES="[]"
else
DISABLED_PACKAGES=$(echo "$BUNDLE_DISABLE" | sed 's/,/\n/g' | awk 'BEGIN{print}{print " - "$0}')
DISABLED_PACKAGES=$(echo "$BUNDLE_DISABLE" | sed 's/,/\n/g' | awk 'BEGIN{print}{print " - cozystack."$0}')
fi
if [ -z "$BUNDLE_ENABLE" ]; then
ENABLED_PACKAGES="[]"
else
ENABLED_PACKAGES=$(echo "$BUNDLE_ENABLE" | sed 's/,/\n/g' | awk 'BEGIN{print}{print " - "$0}')
ENABLED_PACKAGES=$(echo "$BUNDLE_ENABLE" | sed 's/,/\n/g' | awk 'BEGIN{print}{print " - cozystack."$0}')
fi
if [ -z "$EXPOSE_SERVICES" ]; then
@@ -151,7 +175,7 @@ BUNDLE_NAME=$(echo "$BUNDLE_NAME" | sed 's/paas/isp/')
# Extract branding if available
BRANDING=$(echo "$BRANDING_CM" | jq -r '.data // {} | to_entries[] | "\(.key): \"\(.value)\""')
if [ -z "$BRANDING" ]; then
if [ -z "$BRANDING" ]; then
BRANDING="{}"
else
BRANDING=$(echo "$BRANDING" | awk 'BEGIN{print}{print " " $0}')

View File

@@ -195,6 +195,7 @@ func applyListInputOverrides(schema map[string]any, kind string, openAPIProps ma
"valueUri": "/api/clusters/{cluster}/k8s/apis/instancetype.kubevirt.io/v1beta1/virtualmachineclusterinstancetypes",
"keysToValue": []any{"metadata", "name"},
"keysToLabel": []any{"metadata", "name"},
"allowEmpty": true,
},
}
if prop, _ := openAPIProps["instanceType"].(map[string]any); prop != nil {
@@ -214,6 +215,34 @@ func applyListInputOverrides(schema map[string]any, kind string, openAPIProps ma
"keysToLabel": []any{"metadata", "name"},
},
}
case "ClickHouse", "Harbor", "HTTPCache", "Kubernetes", "MariaDB", "MongoDB",
"NATS", "OpenBAO", "Postgres", "Qdrant", "RabbitMQ", "Redis", "VMDisk":
specProps := ensureSchemaPath(schema, "spec")
specProps["storageClass"] = storageClassListInput()
case "FoundationDB":
storageProps := ensureSchemaPath(schema, "spec", "storage")
storageProps["storageClass"] = storageClassListInput()
case "Kafka":
kafkaProps := ensureSchemaPath(schema, "spec", "kafka")
kafkaProps["storageClass"] = storageClassListInput()
zkProps := ensureSchemaPath(schema, "spec", "zookeeper")
zkProps["storageClass"] = storageClassListInput()
}
}
// storageClassListInput returns a listInput field config for a storageClass dropdown
// backed by the cluster's available StorageClasses.
func storageClassListInput() map[string]any {
return map[string]any{
"type": "listInput",
"customProps": map[string]any{
"valueUri": "/api/clusters/{cluster}/k8s/apis/storage.k8s.io/v1/storageclasses",
"keysToValue": []any{"metadata", "name"},
"keysToLabel": []any{"metadata", "name"},
},
}
}

View File

@@ -202,6 +202,10 @@ func TestApplyListInputOverrides_VMInstance(t *testing.T) {
t.Errorf("expected valueUri %s, got %v", expectedURI, customProps["valueUri"])
}
if customProps["allowEmpty"] != true {
t.Errorf("expected allowEmpty true, got %v", customProps["allowEmpty"])
}
// Check disks[].name is a listInput
disks, ok := specProps["disks"].(map[string]any)
if !ok {
@@ -232,6 +236,72 @@ func TestApplyListInputOverrides_VMInstance(t *testing.T) {
}
}
func TestApplyListInputOverrides_StorageClassSimple(t *testing.T) {
for _, kind := range []string{
"ClickHouse", "Harbor", "HTTPCache", "Kubernetes", "MariaDB", "MongoDB",
"NATS", "OpenBAO", "Postgres", "Qdrant", "RabbitMQ", "Redis", "VMDisk",
} {
t.Run(kind, func(t *testing.T) {
schema := map[string]any{}
applyListInputOverrides(schema, kind, map[string]any{})
specProps := schema["properties"].(map[string]any)["spec"].(map[string]any)["properties"].(map[string]any)
sc, ok := specProps["storageClass"].(map[string]any)
if !ok {
t.Fatalf("storageClass not found in spec.properties for kind %s", kind)
}
assertStorageClassListInput(t, sc)
})
}
}
func TestApplyListInputOverrides_StorageClassFoundationDB(t *testing.T) {
schema := map[string]any{}
applyListInputOverrides(schema, "FoundationDB", map[string]any{})
storageProps := schema["properties"].(map[string]any)["spec"].(map[string]any)["properties"].(map[string]any)["storage"].(map[string]any)["properties"].(map[string]any)
sc, ok := storageProps["storageClass"].(map[string]any)
if !ok {
t.Fatal("storageClass not found in spec.storage.properties")
}
assertStorageClassListInput(t, sc)
}
func TestApplyListInputOverrides_StorageClassKafka(t *testing.T) {
schema := map[string]any{}
applyListInputOverrides(schema, "Kafka", map[string]any{})
specProps := schema["properties"].(map[string]any)["spec"].(map[string]any)["properties"].(map[string]any)
kafkaSC, ok := specProps["kafka"].(map[string]any)["properties"].(map[string]any)["storageClass"].(map[string]any)
if !ok {
t.Fatal("storageClass not found in spec.kafka.properties")
}
assertStorageClassListInput(t, kafkaSC)
zkSC, ok := specProps["zookeeper"].(map[string]any)["properties"].(map[string]any)["storageClass"].(map[string]any)
if !ok {
t.Fatal("storageClass not found in spec.zookeeper.properties")
}
assertStorageClassListInput(t, zkSC)
}
// assertStorageClassListInput verifies that a field is a correctly configured storageClass listInput.
func assertStorageClassListInput(t *testing.T, field map[string]any) {
t.Helper()
if field["type"] != "listInput" {
t.Errorf("expected type listInput, got %v", field["type"])
}
customProps, ok := field["customProps"].(map[string]any)
if !ok {
t.Fatal("customProps not found")
}
expectedURI := "/api/clusters/{cluster}/k8s/apis/storage.k8s.io/v1/storageclasses"
if customProps["valueUri"] != expectedURI {
t.Errorf("expected valueUri %s, got %v", expectedURI, customProps["valueUri"])
}
}
func TestApplyListInputOverrides_UnknownKind(t *testing.T) {
schema := map[string]any{}
applyListInputOverrides(schema, "SomeOtherKind", map[string]any{})

View File

@@ -307,6 +307,10 @@ func (m *Manager) buildExpectedResourceSet(crds []cozyv1alpha1.ApplicationDefini
"stock-project-builtin-table",
"stock-project-crd-form",
"stock-project-crd-table",
"stock-instance-api-form",
"stock-instance-api-table",
"stock-instance-builtin-form",
"stock-instance-builtin-table",
}
for _, sidebarID := range stockSidebars {
expected["Sidebar"][sidebarID] = true

View File

@@ -68,31 +68,46 @@ func (m *Manager) ensureMarketplacePanel(ctx context.Context, crd *cozyv1alpha1.
tags[i] = t
}
specMap := map[string]any{
"description": d.Description,
"name": displayName,
"type": "nonCrd",
"apiGroup": "apps.cozystack.io",
"apiVersion": "v1alpha1",
"plural": app.Plural, // e.g., "buckets"
"disabled": false,
"hidden": false,
"tags": tags,
"icon": d.Icon,
}
specBytes, err := json.Marshal(specMap)
if err != nil {
return reconcile.Result{}, err
}
_, err = controllerutil.CreateOrUpdate(ctx, m.Client, mp, func() error {
_, err := controllerutil.CreateOrUpdate(ctx, m.Client, mp, func() error {
if err := controllerutil.SetOwnerReference(crd, mp, m.Scheme); err != nil {
return err
}
// Add dashboard labels to dynamic resources
m.addDashboardLabels(mp, crd, ResourceTypeDynamic)
// Preserve user-set disabled/hidden values from existing resource
disabled := false
hidden := false
if mp.Spec.Raw != nil {
var existing map[string]any
if err := json.Unmarshal(mp.Spec.Raw, &existing); err == nil {
if v, ok := existing["disabled"].(bool); ok {
disabled = v
}
if v, ok := existing["hidden"].(bool); ok {
hidden = v
}
}
}
specMap := map[string]any{
"description": d.Description,
"name": displayName,
"type": "nonCrd",
"apiGroup": "apps.cozystack.io",
"apiVersion": "v1alpha1",
"plural": app.Plural, // e.g., "buckets"
"disabled": disabled,
"hidden": hidden,
"tags": tags,
"icon": d.Icon,
}
specBytes, err := json.Marshal(specMap)
if err != nil {
return err
}
// Only update spec if it's different to avoid unnecessary updates
newSpec := dashv1alpha1.ArbitrarySpec{
JSON: apiextv1.JSON{Raw: specBytes},

View File

@@ -38,6 +38,23 @@ func (m *Manager) ensureSidebar(ctx context.Context, crd *cozyv1alpha1.Applicati
}
all = crdList.Items
// 1b) Fetch all MarketplacePanels to determine which resources are hidden
hiddenResources := map[string]bool{}
var mpList dashv1alpha1.MarketplacePanelList
if err := m.List(ctx, &mpList, &client.ListOptions{}); err == nil {
for i := range mpList.Items {
mp := &mpList.Items[i]
if mp.Spec.Raw != nil {
var spec map[string]any
if err := json.Unmarshal(mp.Spec.Raw, &spec); err == nil {
if hidden, ok := spec["hidden"].(bool); ok && hidden {
hiddenResources[mp.Name] = true
}
}
}
}
}
// 2) Build category -> []item map (only for CRDs with spec.dashboard != nil)
type item struct {
Key string
@@ -63,6 +80,11 @@ func (m *Manager) ensureSidebar(ctx context.Context, crd *cozyv1alpha1.Applicati
plural := pickPlural(kind, def)
lowerKind := strings.ToLower(kind)
// Skip resources hidden via MarketplacePanel
if hiddenResources[def.Name] {
continue
}
// Check if this resource is a module
if def.Spec.Dashboard.Module {
// Special case: info should have its own keysAndTags, not be in modules
@@ -243,6 +265,11 @@ func (m *Manager) ensureSidebar(ctx context.Context, crd *cozyv1alpha1.Applicati
"stock-project-builtin-table",
"stock-project-crd-form",
"stock-project-crd-table",
// stock-instance sidebars (namespace-level pages after namespace is selected)
"stock-instance-api-form",
"stock-instance-api-table",
"stock-instance-builtin-form",
"stock-instance-builtin-table",
}
// Add details sidebars for all CRDs with dashboard config

View File

@@ -1924,12 +1924,12 @@ func CreateAllFactories() []*dashboardv1alpha1.Factory {
map[string]any{
"type": "EnrichedTable",
"data": map[string]any{
"id": "external-ips-table",
"fetchUrl": "/api/clusters/{2}/k8s/api/v1/namespaces/{3}/services",
"clusterNamePartOfUrl": "{2}",
"baseprefix": "/openapi-ui",
"customizationId": "factory-details-v1.services",
"pathToItems": []any{"items"},
"id": "external-ips-table",
"fetchUrl": "/api/clusters/{2}/k8s/api/v1/namespaces/{3}/services",
"cluster": "{2}",
"baseprefix": "/openapi-ui",
"customizationId": "factory-details-v1.services",
"pathToItems": ".items",
"fieldSelector": map[string]any{
"spec.type": "LoadBalancer",
},

View File

@@ -2,5 +2,4 @@ include ../../../hack/package.mk
generate:
cozyvalues-gen -v values.yaml -s values.schema.json -r README.md
yq -o json -i '.properties = {}' values.schema.json
../../../hack/update-crd.sh

View File

@@ -1,3 +1,13 @@
# S3 bucket
## Parameters
### Parameters
| Name | Description | Type | Value |
| ---------------------- | -------------------------------------------------------------------------- | ------------------- | ------- |
| `locking` | Provisions bucket from the `-lock` BucketClass (with object lock enabled). | `bool` | `false` |
| `storagePool` | Selects a specific BucketClass by storage pool name. | `string` | `""` |
| `users` | Users configuration map. | `map[string]object` | `{}` |
| `users[name].readonly` | Whether the user has read-only access. | `bool` | `false` |

View File

@@ -1,29 +1,22 @@
{{- $seaweedfs := .Values._namespace.seaweedfs }}
{{- $pool := .Values.storagePool }}
apiVersion: objectstorage.k8s.io/v1alpha1
kind: BucketClaim
metadata:
name: {{ .Release.Name }}
spec:
bucketClassName: {{ $seaweedfs }}
bucketClassName: {{ $seaweedfs }}{{- if $pool }}-{{ $pool }}{{- end }}{{- if .Values.locking }}-lock{{- end }}
protocols:
- s3
{{- range $name, $user := .Values.users }}
---
apiVersion: objectstorage.k8s.io/v1alpha1
kind: BucketAccess
metadata:
name: {{ .Release.Name }}
name: {{ $.Release.Name }}-{{ $name }}
spec:
bucketAccessClassName: {{ $seaweedfs }}
bucketClaimName: {{ .Release.Name }}
credentialsSecretName: {{ .Release.Name }}
protocol: s3
---
apiVersion: objectstorage.k8s.io/v1alpha1
kind: BucketAccess
metadata:
name: {{ .Release.Name }}-readonly
spec:
bucketAccessClassName: {{ $seaweedfs }}-readonly
bucketClaimName: {{ .Release.Name }}
credentialsSecretName: {{ .Release.Name }}-readonly
bucketAccessClassName: {{ $seaweedfs }}{{- if $pool }}-{{ $pool }}{{- end }}{{- if $user.readonly }}-readonly{{- end }}
bucketClaimName: {{ $.Release.Name }}
credentialsSecretName: {{ $.Release.Name }}-{{ $name }}
protocol: s3
{{- end }}

View File

@@ -8,9 +8,9 @@ rules:
resources:
- secrets
resourceNames:
- {{ .Release.Name }}
- {{ .Release.Name }}-credentials
- {{ .Release.Name }}-readonly
{{- range $name, $user := .Values.users }}
- {{ $.Release.Name }}-{{ $name }}-credentials
{{- end }}
verbs: ["get", "list", "watch"]
- apiGroups:
- networking.k8s.io

View File

@@ -23,3 +23,4 @@ spec:
name: cozystack-values
values:
bucketName: {{ .Release.Name }}
users: {{ .Values.users | toJson }}

View File

@@ -1,5 +1,30 @@
{
"title": "Chart Values",
"type": "object",
"properties": {}
}
"properties": {
"locking": {
"description": "Provisions bucket from the `-lock` BucketClass (with object lock enabled).",
"type": "boolean",
"default": false
},
"storagePool": {
"description": "Selects a specific BucketClass by storage pool name.",
"type": "string",
"default": ""
},
"users": {
"description": "Users configuration map.",
"type": "object",
"default": {},
"additionalProperties": {
"type": "object",
"properties": {
"readonly": {
"description": "Whether the user has read-only access.",
"type": "boolean"
}
}
}
}
}
}

View File

@@ -1 +1,11 @@
{}
## @param {bool} locking=false - Provisions bucket from the `-lock` BucketClass (with object lock enabled).
locking: false
## @param {string} [storagePool] - Selects a specific BucketClass by storage pool name.
storagePool: ""
## @typedef {struct} User - Bucket user configuration.
## @field {bool} [readonly] - Whether the user has read-only access.
## @param {map[string]User} users - Users configuration map.
users: {}

View File

@@ -1 +1 @@
ghcr.io/cozystack/cozystack/kubevirt-csi-driver:0.0.0@sha256:434aa3b8e2a3cbf6681426b174e1c4fde23bafd12a6cccd046b5cb1749092ec4
ghcr.io/cozystack/cozystack/kubevirt-csi-driver:0.0.0@sha256:1c8c842277f45f189a5c645fcf7b2023c8ed7189f44029ce8b988019000da14c

View File

@@ -3,6 +3,7 @@ cilium:
k8sServiceHost: {{ .Release.Name }}.{{ .Release.Namespace }}.svc
k8sServicePort: 6443
routingMode: tunnel
MTU: 1350
enableIPv4Masquerade: true
ipv4NativeRoutingCIDR: ""
{{- if $.Values.addons.gatewayAPI.enabled }}

View File

@@ -4,4 +4,4 @@ description: Managed RabbitMQ service
icon: /logos/rabbitmq.svg
type: application
version: 0.0.0 # Placeholder, the actual version will be automatically set during the build process
appVersion: "3.13.2"
appVersion: "4.2.4"

View File

@@ -3,3 +3,7 @@ include ../../../hack/package.mk
generate:
cozyvalues-gen -v values.yaml -s values.schema.json -r README.md
../../../hack/update-crd.sh
update:
hack/update-versions.sh
make generate

View File

@@ -23,6 +23,7 @@ The service utilizes official RabbitMQ operator. This ensures the reliability an
| `size` | Persistent Volume Claim size available for application data. | `quantity` | `10Gi` |
| `storageClass` | StorageClass used to store the data. | `string` | `""` |
| `external` | Enable external access from outside the cluster. | `bool` | `false` |
| `version` | RabbitMQ major.minor version to deploy | `string` | `v4.2` |
### Application-specific parameters

View File

@@ -0,0 +1,4 @@
"v4.2": "4.2.4"
"v4.1": "4.1.8"
"v4.0": "4.0.9"
"v3.13": "3.13.7"

View File

@@ -0,0 +1,129 @@
#!/usr/bin/env bash
set -o errexit
set -o nounset
set -o pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
RABBITMQ_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
VALUES_FILE="${RABBITMQ_DIR}/values.yaml"
VERSIONS_FILE="${RABBITMQ_DIR}/files/versions.yaml"
GITHUB_API_URL="https://api.github.com/repos/rabbitmq/rabbitmq-server/releases"
# Check if jq is installed
if ! command -v jq &> /dev/null; then
echo "Error: jq is not installed. Please install jq and try again." >&2
exit 1
fi
# Fetch releases from GitHub API
echo "Fetching releases from GitHub API..."
RELEASES_JSON=$(curl -sSL "${GITHUB_API_URL}?per_page=100")
if [ -z "$RELEASES_JSON" ]; then
echo "Error: Could not fetch releases from GitHub API" >&2
exit 1
fi
# Extract stable release tags (format: v3.13.7, v4.0.3, etc.)
# Filter out pre-releases and draft releases
RELEASE_TAGS=$(echo "$RELEASES_JSON" | jq -r '.[] | select(.prerelease == false) | select(.draft == false) | .tag_name' | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | sort -V)
if [ -z "$RELEASE_TAGS" ]; then
echo "Error: Could not find any stable release tags" >&2
exit 1
fi
echo "Found release tags: $(echo "$RELEASE_TAGS" | tr '\n' ' ')"
# Supported major.minor versions (newest first)
# We support the last few minor releases of each active major
SUPPORTED_MAJORS=("4.2" "4.1" "4.0" "3.13")
# Build versions map: major.minor -> latest patch version
declare -A VERSION_MAP
MAJOR_VERSIONS=()
for major_minor in "${SUPPORTED_MAJORS[@]}"; do
# Find the latest patch version for this major.minor
MATCHING=$(echo "$RELEASE_TAGS" | grep -E "^v${major_minor//./\\.}\.[0-9]+$" | tail -n1)
if [ -n "$MATCHING" ]; then
# Strip the 'v' prefix for the value (Docker tag format is e.g. 3.13.7)
TAG_VERSION="${MATCHING#v}"
VERSION_MAP["v${major_minor}"]="${TAG_VERSION}"
MAJOR_VERSIONS+=("v${major_minor}")
echo "Found version: v${major_minor} -> ${TAG_VERSION}"
else
echo "Warning: No stable releases found for ${major_minor}, skipping..." >&2
fi
done
if [ ${#MAJOR_VERSIONS[@]} -eq 0 ]; then
echo "Error: No matching versions found" >&2
exit 1
fi
echo "Major versions to add: ${MAJOR_VERSIONS[*]}"
# Create/update versions.yaml file
echo "Updating $VERSIONS_FILE..."
{
for major_ver in "${MAJOR_VERSIONS[@]}"; do
echo "\"${major_ver}\": \"${VERSION_MAP[$major_ver]}\""
done
} > "$VERSIONS_FILE"
echo "Successfully updated $VERSIONS_FILE"
# Update values.yaml - enum with major.minor versions only
TEMP_FILE=$(mktemp)
trap "rm -f $TEMP_FILE" EXIT
# Build new version section
NEW_VERSION_SECTION="## @enum {string} Version"
for major_ver in "${MAJOR_VERSIONS[@]}"; do
NEW_VERSION_SECTION="${NEW_VERSION_SECTION}
## @value $major_ver"
done
NEW_VERSION_SECTION="${NEW_VERSION_SECTION}
## @param {Version} version - RabbitMQ major.minor version to deploy
version: ${MAJOR_VERSIONS[0]}"
# Check if version section already exists
if grep -q "^## @enum {string} Version" "$VALUES_FILE"; then
# Version section exists, update it using awk
echo "Updating existing version section in $VALUES_FILE..."
awk -v new_section="$NEW_VERSION_SECTION" '
/^## @enum {string} Version/ {
in_section = 1
print new_section
next
}
in_section && /^version: / {
in_section = 0
next
}
in_section {
next
}
{ print }
' "$VALUES_FILE" > "$TEMP_FILE.tmp"
mv "$TEMP_FILE.tmp" "$VALUES_FILE"
else
# Version section doesn't exist, insert it before Application-specific parameters section
echo "Inserting new version section in $VALUES_FILE..."
awk -v new_section="$NEW_VERSION_SECTION" '
/^## @section Application-specific parameters/ {
print new_section
print ""
}
{ print }
' "$VALUES_FILE" > "$TEMP_FILE.tmp"
mv "$TEMP_FILE.tmp" "$VALUES_FILE"
fi
echo "Successfully updated $VALUES_FILE with major.minor versions: ${MAJOR_VERSIONS[*]}"

View File

@@ -0,0 +1,8 @@
{{- define "rabbitmq.versionMap" }}
{{- $versionMap := .Files.Get "files/versions.yaml" | fromYaml }}
{{- if not (hasKey $versionMap .Values.version) }}
{{- printf `RabbitMQ version %s is not supported, allowed versions are %s` $.Values.version (keys $versionMap) | fail }}
{{- end }}
{{- index $versionMap .Values.version }}
{{- end }}

View File

@@ -7,6 +7,7 @@ metadata:
app.kubernetes.io/managed-by: {{ .Release.Service }}
spec:
replicas: {{ .Values.replicas }}
image: 'rabbitmq:{{ include "rabbitmq.versionMap" $ }}-management'
{{- if .Values.external }}
service:
type: LoadBalancer

View File

@@ -92,6 +92,17 @@
}
}
},
"version": {
"description": "RabbitMQ major.minor version to deploy",
"type": "string",
"default": "v4.2",
"enum": [
"v4.2",
"v4.1",
"v4.0",
"v3.13"
]
},
"vhosts": {
"description": "Virtual hosts configuration map.",
"type": "object",

View File

@@ -34,6 +34,15 @@ storageClass: ""
external: false
##
## @enum {string} Version
## @value v4.2
## @value v4.1
## @value v4.0
## @value v3.13
## @param {Version} version - RabbitMQ major.minor version to deploy
version: v4.2
## @section Application-specific parameters
##

View File

@@ -34,6 +34,12 @@ spec:
metadata:
annotations:
kubevirt.io/allow-pod-bridge-network-live-migration: "true"
{{- $ovnIPName := printf "%s.%s" (include "virtual-machine.fullname" .) .Release.Namespace }}
{{- $ovnIP := lookup "kubeovn.io/v1" "IP" "" $ovnIPName }}
{{- if $ovnIP }}
ovn.kubernetes.io/mac_address: {{ $ovnIP.spec.macAddress | quote }}
ovn.kubernetes.io/ip_address: {{ $ovnIP.spec.ipAddress | quote }}
{{- end }}
labels:
{{- include "virtual-machine.labels" . | nindent 8 }}
spec:

View File

@@ -1,9 +1,9 @@
cozystackOperator:
# Deployment variant: talos, generic, hosted
variant: talos
image: ghcr.io/cozystack/cozystack/cozystack-operator:v1.0.0@sha256:9e5229764b6077809a1c16566881a524c33e8986e36597e6833f8857a7e6a335
image: ghcr.io/cozystack/cozystack/cozystack-operator:v1.1.2@sha256:f58d32634f6e01f56aae4d5f21947985dd90fca7042ddbd0a4dbcc636da28fc7
platformSourceUrl: 'oci://ghcr.io/cozystack/cozystack/cozystack-packages'
platformSourceRef: 'digest=sha256:ef3e4ba7d21572a61794d8be594805f063aa04f4a8c3753351fc89c7804d337e'
platformSourceRef: 'digest=sha256:69b19e0e7a97944668134b4131de35cc08ab7d86a24554a4fbd11efb74aee2b4'
# Generic variant configuration (only used when cozystackOperator.variant=generic)
cozystack:
# Kubernetes API server host (IP only, no protocol/port)

View File

@@ -0,0 +1,46 @@
#!/bin/sh
# Migration 34 --> 35
# Backfill spec.version on rabbitmq.apps.cozystack.io resources.
#
# Before this migration RabbitMQ had no user-selectable version; the
# operator always used its built-in default image (v3.x). A version field
# was added in this release. Without this migration every existing cluster
# would be upgraded to the new default (v4.2) on the next reconcile.
#
# Set spec.version to "v3.13" for any rabbitmq app resource that does not
# already have it set.
set -euo pipefail
DEFAULT_VERSION="v3.13"
# Skip if the CRD does not exist (rabbitmq was never installed)
if ! kubectl api-resources --api-group=apps.cozystack.io -o name 2>/dev/null | grep -q '^rabbitmqs\.'; then
echo "CRD rabbitmqs.apps.cozystack.io not found, skipping migration"
kubectl create configmap -n cozy-system cozystack-version \
--from-literal=version=35 --dry-run=client -o yaml | kubectl apply -f-
exit 0
fi
RABBITMQS=$(kubectl get rabbitmqs.apps.cozystack.io -A -o jsonpath='{range .items[*]}{.metadata.namespace}/{.metadata.name}{"\n"}{end}')
for resource in $RABBITMQS; do
NS="${resource%%/*}"
APP_NAME="${resource##*/}"
# Skip if spec.version is already set
CURRENT_VER=$(kubectl get rabbitmqs.apps.cozystack.io -n "$NS" "$APP_NAME" \
-o jsonpath='{.spec.version}')
if [ -n "$CURRENT_VER" ]; then
echo "SKIP $NS/$APP_NAME: spec.version already set to '$CURRENT_VER'"
continue
fi
echo "Patching rabbitmq/$APP_NAME in $NS: setting version=$DEFAULT_VERSION"
kubectl patch rabbitmqs.apps.cozystack.io -n "$NS" "$APP_NAME" --type=merge \
--patch "{\"spec\":{\"version\":\"${DEFAULT_VERSION}\"}}"
done
# Stamp version
kubectl create configmap -n cozy-system cozystack-version \
--from-literal=version=35 --dry-run=client -o yaml | kubectl apply -f-

View File

@@ -24,7 +24,7 @@ if [ "$CURRENT_VERSION" -ge "$TARGET_VERSION" ]; then
fi
# Run migrations sequentially from current version to target version
for i in $(seq $((CURRENT_VERSION + 1)) $TARGET_VERSION); do
for i in $(seq $CURRENT_VERSION $((TARGET_VERSION - 1))); do
if [ -f "/migrations/$i" ]; then
echo "Running migration $i"
chmod +x /migrations/$i

View File

@@ -5,8 +5,8 @@ sourceRef:
path: /
migrations:
enabled: false
image: ghcr.io/cozystack/cozystack/platform-migrations:v1.0.0@sha256:68dabdebc38ac439228ae07031cc70e0fa184a24bd4e5b3b22c17466b2a55201
targetVersion: 34
image: ghcr.io/cozystack/cozystack/platform-migrations:v1.1.2@sha256:bcbe612879cecd2ae1cef91dfff6d34d009c2f7de6592145c04a2d6d21b28f4b
targetVersion: 35
# Bundle deployment configuration
bundles:
system:

View File

@@ -1,2 +1,2 @@
e2e:
image: ghcr.io/cozystack/cozystack/e2e-sandbox:v1.0.0@sha256:0eae9f519669667d60b160ebb93c127843c470ad9ca3447fceaa54604503a7ba
image: ghcr.io/cozystack/cozystack/e2e-sandbox:v1.1.2@sha256:0eae9f519669667d60b160ebb93c127843c470ad9ca3447fceaa54604503a7ba

View File

@@ -1 +1 @@
ghcr.io/cozystack/cozystack/matchbox:v1.0.0@sha256:c48eb7b23f01a8ff58d409fdb51c88e771f819cb914eee03da89471e62302f33
ghcr.io/cozystack/cozystack/matchbox:v1.1.2@sha256:6314c9058fb2a954f9293b7c3b2ce72fe398f48e5bbc9078a326d1d162a297c7

View File

@@ -104,6 +104,7 @@ spec:
- {{ .Release.Name }}
secretName: etcd-peer-ca-tls
privateKey:
rotationPolicy: Never
algorithm: RSA
size: 4096
issuerRef:
@@ -130,6 +131,7 @@ spec:
- {{ .Release.Name }}
secretName: etcd-ca-tls
privateKey:
rotationPolicy: Never
algorithm: RSA
size: 4096
issuerRef:

View File

@@ -1 +1 @@
ghcr.io/cozystack/cozystack/objectstorage-sidecar:v1.0.0@sha256:2a3595cd88b30af55b2000d3ca204899beecef0012b0e0402754c3914aad1f7f
ghcr.io/cozystack/cozystack/objectstorage-sidecar:v1.1.2@sha256:2a3595cd88b30af55b2000d3ca204899beecef0012b0e0402754c3914aad1f7f

View File

@@ -7,10 +7,32 @@ metadata:
driverName: {{ .Release.Namespace }}.seaweedfs.objectstorage.k8s.io
deletionPolicy: Delete
---
kind: BucketClass
apiVersion: objectstorage.k8s.io/v1alpha1
metadata:
name: {{ .Release.Namespace }}-lock
driverName: {{ .Release.Namespace }}.seaweedfs.objectstorage.k8s.io
deletionPolicy: Retain
parameters:
objectLockEnabled: "true"
objectLockRetentionMode: "COMPLIANCE"
objectLockRetentionDays: "365"
---
kind: BucketAccessClass
apiVersion: objectstorage.k8s.io/v1alpha1
metadata:
name: {{ .Release.Namespace }}
driverName: {{ .Release.Namespace }}.seaweedfs.objectstorage.k8s.io
authenticationType: KEY
parameters:
accessPolicy: readwrite
---
kind: BucketAccessClass
apiVersion: objectstorage.k8s.io/v1alpha1
metadata:
name: {{ .Release.Namespace }}-readonly
driverName: {{ .Release.Namespace }}.seaweedfs.objectstorage.k8s.io
authenticationType: KEY
parameters:
accessPolicy: readonly
{{- end }}

View File

@@ -32,8 +32,8 @@
{{- if not (regexMatch "^[a-z0-9]([a-z0-9-]*[a-z0-9])?$" $poolName) }}
{{- fail (printf "volume.pools key '%s' must be a valid DNS label (lowercase alphanumeric and hyphens, no dots)." $poolName) }}
{{- end }}
{{- if or (hasSuffix "-worm" $poolName) (hasSuffix "-readonly" $poolName) }}
{{- fail (printf "volume.pools key '%s' must not end with '-worm' or '-readonly' (reserved suffixes for COSI resources)." $poolName) }}
{{- if or (hasSuffix "-lock" $poolName) (hasSuffix "-readonly" $poolName) }}
{{- fail (printf "volume.pools key '%s' must not end with '-lock' or '-readonly' (reserved suffixes for COSI resources)." $poolName) }}
{{- end }}
{{- if not $pool.diskType }}
{{- fail (printf "volume.pools.%s.diskType is required." $poolName) }}
@@ -52,8 +52,8 @@
{{- if not (regexMatch "^[a-z0-9]([a-z0-9-]*[a-z0-9])?$" $poolName) }}
{{- fail (printf "volume.zones.%s.pools key '%s' must be a valid DNS label." $zoneName $poolName) }}
{{- end }}
{{- if or (hasSuffix "-worm" $poolName) (hasSuffix "-readonly" $poolName) }}
{{- fail (printf "volume.zones.%s.pools key '%s' must not end with '-worm' or '-readonly' (reserved suffixes for COSI resources)." $zoneName $poolName) }}
{{- if or (hasSuffix "-lock" $poolName) (hasSuffix "-readonly" $poolName) }}
{{- fail (printf "volume.zones.%s.pools key '%s' must not end with '-lock' or '-readonly' (reserved suffixes for COSI resources)." $zoneName $poolName) }}
{{- end }}
{{- if not $pool.diskType }}
{{- fail (printf "volume.zones.%s.pools.%s.diskType is required." $zoneName $poolName) }}

View File

@@ -25,14 +25,14 @@ parameters:
kind: BucketClass
apiVersion: objectstorage.k8s.io/v1alpha1
metadata:
name: {{ $.Release.Namespace }}-{{ $poolName }}-worm
name: {{ $.Release.Namespace }}-{{ $poolName }}-lock
driverName: {{ $.Release.Namespace }}.seaweedfs.objectstorage.k8s.io
deletionPolicy: Retain
parameters:
disk: {{ $diskType }}
objectLockEnabled: "true"
objectLockRetentionMode: COMPLIANCE
objectLockRetentionDays: "36500"
objectLockRetentionMode: "COMPLIANCE"
objectLockRetentionDays: "365"
---
kind: BucketAccessClass
apiVersion: objectstorage.k8s.io/v1alpha1

View File

@@ -3,30 +3,14 @@ apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: backups.cozystack.io:core-controller
rules:
# Plan: reconcile schedule and update status
- apiGroups: ["backups.cozystack.io"]
resources: ["plans"]
verbs: ["get", "list", "watch"]
- apiGroups: ["backups.cozystack.io"]
resources: ["backupjobs"]
verbs: ["create", "get", "list", "watch", "update", "patch"]
- apiGroups: ["backups.cozystack.io"]
resources: ["backupjobs/status"]
resources: ["plans/status"]
verbs: ["get", "update", "patch"]
# BackupJob: create when schedule fires (status is updated by backupstrategy-controller)
- apiGroups: ["backups.cozystack.io"]
resources: ["backups"]
resources: ["backupjobs"]
verbs: ["create", "get", "list", "watch"]
- apiGroups: ["apps.cozystack.io"]
resources: ["buckets", "bucketaccesses", "virtualmachines"]
verbs: ["get", "list", "watch"]
- apiGroups: ["objectstorage.k8s.io"]
resources: ["buckets", "bucketaccesses"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["secrets"]
verbs: ["create", "get", "list", "watch", "update", "patch"]
- apiGroups: ["kubevirt.io"]
resources: ["virtualmachines"]
verbs: ["get", "list", "watch"]
- apiGroups: ["velero.io"]
resources: ["backups", "backupstoragelocations", "volumesnapshotlocations", "restores"]
verbs: ["create", "get", "list", "watch", "update", "patch"]

View File

@@ -1,5 +1,5 @@
backupController:
image: "ghcr.io/cozystack/cozystack/backup-controller:v1.0.0@sha256:e1a6c8ac7ba64442812464b59c53e782e373a339c18b379c2692921b44c6edb5"
image: "ghcr.io/cozystack/cozystack/backup-controller:v1.1.2@sha256:9ebf859014fa2c567299197134309ea0c46c50f354126ce437c321738e02522a"
replicas: 2
debug: false
metrics:

View File

@@ -3,9 +3,34 @@ apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: backups.cozystack.io:strategy-controller
rules:
# Strategy types (Velero, Job)
- apiGroups: ["strategy.backups.cozystack.io"]
resources: ["*"]
verbs: ["get", "list", "watch"]
# BackupClass: resolve strategy per application
- apiGroups: ["backups.cozystack.io"]
resources: ["backupclasses"]
verbs: ["get", "list", "watch"]
# BackupJob / RestoreJob: reconcile and update status
- apiGroups: ["backups.cozystack.io"]
resources: ["backupjobs", "restorejobs"]
verbs: ["get", "list", "watch"]
- apiGroups: ["backups.cozystack.io"]
resources: ["backupjobs/status", "restorejobs/status"]
verbs: ["get", "update", "patch"]
# Backup: create after Velero backup completes
- apiGroups: ["backups.cozystack.io"]
resources: ["backups"]
verbs: ["create", "get", "list", "watch"]
# Application refs (e.g. VMInstance, VirtualMachine) for backup/restore scope
- apiGroups: ["apps.cozystack.io"]
resources: ["*"]
verbs: ["get", "list", "watch"]
# Velero Backup/Restore in cozy-velero namespace
- apiGroups: ["velero.io"]
resources: ["backups", "restores"]
verbs: ["create", "get", "list", "watch", "update", "patch"]
# Leader election (--leader-elect)
- apiGroups: ["coordination.k8s.io"]
resources: ["leases"]
verbs: ["get", "list", "watch", "create", "update", "patch"]

View File

@@ -1,5 +1,5 @@
backupStrategyController:
image: "ghcr.io/cozystack/cozystack/backupstrategy-controller:v1.0.0@sha256:29735d945c69c6bbaab21068bf4ea30f6b63f4c71a7a8d95590f370abcb4b328"
image: "ghcr.io/cozystack/cozystack/backupstrategy-controller:v1.1.2@sha256:7e26acdb68d7088d0f0f467511d4fff769b70a6db9e6f24f7791e3a22d6ad40e"
replicas: 2
debug: false
metrics:

View File

@@ -8,7 +8,7 @@ spec:
plural: buckets
singular: bucket
openAPISchema: |-
{"title":"Chart Values","type":"object","properties":{}}
{"title":"Chart Values","type":"object","properties":{"locking":{"description":"Provisions bucket from the `-lock` BucketClass (with object lock enabled).","type":"boolean","default":false},"storagePool":{"description":"Selects a specific BucketClass by storage pool name.","type":"string","default":""},"users":{"description":"Users configuration map.","type":"object","default":{},"additionalProperties":{"type":"object","properties":{"readonly":{"description":"Whether the user has read-only access.","type":"boolean"}}}}}}
release:
prefix: bucket-
labels:
@@ -26,14 +26,14 @@ spec:
tags:
- storage
icon: PHN2ZyB3aWR0aD0iMTQ0IiBoZWlnaHQ9IjE0NCIgdmlld0JveD0iMCAwIDE0NCAxNDQiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIxNDQiIGhlaWdodD0iMTQ0IiByeD0iMjQiIGZpbGw9InVybCgjcGFpbnQwX2xpbmVhcl82ODNfMzA5MSkiLz4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik03MiAzMC4xNjQxTDExNy45ODMgMzYuNzc4OVY0MC42NzM5QzExNy45ODMgNDYuNDY1MyA5Ny4zODYyIDUxLjEzMzIgNzEuOTgyNyA1MS4xMzMyQzQ2LjU3OTIgNTEuMTMzMiAyNiA0Ni40NjUzIDI2IDQwLjY3MzlWMzYuNDQzMUw3MiAzMC4xNjQxWk03MiA1OC4yNjc4QzkxLjIwODQgNTguMjY3OCAxMDcuNjU4IDU1LjU5ODYgMTE0LjU0NyA1MS44MDQ4TDExNi44MDMgNDguMTExTDExNy43MjMgNDQuNzUzVjQ4LjkxNzFMMTAyLjY3OSAxMTEuMDMzQzEwMi42NzkgMTE0Ljg5NSA4OC45NTMzIDExOCA3Mi4wMTcyIDExOEM1NS4wODEyIDExOCA0MS4zNzQzIDExNC44OTUgNDEuMzc0MyAxMTEuMDMzTDI2LjMzIDQ4LjkxNzFWNDQuODM2OUwyOS44MDA3IDUxLjkzODJDMzYuNzA2NSA1NS42NjUzIDUyLjk5OTcgNTguMjY3OCA3MiA1OC4yNjc4WiIgZmlsbD0iIzhDMzEyMyIvPgo8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTcyLjAwMDMgMjZDOTcuNDAzOCAyNiAxMTggMzAuNjgzOSAxMTggMzYuNDQyQzExOCA0Mi4yIDk3LjM4NjYgNDYuODUwNyA3Mi4wMDAzIDQ2Ljg1MDdDNDYuNjE0MSA0Ni44NTA3IDI2LjAxNzYgNDIuMjM0NSAyNi4wMTc2IDM2LjQ0MkMyNi4wMTc2IDMwLjY0OTQgNDYuNTk2OCAyNiA3Mi4wMDAzIDI2Wk03Mi4wMDAzIDU0LjEwMzdDOTUuNjg1NyA1NC4xMDM3IDExNS4xNzIgNTAuMDU4IDExNy43MDYgNDQuODE5N0wxMDIuNjYyIDEwNi45MzdDMTAyLjY2MiAxMTAuNzk5IDg4LjkzNjQgMTEzLjkwNSA3Mi4wMDAzIDExMy45MDVDNTUuMDY0MyAxMTMuOTA1IDQxLjMzOSAxMTAuODE2IDQxLjMzOSAxMDYuOTU0TDI2LjI5NTkgNDQuODM3QzI4Ljg0NjYgNTAuMDU4IDQ4LjMzMzMgNTQuMTAzNyA3Mi4wMDAzIDU0LjEwMzdaIiBmaWxsPSIjRTA1MjQzIi8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNNjEuMTcyNSA2MC4wMjkzSDgxLjA5MjhWNzkuMTY3Nkg2MS4xNzI1VjYwLjAyOTNaTTQ1LjMzMDEgOTUuMzY4OEM0NS4zMzAxIDkwLjE0MiA0OS43MTA0IDg1LjkzNDIgNTUuMTUxMSA4NS45MzQyQzYwLjU5MTcgODUuOTM0MiA2NC45NzIxIDkwLjE0MiA2NC45NzIxIDk1LjM2ODhDNjQuOTcyMSAxMDAuNTk2IDYwLjU5MTcgMTA0LjgwMyA1NS4xNTExIDEwNC44MDNDNDkuNzEwNCAxMDQuODAzIDQ1LjMzMDEgMTAwLjU5NiA0NS4zMzAxIDk1LjM2ODhaTTk2LjQ0ODcgMTA0LjM2OEg3Ni43NzIyTDg2LjYxMDUgODYuNzczN0w5Ni40NDg3IDEwNC4zNjhaIiBmaWxsPSJ3aGl0ZSIvPgo8ZGVmcz4KPGxpbmVhckdyYWRpZW50IGlkPSJwYWludDBfbGluZWFyXzY4M18zMDkxIiB4MT0iMCIgeTE9IjAiIHgyPSIxNTEiIHkyPSIxODAiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj4KPHN0b3Agc3RvcC1jb2xvcj0iI0ZGRjBFRSIvPgo8c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiNFQzg4N0QiLz4KPC9saW5lYXJHcmFkaWVudD4KPC9kZWZzPgo8L3N2Zz4K
keysOrder: [["apiVersion"], ["appVersion"], ["kind"], ["metadata"], ["metadata", "name"]]
keysOrder: [["apiVersion"], ["appVersion"], ["kind"], ["metadata"], ["metadata", "name"], ["spec", "locking"], ["spec", "storagePool"], ["spec", "users"]]
secrets:
exclude: []
include:
- resourceNames:
- bucket-{{ .name }}
- bucket-{{ .name }}-credentials
- bucket-{{ .name }}-readonly
- matchLabels:
apps.cozystack.io/user-secret: "true"
ingresses:
exclude: []
include:

View File

@@ -1 +1 @@
ghcr.io/cozystack/cozystack/s3manager:v0.5.0@sha256:279008f87460d709e99ed25ee8a1e4568a290bb9afa0e3dd3a06d524163a132b
ghcr.io/cozystack/cozystack/s3manager:v0.5.0@sha256:20a6e3113b5c2005a3de7772da51a0242bec93ba1bd8936f912d958ef0d70214

View File

@@ -9,6 +9,7 @@ WORKDIR /usr/src/app
RUN wget -O- https://github.com/cloudlena/s3manager/archive/9a7c8e446b422f8973b8c461990f39fdafee9c27.tar.gz | tar -xzf- --strip 1
ADD cozystack.patch /
RUN git apply /cozystack.patch
RUN go mod tidy
RUN GOOS=$TARGETOS GOARCH=$TARGETARCH CGO_ENABLED=0 go build -ldflags="-s -w" -a -installsuffix cgo -o bin/s3manager
FROM docker.io/library/alpine:latest

View File

@@ -1,3 +1,235 @@
diff --git a/go.mod b/go.mod
index b5d8540..6ede8e8 100644
--- a/go.mod
+++ b/go.mod
@@ -1,10 +1,11 @@
module github.com/cloudlena/s3manager
-go 1.22.5
+go 1.23
require (
github.com/cloudlena/adapters v0.0.0-20240708203353-a39be02cc801
github.com/gorilla/mux v1.8.1
+ github.com/gorilla/sessions v1.4.0
github.com/matryer/is v1.4.1
github.com/minio/minio-go/v7 v7.0.74
github.com/spf13/viper v1.19.0
@@ -16,6 +17,7 @@ require (
github.com/go-ini/ini v1.67.0 // indirect
github.com/goccy/go-json v0.10.3 // indirect
github.com/google/uuid v1.6.0 // indirect
+ github.com/gorilla/securecookie v1.1.2 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/klauspost/compress v1.17.9 // indirect
github.com/klauspost/cpuid/v2 v2.2.8 // indirect
diff --git a/go.sum b/go.sum
index 1ea1b16..d7866ce 100644
--- a/go.sum
+++ b/go.sum
@@ -16,10 +16,16 @@ github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
+github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
+github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
+github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
+github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=
+github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
diff --git a/main.go b/main.go
index 2ffe8ab..723a1b8 100644
--- a/main.go
+++ b/main.go
@@ -41,10 +41,12 @@ type configuration struct {
Timeout int32
SseType string
SseKey string
+ LoginMode bool
}
func parseConfiguration() configuration {
var accessKeyID, secretAccessKey, iamEndpoint string
+ var loginMode bool
viper.AutomaticEnv()
@@ -57,13 +59,10 @@ func parseConfiguration() configuration {
iamEndpoint = viper.GetString("IAM_ENDPOINT")
} else {
accessKeyID = viper.GetString("ACCESS_KEY_ID")
- if len(accessKeyID) == 0 {
- log.Fatal("please provide ACCESS_KEY_ID")
- }
-
secretAccessKey = viper.GetString("SECRET_ACCESS_KEY")
- if len(secretAccessKey) == 0 {
- log.Fatal("please provide SECRET_ACCESS_KEY")
+ if len(accessKeyID) == 0 || len(secretAccessKey) == 0 {
+ log.Println("ACCESS_KEY_ID or SECRET_ACCESS_KEY not set, starting in login mode")
+ loginMode = true
}
}
@@ -115,6 +114,7 @@ func parseConfiguration() configuration {
Timeout: timeout,
SseType: sseType,
SseKey: sseKey,
+ LoginMode: loginMode,
}
}
@@ -135,57 +135,96 @@ func main() {
log.Fatal(err)
}
- // Set up S3 client
- opts := &minio.Options{
- Secure: configuration.UseSSL,
- }
- if configuration.UseIam {
- opts.Creds = credentials.NewIAM(configuration.IamEndpoint)
- } else {
- var signatureType credentials.SignatureType
-
- switch configuration.SignatureType {
- case "V2":
- signatureType = credentials.SignatureV2
- case "V4":
- signatureType = credentials.SignatureV4
- case "V4Streaming":
- signatureType = credentials.SignatureV4Streaming
- case "Anonymous":
- signatureType = credentials.SignatureAnonymous
- default:
- log.Fatalf("Invalid SIGNATURE_TYPE: %s", configuration.SignatureType)
+ // Set up router
+ r := mux.NewRouter()
+ r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.FS(statics)))).Methods(http.MethodGet)
+
+ if configuration.LoginMode {
+ // Login mode: no pre-configured S3 client, per-session credentials
+ sessionCfg := &s3manager.SessionConfig{
+ Store: s3manager.NewSessionStore(),
+ Endpoint: configuration.Endpoint,
+ UseSSL: configuration.UseSSL,
+ SkipSSLVerify: configuration.SkipSSLVerification,
+ AllowDelete: configuration.AllowDelete,
+ ForceDownload: configuration.ForceDownload,
+ ListRecursive: configuration.ListRecursive,
+ SseInfo: sseType,
+ Templates: templates,
}
- opts.Creds = credentials.NewStatic(configuration.AccessKeyID, configuration.SecretAccessKey, "", signatureType)
- }
+ // Public routes (no auth required)
+ r.Handle("/login", s3manager.HandleLoginView(templates)).Methods(http.MethodGet)
+ r.Handle("/login", s3manager.HandleLogin(sessionCfg)).Methods(http.MethodPost)
+ r.Handle("/logout", s3manager.HandleLogout(sessionCfg)).Methods(http.MethodPost)
+
+ // Protected routes (auth required via middleware)
+ protected := mux.NewRouter()
+ protected.Handle("/", http.RedirectHandler("/buckets", http.StatusPermanentRedirect)).Methods(http.MethodGet)
+ protected.Handle("/buckets", s3manager.HandleBucketsViewDynamic(templates, configuration.AllowDelete)).Methods(http.MethodGet)
+ protected.PathPrefix("/buckets/").Handler(s3manager.HandleBucketViewDynamic(templates, configuration.AllowDelete, configuration.ListRecursive)).Methods(http.MethodGet)
+ protected.Handle("/api/buckets", s3manager.HandleCreateBucketDynamic()).Methods(http.MethodPost)
+ if configuration.AllowDelete {
+ protected.Handle("/api/buckets/{bucketName}", s3manager.HandleDeleteBucketDynamic()).Methods(http.MethodDelete)
+ }
+ protected.Handle("/api/buckets/{bucketName}/objects", s3manager.HandleCreateObjectDynamic(sseType)).Methods(http.MethodPost)
+ protected.Handle("/api/buckets/{bucketName}/objects/{objectName:.*}/url", s3manager.HandleGenerateUrlDynamic()).Methods(http.MethodGet)
+ protected.Handle("/api/buckets/{bucketName}/objects/{objectName:.*}", s3manager.HandleGetObjectDynamic(configuration.ForceDownload)).Methods(http.MethodGet)
+ if configuration.AllowDelete {
+ protected.Handle("/api/buckets/{bucketName}/objects/{objectName:.*}", s3manager.HandleDeleteObjectDynamic()).Methods(http.MethodDelete)
+ }
- if configuration.Region != "" {
- opts.Region = configuration.Region
- }
- if configuration.UseSSL && configuration.SkipSSLVerification {
- opts.Transport = &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}} //nolint:gosec
- }
- s3, err := minio.New(configuration.Endpoint, opts)
- if err != nil {
- log.Fatalln(fmt.Errorf("error creating s3 client: %w", err))
- }
+ r.PathPrefix("/").Handler(s3manager.RequireAuth(sessionCfg, protected))
+ } else {
+ // Pre-configured mode: existing behavior with static S3 client
+ opts := &minio.Options{
+ Secure: configuration.UseSSL,
+ }
+ if configuration.UseIam {
+ opts.Creds = credentials.NewIAM(configuration.IamEndpoint)
+ } else {
+ var signatureType credentials.SignatureType
+
+ switch configuration.SignatureType {
+ case "V2":
+ signatureType = credentials.SignatureV2
+ case "V4":
+ signatureType = credentials.SignatureV4
+ case "V4Streaming":
+ signatureType = credentials.SignatureV4Streaming
+ case "Anonymous":
+ signatureType = credentials.SignatureAnonymous
+ default:
+ log.Fatalf("Invalid SIGNATURE_TYPE: %s", configuration.SignatureType)
+ }
+
+ opts.Creds = credentials.NewStatic(configuration.AccessKeyID, configuration.SecretAccessKey, "", signatureType)
+ }
- // Set up router
- r := mux.NewRouter()
- r.Handle("/", http.RedirectHandler("/buckets", http.StatusPermanentRedirect)).Methods(http.MethodGet)
- r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.FS(statics)))).Methods(http.MethodGet)
- r.Handle("/buckets", s3manager.HandleBucketsView(s3, templates, configuration.AllowDelete)).Methods(http.MethodGet)
- r.PathPrefix("/buckets/").Handler(s3manager.HandleBucketView(s3, templates, configuration.AllowDelete, configuration.ListRecursive)).Methods(http.MethodGet)
- r.Handle("/api/buckets", s3manager.HandleCreateBucket(s3)).Methods(http.MethodPost)
- if configuration.AllowDelete {
- r.Handle("/api/buckets/{bucketName}", s3manager.HandleDeleteBucket(s3)).Methods(http.MethodDelete)
- }
- r.Handle("/api/buckets/{bucketName}/objects", s3manager.HandleCreateObject(s3, sseType)).Methods(http.MethodPost)
- r.Handle("/api/buckets/{bucketName}/objects/{objectName:.*}/url", s3manager.HandleGenerateUrl(s3)).Methods(http.MethodGet)
- r.Handle("/api/buckets/{bucketName}/objects/{objectName:.*}", s3manager.HandleGetObject(s3, configuration.ForceDownload)).Methods(http.MethodGet)
- if configuration.AllowDelete {
- r.Handle("/api/buckets/{bucketName}/objects/{objectName:.*}", s3manager.HandleDeleteObject(s3)).Methods(http.MethodDelete)
+ if configuration.Region != "" {
+ opts.Region = configuration.Region
+ }
+ if configuration.UseSSL && configuration.SkipSSLVerification {
+ opts.Transport = &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}} //nolint:gosec
+ }
+ s3, err := minio.New(configuration.Endpoint, opts)
+ if err != nil {
+ log.Fatalln(fmt.Errorf("error creating s3 client: %w", err))
+ }
+
+ r.Handle("/", http.RedirectHandler("/buckets", http.StatusPermanentRedirect)).Methods(http.MethodGet)
+ r.Handle("/buckets", s3manager.HandleBucketsView(s3, templates, configuration.AllowDelete)).Methods(http.MethodGet)
+ r.PathPrefix("/buckets/").Handler(s3manager.HandleBucketView(s3, templates, configuration.AllowDelete, configuration.ListRecursive)).Methods(http.MethodGet)
+ r.Handle("/api/buckets", s3manager.HandleCreateBucket(s3)).Methods(http.MethodPost)
+ if configuration.AllowDelete {
+ r.Handle("/api/buckets/{bucketName}", s3manager.HandleDeleteBucket(s3)).Methods(http.MethodDelete)
+ }
+ r.Handle("/api/buckets/{bucketName}/objects", s3manager.HandleCreateObject(s3, sseType)).Methods(http.MethodPost)
+ r.Handle("/api/buckets/{bucketName}/objects/{objectName:.*}/url", s3manager.HandleGenerateUrl(s3)).Methods(http.MethodGet)
+ r.Handle("/api/buckets/{bucketName}/objects/{objectName:.*}", s3manager.HandleGetObject(s3, configuration.ForceDownload)).Methods(http.MethodGet)
+ if configuration.AllowDelete {
+ r.Handle("/api/buckets/{bucketName}/objects/{objectName:.*}", s3manager.HandleDeleteObject(s3)).Methods(http.MethodDelete)
+ }
}
lr := logging.Handler(os.Stdout)(r)
diff --git a/web/template/bucket.html.tmpl b/web/template/bucket.html.tmpl
index e2f8d28..87add13 100644
--- a/web/template/bucket.html.tmpl
@@ -24,3 +256,298 @@ index c7ea184..fb1dce7 100644
</div>
</nav>
diff --git a/internal/app/s3manager/auth.go b/internal/app/s3manager/auth.go
new file mode 100644
index 0000000..58589e2
--- /dev/null
+++ b/internal/app/s3manager/auth.go
@@ -0,0 +1,237 @@
+package s3manager
+
+import (
+ "context"
+ "crypto/rand"
+ "crypto/tls"
+ "fmt"
+ "html/template"
+ "io/fs"
+ "log"
+ "net/http"
+
+ "github.com/gorilla/sessions"
+ "github.com/minio/minio-go/v7"
+ "github.com/minio/minio-go/v7/pkg/credentials"
+)
+
+type contextKey string
+
+const s3ContextKey contextKey = "s3client"
+
+// SessionConfig holds session store and S3 connection settings for login mode.
+type SessionConfig struct {
+ Store *sessions.CookieStore
+ Endpoint string
+ UseSSL bool
+ SkipSSLVerify bool
+ AllowDelete bool
+ ForceDownload bool
+ ListRecursive bool
+ SseInfo SSEType
+ Templates fs.FS
+}
+
+// NewSessionStore creates a CookieStore with a random encryption key.
+func NewSessionStore() *sessions.CookieStore {
+ key := make([]byte, 32)
+ if _, err := rand.Read(key); err != nil {
+ log.Fatal("failed to generate session key:", err)
+ }
+ store := sessions.NewCookieStore(key)
+ store.Options = &sessions.Options{
+ Path: "/",
+ MaxAge: 86400,
+ HttpOnly: true,
+ Secure: true,
+ SameSite: http.SameSiteLaxMode,
+ }
+ return store
+}
+
+// NewS3Client creates a minio client from user-provided credentials.
+func NewS3Client(endpoint, accessKey, secretKey string, useSSL, skipSSLVerify bool) (*minio.Client, error) {
+ opts := &minio.Options{
+ Creds: credentials.NewStaticV4(accessKey, secretKey, ""),
+ Secure: useSSL,
+ }
+ if useSSL && skipSSLVerify {
+ opts.Transport = &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}} //nolint:gosec
+ }
+ return minio.New(endpoint, opts)
+}
+
+// S3FromContext retrieves the S3 client stored in request context.
+func S3FromContext(ctx context.Context) S3 {
+ if s3, ok := ctx.Value(s3ContextKey).(S3); ok {
+ return s3
+ }
+ return nil
+}
+
+func contextWithS3(ctx context.Context, s3 S3) context.Context {
+ return context.WithValue(ctx, s3ContextKey, s3)
+}
+
+// RequireAuth is middleware that validates session credentials and injects
+// an S3 client into the request context. Redirects to /login if no session.
+func RequireAuth(cfg *SessionConfig, next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ session, _ := cfg.Store.Get(r, "s3session")
+ accessKey, ok1 := session.Values["accessKey"].(string)
+ secretKey, ok2 := session.Values["secretKey"].(string)
+ if !ok1 || !ok2 || accessKey == "" || secretKey == "" {
+ http.Redirect(w, r, "/login", http.StatusFound)
+ return
+ }
+
+ s3, err := NewS3Client(cfg.Endpoint, accessKey, secretKey, cfg.UseSSL, cfg.SkipSSLVerify)
+ if err != nil {
+ // Session has bad credentials — clear and redirect to login
+ session.Options.MaxAge = -1
+ _ = session.Save(r, w)
+ http.Redirect(w, r, "/login", http.StatusFound)
+ return
+ }
+
+ ctx := contextWithS3(r.Context(), s3)
+ next.ServeHTTP(w, r.WithContext(ctx))
+ })
+}
+
+// HandleLoginView renders the login page.
+func HandleLoginView(templates fs.FS) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ errorMsg := r.URL.Query().Get("error")
+
+ data := struct {
+ Error string
+ }{
+ Error: errorMsg,
+ }
+
+ t, err := template.ParseFS(templates, "layout.html.tmpl", "login.html.tmpl")
+ if err != nil {
+ handleHTTPError(w, fmt.Errorf("error parsing login template: %w", err))
+ return
+ }
+ err = t.ExecuteTemplate(w, "layout", data)
+ if err != nil {
+ handleHTTPError(w, fmt.Errorf("error executing login template: %w", err))
+ return
+ }
+ }
+}
+
+// HandleLogin processes the login form POST.
+func HandleLogin(cfg *SessionConfig) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ accessKey := r.FormValue("accessKey")
+ secretKey := r.FormValue("secretKey")
+
+ if accessKey == "" || secretKey == "" {
+ http.Redirect(w, r, "/login?error=credentials+required", http.StatusFound)
+ return
+ }
+
+ // Validate credentials by attempting ListBuckets
+ s3, err := NewS3Client(cfg.Endpoint, accessKey, secretKey, cfg.UseSSL, cfg.SkipSSLVerify)
+ if err != nil {
+ http.Redirect(w, r, "/login?error=connection+failed", http.StatusFound)
+ return
+ }
+ _, err = s3.ListBuckets(r.Context())
+ if err != nil {
+ http.Redirect(w, r, "/login?error=invalid+credentials", http.StatusFound)
+ return
+ }
+
+ // Save credentials to session
+ session, _ := cfg.Store.Get(r, "s3session")
+ session.Values["accessKey"] = accessKey
+ session.Values["secretKey"] = secretKey
+ err = session.Save(r, w)
+ if err != nil {
+ handleHTTPError(w, fmt.Errorf("error saving session: %w", err))
+ return
+ }
+
+ http.Redirect(w, r, "/buckets", http.StatusFound)
+ }
+}
+
+// HandleLogout destroys the session and redirects to login.
+func HandleLogout(cfg *SessionConfig) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ session, _ := cfg.Store.Get(r, "s3session")
+ session.Options.MaxAge = -1
+ _ = session.Save(r, w)
+ http.Redirect(w, r, "/login", http.StatusFound)
+ }
+}
+
+// Dynamic handler wrappers — extract S3 from context, delegate to original handlers.
+
+// HandleBucketsViewDynamic wraps HandleBucketsView for login mode.
+func HandleBucketsViewDynamic(templates fs.FS, allowDelete bool) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ s3 := S3FromContext(r.Context())
+ HandleBucketsView(s3, templates, allowDelete).ServeHTTP(w, r)
+ }
+}
+
+// HandleBucketViewDynamic wraps HandleBucketView for login mode.
+func HandleBucketViewDynamic(templates fs.FS, allowDelete bool, listRecursive bool) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ s3 := S3FromContext(r.Context())
+ HandleBucketView(s3, templates, allowDelete, listRecursive).ServeHTTP(w, r)
+ }
+}
+
+// HandleCreateBucketDynamic wraps HandleCreateBucket for login mode.
+func HandleCreateBucketDynamic() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ s3 := S3FromContext(r.Context())
+ HandleCreateBucket(s3).ServeHTTP(w, r)
+ }
+}
+
+// HandleDeleteBucketDynamic wraps HandleDeleteBucket for login mode.
+func HandleDeleteBucketDynamic() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ s3 := S3FromContext(r.Context())
+ HandleDeleteBucket(s3).ServeHTTP(w, r)
+ }
+}
+
+// HandleCreateObjectDynamic wraps HandleCreateObject for login mode.
+func HandleCreateObjectDynamic(sseInfo SSEType) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ s3 := S3FromContext(r.Context())
+ HandleCreateObject(s3, sseInfo).ServeHTTP(w, r)
+ }
+}
+
+// HandleGenerateUrlDynamic wraps HandleGenerateUrl for login mode.
+func HandleGenerateUrlDynamic() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ s3 := S3FromContext(r.Context())
+ HandleGenerateUrl(s3).ServeHTTP(w, r)
+ }
+}
+
+// HandleGetObjectDynamic wraps HandleGetObject for login mode.
+func HandleGetObjectDynamic(forceDownload bool) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ s3 := S3FromContext(r.Context())
+ HandleGetObject(s3, forceDownload).ServeHTTP(w, r)
+ }
+}
+
+// HandleDeleteObjectDynamic wraps HandleDeleteObject for login mode.
+func HandleDeleteObjectDynamic() http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ s3 := S3FromContext(r.Context())
+ HandleDeleteObject(s3).ServeHTTP(w, r)
+ }
+}
diff --git a/web/template/login.html.tmpl b/web/template/login.html.tmpl
new file mode 100644
index 0000000..f153018
--- /dev/null
+++ b/web/template/login.html.tmpl
@@ -0,0 +1,46 @@
+{{ define "content" }}
+<nav>
+ <div class="nav-wrapper container">
+ <a href="/" class="brand-logo">Cozystack S3 Manager</a>
+ </div>
+</nav>
+
+<div class="container">
+ <div class="section">
+ <div class="row">
+ <div class="col l6 offset-l3 m8 offset-m2 s12">
+ <div class="card">
+ <div class="card-content">
+ <span class="card-title">Sign In</span>
+ <p>Enter your S3 credentials to access the bucket manager.</p>
+ <br>
+
+ {{ if .Error }}
+ <div class="card-panel red lighten-4 red-text text-darken-4">
+ <i class="material-icons tiny">error</i> {{ .Error }}
+ </div>
+ {{ end }}
+
+ <form method="POST" action="/login">
+ <div class="input-field">
+ <i class="material-icons prefix">vpn_key</i>
+ <input id="accessKey" name="accessKey" type="text" required>
+ <label for="accessKey">Access Key ID</label>
+ </div>
+ <div class="input-field">
+ <i class="material-icons prefix">lock</i>
+ <input id="secretKey" name="secretKey" type="password" required>
+ <label for="secretKey">Secret Access Key</label>
+ </div>
+ <br>
+ <button type="submit" class="btn waves-effect waves-light" style="width:100%;">
+ Sign In <i class="material-icons right">send</i>
+ </button>
+ </form>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+</div>
+{{ end }}

View File

@@ -1,3 +1,12 @@
{{- $endpoint := printf "s3.%s" .Values._namespace.host }}
{{- range $name, $user := .Values.users }}
{{- $secretName := printf "%s-%s" $.Values.bucketName $name }}
{{- $existingSecret := lookup "v1" "Secret" $.Release.Namespace $secretName }}
{{- if $existingSecret }}
{{- $bucketInfo := fromJson (b64dec (index $existingSecret.data "BucketInfo")) }}
{{- $endpoint = trimPrefix "https://" (index $bucketInfo.spec.secretS3 "endpoint") }}
{{- end }}
{{- end }}
apiVersion: apps/v1
kind: Deployment
metadata:
@@ -17,19 +26,6 @@ spec:
image: "{{ $.Files.Get "images/s3manager.tag" | trim }}"
env:
- name: ENDPOINT
valueFrom:
secretKeyRef:
name: {{ .Values.bucketName }}-credentials
key: endpoint
value: {{ $endpoint | quote }}
- name: SKIP_SSL_VERIFICATION
value: "true"
- name: ACCESS_KEY_ID
valueFrom:
secretKeyRef:
name: {{ .Values.bucketName }}-credentials
key: accessKey
- name: SECRET_ACCESS_KEY
valueFrom:
secretKeyRef:
name: {{ .Values.bucketName }}-credentials
key: secretKey

View File

@@ -8,9 +8,6 @@ kind: Ingress
metadata:
name: {{ .Values.bucketName }}-ui
annotations:
nginx.ingress.kubernetes.io/auth-type: "basic"
nginx.ingress.kubernetes.io/auth-secret: "{{ .Values.bucketName }}-ui-auth"
nginx.ingress.kubernetes.io/auth-realm: "Authentication Required"
nginx.ingress.kubernetes.io/proxy-body-size: "0"
nginx.ingress.kubernetes.io/proxy-read-timeout: "99999"
nginx.ingress.kubernetes.io/proxy-send-timeout: "99999"

View File

@@ -1,24 +1,2 @@
{{- $existingSecret := lookup "v1" "Secret" .Release.Namespace .Values.bucketName }}
{{- $bucketInfo := fromJson (b64dec (index $existingSecret.data "BucketInfo")) }}
{{- $accessKeyID := index $bucketInfo.spec.secretS3 "accessKeyID" }}
{{- $accessSecretKey := index $bucketInfo.spec.secretS3 "accessSecretKey" }}
{{- $endpoint := index $bucketInfo.spec.secretS3 "endpoint" }}
{{- $bucketName := index $bucketInfo.spec "bucketName" }}
apiVersion: v1
kind: Secret
metadata:
name: {{ .Values.bucketName }}-credentials
type: Opaque
stringData:
accessKey: {{ $accessKeyID | quote }}
secretKey: {{ $accessSecretKey | quote }}
endpoint: {{ trimPrefix "https://" $endpoint }}
bucketName: {{ $bucketName | quote }}
---
apiVersion: v1
kind: Secret
metadata:
name: {{ .Values.bucketName }}-ui-auth
data:
auth: {{ htpasswd $accessKeyID $accessSecretKey | b64enc | quote }}
{{/* Secrets previously used for s3manager credential injection and nginx basic auth */}}
{{/* are no longer needed — s3manager now handles authentication via its own login page */}}

View File

@@ -0,0 +1,20 @@
{{- range $name, $user := .Values.users }}
{{- $secretName := printf "%s-%s" $.Values.bucketName $name }}
{{- $existingSecret := lookup "v1" "Secret" $.Release.Namespace $secretName }}
{{- if $existingSecret }}
{{- $bucketInfo := fromJson (b64dec (index $existingSecret.data "BucketInfo")) }}
---
apiVersion: v1
kind: Secret
metadata:
name: {{ $secretName }}-credentials
labels:
apps.cozystack.io/user-secret: "true"
type: Opaque
stringData:
accessKey: {{ index $bucketInfo.spec.secretS3 "accessKeyID" | quote }}
secretKey: {{ index $bucketInfo.spec.secretS3 "accessSecretKey" | quote }}
endpoint: {{ trimPrefix "https://" (index $bucketInfo.spec.secretS3 "endpoint") }}
bucketName: {{ index $bucketInfo.spec "bucketName" | quote }}
{{- end }}
{{- end }}

View File

@@ -1 +1,2 @@
bucketName: "cozystack"
users: {}

View File

@@ -18,6 +18,8 @@ spec:
issuerRef:
name: cozystack-api-selfsigned
isCA: true
privateKey:
rotationPolicy: Never
---
apiVersion: cert-manager.io/v1
kind: Issuer

View File

@@ -1,3 +1,3 @@
cozystackAPI:
image: ghcr.io/cozystack/cozystack/cozystack-api:v1.0.0@sha256:bd70ecb944bde9a0d6b88114aea89bdbbe2d07e33f03175cfd885de013e88294
image: ghcr.io/cozystack/cozystack/cozystack-api:v1.1.2@sha256:834b4cbb0c7b3a1b41cc58c54f2147338a171b84b0e7e4c880a79e557f3b0997
replicas: 2

View File

@@ -1,4 +1,4 @@
cozystackController:
image: ghcr.io/cozystack/cozystack/cozystack-controller:v1.0.0@sha256:da01085026a4a01514ae435c7bfb48cca2cf00eb17feb2ed7ae88711f82693e0
image: ghcr.io/cozystack/cozystack/cozystack-controller:v1.1.2@sha256:2ae390a9c312c3d76fbf7f8f86e761d43033ab2f0a4c13cd9e9ee52ec8abb0d2
debug: false
disableTelemetry: false

View File

@@ -6,7 +6,7 @@ FROM node:${NODE_VERSION}-alpine AS openapi-k8s-toolkit-builder
RUN apk add git
WORKDIR /src
# release/1.4.0
ARG COMMIT=c67029cc7b7495c65ee0406033576e773a73bb01
ARG COMMIT=d6b9e4ad0d1eb9d3730f7f0c664792c8dda3214d
RUN wget -O- https://github.com/PRO-Robotech/openapi-k8s-toolkit/archive/${COMMIT}.tar.gz | tar -xzvf- --strip-components=1
COPY openapi-k8s-toolkit/patches /patches

View File

@@ -0,0 +1,37 @@
diff --git a/src/localTypes/formExtensions.ts b/src/localTypes/formExtensions.ts
--- a/src/localTypes/formExtensions.ts
+++ b/src/localTypes/formExtensions.ts
@@ -59,2 +59,4 @@
relatedValuePath?: string
+ allowEmpty?: boolean
+ persistType?: 'str' | 'number' | 'arr' | 'obj'
}
diff --git a/src/components/molecules/BlackholeForm/molecules/FormListInput/FormListInput.tsx b/src/components/molecules/BlackholeForm/molecules/FormListInput/FormListInput.tsx
--- a/src/components/molecules/BlackholeForm/molecules/FormListInput/FormListInput.tsx
+++ b/src/components/molecules/BlackholeForm/molecules/FormListInput/FormListInput.tsx
@@ -149,3 +149,10 @@
}, [relatedPath, form, arrName, fixedName, relatedFieldValue, onValuesChangeCallBack, isTouchedPeristed])
+ // When allowEmpty is set, auto-persist the field so the BFF preserves empty values
+ useEffect(() => {
+ if (customProps.allowEmpty) {
+ persistedControls.onPersistMark(persistName || name, customProps.persistType ?? 'str')
+ }
+ }, [customProps.allowEmpty, customProps.persistType, persistedControls, persistName, name])
+
const uri = prepareTemplate({
@@ -267,5 +274,14 @@
validateTrigger="onBlur"
hasFeedback={designNewLayout ? { icons: feedbackIcons } : true}
style={{ flex: 1 }}
+ normalize={(value: unknown) => {
+ if (customProps.allowEmpty && (value === undefined || value === null)) {
+ if (customProps.persistType === 'number') return 0
+ if (customProps.persistType === 'arr') return []
+ if (customProps.persistType === 'obj') return {}
+ return ''
+ }
+ return value
+ }}
>
<Select

View File

@@ -1,49 +0,0 @@
diff --git a/src/components/molecules/BlackholeForm/molecules/FormListInput/FormListInput.tsx b/src/components/molecules/BlackholeForm/molecules/FormListInput/FormListInput.tsx
index d5e5230..9038dbb 100644
--- a/src/components/molecules/BlackholeForm/molecules/FormListInput/FormListInput.tsx
+++ b/src/components/molecules/BlackholeForm/molecules/FormListInput/FormListInput.tsx
@@ -259,14 +259,15 @@ export const FormListInput: FC<TFormListInputProps> = ({
<PersistedCheckbox formName={persistName || name} persistedControls={persistedControls} type="arr" />
</Flex>
</Flex>
- <ResetedFormItem
- key={arrKey !== undefined ? arrKey : Array.isArray(name) ? name.slice(-1)[0] : name}
- name={arrName || fixedName}
- rules={[getRequiredRule(forceNonRequired === false && !!required?.includes(getStringByName(name)), name)]}
- validateTrigger="onBlur"
- hasFeedback={designNewLayout ? { icons: feedbackIcons } : true}
- >
- <Flex gap={8} align="center">
+ <Flex gap={8} align="center">
+ <ResetedFormItem
+ key={arrKey !== undefined ? arrKey : Array.isArray(name) ? name.slice(-1)[0] : name}
+ name={arrName || fixedName}
+ rules={[getRequiredRule(forceNonRequired === false && !!required?.includes(getStringByName(name)), name)]}
+ validateTrigger="onBlur"
+ hasFeedback={designNewLayout ? { icons: feedbackIcons } : true}
+ style={{ flex: 1 }}
+ >
<Select
mode={customProps.mode}
placeholder="Select"
@@ -277,13 +278,13 @@ export const FormListInput: FC<TFormListInputProps> = ({
showSearch
style={{ width: '100%' }}
/>
- {relatedValueTooltip && (
- <Tooltip title={relatedValueTooltip}>
- <QuestionCircleOutlined />
- </Tooltip>
- )}
- </Flex>
- </ResetedFormItem>
+ </ResetedFormItem>
+ {relatedValueTooltip && (
+ <Tooltip title={relatedValueTooltip}>
+ <QuestionCircleOutlined />
+ </Tooltip>
+ )}
+ </Flex>
</HiddenContainer>
)
}

View File

@@ -0,0 +1,29 @@
diff --git a/src/components/organisms/DynamicComponents/molecules/SecretBase64Plain/SecretBase64Plain.tsx b/src/components/organisms/DynamicComponents/molecules/SecretBase64Plain/SecretBase64Plain.tsx
--- a/src/components/organisms/DynamicComponents/molecules/SecretBase64Plain/SecretBase64Plain.tsx
+++ b/src/components/organisms/DynamicComponents/molecules/SecretBase64Plain/SecretBase64Plain.tsx
@@ -145,6 +145,12 @@
<Styled.DisabledInput
$hidden={effectiveHidden}
onClick={e => handleInputClick(e, effectiveHidden, value)}
+ onCopy={e => {
+ if (!effectiveHidden) {
+ e.preventDefault()
+ e.clipboardData?.setData('text/plain', value)
+ }
+ }}
value={shownValue}
readOnly
/>
@@ -161,6 +167,12 @@
<Styled.DisabledInput
$hidden={effectiveHidden}
onClick={e => handleInputClick(e, effectiveHidden, value)}
+ onCopy={e => {
+ if (!effectiveHidden) {
+ e.preventDefault()
+ e.clipboardData?.setData('text/plain', value)
+ }
+ }}
value={shownValue}
readOnly
/>

View File

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

View File

@@ -1,6 +1,6 @@
openapiUI:
image: ghcr.io/cozystack/cozystack/openapi-ui:v1.0.0@sha256:73a8bd4283a46a99d22536eece9c2059fa2fb1c17b43ddefe6716e8960e4731e
image: ghcr.io/cozystack/cozystack/openapi-ui:v1.1.2@sha256:047531b14cc6590c093052bbbda2c2c223c942365491aa47ef063ee91b171e42
openapiUIK8sBff:
image: ghcr.io/cozystack/cozystack/openapi-ui-k8s-bff:v1.0.0@sha256:c938fee904acd948800d4dc5e121c4c5cd64cb4a3160fb8d2f9dbff0e5168740
image: ghcr.io/cozystack/cozystack/openapi-ui-k8s-bff:v1.1.2@sha256:c401a21288a3d48f064916f831d3b30c3cb504112790bf35f5d07ebb96263954
tokenProxy:
image: ghcr.io/cozystack/cozystack/token-proxy:v1.0.0@sha256:2e280991e07853ea48f97b0a42946afffa10d03d6a83d41099ed83e6ffc94fdc
image: ghcr.io/cozystack/cozystack/token-proxy:v1.1.2@sha256:2e280991e07853ea48f97b0a42946afffa10d03d6a83d41099ed83e6ffc94fdc

View File

@@ -38,8 +38,8 @@
| kubeRbacProxy.args[2] | string | `"--logtostderr=true"` | |
| kubeRbacProxy.args[3] | string | `"--v=0"` | |
| kubeRbacProxy.image.pullPolicy | string | `"IfNotPresent"` | Image pull policy |
| kubeRbacProxy.image.repository | string | `"gcr.io/kubebuilder/kube-rbac-proxy"` | Image repository |
| kubeRbacProxy.image.tag | string | `"v0.16.0"` | Version of image |
| kubeRbacProxy.image.repository | string | `"quay.io/brancz/kube-rbac-proxy"` | Image repository |
| kubeRbacProxy.image.tag | string | `"v0.18.1"` | Version of image |
| kubeRbacProxy.livenessProbe | object | `{}` | https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/ |
| kubeRbacProxy.readinessProbe | object | `{}` | https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/ |
| kubeRbacProxy.resources | object | `{"limits":{"cpu":"250m","memory":"128Mi"},"requests":{"cpu":"100m","memory":"64Mi"}}` | ref: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ |

View File

@@ -98,13 +98,13 @@ kubeRbacProxy:
image:
# -- Image repository
repository: gcr.io/kubebuilder/kube-rbac-proxy
repository: quay.io/brancz/kube-rbac-proxy
# -- Image pull policy
pullPolicy: IfNotPresent
# -- Version of image
tag: v0.16.0
tag: v0.18.1
args:
- --secure-listen-address=0.0.0.0:8443

View File

@@ -1 +1 @@
ghcr.io/cozystack/cozystack/grafana-dashboards:v1.0.0@sha256:7a3c9af59f8d74d5a23750bbc845c7de64610dbd4d4f84011e10be037b3ce2a0
ghcr.io/cozystack/cozystack/grafana-dashboards:v1.1.2@sha256:2c9aa0b48e2bf6167db198f4d15882bfe51700108edf2e9f6d0942940a2c1204

View File

@@ -8,5 +8,6 @@ update:
helm repo update ingress-nginx
helm pull ingress-nginx/ingress-nginx --untar --untardir charts
patch --no-backup-if-mismatch -p 3 < patches/add-metrics2.patch
patch --no-backup-if-mismatch -p4 < patches/disable-ca-key-rotation.patch
rm -f charts/ingress-nginx/templates/controller-deployment.yaml.orig
rm -rf charts/ingress-nginx/changelog/

View File

@@ -23,6 +23,8 @@ spec:
name: {{ include "ingress-nginx.fullname" . }}-self-signed-issuer
commonName: "ca.webhook.ingress-nginx"
isCA: true
privateKey:
rotationPolicy: Never
subject:
organizations:
- ingress-nginx

View File

@@ -0,0 +1,13 @@
diff --git a/packages/system/ingress-nginx/charts/ingress-nginx/templates/admission-webhooks/cert-manager.yaml b/packages/system/ingress-nginx/charts/ingress-nginx/templates/admission-webhooks/cert-manager.yaml
index db2946c3..fab1e02e 100644
--- a/packages/system/ingress-nginx/charts/ingress-nginx/templates/admission-webhooks/cert-manager.yaml
+++ b/packages/system/ingress-nginx/charts/ingress-nginx/templates/admission-webhooks/cert-manager.yaml
@@ -23,6 +23,8 @@ spec:
name: {{ include "ingress-nginx.fullname" . }}-self-signed-issuer
commonName: "ca.webhook.ingress-nginx"
isCA: true
+ privateKey:
+ rotationPolicy: Never
subject:
organizations:
- ingress-nginx

View File

@@ -3,7 +3,7 @@ kamaji:
deploy: false
image:
pullPolicy: IfNotPresent
tag: v1.0.0@sha256:50db517ebe7698083dd32223a96c987b6ed0c88d3a093969beb571e4a96d18e4
tag: v1.1.2@sha256:914d04f7442f0faecf18f8282c192dee9fe244a711494a8c892e2f9e2ad415f7
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.0.0@sha256:50db517ebe7698083dd32223a96c987b6ed0c88d3a093969beb571e4a96d18e4
- --migrate-image=ghcr.io/cozystack/cozystack/kamaji:v1.1.2@sha256:914d04f7442f0faecf18f8282c192dee9fe244a711494a8c892e2f9e2ad415f7

View File

@@ -76,14 +76,18 @@ spec:
{{- end }}
- name: KC_METRICS_ENABLED
value: "true"
- name: KC_HEALTH_ENABLED
value: "true"
- name: KC_LOG_LEVEL
value: "info"
- name: KC_CACHE
value: "ispn"
- name: KC_CACHE_STACK
value: "kubernetes"
- name: KC_PROXY
value: "edge"
- name: KC_PROXY_HEADERS
value: "xforwarded"
- name: KC_HTTP_ENABLED
value: "true"
- name: KEYCLOAK_ADMIN
value: admin
- name: KEYCLOAK_ADMIN_PASSWORD
@@ -128,16 +132,27 @@ spec:
- name: http
containerPort: 8080
protocol: TCP
- name: management
containerPort: 9000
protocol: TCP
startupProbe:
httpGet:
path: /health/ready
port: management
failureThreshold: 30
periodSeconds: 10
livenessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: 120
path: /health/live
port: management
periodSeconds: 15
timeoutSeconds: 5
failureThreshold: 5
readinessProbe:
httpGet:
path: /realms/master
port: http
initialDelaySeconds: 60
timeoutSeconds: 1
path: /health/ready
port: management
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
terminationGracePeriodSeconds: 60

View File

@@ -1,4 +1,4 @@
portSecurity: true
routes: ""
image: ghcr.io/cozystack/cozystack/kubeovn-plunger:v1.0.0@sha256:b6045fdb4f324b9b1cb44a218c40422aafbbc600b085c819ff58809bb6e97220
image: ghcr.io/cozystack/cozystack/kubeovn-plunger:v1.1.2@sha256:dacd40971a6d2365e3793e48078525120212d643e20d057503493eadeff960bc
ovnCentralName: ovn-central

View File

@@ -18,6 +18,8 @@ spec:
issuerRef:
name: {{ include "namespace-annotation-webhook.fullname" . }}-selfsigned-issuer
isCA: true
privateKey:
rotationPolicy: Never
---
apiVersion: cert-manager.io/v1
kind: Issuer

View File

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

View File

@@ -1,3 +1,3 @@
storageClass: replicated
csiDriver:
image: ghcr.io/cozystack/cozystack/kubevirt-csi-driver:0.0.0@sha256:434aa3b8e2a3cbf6681426b174e1c4fde23bafd12a6cccd046b5cb1749092ec4
image: ghcr.io/cozystack/cozystack/kubevirt-csi-driver:0.0.0@sha256:1c8c842277f45f189a5c645fcf7b2023c8ed7189f44029ce8b988019000da14c

View File

@@ -18,6 +18,8 @@ spec:
issuerRef:
name: lineage-controller-webhook-selfsigned
isCA: true
privateKey:
rotationPolicy: Never
---
apiVersion: cert-manager.io/v1
kind: Issuer

View File

@@ -1,5 +1,5 @@
lineageControllerWebhook:
image: ghcr.io/cozystack/cozystack/lineage-controller-webhook:v1.0.0@sha256:af765c2829db4f513084522a384710acc321bd4a332eaf7fe814fecacea1022f
image: ghcr.io/cozystack/cozystack/lineage-controller-webhook:v1.1.2@sha256:eb6d54d4cd20276318aa0937cf084cd764d688016c150f9dec54e1ed3f85a740
debug: false
localK8sAPIEndpoint:
enabled: true

View File

@@ -8,3 +8,4 @@ update:
helm repo add piraeus-charts https://piraeus.io/helm-charts/
helm repo update piraeus-charts
helm pull piraeus-charts/linstor-scheduler --untar --untardir charts
patch --no-backup-if-mismatch -p4 < patches/disable-ca-key-rotation.patch

View File

@@ -24,6 +24,8 @@ spec:
issuerRef:
name: {{ include "linstor-scheduler.fullname" . }}-admission-selfsigned
isCA: true
privateKey:
rotationPolicy: Never
---
apiVersion: cert-manager.io/v1
kind: Issuer

View File

@@ -0,0 +1,13 @@
diff --git a/packages/system/linstor-scheduler/charts/linstor-scheduler/templates/certmanager.yaml b/packages/system/linstor-scheduler/charts/linstor-scheduler/templates/certmanager.yaml
index 3942555b..760a9d5d 100644
--- a/packages/system/linstor-scheduler/charts/linstor-scheduler/templates/certmanager.yaml
+++ b/packages/system/linstor-scheduler/charts/linstor-scheduler/templates/certmanager.yaml
@@ -24,6 +24,8 @@ spec:
issuerRef:
name: {{ include "linstor-scheduler.fullname" . }}-admission-selfsigned
isCA: true
+ privateKey:
+ rotationPolicy: Never
---
apiVersion: cert-manager.io/v1
kind: Issuer

View File

@@ -9,6 +9,8 @@ spec:
secretName: linstor-api-ca
duration: 87600h # 10 years
isCA: true
privateKey:
rotationPolicy: Never
usages:
- signing
- key encipherment

View File

@@ -9,6 +9,8 @@ spec:
secretName: linstor-internal-ca
duration: 87600h # 10 years
isCA: true
privateKey:
rotationPolicy: Never
usages:
- signing
- key encipherment

View File

@@ -13,4 +13,4 @@ linstor:
linstorCSI:
image:
repository: ghcr.io/cozystack/cozystack/linstor-csi
tag: v1.10.5@sha256:c87b6f6dadaa6e3a3643d3279e81742830147f6c38f99e9232d9780abbcac897
tag: v1.10.5@sha256:65d0407a51306dd1add40bafcf0022b25e9c90ca70f82202f4da5255ebca21c8

View File

@@ -45,3 +45,5 @@ hubble/l7-http-metrics
hubble/network-overview
nats/nats-jetstream
nats/nats-server
mongodb/mongodb-overview
mongodb/mongodb-inmemory

View File

@@ -1,3 +1,3 @@
objectstorage:
controller:
image: "ghcr.io/cozystack/cozystack/objectstorage-controller:v1.0.0@sha256:e40e94f3014cfd04cce4230597315a1acfcca2daa8051b987614d0c05da6d928"
image: "ghcr.io/cozystack/cozystack/objectstorage-controller:v1.1.2@sha256:e40e94f3014cfd04cce4230597315a1acfcca2daa8051b987614d0c05da6d928"

View File

@@ -8,7 +8,7 @@ spec:
plural: rabbitmqs
singular: rabbitmq
openAPISchema: |-
{"title":"Chart Values","type":"object","properties":{"external":{"description":"Enable external access from outside the cluster.","type":"boolean","default":false},"replicas":{"description":"Number of RabbitMQ replicas.","type":"integer","default":3},"resources":{"description":"Explicit CPU and memory configuration for each RabbitMQ replica. When omitted, the preset defined in `resourcesPreset` is applied.","type":"object","default":{},"properties":{"cpu":{"description":"CPU available to each replica.","pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true},"memory":{"description":"Memory (RAM) available to each replica.","pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true}}},"resourcesPreset":{"description":"Default sizing preset used when `resources` is omitted.","type":"string","default":"nano","enum":["nano","micro","small","medium","large","xlarge","2xlarge"]},"size":{"description":"Persistent Volume Claim size available for application data.","default":"10Gi","pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true},"storageClass":{"description":"StorageClass used to store the data.","type":"string","default":""},"users":{"description":"Users configuration map.","type":"object","default":{},"additionalProperties":{"type":"object","properties":{"password":{"description":"Password for the user.","type":"string"}}}},"vhosts":{"description":"Virtual hosts configuration map.","type":"object","default":{},"additionalProperties":{"type":"object","required":["roles"],"properties":{"roles":{"description":"Virtual host roles list.","type":"object","properties":{"admin":{"description":"List of admin users.","type":"array","items":{"type":"string"}},"readonly":{"description":"List of readonly users.","type":"array","items":{"type":"string"}}}}}}}}}
{"title":"Chart Values","type":"object","properties":{"external":{"description":"Enable external access from outside the cluster.","type":"boolean","default":false},"replicas":{"description":"Number of RabbitMQ replicas.","type":"integer","default":3},"resources":{"description":"Explicit CPU and memory configuration for each RabbitMQ replica. When omitted, the preset defined in `resourcesPreset` is applied.","type":"object","default":{},"properties":{"cpu":{"description":"CPU available to each replica.","pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true},"memory":{"description":"Memory (RAM) available to each replica.","pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true}}},"resourcesPreset":{"description":"Default sizing preset used when `resources` is omitted.","type":"string","default":"nano","enum":["nano","micro","small","medium","large","xlarge","2xlarge"]},"size":{"description":"Persistent Volume Claim size available for application data.","default":"10Gi","pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true},"storageClass":{"description":"StorageClass used to store the data.","type":"string","default":""},"users":{"description":"Users configuration map.","type":"object","default":{},"additionalProperties":{"type":"object","properties":{"password":{"description":"Password for the user.","type":"string"}}}},"version":{"description":"RabbitMQ major.minor version to deploy","type":"string","default":"v4.2","enum":["v4.2","v4.1","v4.0","v3.13"]},"vhosts":{"description":"Virtual hosts configuration map.","type":"object","default":{},"additionalProperties":{"type":"object","required":["roles"],"properties":{"roles":{"description":"Virtual host roles list.","type":"object","properties":{"admin":{"description":"List of admin users.","type":"array","items":{"type":"string"}},"readonly":{"description":"List of readonly users.","type":"array","items":{"type":"string"}}}}}}}}}
release:
prefix: rabbitmq-
labels:
@@ -25,7 +25,7 @@ spec:
tags:
- messaging
icon: PHN2ZyB3aWR0aD0iMTQ0IiBoZWlnaHQ9IjE0NCIgdmlld0JveD0iMCAwIDE0NCAxNDQiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHg9Ii0wLjAwMTk1MzEyIiB3aWR0aD0iMTQ0IiBoZWlnaHQ9IjE0NCIgcng9IjI0IiBmaWxsPSJ1cmwoI3BhaW50MF9saW5lYXJfNjgzXzI5NzIpIi8+CjxwYXRoIGQ9Ik0xMTEuNDExIDYyLjhIODIuNDkzOUM4MS43OTY5IDYyLjc5OTcgODEuMTI4NSA2Mi41MjI4IDgwLjYzNTYgNjIuMDMwMUM4MC4xNDI3IDYxLjUzNzMgNzkuODY1NiA2MC44NjkxIDc5Ljg2NTMgNjAuMTcyMlYzMC4wNDEyQzc5Ljg2NTMgMjcuODEgNzguMDU1NiAyNiA3NS44MjQ5IDI2SDY1LjUwMjFDNjMuMjcgMjYgNjEuNDYxNCAyNy44MSA2MS40NjE0IDMwLjA0MTJWNTkuOTg5OEM2MS40NjE0IDYxLjU0MzUgNjAuMjA1IDYyLjgwNjUgNTguNjUwOCA2Mi44MTMzTDQ5LjE3NDMgNjIuODU4NEM0Ny42MDY5IDYyLjg2NjkgNDYuMzMzNiA2MS41OTU1IDQ2LjMzNjYgNjAuMDI5OEw0Ni4zOTU0IDMwLjA0OEM0Ni40MDA1IDI3LjgxMzQgNDQuNTkwMiAyNiA0Mi4zNTUgMjZIMzIuMDQwN0MyOS44MDgzIDI2IDI4IDI3LjgxIDI4IDMwLjA0MTJWMTE0LjQxMkMyOCAxMTYuMzk0IDI5LjYwNjEgMTE4IDMxLjU4NzEgMTE4SDExMS40MTFDMTEzLjM5NCAxMTggMTE1IDExNi4zOTQgMTE1IDExNC40MTJWNjYuMzg4QzExNSA2NC40MDU4IDExMy4zOTQgNjIuOCAxMTEuNDExIDYyLjhaTTk3Ljg1MDggOTQuNDc3OUM5Ny44NTA4IDk3LjA3NTUgOTUuNzQ0NSA5OS4xODE3IDkzLjE0NjQgOTkuMTgxN0g4NC45ODg0QzgyLjM5IDk5LjE4MTcgODAuMjgzNiA5Ny4wNzU1IDgwLjI4MzYgOTQuNDc3OVY4Ni4zMjE3QzgwLjI4MzYgODMuNzIzOCA4Mi4zOSA4MS42MTc5IDg0Ljk4ODQgODEuNjE3OUg5My4xNDY0Qzk1Ljc0NDUgODEuNjE3OSA5Ny44NTA4IDgzLjcyMzggOTcuODUwOCA4Ni4zMjE3Vjk0LjQ3NzlaIiBmaWxsPSJ3aGl0ZSIvPgo8ZGVmcz4KPGxpbmVhckdyYWRpZW50IGlkPSJwYWludDBfbGluZWFyXzY4M18yOTcyIiB4MT0iNSIgeTE9Ii03LjUiIHgyPSIxNDEiIHkyPSIxMjQuNSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPgo8c3RvcCBzdG9wLWNvbG9yPSIjRkY4MjJGIi8+CjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iI0ZGNjYwMCIvPgo8L2xpbmVhckdyYWRpZW50Pgo8L2RlZnM+Cjwvc3ZnPgo=
keysOrder: [["apiVersion"], ["appVersion"], ["kind"], ["metadata"], ["metadata", "name"], ["spec", "replicas"], ["spec", "resources"], ["spec", "resourcesPreset"], ["spec", "size"], ["spec", "storageClass"], ["spec", "external"], ["spec", "users"], ["spec", "vhosts"]]
keysOrder: [["apiVersion"], ["appVersion"], ["kind"], ["metadata"], ["metadata", "name"], ["spec", "replicas"], ["spec", "resources"], ["spec", "resourcesPreset"], ["spec", "size"], ["spec", "storageClass"], ["spec", "external"], ["spec", "version"], ["spec", "users"], ["spec", "vhosts"]]
secrets:
exclude: []
include:

View File

@@ -11,5 +11,6 @@ update:
tar xzvf - --strip 3 -C charts seaweedfs-$${version}/k8s/charts/seaweedfs
patch --no-backup-if-mismatch -p4 < patches/resize-api-server-annotation.diff
patch --no-backup-if-mismatch -p4 < patches/s3-traffic-distribution.patch
patch --no-backup-if-mismatch -p4 < patches/disable-ca-key-rotation.patch
#patch --no-backup-if-mismatch -p4 < patches/retention-policy-delete.yaml

View File

@@ -13,6 +13,8 @@ spec:
secretName: {{ template "seaweedfs.name" . }}-ca-cert
commonName: "{{ template "seaweedfs.name" . }}-root-ca"
isCA: true
privateKey:
rotationPolicy: Never
{{- if .Values.certificates.ca.duration }}
duration: {{ .Values.certificates.ca.duration }}
{{- end }}

View File

@@ -7,12 +7,25 @@ metadata:
driverName: {{ .Values.cosi.driverName }}
deletionPolicy: Delete
---
kind: BucketClass
apiVersion: objectstorage.k8s.io/v1alpha1
metadata:
name: {{ .Values.cosi.bucketClassName }}-lock
driverName: {{ .Values.cosi.driverName }}
deletionPolicy: Retain
parameters:
objectLockEnabled: "true"
objectLockRetentionMode: "COMPLIANCE"
objectLockRetentionDays: "365"
---
kind: BucketAccessClass
apiVersion: objectstorage.k8s.io/v1alpha1
metadata:
name: {{ .Values.cosi.bucketClassName }}
driverName: {{ .Values.cosi.driverName }}
authenticationType: KEY
parameters:
accessPolicy: readwrite
---
kind: BucketAccessClass
apiVersion: objectstorage.k8s.io/v1alpha1
@@ -21,5 +34,5 @@ metadata:
driverName: {{ .Values.cosi.driverName }}
authenticationType: KEY
parameters:
accessPolicy: "readonly"
accessPolicy: readonly
{{- end }}

View File

@@ -1546,7 +1546,7 @@ allInOne:
# For more information, visit: https://container-object-storage-interface.github.io/docs/deployment-guide
cosi:
enabled: false
image: "ghcr.io/seaweedfs/seaweedfs-cosi-driver:v0.1.2"
image: "ghcr.io/seaweedfs/seaweedfs-cosi-driver:v0.3.0"
driverName: "seaweedfs.objectstorage.k8s.io"
bucketClassName: "seaweedfs"
endpoint: ""

View File

@@ -0,0 +1,13 @@
diff --git a/packages/system/seaweedfs/charts/seaweedfs/templates/cert/ca-cert.yaml b/packages/system/seaweedfs/charts/seaweedfs/templates/cert/ca-cert.yaml
index b01a8dcc..a38287de 100644
--- a/packages/system/seaweedfs/charts/seaweedfs/templates/cert/ca-cert.yaml
+++ b/packages/system/seaweedfs/charts/seaweedfs/templates/cert/ca-cert.yaml
@@ -13,6 +13,8 @@ spec:
secretName: {{ template "seaweedfs.name" . }}-ca-cert
commonName: "{{ template "seaweedfs.name" . }}-root-ca"
isCA: true
+ privateKey:
+ rotationPolicy: Never
{{- if .Values.certificates.ca.duration }}
duration: {{ .Values.certificates.ca.duration }}
{{- end }}

View File

@@ -177,7 +177,7 @@ seaweedfs:
bucketClassName: "seaweedfs"
region: ""
sidecar:
image: "ghcr.io/cozystack/cozystack/objectstorage-sidecar:v1.0.0@sha256:2a3595cd88b30af55b2000d3ca204899beecef0012b0e0402754c3914aad1f7f"
image: "ghcr.io/cozystack/cozystack/objectstorage-sidecar:v1.1.2@sha256:2a3595cd88b30af55b2000d3ca204899beecef0012b0e0402754c3914aad1f7f"
certificates:
commonName: "SeaweedFS CA"
ipAddresses: []

View File

@@ -9,3 +9,4 @@ update:
helm repo add vm https://victoriametrics.github.io/helm-charts/
helm repo update vm
helm pull vm/victoria-metrics-operator --untar --untardir charts
patch --no-backup-if-mismatch -p4 < patches/disable-ca-key-rotation.patch

View File

@@ -78,6 +78,8 @@ spec:
name: {{ $fullname }}-root
commonName: {{ $certManager.ca.commonName }}
isCA: true
privateKey:
rotationPolicy: Never
---
apiVersion: cert-manager.io/v1
kind: Issuer

View File

@@ -0,0 +1,13 @@
diff --git a/packages/system/victoria-metrics-operator/charts/victoria-metrics-operator/templates/webhook.yaml b/packages/system/victoria-metrics-operator/charts/victoria-metrics-operator/templates/webhook.yaml
index 2e027ab4..1bead84d 100644
--- a/packages/system/victoria-metrics-operator/charts/victoria-metrics-operator/templates/webhook.yaml
+++ b/packages/system/victoria-metrics-operator/charts/victoria-metrics-operator/templates/webhook.yaml
@@ -78,6 +78,8 @@ spec:
name: {{ $fullname }}-root
commonName: {{ $certManager.ca.commonName }}
isCA: true
+ privateKey:
+ rotationPolicy: Never
---
apiVersion: cert-manager.io/v1
kind: Issuer

View File

@@ -224,8 +224,8 @@ func buildPostProcessV3(kindSchemas map[string]string) func(*spec3.OpenAPI) (*sp
base, ok1 := doc.Components.Schemas[baseRef]
list, ok2 := doc.Components.Schemas[baseListRef]
stat, ok3 := doc.Components.Schemas[baseStatusRef]
if !(ok1 && ok2 && ok3) && len(kindSchemas) > 0 {
return doc, fmt.Errorf("base Application* schemas not found")
if !(ok1 && ok2 && ok3) {
return doc, nil // not the apps GV — nothing to patch
}
// Clone base schemas for each kind
@@ -339,8 +339,8 @@ func buildPostProcessV2(kindSchemas map[string]string) func(*spec.Swagger) (*spe
base, ok1 := defs[baseRef]
list, ok2 := defs[baseListRef]
stat, ok3 := defs[baseStatusRef]
if !(ok1 && ok2 && ok3) && len(kindSchemas) > 0 {
return sw, fmt.Errorf("base Application* schemas not found")
if !(ok1 && ok2 && ok3) {
return sw, nil // not the apps GV — nothing to patch
}
for kind, raw := range kindSchemas {