pkg/license/license.go (200 lines of code) (raw):

// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one // or more contributor license agreements. Licensed under the Elastic License 2.0; // you may not use this file except in compliance with the Elastic License 2.0. package license import ( "context" "fmt" "math" "reflect" "strconv" "time" "github.com/prometheus/client_golang/prometheus" "go.elastic.co/apm/v2" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" commonv1 "github.com/elastic/cloud-on-k8s/v3/pkg/apis/common/v1" "github.com/elastic/cloud-on-k8s/v3/pkg/controller/common/license" "github.com/elastic/cloud-on-k8s/v3/pkg/controller/common/reconciler" "github.com/elastic/cloud-on-k8s/v3/pkg/controller/common/tracing" "github.com/elastic/cloud-on-k8s/v3/pkg/utils/k8s" ulog "github.com/elastic/cloud-on-k8s/v3/pkg/utils/log" "github.com/elastic/cloud-on-k8s/v3/pkg/utils/metrics" ) const ( // defaultOperatorLicenseLevel is the default license level when no operator license is installed defaultOperatorLicenseLevel = "basic" // LicensingCfgMapName is the name of the config map used to store licensing information LicensingCfgMapName = "elastic-licensing" // Type represents the Elastic usage type used to mark the config map that stores licensing information Type = "elastic-usage" // GiB represents the number of bytes for 1 GiB GiB = 1024 * 1024 * 1024 elasticsearchKey = "elasticsearch" kibanaKey = "kibana" apmKey = "apm" entSearchKey = "enterprise_search" logstashKey = "logstash" totalKey = "total_managed" ) type managedMemory struct { resource.Quantity label string } func newManagedMemory(binarySI int64, label string) managedMemory { return managedMemory{ Quantity: *resource.NewQuantity(binarySI, resource.BinarySI), label: label, } } func (mm managedMemory) inGiB() float64 { return inGiB(mm.Quantity) } func (mm managedMemory) intoMap(m map[string]string) { m[mm.label+"_memory"] = fmt.Sprintf("%0.2fGiB", inGiB(mm.Quantity)) m[mm.label+"_memory_bytes"] = fmt.Sprintf("%d", mm.Quantity.Value()) } type memoryUsage struct { appUsage map[string]managedMemory totalMemory managedMemory } func newMemoryUsage() memoryUsage { return memoryUsage{ appUsage: map[string]managedMemory{}, totalMemory: managedMemory{label: totalKey}, } } func (mu *memoryUsage) add(memory managedMemory) { mu.appUsage[memory.label] = memory mu.totalMemory.Add(memory.Quantity) } // LicensingInfo represents information about the operator license including the total memory of all Elastic managed // components type LicensingInfo struct { memoryUsage Timestamp string EckLicenseLevel string EckLicenseExpiryDate *time.Time MaxEnterpriseResourceUnits int64 EnterpriseResourceUnits int64 } // toMap transforms a LicensingInfo to a map of string, in order to fill in the data of a config map func (li LicensingInfo) toMap() map[string]string { m := map[string]string{ "timestamp": li.Timestamp, "eck_license_level": li.EckLicenseLevel, "enterprise_resource_units": strconv.FormatInt(li.EnterpriseResourceUnits, 10), } for _, v := range li.appUsage { v.intoMap(m) } li.totalMemory.intoMap(m) if li.MaxEnterpriseResourceUnits > 0 { m["max_enterprise_resource_units"] = strconv.FormatInt(li.MaxEnterpriseResourceUnits, 10) } if li.EckLicenseExpiryDate != nil { m["eck_license_expiry_date"] = li.EckLicenseExpiryDate.Format(time.RFC3339) } return m } func (li LicensingInfo) ReportAsMetrics() { labels := prometheus.Labels{metrics.LicenseLevelLabel: li.EckLicenseLevel} metrics.LicensingTotalMemoryGauge.With(labels).Set(li.totalMemory.inGiB()) metrics.LicensingESMemoryGauge.With(labels).Set(li.appUsage[elasticsearchKey].inGiB()) metrics.LicensingKBMemoryGauge.With(labels).Set(li.appUsage[kibanaKey].inGiB()) metrics.LicensingAPMMemoryGauge.With(labels).Set(li.appUsage[apmKey].inGiB()) metrics.LicensingEntSearchMemoryGauge.With(labels).Set(li.appUsage[entSearchKey].inGiB()) metrics.LicensingLogstashMemoryGauge.With(labels).Set(li.appUsage[logstashKey].inGiB()) metrics.LicensingTotalERUGauge.With(labels).Set(float64(li.EnterpriseResourceUnits)) if li.MaxEnterpriseResourceUnits > 0 { metrics.LicensingMaxERUGauge.With(labels).Set(float64(li.MaxEnterpriseResourceUnits)) } } // LicensingResolver resolves the licensing information of the operator type LicensingResolver struct { operatorNs string client k8s.Client } // ToInfo returns licensing information given the total memory of all Elastic managed components func (r LicensingResolver) ToInfo(ctx context.Context, memoryUsage memoryUsage) (LicensingInfo, error) { operatorLicense, err := r.getOperatorLicense(ctx) if err != nil { return LicensingInfo{}, err } licensingInfo := LicensingInfo{ memoryUsage: memoryUsage, Timestamp: time.Now().Format(time.RFC3339), EckLicenseLevel: r.getOperatorLicenseLevel(operatorLicense), EckLicenseExpiryDate: r.getOperatorLicenseExpiry(operatorLicense), EnterpriseResourceUnits: inEnterpriseResourceUnits(memoryUsage.totalMemory.Quantity), } // include the max ERUs only for a non trial/basic license if maxERUs := r.getMaxEnterpriseResourceUnits(operatorLicense); maxERUs > 0 { licensingInfo.MaxEnterpriseResourceUnits = maxERUs } return licensingInfo, nil } // Save updates or creates licensing information in a config map // This relies on UnconditionalUpdates being supported configmaps and may change in k8s v2: https://github.com/kubernetes/kubernetes/issues/21330 func (r LicensingResolver) Save(ctx context.Context, info LicensingInfo) error { span, ctx := apm.StartSpan(ctx, "save_license_info", tracing.SpanTypeApp) defer span.End() ulog.FromContext(ctx).V(1).Info("Saving", "namespace", r.operatorNs, "configmap_name", LicensingCfgMapName, "license_info", info) nsn := types.NamespacedName{ Namespace: r.operatorNs, Name: LicensingCfgMapName, } expected := corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ Namespace: nsn.Namespace, Name: nsn.Name, Labels: map[string]string{ commonv1.TypeLabelName: Type, }, }, Data: info.toMap(), } reconciled := &corev1.ConfigMap{} return reconciler.ReconcileResource(reconciler.Params{ Context: ctx, Client: r.client, Expected: &expected, Reconciled: reconciled, NeedsUpdate: func() bool { // do not compare timestamp, as it will always change expectedData, reconciledData := map[string]string{}, map[string]string{} for k, v := range expected.Data { expectedData[k] = v } for k, v := range reconciled.Data { reconciledData[k] = v } delete(expectedData, "timestamp") delete(reconciledData, "timestamp") return !reflect.DeepEqual(expectedData, reconciledData) }, UpdateReconciled: func() { expected.DeepCopyInto(reconciled) }, }) } // getOperatorLicense gets the operator license. func (r LicensingResolver) getOperatorLicense(ctx context.Context) (*license.EnterpriseLicense, error) { checker := license.NewLicenseChecker(r.client, r.operatorNs) return checker.CurrentEnterpriseLicense(ctx) } // getOperatorLicenseLevel gets the level of the operator license. // If no license is given, the defaultOperatorLicenseLevel is returned. func (r LicensingResolver) getOperatorLicenseLevel(lic *license.EnterpriseLicense) string { if lic == nil { return defaultOperatorLicenseLevel } return string(lic.License.Type) } // getOperatorLicenseExpiry returns the expiry date of the given Enterprise license or nil. func (r LicensingResolver) getOperatorLicenseExpiry(lic *license.EnterpriseLicense) *time.Time { if lic != nil { t := time.Unix(0, lic.License.ExpiryDateInMillis*int64(time.Millisecond)) return &t } return nil } // getMaxEnterpriseResourceUnits returns the maximum of enterprise resources units that is allowed for a given license. // For old style enterprise orchestration licenses which only have max_instances, the maximum of enterprise resources // units is derived by dividing max_instances by 2. func (r LicensingResolver) getMaxEnterpriseResourceUnits(lic *license.EnterpriseLicense) int64 { if lic == nil { return 0 } maxERUs := lic.License.MaxResourceUnits if maxERUs == 0 { maxERUs = lic.License.MaxInstances / 2 } return int64(maxERUs) } // inGiB converts a resource.Quantity in gibibytes func inGiB(q resource.Quantity) float64 { // divide the value (in bytes) per 1GiB return float64(q.Value()) / (1 * GiB) } // inEnterpriseResourceUnits converts a resource.Quantity to Elastic Enterprise resource units func inEnterpriseResourceUnits(q resource.Quantity) int64 { // divide by the value (in bytes) per 64 GiB eru := float64(q.Value()) / (64 * GiB) // round to the nearest superior integer return int64(math.Ceil(eru)) }