335 lines
8.0 KiB
Go
335 lines
8.0 KiB
Go
|
package license
|
||
|
|
||
|
import (
|
||
|
"context"
|
||
|
"fmt"
|
||
|
"sync/atomic"
|
||
|
"time"
|
||
|
|
||
|
"github.com/jmoiron/sqlx"
|
||
|
|
||
|
"sync"
|
||
|
|
||
|
"go.signoz.io/signoz/pkg/query-service/auth"
|
||
|
baseconstants "go.signoz.io/signoz/pkg/query-service/constants"
|
||
|
|
||
|
validate "go.signoz.io/signoz/ee/query-service/integrations/signozio"
|
||
|
"go.signoz.io/signoz/ee/query-service/model"
|
||
|
basemodel "go.signoz.io/signoz/pkg/query-service/model"
|
||
|
"go.signoz.io/signoz/pkg/query-service/telemetry"
|
||
|
"go.uber.org/zap"
|
||
|
)
|
||
|
|
||
|
var LM *Manager
|
||
|
|
||
|
// validate and update license every 24 hours
|
||
|
var validationFrequency = 24 * 60 * time.Minute
|
||
|
|
||
|
type Manager struct {
|
||
|
repo *Repo
|
||
|
mutex sync.Mutex
|
||
|
|
||
|
validatorRunning bool
|
||
|
|
||
|
// end the license validation, this is important to gracefully
|
||
|
// stopping validation and protect in-consistent updates
|
||
|
done chan struct{}
|
||
|
|
||
|
// terminated waits for the validate go routine to end
|
||
|
terminated chan struct{}
|
||
|
|
||
|
// last time the license was validated
|
||
|
lastValidated int64
|
||
|
|
||
|
// keep track of validation failure attempts
|
||
|
failedAttempts uint64
|
||
|
|
||
|
// keep track of active license and features
|
||
|
activeLicense *model.License
|
||
|
activeFeatures basemodel.FeatureSet
|
||
|
}
|
||
|
|
||
|
func StartManager(dbType string, db *sqlx.DB, features ...basemodel.Feature) (*Manager, error) {
|
||
|
if LM != nil {
|
||
|
return LM, nil
|
||
|
}
|
||
|
|
||
|
repo := NewLicenseRepo(db)
|
||
|
err := repo.InitDB(dbType)
|
||
|
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("failed to initiate license repo: %v", err)
|
||
|
}
|
||
|
|
||
|
m := &Manager{
|
||
|
repo: &repo,
|
||
|
}
|
||
|
|
||
|
if err := m.start(features...); err != nil {
|
||
|
return m, err
|
||
|
}
|
||
|
LM = m
|
||
|
return m, nil
|
||
|
}
|
||
|
|
||
|
// start loads active license in memory and initiates validator
|
||
|
func (lm *Manager) start(features ...basemodel.Feature) error {
|
||
|
err := lm.LoadActiveLicense(features...)
|
||
|
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
func (lm *Manager) Stop() {
|
||
|
close(lm.done)
|
||
|
<-lm.terminated
|
||
|
}
|
||
|
|
||
|
func (lm *Manager) SetActive(l *model.License, features ...basemodel.Feature) {
|
||
|
lm.mutex.Lock()
|
||
|
defer lm.mutex.Unlock()
|
||
|
|
||
|
if l == nil {
|
||
|
return
|
||
|
}
|
||
|
|
||
|
lm.activeLicense = l
|
||
|
lm.activeFeatures = append(l.FeatureSet, features...)
|
||
|
// set default features
|
||
|
setDefaultFeatures(lm)
|
||
|
|
||
|
err := lm.InitFeatures(lm.activeFeatures)
|
||
|
if err != nil {
|
||
|
zap.L().Panic("Couldn't activate features", zap.Error(err))
|
||
|
}
|
||
|
if !lm.validatorRunning {
|
||
|
// we want to make sure only one validator runs,
|
||
|
// we already have lock() so good to go
|
||
|
lm.validatorRunning = true
|
||
|
go lm.Validator(context.Background())
|
||
|
}
|
||
|
|
||
|
}
|
||
|
|
||
|
func setDefaultFeatures(lm *Manager) {
|
||
|
lm.activeFeatures = append(lm.activeFeatures, baseconstants.DEFAULT_FEATURE_SET...)
|
||
|
}
|
||
|
|
||
|
// LoadActiveLicense loads the most recent active license
|
||
|
func (lm *Manager) LoadActiveLicense(features ...basemodel.Feature) error {
|
||
|
active, err := lm.repo.GetActiveLicense(context.Background())
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
if active != nil {
|
||
|
lm.SetActive(active, features...)
|
||
|
} else {
|
||
|
zap.L().Info("No active license found, defaulting to basic plan")
|
||
|
// if no active license is found, we default to basic(free) plan with all default features
|
||
|
lm.activeFeatures = model.BasicPlan
|
||
|
setDefaultFeatures(lm)
|
||
|
err := lm.InitFeatures(lm.activeFeatures)
|
||
|
if err != nil {
|
||
|
zap.L().Error("Couldn't initialize features", zap.Error(err))
|
||
|
return err
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (lm *Manager) GetLicenses(ctx context.Context) (response []model.License, apiError *model.ApiError) {
|
||
|
|
||
|
licenses, err := lm.repo.GetLicenses(ctx)
|
||
|
if err != nil {
|
||
|
return nil, model.InternalError(err)
|
||
|
}
|
||
|
|
||
|
for _, l := range licenses {
|
||
|
l.ParsePlan()
|
||
|
|
||
|
if l.Key == lm.activeLicense.Key {
|
||
|
l.IsCurrent = true
|
||
|
}
|
||
|
|
||
|
if l.ValidUntil == -1 {
|
||
|
// for subscriptions, there is no end-date as such
|
||
|
// but for showing user some validity we default one year timespan
|
||
|
l.ValidUntil = l.ValidFrom + 31556926
|
||
|
}
|
||
|
|
||
|
response = append(response, l)
|
||
|
}
|
||
|
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// Validator validates license after an epoch of time
|
||
|
func (lm *Manager) Validator(ctx context.Context) {
|
||
|
defer close(lm.terminated)
|
||
|
tick := time.NewTicker(validationFrequency)
|
||
|
defer tick.Stop()
|
||
|
|
||
|
lm.Validate(ctx)
|
||
|
|
||
|
for {
|
||
|
select {
|
||
|
case <-lm.done:
|
||
|
return
|
||
|
default:
|
||
|
select {
|
||
|
case <-lm.done:
|
||
|
return
|
||
|
case <-tick.C:
|
||
|
lm.Validate(ctx)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Validate validates the current active license
|
||
|
func (lm *Manager) Validate(ctx context.Context) (reterr error) {
|
||
|
zap.L().Info("License validation started")
|
||
|
if lm.activeLicense == nil {
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
defer func() {
|
||
|
lm.mutex.Lock()
|
||
|
|
||
|
lm.lastValidated = time.Now().Unix()
|
||
|
if reterr != nil {
|
||
|
zap.L().Error("License validation completed with error", zap.Error(reterr))
|
||
|
atomic.AddUint64(&lm.failedAttempts, 1)
|
||
|
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_LICENSE_CHECK_FAILED,
|
||
|
map[string]interface{}{"err": reterr.Error()}, "", true, false)
|
||
|
} else {
|
||
|
zap.L().Info("License validation completed with no errors")
|
||
|
}
|
||
|
|
||
|
lm.mutex.Unlock()
|
||
|
}()
|
||
|
|
||
|
response, apiError := validate.ValidateLicense(lm.activeLicense.ActivationId)
|
||
|
if apiError != nil {
|
||
|
zap.L().Error("failed to validate license", zap.Error(apiError.Err))
|
||
|
return apiError.Err
|
||
|
}
|
||
|
|
||
|
if response.PlanDetails == lm.activeLicense.PlanDetails {
|
||
|
// license plan hasnt changed, nothing to do
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
if response.PlanDetails != "" {
|
||
|
|
||
|
// copy and replace the active license record
|
||
|
l := model.License{
|
||
|
Key: lm.activeLicense.Key,
|
||
|
CreatedAt: lm.activeLicense.CreatedAt,
|
||
|
PlanDetails: response.PlanDetails,
|
||
|
ValidationMessage: lm.activeLicense.ValidationMessage,
|
||
|
ActivationId: lm.activeLicense.ActivationId,
|
||
|
}
|
||
|
|
||
|
if err := l.ParsePlan(); err != nil {
|
||
|
zap.L().Error("failed to parse updated license", zap.Error(err))
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
// updated plan is parsable, check if plan has changed
|
||
|
if lm.activeLicense.PlanDetails != response.PlanDetails {
|
||
|
err := lm.repo.UpdatePlanDetails(ctx, lm.activeLicense.Key, response.PlanDetails)
|
||
|
if err != nil {
|
||
|
// unexpected db write issue but we can let the user continue
|
||
|
// and wait for update to work in next cycle.
|
||
|
zap.L().Error("failed to validate license", zap.Error(err))
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// activate the update license plan
|
||
|
lm.SetActive(&l)
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// Activate activates a license key with signoz server
|
||
|
func (lm *Manager) Activate(ctx context.Context, key string) (licenseResponse *model.License, errResponse *model.ApiError) {
|
||
|
defer func() {
|
||
|
if errResponse != nil {
|
||
|
userEmail, err := auth.GetEmailFromJwt(ctx)
|
||
|
if err == nil {
|
||
|
telemetry.GetInstance().SendEvent(telemetry.TELEMETRY_LICENSE_ACT_FAILED,
|
||
|
map[string]interface{}{"err": errResponse.Err.Error()}, userEmail, true, false)
|
||
|
}
|
||
|
}
|
||
|
}()
|
||
|
|
||
|
response, apiError := validate.ActivateLicense(key, "")
|
||
|
if apiError != nil {
|
||
|
zap.L().Error("failed to activate license", zap.Error(apiError.Err))
|
||
|
return nil, apiError
|
||
|
}
|
||
|
|
||
|
l := &model.License{
|
||
|
Key: key,
|
||
|
ActivationId: response.ActivationId,
|
||
|
PlanDetails: response.PlanDetails,
|
||
|
}
|
||
|
|
||
|
// parse validity and features from the plan details
|
||
|
err := l.ParsePlan()
|
||
|
|
||
|
if err != nil {
|
||
|
zap.L().Error("failed to activate license", zap.Error(err))
|
||
|
return nil, model.InternalError(err)
|
||
|
}
|
||
|
|
||
|
// store the license before activating it
|
||
|
err = lm.repo.InsertLicense(ctx, l)
|
||
|
if err != nil {
|
||
|
zap.L().Error("failed to activate license", zap.Error(err))
|
||
|
return nil, model.InternalError(err)
|
||
|
}
|
||
|
|
||
|
// license is valid, activate it
|
||
|
lm.SetActive(l)
|
||
|
return l, nil
|
||
|
}
|
||
|
|
||
|
// CheckFeature will be internally used by backend routines
|
||
|
// for feature gating
|
||
|
func (lm *Manager) CheckFeature(featureKey string) error {
|
||
|
feature, err := lm.repo.GetFeature(featureKey)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
if feature.Active {
|
||
|
return nil
|
||
|
}
|
||
|
return basemodel.ErrFeatureUnavailable{Key: featureKey}
|
||
|
}
|
||
|
|
||
|
// GetFeatureFlags returns current active features
|
||
|
func (lm *Manager) GetFeatureFlags() (basemodel.FeatureSet, error) {
|
||
|
return lm.repo.GetAllFeatures()
|
||
|
}
|
||
|
|
||
|
func (lm *Manager) InitFeatures(features basemodel.FeatureSet) error {
|
||
|
return lm.repo.InitFeatures(features)
|
||
|
}
|
||
|
|
||
|
func (lm *Manager) UpdateFeatureFlag(feature basemodel.Feature) error {
|
||
|
return lm.repo.UpdateFeature(feature)
|
||
|
}
|
||
|
|
||
|
func (lm *Manager) GetFeatureFlag(key string) (basemodel.Feature, error) {
|
||
|
return lm.repo.GetFeature(key)
|
||
|
}
|
||
|
|
||
|
// GetRepo return the license repo
|
||
|
func (lm *Manager) GetRepo() *Repo {
|
||
|
return lm.repo
|
||
|
}
|