Repository: cozystack/cozystack

Issue:
Add CNPG postgres alerts


Hints from issue comments:
@kvaps: fixed by https://github.com/cozystack/cozystack/pull/558

Solution patch:
diff --git a/MAINTAINERS.md b/MAINTAINERS.md
index cbb2b079..ba191681 100644
--- a/MAINTAINERS.md
+++ b/MAINTAINERS.md
@@ -1,7 +1,12 @@
 # The Cozystack Maintainers
 
-| Maintainer | GitHub Username | Company |
-| ---------- | --------------- | ------- |
-| Andrei Kvapil | [@kvaps](https://github.com/kvaps) | Ænix |
-| George Gaál | [@gecube](https://github.com/gecube) | Ænix |
-| Eduard Generalov | [@egeneralov](https://github.com/egeneralov) | Ænix |
+| Maintainer | GitHub Username | Company |          Responsibility           |
+| ---------- | --------------- | ------- | --------------------------------- |
+| Andrei Kvapil | [@kvaps](https://github.com/kvaps) | Ænix | Core Maintainer |
+| George Gaál | [@gecube](https://github.com/gecube) | Ænix | DevOps Practices in Platform, Developers Advocate |
+| Kingdon Barrett | [@kingdonb](https://github.com/kingdonb) | Urmanac | FluxCD and flux-operator |
+| Timofei Larkin | [@lllamnyp](https://github.com/lllamnyp) | 3commas | Etcd-operator Lead |
+| Artem Bortnikov | [@aobort](https://github.com/aobort) | Timescale | Etcd-operator Lead |
+| Andrei Gumilev | [@chumkaska](https://github.com/chumkaska) | Ænix | Platform Documentation |
+| Timur Tukaev | [@tym83](https://github.com/tym83) | Ænix | Cozystack Website, Marketing, Community Management |
+| Kirill Klinchenkov | [@klinch0](https://github.com/klinch0) | Ænix | Core Maintainer |
diff --git a/Makefile b/Makefile
index 89c3b638..ee4a29e2 100644
--- a/Makefile
+++ b/Makefile
@@ -37,3 +37,6 @@ test:
 	make -C packages/core/testing apply
 	make -C packages/core/testing test
 	make -C packages/core/testing test-applications
+
+generate:
+	hack/update-codegen.sh
diff --git a/api/api-rules/cozystack_api_violation_exceptions.list b/api/api-rules/cozystack_api_violation_exceptions.list
index fc2d804c..5c65e849 100644
--- a/api/api-rules/cozystack_api_violation_exceptions.list
+++ b/api/api-rules/cozystack_api_violation_exceptions.list
@@ -1,4 +1,4 @@
-API rule violation: list_type_missing,github.com/aenix.io/cozystack/pkg/apis/apps/v1alpha1,ApplicationStatus,Conditions
+API rule violation: list_type_missing,github.com/aenix-io/cozystack/pkg/apis/apps/v1alpha1,ApplicationStatus,Conditions
 API rule violation: names_match,k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1,JSONSchemaProps,Ref
 API rule violation: names_match,k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1,JSONSchemaProps,Schema
 API rule violation: names_match,k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1,JSONSchemaProps,XEmbeddedResource
diff --git a/api/v1alpha1/groupversion_info.go b/api/v1alpha1/groupversion_info.go
new file mode 100644
index 00000000..1dd84ee1
--- /dev/null
+++ b/api/v1alpha1/groupversion_info.go
@@ -0,0 +1,36 @@
+/*
+Copyright 2025.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Package v1alpha1 contains API Schema definitions for the  v1alpha1 API group.
+// +kubebuilder:object:generate=true
+// +groupName=cozystack.io
+package v1alpha1
+
+import (
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	"sigs.k8s.io/controller-runtime/pkg/scheme"
+)
+
+var (
+	// GroupVersion is group version used to register these objects.
+	GroupVersion = schema.GroupVersion{Group: "cozystack.io", Version: "v1alpha1"}
+
+	// SchemeBuilder is used to add go types to the GroupVersionKind scheme.
+	SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion}
+
+	// AddToScheme adds the types in this group-version to the given scheme.
+	AddToScheme = SchemeBuilder.AddToScheme
+)
diff --git a/api/v1alpha1/workload_types.go b/api/v1alpha1/workload_types.go
new file mode 100644
index 00000000..41bd9eda
--- /dev/null
+++ b/api/v1alpha1/workload_types.go
@@ -0,0 +1,70 @@
+/*
+Copyright 2025.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package v1alpha1
+
+import (
+	"k8s.io/apimachinery/pkg/api/resource"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+// WorkloadStatus defines the observed state of Workload
+type WorkloadStatus struct {
+	// Kind represents the type of workload (redis, postgres, etc.)
+	// +required
+	Kind string `json:"kind"`
+
+	// Type represents the specific role of the workload (redis, sentinel, etc.)
+	// If not specified, defaults to Kind
+	// +optional
+	Type string `json:"type,omitempty"`
+
+	// Resources specifies the compute resources allocated to this workload
+	// +required
+	Resources map[string]resource.Quantity `json:"resources"`
+
+	// Operational indicates if all pods of the workload are ready
+	// +optional
+	Operational bool `json:"operational"`
+}
+
+// +kubebuilder:object:root=true
+// +kubebuilder:printcolumn:name="Kind",type="string",JSONPath=".status.kind"
+// +kubebuilder:printcolumn:name="Type",type="string",JSONPath=".status.type"
+// +kubebuilder:printcolumn:name="CPU",type="string",JSONPath=".status.resources.cpu"
+// +kubebuilder:printcolumn:name="Memory",type="string",JSONPath=".status.resources.memory"
+// +kubebuilder:printcolumn:name="Operational",type="boolean",JSONPath=`.status.operational`
+
+// Workload is the Schema for the workloads API
+type Workload struct {
+	metav1.TypeMeta   `json:",inline"`
+	metav1.ObjectMeta `json:"metadata,omitempty"`
+
+	Status WorkloadStatus `json:"status,omitempty"`
+}
+
+// +kubebuilder:object:root=true
+
+// WorkloadList contains a list of Workload
+type WorkloadList struct {
+	metav1.TypeMeta `json:",inline"`
+	metav1.ListMeta `json:"metadata,omitempty"`
+	Items           []Workload `json:"items"`
+}
+
+func init() {
+	SchemeBuilder.Register(&Workload{}, &WorkloadList{})
+}
diff --git a/api/v1alpha1/workloadmonitor_types.go b/api/v1alpha1/workloadmonitor_types.go
new file mode 100644
index 00000000..e7639edb
--- /dev/null
+++ b/api/v1alpha1/workloadmonitor_types.go
@@ -0,0 +1,91 @@
+package v1alpha1
+
+import (
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+// WorkloadMonitorSpec defines the desired state of WorkloadMonitor
+type WorkloadMonitorSpec struct {
+	// Selector is a label selector to find workloads to monitor
+	// +required
+	Selector map[string]string `json:"selector"`
+
+	// Kind specifies the kind of the workload
+	// +optional
+	Kind string `json:"kind,omitempty"`
+
+	// Type specifies the type of the workload
+	// +optional
+	Type string `json:"type,omitempty"`
+
+	// Version specifies the version of the workload
+	// +optional
+	Version string `json:"version,omitempty"`
+
+	// MinReplicas specifies the minimum number of replicas that should be available
+	// +kubebuilder:validation:Minimum=0
+	// +optional
+	MinReplicas *int32 `json:"minReplicas,omitempty"`
+
+	// Replicas is the desired number of replicas
+	// If not specified, will use observedReplicas as the target
+	// +kubebuilder:validation:Minimum=0
+	// +optional
+	Replicas *int32 `json:"replicas,omitempty"`
+}
+
+// WorkloadMonitorStatus defines the observed state of WorkloadMonitor
+type WorkloadMonitorStatus struct {
+	// Operational indicates if the workload meets all operational requirements
+	// +optional
+	Operational *bool `json:"operational,omitempty"`
+
+	// AvailableReplicas is the number of ready replicas
+	// +optional
+	AvailableReplicas int32 `json:"availableReplicas"`
+
+	// ObservedReplicas is the total number of pods observed
+	// +optional
+	ObservedReplicas int32 `json:"observedReplicas"`
+}
+
+// +kubebuilder:object:root=true
+// +kubebuilder:subresource:status
+// +kubebuilder:printcolumn:name="Kind",type="string",JSONPath=".spec.kind"
+// +kubebuilder:printcolumn:name="Type",type="string",JSONPath=".spec.type"
+// +kubebuilder:printcolumn:name="Version",type="string",JSONPath=".spec.version"
+// +kubebuilder:printcolumn:name="Replicas",type="integer",JSONPath=".spec.replicas"
+// +kubebuilder:printcolumn:name="MinReplicas",type="integer",JSONPath=".spec.minReplicas"
+// +kubebuilder:printcolumn:name="Available",type="integer",JSONPath=".status.availableReplicas"
+// +kubebuilder:printcolumn:name="Observed",type="integer",JSONPath=".status.observedReplicas"
+// +kubebuilder:printcolumn:name="Operational",type="boolean",JSONPath=".status.operational"
+
+// WorkloadMonitor is the Schema for the workloadmonitors API
+type WorkloadMonitor struct {
+	metav1.TypeMeta   `json:",inline"`
+	metav1.ObjectMeta `json:"metadata,omitempty"`
+
+	Spec   WorkloadMonitorSpec   `json:"spec,omitempty"`
+	Status WorkloadMonitorStatus `json:"status,omitempty"`
+}
+
+// +kubebuilder:object:root=true
+
+// WorkloadMonitorList contains a list of WorkloadMonitor
+type WorkloadMonitorList struct {
+	metav1.TypeMeta `json:",inline"`
+	metav1.ListMeta `json:"metadata,omitempty"`
+	Items           []WorkloadMonitor `json:"items"`
+}
+
+func init() {
+	SchemeBuilder.Register(&WorkloadMonitor{}, &WorkloadMonitorList{})
+}
+
+// GetSelector returns the label selector from metadata
+func (w *WorkloadMonitor) GetSelector() map[string]string {
+	return w.Spec.Selector
+}
+
+// Selector specifies the label selector for workloads
+type Selector map[string]string
diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go
new file mode 100644
index 00000000..f510e197
--- /dev/null
+++ b/api/v1alpha1/zz_generated.deepcopy.go
@@ -0,0 +1,238 @@
+//go:build !ignore_autogenerated
+
+/*
+Copyright 2025 The Cozystack Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+// Code generated by controller-gen. DO NOT EDIT.
+
+package v1alpha1
+
+import (
+	"k8s.io/apimachinery/pkg/api/resource"
+	runtime "k8s.io/apimachinery/pkg/runtime"
+)
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in Selector) DeepCopyInto(out *Selector) {
+	{
+		in := &in
+		*out = make(Selector, len(*in))
+		for key, val := range *in {
+			(*out)[key] = val
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Selector.
+func (in Selector) DeepCopy() Selector {
+	if in == nil {
+		return nil
+	}
+	out := new(Selector)
+	in.DeepCopyInto(out)
+	return *out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *Workload) DeepCopyInto(out *Workload) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
+	in.Status.DeepCopyInto(&out.Status)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Workload.
+func (in *Workload) DeepCopy() *Workload {
+	if in == nil {
+		return nil
+	}
+	out := new(Workload)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *Workload) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *WorkloadList) DeepCopyInto(out *WorkloadList) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ListMeta.DeepCopyInto(&out.ListMeta)
+	if in.Items != nil {
+		in, out := &in.Items, &out.Items
+		*out = make([]Workload, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WorkloadList.
+func (in *WorkloadList) DeepCopy() *WorkloadList {
+	if in == nil {
+		return nil
+	}
+	out := new(WorkloadList)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *WorkloadList) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *WorkloadMonitor) DeepCopyInto(out *WorkloadMonitor) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
+	in.Spec.DeepCopyInto(&out.Spec)
+	in.Status.DeepCopyInto(&out.Status)
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WorkloadMonitor.
+func (in *WorkloadMonitor) DeepCopy() *WorkloadMonitor {
+	if in == nil {
+		return nil
+	}
+	out := new(WorkloadMonitor)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *WorkloadMonitor) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *WorkloadMonitorList) DeepCopyInto(out *WorkloadMonitorList) {
+	*out = *in
+	out.TypeMeta = in.TypeMeta
+	in.ListMeta.DeepCopyInto(&out.ListMeta)
+	if in.Items != nil {
+		in, out := &in.Items, &out.Items
+		*out = make([]WorkloadMonitor, len(*in))
+		for i := range *in {
+			(*in)[i].DeepCopyInto(&(*out)[i])
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WorkloadMonitorList.
+func (in *WorkloadMonitorList) DeepCopy() *WorkloadMonitorList {
+	if in == nil {
+		return nil
+	}
+	out := new(WorkloadMonitorList)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
+func (in *WorkloadMonitorList) DeepCopyObject() runtime.Object {
+	if c := in.DeepCopy(); c != nil {
+		return c
+	}
+	return nil
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *WorkloadMonitorSpec) DeepCopyInto(out *WorkloadMonitorSpec) {
+	*out = *in
+	if in.Selector != nil {
+		in, out := &in.Selector, &out.Selector
+		*out = make(map[string]string, len(*in))
+		for key, val := range *in {
+			(*out)[key] = val
+		}
+	}
+	if in.MinReplicas != nil {
+		in, out := &in.MinReplicas, &out.MinReplicas
+		*out = new(int32)
+		**out = **in
+	}
+	if in.Replicas != nil {
+		in, out := &in.Replicas, &out.Replicas
+		*out = new(int32)
+		**out = **in
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WorkloadMonitorSpec.
+func (in *WorkloadMonitorSpec) DeepCopy() *WorkloadMonitorSpec {
+	if in == nil {
+		return nil
+	}
+	out := new(WorkloadMonitorSpec)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *WorkloadMonitorStatus) DeepCopyInto(out *WorkloadMonitorStatus) {
+	*out = *in
+	if in.Operational != nil {
+		in, out := &in.Operational, &out.Operational
+		*out = new(bool)
+		**out = **in
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WorkloadMonitorStatus.
+func (in *WorkloadMonitorStatus) DeepCopy() *WorkloadMonitorStatus {
+	if in == nil {
+		return nil
+	}
+	out := new(WorkloadMonitorStatus)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *WorkloadStatus) DeepCopyInto(out *WorkloadStatus) {
+	*out = *in
+	if in.Resources != nil {
+		in, out := &in.Resources, &out.Resources
+		*out = make(map[string]resource.Quantity, len(*in))
+		for key, val := range *in {
+			(*out)[key] = val.DeepCopy()
+		}
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WorkloadStatus.
+func (in *WorkloadStatus) DeepCopy() *WorkloadStatus {
+	if in == nil {
+		return nil
+	}
+	out := new(WorkloadStatus)
+	in.DeepCopyInto(out)
+	return out
+}
diff --git a/cmd/cozystack-api/main.go b/cmd/cozystack-api/main.go
index dfd2eeb5..5c743da1 100644
--- a/cmd/cozystack-api/main.go
+++ b/cmd/cozystack-api/main.go
@@ -19,7 +19,7 @@ package main
 import (
 	"os"
 
-	"github.com/aenix.io/cozystack/pkg/cmd/server"
+	"github.com/aenix-io/cozystack/pkg/cmd/server"
 	genericapiserver "k8s.io/apiserver/pkg/server"
 	"k8s.io/component-base/cli"
 )
diff --git a/cmd/cozystack-controller/main.go b/cmd/cozystack-controller/main.go
new file mode 100644
index 00000000..04befbfa
--- /dev/null
+++ b/cmd/cozystack-controller/main.go
@@ -0,0 +1,210 @@
+/*
+Copyright 2025.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package main
+
+import (
+	"crypto/tls"
+	"flag"
+	"os"
+	"time"
+
+	// Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.)
+	// to ensure that exec-entrypoint and run can make use of them.
+	_ "k8s.io/client-go/plugin/pkg/client/auth"
+
+	"k8s.io/apimachinery/pkg/runtime"
+	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
+	clientgoscheme "k8s.io/client-go/kubernetes/scheme"
+	ctrl "sigs.k8s.io/controller-runtime"
+	"sigs.k8s.io/controller-runtime/pkg/healthz"
+	"sigs.k8s.io/controller-runtime/pkg/log/zap"
+	"sigs.k8s.io/controller-runtime/pkg/metrics/filters"
+	metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
+	"sigs.k8s.io/controller-runtime/pkg/webhook"
+
+	cozystackiov1alpha1 "github.com/aenix-io/cozystack/api/v1alpha1"
+	"github.com/aenix-io/cozystack/internal/controller"
+	"github.com/aenix-io/cozystack/internal/telemetry"
+	// +kubebuilder:scaffold:imports
+)
+
+var (
+	scheme   = runtime.NewScheme()
+	setupLog = ctrl.Log.WithName("setup")
+)
+
+func init() {
+	utilruntime.Must(clientgoscheme.AddToScheme(scheme))
+
+	utilruntime.Must(cozystackiov1alpha1.AddToScheme(scheme))
+	// +kubebuilder:scaffold:scheme
+}
+
+func main() {
+	var metricsAddr string
+	var enableLeaderElection bool
+	var probeAddr string
+	var secureMetrics bool
+	var enableHTTP2 bool
+	var disableTelemetry bool
+	var telemetryEndpoint string
+	var telemetryInterval string
+	var cozystackVersion string
+	var tlsOpts []func(*tls.Config)
+	flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. "+
+		"Use :8443 for HTTPS or :8080 for HTTP, or leave as 0 to disable the metrics service.")
+	flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.")
+	flag.BoolVar(&enableLeaderElection, "leader-elect", false,
+		"Enable leader election for controller manager. "+
+			"Enabling this will ensure there is only one active controller manager.")
+	flag.BoolVar(&secureMetrics, "metrics-secure", true,
+		"If set, the metrics endpoint is served securely via HTTPS. Use --metrics-secure=false to use HTTP instead.")
+	flag.BoolVar(&enableHTTP2, "enable-http2", false,
+		"If set, HTTP/2 will be enabled for the metrics and webhook servers")
+	flag.BoolVar(&disableTelemetry, "disable-telemetry", false,
+		"Disable telemetry collection")
+	flag.StringVar(&telemetryEndpoint, "telemetry-endpoint", "https://telemetry.cozystack.io",
+		"Endpoint for sending telemetry data")
+	flag.StringVar(&telemetryInterval, "telemetry-interval", "15m",
+		"Interval between telemetry data collection (e.g. 15m, 1h)")
+	flag.StringVar(&cozystackVersion, "cozystack-version", "unknown",
+		"Version of Cozystack")
+	opts := zap.Options{
+		Development: false,
+	}
+	opts.BindFlags(flag.CommandLine)
+	flag.Parse()
+
+	// Parse telemetry interval
+	interval, err := time.ParseDuration(telemetryInterval)
+	if err != nil {
+		setupLog.Error(err, "invalid telemetry interval")
+		os.Exit(1)
+	}
+
+	// Configure telemetry
+	telemetryConfig := telemetry.Config{
+		Disabled:         disableTelemetry,
+		Endpoint:         telemetryEndpoint,
+		Interval:         interval,
+		CozystackVersion: cozystackVersion,
+	}
+
+	ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))
+
+	// if the enable-http2 flag is false (the default), http/2 should be disabled
+	// due to its vulnerabilities. More specifically, disabling http/2 will
+	// prevent from being vulnerable to the HTTP/2 Stream Cancellation and
+	// Rapid Reset CVEs. For more information see:
+	// - https://github.com/advisories/GHSA-qppj-fm5r-hxr3
+	// - https://github.com/advisories/GHSA-4374-p667-p6c8
+	disableHTTP2 := func(c *tls.Config) {
+		setupLog.Info("disabling http/2")
+		c.NextProtos = []string{"http/1.1"}
+	}
+
+	if !enableHTTP2 {
+		tlsOpts = append(tlsOpts, disableHTTP2)
+	}
+
+	webhookServer := webhook.NewServer(webhook.Options{
+		TLSOpts: tlsOpts,
+	})
+
+	// Metrics endpoint is enabled in 'config/default/kustomization.yaml'. The Metrics options configure the server.
+	// More info:
+	// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.19.1/pkg/metrics/server
+	// - https://book.kubebuilder.io/reference/metrics.html
+	metricsServerOptions := metricsserver.Options{
+		BindAddress:   metricsAddr,
+		SecureServing: secureMetrics,
+		TLSOpts:       tlsOpts,
+	}
+
+	if secureMetrics {
+		// FilterProvider is used to protect the metrics endpoint with authn/authz.
+		// These configurations ensure that only authorized users and service accounts
+		// can access the metrics endpoint. The RBAC are configured in 'config/rbac/kustomization.yaml'. More info:
+		// https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.19.1/pkg/metrics/filters#WithAuthenticationAndAuthorization
+		metricsServerOptions.FilterProvider = filters.WithAuthenticationAndAuthorization
+
+		// TODO(user): If CertDir, CertName, and KeyName are not specified, controller-runtime will automatically
+		// generate self-signed certificates for the metrics server. While convenient for development and testing,
+		// this setup is not recommended for production.
+	}
+
+	mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
+		Scheme:                 scheme,
+		Metrics:                metricsServerOptions,
+		WebhookServer:          webhookServer,
+		HealthProbeBindAddress: probeAddr,
+		LeaderElection:         enableLeaderElection,
+		LeaderElectionID:       "19a0338c.cozystack.io",
+		// LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily
+		// when the Manager ends. This requires the binary to immediately end when the
+		// Manager is stopped, otherwise, this setting is unsafe. Setting this significantly
+		// speeds up voluntary leader transitions as the new leader don't have to wait
+		// LeaseDuration time first.
+		//
+		// In the default scaffold provided, the program ends immediately after
+		// the manager stops, so would be fine to enable this option. However,
+		// if you are doing or is intended to do any operation such as perform cleanups
+		// after the manager stops then its usage might be unsafe.
+		// LeaderElectionReleaseOnCancel: true,
+	})
+	if err != nil {
+		setupLog.Error(err, "unable to start manager")
+		os.Exit(1)
+	}
+
+	if err = (&controller.WorkloadMonitorReconciler{
+		Client: mgr.GetClient(),
+		Scheme: mgr.GetScheme(),
+	}).SetupWithManager(mgr); err != nil {
+		setupLog.Error(err, "unable to create controller", "controller", "WorkloadMonitor")
+		os.Exit(1)
+	}
+	// +kubebuilder:scaffold:builder
+
+	if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
+		setupLog.Error(err, "unable to set up health check")
+		os.Exit(1)
+	}
+	if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil {
+		setupLog.Error(err, "unable to set up ready check")
+		os.Exit(1)
+	}
+
+	// Initialize telemetry collector
+	collector, err := telemetry.NewCollector(mgr.GetClient(), &telemetryConfig, mgr.GetConfig())
+	if err != nil {
+		setupLog.V(1).Error(err, "unable to create telemetry collector, telemetry will be disabled")
+	}
+
+	if collector != nil {
+		if err := mgr.Add(collector); err != nil {
+			setupLog.Error(err, "unable to set up telemetry collector")
+			setupLog.V(1).Error(err, "unable to set up telemetry collector, continuing without telemetry")
+		}
+	}
+
+	setupLog.Info("starting manager")
+	if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
+		setupLog.Error(err, "problem running manager")
+		os.Exit(1)
+	}
+}
diff --git a/go.mod b/go.mod
index 103f0929..a9bbce89 100644
--- a/go.mod
+++ b/go.mod
@@ -1,22 +1,27 @@
 // This is a generated file. Do not edit directly.
 
-module github.com/aenix.io/cozystack
+module github.com/aenix-io/cozystack
 
 go 1.23.0
 
 require (
-	github.com/emicklei/go-restful/v3 v3.11.0
+	github.com/fluxcd/helm-controller/api v1.1.0
 	github.com/google/gofuzz v1.2.0
+	github.com/onsi/ginkgo/v2 v2.19.0
+	github.com/onsi/gomega v1.33.1
 	github.com/spf13/cobra v1.8.1
 	github.com/stretchr/testify v1.9.0
+	gopkg.in/yaml.v2 v2.4.0
+	k8s.io/api v0.31.2
 	k8s.io/apiextensions-apiserver v0.31.2
 	k8s.io/apimachinery v0.31.2
 	k8s.io/apiserver v0.31.2
 	k8s.io/client-go v0.31.2
-	k8s.io/code-generator v0.31.2
 	k8s.io/component-base v0.31.2
+	k8s.io/klog/v2 v2.130.1
 	k8s.io/kube-openapi v0.0.0-20240827152857-f7e401e7b4c2
 	k8s.io/utils v0.0.0-20240711033017-18e509b52bc8
+	sigs.k8s.io/controller-runtime v0.19.0
 	sigs.k8s.io/structured-merge-diff/v4 v4.4.1
 )
 
@@ -31,23 +36,27 @@ require (
 	github.com/coreos/go-semver v0.3.1 // indirect
 	github.com/coreos/go-systemd/v22 v22.5.0 // indirect
 	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
+	github.com/emicklei/go-restful/v3 v3.11.0 // indirect
+	github.com/evanphx/json-patch/v5 v5.9.0 // indirect
 	github.com/felixge/httpsnoop v1.0.4 // indirect
-	github.com/fluxcd/helm-controller/api v1.1.0 // indirect
 	github.com/fluxcd/pkg/apis/kustomize v1.6.1 // indirect
 	github.com/fluxcd/pkg/apis/meta v1.6.1 // indirect
 	github.com/fsnotify/fsnotify v1.7.0 // indirect
 	github.com/fxamacker/cbor/v2 v2.7.0 // indirect
 	github.com/go-logr/logr v1.4.2 // indirect
 	github.com/go-logr/stdr v1.2.2 // indirect
+	github.com/go-logr/zapr v1.3.0 // indirect
 	github.com/go-openapi/jsonpointer v0.21.0 // indirect
 	github.com/go-openapi/jsonreference v0.20.2 // indirect
 	github.com/go-openapi/swag v0.23.0 // indirect
+	github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
 	github.com/gogo/protobuf v1.3.2 // indirect
 	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
 	github.com/golang/protobuf v1.5.4 // indirect
 	github.com/google/cel-go v0.21.0 // indirect
 	github.com/google/gnostic-models v0.6.8 // indirect
 	github.com/google/go-cmp v0.6.0 // indirect
+	github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 // indirect
 	github.com/google/uuid v1.6.0 // indirect
 	github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect
 	github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect
@@ -84,7 +93,6 @@ require (
 	go.uber.org/zap v1.27.0 // indirect
 	golang.org/x/crypto v0.28.0 // indirect
 	golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
-	golang.org/x/mod v0.21.0 // indirect
 	golang.org/x/net v0.30.0 // indirect
 	golang.org/x/oauth2 v0.23.0 // indirect
 	golang.org/x/sync v0.8.0 // indirect
@@ -93,6 +101,7 @@ require (
 	golang.org/x/text v0.19.0 // indirect
 	golang.org/x/time v0.7.0 // indirect
 	golang.org/x/tools v0.26.0 // indirect
+	gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
 	google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 // indirect
 	google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect
 	google.golang.org/grpc v1.65.0 // indirect
@@ -100,14 +109,9 @@ require (
 	gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
 	gopkg.in/inf.v0 v0.9.1 // indirect
 	gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
-	gopkg.in/yaml.v2 v2.4.0 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
-	k8s.io/api v0.31.2 // indirect
-	k8s.io/gengo/v2 v2.0.0-20240911193312-2b36238f13e9 // indirect
-	k8s.io/klog/v2 v2.130.1 // indirect
 	k8s.io/kms v0.31.2 // indirect
 	sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.0 // indirect
-	sigs.k8s.io/controller-runtime v0.19.0 // indirect
 	sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
 	sigs.k8s.io/yaml v1.4.0 // indirect
 )
diff --git a/go.sum b/go.sum
index a75e23ba..382420fe 100644
--- a/go.sum
+++ b/go.sum
@@ -26,6 +26,10 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
 github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
 github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
 github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
+github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k=
+github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ=
+github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg=
+github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ=
 github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
 github.com/fluxcd/helm-controller/api v1.1.0 h1:NS5Wm3U6Kv4w7Cw2sDOV++vf2ecGfFV00x1+2Y3QcOY=
@@ -214,8 +218,6 @@ golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0
 golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
 golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
-golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -252,6 +254,8 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw=
+gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=
 google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d h1:VBu5YqKPv6XiJ199exd8Br+Aetz+o08F+PLMnwJQHAY=
 google.golang.org/genproto v0.0.0-20230822172742-b8732ec3820d/go.mod h1:yZTlhN0tQnXo3h00fuXNCxJdLdIdnVFVBaRJ5LWBbw4=
 google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 h1:7whR9kGa5LUwFtpLm2ArCEejtnxlGeLbAyjFY8sGNFw=
@@ -287,12 +291,8 @@ k8s.io/apiserver v0.31.2 h1:VUzOEUGRCDi6kX1OyQ801m4A7AUPglpsmGvdsekmcI4=
 k8s.io/apiserver v0.31.2/go.mod h1:o3nKZR7lPlJqkU5I3Ove+Zx3JuoFjQobGX1Gctw6XuE=
 k8s.io/client-go v0.31.2 h1:Y2F4dxU5d3AQj+ybwSMqQnpZH9F30//1ObxOKlTI9yc=
 k8s.io/client-go v0.31.2/go.mod h1:NPa74jSVR/+eez2dFsEIHNa+3o09vtNaWwWwb1qSxSs=
-k8s.io/code-generator v0.31.2 h1:xLWxG0HEpMSHfcM//3u3Ro2Hmc6AyyLINQS//Z2GEOI=
-k8s.io/code-generator v0.31.2/go.mod h1:eEQHXgBU/m7LDaToDoiz3t97dUUVyOblQdwOr8rivqc=
 k8s.io/component-base v0.31.2 h1:Z1J1LIaC0AV+nzcPRFqfK09af6bZ4D1nAOpWsy9owlA=
 k8s.io/component-base v0.31.2/go.mod h1:9PeyyFN/drHjtJZMCTkSpQJS3U9OXORnHQqMLDz0sUQ=
-k8s.io/gengo/v2 v2.0.0-20240911193312-2b36238f13e9 h1:si3PfKm8dDYxgfbeA6orqrtLkvvIeH8UqffFJDl0bz4=
-k8s.io/gengo/v2 v2.0.0-20240911193312-2b36238f13e9/go.mod h1:EJykeLsmFC60UQbYJezXkEsG2FLrt0GPNkU5iK5GWxU=
 k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
 k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
 k8s.io/kms v0.31.2 h1:pyx7l2qVOkClzFMIWMVF/FxsSkgd+OIGH7DecpbscJI=
diff --git a/hack/boilerplate.go.txt b/hack/boilerplate.go.txt
index b10e59a5..4b746998 100644
--- a/hack/boilerplate.go.txt
+++ b/hack/boilerplate.go.txt
@@ -1,5 +1,5 @@
 /*
-Copyright 2024 The Cozystack Authors.
+Copyright 2025 The Cozystack Authors.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
diff --git a/hack/update-codegen.sh b/hack/update-codegen.sh
index b56d0799..a2a3b581 100755
--- a/hack/update-codegen.sh
+++ b/hack/update-codegen.sh
@@ -22,6 +22,7 @@ SCRIPT_ROOT=$(dirname "${BASH_SOURCE[0]}")/..
 CODEGEN_PKG=${CODEGEN_PKG:-$(cd "${SCRIPT_ROOT}"; ls -d -1 ./vendor/k8s.io/code-generator 2>/dev/null || echo ../code-generator)}
 API_KNOWN_VIOLATIONS_DIR="${API_KNOWN_VIOLATIONS_DIR:-"${SCRIPT_ROOT}/api/api-rules"}"
 UPDATE_API_KNOWN_VIOLATIONS="${UPDATE_API_KNOWN_VIOLATIONS:-true}"
+CONTROLLER_GEN="go run sigs.k8s.io/controller-tools/cmd/controller-gen@v0.16.4"
 
 source "${CODEGEN_PKG}/kube_codegen.sh"
 
@@ -46,3 +47,6 @@ kube::codegen::gen_openapi \
     ${update_report:+"${update_report}"} \
     --boilerplate "${SCRIPT_ROOT}/hack/boilerplate.go.txt" \
     "${SCRIPT_ROOT}/pkg/apis"
+
+$CONTROLLER_GEN object:headerFile="hack/boilerplate.go.txt" paths="./api/..."
+$CONTROLLER_GEN rbac:roleName=manager-role crd paths="./api/..." output:crd:artifacts:config=packages/system/cozystack-controller/templates/crds
diff --git a/internal/controller/workloadmonitor_controller.go b/internal/controller/workloadmonitor_controller.go
new file mode 100644
index 00000000..a6096c1a
--- /dev/null
+++ b/internal/controller/workloadmonitor_controller.go
@@ -0,0 +1,273 @@
+package controller
+
+import (
+	"context"
+	"encoding/json"
+	"sort"
+
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/types"
+	"k8s.io/utils/pointer"
+	ctrl "sigs.k8s.io/controller-runtime"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/handler"
+	"sigs.k8s.io/controller-runtime/pkg/log"
+	"sigs.k8s.io/controller-runtime/pkg/reconcile"
+
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/api/resource"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	cozyv1alpha1 "github.com/aenix-io/cozystack/api/v1alpha1"
+)
+
+// WorkloadMonitorReconciler reconciles a WorkloadMonitor object
+type WorkloadMonitorReconciler struct {
+	client.Client
+	Scheme *runtime.Scheme
+}
+
+// +kubebuilder:rbac:groups=cozystack.io,resources=workloadmonitors,verbs=get;list;watch;create;update;patch;delete
+// +kubebuilder:rbac:groups=cozystack.io,resources=workloadmonitors/status,verbs=get;update;patch
+// +kubebuilder:rbac:groups=cozystack.io,resources=workloads,verbs=get;list;watch;create;update;patch;delete
+// +kubebuilder:rbac:groups=cozystack.io,resources=workloads/status,verbs=get;update;patch
+// +kubebuilder:rbac:groups=core,resources=pods,verbs=get;list;watch
+
+// isPodReady checks if the Pod is in the Ready condition.
+func (r *WorkloadMonitorReconciler) isPodReady(pod *corev1.Pod) bool {
+	for _, c := range pod.Status.Conditions {
+		if c.Type == corev1.PodReady && c.Status == corev1.ConditionTrue {
+			return true
+		}
+	}
+	return false
+}
+
+// updateOwnerReferences adds the given monitor as a new owner reference to the object if not already present.
+// It then sorts the owner references to enforce a consistent order.
+func updateOwnerReferences(obj metav1.Object, monitor client.Object) {
+	// Retrieve current owner references
+	owners := obj.GetOwnerReferences()
+
+	// Check if current monitor is already in owner references
+	var alreadyOwned bool
+	for _, ownerRef := range owners {
+		if ownerRef.UID == monitor.GetUID() {
+			alreadyOwned = true
+			break
+		}
+	}
+
+	runtimeObj, ok := monitor.(runtime.Object)
+	if !ok {
+		return
+	}
+	gvk := runtimeObj.GetObjectKind().GroupVersionKind()
+
+	// If not already present, add new owner reference without controller flag
+	if !alreadyOwned {
+		newOwnerRef := metav1.OwnerReference{
+			APIVersion: gvk.GroupVersion().String(),
+			Kind:       gvk.Kind,
+			Name:       monitor.GetName(),
+			UID:        monitor.GetUID(),
+			// Set Controller to false to avoid conflict as multiple controllers are not allowed
+			Controller:         pointer.BoolPtr(false),
+			BlockOwnerDeletion: pointer.BoolPtr(true),
+		}
+		owners = append(owners, newOwnerRef)
+	}
+
+	// Sort owner references to enforce a consistent order by UID
+	sort.SliceStable(owners, func(i, j int) bool {
+		return owners[i].UID < owners[j].UID
+	})
+
+	// Update the owner references of the object
+	obj.SetOwnerReferences(owners)
+}
+
+// reconcilePodForMonitor creates or updates a Workload object for the given Pod and WorkloadMonitor.
+func (r *WorkloadMonitorReconciler) reconcilePodForMonitor(
+	ctx context.Context,
+	monitor *cozyv1alpha1.WorkloadMonitor,
+	pod corev1.Pod,
+) error {
+	logger := log.FromContext(ctx)
+
+	// Combine both init containers and normal containers to sum resources properly
+	combinedContainers := append(pod.Spec.InitContainers, pod.Spec.Containers...)
+
+	// totalResources will store the sum of all container resource limits
+	totalResources := make(map[string]resource.Quantity)
+
+	// Iterate over all containers to aggregate their Limits
+	for _, container := range combinedContainers {
+		for name, qty := range container.Resources.Limits {
+			if existing, exists := totalResources[name.String()]; exists {
+				existing.Add(qty)
+				totalResources[name.String()] = existing
+			} else {
+				totalResources[name.String()] = qty.DeepCopy()
+			}
+		}
+	}
+
+	// If annotation "workload.cozystack.io/resources" is present, parse and merge
+	if resourcesStr, ok := pod.Annotations["workload.cozystack.io/resources"]; ok {
+		annRes := map[string]string{}
+		if err := json.Unmarshal([]byte(resourcesStr), &annRes); err != nil {
+			logger.Error(err, "Failed to parse resources annotation", "pod", pod.Name)
+		} else {
+			for k, v := range annRes {
+				parsed, err := resource.ParseQuantity(v)
+				if err != nil {
+					logger.Error(err, "Failed to parse resource quantity from annotation", "key", k, "value", v)
+					continue
+				}
+				totalResources[k] = parsed
+			}
+		}
+	}
+
+	workload := &cozyv1alpha1.Workload{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      pod.Name,
+			Namespace: pod.Namespace,
+		},
+	}
+
+	_, err := ctrl.CreateOrUpdate(ctx, r.Client, workload, func() error {
+		// Update owner references with the new monitor
+		updateOwnerReferences(workload.GetObjectMeta(), monitor)
+
+		// Copy labels from the Pod if needed
+		workload.Labels = pod.Labels
+
+		// Fill Workload status fields:
+		workload.Status.Kind = monitor.Spec.Kind
+		workload.Status.Type = monitor.Spec.Type
+		workload.Status.Resources = totalResources
+		workload.Status.Operational = r.isPodReady(&pod)
+
+		return nil
+	})
+	if err != nil {
+		logger.Error(err, "Failed to CreateOrUpdate Workload", "workload", workload.Name)
+		return err
+	}
+
+	return nil
+}
+
+// Reconcile is the main reconcile loop.
+// 1. It reconciles WorkloadMonitor objects themselves (create/update/delete).
+// 2. It also reconciles Pod events mapped to WorkloadMonitor via label selector.
+func (r *WorkloadMonitorReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
+	logger := log.FromContext(ctx)
+
+	// Fetch the WorkloadMonitor object if it exists
+	monitor := &cozyv1alpha1.WorkloadMonitor{}
+	err := r.Get(ctx, req.NamespacedName, monitor)
+	if err != nil {
+		// If the resource is not found, it may be a Pod event (mapFunc).
+		if apierrors.IsNotFound(err) {
+			return ctrl.Result{}, nil
+		}
+		logger.Error(err, "Unable to fetch WorkloadMonitor")
+		return ctrl.Result{}, err
+	}
+
+	// List Pods that match the WorkloadMonitor's selector
+	podList := &corev1.PodList{}
+	if err := r.List(
+		ctx,
+		podList,
+		client.InNamespace(monitor.Namespace),
+		client.MatchingLabels(monitor.Spec.Selector),
+	); err != nil {
+		logger.Error(err, "Unable to list Pods for WorkloadMonitor", "monitor", monitor.Name)
+		return ctrl.Result{}, err
+	}
+
+	var observedReplicas, availableReplicas int32
+
+	// For each matching Pod, reconcile the corresponding Workload
+	for _, pod := range podList.Items {
+		observedReplicas++
+		if err := r.reconcilePodForMonitor(ctx, monitor, pod); err != nil {
+			logger.Error(err, "Failed to reconcile Workload for Pod", "pod", pod.Name)
+			continue
+		}
+		if r.isPodReady(&pod) {
+			availableReplicas++
+		}
+	}
+
+	// Update WorkloadMonitor status based on observed pods
+	monitor.Status.ObservedReplicas = observedReplicas
+	monitor.Status.AvailableReplicas = availableReplicas
+
+	// Default to operational = true, but check MinReplicas if set
+	monitor.Status.Operational = pointer.Bool(true)
+	if monitor.Spec.MinReplicas != nil && availableReplicas < *monitor.Spec.MinReplicas {
+		monitor.Status.Operational = pointer.Bool(false)
+	}
+
+	// Update the WorkloadMonitor status in the cluster
+	if err := r.Status().Update(ctx, monitor); err != nil {
+		logger.Error(err, "Unable to update WorkloadMonitor status", "monitor", monitor.Name)
+		return ctrl.Result{}, err
+	}
+
+	// Return without requeue if we want purely event-driven reconciliations
+	return ctrl.Result{}, nil
+}
+
+// SetupWithManager registers our controller with the Manager and sets up watches.
+func (r *WorkloadMonitorReconciler) SetupWithManager(mgr ctrl.Manager) error {
+	return ctrl.NewControllerManagedBy(mgr).
+		// Watch WorkloadMonitor objects
+		For(&cozyv1alpha1.WorkloadMonitor{}).
+		// Also watch Pod objects and map them back to WorkloadMonitor if labels match
+		Watches(
+			&corev1.Pod{},
+			handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, obj client.Object) []reconcile.Request {
+				pod, ok := obj.(*corev1.Pod)
+				if !ok {
+					return nil
+				}
+
+				var monitorList cozyv1alpha1.WorkloadMonitorList
+				// List all WorkloadMonitors in the same namespace
+				if err := r.List(ctx, &monitorList, client.InNamespace(pod.Namespace)); err != nil {
+					return nil
+				}
+
+				// Match each monitor's selector with the Pod's labels
+				var requests []reconcile.Request
+				for _, m := range monitorList.Items {
+					matches := true
+					for k, v := range m.Spec.Selector {
+						if podVal, exists := pod.Labels[k]; !exists || podVal != v {
+							matches = false
+							break
+						}
+					}
+					if matches {
+						requests = append(requests, reconcile.Request{
+							NamespacedName: types.NamespacedName{
+								Namespace: m.Namespace,
+								Name:      m.Name,
+							},
+						})
+					}
+				}
+				return requests
+			}),
+		).
+		// Watch for changes to Workload objects we create (owned by WorkloadMonitor)
+		Owns(&cozyv1alpha1.Workload{}).
+		Complete(r)
+}
diff --git a/internal/telemetry/collector.go b/internal/telemetry/collector.go
new file mode 100644
index 00000000..7c542961
--- /dev/null
+++ b/internal/telemetry/collector.go
@@ -0,0 +1,292 @@
+package telemetry
+
+import (
+	"bytes"
+	"context"
+	"fmt"
+	"net/http"
+	"strings"
+	"time"
+
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/api/resource"
+	"k8s.io/apimachinery/pkg/types"
+	"k8s.io/client-go/discovery"
+	"k8s.io/client-go/rest"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/log"
+
+	cozyv1alpha1 "github.com/aenix-io/cozystack/api/v1alpha1"
+)
+
+// Collector handles telemetry data collection and sending
+type Collector struct {
+	client          client.Client
+	discoveryClient discovery.DiscoveryInterface
+	config          *Config
+	ticker          *time.Ticker
+	stopCh          chan struct{}
+}
+
+// NewCollector creates a new telemetry collector
+func NewCollector(client client.Client, config *Config, kubeConfig *rest.Config) (*Collector, error) {
+	discoveryClient, err := discovery.NewDiscoveryClientForConfig(kubeConfig)
+	if err != nil {
+		return nil, fmt.Errorf("failed to create discovery client: %w", err)
+	}
+	return &Collector{
+		client:          client,
+		discoveryClient: discoveryClient,
+		config:          config,
+	}, nil
+}
+
+// Start implements manager.Runnable
+func (c *Collector) Start(ctx context.Context) error {
+	if c.config.Disabled {
+		return nil
+	}
+
+	c.ticker = time.NewTicker(c.config.Interval)
+	c.stopCh = make(chan struct{})
+
+	// Initial collection
+	c.collect(ctx)
+
+	for {
+		select {
+		case <-ctx.Done():
+			c.ticker.Stop()
+			close(c.stopCh)
+			return nil
+		case <-c.ticker.C:
+			c.collect(ctx)
+		}
+	}
+}
+
+// NeedLeaderElection implements manager.LeaderElectionRunnable
+func (c *Collector) NeedLeaderElection() bool {
+	// Only run telemetry collector on the leader
+	return true
+}
+
+// Stop halts telemetry collection
+func (c *Collector) Stop() {
+	close(c.stopCh)
+}
+
+// getSizeGroup returns the exponential size group for PVC
+func getSizeGroup(size resource.Quantity) string {
+	gb := size.Value() / (1024 * 1024 * 1024)
+	switch {
+	case gb <= 1:
+		return "1Gi"
+	case gb <= 5:
+		return "5Gi"
+	case gb <= 10:
+		return "10Gi"
+	case gb <= 25:
+		return "25Gi"
+	case gb <= 50:
+		return "50Gi"
+	case gb <= 100:
+		return "100Gi"
+	case gb <= 250:
+		return "250Gi"
+	case gb <= 500:
+		return "500Gi"
+	case gb <= 1024:
+		return "1Ti"
+	case gb <= 2048:
+		return "2Ti"
+	case gb <= 5120:
+		return "5Ti"
+	default:
+		return "10Ti"
+	}
+}
+
+// collect gathers and sends telemetry data
+func (c *Collector) collect(ctx context.Context) {
+	logger := log.FromContext(ctx).V(1)
+
+	// Get cluster ID from kube-system namespace
+	var kubeSystemNS corev1.Namespace
+	if err := c.client.Get(ctx, types.NamespacedName{Name: "kube-system"}, &kubeSystemNS); err != nil {
+		logger.Info(fmt.Sprintf("Failed to get kube-system namespace: %v", err))
+		return
+	}
+
+	clusterID := string(kubeSystemNS.UID)
+
+	var cozystackCM corev1.ConfigMap
+	if err := c.client.Get(ctx, types.NamespacedName{Namespace: "cozy-system", Name: "cozystack"}, &cozystackCM); err != nil {
+		logger.Info(fmt.Sprintf("Failed to get cozystack configmap in cozy-system namespace: %v", err))
+		return
+	}
+
+	oidcEnabled := cozystackCM.Data["oidc-enabled"]
+	bundle := cozystackCM.Data["bundle-name"]
+	bundleEnable := cozystackCM.Data["bundle-enable"]
+	bundleDisable := cozystackCM.Data["bundle-disable"]
+
+	// Get Kubernetes version from nodes
+	var nodeList corev1.NodeList
+	if err := c.client.List(ctx, &nodeList); err != nil {
+		logger.Info(fmt.Sprintf("Failed to list nodes: %v", err))
+		return
+	}
+
+	// Create metrics buffer
+	var metrics strings.Builder
+
+	// Add Cozystack info metric
+	if len(nodeList.Items) > 0 {
+		k8sVersion, _ := c.discoveryClient.ServerVersion()
+		metrics.WriteString(fmt.Sprintf(
+			"cozy_cluster_info{cozystack_version=\"%s\",kubernetes_version=\"%s\",oidc_enabled=\"%s\",bundle_name=\"%s\",bunde_enable=\"%s\",bunde_disable=\"%s\"} 1\n",
+			c.config.CozystackVersion,
+			k8sVersion,
+			oidcEnabled,
+			bundle,
+			bundleEnable,
+			bundleDisable,
+		))
+	}
+
+	// Collect node metrics
+	nodeOSCount := make(map[string]int)
+	for _, node := range nodeList.Items {
+		key := fmt.Sprintf("%s (%s)", node.Status.NodeInfo.OperatingSystem, node.Status.NodeInfo.OSImage)
+		nodeOSCount[key] = nodeOSCount[key] + 1
+	}
+
+	for osKey, count := range nodeOSCount {
+		metrics.WriteString(fmt.Sprintf(
+			"cozy_nodes_count{os=\"%s\",kernel=\"%s\"} %d\n",
+			osKey,
+			nodeList.Items[0].Status.NodeInfo.KernelVersion,
+			count,
+		))
+	}
+
+	// Collect LoadBalancer services metrics
+	var serviceList corev1.ServiceList
+	if err := c.client.List(ctx, &serviceList); err != nil {
+		logger.Info(fmt.Sprintf("Failed to list Services: %v", err))
+	} else {
+		lbCount := 0
+		for _, svc := range serviceList.Items {
+			if svc.Spec.Type == corev1.ServiceTypeLoadBalancer {
+				lbCount++
+			}
+		}
+		metrics.WriteString(fmt.Sprintf("cozy_loadbalancers_count %d\n", lbCount))
+	}
+
+	// Count tenant namespaces
+	var nsList corev1.NamespaceList
+	if err := c.client.List(ctx, &nsList); err != nil {
+		logger.Info(fmt.Sprintf("Failed to list Namespaces: %v", err))
+	} else {
+		tenantCount := 0
+		for _, ns := range nsList.Items {
+			if strings.HasPrefix(ns.Name, "tenant-") {
+				tenantCount++
+			}
+		}
+		metrics.WriteString(fmt.Sprintf("cozy_tenants_count %d\n", tenantCount))
+	}
+
+	// Collect PV metrics grouped by driver and size
+	var pvList corev1.PersistentVolumeList
+	if err := c.client.List(ctx, &pvList); err != nil {
+		logger.Info(fmt.Sprintf("Failed to list PVs: %v", err))
+	} else {
+		// Map to store counts by size and driver
+		pvMetrics := make(map[string]map[string]int)
+
+		for _, pv := range pvList.Items {
+			if capacity, ok := pv.Spec.Capacity[corev1.ResourceStorage]; ok {
+				sizeGroup := getSizeGroup(capacity)
+
+				// Get the CSI driver name
+				driver := "unknown"
+				if pv.Spec.CSI != nil {
+					driver = pv.Spec.CSI.Driver
+				} else if pv.Spec.HostPath != nil {
+					driver = "hostpath"
+				} else if pv.Spec.NFS != nil {
+					driver = "nfs"
+				}
+
+				// Initialize nested map if needed
+				if _, exists := pvMetrics[sizeGroup]; !exists {
+					pvMetrics[sizeGroup] = make(map[string]int)
+				}
+
+				// Increment count for this size/driver combination
+				pvMetrics[sizeGroup][driver]++
+			}
+		}
+
+		// Write metrics
+		for size, drivers := range pvMetrics {
+			for driver, count := range drivers {
+				metrics.WriteString(fmt.Sprintf(
+					"cozy_pvs_count{driver=\"%s\",size=\"%s\"} %d\n",
+					driver,
+					size,
+					count,
+				))
+			}
+		}
+	}
+
+	// Collect workload metrics
+	var monitorList cozyv1alpha1.WorkloadMonitorList
+	if err := c.client.List(ctx, &monitorList); err != nil {
+		logger.Info(fmt.Sprintf("Failed to list WorkloadMonitors: %v", err))
+		return
+	}
+
+	for _, monitor := range monitorList.Items {
+		metrics.WriteString(fmt.Sprintf(
+			"cozy_workloads_count{uid=\"%s\",kind=\"%s\",type=\"%s\",version=\"%s\"} %d\n",
+			monitor.UID,
+			monitor.Spec.Kind,
+			monitor.Spec.Type,
+			monitor.Spec.Version,
+			monitor.Status.ObservedReplicas,
+		))
+	}
+
+	// Send metrics
+	if err := c.sendMetrics(clusterID, metrics.String()); err != nil {
+		logger.Info(fmt.Sprintf("Failed to send metrics: %v", err))
+	}
+}
+
+// sendMetrics sends collected metrics to the configured endpoint
+func (c *Collector) sendMetrics(clusterID, metrics string) error {
+	req, err := http.NewRequest("POST", c.config.Endpoint, bytes.NewBufferString(metrics))
+	if err != nil {
+		return fmt.Errorf("failed to create request: %w", err)
+	}
+
+	req.Header.Set("Content-Type", "text/plain")
+	req.Header.Set("X-Cluster-ID", clusterID)
+
+	resp, err := http.DefaultClient.Do(req)
+	if err != nil {
+		return fmt.Errorf("failed to send request: %w", err)
+	}
+	defer resp.Body.Close()
+
+	if resp.StatusCode != http.StatusOK {
+		return fmt.Errorf("unexpected status code: %d", resp.StatusCode)
+	}
+
+	return nil
+}
diff --git a/internal/telemetry/config.go b/internal/telemetry/config.go
new file mode 100644
index 00000000..b4c9b4d1
--- /dev/null
+++ b/internal/telemetry/config.go
@@ -0,0 +1,27 @@
+package telemetry
+
+import (
+	"time"
+)
+
+// Config holds telemetry configuration
+type Config struct {
+	// Disable telemetry collection if set to true
+	Disabled bool
+	// Endpoint to send telemetry data to
+	Endpoint string
+	// Interval between telemetry data collection
+	Interval time.Duration
+	// CozystackVersion represents the current version of Cozystack
+	CozystackVersion string
+}
+
+// DefaultConfig returns default telemetry configuration
+func DefaultConfig() *Config {
+	return &Config{
+		Disabled:         false,
+		Endpoint:         "https://telemetry.cozystack.io",
+		Interval:         15 * time.Minute,
+		CozystackVersion: "unknown",
+	}
+}
diff --git a/packages/apps/kubernetes/Chart.yaml b/packages/apps/kubernetes/Chart.yaml
index 3d5a7830..1a1a73af 100644
--- a/packages/apps/kubernetes/Chart.yaml
+++ b/packages/apps/kubernetes/Chart.yaml
@@ -16,7 +16,7 @@ type: application
 # This is the chart version. This version number should be incremented each time you make changes
 # to the chart and its templates, including the app version.
 # Versions are expected to follow Semantic Versioning (https://semver.org/)
-version: 0.14.1
+version: 0.15.0
 
 # This is the version number of the application being deployed. This version number should be
 # incremented each time you make changes to the application. Versions are not expected to
diff --git a/packages/apps/kubernetes/images/cluster-autoscaler.tag b/packages/apps/kubernetes/images/cluster-autoscaler.tag
index 2d60c1f0..fa28293c 100644
--- a/packages/apps/kubernetes/images/cluster-autoscaler.tag
+++ b/packages/apps/kubernetes/images/cluster-autoscaler.tag
@@ -1 +1 @@
-ghcr.io/aenix-io/cozystack/cluster-autoscaler:0.14.1@sha256:0ea139c71e08db5adb275d81a7efa9a0d8b8db61a1fc1a67167a33a347c07fd8
+ghcr.io/aenix-io/cozystack/cluster-autoscaler:latest@sha256:a8035edb5abee8e88c9153ea4f612d03f490c9bba757c2e57f61ae66cc6fe972
diff --git a/packages/apps/kubernetes/images/cluster-autoscaler/Dockerfile b/packages/apps/kubernetes/images/cluster-autoscaler/Dockerfile
index 41b4c291..17a5e501 100644
--- a/packages/apps/kubernetes/images/cluster-autoscaler/Dockerfile
+++ b/packages/apps/kubernetes/images/cluster-autoscaler/Dockerfile
@@ -1,12 +1,14 @@
 # Source: https://raw.githubusercontent.com/kubernetes/autoscaler/refs/heads/master/cluster-autoscaler/Dockerfile.amd64
-ARG builder_image=docker.io/library/golang:1.22.5
+ARG builder_image=docker.io/library/golang:1.23.4
 ARG BASEIMAGE=gcr.io/distroless/static:nonroot-amd64
 FROM ${builder_image} AS builder
 RUN git clone https://github.com/kubernetes/autoscaler /src/autoscaler \
  && cd /src/autoscaler/cluster-autoscaler \
- && git checkout cluster-autoscaler-1.31.0
+ && git checkout cluster-autoscaler-1.32.0
 
 WORKDIR /src/autoscaler/cluster-autoscaler
+COPY fix-downscale.diff /fix-downscale.diff
+RUN git apply /fix-downscale.diff
 RUN make build
 
 FROM $BASEIMAGE
diff --git a/packages/apps/kubernetes/images/cluster-autoscaler/fix-downscale.diff b/packages/apps/kubernetes/images/cluster-autoscaler/fix-downscale.diff
new file mode 100644
index 00000000..94c680bc
--- /dev/null
+++ b/packages/apps/kubernetes/images/cluster-autoscaler/fix-downscale.diff
@@ -0,0 +1,13 @@
+diff --git a/cluster-autoscaler/cloudprovider/clusterapi/clusterapi_unstructured.go b/cluster-autoscaler/cloudprovider/clusterapi/clusterapi_unstructured.go
+index 4eec0e4bf..f28fd9241 100644
+--- a/cluster-autoscaler/cloudprovider/clusterapi/clusterapi_unstructured.go
++++ b/cluster-autoscaler/cloudprovider/clusterapi/clusterapi_unstructured.go
+@@ -106,8 +106,6 @@ func (r unstructuredScalableResource) Replicas() (int, error) {
+ 
+ func (r unstructuredScalableResource) SetSize(nreplicas int) error {
+ 	switch {
+-	case nreplicas > r.maxSize:
+-		return fmt.Errorf("size increase too large - desired:%d max:%d", nreplicas, r.maxSize)
+ 	case nreplicas < r.minSize:
+ 		return fmt.Errorf("size decrease too large - desired:%d min:%d", nreplicas, r.minSize)
+ 	}
diff --git a/packages/apps/kubernetes/templates/cluster-autoscaler/deployment.yaml b/packages/apps/kubernetes/templates/cluster-autoscaler/deployment.yaml
index 4ec017af..6b31f53a 100644
--- a/packages/apps/kubernetes/templates/cluster-autoscaler/deployment.yaml
+++ b/packages/apps/kubernetes/templates/cluster-autoscaler/deployment.yaml
@@ -30,6 +30,12 @@ spec:
         - /cluster-autoscaler
         args:
         - --cloud-provider=clusterapi
+        - --enforce-node-group-min-size=true
+        - --ignore-daemonsets-utilization=true
+        - --ignore-mirror-pods-utilization=true
+        - --scale-down-unneeded-time=30s
+        - --scan-interval=25s
+        - --force-delete-unregistered-nodes=true
         - --kubeconfig=/etc/kubernetes/kubeconfig/super-admin.svc
         - --clusterapi-cloud-config-authoritative
         - --node-group-auto-discovery=clusterapi:namespace={{ .Release.Namespace }},clusterName={{ .Release.Name }}
diff --git a/packages/apps/kubernetes/templates/cluster.yaml b/packages/apps/kubernetes/templates/cluster.yaml
index 3be0f0d9..a6d596c3 100644
--- a/packages/apps/kubernetes/templates/cluster.yaml
+++ b/packages/apps/kubernetes/templates/cluster.yaml
@@ -29,6 +29,7 @@ spec:
             {{- range .group.roles }}
             node-role.kubernetes.io/{{ . }}: ""
             {{- end }}
+            cluster.x-k8s.io/deployment-name: {{ $.Release.Name }}-{{ .groupName }}
         spec:
           domain:
             {{- if and .group.resources .group.resources.cpu }}
@@ -126,6 +127,21 @@ spec:
   replicas: 2
   version: 1.30.1
 ---
+apiVersion: cozystack.io/v1alpha1
+kind: WorkloadMonitor
+metadata:
+  name: {{ .Release.Name }}
+  namespace: {{ .Release.Namespace }}
+spec:
+  replicas: 2
+  minReplicas: 1
+  kind: kubernetes
+  type: control-plane
+  selector:
+    kamaji.clastix.io/component: deployment
+    kamaji.clastix.io/name: {{ .Release.Name }}
+  version: {{ $.Chart.Version }}
+---
 apiVersion: infrastructure.cluster.x-k8s.io/v1alpha1
 kind: KubevirtCluster
 metadata:
@@ -172,6 +188,7 @@ spec:
 ---
 {{- $context := deepCopy $ }}
 {{- $_ := set $context "group" $group }}
+{{- $_ := set $context "groupName" $groupName }}
 {{- $kubevirtmachinetemplate := include "kubevirtmachinetemplate" $context }}
 {{- $kubevirtmachinetemplateHash := $kubevirtmachinetemplate | sha256sum | trunc 6 }}
 {{- $kubevirtmachinetemplateName := printf "%s-%s-%s" $.Release.Name $groupName $kubevirtmachinetemplateHash }}
@@ -255,6 +272,21 @@ spec:
   - type: Ready
     status: "False"
     timeout: 300s
+---
+apiVersion: cozystack.io/v1alpha1
+kind: WorkloadMonitor
+metadata:
+  name: {{ $.Release.Name }}-{{ $groupName }}
+  namespace: {{ $.Release.Namespace }}
+spec:
+  minReplicas: {{ $group.minReplicas }}
+  kind: kubernetes
+  type: worker
+  selector:
+    cluster.x-k8s.io/cluster-name: {{ $.Release.Name }}
+    cluster.x-k8s.io/deployment-name: {{ $.Release.Name }}-{{ $groupName }}
+    cluster.x-k8s.io/role: worker
+  version: {{ $.Chart.Version }}
 {{- end }}
 ---
 {{- /*
diff --git a/packages/apps/kubernetes/templates/dashboard-resourcemap.yaml b/packages/apps/kubernetes/templates/dashboard-resourcemap.yaml
index 01c38900..aebb87df 100644
--- a/packages/apps/kubernetes/templates/dashboard-resourcemap.yaml
+++ b/packages/apps/kubernetes/templates/dashboard-resourcemap.yaml
@@ -24,3 +24,13 @@ rules:
   resourceNames:
   - {{ .Release.Name }}
   verbs: ["get", "list", "watch"]
+- apiGroups:
+  - cozystack.io
+  resources:
+  - workloadmonitors
+  resourceNames:
+  - {{ .Release.Name }}
+  {{- range $groupName, $group := .Values.nodeGroups }}
+  - {{ $.Release.Name }}-{{ $groupName }}
+  {{- end }}
+  verbs: ["get", "list", "watch"]
diff --git a/packages/apps/postgres/Chart.yaml b/packages/apps/postgres/Chart.yaml
index 40483895..988feb50 100644
--- a/packages/apps/postgres/Chart.yaml
+++ b/packages/apps/postgres/Chart.yaml
@@ -16,7 +16,7 @@ type: application
 # This is the chart version. This version number should be incremented each time you make changes
 # to the chart and its templates, including the app version.
 # Versions are expected to follow Semantic Versioning (https://semver.org/)
-version: 0.7.1
+version: 0.8.0
 
 # This is the version number of the application being deployed. This version number should be
 # incremented each time you make changes to the application. Versions are not expected to
diff --git a/packages/apps/postgres/templates/dashboard-resourcemap.yaml b/packages/apps/postgres/templates/dashboard-resourcemap.yaml
index dc80d4cb..f858d1b8 100644
--- a/packages/apps/postgres/templates/dashboard-resourcemap.yaml
+++ b/packages/apps/postgres/templates/dashboard-resourcemap.yaml
@@ -19,3 +19,10 @@ rules:
   resourceNames:
   - {{ .Release.Name }}-credentials
   verbs: ["get", "list", "watch"]
+- apiGroups:
+  - cozystack.io
+  resources:
+  - workloadmonitors
+  resourceNames:
+  - {{ .Release.Name }}
+  verbs: ["get", "list", "watch"]
diff --git a/packages/apps/postgres/templates/db.yaml b/packages/apps/postgres/templates/db.yaml
index 8319ac4f..22db426b 100644
--- a/packages/apps/postgres/templates/db.yaml
+++ b/packages/apps/postgres/templates/db.yaml
@@ -29,3 +29,17 @@ spec:
   inheritedMetadata:
     labels:
       policy.cozystack.io/allow-to-apiserver: "true"
+---
+apiVersion: cozystack.io/v1alpha1
+kind: WorkloadMonitor
+metadata:
+  name: {{ $.Release.Name }}
+spec:
+  replicas: {{ .Values.replicas }}
+  minReplicas: 1
+  kind: postgres
+  type: postgres
+  selector:
+    cnpg.io/cluster: {{ .Release.Name }}
+    cnpg.io/podRole: instance
+  version: {{ $.Chart.Version }}
diff --git a/packages/apps/redis/Chart.yaml b/packages/apps/redis/Chart.yaml
index f1666ada..492c67ef 100644
--- a/packages/apps/redis/Chart.yaml
+++ b/packages/apps/redis/Chart.yaml
@@ -16,7 +16,7 @@ type: application
 # This is the chart version. This version number should be incremented each time you make changes
 # to the chart and its templates, including the app version.
 # Versions are expected to follow Semantic Versioning (https://semver.org/)
-version: 0.4.0
+version: 0.5.0
 
 # This is the version number of the application being deployed. This version number should be
 # incremented each time you make changes to the application. Versions are not expected to
diff --git a/packages/apps/redis/templates/dashboard-resourcemap.yaml b/packages/apps/redis/templates/dashboard-resourcemap.yaml
index 45958e8f..f2682c71 100644
--- a/packages/apps/redis/templates/dashboard-resourcemap.yaml
+++ b/packages/apps/redis/templates/dashboard-resourcemap.yaml
@@ -20,3 +20,11 @@ rules:
   resourceNames:
   - "{{ .Release.Name }}-auth"
   verbs: ["get", "list", "watch"]
+- apiGroups:
+  - cozystack.io
+  resources:
+  - workloadmonitors
+  resourceNames:
+  - {{ .Release.Name }}-redis
+  - {{ .Release.Name }}-sentinel
+  verbs: ["get", "list", "watch"]
diff --git a/packages/apps/redis/templates/redisfailover.yaml b/packages/apps/redis/templates/redisfailover.yaml
index 0d7c4e03..0dfd2f56 100644
--- a/packages/apps/redis/templates/redisfailover.yaml
+++ b/packages/apps/redis/templates/redisfailover.yaml
@@ -73,3 +73,34 @@ spec:
   auth:
     secretPath: {{ .Release.Name }}-auth
   {{- end }}
+
+---
+apiVersion: cozystack.io/v1alpha1
+kind: WorkloadMonitor
+metadata:
+  name: {{ $.Release.Name }}-redis
+  namespace: {{ $.Release.Namespace }}
+spec:
+  minReplicas: 1
+  replicas: {{ .Values.replicas }}
+  kind: redis
+  type: redis
+  selector:
+    app.kubernetes.io/component: redis
+    app.kubernetes.io/instance: {{ $.Release.Name }}
+  version: {{ $.Chart.Version }}
+---
+apiVersion: cozystack.io/v1alpha1
+kind: WorkloadMonitor
+metadata:
+  name: {{ $.Release.Name }}-sentinel
+  namespace: {{ $.Release.Namespace }}
+spec:
+  minReplicas: 2
+  replicas: 3
+  kind: redis
+  type: sentinel
+  selector:
+    app.kubernetes.io/component: sentinel
+    app.kubernetes.io/instance: {{ $.Release.Name }}
+  version: {{ $.Chart.Version }}
diff --git a/packages/apps/tenant/Chart.yaml b/packages/apps/tenant/Chart.yaml
index d98b9fa6..89a884a2 100644
--- a/packages/apps/tenant/Chart.yaml
+++ b/packages/apps/tenant/Chart.yaml
@@ -4,4 +4,4 @@ description: Separated tenant namespace
 icon: /logos/tenant.svg
 
 type: application
-version: 1.6.5
+version: 1.6.6
diff --git a/packages/apps/tenant/templates/monitoring.yaml b/packages/apps/tenant/templates/monitoring.yaml
index f0fef4a2..e62cb507 100644
--- a/packages/apps/tenant/templates/monitoring.yaml
+++ b/packages/apps/tenant/templates/monitoring.yaml
@@ -26,12 +26,24 @@ spec:
     metricsStorages:
     - name: shortterm
       retentionPeriod: "3d"
-      deduplicationInterval: "5m"
+      deduplicationInterval: "15s"
       storage: 10Gi
+      vminsert:
+        resources: {}
+      vmselect:
+        resources: {}
+      vmstorage:
+        resources: {}
     - name: longterm
       retentionPeriod: "14d"
-      deduplicationInterval: "15s"
+      deduplicationInterval: "5m"
       storage: 10Gi
+      vminsert:
+        resources: {}
+      vmselect:
+        resources: {}
+      vmstorage:
+        resources: {}
     oncall:
       enabled: false
 {{- end }}
diff --git a/packages/apps/versions_map b/packages/apps/versions_map
index 26483c07..43887463 100644
--- a/packages/apps/versions_map
+++ b/packages/apps/versions_map
@@ -42,7 +42,8 @@ kubernetes 0.12.0 74649f8
 kubernetes 0.12.1 28fca4e
 kubernetes 0.13.0 ced8e5b9
 kubernetes 0.14.0 bfbde07c
-kubernetes 0.14.1 HEAD
+kubernetes 0.14.1 fde4bcfa
+kubernetes 0.15.0 HEAD
 mysql 0.1.0 f642698
 mysql 0.2.0 8b975ff0
 mysql 0.3.0 5ca8823
@@ -65,7 +66,8 @@ postgres 0.5.0 c07c4bbd
 postgres 0.6.0 2a4768a
 postgres 0.6.2 54fd61c
 postgres 0.7.0 dc9d8bb
-postgres 0.7.1 HEAD
+postgres 0.7.1 175a65f
+postgres 0.8.0 HEAD
 rabbitmq 0.1.0 f642698
 rabbitmq 0.2.0 5ca8823
 rabbitmq 0.3.0 9e33dc0
@@ -77,7 +79,8 @@ redis 0.1.1 f642698
 redis 0.2.0 5ca8823
 redis 0.3.0 c07c4bbd
 redis 0.3.1 b7375f73
-redis 0.4.0 HEAD
+redis 0.4.0 abc8f082
+redis 0.5.0 HEAD
 tcp-balancer 0.1.0 f642698
 tcp-balancer 0.2.0 HEAD
 tenant 0.1.3 3d1b86c
@@ -95,16 +98,19 @@ tenant 1.6.1 edbbb9be
 tenant 1.6.2 ccedc5fe
 tenant 1.6.3 2057bb96
 tenant 1.6.4 3c9e50a4
-tenant 1.6.5 HEAD
+tenant 1.6.5 f1e11451
+tenant 1.6.6 HEAD
 virtual-machine 0.1.4 f2015d6
 virtual-machine 0.1.5 7cd7de7
 virtual-machine 0.2.0 5ca8823
 virtual-machine 0.3.0 b908400
 virtual-machine 0.4.0 4746d51
-virtual-machine 0.5.0 HEAD
+virtual-machine 0.5.0 cad9cde
+virtual-machine 0.6.0 HEAD
 vm-disk 0.1.0 HEAD
 vm-instance 0.1.0 ced8e5b9
-vm-instance 0.2.0 HEAD
+vm-instance 0.2.0 4f767ee3
+vm-instance 0.3.0 HEAD
 vpn 0.1.0 f642698
 vpn 0.2.0 7151424
 vpn 0.3.0 a2bcf100
diff --git a/packages/apps/virtual-machine/Chart.yaml b/packages/apps/virtual-machine/Chart.yaml
index 40b76b04..ffff00a8 100644
--- a/packages/apps/virtual-machine/Chart.yaml
+++ b/packages/apps/virtual-machine/Chart.yaml
@@ -17,7 +17,7 @@ type: application
 # This is the chart version. This version number should be incremented each time you make changes
 # to the chart and its templates, including the app version.
 # Versions are expected to follow Semantic Versioning (https://semver.org/)
-version: 0.5.0
+version: 0.6.0
 
 # This is the version number of the application being deployed. This version number should be
 # incremented each time you make changes to the application. Versions are not expected to
diff --git a/packages/apps/virtual-machine/templates/dashboard-resourcemap.yaml b/packages/apps/virtual-machine/templates/dashboard-resourcemap.yaml
new file mode 100644
index 00000000..22af2faa
--- /dev/null
+++ b/packages/apps/virtual-machine/templates/dashboard-resourcemap.yaml
@@ -0,0 +1,25 @@
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+  name: {{ .Release.Name }}-dashboard-resources
+rules:
+- apiGroups:
+  - cozystack.io
+  resources:
+  - workloadmonitors
+  resourceNames:
+  - {{ .Release.Name }}
+  verbs: ["get", "list", "watch"]
+---
+apiVersion: cozystack.io/v1alpha1
+kind: WorkloadMonitor
+metadata:
+  name: {{ $.Release.Name }}
+spec:
+  replicas: 1
+  minReplicas: 1
+  kind: virtual-machine
+  type: virtual-machine
+  selector:
+    vm.kubevirt.io/name: {{ .Release.Name }}
+  version: {{ $.Chart.Version }}
diff --git a/packages/apps/vm-instance/Chart.yaml b/packages/apps/vm-instance/Chart.yaml
index 06b32e2b..055a0bd5 100644
--- a/packages/apps/vm-instance/Chart.yaml
+++ b/packages/apps/vm-instance/Chart.yaml
@@ -17,7 +17,7 @@ type: application
 # This is the chart version. This version number should be incremented each time you make changes
 # to the chart and its templates, including the app version.
 # Versions are expected to follow Semantic Versioning (https://semver.org/)
-version: 0.2.0
+version: 0.3.0
 
 # This is the version number of the application being deployed. This version number should be
 # incremented each time you make changes to the application. Versions are not expected to
diff --git a/packages/apps/vm-instance/templates/dashboard-resourcemap.yaml b/packages/apps/vm-instance/templates/dashboard-resourcemap.yaml
new file mode 100644
index 00000000..e1b7242b
--- /dev/null
+++ b/packages/apps/vm-instance/templates/dashboard-resourcemap.yaml
@@ -0,0 +1,26 @@
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+  name: {{ .Release.Name }}-dashboard-resources
+rules:
+- apiGroups:
+  - cozystack.io
+  resources:
+  - workloadmonitors
+  resourceNames:
+  - {{ .Release.Name }}
+  verbs: ["get", "list", "watch"]
+---
+apiVersion: cozystack.io/v1alpha1
+kind: WorkloadMonitor
+metadata:
+  name: {{ $.Release.Name }}
+  namespace: {{ $.Release.Namespace }}
+spec:
+  replicas: 1
+  minReplicas: 1
+  kind: virtual-machine
+  type: virtual-machine
+  selector:
+    vm.kubevirt.io/name: {{ $.Release.Name }}
+  version: {{ $.Chart.Version }}
diff --git a/packages/core/builder/Chart.yaml b/packages/core/builder/Chart.yaml
new file mode 100755
index 00000000..91337a4b
--- /dev/null
+++ b/packages/core/builder/Chart.yaml
@@ -0,0 +1,3 @@
+apiVersion: v2
+name: builder
+version: 0.0.0 # Placeholder, the actual version will be automatically set during the build process
diff --git a/packages/core/builder/Makefile b/packages/core/builder/Makefile
new file mode 100755
index 00000000..9eee5594
--- /dev/null
+++ b/packages/core/builder/Makefile
@@ -0,0 +1,35 @@
+NAMESPACE=cozy-builder
+NAME := builder
+
+TALOS_VERSION=$(shell awk '/^version:/ {print $$2}' ../installer/images/talos/profiles/installer.yaml)
+
+include ../../../scripts/common-envs.mk
+
+help: ## Show this help.
+	@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {sub("\\\\n",sprintf("\n%22c"," "), $$2);printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)
+
+show:
+	helm template -n $(NAMESPACE) $(NAME) .
+
+apply: ## Create builder sandbox in existing Kubernetes cluster.
+	helm template -n $(NAMESPACE) $(NAME) . | kubectl apply -f -
+	docker buildx ls | grep -q '^buildkit-builder*' || docker buildx create \
+		--bootstrap \
+		--name=buildkit-$(NAME) \
+		--driver=kubernetes \
+		--driver-opt=namespace=$(NAMESPACE),replicas=1 \
+		--platform=linux/amd64 \
+		--platform=linux/arm64 \
+		--use \
+		--config config.toml
+
+diff:
+	helm template -n $(NAMESPACE) $(NAME) . | kubectl diff -f -
+
+delete: ## Remove builder sandbox from existing Kubernetes cluster.
+	kubectl delete deploy -n $(NAMESPACE) $(NAME)-talos-imager
+	docker buildx rm buildkit-$(NAME)
+
+wait-for-builder:
+	kubectl wait deploy --for=condition=Progressing -n $(NAMESPACE) $(NAME)-talos-imager
+	kubectl wait pod --for=condition=Ready -n $(NAMESPACE) -l app=$(NAME)-talos-imager
diff --git a/packages/core/builder/config.toml b/packages/core/builder/config.toml
new file mode 100644
index 00000000..a3cb03a3
--- /dev/null
+++ b/packages/core/builder/config.toml
@@ -0,0 +1,11 @@
+[worker.oci]
+  gc = true
+  gckeepstorage = 50000
+
+  [[worker.oci.gcpolicy]]
+    keepBytes = 10737418240
+    keepDuration = 604800
+    filters = [ "type==source.local", "type==exec.cachemount", "type==source.git.checkout"]
+  [[worker.oci.gcpolicy]]
+    all = true
+    keepBytes = 53687091200
diff --git a/packages/core/builder/templates/sandbox.yaml b/packages/core/builder/templates/sandbox.yaml
new file mode 100755
index 00000000..9189e8b1
--- /dev/null
+++ b/packages/core/builder/templates/sandbox.yaml
@@ -0,0 +1,43 @@
+---
+apiVersion: v1
+kind: Namespace
+metadata:
+  name: {{ .Release.Namespace }}
+  labels:
+    pod-security.kubernetes.io/enforce: privileged
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: {{ .Release.Name }}-talos-imager
+  namespace: {{ .Release.Namespace }}
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: {{ .Release.Name }}-talos-imager
+  strategy:
+    type: Recreate
+  template:
+    metadata:
+      labels:
+        app: {{ .Release.Name }}-talos-imager
+    spec:
+      automountServiceAccountToken: false
+      terminationGracePeriodSeconds: 1
+      containers:
+      - name: imagere
+        image: "{{ .Values.talos.imager.image }}"
+        securityContext:
+          privileged: true
+        command:
+        - sleep
+        - infinity
+        volumeMounts:
+        - mountPath: /dev
+          name: dev
+      volumes:
+      - hostPath:
+          path: /dev
+          type: Directory
+        name: dev
diff --git a/packages/core/builder/values.yaml b/packages/core/builder/values.yaml
new file mode 100755
index 00000000..5899a23b
--- /dev/null
+++ b/packages/core/builder/values.yaml
@@ -0,0 +1,3 @@
+talos:
+  imager:
+    image: ghcr.io/siderolabs/imager:v1.8.4
diff --git a/packages/core/installer/Makefile b/packages/core/installer/Makefile
index 386006b6..2f0ef052 100644
--- a/packages/core/installer/Makefile
+++ b/packages/core/installer/Makefile
@@ -19,10 +19,12 @@ diff:
 
 update:
 	hack/gen-profiles.sh
+	IMAGE=$$(awk '/^version:/ {print $$2}' images/talos/profiles/installer.yaml) \
+		yq -i '.talos.imager.image = "ghcr.io/siderolabs/imager:" + strenv(IMAGE)' ../builder/values.yaml
 
 image: pre-checks image-cozystack image-talos image-matchbox
 
-image-cozystack:
+image-cozystack: run-builder
 	make -C ../../.. repos
 	docker buildx build -f images/cozystack/Dockerfile ../../.. \
 		--provenance false \
@@ -37,13 +39,11 @@ image-cozystack:
 		yq -i '.cozystack.image = strenv(IMAGE)' values.yaml
 	rm -f images/cozystack.json
 
-image-talos:
+image-talos: run-builder
 	test -f ../../../_out/assets/installer-amd64.tar || make talos-installer
-	docker load -i ../../../_out/assets/installer-amd64.tar
-	docker tag ghcr.io/siderolabs/installer:$(TALOS_VERSION) $(REGISTRY)/talos:$(call settag,$(TALOS_VERSION))
-	docker push $(REGISTRY)/talos:$(call settag,$(TALOS_VERSION))
+	skopeo copy docker-archive:../../../_out/assets/installer-amd64.tar docker://$(REGISTRY)/talos:$(call settag,$(TALOS_VERSION))
 
-image-matchbox:
+image-matchbox:  run-builder
 	test -f ../../../_out/assets/kernel-amd64 || make talos-kernel
 	test -f ../../../_out/assets/initramfs-metal-amd64.xz || make talos-initramfs
 	docker buildx build -f images/matchbox/Dockerfile ../../.. \
@@ -62,5 +62,8 @@ assets: talos-iso talos-nocloud talos-metal
 talos-initramfs talos-kernel talos-installer talos-iso talos-nocloud talos-metal:
 	mkdir -p ../../../_out/assets
 	cat images/talos/profiles/$(subst talos-,,$@).yaml | \
-		docker run --rm -i -v /dev:/dev --privileged "ghcr.io/siderolabs/imager:$(TALOS_VERSION)" --tar-to-stdout - | \
+		kubectl exec -i -n cozy-builder deploy/builder-talos-imager -- imager --tar-to-stdout - | \
 		tar -C ../../../_out/assets -xzf-
+
+run-builder:
+	make -C ../builder/ apply wait-for-builder
diff --git a/packages/core/platform/bundles/distro-full.yaml b/packages/core/platform/bundles/distro-full.yaml
index 26ef8aac..c232cc3f 100644
--- a/packages/core/platform/bundles/distro-full.yaml
+++ b/packages/core/platform/bundles/distro-full.yaml
@@ -37,6 +37,17 @@ releases:
   namespace: cozy-cert-manager
   dependsOn: [cilium]
 
+- name: cozystack-controller
+  releaseName: cozystack-controller
+  chart: cozy-cozystack-controller
+  namespace: cozy-system
+  dependsOn: [cilium]
+  {{- if eq (index $cozyConfig.data "telemetry-enabled") "false" }}
+  values:
+    cozystackController:
+      disableTelemetry: true
+  {{- end }}
+
 - name: cert-manager
   releaseName: cert-manager
   chart: cozy-cert-manager
diff --git a/packages/core/platform/bundles/distro-hosted.yaml b/packages/core/platform/bundles/distro-hosted.yaml
index 7b138a8d..eb826b16 100644
--- a/packages/core/platform/bundles/distro-hosted.yaml
+++ b/packages/core/platform/bundles/distro-hosted.yaml
@@ -20,6 +20,17 @@ releases:
   namespace: cozy-cert-manager
   dependsOn: []
 
+- name: cozystack-controller
+  releaseName: cozystack-controller
+  chart: cozy-cozystack-controller
+  namespace: cozy-system
+  dependsOn: [cilium]
+  {{- if eq (index $cozyConfig.data "telemetry-enabled") "false" }}
+  values:
+    cozystackController:
+      disableTelemetry: true
+  {{- end }}
+
 - name: cert-manager
   releaseName: cert-manager
   chart: cozy-cert-manager
diff --git a/packages/core/platform/bundles/paas-full.yaml b/packages/core/platform/bundles/paas-full.yaml
index ff8cbef4..8563824f 100644
--- a/packages/core/platform/bundles/paas-full.yaml
+++ b/packages/core/platform/bundles/paas-full.yaml
@@ -62,6 +62,17 @@ releases:
   namespace: cozy-system
   dependsOn: [cilium,kubeovn]
 
+- name: cozystack-controller
+  releaseName: cozystack-controller
+  chart: cozy-cozystack-controller
+  namespace: cozy-system
+  dependsOn: [cilium,kubeovn]
+  {{- if eq (index $cozyConfig.data "telemetry-enabled") "false" }}
+  values:
+    cozystackController:
+      disableTelemetry: true
+  {{- end }}
+
 - name: cert-manager
   releaseName: cert-manager
   chart: cozy-cert-manager
diff --git a/packages/core/platform/bundles/paas-hosted.yaml b/packages/core/platform/bundles/paas-hosted.yaml
index 6b218497..c986a06b 100644
--- a/packages/core/platform/bundles/paas-hosted.yaml
+++ b/packages/core/platform/bundles/paas-hosted.yaml
@@ -35,6 +35,17 @@ releases:
   namespace: cozy-system
   dependsOn: []
 
+- name: cozystack-controller
+  releaseName: cozystack-controller
+  chart: cozy-cozystack-controller
+  namespace: cozy-system
+  dependsOn: []
+  {{- if eq (index $cozyConfig.data "telemetry-enabled") "false" }}
+  values:
+    cozystackController:
+      disableTelemetry: true
+  {{- end }}
+
 - name: cert-manager
   releaseName: cert-manager
   chart: cozy-cert-manager
diff --git a/packages/extra/etcd/Chart.yaml b/packages/extra/etcd/Chart.yaml
index 4f260e04..b961617c 100644
--- a/packages/extra/etcd/Chart.yaml
+++ b/packages/extra/etcd/Chart.yaml
@@ -3,4 +3,4 @@ name: etcd
 description: Storage for Kubernetes clusters
 icon: /logos/etcd.svg
 type: application
-version: 2.3.0
+version: 2.4.0
diff --git a/packages/extra/etcd/templates/dashboard-resourcemap.yaml b/packages/extra/etcd/templates/dashboard-resourcemap.yaml
new file mode 100644
index 00000000..25a0dfd3
--- /dev/null
+++ b/packages/extra/etcd/templates/dashboard-resourcemap.yaml
@@ -0,0 +1,19 @@
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+  name: {{ .Release.Name }}-dashboard-resources
+rules:
+- apiGroups:
+  - ""
+  resources:
+  - services
+  resourceNames:
+  - etcd
+  verbs: ["get", "list", "watch"]
+- apiGroups:
+  - cozystack.io
+  resources:
+  - workloadmonitors
+  resourceNames:
+  - {{ .Release.Name }}
+  verbs: ["get", "list", "watch"]
diff --git a/packages/extra/etcd/templates/etcd-cluster.yaml b/packages/extra/etcd/templates/etcd-cluster.yaml
index 025363a2..d90efe2a 100644
--- a/packages/extra/etcd/templates/etcd-cluster.yaml
+++ b/packages/extra/etcd/templates/etcd-cluster.yaml
@@ -193,3 +193,19 @@ spec:
   issuerRef:
     name: etcd-issuer
     kind: Issuer
+---
+apiVersion: cozystack.io/v1alpha1
+kind: WorkloadMonitor
+metadata:
+  name: {{ $.Release.Name }}
+  namespace: {{ $.Release.Namespace }}
+spec:
+  replicas: {{ .Values.replicas }}
+  minReplicas: {{ div .Values.replicas 2 | add1 }}
+  kind: etcd
+  type: etcd
+  selector:
+    app.kubernetes.io/instance: etcd
+    app.kubernetes.io/managed-by: etcd-operator
+    app.kubernetes.io/name: etcd
+  version: {{ $.Chart.Version }}
diff --git a/packages/extra/ingress/Chart.yaml b/packages/extra/ingress/Chart.yaml
index f0ff843f..d9524db2 100644
--- a/packages/extra/ingress/Chart.yaml
+++ b/packages/extra/ingress/Chart.yaml
@@ -3,4 +3,4 @@ name: ingress
 description: NGINX Ingress Controller
 icon: /logos/ingress-nginx.svg
 type: application
-version: 1.3.0
+version: 1.4.0
diff --git a/packages/extra/ingress/templates/dashboard-resourcemap.yaml b/packages/extra/ingress/templates/dashboard-resourcemap.yaml
new file mode 100644
index 00000000..38b9a033
--- /dev/null
+++ b/packages/extra/ingress/templates/dashboard-resourcemap.yaml
@@ -0,0 +1,19 @@
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+  name: {{ .Release.Name }}-dashboard-resources
+rules:
+- apiGroups:
+  - ""
+  resources:
+  - services
+  resourceNames:
+  - {{ trimPrefix "tenant-" .Release.Namespace }}-ingress-controller
+  verbs: ["get", "list", "watch"]
+- apiGroups:
+  - cozystack.io
+  resources:
+  - workloadmonitors
+  resourceNames:
+  - {{ .Release.Name }}
+  verbs: ["get", "list", "watch"]
diff --git a/packages/extra/ingress/templates/workloadmonitor.yaml b/packages/extra/ingress/templates/workloadmonitor.yaml
new file mode 100644
index 00000000..c3f286f6
--- /dev/null
+++ b/packages/extra/ingress/templates/workloadmonitor.yaml
@@ -0,0 +1,16 @@
+---
+apiVersion: cozystack.io/v1alpha1
+kind: WorkloadMonitor
+metadata:
+  name: {{ $.Release.Name }}
+  namespace: {{ $.Release.Namespace }}
+spec:
+  replicas: {{ .Values.replicas }}
+  minReplicas: {{ div .Values.replicas 2 | add1 }}
+  kind: ingress
+  type: controller
+  selector:
+    app.kubernetes.io/component: controller
+    app.kubernetes.io/instance: ingress-nginx-system
+    app.kubernetes.io/name: ingress-nginx
+  version: {{ $.Chart.Version }}
diff --git a/packages/extra/monitoring/Chart.yaml b/packages/extra/monitoring/Chart.yaml
index 02a70f33..e7cf49d0 100644
--- a/packages/extra/monitoring/Chart.yaml
+++ b/packages/extra/monitoring/Chart.yaml
@@ -3,4 +3,4 @@ name: monitoring
 description: Monitoring and observability stack
 icon: /logos/monitoring.svg
 type: application
-version: 1.5.3
+version: 1.6.0
diff --git a/packages/extra/monitoring/templates/alerta/alerta-db.yaml b/packages/extra/monitoring/templates/alerta/alerta-db.yaml
index e06c322c..91c4e4aa 100644
--- a/packages/extra/monitoring/templates/alerta/alerta-db.yaml
+++ b/packages/extra/monitoring/templates/alerta/alerta-db.yaml
@@ -14,3 +14,17 @@ spec:
   inheritedMetadata:
     labels:
       policy.cozystack.io/allow-to-apiserver: "true"
+---
+apiVersion: cozystack.io/v1alpha1
+kind: WorkloadMonitor
+metadata:
+  name: alerta-db
+spec:
+  replicas: 2
+  minReplicas: 1
+  kind: monitoring
+  type: postgres
+  selector:
+    cnpg.io/cluster: alerta-db
+    cnpg.io/podRole: instance
+  version: {{ $.Chart.Version }}
diff --git a/packages/extra/monitoring/templates/alerta/alerta.yaml b/packages/extra/monitoring/templates/alerta/alerta.yaml
index 928acb82..8158eed6 100644
--- a/packages/extra/monitoring/templates/alerta/alerta.yaml
+++ b/packages/extra/monitoring/templates/alerta/alerta.yaml
@@ -170,6 +170,20 @@ spec:
                 port:
                   name: http
 ---
+apiVersion: cozystack.io/v1alpha1
+kind: WorkloadMonitor
+metadata:
+  name: alerta
+spec:
+  replicas: 1
+  minReplicas: 1
+  kind: monitoring
+  type: alerta
+  selector:
+    app: alerta
+    release: alerta
+  version: {{ $.Chart.Version }}
+---
 apiVersion: v1
 kind: Secret
 metadata:
@@ -217,3 +231,17 @@ spec:
   podMetadata:
     labels:
       policy.cozystack.io/allow-to-apiserver: "true"
+---
+apiVersion: cozystack.io/v1alpha1
+kind: WorkloadMonitor
+metadata:
+  name: alertmanager
+spec:
+  replicas: 3
+  minReplicas: 2
+  kind: monitoring
+  type: alertmanager
+  selector:
+    app.kubernetes.io/instance: alertmanager
+    app.kubernetes.io/name: vmalertmanager
+  version: {{ $.Chart.Version }}
diff --git a/packages/extra/monitoring/templates/dashboard-resourcemap.yaml b/packages/extra/monitoring/templates/dashboard-resourcemap.yaml
index f15714da..a13d18be 100644
--- a/packages/extra/monitoring/templates/dashboard-resourcemap.yaml
+++ b/packages/extra/monitoring/templates/dashboard-resourcemap.yaml
@@ -26,3 +26,28 @@ rules:
   - grafana-service
   - alerta
   verbs: ["get", "list", "watch"]
+- apiGroups:
+  - cozystack.io
+  resources:
+  - workloadmonitors
+  resourceNames:
+  - alerta
+  - grafana
+  - grafana-db
+  - alerta-db
+  - alermanager
+  {{- range .Values.metricsStorages }}
+  - {{ .name }}-vmstorage
+  - {{ .name }}-vmselect
+  - {{ .name }}-vminsert
+  {{- end }}
+  {{- range .Values.logsStorages }}
+  - {{ $.Release.Name }}-vlogs-{{ .name }}
+  {{- end }}
+  {{- range .Values.metricsStorages }}
+  - vmalert-{{ .name }}
+  {{- break }}
+  {{- end }}
+  verbs: ["get", "list", "watch"]
+
+
diff --git a/packages/extra/monitoring/templates/grafana/db.yaml b/packages/extra/monitoring/templates/grafana/db.yaml
index e35e5aa4..9ec952c3 100644
--- a/packages/extra/monitoring/templates/grafana/db.yaml
+++ b/packages/extra/monitoring/templates/grafana/db.yaml
@@ -10,3 +10,17 @@ spec:
   inheritedMetadata:
     labels:
       policy.cozystack.io/allow-to-apiserver: "true"
+---
+apiVersion: cozystack.io/v1alpha1
+kind: WorkloadMonitor
+metadata:
+  name: grafana-db
+spec:
+  replicas: 2
+  minReplicas: 1
+  kind: monitoring
+  type: postgres
+  selector:
+    cnpg.io/cluster: grafana-db
+    cnpg.io/podRole: instance
+  version: {{ $.Chart.Version }}
diff --git a/packages/extra/monitoring/templates/grafana/grafana.yaml b/packages/extra/monitoring/templates/grafana/grafana.yaml
index eb6a24ed..234618e3 100644
--- a/packages/extra/monitoring/templates/grafana/grafana.yaml
+++ b/packages/extra/monitoring/templates/grafana/grafana.yaml
@@ -114,3 +114,16 @@ spec:
       - hosts:
         - "{{ printf "grafana.%s" (.Values.host | default $host) }}"
         secretName: grafana-ingress-tls
+---
+apiVersion: cozystack.io/v1alpha1
+kind: WorkloadMonitor
+metadata:
+  name: grafana
+spec:
+  replicas: 2
+  minReplicas: 1
+  kind: monitoring
+  type: grafana
+  selector:
+    app: grafana
+  version: {{ $.Chart.Version }}
diff --git a/packages/extra/monitoring/templates/vlogs/vlogs.yaml b/packages/extra/monitoring/templates/vlogs/vlogs.yaml
index 331a55e0..8c9e40ac 100644
--- a/packages/extra/monitoring/templates/vlogs/vlogs.yaml
+++ b/packages/extra/monitoring/templates/vlogs/vlogs.yaml
@@ -12,4 +12,19 @@ spec:
     accessModes: [ReadWriteOnce]
   retentionPeriod: "{{ .retentionPeriod }}"
   removePvcAfterDelete: true
+---
+apiVersion: cozystack.io/v1alpha1
+kind: WorkloadMonitor
+metadata:
+  name: vlogs-{{ .name }}
+spec:
+  replicas: 1
+  minReplicas: 1
+  kind: monitoring
+  type: vlogs
+  selector:
+    app.kubernetes.io/component: monitoring
+    app.kubernetes.io/instance: {{ .name }}
+    app.kubernetes.io/name: vlogs
+  version: {{ $.Chart.Version }}
 {{- end }}
diff --git a/packages/extra/monitoring/templates/vm/vmalert.yaml b/packages/extra/monitoring/templates/vm/vmalert.yaml
index 9f87a80a..8db6a2ed 100644
--- a/packages/extra/monitoring/templates/vm/vmalert.yaml
+++ b/packages/extra/monitoring/templates/vm/vmalert.yaml
@@ -18,5 +18,19 @@ spec:
     url: http://vminsert-{{ .name }}.{{ $.Release.Namespace }}.svc:8480/insert/0/prometheus/api/v1/write
   resources: {}
   selectAllByDefault: true
+---
+apiVersion: cozystack.io/v1alpha1
+kind: WorkloadMonitor
+metadata:
+  name: vmalert-{{ .name }}
+spec:
+  replicas: 1
+  minReplicas: 1
+  kind: monitoring
+  type: vmalert
+  selector:
+    app.kubernetes.io/instance: vmalert-{{ .name }}
+    app.kubernetes.io/name: vmalert
+  version: {{ $.Chart.Version }}
 {{- break }}
 {{- end }}
diff --git a/packages/extra/monitoring/templates/vm/vmcluster.yaml b/packages/extra/monitoring/templates/vm/vmcluster.yaml
index 4c8c9420..1391e55e 100644
--- a/packages/extra/monitoring/templates/vm/vmcluster.yaml
+++ b/packages/extra/monitoring/templates/vm/vmcluster.yaml
@@ -6,22 +6,35 @@ metadata:
   name: {{ .name }}
 spec:
   replicationFactor: 2
-  retentionPeriod: "3"
+  retentionPeriod: {{ .retentionPeriod | quote }}
   vminsert:
     replicaCount: 2
-    resources: {}
+    resources:
+      {{- if empty .vminsert.resources }}
+      limits:
+        memory: 1000Mi
+      requests:
+        cpu: 100m
+        memory: 500Mi
+      {{- else }}
+      {{- toYaml .vminsert.resources | nindent 6 }}
+      {{- end }}
   vmselect:
     replicaCount: 2
     resources:
+      {{- if empty .vmselect.resources }}
       limits:
         memory: 1000Mi
       requests:
         cpu: 100m
         memory: 500Mi
+      {{- else }}
+      {{- toYaml .vmselect.resources | nindent 6 }}
+      {{- end }}
     extraArgs:
       search.maxUniqueTimeseries: "600000"
       vmalert.proxyURL: http://vmalert-{{ .name }}.{{ $.Release.Namespace }}.svc:8080
-      dedup.minScrapeInterval: "15s"
+      dedup.minScrapeInterval: {{ .deduplicationInterval | quote}}
     cacheMountPath: /select-cache
     storage:
       volumeClaimTemplate:
@@ -35,11 +48,15 @@ spec:
   vmstorage:
     replicaCount: 2
     resources:
+      {{- if empty .vmstorage.resources }}
       limits:
-        memory: 1000Mi
+        memory: 2048Mi
       requests:
         cpu: 100m
         memory: 500Mi
+      {{- else }}
+      {{- toYaml .vmstorage.resources | nindent 6 }}
+      {{- end }}
     storage:
       volumeClaimTemplate:
         spec:
@@ -50,4 +67,49 @@ spec:
             requests:
               storage: {{ .storage }}
     storageDataPath: /vm-data
+---
+apiVersion: cozystack.io/v1alpha1
+kind: WorkloadMonitor
+metadata:
+  name: {{ .name }}-vmstorage
+spec:
+  replicas: 2
+  minReplicas: 1
+  kind: monitoring
+  type: vmstorage
+  selector:
+    app.kubernetes.io/component: monitoring
+    app.kubernetes.io/instance: {{ .name }}
+    app.kubernetes.io/name: vmstorage
+  version: {{ $.Chart.Version }}
+---
+apiVersion: cozystack.io/v1alpha1
+kind: WorkloadMonitor
+metadata:
+  name: {{ .name }}-vmselect
+spec:
+  replicas: 2
+  minReplicas: 1
+  kind: monitoring
+  type: vmselect
+  selector:
+    app.kubernetes.io/component: monitoring
+    app.kubernetes.io/instance: {{ .name }}
+    app.kubernetes.io/name: vmselect
+  version: {{ $.Chart.Version }}
+---
+apiVersion: cozystack.io/v1alpha1
+kind: WorkloadMonitor
+metadata:
+  name: {{ .name }}-vminsert
+spec:
+  replicas: 2
+  minReplicas: 1
+  kind: monitoring
+  type: vminsert
+  selector:
+    app.kubernetes.io/component: monitoring
+    app.kubernetes.io/instance: {{ .name }}
+    app.kubernetes.io/name: vminsert
+  version: {{ $.Chart.Version }}
 {{- end }}
diff --git a/packages/extra/monitoring/values.yaml b/packages/extra/monitoring/values.yaml
index 4812844e..3633b622 100644
--- a/packages/extra/monitoring/values.yaml
+++ b/packages/extra/monitoring/values.yaml
@@ -5,17 +5,59 @@ host: ""
 
 ## @param metricsStorages [array] Configuration of metrics storage instances
 ##
+## Example:
+## metricsStorages:
+## - name: shortterm
+##   retentionPeriod: "3d"
+##   deduplicationInterval: "15s"
+##   storage: 10Gi
+##   storageClassName: ""
+##   vminsert:
+##     resources:
+##       limits:
+##         memory: 1024Mi
+##       requests:
+##         cpu: 200m
+##         memory: 512Mi
+##   vmselect:
+##     resources:
+##       limits:
+##         memory: 2048Mi
+##       requests:
+##         cpu: 300m
+##         memory: 1Gi
+##   vmstorage:
+##     resources:
+##       limits:
+##         memory: 4096Mi
+##       requests:
+##         cpu: 500m
+##         memory: 2Gi
+##
 metricsStorages:
 - name: shortterm
   retentionPeriod: "3d"
-  deduplicationInterval: "5m"
+  deduplicationInterval: "15s"
   storage: 10Gi
   storageClassName: ""
+  vminsert:
+    resources: {}
+  vmselect:
+    resources: {}
+  vmstorage:
+    resources: {}
 - name: longterm
   retentionPeriod: "14d"
-  deduplicationInterval: "15s"
+  deduplicationInterval: "5m"
   storage: 10Gi
   storageClassName: ""
+  vminsert:
+    resources: {}
+  vmselect:
+    resources: {}
+  vmstorage:
+    resources: {}
+
 
 ## @param logsStorages [array] Configuration of logs storage instances
 ##
diff --git a/packages/extra/seaweedfs/Chart.yaml b/packages/extra/seaweedfs/Chart.yaml
index c3f187ef..185ebc9c 100644
--- a/packages/extra/seaweedfs/Chart.yaml
+++ b/packages/extra/seaweedfs/Chart.yaml
@@ -16,7 +16,7 @@ type: application
 # This is the chart version. This version number should be incremented each time you make changes
 # to the chart and its templates, including the app version.
 # Versions are expected to follow Semantic Versioning (https://semver.org/)
-version: 0.2.1
+version: 0.3.0
 
 # This is the version number of the application being deployed. This version number should be
 # incremented each time you make changes to the application. Versions are not expected to
diff --git a/packages/extra/seaweedfs/templates/dashboard-resourcemap.yaml b/packages/extra/seaweedfs/templates/dashboard-resourcemap.yaml
new file mode 100644
index 00000000..49b55aad
--- /dev/null
+++ b/packages/extra/seaweedfs/templates/dashboard-resourcemap.yaml
@@ -0,0 +1,29 @@
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+  name: {{ .Release.Name }}-dashboard-resources
+rules:
+- apiGroups:
+  - ""
+  resources:
+  - services
+  resourceNames:
+  - {{ $.Release.Name }}-s3
+  verbs: ["get", "list", "watch"]
+- apiGroups:
+  - networking.k8s.io
+  resources:
+  - ingresses
+  resourceNames:
+  - ingress-{{ $.Release.Name }}-s3
+  verbs: ["get", "list", "watch"]
+- apiGroups:
+  - cozystack.io
+  resources:
+  - workloadmonitors
+  resourceNames:
+  - {{ $.Release.Name }}-master
+  - {{ $.Release.Name }}-filer
+  - {{ $.Release.Name }}-volume
+  - {{ $.Release.Name }}-db
+  verbs: ["get", "list", "watch"]
diff --git a/packages/extra/seaweedfs/templates/seaweedfs.yaml b/packages/extra/seaweedfs/templates/seaweedfs.yaml
index 70cf06d2..5ab911b5 100644
--- a/packages/extra/seaweedfs/templates/seaweedfs.yaml
+++ b/packages/extra/seaweedfs/templates/seaweedfs.yaml
@@ -60,3 +60,59 @@ spec:
       cosi:
         driverName: "{{ .Release.Namespace }}.seaweedfs.objectstorage.k8s.io"
         bucketClassName: "{{ .Release.Namespace }}"
+---
+apiVersion: cozystack.io/v1alpha1
+kind: WorkloadMonitor
+metadata:
+  name: {{ $.Release.Name }}-master
+spec:
+  replicas: 3
+  minReplicas: 2
+  kind: seaweedfs
+  type: master
+  selector:
+    app.kubernetes.io/component: master
+    app.kubernetes.io/name: seaweedfs
+  version: {{ $.Chart.Version }}
+---
+apiVersion: cozystack.io/v1alpha1
+kind: WorkloadMonitor
+metadata:
+  name: {{ $.Release.Name }}-filer
+spec:
+  replicas: 2
+  minReplicas: 1
+  kind: seaweedfs
+  type: filer
+  selector:
+    app.kubernetes.io/component: filer
+    app.kubernetes.io/name: seaweedfs
+  version: {{ $.Chart.Version }}
+---
+apiVersion: cozystack.io/v1alpha1
+kind: WorkloadMonitor
+metadata:
+  name: {{ $.Release.Name }}-volume
+spec:
+  replicas: {{ .Values.replicas }}
+  minReplicas: {{ div .Values.replicas 2 | add1 }}
+  kind: seaweedfs
+  type: volume
+  selector:
+    app.kubernetes.io/component: volume
+    app.kubernetes.io/name: seaweedfs
+  version: {{ $.Chart.Version }}
+---
+apiVersion: cozystack.io/v1alpha1
+kind: WorkloadMonitor
+metadata:
+  name: {{ $.Release.Name }}-db
+spec:
+  replicas: 2
+  minReplicas: 1
+  kind: seaweedfs
+  type: postgres
+  selector:
+    cnpg.io/cluster: seaweedfs-db
+    cnpg.io/podRole: instance
+  version: {{ $.Chart.Version }}
diff --git a/packages/extra/versions_map b/packages/extra/versions_map
index bbeb158f..675ebd20 100644
--- a/packages/extra/versions_map
+++ b/packages/extra/versions_map
@@ -3,11 +3,13 @@ etcd 2.0.0 a6d0f7cf
 etcd 2.0.1 6fc1cc7d
 etcd 2.1.0 2b00fcf8
 etcd 2.2.0 5ca8823
-etcd 2.3.0 HEAD
+etcd 2.3.0 b908400d
+etcd 2.4.0 HEAD
 ingress 1.0.0 f642698
 ingress 1.1.0 838bee5d
 ingress 1.2.0 ced8e5b
-ingress 1.3.0 HEAD
+ingress 1.3.0 edbbb9be
+ingress 1.4.0 HEAD
 monitoring 1.0.0 f642698
 monitoring 1.1.0 15478a88
 monitoring 1.2.0 c9e0d63b
@@ -17,7 +19,10 @@ monitoring 1.4.0 adaf603b
 monitoring 1.5.0 4b90bf5a
 monitoring 1.5.1 57e90b70
 monitoring 1.5.2 898374b5
-monitoring 1.5.3 HEAD
+monitoring 1.5.3 c1ca19dc
+monitoring 1.5.4 d4634797
+monitoring 1.6.0 HEAD
 seaweedfs 0.1.0 5ca8823
 seaweedfs 0.2.0 9e33dc0
-seaweedfs 0.2.1 HEAD
+seaweedfs 0.2.1 249bf35
+seaweedfs 0.3.0 HEAD
diff --git a/packages/system/cozystack-api/images/cozystack-api/Dockerfile b/packages/system/cozystack-api/images/cozystack-api/Dockerfile
index 176420fb..6b8e5cc1 100644
--- a/packages/system/cozystack-api/images/cozystack-api/Dockerfile
+++ b/packages/system/cozystack-api/images/cozystack-api/Dockerfile
@@ -1,4 +1,4 @@
-FROM golang:alpine3.19 as builder
+FROM golang:1.23-alpine as builder
 
 WORKDIR /workspace
 
@@ -8,6 +8,7 @@ RUN go mod download
 COPY api api/
 COPY pkg pkg/
 COPY cmd cmd/
+COPY internal internal/
 
 RUN CGO_ENABLED=0 go build -ldflags="-extldflags=-static" -o /cozystack-api cmd/cozystack-api/main.go
 
diff --git a/packages/system/cozystack-api/templates/deployment.yaml b/packages/system/cozystack-api/templates/deployment.yaml
index fbcb2f5f..3dd93bd8 100644
--- a/packages/system/cozystack-api/templates/deployment.yaml
+++ b/packages/system/cozystack-api/templates/deployment.yaml
@@ -6,7 +6,7 @@ metadata:
   labels:
     app: cozystack-api
 spec:
-  replicas: 1
+  replicas: 2
   selector:
     matchLabels:
       app: cozystack-api
diff --git a/packages/system/cozystack-controller/.gitignore b/packages/system/cozystack-controller/.gitignore
new file mode 100644
index 00000000..ada68ff0
--- /dev/null
+++ b/packages/system/cozystack-controller/.gitignore
@@ -0,0 +1,27 @@
+# Binaries for programs and plugins
+*.exe
+*.exe~
+*.dll
+*.so
+*.dylib
+bin/*
+Dockerfile.cross
+
+# Test binary, built with `go test -c`
+*.test
+
+# Output of the go coverage tool, specifically when used with LiteIDE
+*.out
+
+# Go workspace file
+go.work
+
+# Kubernetes Generated files - skip generated files, except for vendored files
+!vendor/**/zz_generated.*
+
+# editor and IDE paraphernalia
+.idea
+.vscode
+*.swp
+*.swo
+*~
diff --git a/packages/system/cozystack-controller/Chart.yaml b/packages/system/cozystack-controller/Chart.yaml
new file mode 100644
index 00000000..66080059
--- /dev/null
+++ b/packages/system/cozystack-controller/Chart.yaml
@@ -0,0 +1,3 @@
+apiVersion: v2
+name: cozy-cozystack-controller
+version: 0.0.0 # Placeholder, the actual version will be automatically set during the build process
diff --git a/packages/system/cozystack-controller/Makefile b/packages/system/cozystack-controller/Makefile
new file mode 100644
index 00000000..22bc1fef
--- /dev/null
+++ b/packages/system/cozystack-controller/Makefile
@@ -0,0 +1,24 @@
+NAME=cozystack-controller
+NAMESPACE=cozy-system
+
+include ../../../scripts/common-envs.mk
+include ../../../scripts/package.mk
+
+image: image-cozystack-controller update-version
+
+image-cozystack-controller:
+	docker buildx build -f images/cozystack-controller/Dockerfile ../../.. \
+		--provenance false \
+		--tag $(REGISTRY)/cozystack-controller:$(call settag,$(TAG)) \
+		--cache-from type=registry,ref=$(REGISTRY)/cozystack-controller:latest \
+		--cache-to type=inline \
+		--metadata-file images/cozystack-controller.json \
+		--push=$(PUSH) \
+		--load=$(LOAD)
+	IMAGE="$(REGISTRY)/cozystack-controller:$(call settag,$(TAG))@$$(yq e '."containerimage.digest"' images/cozystack-controller.json -o json -r)" \
+		yq -i '.cozystackController.image = strenv(IMAGE)' values.yaml
+	rm -f images/cozystack-controller.json
+
+update-version:
+	TAG="$(call settag,$(TAG))" \
+		yq -i '.cozystackController.cozystackVersion = strenv(TAG)' values.yaml
diff --git a/packages/system/cozystack-controller/images/cozystack-controller/Dockerfile b/packages/system/cozystack-controller/images/cozystack-controller/Dockerfile
new file mode 100644
index 00000000..c289c8d2
--- /dev/null
+++ b/packages/system/cozystack-controller/images/cozystack-controller/Dockerfile
@@ -0,0 +1,20 @@
+FROM golang:1.23-alpine as builder
+
+WORKDIR /workspace
+
+COPY go.mod go.sum ./
+RUN go mod download
+
+COPY api api/
+COPY pkg pkg/
+COPY cmd cmd/
+COPY internal internal/
+
+RUN CGO_ENABLED=0 go build -ldflags="-extldflags=-static" -o /cozystack-controller cmd/cozystack-controller/main.go
+
+FROM scratch
+
+COPY --from=builder /cozystack-controller /cozystack-controller
+COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
+
+ENTRYPOINT ["/cozystack-controller"]
diff --git a/packages/system/cozystack-controller/templates/crds/cozystack.io_workloadmonitors.yaml b/packages/system/cozystack-controller/templates/crds/cozystack.io_workloadmonitors.yaml
new file mode 100644
index 00000000..5c1318c0
--- /dev/null
+++ b/packages/system/cozystack-controller/templates/crds/cozystack.io_workloadmonitors.yaml
@@ -0,0 +1,117 @@
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.16.4
+  name: workloadmonitors.cozystack.io
+spec:
+  group: cozystack.io
+  names:
+    kind: WorkloadMonitor
+    listKind: WorkloadMonitorList
+    plural: workloadmonitors
+    singular: workloadmonitor
+  scope: Namespaced
+  versions:
+  - additionalPrinterColumns:
+    - jsonPath: .spec.kind
+      name: Kind
+      type: string
+    - jsonPath: .spec.type
+      name: Type
+      type: string
+    - jsonPath: .spec.version
+      name: Version
+      type: string
+    - jsonPath: .spec.replicas
+      name: Replicas
+      type: integer
+    - jsonPath: .spec.minReplicas
+      name: MinReplicas
+      type: integer
+    - jsonPath: .status.availableReplicas
+      name: Available
+      type: integer
+    - jsonPath: .status.observedReplicas
+      name: Observed
+      type: integer
+    - jsonPath: .status.operational
+      name: Operational
+      type: boolean
+    name: v1alpha1
+    schema:
+      openAPIV3Schema:
+        description: WorkloadMonitor is the Schema for the workloadmonitors API
+        properties:
+          apiVersion:
+            description: |-
+              APIVersion defines the versioned schema of this representation of an object.
+              Servers should convert recognized schemas to the latest internal value, and
+              may reject unrecognized values.
+              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
+            type: string
+          kind:
+            description: |-
+              Kind is a string value representing the REST resource this object represents.
+              Servers may infer this from the endpoint the client submits requests to.
+              Cannot be updated.
+              In CamelCase.
+              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
+            type: string
+          metadata:
+            type: object
+          spec:
+            description: WorkloadMonitorSpec defines the desired state of WorkloadMonitor
+            properties:
+              kind:
+                description: Kind specifies the kind of the workload
+                type: string
+              minReplicas:
+                description: MinReplicas specifies the minimum number of replicas
+                  that should be available
+                format: int32
+                minimum: 0
+                type: integer
+              replicas:
+                description: |-
+                  Replicas is the desired number of replicas
+                  If not specified, will use observedReplicas as the target
+                format: int32
+                minimum: 0
+                type: integer
+              selector:
+                additionalProperties:
+                  type: string
+                description: Selector is a label selector to find workloads to monitor
+                type: object
+              type:
+                description: Type specifies the type of the workload
+                type: string
+              version:
+                description: Version specifies the version of the workload
+                type: string
+            required:
+            - selector
+            type: object
+          status:
+            description: WorkloadMonitorStatus defines the observed state of WorkloadMonitor
+            properties:
+              availableReplicas:
+                description: AvailableReplicas is the number of ready replicas
+                format: int32
+                type: integer
+              observedReplicas:
+                description: ObservedReplicas is the total number of pods observed
+                format: int32
+                type: integer
+              operational:
+                description: Operational indicates if the workload meets all operational
+                  requirements
+                type: boolean
+            type: object
+        type: object
+    served: true
+    storage: true
+    subresources:
+      status: {}
diff --git a/packages/system/cozystack-controller/templates/crds/cozystack.io_workloads.yaml b/packages/system/cozystack-controller/templates/crds/cozystack.io_workloads.yaml
new file mode 100644
index 00000000..d09d4902
--- /dev/null
+++ b/packages/system/cozystack-controller/templates/crds/cozystack.io_workloads.yaml
@@ -0,0 +1,88 @@
+---
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+  annotations:
+    controller-gen.kubebuilder.io/version: v0.16.4
+  name: workloads.cozystack.io
+spec:
+  group: cozystack.io
+  names:
+    kind: Workload
+    listKind: WorkloadList
+    plural: workloads
+    singular: workload
+  scope: Namespaced
+  versions:
+  - additionalPrinterColumns:
+    - jsonPath: .status.kind
+      name: Kind
+      type: string
+    - jsonPath: .status.type
+      name: Type
+      type: string
+    - jsonPath: .status.resources.cpu
+      name: CPU
+      type: string
+    - jsonPath: .status.resources.memory
+      name: Memory
+      type: string
+    - jsonPath: .status.operational
+      name: Operational
+      type: boolean
+    name: v1alpha1
+    schema:
+      openAPIV3Schema:
+        description: Workload is the Schema for the workloads API
+        properties:
+          apiVersion:
+            description: |-
+              APIVersion defines the versioned schema of this representation of an object.
+              Servers should convert recognized schemas to the latest internal value, and
+              may reject unrecognized values.
+              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
+            type: string
+          kind:
+            description: |-
+              Kind is a string value representing the REST resource this object represents.
+              Servers may infer this from the endpoint the client submits requests to.
+              Cannot be updated.
+              In CamelCase.
+              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
+            type: string
+          metadata:
+            type: object
+          status:
+            description: WorkloadStatus defines the observed state of Workload
+            properties:
+              kind:
+                description: Kind represents the type of workload (redis, postgres,
+                  etc.)
+                type: string
+              operational:
+                description: Operational indicates if all pods of the workload are
+                  ready
+                type: boolean
+              resources:
+                additionalProperties:
+                  anyOf:
+                  - type: integer
+                  - type: string
+                  pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
+                  x-kubernetes-int-or-string: true
+                description: Resources specifies the compute resources allocated to
+                  this workload
+                type: object
+              type:
+                description: |-
+                  Type represents the specific role of the workload (redis, sentinel, etc.)
+                  If not specified, defaults to Kind
+                type: string
+            required:
+            - kind
+            - resources
+            type: object
+        type: object
+    served: true
+    storage: true
+    subresources: {}
diff --git a/packages/system/cozystack-controller/templates/deployment.yaml b/packages/system/cozystack-controller/templates/deployment.yaml
new file mode 100644
index 00000000..33b309df
--- /dev/null
+++ b/packages/system/cozystack-controller/templates/deployment.yaml
@@ -0,0 +1,31 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: cozystack-controller
+  namespace: cozy-system
+  labels:
+    app: cozystack-controller
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: cozystack-controller
+  template:
+    metadata:
+      labels:
+        app: cozystack-controller
+    spec:
+      serviceAccountName: cozystack-controller
+      containers:
+      - name: cozystack-controller
+        image: "{{ .Values.cozystackController.image }}"
+        args:
+        - --cozystack-version={{ .Values.cozystackController.cozystackVersion }}
+        {{- if .Values.cozystackController.debug }}
+        - --zap-log-level=debug
+        {{- else }}
+        - --zap-log-level=info
+        {{- end }}
+        {{- if .Values.cozystackController.disableTelemetry }}
+        - --disable-telemetry
+        {{- end }}
diff --git a/packages/system/cozystack-controller/templates/rbac-bind.yaml b/packages/system/cozystack-controller/templates/rbac-bind.yaml
new file mode 100644
index 00000000..67661e62
--- /dev/null
+++ b/packages/system/cozystack-controller/templates/rbac-bind.yaml
@@ -0,0 +1,12 @@
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+  name: cozystack-controller
+roleRef:
+  apiGroup: rbac.authorization.k8s.io
+  kind: ClusterRole
+  name: cozystack-controller
+subjects:
+- kind: ServiceAccount
+  name: cozystack-controller
+  namespace: cozy-system
diff --git a/packages/system/cozystack-controller/templates/rbac.yaml b/packages/system/cozystack-controller/templates/rbac.yaml
new file mode 100644
index 00000000..be3f28f8
--- /dev/null
+++ b/packages/system/cozystack-controller/templates/rbac.yaml
@@ -0,0 +1,11 @@
+kind: ClusterRole
+apiVersion: rbac.authorization.k8s.io/v1
+metadata:
+  name: cozystack-controller
+rules:
+- apiGroups: [""]
+  resources: ["configmaps", "pods", "namespaces", "nodes", "services", "persistentvolumes"]
+  verbs: ["get", "watch", "list"]
+- apiGroups: ['cozystack.io']
+  resources: ['*']
+  verbs: ['*']
diff --git a/packages/system/cozystack-controller/templates/sa.yaml b/packages/system/cozystack-controller/templates/sa.yaml
new file mode 100644
index 00000000..55ef1c4a
--- /dev/null
+++ b/packages/system/cozystack-controller/templates/sa.yaml
@@ -0,0 +1,5 @@
+kind: ServiceAccount
+apiVersion: v1
+metadata:
+  name: cozystack-controller
+  namespace: cozy-system
diff --git a/packages/system/cozystack-controller/values.yaml b/packages/system/cozystack-controller/values.yaml
new file mode 100644
index 00000000..a9a7b543
--- /dev/null
+++ b/packages/system/cozystack-controller/values.yaml
@@ -0,0 +1,5 @@
+cozystackController:
+  image: ghcr.io/aenix-io/cozystack/cozystack-controller:latest@sha256:380bb9d4402b2f2025dec29e0dccca2a6a456cf9b66b3553b328fb4498f3fea3
+  debug: false
+  disableTelemetry: false
+  cozystackVersion: "latest"
diff --git a/packages/system/dashboard/charts/kubeapps/templates/dashboard/configmap.yaml b/packages/system/dashboard/charts/kubeapps/templates/dashboard/configmap.yaml
index 4d66428c..b1f1891b 100644
--- a/packages/system/dashboard/charts/kubeapps/templates/dashboard/configmap.yaml
+++ b/packages/system/dashboard/charts/kubeapps/templates/dashboard/configmap.yaml
@@ -76,7 +76,7 @@ data:
       "kubeappsNamespace": {{ .Release.Namespace | quote }},
       "helmGlobalNamespace": {{ include "kubeapps.helmGlobalPackagingNamespace" . | quote }},
       "carvelGlobalNamespace": {{ .Values.kubeappsapis.pluginConfig.kappController.packages.v1alpha1.globalPackagingNamespace | quote }},
-      "appVersion": "v0.21.1",
+      "appVersion": "latest",
       "authProxyEnabled": {{ .Values.authProxy.enabled }},
       "oauthLoginURI": {{ .Values.authProxy.oauthLoginURI | quote }},
       "oauthLogoutURI": {{ .Values.authProxy.oauthLogoutURI | quote }},
diff --git a/packages/system/dashboard/images/dashboard/Dockerfile b/packages/system/dashboard/images/dashboard/Dockerfile
index af3f571e..1c2e3617 100644
--- a/packages/system/dashboard/images/dashboard/Dockerfile
+++ b/packages/system/dashboard/images/dashboard/Dockerfile
@@ -1,29 +1,8 @@
-# Copyright 2018-2023 the Kubeapps contributors.
-# SPDX-License-Identifier: Apache-2.0
-
 FROM bitnami/node:20.15.1 AS build
 WORKDIR /app
 
-ARG VERSION=2.12.0
-RUN wget -O- https://github.com/vmware-tanzu/kubeapps/archive/refs/tags/v${VERSION}.tar.gz | tar xzf - --strip-components=2 kubeapps-${VERSION}/dashboard
-
-COPY apple-touch-icon.png favicon-16x16.png favicon-32x32.png favicon.ico mstile-144x144.png mstile-150x150.png mstile-310x150.png mstile-310x310.png mstile-70x70.png safari-pinned-tab.svg public/
-
-COPY release-url.diff /patches/release-url.diff
-COPY remove-manage-repositories.diff /patches/remove-manage-repositories.diff
-COPY documentation.diff /patches/documentation.diff
-RUN patch -p2 < /patches/release-url.diff
-RUN patch -p2 < /patches/remove-manage-repositories.diff
-#RUN patch -p2 < /patches/documentation.diff
-
-RUN sed -i 's/opacity: 0.05;/display: none;/g' src/components/InfoCard/InfoCard.scss
-RUN sed -i 's/"Kubeapps",/"Cozystack",/' lang/en.json
-RUN sed -i 's/Kubeapps/Cozystack/g' public/site.webmanifest
-RUN sed -i 's/Welcome To Kubeapps/Welcome To Cozystack/g' ./src/components/LoginForm/LoginForm.tsx ./src/components/AppList/AppListGrid.tsx ./src/components/AppList/AppList.test.tsx
-RUN sed -i '/by VMware/,/ \.$/d' src/components/Header/Menu.tsx
-RUN sed -i '/height: 4em;/ s/4em/4.5em/' src/components/Header/Header.scss
-RUN sed -i '/background-image/ s|base64,[^"]\+|base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgd2lkdGg9IjUyMSIKICAgaGVpZ2h0PSIxODUiCiAgIHZpZXdCb3g9IjAgMCA1MjEgMTg1IgogICBmaWxsPSJub25lIgogICB2ZXJzaW9uPSIxLjEiCiAgIGlkPSJzdmc5NTgiCiAgIHNvZGlwb2RpOmRvY25hbWU9IkNPWllTVEFDSy5zdmciCiAgIGlua3NjYXBlOnZlcnNpb249IjEuMS4xIChjMzA4NGVmLCAyMDIxLTA5LTIyKSIKICAgeG1sbnM6aW5rc2NhcGU9Imh0dHA6Ly93d3cuaW5rc2NhcGUub3JnL25hbWVzcGFjZXMvaW5rc2NhcGUiCiAgIHhtbG5zOnNvZGlwb2RpPSJodHRwOi8vc29kaXBvZGkuc291cmNlZm9yZ2UubmV0L0RURC9zb2RpcG9kaS0wLmR0ZCIKICAgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIgogICB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8ZGVmcwogICAgIGlkPSJkZWZzOTYyIiAvPgogIDxzb2RpcG9kaTpuYW1lZHZpZXcKICAgICBpZD0ibmFtZWR2aWV3OTYwIgogICAgIHBhZ2Vjb2xvcj0iI2ZmZmZmZiIKICAgICBib3JkZXJjb2xvcj0iIzY2NjY2NiIKICAgICBib3JkZXJvcGFjaXR5PSIxLjAiCiAgICAgaW5rc2NhcGU6cGFnZXNoYWRvdz0iMiIKICAgICBpbmtzY2FwZTpwYWdlb3BhY2l0eT0iMC4wIgogICAgIGlua3NjYXBlOnBhZ2VjaGVja2VyYm9hcmQ9IjAiCiAgICAgc2hvd2dyaWQ9ImZhbHNlIgogICAgIHNob3dndWlkZXM9InRydWUiCiAgICAgaW5rc2NhcGU6Z3VpZGUtYmJveD0idHJ1ZSIKICAgICBpbmtzY2FwZTp6b29tPSIwLjU2NjEwNjA5IgogICAgIGlua3NjYXBlOmN4PSIxNTEuMDMxNzYiCiAgICAgaW5rc2NhcGU6Y3k9IjMxNy4wNzgzNyIKICAgICBpbmtzY2FwZTp3aW5kb3ctd2lkdGg9IjEyOTYiCiAgICAgaW5rc2NhcGU6d2luZG93LWhlaWdodD0iOTY5IgogICAgIGlua3NjYXBlOndpbmRvdy14PSIwIgogICAgIGlua3NjYXBlOndpbmRvdy15PSIyNSIKICAgICBpbmtzY2FwZTp3aW5kb3ctbWF4aW1pemVkPSIwIgogICAgIGlua3NjYXBlOmN1cnJlbnQtbGF5ZXI9InN2Zzk1OCIgLz4KICA8cGF0aAogICAgIGQ9Ik0gNDcyLjk5MzQ1LDEyMC44NjMxMiBWIDU3LjQwODc3OSBoIDcuMzggViA4Ni4yMTA3MiBoIDAuMTggbCAzMC43ODIsLTI4LjgwMTk0MSBoIDkuNDUxIGwgLTM1LjI4MywzMi44NTIyNDEgMC4wOSwtMy45NjAyIDM3LjE3MywzNC41NjIzIGggLTkuNzIxIGwgLTMyLjQ5MiwtMjkuODgyIGggLTAuMTggdiAyOS44ODIgeiIKICAgICBmaWxsPSIjZmZmZmZmIgogICAgIGlkPSJwYXRoOTQwIiAvPgogIDxwYXRoCiAgICAgZD0ibSA0MzkuODg0NDUsMTIxLjY3MzIyIGMgLTYuMjQxLDAgLTExLjU4MSwtMS4zMjAxIC0xNi4wMjEsLTMuOTYwMyAtNC40NDEsLTIuNjQwMiAtNy44NjEsLTYuMzkwNCAtMTAuMjYxLC0xMS4yNTA4IC0yLjM0LC00Ljg2MDMgLTMuNTEsLTEwLjY1MDcgLTMuNTEsLTE3LjM3MTIgMCwtNi43MjA0IDEuMTcsLTEyLjQ4MDggMy41MSwtMTcuMjgxMSAyLjQsLTQuODYwNCA1LjgyLC04LjYxMDY0IDEwLjI2MSwtMTEuMjUwODIgNC40NCwtMi42NDAxOSA5Ljc4LC0zLjk2MDI4IDE2LjAyMSwtMy45NjAyOCA0LjM4LDAgOC40MywwLjY5MDA0OCAxMi4xNTEsMi4wNzAxNCAzLjcyLDEuMzgwMSA2Ljg0LDMuMzkwMjQgOS4zNiw2LjAzMDQyIGwgLTIuODgsNi4wMzA0NCBjIC0yLjc2LC0yLjU4MDIgLTUuNjQsLTQuNDQwMzMgLTguNjQxLC01LjU4MDQxIC0yLjk0LC0xLjIwMDA4IC02LjIxLC0xLjgwMDEyIC05LjgxLC0xLjgwMDEyIC03LjE0MSwwIC0xMi42MDEsMi4yNTAxNSAtMTYuMzgxLDYuNzUwNDMgLTMuNzgxLDQuNTAwMyAtNS42NzEsMTAuODMwOCAtNS42NzEsMTguOTkxMyAwLDguMTYwNiAxLjg5LDE0LjUyMSA1LjY3MSwxOS4wODE0IDMuNzgsNC41MDAzIDkuMjQsNi43NTA0IDE2LjM4MSw2Ljc1MDQgMy42LDAgNi44NywtMC41NyA5LjgxLC0xLjcxMDEgMy4wMDEsLTEuMjAwMSA1Ljg4MSwtMy4wOTAyIDguNjQxLC01LjY3MDQgbCAyLjg4LDYuMDMwNCBjIC0yLjUyLDIuNTgwMiAtNS42NCw0LjU5MDQgLTkuMzYsNi4wMzA1IC0zLjcyMSwxLjM4IC03Ljc3MSwyLjA3MDEgLTEyLjE1MSwyLjA3MDEgeiIKICAgICBmaWxsPSIjZmZmZmZmIgogICAgIGlkPSJwYXRoOTQyIiAvPgogIDxwYXRoCiAgICAgZD0ibSAzNDEuNzk1NDUsMTIwLjg2MzEyIDI3Ljk5MiwtNjMuNDU0MzQxIGggNi4zIGwgMjcuOTkyLDYzLjQ1NDM0MSBoIC03LjY1IGwgLTcuODMxLC0xOC4wOTEyIDMuNiwxLjg5MDEgaCAtMzguNzAyIGwgMy42OSwtMS44OTAxIC03Ljc0MSwxOC4wOTEyIHogbSAzMS4wNTIsLTU0LjgxMzc0IC0xNC40OTEsMzQuMTEyMzQgLTIuMTYsLTEuNzEwMSBoIDMzLjMwMiBsIC0xLjk4LDEuNzEwMSAtMTQuNDkxLC0zNC4xMTIzNCB6IgogICAgIGZpbGw9IiNmZmZmZmYiCiAgICAgaWQ9InBhdGg5NDQiIC8+CiAgPHBhdGgKICAgICBkPSJtIDMxNi4xOTk0NSwxMjAuODYzMTIgdiAtNTcuMDYzOSBoIC0yMy4wNDIgdiAtNi4zOTA0NDEgaCA1My41NTQgdiA2LjM5MDQ0MSBoIC0yMy4wNDIgdiA1Ny4wNjM5IHoiCiAgICAgZmlsbD0iI2ZmZmZmZiIKICAgICBpZD0icGF0aDk0NiIgLz4KICA8cGF0aAogICAgIGZpbGwtcnVsZT0iZXZlbm9kZCIKICAgICBjbGlwLXJ1bGU9ImV2ZW5vZGQiCiAgICAgZD0ibSAyMzYuNjQ1NDUsNTcuMzI5MzExIGggNTAuODI3IHYgNi4zNTMzNzkgaCAtNTAuODI3IHogbSAwLDU3LjE4MDQwOSBoIDUwLjgyNyB2IDYuMzUzNCBoIC01MC44MjcgeiBtIDUwLjgyNywtMjguNTkwMiBoIC01MC44MjcgdiA2LjM1MzQgaCA1MC44MjcgeiIKICAgICBmaWxsPSIjZmZmZmZmIgogICAgIGlkPSJwYXRoOTQ4IiAvPgogIDxwYXRoCiAgICAgZD0ibSAxOTkuODYzNDUsMTIwLjg2MzEyIHYgLTMyLjg1MjIgbCAxLjYyLDUuMTMwMyAtMjUuOTIyLC0zNS43MzI0NDEgaCA4LjY0MSBsIDIwLjQzMSwyOC4yNjE5NDEgaCAtMS44OSBsIDIwLjQzMiwtMjguMjYxOTQxIGggOC4zNyBsIC0yNS44MzIsMzUuNzMyNDQxIDEuNTMxLC01LjEzMDMgdiAzMi44NTIyIHoiCiAgICAgZmlsbD0iI2ZmZmZmZiIKICAgICBpZD0icGF0aDk1MCIgLz4KICA8cGF0aAogICAgIGQ9Im0gMTI2Ljk5MTQ1LDEyMC44NjMxMiB2IC01LjQ5MDMgbCAzOC41MjMsLTU0LjQ1MzggdiAyLjg4MDIgaCAtMzguNTIzIHYgLTYuMzkwNDQxIGggNDUuNDUzIHYgNS40OTAzODEgbCAtMzguNTIyLDU0LjM2Mzc2IHYgLTIuNzkwMiBoIDM5Ljc4MyB2IDYuMzkwNCB6IgogICAgIGZpbGw9IiNmZmZmZmYiCiAgICAgaWQ9InBhdGg5NTIiIC8+CiAgPHBhdGgKICAgICBkPSJtIDg5LjQ2MzU1MywxMjEuNjczMjIgYyAtNC4zODAzLDAgLTguMzcwNiwtMC43NTAxIC0xMS45NzA4LC0yLjI1MDIgLTMuNjAwMywtMS41NjAxIC02LjY2MDUsLTMuNzUwMiAtOS4xODA3LC02LjU3MDQgLTIuNTIwMSwtMi44MjAyIC00LjQ3MDMsLTYuMjQwNCAtNS44NTA0LC0xMC4yNjA3IC0xLjMyMDEsLTQuMDIwMyAtMS45ODAxLC04LjUyMDYgLTEuOTgwMSwtMTMuNTAxIDAsLTUuMDQwMyAwLjY2LC05LjU0MDYgMS45ODAxLC0xMy41MDA5IDEuMzgwMSwtNC4wMjAzIDMuMzMwMywtNy40MTA1IDUuODUwNCwtMTAuMTcwNjkgMi41MjAyLC0yLjgyMDIgNS41NTA0LC00Ljk4MDM0IDkuMDkwNywtNi40ODA0NSAzLjYwMDIsLTEuNTYwMTA0IDcuNjIwNSwtMi4zNDAxNiAxMi4wNjA4LC0yLjM0MDE2IDQuNTAwMywwIDguNTIwNiwwLjc1MDA1NCAxMi4wNjA3OTcsMi4yNTAxNiAzLjYwMDEsMS41MDAxIDYuNjYwMSwzLjY2MDI1IDkuMTgxMSw2LjQ4MDQ0IDIuNTgsMi44MjAyIDQuNTMsNi4yNDA0IDUuODUsMTAuMjYwNyAxLjM4LDMuOTYwMyAyLjA3LDguNDMwNiAyLjA3LDEzLjQxMDkgMCw1LjA0MDQgLTAuNjksOS41NzA3IC0yLjA3LDEzLjU5MSAtMS4zOCw0LjAyMDMgLTMuMzMsNy40NDA1IC01Ljg1LDEwLjI2MDcgLTIuNTIxLDIuODIwMiAtNS41ODEsNS4wMTAzIC05LjE4MTEsNi41NzA0IC0zLjU0MDE5NywxLjUwMDEgLTcuNTYwNDk3LDIuMjUwMiAtMTIuMDYwNzk3LDIuMjUwMiB6IG0gMCwtNi41NzA1IGMgNC41NjAzLDAgOC40MDA2LC0xLjAyIDExLjUyMDc5NywtMy4wNjAyIDMuMTgwMSwtMi4wNDAxIDUuNjEwMSwtNS4wMTAzIDcuMjkwMSwtOC45MTA2IDEuNjgxLC0zLjkwMDMgMi41MjEsLTguNTgwNiAyLjUyMSwtMTQuMDQxIDAsLTUuNTIwMyAtMC44NCwtMTAuMjAwNyAtMi41MjEsLTE0LjA0MDkgLTEuNjgsLTMuODQwMyAtNC4xMSwtNi43ODA1IC03LjI5MDEsLTguODIwNjQgLTMuMTIwMTk3LC0yLjA0MDE0IC02Ljk2MDQ5NywtMy4wNjAyIC0xMS41MjA3OTcsLTMuMDYwMiAtNC40NDAzLDAgLTguMjUwNiwxLjAyMDA2IC0xMS40MzA4LDMuMDYwMiAtMy4xMjAyLDIuMDQwMTQgLTUuNTIwNCw1LjAxMDM0IC03LjIwMDUsOC45MTA2NCAtMS42ODAxLDMuODQwMiAtMi41MjAyLDguNDkwNiAtMi41MjAyLDEzLjk1MDkgMCw1LjQ2MDQgMC44NDAxLDEwLjE0MDcgMi41MjAyLDE0LjA0MSAxLjY4MDEsMy44NDAzIDQuMDgwMyw2LjgxMDUgNy4yMDA1LDguOTEwNiAzLjE4MDIsMi4wNDAyIDYuOTkwNSwzLjA2MDIgMTEuNDMwOCwzLjA2MDIgeiIKICAgICBmaWxsPSIjZmZmZmZmIgogICAgIGlkPSJwYXRoOTU0IiAvPgogIDxwYXRoCiAgICAgZD0ibSAzMS41NTg1NTMsMTIxLjY3MzIyIGMgLTYuMjQwNSwwIC0xMS41ODA4LC0xLjMyMDEgLTE2LjAyMTIsLTMuOTYwMyAtNC40NDAyNiwtMi42NDAyIC03Ljg2MDQ4OTcsLTYuMzkwNCAtMTAuMjYwNjU5NywtMTEuMjUwOCAtMi4zNDAxNiwtNC44NjAzIC0zLjUxMDI0LC0xMC42NTA3IC0zLjUxMDI0LC0xNy4zNzEyIDAsLTYuNzIwNCAxLjE3MDA4LC0xMi40ODA4IDMuNTEwMjQsLTE3LjI4MTEgMi40MDAxNywtNC44NjA0IDUuODIwMzk5NywtOC42MTA2NCAxMC4yNjA2NTk3LC0xMS4yNTA4MiA0LjQ0MDQsLTIuNjQwMTkgOS43ODA3LC0zLjk2MDI4IDE2LjAyMTIsLTMuOTYwMjggNC4zODAzLDAgOC40MzA1LDAuNjkwMDQ4IDEyLjE1MDgsMi4wNzAxNCAzLjcyMDMsMS4zODAxIDYuODQwNSwzLjM5MDI0IDkuMzYwNiw2LjAzMDQyIGwgLTIuODgwMiw2LjAzMDQ0IGMgLTIuNzYwMSwtMi41ODAyIC01LjY0MDMsLTQuNDQwMzMgLTguNjQwNiwtNS41ODA0MSAtMi45NDAyLC0xLjIwMDA4IC02LjIxMDQsLTEuODAwMTIgLTkuODEwNiwtMS44MDAxMiAtNy4xNDA1LDAgLTEyLjYwMDksMi4yNTAxNSAtMTYuMzgxMiw2Ljc1MDQzIC0zLjc4MDIyLDQuNTAwMyAtNS42NzAzNDk3LDEwLjgzMDggLTUuNjcwMzQ5NywxOC45OTEzIDAsOC4xNjA2IDEuODkwMTI5NywxNC41MjEgNS42NzAzNDk3LDE5LjA4MTQgMy43ODAzLDQuNTAwMyA5LjI0MDcsNi43NTA0IDE2LjM4MTIsNi43NTA0IDMuNjAwMiwwIDYuODcwNCwtMC41NyA5LjgxMDYsLTEuNzEwMSAzLjAwMDMsLTEuMjAwMSA1Ljg4MDUsLTMuMDkwMiA4LjY0MDYsLTUuNjcwNCBsIDIuODgwMiw2LjAzMDQgYyAtMi41MjAxLDIuNTgwMiAtNS42NDAzLDQuNTkwNCAtOS4zNjA2LDYuMDMwNSAtMy43MjAzLDEuMzggLTcuNzcwNSwyLjA3MDEgLTEyLjE1MDgsMi4wNzAxIHoiCiAgICAgZmlsbD0iI2ZmZmZmZiIKICAgICBpZD0icGF0aDk1NiIgLz4KPC9zdmc+Cg==|' src/components/Header/Header.scss
-RUN sed -i '/background-image/a \  background-size: 85%;' src/components/Header/Header.scss
+ARG COMMIT_REF=215c323b0754c8f7328819df9a253e0e507eccb4
+RUN wget -O- https://github.com/aenix-io/kubeapps/archive/${COMMIT_REF}.tar.gz | tar xzf - --strip-components=2 kubeapps-${COMMIT_REF}/dashboard
 
 RUN yarn install --frozen-lockfile
 
diff --git a/packages/system/dashboard/images/dashboard/apple-touch-icon.png b/packages/system/dashboard/images/dashboard/apple-touch-icon.png
deleted file mode 100644
index 39c1ff6e..00000000
Binary files a/packages/system/dashboard/images/dashboard/apple-touch-icon.png and /dev/null differ
diff --git a/packages/system/dashboard/images/dashboard/documentation.diff b/packages/system/dashboard/images/dashboard/documentation.diff
deleted file mode 100644
index 2ad63726..00000000
--- a/packages/system/dashboard/images/dashboard/documentation.diff
+++ /dev/null
@@ -1,13 +0,0 @@
-diff --git a/dashboard/src/components/AppList/AppListGrid.tsx b/dashboard/src/components/AppList/AppListGrid.tsx
-index d3261e459..dee6a50c1 100644
---- a/dashboard/src/components/AppList/AppListGrid.tsx
-+++ b/dashboard/src/components/AppList/AppListGrid.tsx
-@@ -42,7 +42,7 @@ function AppListGrid(props: IAppListProps) {
-           Start browsing your <Link to={url.app.catalog(cluster, namespace)}>favourite apps</Link>{" "}
-           or check the{" "}
-           <a
--            href={`https://github.com/vmware-tanzu/kubeapps/blob/${appVersion}/site/content/docs/latest/tutorials/getting-started.md`}
-+            href={"https://cozystack.io/docs/"}
-             target="_blank"
-             rel="noopener noreferrer"
-           >
diff --git a/packages/system/dashboard/images/dashboard/favicon-16x16.png b/packages/system/dashboard/images/dashboard/favicon-16x16.png
deleted file mode 100644
index a3d1fb22..00000000
Binary files a/packages/system/dashboard/images/dashboard/favicon-16x16.png and /dev/null differ
diff --git a/packages/system/dashboard/images/dashboard/favicon-32x32.png b/packages/system/dashboard/images/dashboard/favicon-32x32.png
deleted file mode 100644
index b149bda6..00000000
Binary files a/packages/system/dashboard/images/dashboard/favicon-32x32.png and /dev/null differ
diff --git a/packages/system/dashboard/images/dashboard/favicon.ico b/packages/system/dashboard/images/dashboard/favicon.ico
deleted file mode 100644
index fc042661..00000000
Binary files a/packages/system/dashboard/images/dashboard/favicon.ico and /dev/null differ
diff --git a/packages/system/dashboard/images/dashboard/mstile-144x144.png b/packages/system/dashboard/images/dashboard/mstile-144x144.png
deleted file mode 100644
index 622ab2b0..00000000
Binary files a/packages/system/dashboard/images/dashboard/mstile-144x144.png and /dev/null differ
diff --git a/packages/system/dashboard/images/dashboard/mstile-150x150.png b/packages/system/dashboard/images/dashboard/mstile-150x150.png
deleted file mode 100644
index 70d40bcd..00000000
Binary files a/packages/system/dashboard/images/dashboard/mstile-150x150.png and /dev/null differ
diff --git a/packages/system/dashboard/images/dashboard/mstile-310x150.png b/packages/system/dashboard/images/dashboard/mstile-310x150.png
deleted file mode 100644
index 03f655f0..00000000
Binary files a/packages/system/dashboard/images/dashboard/mstile-310x150.png and /dev/null differ
diff --git a/packages/system/dashboard/images/dashboard/mstile-310x310.png b/packages/system/dashboard/images/dashboard/mstile-310x310.png
deleted file mode 100644
index 0faae8a9..00000000
Binary files a/packages/system/dashboard/images/dashboard/mstile-310x310.png and /dev/null differ
diff --git a/packages/system/dashboard/images/dashboard/mstile-70x70.png b/packages/system/dashboard/images/dashboard/mstile-70x70.png
deleted file mode 100644
index f59b5aaf..00000000
Binary files a/packages/system/dashboard/images/dashboard/mstile-70x70.png and /dev/null differ
diff --git a/packages/system/dashboard/images/dashboard/release-url.diff b/packages/system/dashboard/images/dashboard/release-url.diff
deleted file mode 100644
index 97c125f6..00000000
--- a/packages/system/dashboard/images/dashboard/release-url.diff
+++ /dev/null
@@ -1,34 +0,0 @@
-diff --git a/dashboard/src/shared/url.ts b/dashboard/src/shared/url.ts
-index 7918652b0..64c3435af 100644
---- a/dashboard/src/shared/url.ts
-+++ b/dashboard/src/shared/url.ts
-@@ -36,7 +36,7 @@ export const app = {
-       return `${app.apps.list(
-         pkgCluster,
-         pkgNamespace,
--      )}/${pkgPluginName}/${pkgPluginVersion}/${pkgId}`;
-+      )}/${pkgPluginName}/${pkgPluginVersion}/${encodeURIComponent(pkgId)}`;
-     },
-     upgrade: (ref: InstalledPackageReference) => `${app.apps.get(ref)}/upgrade`,
-     upgradeTo: (ref: InstalledPackageReference, version?: string) =>
-diff --git a/dashboard/src/components/DeploymentForm/DeploymentForm.tsx b/dashboard/src/components/DeploymentForm/DeploymentForm.tsx
-index 7ccb77b5d..589f72b65 100644
---- a/dashboard/src/components/DeploymentForm/DeploymentForm.tsx
-+++ b/dashboard/src/components/DeploymentForm/DeploymentForm.tsx
-@@ -144,13 +144,15 @@ export default function DeploymentForm() {
-       );
-       setDeploying(false);
-       if (deployed) {
-+        const chartParts = packageId?.split("/") || [];
-+        const kind = chartParts[chartParts.length - 1];
-         push(
-           // Redirect to the installed package, note that the cluster/ns are the ones passed
-           // in the URL, not the ones from the package.
-           url.app.apps.get({
-             context: { cluster: targetCluster, namespace: targetNamespace },
-             plugin: pluginObj,
--            identifier: releaseName,
-+            identifier: `${kind}%2F${releaseName}`,
-           } as AvailablePackageReference),
-         );
-       }
diff --git a/packages/system/dashboard/images/dashboard/remove-manage-repositories.diff b/packages/system/dashboard/images/dashboard/remove-manage-repositories.diff
deleted file mode 100644
index a2212160..00000000
--- a/packages/system/dashboard/images/dashboard/remove-manage-repositories.diff
+++ /dev/null
@@ -1,66 +0,0 @@
-diff --git a/dashboard/src/components/Catalog/Catalog.tsx b/dashboard/src/components/Catalog/Catalog.tsx
-index 5f2d2a1c5..093cb598d 100644
---- a/dashboard/src/components/Catalog/Catalog.tsx
-+++ b/dashboard/src/components/Catalog/Catalog.tsx
-@@ -15,7 +15,6 @@ import qs from "qs";
- import React, { useEffect } from "react";
- import { useDispatch, useSelector } from "react-redux";
- import * as ReactRouter from "react-router-dom";
--import { Link } from "react-router-dom";
- import { IClusterServiceVersion, IStoreState } from "shared/types";
- import { app } from "shared/url";
- import { escapeRegExp, getPluginPackageName } from "shared/utils";
-@@ -85,7 +84,6 @@ export default function Catalog() {
-     operators,
-     repos: { reposSummaries: repos },
-     config: {
--      appVersion,
-       kubeappsCluster,
-       helmGlobalNamespace,
-       carvelGlobalNamespace,
-@@ -420,24 +418,6 @@ export default function Catalog() {
-         <div className="empty-catalog">
-           <CdsIcon shape="bundle" />
-           <p>The current catalog is empty.</p>
--          <p>
--            Manage your Package Repositories in Kubeapps by visiting the Package repositories
--            configuration page.
--          </p>
--          <Link to={app.config.pkgrepositories(cluster || "", namespace || "")}>
--            <CdsButton>Manage Package Repositories</CdsButton>
--          </Link>
--          <p>
--            For help managing other packaging formats, such as Flux or Carvel, please refer to the{" "}
--            <a
--              target="_blank"
--              rel="noopener noreferrer"
--              href={`https://github.com/vmware-tanzu/kubeapps/tree/${appVersion}/site/content/docs/latest`}
--            >
--              Kubeapps documentation
--            </a>
--            .
--          </p>
-         </div>
-       ) : (
-         <Row>
-diff --git a/dashboard/src/components/Header/Menu.tsx b/dashboard/src/components/Header/Menu.tsx
-index c8ec1da8c..e59f90190 100644
---- a/dashboard/src/components/Header/Menu.tsx
-+++ b/dashboard/src/components/Header/Menu.tsx
-@@ -78,16 +78,6 @@ function Menu({ clusters, appVersion, logout }: IContextSelectorProps) {
-           <div className="dropdown-menu dropdown-configuration-menu" role="menu" hidden={!open}>
-             <div>
-               <label className="dropdown-menu-padding dropdown-menu-label">Administration</label>
--              <Link
--                to={app.config.pkgrepositories(clusters.currentCluster, namespaceSelected)}
--                className="dropdown-menu-link"
--                onClick={toggleOpen}
--              >
--                <div className="dropdown-menu-item" role="menuitem">
--                  <CdsIcon solid={true} size="md" shape="library" />{" "}
--                  <span>Package Repositories</span>
--                </div>
--              </Link>
-               <div className="dropdown-divider" role="separator" />
-               {featureFlags?.operators && (
-                 <Link
diff --git a/packages/system/dashboard/images/dashboard/safari-pinned-tab.svg b/packages/system/dashboard/images/dashboard/safari-pinned-tab.svg
deleted file mode 100644
index d72f1f44..00000000
--- a/packages/system/dashboard/images/dashboard/safari-pinned-tab.svg
+++ /dev/null
@@ -1,49 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<svg
-   width="933.33301"
-   height="933.33301"
-   viewBox="0 0 700 700"
-   preserveAspectRatio="xMidYMid meet"
-   version="1.1"
-   id="svg826"
-   sodipodi:docname="safari-pinned-tab.svg"
-   inkscape:version="1.1.1 (c3084ef, 2021-09-22)"
-   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
-   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
-   xmlns="http://www.w3.org/2000/svg"
-   xmlns:svg="http://www.w3.org/2000/svg">
-  <defs
-     id="defs830" />
-  <sodipodi:namedview
-     id="namedview828"
-     pagecolor="#ffffff"
-     bordercolor="#666666"
-     borderopacity="1.0"
-     inkscape:pageshadow="2"
-     inkscape:pageopacity="0.0"
-     inkscape:pagecheckerboard="0"
-     showgrid="false"
-     inkscape:zoom="0.77035741"
-     inkscape:cx="436.81023"
-     inkscape:cy="541.95623"
-     inkscape:window-width="1720"
-     inkscape:window-height="1387"
-     inkscape:window-x="1720"
-     inkscape:window-y="25"
-     inkscape:window-maximized="0"
-     inkscape:current-layer="svg826" />
-  <rect
-     style="fill:#000000;stroke-width:6.33749"
-     id="rect938"
-     width="700"
-     height="700"
-     x="0"
-     y="-1.7763568e-15" />
-  <path
-     fill-rule="evenodd"
-     clip-rule="evenodd"
-     d="m 170.27273,142.84005 c 0,-13.12938 10.41078,-23.77292 23.25562,-23.77292 h 325.57872 c 12.8449,0 23.25558,10.64354 23.25558,23.77292 v 0 c 0,13.12957 -10.41068,23.77302 -23.25558,23.77302 H 193.52835 c -12.84484,0 -23.25562,-10.64345 -23.25562,-23.77302 z m 0,427.91341 c 0,-13.12903 10.41078,-23.77297 23.25562,-23.77297 h 325.57872 c 12.8449,0 23.25558,10.64394 23.25558,23.77297 v 0 c 0,13.12982 -10.41068,23.77298 -23.25558,23.77298 H 193.52835 c -12.84484,0 -23.25562,-10.64316 -23.25562,-23.77298 z M 542.36265,356.79661 c 0,-13.12902 -10.41068,-23.77296 -23.25558,-23.77296 H 193.52835 c -12.84484,0 -23.25562,10.64394 -23.25562,23.77296 v 0 c 0,13.12982 10.41078,23.77298 23.25562,23.77298 h 325.57872 c 12.8449,0 23.25558,-10.64316 23.25558,-23.77298 z"
-     fill="#ffffff"
-     id="path840"
-     style="stroke-width:7.83755" />
-</svg>
diff --git a/packages/system/dashboard/images/kubeapps-apis/Dockerfile b/packages/system/dashboard/images/kubeapps-apis/Dockerfile
index a8e57512..54bc172f 100644
--- a/packages/system/dashboard/images/kubeapps-apis/Dockerfile
+++ b/packages/system/dashboard/images/kubeapps-apis/Dockerfile
@@ -4,7 +4,7 @@
 # syntax = docker/dockerfile:1
 
 FROM alpine as source
-ARG COMMIT_REF=e146cf8660c58a4f585611ab3cbce62ebfa4c5a3
+ARG COMMIT_REF=215c323b0754c8f7328819df9a253e0e507eccb4
 RUN apk add --no-cache patch
 WORKDIR /source
 RUN wget -O- https://github.com/aenix-io/kubeapps/archive/${COMMIT_REF}.tar.gz | tar xzf - --strip-components=1
diff --git a/packages/system/dashboard/values.yaml b/packages/system/dashboard/values.yaml
index 89f2d4db..1a9d1c57 100644
--- a/packages/system/dashboard/values.yaml
+++ b/packages/system/dashboard/values.yaml
@@ -40,14 +40,14 @@ kubeapps:
     image:
       registry: ghcr.io/aenix-io/cozystack
       repository: dashboard
-      tag: v0.21.1
-      digest: "sha256:fa9b6238da1dfaa15ec1c20c041103d6e07b5194cc54cf3cf4872f758ceaa085"
+      tag: latest
+      digest: "sha256:cdf9d93a9733ce6f59d467a03a34bb66177eb4b42715fcf81f84705b150d9dad"
   kubeappsapis:
     image:
       registry: ghcr.io/aenix-io/cozystack
       repository: kubeapps-apis
-      tag: v0.21.1
-      digest: "sha256:3ad47a120ae2bd83e1242430e17616a6500d627a7a6cef94095b9e6c1a9e85a8"
+      tag: latest
+      digest: "sha256:4842d2e0c4067b73414f815419ae8c83ddb56c287cc45e1648cb700da3842770"
     pluginConfig:
       flux:
         packages:
diff --git a/packages/system/fluxcd-operator/charts/flux-operator/Chart.yaml b/packages/system/fluxcd-operator/charts/flux-operator/Chart.yaml
index c0093bd1..f0db204a 100644
--- a/packages/system/fluxcd-operator/charts/flux-operator/Chart.yaml
+++ b/packages/system/fluxcd-operator/charts/flux-operator/Chart.yaml
@@ -8,7 +8,7 @@ annotations:
     - name: Upstream Project
       url: https://github.com/controlplaneio-fluxcd/flux-operator
 apiVersion: v2
-appVersion: v0.10.0
+appVersion: v0.12.0
 description: 'A Helm chart for deploying the Flux Operator. '
 home: https://github.com/controlplaneio-fluxcd
 icon: https://raw.githubusercontent.com/cncf/artwork/main/projects/flux/icon/color/flux-icon-color.png
@@ -25,4 +25,4 @@ sources:
 - https://github.com/controlplaneio-fluxcd/flux-operator
 - https://github.com/controlplaneio-fluxcd/charts
 type: application
-version: 0.10.0
+version: 0.12.0
diff --git a/packages/system/fluxcd-operator/charts/flux-operator/README.md b/packages/system/fluxcd-operator/charts/flux-operator/README.md
index 52f04f8b..18f209d5 100644
--- a/packages/system/fluxcd-operator/charts/flux-operator/README.md
+++ b/packages/system/fluxcd-operator/charts/flux-operator/README.md
@@ -1,6 +1,6 @@
 # flux-operator
 
-![Version: 0.10.0](https://img.shields.io/badge/Version-0.10.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: v0.10.0](https://img.shields.io/badge/AppVersion-v0.10.0-informational?style=flat-square)
+![Version: 0.12.0](https://img.shields.io/badge/Version-0.12.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: v0.12.0](https://img.shields.io/badge/AppVersion-v0.12.0-informational?style=flat-square)
 
 The [Flux Operator](https://github.com/controlplaneio-fluxcd/flux-operator) provides a
 declarative API for the installation and upgrade of CNCF [Flux](https://fluxcd.io) and the
diff --git a/packages/system/fluxcd-operator/charts/flux-operator/templates/crds.yaml b/packages/system/fluxcd-operator/charts/flux-operator/templates/crds.yaml
index 17b74747..804c421b 100644
--- a/packages/system/fluxcd-operator/charts/flux-operator/templates/crds.yaml
+++ b/packages/system/fluxcd-operator/charts/flux-operator/templates/crds.yaml
@@ -99,6 +99,23 @@ spec:
                 - domain
                 - networkPolicy
                 type: object
+              commonMetadata:
+                description: |-
+                  CommonMetadata specifies the common labels and annotations that are
+                  applied to all resources. Any existing label or annotation will be
+                  overridden if its key matches a common one.
+                properties:
+                  annotations:
+                    additionalProperties:
+                      type: string
+                    description: Annotations to be added to the object's metadata.
+                    type: object
+                  labels:
+                    additionalProperties:
+                      type: string
+                    description: Labels to be added to the object's metadata.
+                    type: object
+                type: object
               components:
                 description: |-
                   Components is the list of controllers to install.
@@ -265,6 +282,15 @@ spec:
                     - GitRepository
                     - Bucket
                     type: string
+                  name:
+                    description: |-
+                      Name is the name of the Flux source and kustomization resources.
+                      When not specified, the name is set to the namespace name of the FluxInstance.
+                    maxLength: 63
+                    type: string
+                    x-kubernetes-validations:
+                    - message: Sync name is immutable
+                      rule: self == oldSelf
                   path:
                     description: |-
                       Path is the path to the source directory containing
@@ -422,6 +448,11 @@ spec:
                   LastAppliedRevision is the version and digest of the
                   distribution config that was last reconcile.
                 type: string
+              lastArtifactRevision:
+                description: |-
+                  LastArtifactRevision is the digest of the last pulled
+                  distribution artifact.
+                type: string
               lastAttemptedRevision:
                 description: |-
                   LastAttemptedRevision is the version and digest of the
diff --git a/packages/system/fluxcd/charts/flux-instance/Chart.yaml b/packages/system/fluxcd/charts/flux-instance/Chart.yaml
index 8994b5cd..c7b895fa 100644
--- a/packages/system/fluxcd/charts/flux-instance/Chart.yaml
+++ b/packages/system/fluxcd/charts/flux-instance/Chart.yaml
@@ -8,7 +8,7 @@ annotations:
     - name: Upstream Project
       url: https://github.com/controlplaneio-fluxcd/flux-operator
 apiVersion: v2
-appVersion: v0.9.0
+appVersion: v0.12.0
 description: 'A Helm chart for deploying a Flux instance managed by Flux Operator. '
 home: https://github.com/controlplaneio-fluxcd
 icon: https://raw.githubusercontent.com/cncf/artwork/main/projects/flux/icon/color/flux-icon-color.png
@@ -25,4 +25,4 @@ sources:
 - https://github.com/controlplaneio-fluxcd/flux-operator
 - https://github.com/controlplaneio-fluxcd/charts
 type: application
-version: 0.9.0
+version: 0.12.0
diff --git a/packages/system/fluxcd/charts/flux-instance/README.md b/packages/system/fluxcd/charts/flux-instance/README.md
index 68dbba33..fa5896e0 100644
--- a/packages/system/fluxcd/charts/flux-instance/README.md
+++ b/packages/system/fluxcd/charts/flux-instance/README.md
@@ -1,6 +1,6 @@
 # flux-instance
 
-![Version: 0.9.0](https://img.shields.io/badge/Version-0.9.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: v0.9.0](https://img.shields.io/badge/AppVersion-v0.9.0-informational?style=flat-square)
+![Version: 0.12.0](https://img.shields.io/badge/Version-0.12.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: v0.12.0](https://img.shields.io/badge/AppVersion-v0.12.0-informational?style=flat-square)
 
 This chart is a thin wrapper around the `FluxInstance` custom resource, which is
 used by the [Flux Operator](https://github.com/controlplaneio-fluxcd/flux-operator)
diff --git a/packages/system/postgres-operator/templates/prometheusrule.yaml b/packages/system/postgres-operator/templates/prometheusrule.yaml
new file mode 100644
index 00000000..6bc7cdb2
--- /dev/null
+++ b/packages/system/postgres-operator/templates/prometheusrule.yaml
@@ -0,0 +1,101 @@
+apiVersion: monitoring.coreos.com/v1
+kind: PrometheusRule
+metadata:
+  name: cnpg-default-alerts
+spec:
+  groups:
+  - name: cnp-default.rules
+    rules:
+    - alert: LongRunningTransaction
+      annotations:
+        description: Namespace {{ $labels.namespace }} Job {{ $labels.job }} Pod {{ $labels.pod }} is taking more than 5 minutes (300 seconds) for a query.
+        summary: A query is taking longer than 5 minutes.
+      expr: |-
+        cnpg_backends_max_tx_duration_seconds > 300
+      for: 1m
+      labels:
+        severity: warning
+        namespace: {{ $labels.namespace }}
+        job: {{ $labels.job }}
+    - alert: BackendsWaiting
+      annotations:
+        description: Namespace {{ $labels.namespace }} Job {{ $labels.job }} Pod {{ $labels.pod }} has been waiting for longer than 5 minutes
+        summary: If a backend is waiting for longer than 5 minutes
+      expr: |-
+        cnpg_backends_waiting_total > 300
+      for: 1m
+      labels:
+        severity: warning
+        namespace: {{ $labels.namespace }}
+        job: {{ $labels.job }}
+    - alert: PGDatabaseXidAge
+      annotations:
+        description: Over 300,000,000 transactions from frozen xid on Namespace {{ $labels.namespace }} Job {{ $labels.job }} Pod {{ $labels.pod }}
+        summary: Number of transactions from the frozen XID to the current one
+      expr: |-
+        cnpg_pg_database_xid_age > 300000000
+      for: 1m
+      labels:
+        severity: warning
+        namespace: {{ $labels.namespace }}
+        job: {{ $labels.job }}
+    - alert: PGReplication
+      annotations:
+        description: Namespace {{ $labels.namespace }} Job {{ $labels.job }} Pod {{ $labels.pod }} Standby is lagging behind by over 300 seconds (5 minutes)
+        summary: The standby is lagging behind the primary
+      expr: |-
+        cnpg_pg_replication_lag > 300
+      for: 1m
+      labels:
+        severity: warning
+        namespace: {{ $labels.namespace }}
+        job: {{ $labels.job }}
+    - alert: LastFailedArchiveTime
+      annotations:
+        description: Archiving failed for Namespace {{ $labels.namespace }} Job {{ $labels.job }} Pod {{ $labels.pod }}
+        summary: Checks the last time archiving failed. Will be < 0 when it has not failed.
+      expr: |-
+        sum by (namespace, job) (cnpg_pg_stat_archiver_last_failed_time) - sum by (namespace, job) (cnpg_pg_stat_archiver_last_archived_time) > 1
+      for: 1m
+      labels:
+        severity: warning
+        namespace: {{ $labels.namespace }}
+        job: {{ $labels.job }}
+    - alert: DatabaseDeadlockConflicts
+      annotations:
+        description: There are over 10 deadlock conflicts in Namespace {{ $labels.namespace }} Job {{ $labels.job }} Pod {{ $labels.pod }}
+        summary: Checks the number of database conflicts
+      expr: |-
+        cnpg_pg_stat_database_deadlocks > 10
+      for: 1m
+      labels:
+        severity: warning
+        namespace: {{ $labels.namespace }}
+        job: {{ $labels.job }}
+    - alert: ReplicaFailingReplication
+      annotations:
+        description: Namespace {{ $labels.namespace }} Job {{ $labels.job }} Pod {{ $labels.pod }} Replica is failing to replicate
+        summary: Checks if the replica is failing to replicate
+      expr: |-
+        sum by (namespace, job) (cnpg_pg_replication_in_recovery) - sum by (namespace, job) (cnpg_pg_replication_is_wal_receiver_up) > 0
+      for: 1m
+      labels:
+        severity: warning
+        namespace: {{ $labels.namespace }}
+        job: {{ $labels.job }}
+    - alert: CNPGClusterOffline
+      annotations:
+        summary: CNPG Cluster has no running instances!
+        description: |-
+          CloudNativePG Cluster Namespace: {{ $labels.namespace }} Job: {{ $labels.job }} Pod: {{ $labels.pod }} has no ready instances.
+          Having an offline cluster means your applications will not be able to access the database, leading to
+          potential service disruption and/or data loss.
+        runbook_url: https://github.com/cloudnative-pg/charts/blob/main/charts/cluster/docs/runbooks/CNPGClusterOffline.md
+      expr: |
+        sum by (namespace, pod) (cnpg_collector_up)) OR on() vector(0) == 0
+      for: 5m
+      labels:
+        severity: critical
+        namespace: {{ $labels.namespace }}
+        job: {{ $labels.job }}
+        pod: {{ $labels.pod }}
diff --git a/pkg/apis/apps/fuzzer/fuzzer.go b/pkg/apis/apps/fuzzer/fuzzer.go
index be35c0e1..428e5244 100644
--- a/pkg/apis/apps/fuzzer/fuzzer.go
+++ b/pkg/apis/apps/fuzzer/fuzzer.go
@@ -17,7 +17,7 @@ limitations under the License.
 package fuzzer
 
 import (
-	"github.com/aenix.io/cozystack/pkg/apis/apps"
+	"github.com/aenix-io/cozystack/pkg/apis/apps"
 	fuzz "github.com/google/gofuzz"
 
 	runtimeserializer "k8s.io/apimachinery/pkg/runtime/serializer"
diff --git a/pkg/apis/apps/install/install.go b/pkg/apis/apps/install/install.go
index 2b6ae886..f51b3685 100644
--- a/pkg/apis/apps/install/install.go
+++ b/pkg/apis/apps/install/install.go
@@ -17,7 +17,7 @@ limitations under the License.
 package install
 
 import (
-	appsv1alpha1 "github.com/aenix.io/cozystack/pkg/apis/apps/v1alpha1"
+	appsv1alpha1 "github.com/aenix-io/cozystack/pkg/apis/apps/v1alpha1"
 	"k8s.io/apimachinery/pkg/runtime"
 	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
 )
diff --git a/pkg/apis/apps/v1alpha1/doc.go b/pkg/apis/apps/v1alpha1/doc.go
index a696dc63..b4652358 100644
--- a/pkg/apis/apps/v1alpha1/doc.go
+++ b/pkg/apis/apps/v1alpha1/doc.go
@@ -16,10 +16,10 @@ limitations under the License.
 
 // +k8s:openapi-gen=true
 // +k8s:deepcopy-gen=package
-// +k8s:conversion-gen=github.com/aenix.io/cozystack/pkg/apis/apps
+// +k8s:conversion-gen=github.com/aenix-io/cozystack/pkg/apis/apps
 // +k8s:conversion-gen=k8s.io/apiextensions-apiserver/pkg/apis/apiextensions
 // +k8s:defaulter-gen=TypeMeta
 // +groupName=apps.cozystack.io
 
 // Package v1alpha1 is the v1alpha1 version of the API.
-package v1alpha1 // import "github.com/aenix.io/cozystack/pkg/apis/apps/v1alpha1"
+package v1alpha1 // import "github.com/aenix-io/cozystack/pkg/apis/apps/v1alpha1"
diff --git a/pkg/apis/apps/v1alpha1/register.go b/pkg/apis/apps/v1alpha1/register.go
index 2849b078..362b6026 100644
--- a/pkg/apis/apps/v1alpha1/register.go
+++ b/pkg/apis/apps/v1alpha1/register.go
@@ -17,7 +17,7 @@ limitations under the License.
 package v1alpha1
 
 import (
-	"github.com/aenix.io/cozystack/pkg/config"
+	"github.com/aenix-io/cozystack/pkg/config"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/runtime/schema"
diff --git a/pkg/apis/apps/v1alpha1/zz_generated.conversion.go b/pkg/apis/apps/v1alpha1/zz_generated.conversion.go
index a379c96d..3b3b068a 100644
--- a/pkg/apis/apps/v1alpha1/zz_generated.conversion.go
+++ b/pkg/apis/apps/v1alpha1/zz_generated.conversion.go
@@ -2,7 +2,7 @@
 // +build !ignore_autogenerated
 
 /*
-Copyright 2024 The Cozystack Authors.
+Copyright 2025 The Cozystack Authors.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
diff --git a/pkg/apis/apps/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/apps/v1alpha1/zz_generated.deepcopy.go
index 1d6082ef..5ea7f311 100644
--- a/pkg/apis/apps/v1alpha1/zz_generated.deepcopy.go
+++ b/pkg/apis/apps/v1alpha1/zz_generated.deepcopy.go
@@ -2,7 +2,7 @@
 // +build !ignore_autogenerated
 
 /*
-Copyright 2024 The Cozystack Authors.
+Copyright 2025 The Cozystack Authors.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
diff --git a/pkg/apis/apps/v1alpha1/zz_generated.defaults.go b/pkg/apis/apps/v1alpha1/zz_generated.defaults.go
index dc03a5bb..4e70d879 100644
--- a/pkg/apis/apps/v1alpha1/zz_generated.defaults.go
+++ b/pkg/apis/apps/v1alpha1/zz_generated.defaults.go
@@ -2,7 +2,7 @@
 // +build !ignore_autogenerated
 
 /*
-Copyright 2024 The Cozystack Authors.
+Copyright 2025 The Cozystack Authors.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
diff --git a/pkg/apis/apps/validation/validation.go b/pkg/apis/apps/validation/validation.go
index bac817c1..01d53689 100644
--- a/pkg/apis/apps/validation/validation.go
+++ b/pkg/apis/apps/validation/validation.go
@@ -17,7 +17,7 @@ limitations under the License.
 package validation
 
 import (
-	"github.com/aenix.io/cozystack/pkg/apis/apps"
+	"github.com/aenix-io/cozystack/pkg/apis/apps"
 	"k8s.io/apimachinery/pkg/util/validation/field"
 )
 
diff --git a/pkg/apiserver/apiserver.go b/pkg/apiserver/apiserver.go
index aa8ec349..de23e4cc 100644
--- a/pkg/apiserver/apiserver.go
+++ b/pkg/apiserver/apiserver.go
@@ -29,11 +29,11 @@ import (
 	"k8s.io/client-go/dynamic"
 	restclient "k8s.io/client-go/rest"
 
-	"github.com/aenix.io/cozystack/pkg/apis/apps"
-	"github.com/aenix.io/cozystack/pkg/apis/apps/install"
-	"github.com/aenix.io/cozystack/pkg/config"
-	appsregistry "github.com/aenix.io/cozystack/pkg/registry"
-	applicationstorage "github.com/aenix.io/cozystack/pkg/registry/apps/application"
+	"github.com/aenix-io/cozystack/pkg/apis/apps"
+	"github.com/aenix-io/cozystack/pkg/apis/apps/install"
+	"github.com/aenix-io/cozystack/pkg/config"
+	appsregistry "github.com/aenix-io/cozystack/pkg/registry"
+	applicationstorage "github.com/aenix-io/cozystack/pkg/registry/apps/application"
 )
 
 var (
diff --git a/pkg/cmd/server/start.go b/pkg/cmd/server/start.go
index f8ca25c9..f36f2133 100644
--- a/pkg/cmd/server/start.go
+++ b/pkg/cmd/server/start.go
@@ -23,10 +23,10 @@ import (
 	"io"
 	"net"
 
-	"github.com/aenix.io/cozystack/pkg/apis/apps/v1alpha1"
-	"github.com/aenix.io/cozystack/pkg/apiserver"
-	"github.com/aenix.io/cozystack/pkg/config"
-	sampleopenapi "github.com/aenix.io/cozystack/pkg/generated/openapi"
+	"github.com/aenix-io/cozystack/pkg/apis/apps/v1alpha1"
+	"github.com/aenix-io/cozystack/pkg/apiserver"
+	"github.com/aenix-io/cozystack/pkg/config"
+	sampleopenapi "github.com/aenix-io/cozystack/pkg/generated/openapi"
 	"github.com/spf13/cobra"
 	utilerrors "k8s.io/apimachinery/pkg/util/errors"
 	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
@@ -202,12 +202,12 @@ func (o *AppsServerOptions) Config() (*apiserver.Config, error) {
 		defs := swagger.Definitions
 
 		// Verify the presence of the base Application/ApplicationList definitions
-		appDef, exists := defs["com.github.aenix.io.cozystack.pkg.apis.apps.v1alpha1.Application"]
+		appDef, exists := defs["com.github.aenix-io.cozystack.pkg.apis.apps.v1alpha1.Application"]
 		if !exists {
 			return swagger, fmt.Errorf("Application definition not found")
 		}
 
-		listDef, exists := defs["com.github.aenix.io.cozystack.pkg.apis.apps.v1alpha1.ApplicationList"]
+		listDef, exists := defs["com.github.aenix-io.cozystack.pkg.apis.apps.v1alpha1.ApplicationList"]
 		if !exists {
 			return swagger, fmt.Errorf("ApplicationList definition not found")
 		}
@@ -215,8 +215,8 @@ func (o *AppsServerOptions) Config() (*apiserver.Config, error) {
 		// Iterate over all registered GVKs (e.g., Bucket, Database, etc.)
 		for _, gvk := range v1alpha1.RegisteredGVKs {
 			// This will be something like:
-			// "com.github.aenix.io.cozystack.pkg.apis.apps.v1alpha1.Bucket"
-			resourceName := fmt.Sprintf("com.github.aenix.io.cozystack.pkg.apis.apps.v1alpha1.%s", gvk.Kind)
+			// "com.github.aenix-io.cozystack.pkg.apis.apps.v1alpha1.Bucket"
+			resourceName := fmt.Sprintf("com.github.aenix-io.cozystack.pkg.apis.apps.v1alpha1.%s", gvk.Kind)
 
 			// 1. Create a copy of the base Application definition for the new resource
 			newDef, err := DeepCopySchema(&appDef)
@@ -242,7 +242,7 @@ func (o *AppsServerOptions) Config() (*apiserver.Config, error) {
 
 			// 4. Now handle the corresponding List type (e.g., BucketList).
 			//    We'll start by copying the ApplicationList definition.
-			listResourceName := fmt.Sprintf("com.github.aenix.io.cozystack.pkg.apis.apps.v1alpha1.%sList", gvk.Kind)
+			listResourceName := fmt.Sprintf("com.github.aenix-io.cozystack.pkg.apis.apps.v1alpha1.%sList", gvk.Kind)
 			newListDef, err := DeepCopySchema(&listDef)
 			if err != nil {
 				return nil, fmt.Errorf("failed to deepcopy schema for %sList: %w", gvk.Kind, err)
@@ -275,8 +275,8 @@ func (o *AppsServerOptions) Config() (*apiserver.Config, error) {
 		}
 
 		// Remove the original Application/ApplicationList from the definitions
-		delete(defs, "com.github.aenix.io.cozystack.pkg.apis.apps.v1alpha1.Application")
-		delete(defs, "com.github.aenix.io.cozystack.pkg.apis.apps.v1alpha1.ApplicationList")
+		delete(defs, "com.github.aenix-io.cozystack.pkg.apis.apps.v1alpha1.Application")
+		delete(defs, "com.github.aenix-io.cozystack.pkg.apis.apps.v1alpha1.ApplicationList")
 
 		swagger.Definitions = defs
 		return swagger, nil
diff --git a/pkg/generated/applyconfiguration/apps/v1alpha1/application.go b/pkg/generated/applyconfiguration/apps/v1alpha1/application.go
index 65de594e..90eb7cd8 100644
--- a/pkg/generated/applyconfiguration/apps/v1alpha1/application.go
+++ b/pkg/generated/applyconfiguration/apps/v1alpha1/application.go
@@ -22,7 +22,7 @@ import (
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	types "k8s.io/apimachinery/pkg/types"
 	v1 "k8s.io/client-go/applyconfigurations/meta/v1"
-	appsv1alpha1 "github.com/aenix.io/cozystack/pkg/apis/apps/v1alpha1"
+	appsv1alpha1 "github.com/aenix-io/cozystack/pkg/apis/apps/v1alpha1"
 )
 
 // ApplicationApplyConfiguration represents a declarative configuration of the Application type for use
diff --git a/pkg/generated/applyconfiguration/utils.go b/pkg/generated/applyconfiguration/utils.go
index df2a5db6..356c5854 100644
--- a/pkg/generated/applyconfiguration/utils.go
+++ b/pkg/generated/applyconfiguration/utils.go
@@ -22,9 +22,9 @@ import (
 	runtime "k8s.io/apimachinery/pkg/runtime"
 	schema "k8s.io/apimachinery/pkg/runtime/schema"
 	testing "k8s.io/client-go/testing"
-	v1alpha1 "github.com/aenix.io/cozystack/pkg/apis/apps/v1alpha1"
-	internal "github.com/aenix.io/cozystack/pkg/generated/applyconfiguration/internal"
-	appsv1alpha1 "github.com/aenix.io/cozystack/pkg/generated/applyconfiguration/apps/v1alpha1"
+	v1alpha1 "github.com/aenix-io/cozystack/pkg/apis/apps/v1alpha1"
+	internal "github.com/aenix-io/cozystack/pkg/generated/applyconfiguration/internal"
+	appsv1alpha1 "github.com/aenix-io/cozystack/pkg/generated/applyconfiguration/apps/v1alpha1"
 )
 
 // ForKind returns an apply configuration type for the given GroupVersionKind, or nil if no
diff --git a/pkg/generated/listers/apps/v1alpha1/application.go b/pkg/generated/listers/apps/v1alpha1/application.go
index bf35b20b..2ab3c8e6 100644
--- a/pkg/generated/listers/apps/v1alpha1/application.go
+++ b/pkg/generated/listers/apps/v1alpha1/application.go
@@ -22,7 +22,7 @@ import (
 	labels "k8s.io/apimachinery/pkg/labels"
 	listers "k8s.io/client-go/listers"
 	cache "k8s.io/client-go/tools/cache"
-	appsv1alpha1 "github.com/aenix.io/cozystack/pkg/apis/apps/v1alpha1"
+	appsv1alpha1 "github.com/aenix-io/cozystack/pkg/apis/apps/v1alpha1"
 )
 
 // ApplicationLister helps list Applications.
diff --git a/pkg/generated/openapi/zz_generated.openapi.go b/pkg/generated/openapi/zz_generated.openapi.go
index 1b53cf7e..da8f8d15 100644
--- a/pkg/generated/openapi/zz_generated.openapi.go
+++ b/pkg/generated/openapi/zz_generated.openapi.go
@@ -2,7 +2,7 @@
 // +build !ignore_autogenerated
 
 /*
-Copyright 2024 The Cozystack Authors.
+Copyright 2025 The Cozystack Authors.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
@@ -30,9 +30,9 @@ import (
 
 func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition {
 	return map[string]common.OpenAPIDefinition{
-		"github.com/aenix.io/cozystack/pkg/apis/apps/v1alpha1.Application":                           schema_pkg_apis_apps_v1alpha1_Application(ref),
-		"github.com/aenix.io/cozystack/pkg/apis/apps/v1alpha1.ApplicationList":                       schema_pkg_apis_apps_v1alpha1_ApplicationList(ref),
-		"github.com/aenix.io/cozystack/pkg/apis/apps/v1alpha1.ApplicationStatus":                     schema_pkg_apis_apps_v1alpha1_ApplicationStatus(ref),
+		"github.com/aenix-io/cozystack/pkg/apis/apps/v1alpha1.Application":                           schema_pkg_apis_apps_v1alpha1_Application(ref),
+		"github.com/aenix-io/cozystack/pkg/apis/apps/v1alpha1.ApplicationList":                       schema_pkg_apis_apps_v1alpha1_ApplicationList(ref),
+		"github.com/aenix-io/cozystack/pkg/apis/apps/v1alpha1.ApplicationStatus":                     schema_pkg_apis_apps_v1alpha1_ApplicationStatus(ref),
 		"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1.ConversionRequest":                 schema_pkg_apis_apiextensions_v1_ConversionRequest(ref),
 		"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1.ConversionResponse":                schema_pkg_apis_apiextensions_v1_ConversionResponse(ref),
 		"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1.ConversionReview":                  schema_pkg_apis_apiextensions_v1_ConversionReview(ref),
@@ -157,14 +157,14 @@ func schema_pkg_apis_apps_v1alpha1_Application(ref common.ReferenceCallback) com
 					"status": {
 						SchemaProps: spec.SchemaProps{
 							Default: map[string]interface{}{},
-							Ref:     ref("github.com/aenix.io/cozystack/pkg/apis/apps/v1alpha1.ApplicationStatus"),
+							Ref:     ref("github.com/aenix-io/cozystack/pkg/apis/apps/v1alpha1.ApplicationStatus"),
 						},
 					},
 				},
 			},
 		},
 		Dependencies: []string{
-			"github.com/aenix.io/cozystack/pkg/apis/apps/v1alpha1.ApplicationStatus", "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1.JSON", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"},
+			"github.com/aenix-io/cozystack/pkg/apis/apps/v1alpha1.ApplicationStatus", "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1.JSON", "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta"},
 	}
 }
 
@@ -202,7 +202,7 @@ func schema_pkg_apis_apps_v1alpha1_ApplicationList(ref common.ReferenceCallback)
 								Schema: &spec.Schema{
 									SchemaProps: spec.SchemaProps{
 										Default: map[string]interface{}{},
-										Ref:     ref("github.com/aenix.io/cozystack/pkg/apis/apps/v1alpha1.Application"),
+										Ref:     ref("github.com/aenix-io/cozystack/pkg/apis/apps/v1alpha1.Application"),
 									},
 								},
 							},
@@ -213,7 +213,7 @@ func schema_pkg_apis_apps_v1alpha1_ApplicationList(ref common.ReferenceCallback)
 			},
 		},
 		Dependencies: []string{
-			"github.com/aenix.io/cozystack/pkg/apis/apps/v1alpha1.Application", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"},
+			"github.com/aenix-io/cozystack/pkg/apis/apps/v1alpha1.Application", "k8s.io/apimachinery/pkg/apis/meta/v1.ListMeta"},
 	}
 }
 
diff --git a/pkg/registry/apps/application/rest.go b/pkg/registry/apps/application/rest.go
index 94facf36..4b0028fd 100644
--- a/pkg/registry/apps/application/rest.go
+++ b/pkg/registry/apps/application/rest.go
@@ -39,8 +39,8 @@ import (
 	"k8s.io/client-go/dynamic"
 	"k8s.io/klog/v2"
 
-	appsv1alpha1 "github.com/aenix.io/cozystack/pkg/apis/apps/v1alpha1"
-	"github.com/aenix.io/cozystack/pkg/config"
+	appsv1alpha1 "github.com/aenix-io/cozystack/pkg/apis/apps/v1alpha1"
+	"github.com/aenix-io/cozystack/pkg/config"
 
 	// Importing API errors package to construct appropriate error responses
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
diff --git a/pkg/registry/registry.go b/pkg/registry/registry.go
index 6e198356..3aa163ab 100644
--- a/pkg/registry/registry.go
+++ b/pkg/registry/registry.go
@@ -17,7 +17,7 @@ limitations under the License.
 package registry
 
 import (
-	"github.com/aenix.io/cozystack/pkg/registry/apps/application"
+	"github.com/aenix-io/cozystack/pkg/registry/apps/application"
 	"k8s.io/apimachinery/pkg/runtime/schema"
 	genericregistry "k8s.io/apiserver/pkg/registry/generic/registry"
 	"k8s.io/apiserver/pkg/registry/rest"



Test patch:
diff --git a/internal/controller/suite_test.go b/internal/controller/suite_test.go
new file mode 100644
index 00000000..b5db051b
--- /dev/null
+++ b/internal/controller/suite_test.go
@@ -0,0 +1,96 @@
+/*
+Copyright 2025.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package controller
+
+import (
+	"context"
+	"fmt"
+	"path/filepath"
+	"runtime"
+	"testing"
+
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+
+	"k8s.io/client-go/kubernetes/scheme"
+	"k8s.io/client-go/rest"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/envtest"
+	logf "sigs.k8s.io/controller-runtime/pkg/log"
+	"sigs.k8s.io/controller-runtime/pkg/log/zap"
+
+	cozystackiov1alpha1 "github.com/aenix-io/cozystack/api/v1alpha1"
+	// +kubebuilder:scaffold:imports
+)
+
+// These tests use Ginkgo (BDD-style Go testing framework). Refer to
+// http://onsi.github.io/ginkgo/ to learn more about Ginkgo.
+
+var cfg *rest.Config
+var k8sClient client.Client
+var testEnv *envtest.Environment
+var ctx context.Context
+var cancel context.CancelFunc
+
+func TestControllers(t *testing.T) {
+	RegisterFailHandler(Fail)
+
+	RunSpecs(t, "Controller Suite")
+}
+
+var _ = BeforeSuite(func() {
+	logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))
+
+	ctx, cancel = context.WithCancel(context.TODO())
+
+	By("bootstrapping test environment")
+	testEnv = &envtest.Environment{
+		CRDDirectoryPaths:     []string{filepath.Join("..", "..", "config", "crd", "bases")},
+		ErrorIfCRDPathMissing: true,
+
+		// The BinaryAssetsDirectory is only required if you want to run the tests directly
+		// without call the makefile target test. If not informed it will look for the
+		// default path defined in controller-runtime which is /usr/local/kubebuilder/.
+		// Note that you must have the required binaries setup under the bin directory to perform
+		// the tests directly. When we run make test it will be setup and used automatically.
+		BinaryAssetsDirectory: filepath.Join("..", "..", "bin", "k8s",
+			fmt.Sprintf("1.31.0-%s-%s", runtime.GOOS, runtime.GOARCH)),
+	}
+
+	var err error
+	// cfg is defined in this file globally.
+	cfg, err = testEnv.Start()
+	Expect(err).NotTo(HaveOccurred())
+	Expect(cfg).NotTo(BeNil())
+
+	err = cozystackiov1alpha1.AddToScheme(scheme.Scheme)
+	Expect(err).NotTo(HaveOccurred())
+
+	// +kubebuilder:scaffold:scheme
+
+	k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
+	Expect(err).NotTo(HaveOccurred())
+	Expect(k8sClient).NotTo(BeNil())
+
+})
+
+var _ = AfterSuite(func() {
+	By("tearing down the test environment")
+	cancel()
+	err := testEnv.Stop()
+	Expect(err).NotTo(HaveOccurred())
+})
diff --git a/pkg/apis/apps/install/roundtrip_test.go b/pkg/apis/apps/install/roundtrip_test.go
index 48f6fad0..2393d6fe 100644
--- a/pkg/apis/apps/install/roundtrip_test.go
+++ b/pkg/apis/apps/install/roundtrip_test.go
@@ -19,7 +19,7 @@ package install
 import (
 	"testing"
 
-	appsfuzzer "github.com/aenix.io/cozystack/pkg/apis/apps/fuzzer"
+	appsfuzzer "github.com/aenix-io/cozystack/pkg/apis/apps/fuzzer"
 	"k8s.io/apimachinery/pkg/api/apitesting/roundtrip"
 )
 
diff --git a/pkg/apiserver/scheme_test.go b/pkg/apiserver/scheme_test.go
index e51f29ae..98dd1386 100644
--- a/pkg/apiserver/scheme_test.go
+++ b/pkg/apiserver/scheme_test.go
@@ -19,7 +19,7 @@ package apiserver
 import (
 	"testing"
 
-	appsfuzzer "github.com/aenix.io/cozystack/pkg/apis/apps/fuzzer"
+	appsfuzzer "github.com/aenix-io/cozystack/pkg/apis/apps/fuzzer"
 	"k8s.io/apimachinery/pkg/api/apitesting/roundtrip"
 )