From 76dec5a2b7efe241c476e06c5e3d8116f6aac778 Mon Sep 17 00:00:00 2001 From: r-vasquez Date: Tue, 29 Oct 2024 09:33:48 -0700 Subject: [PATCH 1/3] rpk: change expiry check for free_trial If it's a free trial license the expiry warning check threshold will be set to 15days instead of 30 for the rest of license types. Fixes DEVEX-36 --- src/go/rpk/pkg/cli/cluster/license/info.go | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/go/rpk/pkg/cli/cluster/license/info.go b/src/go/rpk/pkg/cli/cluster/license/info.go index 6aa58f7693ff2..be69b4fa489ba 100644 --- a/src/go/rpk/pkg/cli/cluster/license/info.go +++ b/src/go/rpk/pkg/cli/cluster/license/info.go @@ -3,6 +3,7 @@ package license import ( "fmt" "os" + "strings" "time" "github.com/redpanda-data/common-go/rpadmin" @@ -133,17 +134,21 @@ func printTextLicenseInfo(resp infoResponse) { if *resp.Expired { tw.Print("License expired:", *resp.Expired) } - checkLicenseExpiry(resp.ExpiresUnix) + checkLicenseExpiry(resp.ExpiresUnix, resp.Type) } out.Section("LICENSE INFORMATION") tw.Flush() } -func checkLicenseExpiry(expiresUnix int64) { +func checkLicenseExpiry(expiresUnix int64, licenseType string) { ut := time.Unix(expiresUnix, 0) daysLeft := int(time.Until(ut).Hours() / 24) - if daysLeft < 30 && !ut.Before(time.Now()) { + dayThreshold := 30 + if strings.EqualFold(licenseType, "free_trial") { + dayThreshold = 15 + } + if daysLeft < dayThreshold && !ut.Before(time.Now()) { fmt.Fprintf(os.Stderr, "WARNING: your license will expire soon.\n\n") } } From 84a3ff0fc25b61023942c1fd91ff5db5eb368e33 Mon Sep 17 00:00:00 2001 From: r-vasquez Date: Tue, 29 Oct 2024 13:14:52 -0700 Subject: [PATCH 2/3] 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 a29a2c653ecee..10273efd190c2 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 1464596229507..9c8f417fae41f 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)) + } } } From 663291e4c72387ca860d55c90b2fe8e563a8333f Mon Sep 17 00:00:00 2001 From: r-vasquez Date: Tue, 29 Oct 2024 16:03:00 -0700 Subject: [PATCH 3/3] rpk: wrap license nag message on term size Better readability since it's a long line. --- MODULE.bazel | 1 + src/go/rpk/go.mod | 1 + src/go/rpk/go.sum | 1 + src/go/rpk/pkg/adminapi/BUILD | 2 ++ src/go/rpk/pkg/adminapi/admin.go | 7 +++++++ 5 files changed, 12 insertions(+) diff --git a/MODULE.bazel b/MODULE.bazel index 2182c2708b5e6..efa1f7724b474 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -190,6 +190,7 @@ use_repo( "com_github_hashicorp_go_multierror", "com_github_kballard_go_shellquote", "com_github_kr_pretty", + "com_github_kr_text", "com_github_lestrrat_go_jwx", "com_github_linkedin_goavro_v2", "com_github_lorenzosaino_go_sysctl", diff --git a/src/go/rpk/go.mod b/src/go/rpk/go.mod index 2919b0e1836da..23a7c9d510d61 100644 --- a/src/go/rpk/go.mod +++ b/src/go/rpk/go.mod @@ -26,6 +26,7 @@ require ( github.com/hamba/avro/v2 v2.25.2 github.com/hashicorp/go-multierror v1.1.1 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 + github.com/kr/text v0.2.0 github.com/lestrrat-go/jwx v1.2.30 github.com/linkedin/goavro/v2 v2.13.0 github.com/lorenzosaino/go-sysctl v0.3.1 diff --git a/src/go/rpk/go.sum b/src/go/rpk/go.sum index 0eb79316a5340..a2c0e4d575f31 100644 --- a/src/go/rpk/go.sum +++ b/src/go/rpk/go.sum @@ -43,6 +43,7 @@ github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3 github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= diff --git a/src/go/rpk/pkg/adminapi/BUILD b/src/go/rpk/pkg/adminapi/BUILD index c867e56f0d40b..1e9be30a56996 100644 --- a/src/go/rpk/pkg/adminapi/BUILD +++ b/src/go/rpk/pkg/adminapi/BUILD @@ -9,6 +9,8 @@ go_library( "//src/go/rpk/pkg/config", "//src/go/rpk/pkg/oauth", "//src/go/rpk/pkg/oauth/providers/auth0", + "@com_github_kr_text//:text", + "@com_github_moby_term//:term", "@com_github_redpanda_data_common_go_rpadmin//:rpadmin", "@com_github_spf13_afero//:afero", "@org_uber_go_zap//:zap", diff --git a/src/go/rpk/pkg/adminapi/admin.go b/src/go/rpk/pkg/adminapi/admin.go index 10273efd190c2..969e690f24802 100644 --- a/src/go/rpk/pkg/adminapi/admin.go +++ b/src/go/rpk/pkg/adminapi/admin.go @@ -19,6 +19,9 @@ import ( "strings" "time" + "github.com/kr/text" + mTerm "github.com/moby/term" + "go.uber.org/zap" "github.com/redpanda-data/common-go/rpadmin" @@ -178,6 +181,10 @@ func licenseFeatureChecks(ctx context.Context, fs afero.Fs, cl *rpadmin.AdminAPI } } } + if ws, err := mTerm.GetWinsize(0); err == nil { + // text.Wrap removes the newline from the text. We add it back. + msg = "\n" + text.Wrap(msg, int(ws.Width)) + "\n" + } return msg }