From 84a3ff0fc25b61023942c1fd91ff5db5eb368e33 Mon Sep 17 00:00:00 2001 From: r-vasquez Date: Tue, 29 Oct 2024 13:14:52 -0700 Subject: [PATCH] rpk: show nag for free_trial about to expire Now the license nags will be shown for customers with free_trial licenses that has less than 15 days left. --- src/go/rpk/pkg/adminapi/admin.go | 38 +++++++++++++++++++----- src/go/rpk/pkg/adminapi/admin_test.go | 42 +++++++++++++++++++++++---- 2 files changed, 66 insertions(+), 14 deletions(-) diff --git a/src/go/rpk/pkg/adminapi/admin.go b/src/go/rpk/pkg/adminapi/admin.go index a29a2c653ece..10273efd190c 100644 --- a/src/go/rpk/pkg/adminapi/admin.go +++ b/src/go/rpk/pkg/adminapi/admin.go @@ -16,6 +16,7 @@ import ( "fmt" "os" "strconv" + "strings" "time" "go.uber.org/zap" @@ -137,22 +138,31 @@ func licenseFeatureChecks(ctx context.Context, fs afero.Fs, cl *rpadmin.AdminAPI // violation. (we only save successful responses). // 2. LicenseStatus was last checked more than 1 hour ago. if p.LicenseCheck == nil || p.LicenseCheck != nil && time.Unix(p.LicenseCheck.LastUpdate, 0).Add(1*time.Hour).Before(time.Now()) { - resp, err := cl.GetEnterpriseFeatures(ctx) + featResp, err := cl.GetEnterpriseFeatures(ctx) if err != nil { zap.L().Sugar().Warnf("unable to check licensed enterprise features in the cluster: %v", err) return "" } + info, err := cl.GetLicenseInfo(ctx) + if err != nil { + zap.L().Sugar().Warnf("unable to check license information: %v", err) + return "" + } // We don't write a profile if the config doesn't exist. y, exists := p.ActualConfig() var licenseCheck *config.LicenseStatusCache - if resp.Violation { - var features []string - for _, f := range resp.Features { - if f.Enabled { - features = append(features, f.Name) - } + var enabledFeatures []string + for _, f := range featResp.Features { + if f.Enabled { + enabledFeatures = append(enabledFeatures, f.Name) } - msg = fmt.Sprintf("\nWARNING: The following Enterprise features are being used in your Redpanda cluster: %v. These features require a license. To get a license, contact us at https://www.redpanda.com/contact. For more information, see https://docs.redpanda.com/current/get-started/licenses/#redpanda-enterprise-edition\n", features) + } + isTrialCheck := isTrialAboutToExpire(info, enabledFeatures) + + if featResp.Violation { + msg = fmt.Sprintf("\nWARNING: The following Enterprise features are being used in your Redpanda cluster: %v. These features require a license. To get a license, contact us at https://www.redpanda.com/contact. For more information, see https://docs.redpanda.com/current/get-started/licenses/#redpanda-enterprise-edition\n", enabledFeatures) + } else if isTrialCheck { + msg = fmt.Sprintf("\nWARNING: your TRIAL license is about to expire. The following Enterprise features are being used in your Redpanda cluster: %v. These features require a license. To get a license, contact us at https://www.redpanda.com/contact. For more information, see https://docs.redpanda.com/current/get-started/licenses/#redpanda-enterprise-edition\n", enabledFeatures) } else { licenseCheck = &config.LicenseStatusCache{ LastUpdate: time.Now().Unix(), @@ -170,3 +180,15 @@ func licenseFeatureChecks(ctx context.Context, fs afero.Fs, cl *rpadmin.AdminAPI } return msg } + +// isTrialAboutToExpire returns true if we have a loaded free_trial license that +// expires in less than 15 days, and we have enterprise features enabled. +func isTrialAboutToExpire(info rpadmin.License, enabledFeatures []string) bool { + if len(enabledFeatures) > 0 && info.Loaded && strings.EqualFold(info.Properties.Type, "free_trial") { + ut := time.Unix(info.Properties.Expires, 0) + daysLeft := int(time.Until(ut).Hours() / 24) + + return daysLeft < 15 && !ut.Before(time.Now()) + } + return false +} diff --git a/src/go/rpk/pkg/adminapi/admin_test.go b/src/go/rpk/pkg/adminapi/admin_test.go index 146459622950..9c8f417fae41 100644 --- a/src/go/rpk/pkg/adminapi/admin_test.go +++ b/src/go/rpk/pkg/adminapi/admin_test.go @@ -2,6 +2,7 @@ package adminapi import ( "context" + "fmt" "net/http" "net/http/httptest" "testing" @@ -16,7 +17,7 @@ func Test_licenseFeatureChecks(t *testing.T) { tests := []struct { name string prof *config.RpkProfile - responseCase string // See the mapLicenseResponses below. + responseCase string // See the mapLicenseFeatureResponses below. expContain string withErr bool checkCache func(t *testing.T, before int64, after int64) @@ -27,6 +28,18 @@ func Test_licenseFeatureChecks(t *testing.T) { responseCase: "ok", expContain: "", }, + { + name: "free_trial about to expire, no features", + prof: &config.RpkProfile{}, + responseCase: "ok-free", + expContain: "", + }, + { + name: "free_trial about to expire, with features", + prof: &config.RpkProfile{}, + responseCase: "ok-features", + expContain: "WARNING: your TRIAL license is about to expire", + }, { name: "license ok, cache valid", prof: &config.RpkProfile{LicenseCheck: &config.LicenseStatusCache{LastUpdate: time.Now().Add(20 * time.Minute).Unix()}}, @@ -144,16 +157,33 @@ type response struct { body string } -var mapLicenseResponses = map[string]response{ - "ok": {http.StatusOK, `{"license_status": "valid", "violation": false}`}, +var mapLicenseFeatureResponses = map[string]response{ + "ok": {http.StatusOK, `{"license_status": "valid", "violation": false, "features": [{"name": "fips", "enabled": true},{"name": "partition_auto_balancing_continuous", "enabled": false}]}`}, "inViolation": {http.StatusOK, `{"license_status": "expired", "violation": true, "features": [{"name": "partition_auto_balancing_continuous", "enabled": true}]}`}, "failedRequest": {http.StatusBadRequest, ""}, + "ok-free": {http.StatusOK, `{"license_status": "valid", "violation": false}`}, + "ok-features": {http.StatusOK, `{"license_status": "valid", "violation": false, "features": [{"name": "partition_auto_balancing_continuous", "enabled": true}]}`}, +} + +var mapLicenseInfoResponses = map[string]response{ + "ok": {http.StatusOK, fmt.Sprintf(`{"loaded": true, "license": {"type": "enterprise", "expires": %d}}`, time.Now().Add(60*24*time.Hour).Unix())}, + "inViolation": {http.StatusOK, fmt.Sprintf(`{"loaded": true, "license": {"type": "enterprise", "expires": %d}}`, time.Now().Add(60*24*time.Hour).Unix())}, + "failedRequest": {http.StatusBadRequest, ""}, + "ok-free": {http.StatusOK, fmt.Sprintf(`{"loaded": true, "license": {"type": "free_trial", "expires": %d}}`, time.Now().Add(24*time.Hour).Unix())}, // expires in 1 day. + "ok-features": {http.StatusOK, fmt.Sprintf(`{"loaded": true, "license": {"type": "free_trial", "expires": %d}}`, time.Now().Add(24*time.Hour).Unix())}, } func licenseHandler(respCase string) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - resp := mapLicenseResponses[respCase] - w.WriteHeader(resp.status) - w.Write([]byte(resp.body)) + fmt.Println(r.URL.Path) + if r.URL.Path == "/v1/features/enterprise" { + resp := mapLicenseFeatureResponses[respCase] + w.WriteHeader(resp.status) + w.Write([]byte(resp.body)) + } else if r.URL.Path == "/v1/features/license" { + resp := mapLicenseInfoResponses[respCase] + w.WriteHeader(resp.status) + w.Write([]byte(resp.body)) + } } }