Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add license expiry checks #6917

Merged
merged 3 commits into from
Dec 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion cmd/nginx-ingress/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@
var licenseReporter *license_reporting.LicenseReporter

if *nginxPlus {
licenseReporter = license_reporting.NewLicenseReporter(kubeClient)
licenseReporter = license_reporting.NewLicenseReporter(kubeClient, eventRecorder, pod)

Check warning on line 123 in cmd/nginx-ingress/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/nginx-ingress/main.go#L123

Added line #L123 was not covered by tests
}

nginxManager, useFakeNginxManager := createNginxManager(ctx, managerCollector, licenseReporter)
Expand Down Expand Up @@ -214,6 +214,9 @@
process := startChildProcesses(nginxManager, appProtectV5)

plusClient := createPlusClient(ctx, *nginxPlus, useFakeNginxManager, nginxManager)
if *nginxPlus {
licenseReporter.Config.PlusClient = plusClient
}

Check warning on line 219 in cmd/nginx-ingress/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/nginx-ingress/main.go#L217-L219

Added lines #L217 - L219 were not covered by tests

plusCollector, syslogListener, latencyCollector := createPlusAndLatencyCollectors(ctx, registry, constLabels, kubeClient, plusClient, staticCfgParams.NginxServiceMesh)
cnf := configs.NewConfigurator(configs.ConfiguratorParams{
Expand Down
67 changes: 59 additions & 8 deletions internal/license_reporting/license_reporting.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,25 @@
import (
"context"
"encoding/json"
"fmt"
"log/slog"
"os"
"path/filepath"
"time"

nl "github.com/nginxinc/kubernetes-ingress/internal/logger"
"github.com/nginxinc/nginx-plus-go-client/v2/client"

clusterInfo "github.com/nginxinc/kubernetes-ingress/internal/common_cluster_info"
api_v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/record"
)

const expiryThreshold = 30 * (time.Hour * 24)

var (
reportingDir = "/etc/nginx/reporting"
reportingFile = "tracking.info"
Expand Down Expand Up @@ -51,46 +57,91 @@

// LicenseReporter can start the license reporting process
type LicenseReporter struct {
config LicenseReporterConfig
Config LicenseReporterConfig
}

// LicenseReporterConfig contains the information needed for license reporting
type LicenseReporterConfig struct {
Period time.Duration
K8sClientReader kubernetes.Interface
PodNSName types.NamespacedName
EventLog record.EventRecorder
Pod *api_v1.Pod
PlusClient *client.NginxClient
}

// NewLicenseReporter creates a new LicenseReporter
func NewLicenseReporter(client kubernetes.Interface) *LicenseReporter {
func NewLicenseReporter(client kubernetes.Interface, eventLog record.EventRecorder, pod *api_v1.Pod) *LicenseReporter {
return &LicenseReporter{
config: LicenseReporterConfig{
Period: 24 * time.Hour,
Config: LicenseReporterConfig{
EventLog: eventLog,
Period: time.Hour,
K8sClientReader: client,
PodNSName: types.NamespacedName{Namespace: os.Getenv("POD_NAMESPACE"), Name: os.Getenv("POD_NAME")},
Pod: pod,
},
}
}

// Start begins the license report writer process for NIC
func (lr *LicenseReporter) Start(ctx context.Context) {
wait.JitterUntilWithContext(ctx, lr.collectAndWrite, lr.config.Period, 0.1, true)
wait.JitterUntilWithContext(ctx, lr.collectAndWrite, lr.Config.Period, 0.1, true)

Check warning on line 88 in internal/license_reporting/license_reporting.go

View check run for this annotation

Codecov / codecov/patch

internal/license_reporting/license_reporting.go#L88

Added line #L88 was not covered by tests
}

func (lr *LicenseReporter) collectAndWrite(ctx context.Context) {
l := nl.LoggerFromContext(ctx)
clusterID, err := clusterInfo.GetClusterID(ctx, lr.config.K8sClientReader)
clusterID, err := clusterInfo.GetClusterID(ctx, lr.Config.K8sClientReader)

Check warning on line 93 in internal/license_reporting/license_reporting.go

View check run for this annotation

Codecov / codecov/patch

internal/license_reporting/license_reporting.go#L93

Added line #L93 was not covered by tests
if err != nil {
nl.Errorf(l, "Error collecting ClusterIDS: %v", err)
}
nodeCount, err := clusterInfo.GetNodeCount(ctx, lr.config.K8sClientReader)
nodeCount, err := clusterInfo.GetNodeCount(ctx, lr.Config.K8sClientReader)

Check warning on line 97 in internal/license_reporting/license_reporting.go

View check run for this annotation

Codecov / codecov/patch

internal/license_reporting/license_reporting.go#L97

Added line #L97 was not covered by tests
if err != nil {
nl.Errorf(l, "Error collecting ClusterNodeCount: %v", err)
}
installationID, err := clusterInfo.GetInstallationID(ctx, lr.config.K8sClientReader, lr.config.PodNSName)
installationID, err := clusterInfo.GetInstallationID(ctx, lr.Config.K8sClientReader, lr.Config.PodNSName)

Check warning on line 101 in internal/license_reporting/license_reporting.go

View check run for this annotation

Codecov / codecov/patch

internal/license_reporting/license_reporting.go#L101

Added line #L101 was not covered by tests
if err != nil {
nl.Errorf(l, "Error collecting InstallationID: %v", err)
}
info := newLicenseInfo(clusterID, installationID, nodeCount)
writeLicenseInfo(l, info)
if lr.Config.PlusClient != nil {
lr.checkLicenseExpiry(ctx)
}

Check warning on line 109 in internal/license_reporting/license_reporting.go

View check run for this annotation

Codecov / codecov/patch

internal/license_reporting/license_reporting.go#L107-L109

Added lines #L107 - L109 were not covered by tests
}

func (lr *LicenseReporter) checkLicenseExpiry(ctx context.Context) {
l := nl.LoggerFromContext(ctx)
licenseData, err := lr.Config.PlusClient.GetNginxLicense(context.Background())
if err != nil {
nl.Errorf(l, "could not get license data, %v", err)
return
}
var licenseEventText string
if expiring, days := licenseExpiring(licenseData); expiring {
licenseEventText = fmt.Sprintf("License expiring in %d day(s)", days)
nl.Warn(l, licenseEventText)
lr.Config.EventLog.Event(lr.Config.Pod, api_v1.EventTypeWarning, "LicenseExpiry", licenseEventText)
}
var usageGraceEventText string
if ending, days := usageGraceEnding(licenseData); ending {
usageGraceEventText = fmt.Sprintf("Usage reporting grace period ending in %d day(s)", days)
nl.Warn(l, usageGraceEventText)
lr.Config.EventLog.Event(lr.Config.Pod, api_v1.EventTypeWarning, "UsageGraceEnding", usageGraceEventText)
}

Check warning on line 130 in internal/license_reporting/license_reporting.go

View check run for this annotation

Codecov / codecov/patch

internal/license_reporting/license_reporting.go#L112-L130

Added lines #L112 - L130 were not covered by tests
}

func licenseExpiring(licenseData *client.NginxLicense) (bool, int64) {
expiry := time.Unix(int64(licenseData.ActiveTill), 0) //nolint:gosec
now := time.Now()
timeUntilLicenseExpiry := expiry.Sub(now)
daysUntilLicenseExpiry := int64(timeUntilLicenseExpiry.Hours() / 24)
expiryDays := int64(expiryThreshold.Hours() / 24)
return daysUntilLicenseExpiry < expiryDays, daysUntilLicenseExpiry
}

func usageGraceEnding(licenseData *client.NginxLicense) (bool, int64) {
grace := time.Second * time.Duration(licenseData.Reporting.Grace) //nolint:gosec
daysUntilUsageGraceEnds := int64(grace.Hours() / 24)
expiryDays := int64(expiryThreshold.Hours() / 24)
return daysUntilUsageGraceEnds < expiryDays, daysUntilUsageGraceEnds
}
104 changes: 103 additions & 1 deletion internal/license_reporting/license_reporting_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,15 @@ import (
"os"
"path/filepath"
"testing"
"time"

nic_glog "github.com/nginxinc/kubernetes-ingress/internal/logger/glog"
"github.com/nginxinc/kubernetes-ingress/internal/logger/levels"
"github.com/nginxinc/nginx-plus-go-client/v2/client"

v1 "k8s.io/api/core/v1"
"k8s.io/client-go/kubernetes/fake"
"k8s.io/client-go/tools/record"
)

func TestNewLicenseInfo(t *testing.T) {
Expand Down Expand Up @@ -64,8 +68,106 @@ func TestWriteLicenseInfo(t *testing.T) {
}

func TestNewLicenseReporter(t *testing.T) {
reporter := NewLicenseReporter(fake.NewSimpleClientset())
reporter := NewLicenseReporter(fake.NewSimpleClientset(), record.NewFakeRecorder(2048), &v1.Pod{})
if reporter == nil {
t.Fatal("NewLicenseReporter() returned nil")
}
}

func TestLicenseExpiring(t *testing.T) {
t.Parallel()

testCases := []struct {
licenseData client.NginxLicense
belowExpiringThreshold bool
days int64
name string
}{
{
licenseData: client.NginxLicense{
ActiveTill: uint64(time.Now().Add(time.Hour).Unix()), //nolint:gosec
},
belowExpiringThreshold: true,
days: 0,
name: "License expires in 1 hour",
},
{
licenseData: client.NginxLicense{
ActiveTill: uint64(time.Now().Add(-time.Hour).Unix()), //nolint:gosec
},
belowExpiringThreshold: true,
days: 0,
name: "License expired 1 hour ago",
},
{
licenseData: client.NginxLicense{
ActiveTill: uint64(time.Now().Add(time.Hour * 24 * 31).Unix()), //nolint:gosec
},
belowExpiringThreshold: false,
days: 30, // Rounds down
name: "License expires in 31 days",
},
}

for _, tc := range testCases {
actualExpiring, actualDays := licenseExpiring(&tc.licenseData)
if actualExpiring != tc.belowExpiringThreshold {
t.Fatalf("%s: Expected different value for expiring %t", tc.name, tc.belowExpiringThreshold)
}
if actualDays != tc.days {
t.Fatalf("%s: Expected different value for days %d != %d", tc.name, actualDays, tc.days)
}
}
}

func TestUsageGraceEnding(t *testing.T) {
t.Parallel()

testCases := []struct {
licenseData client.NginxLicense
belowExpiringThreshold bool
days int64
name string
}{
{
licenseData: client.NginxLicense{
Reporting: client.LicenseReporting{
Grace: 3600, // seconds
},
},
belowExpiringThreshold: true,
days: 0,
name: "Grace period ends in an hour",
},
{
licenseData: client.NginxLicense{
Reporting: client.LicenseReporting{
Grace: 60 * 60 * 24 * 31, // 31 days
},
},
belowExpiringThreshold: false,
days: 31,
name: "Grace period ends 31 days",
},
{
licenseData: client.NginxLicense{
Reporting: client.LicenseReporting{
Grace: 0,
},
},
belowExpiringThreshold: true,
days: 0,
name: "Grace period ended",
},
}

for _, tc := range testCases {
actualEnding, actualDays := usageGraceEnding(&tc.licenseData)
if actualEnding != tc.belowExpiringThreshold {
t.Fatalf("%s: Expected different value for expiring %t", tc.name, tc.belowExpiringThreshold)
}
if actualDays != tc.days {
t.Fatalf("%s: Expected different value for days %d != %d", tc.name, actualDays, tc.days)
}
}
}
13 changes: 3 additions & 10 deletions internal/nginx/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -303,16 +303,9 @@
// Start starts NGINX.
func (lm *LocalManager) Start(done chan error) {
if lm.nginxPlus {
isR33OrGreater, versionErr := lm.Version().PlusGreaterThanOrEqualTo("nginx-plus-r33")
if versionErr != nil {
nl.Errorf(lm.logger, "Error determining whether nginx version is >= r33: %v", versionErr)
}
if isR33OrGreater {
ctx, cancel := context.WithCancel(context.Background())
nl.ContextWithLogger(ctx, lm.logger)
go lm.licenseReporter.Start(ctx)
lm.licenseReporterCancel = cancel
}
ctx, cancel := context.WithCancel(context.Background())
go lm.licenseReporter.Start(nl.ContextWithLogger(ctx, lm.logger))
lm.licenseReporterCancel = cancel

Check warning on line 308 in internal/nginx/manager.go

View check run for this annotation

Codecov / codecov/patch

internal/nginx/manager.go#L306-L308

Added lines #L306 - L308 were not covered by tests
}

nl.Debug(lm.logger, "Starting nginx")
Expand Down
Loading