diff --git a/cmd/main.go b/cmd/main.go index 1f902b5c4..7bdab0a6a 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -36,6 +36,7 @@ import ( hmcmirantiscomv1alpha1 "github.com/Mirantis/hmc/api/v1alpha1" "github.com/Mirantis/hmc/internal/controller" + "github.com/Mirantis/hmc/internal/telemetry" hmcwebhook "github.com/Mirantis/hmc/internal/webhook" //+kubebuilder:scaffold:imports ) @@ -65,6 +66,7 @@ func main() { var createManagement bool var createTemplates bool var hmcTemplatesChartName string + var enableTelemetry bool var enableWebhook bool var webhookPort int var webhookCertDir string @@ -84,6 +86,7 @@ func main() { flag.BoolVar(&createTemplates, "create-templates", true, "Create HMC Templates.") flag.StringVar(&hmcTemplatesChartName, "hmc-templates-chart-name", "hmc-templates", "The name of the helm chart with HMC Templates.") + flag.BoolVar(&enableTelemetry, "enable-telemetry", true, "Collect and send telemetry data.") flag.BoolVar(&enableWebhook, "enable-webhook", true, "Enable admission webhook.") flag.IntVar(&webhookPort, "webhook-port", 9443, "Admission webhook port.") flag.StringVar(&webhookCertDir, "webhook-cert-dir", "/tmp/k8s-webhook-server/serving-certs/", @@ -186,6 +189,15 @@ func main() { os.Exit(1) } + if enableTelemetry { + if err = mgr.Add(&telemetry.Tracker{ + Client: mgr.GetClient(), + }); err != nil { + setupLog.Error(err, "unable to create telemetry tracker") + os.Exit(1) + } + } + //+kubebuilder:scaffold:builder if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { diff --git a/internal/telemetry/event.go b/internal/telemetry/event.go index adee9bf91..389397762 100644 --- a/internal/telemetry/event.go +++ b/internal/telemetry/event.go @@ -21,7 +21,8 @@ import ( ) const ( - deploymentCreateEvent = "deployment-create" + deploymentCreateEvent = "deployment-create" + deploymentHeartbeatEvent = "deployment-heartbeat" ) func TrackDeploymentCreate(id, deploymentID, template string, dryRun bool) error { @@ -34,6 +35,20 @@ func TrackDeploymentCreate(id, deploymentID, template string, dryRun bool) error return TrackEvent(deploymentCreateEvent, id, props) } +func TrackDeploymentHeartbeat(id, deploymentID, clusterID, template, templateHelmChartVersion, infrastructureProvider, bootstrapProvider, controlPlaneProvider string) error { + props := map[string]interface{}{ + "hmcVersion": build.Version, + "deploymentID": deploymentID, + "clusterID": clusterID, + "template": template, + "templateHelmChartVersion": templateHelmChartVersion, + "infrastructureProvider": infrastructureProvider, + "bootstrapProvider": bootstrapProvider, + "controlPlaneProvider": controlPlaneProvider, + } + return TrackEvent(deploymentHeartbeatEvent, id, props) +} + func TrackEvent(name, id string, properties map[string]interface{}) error { if client == nil { return nil diff --git a/internal/telemetry/tracker.go b/internal/telemetry/tracker.go new file mode 100644 index 000000000..66ad34edd --- /dev/null +++ b/internal/telemetry/tracker.go @@ -0,0 +1,107 @@ +// Copyright 2024 +// +// 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 telemetry + +import ( + "context" + "errors" + "fmt" + "strings" + "time" + + "k8s.io/apimachinery/pkg/types" + crclient "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + + "github.com/Mirantis/hmc/api/v1alpha1" +) + +type Tracker struct { + crclient.Client +} + +const interval = 10 * time.Minute + +func (t *Tracker) Start(ctx context.Context) error { + timer := time.NewTimer(0) + for { + select { + case <-timer.C: + t.Tick(ctx) + timer.Reset(interval) + case <-ctx.Done(): + return nil + } + } +} + +func (t *Tracker) Tick(ctx context.Context) { + l := log.FromContext(ctx).WithName("telemetry tracker") + + logger := l.WithValues("event", deploymentHeartbeatEvent) + err := t.trackDeploymentHeartbeat(ctx) + if err != nil { + logger.Error(err, "failed to track an event") + } else { + logger.Info("successfully tracked an event") + } +} + +func (t *Tracker) trackDeploymentHeartbeat(ctx context.Context) error { + mgmt := &v1alpha1.Management{} + mgmtRef := types.NamespacedName{Namespace: v1alpha1.ManagementNamespace, Name: v1alpha1.ManagementName} + err := t.Get(ctx, mgmtRef, mgmt) + if err != nil { + return err + } + + templates := make(map[string]v1alpha1.Template) + templatesList := &v1alpha1.TemplateList{} + err = t.List(ctx, templatesList, crclient.InNamespace(v1alpha1.TemplatesNamespace)) + if err != nil { + return err + } + for _, template := range templatesList.Items { + templates[template.Name] = template + } + + var errs error + deployments := &v1alpha1.DeploymentList{} + err = t.List(ctx, deployments, &crclient.ListOptions{}) + if err != nil { + return err + } + + for _, deployment := range deployments.Items { + template := templates[deployment.Spec.Template] + // TODO: get k0s cluster ID once it's exposed in k0smotron API + clusterID := "" + err = TrackDeploymentHeartbeat( + string(mgmt.UID), + string(deployment.UID), + clusterID, + deployment.Spec.Template, + template.Spec.Helm.ChartVersion, + strings.Join(template.Status.Providers.InfrastructureProviders, ","), + strings.Join(template.Status.Providers.BootstrapProviders, ","), + strings.Join(template.Status.Providers.ControlPlaneProviders, ","), + ) + if err != nil { + errs = errors.Join(errs, fmt.Errorf("failed to track the heartbeat of the deployment %s/%s", deployment.Namespace, deployment.Name)) + continue + } + } + return errs +} diff --git a/templates/hmc/templates/deployment.yaml b/templates/hmc/templates/deployment.yaml index 7b87c2761..028835a1a 100644 --- a/templates/hmc/templates/deployment.yaml +++ b/templates/hmc/templates/deployment.yaml @@ -28,6 +28,7 @@ spec: {{- end }} - --create-management={{ .Values.controller.createManagement }} - --create-templates={{ .Values.controller.createTemplates }} + - --enable-telemetry={{ .Values.controller.enableTelemetry }} - --enable-webhook={{ .Values.admissionWebhook.enabled }} - --webhook-port={{ .Values.admissionWebhook.port }} - --webhook-cert-dir={{ .Values.admissionWebhook.certDir }} diff --git a/templates/hmc/values.schema.json b/templates/hmc/values.schema.json index 8a1112dc6..f8df0f9d5 100644 --- a/templates/hmc/values.schema.json +++ b/templates/hmc/values.schema.json @@ -118,6 +118,9 @@ }, "createTemplate": { "type": "boolean" + }, + "enableTelemetry": { + "type": "boolean" } } }, diff --git a/templates/hmc/values.yaml b/templates/hmc/values.yaml index c9f6ab6e0..a0ee6dac7 100644 --- a/templates/hmc/values.yaml +++ b/templates/hmc/values.yaml @@ -12,6 +12,7 @@ controller: insecureRegistry: false createManagement: true createTemplates: true + enableTelemetry: true containerSecurityContext: allowPrivilegeEscalation: false