refactor(telemetry): split telemetry between operator and controller

Split telemetry collection between cozystack-operator and cozystack-controller:

cozystack-operator now collects cluster-level metrics:
- cozy_cluster_info (cozystack_version, kubernetes_version)
- cozy_nodes_count (os, kernel)
- cozy_cluster_capacity (cpu, memory, nvidia.com/* resources)
- cozy_loadbalancers_count
- cozy_pvs_count (driver, size)
- cozy_package_info (name, variant)

cozystack-controller now collects application-level metrics:
- cozy_application_count (kind) - counts HelmReleases per ApplicationDefinition

Other changes:
- Add pkg/version for build-time version injection via ldflags
- Remove --cozystack-version flag (version now embedded at build time)
- Remove bundle/oidc configuration from telemetry (replaced by package_info)
- Remove cozy_workloads_count metric (replaced by cozy_application_count)

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
This commit is contained in:
Andrei Kvapil 2026-01-16 19:33:33 +01:00
parent bfd2e1fd15
commit 987a74ae5a
No known key found for this signature in database
GPG key ID: 931CF7FEACEAF765
16 changed files with 415 additions and 225 deletions

View file

@ -68,7 +68,6 @@ func main() {
var disableTelemetry bool
var telemetryEndpoint string
var telemetryInterval string
var cozystackVersion string
var reconcileDeployment bool
var tlsOpts []func(*tls.Config)
flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. "+
@ -87,8 +86,6 @@ func main() {
"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")
flag.BoolVar(&reconcileDeployment, "reconcile-deployment", false,
"If set, the Cozystack API server is assumed to run as a Deployment, else as a DaemonSet.")
opts := zap.Options{
@ -106,10 +103,9 @@ func main() {
// Configure telemetry
telemetryConfig := telemetry.Config{
Disabled: disableTelemetry,
Endpoint: telemetryEndpoint,
Interval: interval,
CozystackVersion: cozystackVersion,
Disabled: disableTelemetry,
Endpoint: telemetryEndpoint,
Interval: interval,
}
ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))

View file

@ -52,6 +52,7 @@ import (
"github.com/cozystack/cozystack/internal/cozyvaluesreplicator"
"github.com/cozystack/cozystack/internal/fluxinstall"
"github.com/cozystack/cozystack/internal/operator"
"github.com/cozystack/cozystack/internal/telemetry"
// +kubebuilder:scaffold:imports
)
@ -77,7 +78,9 @@ func main() {
var secureMetrics bool
var enableHTTP2 bool
var installFlux bool
var cozystackVersion string
var disableTelemetry bool
var telemetryEndpoint string
var telemetryInterval string
var cozyValuesSecretName string
var cozyValuesSecretNamespace string
var cozyValuesNamespaceSelector string
@ -95,8 +98,12 @@ func main() {
flag.BoolVar(&enableHTTP2, "enable-http2", false,
"If set, HTTP/2 will be enabled for the metrics and webhook servers")
flag.BoolVar(&installFlux, "install-flux", false, "Install Flux components before starting reconcile loop")
flag.StringVar(&cozystackVersion, "cozystack-version", "unknown",
"Version of Cozystack")
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(&platformSourceURL, "platform-source-url", "", "Platform source URL (oci:// or https://). If specified, generates OCIRepository or GitRepository resource.")
flag.StringVar(&platformSourceName, "platform-source-name", "cozystack-packages", "Name for the generated platform source resource (default: cozystack-packages)")
flag.StringVar(&platformSourceRef, "platform-source-ref", "", "Reference specification as key=value pairs (e.g., 'branch=main' or 'digest=sha256:...,tag=v1.0'). For OCI: digest, semver, semverFilter, tag. For Git: branch, tag, semver, name, commit.")
@ -240,6 +247,32 @@ func main() {
os.Exit(1)
}
// 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,
}
// Initialize telemetry collector
collector, err := telemetry.NewOperatorCollector(mgr.GetClient(), &telemetryConfig, config)
if err != nil {
setupLog.V(1).Info("unable to create telemetry collector, telemetry will be disabled", "error", err)
}
if collector != nil {
if err := mgr.Add(collector); err != nil {
setupLog.V(1).Info("unable to set up telemetry collector, continuing without telemetry", "error", err)
}
}
setupLog.Info("Starting controller manager")
mgrCtx := ctrl.SetupSignalHandler()
if err := mgr.Start(mgrCtx); err != nil {

1
go.mod
View file

@ -19,7 +19,6 @@ require (
github.com/spf13/cobra v1.9.1
github.com/vmware-tanzu/velero v1.17.1
go.uber.org/zap v1.27.0
gopkg.in/yaml.v2 v2.4.0
k8s.io/api v0.34.1
k8s.io/apiextensions-apiserver v0.34.1
k8s.io/apimachinery v0.34.2

2
go.sum
View file

@ -297,8 +297,6 @@ gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View file

@ -9,35 +9,36 @@ import (
"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"
helmv2 "github.com/fluxcd/helm-controller/api/v2"
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
)
// Collector handles telemetry data collection and sending
const (
// ApplicationKindLabel is the label used to identify application kind on HelmReleases
ApplicationKindLabel = "apps.cozystack.io/application.kind"
// UILabel is the label used to identify UI-managed HelmReleases
UILabel = "cozystack.io/ui"
)
// Collector handles telemetry data collection for cozystack-controller
type Collector struct {
client client.Client
discoveryClient discovery.DiscoveryInterface
config *Config
ticker *time.Ticker
stopCh chan struct{}
client client.Client
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)
}
// NewCollector creates a new telemetry collector for cozystack-controller
func NewCollector(c client.Client, config *Config, _ *rest.Config) (*Collector, error) {
return &Collector{
client: client,
discoveryClient: discoveryClient,
config: config,
client: c,
config: config,
}, nil
}
@ -67,46 +68,9 @@ func (c *Collector) Start(ctx context.Context) error {
// 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)
@ -120,151 +84,54 @@ func (c *Collector) collect(ctx context.Context) {
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))
// Get all ApplicationDefinitions to know which kinds exist
var appDefList cozyv1alpha1.ApplicationDefinitionList
if err := c.client.List(ctx, &appDefList); err != nil {
logger.Info(fmt.Sprintf("Failed to list ApplicationDefinitions: %v", err))
return
}
oidcEnabled := cozystackCM.Data["oidc-enabled"]
bundle := cozystackCM.Data["bundle-name"]
bundleEnable := cozystackCM.Data["bundle-enable"]
bundleDisable := cozystackCM.Data["bundle-disable"]
// Build a map of all known application kinds (initialized with 0)
appKindCounts := make(map[string]int)
for _, appDef := range appDefList.Items {
kind := appDef.Spec.Application.Kind
if kind != "" {
appKindCounts[kind] = 0
}
}
// 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))
// Get all HelmReleases with cozystack.io/ui=true label in one request
var hrList helmv2.HelmReleaseList
if err := c.client.List(ctx, &hrList, client.MatchingLabels{UILabel: "true"}); err != nil {
logger.Info(fmt.Sprintf("Failed to list HelmReleases: %v", err))
return
}
// Count HelmReleases by application kind
for _, hr := range hrList.Items {
kind := hr.Labels[ApplicationKindLabel]
if kind != "" {
appKindCounts[kind]++
}
}
// Create metrics buffer
var metrics strings.Builder
// Add Cozystack info metric
if len(nodeList.Items) > 0 {
k8sVersion, _ := c.discoveryClient.ServerVersion()
// Write application count metrics
for kind, count := range appKindCounts {
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,
"cozy_application_count{kind=\"%s\"} %d\n",
kind,
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++
}
// Send metrics only if there's something to send
if metrics.Len() > 0 {
if err := c.sendMetrics(clusterID, metrics.String()); err != nil {
logger.Info(fmt.Sprintf("Failed to send metrics: %v", err))
}
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))
}
}

View file

@ -12,16 +12,13 @@ type Config struct {
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",
Disabled: false,
Endpoint: "https://telemetry.cozystack.io",
Interval: 15 * time.Minute,
}
}

View file

@ -0,0 +1,282 @@
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/cozystack/cozystack/api/v1alpha1"
"github.com/cozystack/cozystack/pkg/version"
)
// OperatorCollector handles telemetry data collection for cozystack-operator
type OperatorCollector struct {
client client.Client
discoveryClient discovery.DiscoveryInterface
config *Config
ticker *time.Ticker
stopCh chan struct{}
}
// NewOperatorCollector creates a new telemetry collector for cozystack-operator
func NewOperatorCollector(c client.Client, config *Config, kubeConfig *rest.Config) (*OperatorCollector, error) {
discoveryClient, err := discovery.NewDiscoveryClientForConfig(kubeConfig)
if err != nil {
return nil, fmt.Errorf("failed to create discovery client: %w", err)
}
return &OperatorCollector{
client: c,
discoveryClient: discoveryClient,
config: config,
}, nil
}
// Start implements manager.Runnable
func (c *OperatorCollector) 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 *OperatorCollector) NeedLeaderElection() bool {
return true
}
// 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 *OperatorCollector) 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)
// Get Kubernetes version
k8sVersion, err := c.discoveryClient.ServerVersion()
if err != nil {
logger.Info(fmt.Sprintf("Failed to get Kubernetes version: %v", err))
return
}
// Get 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 cluster info metric
metrics.WriteString(fmt.Sprintf(
"cozy_cluster_info{cozystack_version=\"%s\",kubernetes_version=\"%s\"} 1\n",
version.Version,
k8sVersion.GitVersion,
))
// Collect node metrics grouped by OS and kernel
nodeOSCount := make(map[string]map[string]int) // os -> kernel -> count
for _, node := range nodeList.Items {
osKey := fmt.Sprintf("%s (%s)", node.Status.NodeInfo.OperatingSystem, node.Status.NodeInfo.OSImage)
kernelKey := node.Status.NodeInfo.KernelVersion
if _, exists := nodeOSCount[osKey]; !exists {
nodeOSCount[osKey] = make(map[string]int)
}
nodeOSCount[osKey][kernelKey]++
}
for osKey, kernels := range nodeOSCount {
for kernel, count := range kernels {
metrics.WriteString(fmt.Sprintf(
"cozy_nodes_count{os=\"%s\",kernel=\"%s\"} %d\n",
osKey,
kernel,
count,
))
}
}
// Collect cluster capacity metrics (cpu, memory, gpu)
capacityTotals := make(map[string]int64)
for _, node := range nodeList.Items {
for resourceName, quantity := range node.Status.Capacity {
name := string(resourceName)
if name == "cpu" || name == "memory" || strings.HasPrefix(name, "nvidia.com/") {
capacityTotals[name] += quantity.Value()
}
}
}
for resourceName, total := range capacityTotals {
metrics.WriteString(fmt.Sprintf(
"cozy_cluster_capacity{resource=\"%s\"} %d\n",
resourceName,
total,
))
}
// 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))
}
// 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 {
pvMetrics := make(map[string]map[string]int) // size -> driver -> count
for _, pv := range pvList.Items {
if capacity, ok := pv.Spec.Capacity[corev1.ResourceStorage]; ok {
sizeGroup := getSizeGroup(capacity)
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"
}
if _, exists := pvMetrics[sizeGroup]; !exists {
pvMetrics[sizeGroup] = make(map[string]int)
}
pvMetrics[sizeGroup][driver]++
}
}
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 installed packages
var packageList cozyv1alpha1.PackageList
if err := c.client.List(ctx, &packageList); err != nil {
logger.Info(fmt.Sprintf("Failed to list Packages: %v", err))
} else {
for _, pkg := range packageList.Items {
variant := pkg.Spec.Variant
if variant == "" {
variant = "default"
}
metrics.WriteString(fmt.Sprintf(
"cozy_package_info{name=\"%s\",variant=\"%s\"} 1\n",
pkg.Name,
variant,
))
}
}
// 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 *OperatorCollector) 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
}

View file

@ -10,29 +10,26 @@ show:
cozyhr show --namespace $(NAMESPACE) $(NAME) --plain
apply:
cozyhr show --namespace $(NAMESPACE) $(NAME) --plain | kubectl apply --filename -
cozyhr show --namespace $(NAMESPACE) $(NAME) --plain | kubectl apply -f -
diff:
cozyhr show --namespace $(NAMESPACE) $(NAME) --plain | kubectl diff --filename -
cozyhr show --namespace $(NAMESPACE) $(NAME) --plain | kubectl diff -f -
image: pre-checks image-operator image-packages
update-version:
TAG="$(call settag,$(TAG))" \
yq --inplace '.cozystackOperator.cozystackVersion = strenv(TAG)' values.yaml
image-operator:
docker buildx build --file images/cozystack-operator/Dockerfile ../../.. \
docker buildx build -f images/cozystack-operator/Dockerfile ../../.. \
--tag $(REGISTRY)/cozystack-operator:$(call settag,$(TAG)) \
--build-arg VERSION=$(call settag,$(TAG)) \
--cache-from type=registry,ref=$(REGISTRY)/cozystack-operator:latest \
--cache-to type=inline \
--metadata-file images/cozystack-operator.json \
$(BUILDX_ARGS)
IMAGE="$(REGISTRY)/cozystack-operator:$(call settag,$(TAG))@$$(yq -e '.["containerimage.digest"]' images/cozystack-operator.json -o json -r)" \
yq --inplace '.cozystackOperator.image = strenv(IMAGE)' values.yaml
yq -i '.cozystackOperator.image = strenv(IMAGE)' values.yaml
rm -f images/cozystack-operator.json
image-packages: update-version
image-packages:
mkdir -p ../../../_out/assets images
flux push artifact \
oci://$(REGISTRY)/cozystack-packages:$(call settag,$(TAG)) \
@ -41,8 +38,8 @@ image-packages: update-version
--revision="$$(git describe --tags):$$(git rev-parse HEAD)" \
2>&1 | tee images/cozystack-packages.log
export REPO="oci://$(REGISTRY)/cozystack-packages" \
export DIGEST=$$(awk --field-separator @ '/artifact successfully pushed/ {print $$2}' images/cozystack-packages.log) && \
export DIGEST=$$(awk -F @ '/artifact successfully pushed/ {print $$2}' images/cozystack-packages.log) && \
rm -f images/cozystack-packages.log && \
test -n "$$DIGEST" && \
yq --inplace '.cozystackOperator.platformSourceUrl = strenv(REPO)' values.yaml && \
yq --inplace '.cozystackOperator.platformSourceRef = "digest=" + strenv(DIGEST)' values.yaml
yq -i '.cozystackOperator.platformSourceUrl = strenv(REPO)' values.yaml && \
yq -i '.cozystackOperator.platformSourceRef = "digest=" + strenv(DIGEST)' values.yaml

View file

@ -2,6 +2,7 @@ FROM golang:1.25-alpine as builder
ARG TARGETOS
ARG TARGETARCH
ARG VERSION=dev
RUN apk add --no-cache make git
@ -12,7 +13,7 @@ RUN go mod download
# Build cozystack-operator
RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build \
-ldflags="-w -s" \
-ldflags="-w -s -X github.com/cozystack/cozystack/pkg/version.Version=${VERSION}" \
-o /cozystack-operator \
./cmd/cozystack-operator

View file

@ -55,7 +55,6 @@ spec:
- --install-flux=true
- --metrics-bind-address=0
- --health-probe-bind-address=
- --cozystack-version={{ .Values.cozystackOperator.cozystackVersion }}
{{- if .Values.cozystackOperator.disableTelemetry }}
- --disable-telemetry
{{- end }}

View file

@ -2,4 +2,3 @@ cozystackOperator:
image: ghcr.io/cozystack/cozystack/cozystack-operator:v1.0.0-alpha.1@sha256:d1beb9728b5be3ff6ee59eab071ac739cbb69d95f4bae173baa56323e97b0e5a
platformSourceUrl: 'oci://ghcr.io/cozystack/cozystack/cozystack-packages'
platformSourceRef: 'digest=sha256:ab03446b256c0faa9de0656854319f3b56e1252dab6f7b51bc6c95609d192556'
cozystackVersion: v1.0.0-alpha.1

View file

@ -4,11 +4,12 @@ NAMESPACE=cozy-system
include ../../../hack/common-envs.mk
include ../../../hack/package.mk
image: image-cozystack-controller update-version
image: image-cozystack-controller
image-cozystack-controller:
docker buildx build -f images/cozystack-controller/Dockerfile ../../.. \
--tag $(REGISTRY)/cozystack-controller:$(call settag,$(TAG)) \
--build-arg VERSION=$(call settag,$(TAG)) \
--cache-from type=registry,ref=$(REGISTRY)/cozystack-controller:latest \
--cache-to type=inline \
--metadata-file images/cozystack-controller.json \
@ -16,7 +17,3 @@ image-cozystack-controller:
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

View file

@ -2,6 +2,7 @@ FROM golang:1.25-alpine AS builder
ARG TARGETOS
ARG TARGETARCH
ARG VERSION=dev
WORKDIR /workspace
@ -13,7 +14,9 @@ COPY pkg pkg/
COPY cmd cmd/
COPY internal internal/
RUN GOOS=$TARGETOS GOARCH=$TARGETARCH CGO_ENABLED=0 go build -ldflags="-extldflags=-static" -o /cozystack-controller cmd/cozystack-controller/main.go
RUN GOOS=$TARGETOS GOARCH=$TARGETARCH CGO_ENABLED=0 go build \
-ldflags="-extldflags=-static -X github.com/cozystack/cozystack/pkg/version.Version=${VERSION}" \
-o /cozystack-controller cmd/cozystack-controller/main.go
FROM scratch

View file

@ -19,7 +19,6 @@ spec:
- name: cozystack-controller
image: "{{ .Values.cozystackController.image }}"
args:
- --cozystack-version={{ .Values.cozystackController.cozystackVersion }}
{{- if .Values.cozystackController.debug }}
- --zap-log-level=debug
{{- else }}

View file

@ -2,5 +2,4 @@ cozystackController:
image: ghcr.io/cozystack/cozystack/cozystack-controller:v1.0.0-alpha.1@sha256:b9c3f3ca0a65a7ed43d690e805ec42e1a7b3091418b7082e60478a82a5c8ddc3
debug: false
disableTelemetry: false
cozystackVersion: "v1.0.0-alpha.1"
cozystackAPIKind: "DaemonSet"

24
pkg/version/version.go Normal file
View file

@ -0,0 +1,24 @@
/*
Copyright 2025 The Cozystack Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package version provides the version information for cozystack components.
// Version is set at build time via -ldflags:
//
// go build -ldflags "-X github.com/cozystack/cozystack/pkg/version.Version=v1.0.0"
package version
// Version is set at build time via -ldflags.
var Version = "dev"