Compare commits

...

32 Commits

Author SHA1 Message Date
vikrantgupta25
e6cacced7f chore: added v3 support in v2 endpoint 2024-11-11 16:57:22 +05:30
vikrantgupta25
d171861aed feat: added license status and category handling for query-service 2024-11-07 00:35:04 +05:30
vikrantgupta25
a745c72ae9 chore: license testing fixes 2024-11-06 23:06:32 +05:30
vikrantgupta25
bde7b85d17 feat: update zeus URL 2024-11-06 19:04:44 +05:30
Vikrant Gupta
8b3d1f6178 Merge branch 'develop' into licenses-v3 2024-11-06 17:13:40 +05:30
vikrantgupta25
21d25a3b2e feat: cosmetic changes 2024-11-06 17:13:19 +05:30
vikrantgupta25
7136e5d8e2 feat: cosmetic changes 2024-11-06 17:11:01 +05:30
Vikrant Gupta
2161db029b Merge branch 'develop' into licenses-v3 2024-11-06 10:31:24 +05:30
vikrantgupta25
1235d97ba9 chore: make the licenses id not null 2024-11-06 10:31:04 +05:30
vikrantgupta25
70c060f779 feat: added more test cases 2024-11-05 18:28:11 +05:30
vikrantgupta25
5950e3d727 chore: added test cases for new license function 2024-11-05 17:14:12 +05:30
vikrantgupta25
4d11a18977 feat: added default basic plan 2024-11-05 14:32:52 +05:30
vikrantgupta25
1ca305cf86 fix: utilise factory pattern 2024-11-05 14:22:16 +05:30
vikrantgupta25
4021185a0b feat: model changes 2024-11-05 12:39:12 +05:30
vikrantgupta25
9557dad38f feat: model changes 2024-11-05 12:35:28 +05:30
vikrantgupta25
cc645c5488 chore: some minor edits 2024-11-05 08:41:55 +05:30
vikrantgupta25
9214f7cb2f fix: nil pointer error 2024-11-05 08:10:46 +05:30
Vikrant Gupta
11fee16538 Merge branch 'develop' into licenses-v3 2024-11-05 08:08:02 +05:30
vikrantgupta25
508c8a556c chore: refactor the entire code to cleanup interfaces 2024-11-05 08:07:09 +05:30
vikrantgupta25
ee510e6932 feat: added list licenses call 2024-11-04 14:03:15 +05:30
vikrantgupta25
4535e15479 chore: text updates 2024-11-04 13:54:29 +05:30
vikrantgupta25
15d10a0dbe feat: handle the start manager license activation 2024-11-04 13:53:21 +05:30
Vikrant Gupta
78820106a7 Merge branch 'develop' into licenses-v3 2024-11-02 09:18:18 +05:30
Vikrant Gupta
e5cbdc2142 Merge branch 'develop' into licenses-v3 2024-11-01 23:11:28 +05:30
vikrantgupta25
262c2ad690 feat: added refresh licenses handler 2024-11-01 23:04:23 +05:30
Vikrant Gupta
db69f4f67b Merge branch 'develop' into licenses-v3 2024-11-01 22:13:06 +05:30
vikrantgupta25
ce793964b6 feat: some typo fix 2024-11-01 22:12:44 +05:30
vikrantgupta25
a33907872a feat: added config parameter for licenses v3 and the boot option 2024-11-01 22:09:20 +05:30
vikrantgupta25
8554c76e3a chore: added a couple of todos 2024-11-01 19:45:34 +05:30
vikrantgupta25
44679be793 feat: validator changes 2024-11-01 19:36:26 +05:30
vikrantgupta25
eb898df045 feat: added some more logic 2024-11-01 18:55:45 +05:30
vikrantgupta25
551e0b94fc feat: setup for licenses v3 integration 2024-11-01 16:37:00 +05:30
14 changed files with 836 additions and 10 deletions

View File

@@ -40,6 +40,7 @@ type APIHandlerOptions struct {
// Querier Influx Interval
FluxInterval time.Duration
UseLogsNewSchema bool
UseLicensesV3 bool
}
type APIHandler struct {
@@ -65,6 +66,7 @@ func NewAPIHandler(opts APIHandlerOptions) (*APIHandler, error) {
Cache: opts.Cache,
FluxInterval: opts.FluxInterval,
UseLogsNewSchema: opts.UseLogsNewSchema,
UseLicensesV3: opts.UseLicensesV3,
})
if err != nil {
@@ -173,10 +175,25 @@ func (ah *APIHandler) RegisterRoutes(router *mux.Router, am *baseapp.AuthMiddlew
router.HandleFunc("/api/v1/dashboards/{uuid}/lock", am.EditAccess(ah.lockDashboard)).Methods(http.MethodPut)
router.HandleFunc("/api/v1/dashboards/{uuid}/unlock", am.EditAccess(ah.unlockDashboard)).Methods(http.MethodPut)
// v2
router.HandleFunc("/api/v2/licenses",
am.ViewAccess(ah.listLicensesV2)).
Methods(http.MethodGet)
// v3
router.HandleFunc("/api/v3/licenses",
am.ViewAccess(ah.listLicensesV3)).
Methods(http.MethodGet)
router.HandleFunc("/api/v3/licenses",
am.AdminAccess(ah.applyLicenseV3)).
Methods(http.MethodPost)
router.HandleFunc("/api/v3/licenses/refresh",
am.AdminAccess(ah.refreshLicensesV3)).
Methods(http.MethodPut)
// v4
router.HandleFunc("/api/v4/query_range", am.ViewAccess(ah.queryRangeV4)).Methods(http.MethodPost)
// Gateway

View File

@@ -9,6 +9,7 @@ import (
"go.signoz.io/signoz/ee/query-service/constants"
"go.signoz.io/signoz/ee/query-service/model"
basemodel "go.signoz.io/signoz/pkg/query-service/model"
"go.uber.org/zap"
)
@@ -59,6 +60,42 @@ type billingDetails struct {
} `json:"data"`
}
type ApplyLicenseRequest struct {
LicenseKey string `json:"key"`
}
type ListLicenseResponse struct {
ID string `json:"id"`
Data map[string]interface{} `json:"data"`
Features basemodel.FeatureSet `json:"features"`
Plan model.Plan `json:"plan"`
IsCurrent bool `json:"isCurrent"`
ValidFrom int64 `json:"validFrom"`
ValidUntil int64 `json:"validUntil"`
Category string `json:"category"`
Status string `json:"status"`
}
func convertLicenseV3ToListLicenseResponse(licensesV3 []model.LicenseV3) []ListLicenseResponse {
listLicenses := []ListLicenseResponse{}
for _, license := range licensesV3 {
listLicense := ListLicenseResponse{
ID: license.ID,
Data: license.Data,
Features: license.Features,
Plan: license.Plan,
IsCurrent: license.IsCurrent,
ValidFrom: license.ValidFrom,
ValidUntil: license.ValidUntil,
Category: license.Category,
Status: license.Status,
}
listLicenses = append(listLicenses, listLicense)
}
return listLicenses
}
func (ah *APIHandler) listLicenses(w http.ResponseWriter, r *http.Request) {
licenses, apiError := ah.LM().GetLicenses(context.Background())
if apiError != nil {
@@ -88,6 +125,51 @@ func (ah *APIHandler) applyLicense(w http.ResponseWriter, r *http.Request) {
ah.Respond(w, license)
}
func (ah *APIHandler) listLicensesV3(w http.ResponseWriter, r *http.Request) {
licenses, apiError := ah.LM().GetLicensesV3(r.Context())
if apiError != nil {
RespondError(w, apiError, nil)
return
}
ah.Respond(w, convertLicenseV3ToListLicenseResponse(licenses))
}
// this function is called by zeus when inserting licenses in the query-service
func (ah *APIHandler) applyLicenseV3(w http.ResponseWriter, r *http.Request) {
var licenseKey ApplyLicenseRequest
if err := json.NewDecoder(r.Body).Decode(&licenseKey); err != nil {
RespondError(w, model.BadRequest(err), nil)
return
}
if licenseKey.LicenseKey == "" {
RespondError(w, model.BadRequest(fmt.Errorf("license key is required")), nil)
return
}
l, apiError := ah.LM().ActivateV3(r.Context(), licenseKey.LicenseKey)
if apiError != nil {
RespondError(w, apiError, nil)
return
}
ah.Respond(w, l)
}
func (ah *APIHandler) refreshLicensesV3(w http.ResponseWriter, r *http.Request) {
apiError := ah.LM().RefreshLicense(r.Context())
if apiError != nil {
RespondError(w, apiError, nil)
return
}
ah.Respond(w, nil)
}
func (ah *APIHandler) checkout(w http.ResponseWriter, r *http.Request) {
type checkoutResponse struct {
@@ -154,9 +236,41 @@ func (ah *APIHandler) getBilling(w http.ResponseWriter, r *http.Request) {
ah.Respond(w, billingResponse.Data)
}
func convertLicenseV3ToLicenseV2(licenses *[]model.LicenseV3) []model.License {
licensesV2 := []model.License{}
for _, l := range *licenses {
licenseV2 := model.License{
Key: l.Key,
ActivationId: "",
CreatedAt: l.CreatedAt,
PlanDetails: "",
FeatureSet: l.Features,
ValidationMessage: "",
IsCurrent: l.IsCurrent,
LicensePlan: model.LicensePlan{
PlanKey: l.Plan.Name,
ValidFrom: l.ValidFrom,
ValidUntil: l.ValidUntil,
Status: l.Status},
}
licensesV2 = append(licensesV2, licenseV2)
}
return licensesV2
}
func (ah *APIHandler) listLicensesV2(w http.ResponseWriter, r *http.Request) {
licenses, apiError := ah.LM().GetLicenses(context.Background())
var licenses []model.License
var apiError *model.ApiError
if ah.UseLicensesV3 {
licensesV3, err := ah.LM().GetLicensesV3(context.Background())
apiError = err
licenses = convertLicenseV3ToLicenseV2(&licensesV3)
} else {
licenses, apiError = ah.LM().GetLicenses(context.Background())
}
if apiError != nil {
RespondError(w, apiError, nil)
}

View File

@@ -78,6 +78,7 @@ type ServerOptions struct {
Cluster string
GatewayUrl string
UseLogsNewSchema bool
UseLicensesV3 bool
}
// Server runs HTTP api service
@@ -134,7 +135,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
}
// initiate license manager
lm, err := licensepkg.StartManager("sqlite", localDB)
lm, err := licensepkg.StartManager("sqlite", localDB, serverOptions.UseLicensesV3)
if err != nil {
return nil, err
}
@@ -270,6 +271,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
FluxInterval: fluxInterval,
Gateway: gatewayProxy,
UseLogsNewSchema: serverOptions.UseLogsNewSchema,
UseLicensesV3: serverOptions.UseLicensesV3,
}
apiHandler, err := api.NewAPIHandler(apiOpts)

View File

@@ -13,6 +13,7 @@ var LicenseAPIKey = GetOrDefaultEnv("SIGNOZ_LICENSE_API_KEY", "")
var SaasSegmentKey = GetOrDefaultEnv("SIGNOZ_SAAS_SEGMENT_KEY", "")
var FetchFeatures = GetOrDefaultEnv("FETCH_FEATURES", "false")
var ZeusFeaturesURL = GetOrDefaultEnv("ZEUS_FEATURES_URL", "ZeusFeaturesURL")
var ZeusURL = GetOrDefaultEnv("ZEUS_URL", "ZeusURL")
func GetOrDefaultEnv(key string, fallback string) string {
v := os.Getenv(key)

View File

@@ -13,3 +13,8 @@ type ActivationResponse struct {
ActivationId string `json:"ActivationId"`
PlanDetails string `json:"PlanDetails"`
}
type ValidateLicenseResponse struct {
Status status `json:"status"`
Data map[string]interface{} `json:"data"`
}

View File

@@ -7,6 +7,7 @@ import (
"fmt"
"io"
"net/http"
"time"
"github.com/pkg/errors"
"go.uber.org/zap"
@@ -23,12 +24,14 @@ const (
)
type Client struct {
Prefix string
Prefix string
GatewayUrl string
}
func New() *Client {
return &Client{
Prefix: constants.LicenseSignozIo,
Prefix: constants.LicenseSignozIo,
GatewayUrl: constants.ZeusURL,
}
}
@@ -116,6 +119,57 @@ func ValidateLicense(activationId string) (*ActivationResponse, *model.ApiError)
}
func ValidateLicenseV3(licenseKey string) (*model.LicenseV3, *model.ApiError) {
// Creating an HTTP client with a timeout for better control
client := &http.Client{
Timeout: 10 * time.Second,
}
req, err := http.NewRequest("GET", C.GatewayUrl+"/v2/licenses/me", nil)
if err != nil {
return nil, model.BadRequest(errors.Wrap(err, fmt.Sprintf("failed to create request: %w", err)))
}
// Setting the custom header
req.Header.Set("X-Signoz-Cloud-Api-Key", licenseKey)
response, err := client.Do(req)
if err != nil {
return nil, model.BadRequest(errors.Wrap(err, fmt.Sprintf("failed to make post request: %w", err)))
}
body, err := io.ReadAll(response.Body)
if err != nil {
return nil, model.BadRequest(errors.Wrap(err, fmt.Sprintf("failed to read validation response from %v", C.GatewayUrl)))
}
defer response.Body.Close()
switch response.StatusCode {
case 200, 201:
a := ValidateLicenseResponse{}
err = json.Unmarshal(body, &a)
if err != nil {
return nil, model.BadRequest(errors.Wrap(err, "failed to marshal license validation response"))
}
license, err := model.NewLicenseV3(a.Data)
if err != nil {
return nil, model.BadRequest(errors.Wrap(err, "failed to generate new license v3"))
}
return license, nil
case 400, 401:
return nil, model.BadRequest(errors.Wrap(fmt.Errorf(string(body)),
fmt.Sprintf("bad request error received from %v", C.GatewayUrl)))
default:
return nil, model.InternalError(errors.Wrap(fmt.Errorf(string(body)),
fmt.Sprintf("internal request error received from %v", C.GatewayUrl)))
}
}
func NewPostRequestWithCtx(ctx context.Context, url string, contentType string, body io.Reader) (*http.Request, error) {
req, err := http.NewRequestWithContext(ctx, POST, url, body)
if err != nil {

View File

@@ -3,6 +3,7 @@ package license
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"time"
@@ -48,6 +49,34 @@ func (r *Repo) GetLicenses(ctx context.Context) ([]model.License, error) {
return licenses, nil
}
func (r *Repo) GetLicensesV3(ctx context.Context) ([]model.LicenseV3, error) {
licensesData := []model.LicenseDB{}
licenseV3Data := []model.LicenseV3{}
query := "SELECT id,key,data FROM licenses_v3"
err := r.db.Select(&licensesData, query)
if err != nil {
return nil, fmt.Errorf("failed to get licenses from db: %v", err)
}
for _, l := range licensesData {
var licenseData map[string]interface{}
err := json.Unmarshal([]byte(l.Data), &licenseData)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal data into licenseData : %v", err)
}
license, err := model.NewLicenseV3WithIDAndKey(l.ID, l.Key, licenseData)
if err != nil {
return nil, fmt.Errorf("failed to get licenses v3 schema : %v", err)
}
licenseV3Data = append(licenseV3Data, *license)
}
return licenseV3Data, nil
}
// GetActiveLicense fetches the latest active license from DB.
// If the license is not present, expect a nil license and a nil error in the output.
func (r *Repo) GetActiveLicense(ctx context.Context) (*model.License, *basemodel.ApiError) {
@@ -79,6 +108,45 @@ func (r *Repo) GetActiveLicense(ctx context.Context) (*model.License, *basemodel
return active, nil
}
func (r *Repo) GetActiveLicenseV3(ctx context.Context) (*model.LicenseV3, error) {
var err error
licenses := []model.LicenseDB{}
query := "SELECT id,key,data FROM licenses_v3"
err = r.db.Select(&licenses, query)
if err != nil {
return nil, basemodel.InternalError(fmt.Errorf("failed to get active licenses from db: %v", err))
}
var active *model.LicenseV3
for _, l := range licenses {
var licenseData map[string]interface{}
err := json.Unmarshal([]byte(l.Data), &licenseData)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal data into licenseData : %v", err)
}
license, err := model.NewLicenseV3WithIDAndKey(l.ID, l.Key, licenseData)
if err != nil {
return nil, fmt.Errorf("failed to get licenses v3 schema : %v", err)
}
if active == nil &&
(license.ValidFrom != 0) &&
(license.ValidUntil == -1 || license.ValidUntil > time.Now().Unix()) {
active = license
}
if active != nil &&
license.ValidFrom > active.ValidFrom &&
(license.ValidUntil == -1 || license.ValidUntil > time.Now().Unix()) {
active = license
}
}
return active, nil
}
// InsertLicense inserts a new license in db
func (r *Repo) InsertLicense(ctx context.Context, l *model.License) error {
@@ -204,3 +272,53 @@ func (r *Repo) InitFeatures(req basemodel.FeatureSet) error {
}
return nil
}
// InsertLicenseV3 inserts a new license v3 in db
func (r *Repo) InsertLicenseV3(ctx context.Context, l *model.LicenseV3) error {
query := `INSERT INTO licenses_v3 (id, key, data) VALUES ($1, $2, $3)`
// licsense is the entity of zeus so putting the entire license here without defining schema
licenseData, err := json.Marshal(l.Data)
if err != nil {
return fmt.Errorf("insert license failed: license marshal error")
}
_, err = r.db.ExecContext(ctx,
query,
l.ID,
l.Key,
string(licenseData),
)
if err != nil {
zap.L().Error("error in inserting license data: ", zap.Error(err))
return fmt.Errorf("failed to insert license in db: %v", err)
}
return nil
}
// UpdateLicenseV3 updates a new license v3 in db
func (r *Repo) UpdateLicenseV3(ctx context.Context, l *model.LicenseV3) error {
// the key and id for the license can't change so only update the data here!
query := `UPDATE licenses_v3 SET data=$1 WHERE id=$2;`
license, err := json.Marshal(l.Data)
if err != nil {
return fmt.Errorf("insert license failed: license marshal error")
}
_, err = r.db.ExecContext(ctx,
query,
license,
l.ID,
)
if err != nil {
zap.L().Error("error in updating license data: ", zap.Error(err))
return fmt.Errorf("failed to update license in db: %v", err)
}
return nil
}

View File

@@ -7,6 +7,7 @@ import (
"time"
"github.com/jmoiron/sqlx"
"github.com/pkg/errors"
"sync"
@@ -45,11 +46,12 @@ type Manager struct {
failedAttempts uint64
// keep track of active license and features
activeLicense *model.License
activeFeatures basemodel.FeatureSet
activeLicense *model.License
activeLicenseV3 *model.LicenseV3
activeFeatures basemodel.FeatureSet
}
func StartManager(dbType string, db *sqlx.DB, features ...basemodel.Feature) (*Manager, error) {
func StartManager(dbType string, db *sqlx.DB, useLicensesV3 bool, features ...basemodel.Feature) (*Manager, error) {
if LM != nil {
return LM, nil
}
@@ -65,7 +67,7 @@ func StartManager(dbType string, db *sqlx.DB, features ...basemodel.Feature) (*M
repo: &repo,
}
if err := m.start(features...); err != nil {
if err := m.start(useLicensesV3, features...); err != nil {
return m, err
}
LM = m
@@ -73,8 +75,14 @@ func StartManager(dbType string, db *sqlx.DB, features ...basemodel.Feature) (*M
}
// start loads active license in memory and initiates validator
func (lm *Manager) start(features ...basemodel.Feature) error {
err := lm.LoadActiveLicense(features...)
func (lm *Manager) start(useLicensesV3 bool, features ...basemodel.Feature) error {
var err error
if useLicensesV3 {
err = lm.LoadActiveLicenseV3(features...)
} else {
err = lm.LoadActiveLicense(features...)
}
return err
}
@@ -108,6 +116,31 @@ func (lm *Manager) SetActive(l *model.License, features ...basemodel.Feature) {
go lm.Validator(context.Background())
}
}
func (lm *Manager) SetActiveV3(l *model.LicenseV3, features ...basemodel.Feature) {
lm.mutex.Lock()
defer lm.mutex.Unlock()
if l == nil {
return
}
lm.activeLicenseV3 = l
lm.activeFeatures = append(l.Features, 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.ValidatorV3(context.Background())
}
}
func setDefaultFeatures(lm *Manager) {
@@ -137,6 +170,28 @@ func (lm *Manager) LoadActiveLicense(features ...basemodel.Feature) error {
return nil
}
func (lm *Manager) LoadActiveLicenseV3(features ...basemodel.Feature) error {
active, err := lm.repo.GetActiveLicenseV3(context.Background())
if err != nil {
return err
}
if active != nil {
lm.SetActiveV3(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)
@@ -163,6 +218,23 @@ func (lm *Manager) GetLicenses(ctx context.Context) (response []model.License, a
return
}
func (lm *Manager) GetLicensesV3(ctx context.Context) (response []model.LicenseV3, apiError *model.ApiError) {
licenses, err := lm.repo.GetLicensesV3(ctx)
if err != nil {
return nil, model.InternalError(err)
}
for _, l := range licenses {
if lm.activeLicenseV3 != nil && l.Key == lm.activeLicenseV3.Key {
l.IsCurrent = true
}
response = append(response, l)
}
return response, nil
}
// Validator validates license after an epoch of time
func (lm *Manager) Validator(ctx context.Context) {
defer close(lm.terminated)
@@ -187,6 +259,30 @@ func (lm *Manager) Validator(ctx context.Context) {
}
}
// Validator validates license after an epoch of time
func (lm *Manager) ValidatorV3(ctx context.Context) {
defer close(lm.terminated)
tick := time.NewTicker(validationFrequency)
defer tick.Stop()
lm.ValidateV3(ctx)
for {
select {
case <-lm.done:
return
default:
select {
case <-lm.done:
return
case <-tick.C:
lm.ValidateV3(ctx)
}
}
}
}
// Validate validates the current active license
func (lm *Manager) Validate(ctx context.Context) (reterr error) {
zap.L().Info("License validation started")
@@ -254,6 +350,54 @@ func (lm *Manager) Validate(ctx context.Context) (reterr error) {
return nil
}
// todo[vikrantgupta25]: check the comparison here between old and new license!
func (lm *Manager) RefreshLicense(ctx context.Context) *model.ApiError {
license, apiError := validate.ValidateLicenseV3(lm.activeLicenseV3.Key)
if apiError != nil {
zap.L().Error("failed to validate license", zap.Error(apiError.Err))
return apiError
}
err := lm.repo.UpdateLicenseV3(ctx, license)
if err != nil {
return model.BadRequest(errors.Wrap(err, "failed to update the new license"))
}
lm.SetActiveV3(license)
return nil
}
func (lm *Manager) ValidateV3(ctx context.Context) (reterr error) {
zap.L().Info("License validation started")
if lm.activeLicenseV3 == 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()
}()
err := lm.RefreshLicense(ctx)
if err != nil {
return err
}
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() {
@@ -298,6 +442,35 @@ func (lm *Manager) Activate(ctx context.Context, key string) (licenseResponse *m
return l, nil
}
func (lm *Manager) ActivateV3(ctx context.Context, licenseKey string) (licenseResponse *model.LicenseV3, 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)
}
}
}()
license, apiError := validate.ValidateLicenseV3(licenseKey)
if apiError != nil {
zap.L().Error("failed to get the license", zap.Error(apiError.Err))
return nil, apiError
}
// insert the new license to the sqlite db
err := lm.repo.InsertLicenseV3(ctx, license)
if err != nil {
zap.L().Error("failed to activate license", zap.Error(err))
return nil, model.InternalError(err)
}
// license is valid, activate it
lm.SetActiveV3(license)
return license, nil
}
// CheckFeature will be internally used by backend routines
// for feature gating
func (lm *Manager) CheckFeature(featureKey string) error {

View File

@@ -48,5 +48,16 @@ func InitDB(db *sqlx.DB) error {
return fmt.Errorf("error in creating feature_status table: %s", err.Error())
}
table_schema = `CREATE TABLE IF NOT EXISTS licenses_v3 (
id TEXT PRIMARY KEY,
key TEXT NOT NULL UNIQUE,
data TEXT
);`
_, err = db.Exec(table_schema)
if err != nil {
return fmt.Errorf("error in creating licenses_v3 table: %s", err.Error())
}
return nil
}

View File

@@ -94,6 +94,7 @@ func main() {
var cluster string
var useLogsNewSchema bool
var useLicensesV3 bool
var cacheConfigPath, fluxInterval string
var enableQueryServiceLogOTLPExport bool
var preferSpanMetrics bool
@@ -104,6 +105,7 @@ func main() {
var gatewayUrl string
flag.BoolVar(&useLogsNewSchema, "use-logs-new-schema", false, "use logs_v2 schema for logs")
flag.BoolVar(&useLicensesV3, "use-licenses-v3", false, "use licenses_v3 schema for licenses")
flag.StringVar(&promConfigPath, "config", "./config/prometheus.yml", "(prometheus config to read metrics)")
flag.StringVar(&skipTopLvlOpsPath, "skip-top-level-ops", "", "(config file to skip top level operations)")
flag.BoolVar(&disableRules, "rules.disable", false, "(disable rule evaluation)")
@@ -143,6 +145,7 @@ func main() {
Cluster: cluster,
GatewayUrl: gatewayUrl,
UseLogsNewSchema: useLogsNewSchema,
UseLicensesV3: useLicensesV3,
}
// Read the jwt secret key

View File

@@ -104,3 +104,187 @@ type SubscriptionServerResp struct {
Status string `json:"status"`
Data Licenses `json:"data"`
}
type Plan struct {
Name string `json:"name"`
}
type LicenseDB struct {
ID string `json:"id"`
Key string `json:"key"`
Data string `json:"data"`
}
type LicenseV3 struct {
ID string `json:"id"`
Key string `json:"key"`
Data map[string]interface{} `json:"data"`
Plan Plan `json:"plan"`
Features basemodel.FeatureSet `json:"features"`
Category string `json:"category"`
Status string `json:"status"`
IsCurrent bool `json:"isCurrent"`
CreatedAt time.Time `json:"createdAt"`
ValidFrom int64 `json:"validFrom"`
ValidUntil int64 `json:"validUntil"`
}
func NewLicenseV3(data map[string]interface{}) (*LicenseV3, error) {
var licenseID, licenseKey, category, status string
var validFrom, validUntil int64
var licenseData = data
var createdAt time.Time
var features basemodel.FeatureSet
plan := new(Plan)
// extract id from data
if _licenseId, ok := data["id"]; ok {
if licenseId, ok := _licenseId.(string); ok {
licenseID = licenseId
} else {
return nil, errors.New("license id is not a valid string")
}
// if id is present then delete id from licenseData field
delete(licenseData, "id")
} else {
return nil, errors.New("license id is missing")
}
// extract key from data
if _licenseKey, ok := data["key"]; ok {
if licensekey, ok := _licenseKey.(string); ok {
licenseKey = licensekey
} else {
return nil, errors.New("license key is not a valid string")
}
// if key is present then delete id from licenseData field
delete(licenseData, "key")
} else {
return nil, errors.New("license key is missing")
}
// extract category from data
if _licenseCategory, ok := data["category"]; ok {
if licenseCategory, ok := _licenseCategory.(string); ok {
category = licenseCategory
} else {
return nil, errors.New("license category is not a valid string")
}
} else {
return nil, errors.New("license category is missing")
}
// extract status from data
if _licenseStatus, ok := data["status"]; ok {
if licenseStatus, ok := _licenseStatus.(string); ok {
status = licenseStatus
} else {
return nil, errors.New("license status is not a valid string")
}
} else {
return nil, errors.New("license status is missing")
}
if _plan, ok := licenseData["plan"]; ok {
if parsedPlan, ok := _plan.(map[string]interface{}); ok {
if planName, ok := parsedPlan["name"]; ok {
if pName, ok := planName.(string); ok {
plan.Name = pName
} else {
return nil, errors.New("plan name is not a valid string")
}
} else {
return nil, errors.New("plan name is missing in plan struct")
}
} else {
return nil, errors.New("plan is not a valid map[string]interface{} struct")
}
} else {
return nil, errors.New("license plan is missing")
}
// if license status is inactive then default it to basic
if status == LicenseStatusInactive {
plan.Name = PlanNameBasic
}
featuresFromZeus := basemodel.FeatureSet{}
if features, ok := licenseData["features"]; ok {
if val, ok := features.(basemodel.FeatureSet); ok {
featuresFromZeus = val
}
}
if len(featuresFromZeus) > 0 {
features = append(features, featuresFromZeus...)
}
switch plan.Name {
case PlanNameTeams:
features = append(features, ProPlan...)
case PlanNameEnterprise:
features = append(features, EnterprisePlan...)
case PlanNameBasic:
features = append(features, BasicPlan...)
default:
features = append(features, BasicPlan...)
}
if _value, ok := licenseData["valid_from"]; ok {
val, ok := _value.(int64)
if ok {
validFrom = val
} else {
floatVal, ok := _value.(float64)
if !ok || floatVal != float64(int64(floatVal)) {
// if the validFrom is float value default it to 0
validFrom = 0
} else {
validFrom = int64(floatVal)
}
}
}
if _value, ok := licenseData["valid_until"]; ok {
val, ok := _value.(int64)
if ok {
validUntil = val
} else {
floatVal, ok := _value.(float64)
if !ok || floatVal != float64(int64(floatVal)) {
// if the validFrom is float value default it to -1
validUntil = -1
}
validUntil = int64(floatVal)
}
}
// extract createdAt from data
if _createdAt, ok := data["created_at"]; ok {
if createdat, ok := _createdAt.(string); ok {
parsedCreatedAt, err := time.Parse(time.RFC3339, createdat)
if err == nil {
createdAt = parsedCreatedAt
}
}
}
return &LicenseV3{
ID: licenseID,
Key: licenseKey,
Data: licenseData,
Plan: *plan,
Features: features,
ValidFrom: validFrom,
ValidUntil: validUntil,
Category: category,
CreatedAt: createdAt,
Status: status,
}, nil
}
func NewLicenseV3WithIDAndKey(id string, key string, data map[string]interface{}) (*LicenseV3, error) {
licenseDataWithIdAndKey := data
licenseDataWithIdAndKey["id"] = id
licenseDataWithIdAndKey["key"] = key
return NewLicenseV3(licenseDataWithIdAndKey)
}

View File

@@ -0,0 +1,119 @@
package model
import (
"encoding/json"
"testing"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.signoz.io/signoz/pkg/query-service/model"
)
func TestNewLicenseV3(t *testing.T) {
testCases := []struct {
name string
data []byte
pass bool
expected *LicenseV3
error error
}{
{
name: "give plan error when plan not present",
data: []byte(`{"id":"does-not-matter","key":"does-not-matter-key","category":"FREE","status":"ACTIVE","plan":"{}"}`),
pass: false,
error: errors.New("plan is not a valid map[string]interface{} struct"),
},
{
name: "parse the plan properly!",
data: []byte(`{"id":"does-not-matter","key":"does-not-matter-key","category":"FREE","status":"ACTIVE","plan":{"name":"TEAMS"}}`),
pass: true,
expected: &LicenseV3{
ID: "does-not-matter",
Key: "does-not-matter-key",
Data: map[string]interface{}{
"plan": map[string]interface{}{
"name": "TEAMS",
},
"category": "FREE",
"status": "ACTIVE",
},
Plan: Plan{
Name: PlanNameTeams,
},
ValidFrom: 0,
ValidUntil: 0,
Status: "ACTIVE",
Category: "FREE",
IsCurrent: false,
Features: model.FeatureSet{},
},
},
{
name: "parse the validFrom and validUntil",
data: []byte(`{"id":"does-not-matter","key":"does-not-matter-key","category":"FREE","status":"ACTIVE","plan":{"name":"TEAMS"},"valid_from":1234,"valid_until":5678}`),
pass: true,
expected: &LicenseV3{
ID: "does-not-matter",
Key: "does-not-matter-key",
Data: map[string]interface{}{
"plan": map[string]interface{}{
"name": "TEAMS",
},
"valid_from": float64(1234),
"valid_until": float64(5678),
"category": "FREE",
"status": "ACTIVE",
},
Plan: Plan{
Name: PlanNameTeams,
},
ValidFrom: 1234,
ValidUntil: 5678,
Status: "ACTIVE",
Category: "FREE",
IsCurrent: false,
Features: model.FeatureSet{},
},
},
{
name: "Error for missing license id",
data: []byte(`{}`),
pass: false,
error: errors.New("license id is missing"),
},
{
name: "Error for missing license key",
data: []byte(`{"id":"does-not-matter"}`),
pass: false,
error: errors.New("license key is missing"),
},
{
name: "Error for invalid string license key",
data: []byte(`{"id":"does-not-matter","key":10}`),
pass: false,
error: errors.New("license key is not a valid string"),
},
}
for _, tc := range testCases {
var licensePayload map[string]interface{}
err := json.Unmarshal(tc.data, &licensePayload)
require.NoError(t, err)
license, err := NewLicenseV3(licensePayload)
if license != nil {
license.Features = make(model.FeatureSet, 0)
}
if tc.pass {
require.NoError(t, err)
require.NotNil(t, license)
assert.Equal(t, tc.expected, license)
} else {
require.Error(t, err)
assert.EqualError(t, err, tc.error.Error())
require.Nil(t, license)
}
}
}

View File

@@ -9,6 +9,26 @@ const SSO = "SSO"
const Basic = "BASIC_PLAN"
const Pro = "PRO_PLAN"
const Enterprise = "ENTERPRISE_PLAN"
var (
PlanNameEnterprise = "ENTERPRISE"
PlanNameTeams = "TEAMS"
PlanNameBasic = "BASIC"
)
var (
LicenseCategoryFree = "FREE"
LicenseCategoryPaid = "PAID"
)
var (
LicenseStatusIssued = "ISSUED"
LicenseStatusActive = "ACTIVE"
LicenseStatusSuspended = "SUSPENDED"
LicenseStatusHibernate = "HIBERNATE"
LicenseStatusInactive = "INACTIVE"
)
const DisableUpsell = "DISABLE_UPSELL"
const Onboarding = "ONBOARDING"
const ChatSupport = "CHAT_SUPPORT"

View File

@@ -111,6 +111,7 @@ type APIHandler struct {
Upgrader *websocket.Upgrader
UseLogsNewSchema bool
UseLicensesV3 bool
hostsRepo *inframetrics.HostsRepo
processesRepo *inframetrics.ProcessesRepo
@@ -156,6 +157,9 @@ type APIHandlerOpts struct {
// Use Logs New schema
UseLogsNewSchema bool
// Use Licenses V3 structure
UseLicensesV3 bool
}
// NewAPIHandler returns an APIHandler
@@ -211,6 +215,7 @@ func NewAPIHandler(opts APIHandlerOpts) (*APIHandler, error) {
querier: querier,
querierV2: querierv2,
UseLogsNewSchema: opts.UseLogsNewSchema,
UseLicensesV3: opts.UseLicensesV3,
hostsRepo: hostsRepo,
processesRepo: processesRepo,
podsRepo: podsRepo,