From c23ff812f2a4be221c5a549b4c1f1eef5ba579e6 Mon Sep 17 00:00:00 2001 From: Jiawei Huang Date: Thu, 26 Feb 2026 16:26:14 -0800 Subject: [PATCH 1/2] Handle enterprise license expiry with grace period When an enterprise license expires (after the grace period elapses), disable logging (fluentd DaemonSet) and metrics scraping (ServiceMonitors) while keeping the dataplane running. During the grace period, all components continue running normally with only a log warning. This aligns with the enterprise changes that relax license validation so expired licenses are accepted, with individual components handling degraded behavior. Changes: - Add license utility functions in pkg/controller/utils/license.go: - ParseGracePeriod: parses the "90d" format grace period string from the typed LicenseKey status.gracePeriod field - GetLicenseStatus: point-in-time check using a single time.Now() call returning Valid, InGracePeriod, or Expired status - Move existing license helpers (FetchLicenseKey, IsFeatureActive, WaitToAddLicenseKeyWatch) out of utils.go into license.go - Monitor controller: - Add license watch and fetch with grace period support - When expired: set degraded status, move ServiceMonitors to toDelete - When in grace period: log warning, keep everything running normally - LogCollector controller: - Add grace period support to license expiry check - When expired: set degraded status, move fluentd DaemonSet to toDelete - When in grace period: log warning, keep everything running normally - Render layer: - Add LicenseExpired flag to monitor Config and FluentdConfiguration - Conditionally move ServiceMonitors/DaemonSet to toDelete when expired - Bump tigera/api to v0.0.0-20260227222130-df0b9e289a34 which includes the GracePeriod field in LicenseKeyStatus --- Makefile | 4 +- api/go.mod | 14 +-- api/go.sum | 60 +++++------ go.mod | 20 ++-- go.sum | 46 ++++---- .../logcollector/logcollector_controller.go | 23 +++- .../logcollector_controller_test.go | 79 ++++++++++++++ pkg/controller/monitor/monitor_controller.go | 46 +++++++- .../monitor/monitor_controller_test.go | 78 ++++++++++++++ pkg/controller/status/status.go | 6 +- pkg/controller/status/status_test.go | 2 +- pkg/controller/utils/license.go | 102 ++++++++++++++++++ pkg/controller/utils/license_test.go | 81 ++++++++++++++ pkg/controller/utils/utils.go | 24 ----- pkg/render/fluentd.go | 9 +- pkg/render/fluentd_test.go | 40 +++++++ pkg/render/monitor/monitor.go | 14 ++- pkg/render/monitor/monitor_test.go | 58 ++++++++++ 18 files changed, 598 insertions(+), 108 deletions(-) create mode 100644 pkg/controller/utils/license.go create mode 100644 pkg/controller/utils/license_test.go diff --git a/Makefile b/Makefile index 8ceac5adb3..0c5c3742c0 100644 --- a/Makefile +++ b/Makefile @@ -101,8 +101,8 @@ endif REPO?=tigera/operator PACKAGE_NAME?=github.com/tigera/operator LOCAL_USER_ID?=$(shell id -u $$USER) -GO_BUILD_VER?=1.25.7-llvm18.1.8-k8s1.34.3-1 -CALICO_BASE_VER ?= ubi9-1770969585 +GO_BUILD_VER?=1.25.7-llvm18.1.8-k8s1.34.4 +CALICO_BASE_VER ?= ubi9-1771532994 CALICO_BUILD?=calico/go-build:$(GO_BUILD_VER)-$(BUILDARCH) CALICO_BASE ?= calico/base:$(CALICO_BASE_VER) SRC_FILES=$(shell find ./pkg -name '*.go') diff --git a/api/go.mod b/api/go.mod index 7a14c25a2a..16df0fe948 100644 --- a/api/go.mod +++ b/api/go.mod @@ -1,26 +1,26 @@ module github.com/tigera/operator/api -go 1.25.5 +go 1.25.7 require ( github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.80.1 - github.com/tigera/api v0.0.0-20251017180206-9d7c2da4f711 - k8s.io/api v0.34.3 - k8s.io/apimachinery v0.34.3 + github.com/tigera/api v0.0.0-20260227222130-df0b9e289a34 + k8s.io/api v0.34.4 + k8s.io/apimachinery v0.34.4 sigs.k8s.io/controller-runtime v0.20.2 ) require ( github.com/fxamacker/cbor/v2 v2.9.0 // indirect - github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/logr v1.4.3 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/x448/float16 v0.8.4 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect - golang.org/x/net v0.41.0 // indirect - golang.org/x/text v0.26.0 // indirect + golang.org/x/net v0.49.0 // indirect + golang.org/x/text v0.33.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect diff --git a/api/go.sum b/api/go.sum index 0f0cb42b02..76127da3e8 100644 --- a/api/go.sum +++ b/api/go.sum @@ -1,13 +1,13 @@ +github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= +github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= -github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= @@ -15,8 +15,8 @@ github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69 github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= -github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 h1:z2ogiKUYzX5Is6zr/vP9vJGqPwcdqsWjOt+V8J7+bTc= +github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= @@ -31,14 +31,10 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= -github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= -github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= -github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= -github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg= -github.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= -github.com/onsi/gomega v1.38.0 h1:c/WX+w8SLAinvuKKQFh77WEucCnPk4j2OTUr7lt7BeY= -github.com/onsi/gomega v1.38.0/go.mod h1:OcXcwId0b9QsE7Y49u+BTrL4IdKOBOKnD6VQNTJEB6o= +github.com/onsi/ginkgo/v2 v2.28.1 h1:S4hj+HbZp40fNKuLUQOYLDgZLwNUVn19N3Atb98NCyI= +github.com/onsi/ginkgo/v2 v2.28.1/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE= +github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28= +github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.80.1 h1:DP+PUNVOc+Bkft8a4QunLzaZ0RspWuD3tBbcPHr2PeE= @@ -51,43 +47,49 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/tigera/api v0.0.0-20251017180206-9d7c2da4f711 h1:A75XdvxO3SlR5qydLSf+CovlwkRCONGcMhZD0l58kNM= -github.com/tigera/api v0.0.0-20251017180206-9d7c2da4f711/go.mod h1:5vkALOm1TWUzg3ElTWnTE3O6wkNB3F8cTkZtio7eFGw= +github.com/tigera/api v0.0.0-20260227222130-df0b9e289a34 h1:bFz27L2XauCys8mBwGQUXzYL8fS4oC5/ModvfujYKaQ= +github.com/tigera/api v0.0.0-20260227222130-df0b9e289a34/go.mod h1:CX/80DoAQD88kMvkvujV4h3XPmIBo0yeSf9OZmvZY0c= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= +golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= -golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= +golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= +golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= -golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= -golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= +golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= +golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= -golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= +golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= +golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -97,14 +99,12 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -k8s.io/api v0.34.3 h1:D12sTP257/jSH2vHV2EDYrb16bS7ULlHpdNdNhEw2S4= -k8s.io/api v0.34.3/go.mod h1:PyVQBF886Q5RSQZOim7DybQjAbVs8g7gwJNhGtY5MBk= -k8s.io/apimachinery v0.34.3 h1:/TB+SFEiQvN9HPldtlWOTp0hWbJ+fjU+wkxysf/aQnE= -k8s.io/apimachinery v0.34.3/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= +k8s.io/api v0.34.4 h1:Z5hsoQcZ2yBjelb9j5JKzCVo9qv9XLkVm5llnqS4h+0= +k8s.io/api v0.34.4/go.mod h1:6SaGYuGPkMqqCgg8rPG/OQoCrhgSEV+wWn9v21fDP3o= +k8s.io/apimachinery v0.34.4 h1:C5SiSzLEMyWIk53sSbnk0WlOOyqv/MFnWvuc/d6M+xc= +k8s.io/apimachinery v0.34.4/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y= diff --git a/go.mod b/go.mod index 38bc3dd40f..33d5c7dc4a 100644 --- a/go.mod +++ b/go.mod @@ -29,7 +29,7 @@ require ( github.com/sirupsen/logrus v1.9.4 github.com/snowzach/rotatefilehook v0.0.0-20220211133110-53752135082d github.com/stretchr/testify v1.11.1 - github.com/tigera/api v0.0.0-20260131232301-9cd5ef33622f + github.com/tigera/api v0.0.0-20260227222130-df0b9e289a34 github.com/tigera/operator/api v0.0.0-20260120220012-4a3f8a7d8399 github.com/urfave/cli/v3 v3.6.2 go.uber.org/zap v1.27.1 @@ -39,12 +39,12 @@ require ( gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v3 v3.0.1 helm.sh/helm/v3 v3.19.5 - k8s.io/api v0.34.3 - k8s.io/apiextensions-apiserver v0.34.3 - k8s.io/apimachinery v0.34.3 - k8s.io/apiserver v0.34.3 - k8s.io/client-go v0.34.3 - k8s.io/kube-aggregator v0.34.3 + k8s.io/api v0.34.4 + k8s.io/apiextensions-apiserver v0.34.4 + k8s.io/apimachinery v0.34.4 + k8s.io/apiserver v0.34.4 + k8s.io/client-go v0.34.4 + k8s.io/kube-aggregator v0.34.4 k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 sigs.k8s.io/controller-runtime v0.22.5 sigs.k8s.io/gateway-api v1.3.1-0.20250527223622-54df0a899c1c @@ -183,11 +183,11 @@ require ( gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect howett.net/plist v1.0.1 // indirect - k8s.io/cli-runtime v0.34.3 // indirect - k8s.io/component-base v0.34.3 // indirect + k8s.io/cli-runtime v0.34.4 // indirect + k8s.io/component-base v0.34.4 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect - k8s.io/kubectl v0.34.3 // indirect + k8s.io/kubectl v0.34.4 // indirect oras.land/oras-go/v2 v2.6.0 // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/kustomize/api v0.20.1 // indirect diff --git a/go.sum b/go.sum index 7394d746db..29fb59f491 100644 --- a/go.sum +++ b/go.sum @@ -313,12 +313,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8m github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= -github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= -github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/olivere/elastic/v7 v7.0.32 h1:R7CXvbu8Eq+WlsLgxmKVKPox0oOwAE/2T9Si5BnvK6E= github.com/olivere/elastic/v7 v7.0.32/go.mod h1:c7PVmLe3Fxq77PIfY/bZmxY/TAamBhCzZ8xDOE09a9k= -github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= -github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= github.com/onsi/ginkgo/v2 v2.28.1 h1:S4hj+HbZp40fNKuLUQOYLDgZLwNUVn19N3Atb98NCyI= github.com/onsi/ginkgo/v2 v2.28.1/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE= github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28= @@ -410,8 +406,8 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= -github.com/tigera/api v0.0.0-20260131232301-9cd5ef33622f h1:kvsv9htGZBL8L4j+SRjy4iwCM4iVcEEo9RuMQ+o1CgA= -github.com/tigera/api v0.0.0-20260131232301-9cd5ef33622f/go.mod h1:bxcXRBfeRHbx80vBXAg8ivbN/y6seqMm2MPhgbWoShc= +github.com/tigera/api v0.0.0-20260227222130-df0b9e289a34 h1:bFz27L2XauCys8mBwGQUXzYL8fS4oC5/ModvfujYKaQ= +github.com/tigera/api v0.0.0-20260227222130-df0b9e289a34/go.mod h1:CX/80DoAQD88kMvkvujV4h3XPmIBo0yeSf9OZmvZY0c= github.com/urfave/cli/v3 v3.6.2 h1:lQuqiPrZ1cIz8hz+HcrG0TNZFxU70dPZ3Yl+pSrH9A8= github.com/urfave/cli/v3 v3.6.2/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso= github.com/vbatts/tar-split v0.12.1 h1:CqKoORW7BUWBe7UL/iqTVvkTBOF8UvOMKOIZykxnnbo= @@ -577,8 +573,6 @@ gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -593,28 +587,28 @@ helm.sh/helm/v3 v3.19.5 h1:l8zDGBhPaF2z5pTR5ASku/yZwi0qZrWthWMzvf1ZruE= helm.sh/helm/v3 v3.19.5/go.mod h1:PC1rk7PqacpkV4acUFMLStOOis7QM9Jq3DveHBInu4s= howett.net/plist v1.0.1 h1:37GdZ8tP09Q35o9ych3ehygcsL+HqKSwzctveSlarvM= howett.net/plist v1.0.1/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= -k8s.io/api v0.34.3 h1:D12sTP257/jSH2vHV2EDYrb16bS7ULlHpdNdNhEw2S4= -k8s.io/api v0.34.3/go.mod h1:PyVQBF886Q5RSQZOim7DybQjAbVs8g7gwJNhGtY5MBk= -k8s.io/apiextensions-apiserver v0.34.3 h1:p10fGlkDY09eWKOTeUSioxwLukJnm+KuDZdrW71y40g= -k8s.io/apiextensions-apiserver v0.34.3/go.mod h1:aujxvqGFRdb/cmXYfcRTeppN7S2XV/t7WMEc64zB5A0= -k8s.io/apimachinery v0.34.3 h1:/TB+SFEiQvN9HPldtlWOTp0hWbJ+fjU+wkxysf/aQnE= -k8s.io/apimachinery v0.34.3/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= -k8s.io/apiserver v0.34.3 h1:uGH1qpDvSiYG4HVFqc6A3L4CKiX+aBWDrrsxHYK0Bdo= -k8s.io/apiserver v0.34.3/go.mod h1:QPnnahMO5C2m3lm6fPW3+JmyQbvHZQ8uudAu/493P2w= -k8s.io/cli-runtime v0.34.3 h1:YRyMhiwX0dT9lmG0AtZDaeG33Nkxgt9OlCTZhRXj9SI= -k8s.io/cli-runtime v0.34.3/go.mod h1:GVwL1L5uaGEgM7eGeKjaTG2j3u134JgG4dAI6jQKhMc= -k8s.io/client-go v0.34.3 h1:wtYtpzy/OPNYf7WyNBTj3iUA0XaBHVqhv4Iv3tbrF5A= -k8s.io/client-go v0.34.3/go.mod h1:OxxeYagaP9Kdf78UrKLa3YZixMCfP6bgPwPwNBQBzpM= -k8s.io/component-base v0.34.3 h1:zsEgw6ELqK0XncCQomgO9DpUIzlrYuZYA0Cgo+JWpVk= -k8s.io/component-base v0.34.3/go.mod h1:5iIlD8wPfWE/xSHTRfbjuvUul2WZbI2nOUK65XL0E/c= +k8s.io/api v0.34.4 h1:Z5hsoQcZ2yBjelb9j5JKzCVo9qv9XLkVm5llnqS4h+0= +k8s.io/api v0.34.4/go.mod h1:6SaGYuGPkMqqCgg8rPG/OQoCrhgSEV+wWn9v21fDP3o= +k8s.io/apiextensions-apiserver v0.34.4 h1:TAh2mEduc27sR7lfEthOL2oNeQuux9pQCEJCVC9Gxrs= +k8s.io/apiextensions-apiserver v0.34.4/go.mod h1:13rZ7iu/F4APVV0I0StgBmhvWBGgjGTDaqi21G0113E= +k8s.io/apimachinery v0.34.4 h1:C5SiSzLEMyWIk53sSbnk0WlOOyqv/MFnWvuc/d6M+xc= +k8s.io/apimachinery v0.34.4/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= +k8s.io/apiserver v0.34.4 h1:QmMakuCjlFBJpsXKIUom8OUE7+PhZk7hyNiLqlyDH58= +k8s.io/apiserver v0.34.4/go.mod h1:4dM2Pfd+VQQA/4pLVPorZJbIadaTLcvgQn2GYYcA6Ic= +k8s.io/cli-runtime v0.34.4 h1:QdGWDtJENTskib2Ab304Xwklv+lk4mxz+fd2ng36lZY= +k8s.io/cli-runtime v0.34.4/go.mod h1:PED/aZzYDUv6nPRGYXCFUnNOVBWlUDlVITu0Q3djDus= +k8s.io/client-go v0.34.4 h1:IXhvzFdm0e897kXtLbeyMpAGzontcShJ/gi/XCCsOLc= +k8s.io/client-go v0.34.4/go.mod h1:tXIVJTQabT5QRGlFdxZQFxrIhcGUPpKL5DAc4gSWTE8= +k8s.io/component-base v0.34.4 h1:jP4XqR48YelfXIlRpOHQgms5GebU23zSE6xcvTwpXDE= +k8s.io/component-base v0.34.4/go.mod h1:uujRfLNOwNiFWz47eBjNZEj/Swn2cdhqI7lW2MeFdrU= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kube-aggregator v0.34.3 h1:rKsZWTD2As4dKuv+zzdJU0uo5H7bFlAEoSucai4mW6M= -k8s.io/kube-aggregator v0.34.3/go.mod h1:d4D8PV2FK4Qlq6u442FSum1tHPhK9tKdKBfH/A3R0I0= +k8s.io/kube-aggregator v0.34.4 h1:b2Y07+HfImko/ru5BSMsWOSndGJ7+gc5AyZe/rgc+wI= +k8s.io/kube-aggregator v0.34.4/go.mod h1:L2u+9yLYVB5v9+np9EInd63aq5uwN+pnHrlVNaEi8Jc= k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA= k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts= -k8s.io/kubectl v0.34.3 h1:vpM6//153gh5gvsYHXWHVJ4l4xmN5QFwTSmlfd8icm8= -k8s.io/kubectl v0.34.3/go.mod h1:zZQHtIZoUqTP1bAnPzq/3W1jfc0NeOeunFgcswrfg1c= +k8s.io/kubectl v0.34.4 h1:60NkmD2prPpAJIl81CO6QkQXJ2UlhH5LGIpFxlqK9D8= +k8s.io/kubectl v0.34.4/go.mod h1:Yqa6hDnryvuHFWA/NwJExnSATXMdPeMtOZstdTXeeIM= k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= oras.land/oras-go/v2 v2.6.0 h1:X4ELRsiGkrbeox69+9tzTu492FMUu7zJQW6eJU+I2oc= diff --git a/pkg/controller/logcollector/logcollector_controller.go b/pkg/controller/logcollector/logcollector_controller.go index bc89d5a97b..31534b2f14 100644 --- a/pkg/controller/logcollector/logcollector_controller.go +++ b/pkg/controller/logcollector/logcollector_controller.go @@ -18,6 +18,7 @@ import ( "context" "fmt" "strings" + "time" "github.com/tigera/operator/pkg/dns" corev1 "k8s.io/api/core/v1" @@ -438,6 +439,18 @@ func (r *ReconcileLogCollector) Reconcile(ctx context.Context, request reconcile certificateManager.AddToStatusManager(r.status, render.LogCollectorNamespace) + gracePeriod := utils.ParseGracePeriod(license.Status.GracePeriod) + licenseStatus := utils.GetLicenseStatus(license, gracePeriod) + licenseExpired := licenseStatus == utils.LicenseStatusExpired + + // When in the grace period, schedule a requeue so the controller automatically + // transitions to expired state when the grace period elapses. + var graceRequeueAfter time.Duration + if licenseStatus == utils.LicenseStatusInGracePeriod { + reqLogger.Info("License has expired and is within the grace period. Please renew your license to avoid service disruption.") + graceRequeueAfter = time.Until(license.Status.Expiry.Add(gracePeriod)) + } + exportLogs := utils.IsFeatureActive(license, common.ExportLogsFeature) if !exportLogs && instance.Spec.AdditionalStores != nil { r.status.SetDegraded(operatorv1.ResourceValidationError, "Feature is not active - License does not support feature: export-logs", nil, reqLogger) @@ -595,6 +608,7 @@ func (r *ReconcileLogCollector) Reconcile(ctx context.Context, request reconcile EKSLogForwarderKeyPair: eksLogForwarderKeyPair, PacketCapture: packetcaptureapi, NonClusterHost: nonclusterhost, + LicenseExpired: licenseExpired, } // Render the fluentd component for Linux comp := render.Fluentd(fluentdCfg) @@ -666,6 +680,7 @@ func (r *ReconcileLogCollector) Reconcile(ctx context.Context, request reconcile UseSyslogCertificate: useSyslogCertificate, FluentdKeyPair: fluentdKeyPair, EKSLogForwarderKeyPair: eksLogForwarderKeyPair, + LicenseExpired: licenseExpired, } comp = render.Fluentd(fluentdCfg) @@ -683,6 +698,12 @@ func (r *ReconcileLogCollector) Reconcile(ctx context.Context, request reconcile } } + if licenseExpired { + r.status.SetDegraded(operatorv1.ResourceValidationError, + "License is expired - Log forwarding is stopped. Contact Tigera support or email licensing@tigera.io", nil, reqLogger) + return reconcile.Result{}, nil + } + // Clear the degraded bit if we've reached this far. r.status.ClearDegraded() @@ -697,7 +718,7 @@ func (r *ReconcileLogCollector) Reconcile(ctx context.Context, request reconcile if err = r.client.Status().Update(ctx, instance); err != nil { return reconcile.Result{}, err } - return reconcile.Result{}, nil + return reconcile.Result{RequeueAfter: graceRequeueAfter}, nil } func getS3Credential(client client.Client) (*render.S3Credential, error) { diff --git a/pkg/controller/logcollector/logcollector_controller_test.go b/pkg/controller/logcollector/logcollector_controller_test.go index 048089b602..47371ca82b 100644 --- a/pkg/controller/logcollector/logcollector_controller_test.go +++ b/pkg/controller/logcollector/logcollector_controller_test.go @@ -76,6 +76,7 @@ var _ = Describe("LogCollector controller tests", func() { mockStatus.On("AddStatefulSets", mock.Anything).Return() mockStatus.On("AddCronJobs", mock.Anything) mockStatus.On("RemoveCertificateSigningRequests", mock.Anything).Return() + mockStatus.On("RemoveDaemonsets", mock.Anything).Return() mockStatus.On("AddCertificateSigningRequests", mock.Anything).Return() mockStatus.On("IsAvailable").Return(true) mockStatus.On("OnCRFound").Return() @@ -844,4 +845,82 @@ var _ = Describe("LogCollector controller tests", func() { Expect(pullSecret.Kind).To(Equal("LogCollector")) }) }) + + Context("License expiry", func() { + It("should set degraded status and delete fluentd DaemonSet when license is expired", func() { + // First reconcile to create fluentd resources. + _, err := r.Reconcile(ctx, reconcile.Request{}) + Expect(err).ShouldNot(HaveOccurred()) + + // Verify the DaemonSet exists. + ds := appsv1.DaemonSet{ + TypeMeta: metav1.TypeMeta{Kind: "DaemonSet", APIVersion: "apps/v1"}, + ObjectMeta: metav1.ObjectMeta{ + Name: "fluentd-node", + Namespace: render.LogCollectorNamespace, + }, + } + Expect(test.GetResource(c, &ds)).To(BeNil()) + + // Replace the valid license with an expired one. + Expect(c.Delete(ctx, &v3.LicenseKey{ObjectMeta: metav1.ObjectMeta{Name: "default"}})).NotTo(HaveOccurred()) + Expect(c.Create(ctx, &v3.LicenseKey{ + ObjectMeta: metav1.ObjectMeta{Name: "default", CreationTimestamp: metav1.Now()}, + Status: v3.LicenseKeyStatus{ + Expiry: metav1.Time{Time: time.Now().Add(-24 * time.Hour)}, + }, + })).NotTo(HaveOccurred()) + + mockStatus.On("SetDegraded", operatorv1.ResourceValidationError, + "License is expired - Log forwarding is stopped. Contact Tigera support or email licensing@tigera.io", mock.Anything, mock.Anything).Return() + + // Reconcile again with expired license. + _, err = r.Reconcile(ctx, reconcile.Request{}) + Expect(err).ShouldNot(HaveOccurred()) + + // Verify the DaemonSet has been deleted. + ds = appsv1.DaemonSet{ + TypeMeta: metav1.TypeMeta{Kind: "DaemonSet", APIVersion: "apps/v1"}, + ObjectMeta: metav1.ObjectMeta{ + Name: "fluentd-node", + Namespace: render.LogCollectorNamespace, + }, + } + Expect(test.GetResource(c, &ds)).NotTo(BeNil()) + }) + + It("should requeue when license is in the grace period", func() { + // First reconcile to create fluentd resources. + _, err := r.Reconcile(ctx, reconcile.Request{}) + Expect(err).ShouldNot(HaveOccurred()) + + // Replace the valid license with one that expired 1 day ago but has a 90-day grace period. + Expect(c.Delete(ctx, &v3.LicenseKey{ObjectMeta: metav1.ObjectMeta{Name: "default"}})).NotTo(HaveOccurred()) + Expect(c.Create(ctx, &v3.LicenseKey{ + ObjectMeta: metav1.ObjectMeta{Name: "default", CreationTimestamp: metav1.Now()}, + Status: v3.LicenseKeyStatus{ + Expiry: metav1.Time{Time: time.Now().Add(-24 * time.Hour)}, + GracePeriod: "90d", + Features: []string{"export-logs"}, + }, + })).NotTo(HaveOccurred()) + + result, err := r.Reconcile(ctx, reconcile.Request{}) + Expect(err).ShouldNot(HaveOccurred()) + + // Should requeue to re-reconcile when the grace period expires. + Expect(result.RequeueAfter).To(BeNumerically(">", 0)) + Expect(result.RequeueAfter).To(BeNumerically("~", 89*24*time.Hour, 1*time.Hour)) + + // DaemonSet should still exist during the grace period. + ds := appsv1.DaemonSet{ + TypeMeta: metav1.TypeMeta{Kind: "DaemonSet", APIVersion: "apps/v1"}, + ObjectMeta: metav1.ObjectMeta{ + Name: "fluentd-node", + Namespace: render.LogCollectorNamespace, + }, + } + Expect(test.GetResource(c, &ds)).To(BeNil()) + }) + }) }) diff --git a/pkg/controller/monitor/monitor_controller.go b/pkg/controller/monitor/monitor_controller.go index daa58d84ab..93dbc3ea56 100644 --- a/pkg/controller/monitor/monitor_controller.go +++ b/pkg/controller/monitor/monitor_controller.go @@ -19,6 +19,7 @@ import ( _ "embed" "fmt" "reflect" + "time" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" @@ -65,9 +66,10 @@ func Add(mgr manager.Manager, opts options.ControllerOptions) error { prometheusReady := &utils.ReadyFlag{} tierWatchReady := &utils.ReadyFlag{} + licenseAPIReady := &utils.ReadyFlag{} // Create the reconciler - reconciler := newReconciler(mgr, opts, prometheusReady, tierWatchReady) + reconciler := newReconciler(mgr, opts, prometheusReady, tierWatchReady, licenseAPIReady) // Create a new controller c, err := ctrlruntime.NewController("monitor-controller", mgr, controller.Options{Reconciler: reconciler}) @@ -91,10 +93,12 @@ func Add(mgr manager.Manager, opts options.ControllerOptions) error { go waitToAddPrometheusWatch(c, opts.K8sClientset, log, prometheusReady) + go utils.WaitToAddLicenseKeyWatch(c, opts.K8sClientset, log, licenseAPIReady) + return add(mgr, c) } -func newReconciler(mgr manager.Manager, opts options.ControllerOptions, prometheusReady *utils.ReadyFlag, tierWatchReady *utils.ReadyFlag) reconcile.Reconciler { +func newReconciler(mgr manager.Manager, opts options.ControllerOptions, prometheusReady *utils.ReadyFlag, tierWatchReady *utils.ReadyFlag, licenseAPIReady *utils.ReadyFlag) reconcile.Reconciler { r := &ReconcileMonitor{ client: mgr.GetClient(), scheme: mgr.GetScheme(), @@ -102,6 +106,7 @@ func newReconciler(mgr manager.Manager, opts options.ControllerOptions, promethe status: status.New(mgr.GetClient(), "monitor", opts.KubernetesVersion), prometheusReady: prometheusReady, tierWatchReady: tierWatchReady, + licenseAPIReady: licenseAPIReady, clusterDomain: opts.ClusterDomain, multiTenant: opts.MultiTenant, } @@ -184,6 +189,7 @@ type ReconcileMonitor struct { status status.StatusManager prometheusReady *utils.ReadyFlag tierWatchReady *utils.ReadyFlag + licenseAPIReady *utils.ReadyFlag clusterDomain string multiTenant bool } @@ -244,6 +250,33 @@ func (r *ReconcileMonitor) Reconcile(ctx context.Context, request reconcile.Requ } } + if !r.licenseAPIReady.IsReady() { + r.status.SetDegraded(operatorv1.ResourceNotReady, "Waiting for LicenseKeyAPI to be ready", nil, reqLogger) + return reconcile.Result{RequeueAfter: utils.StandardRetry}, nil + } + + license, err := utils.FetchLicenseKey(ctx, r.client) + if err != nil { + if errors.IsNotFound(err) { + r.status.SetDegraded(operatorv1.ResourceNotFound, "License not found", err, reqLogger) + return reconcile.Result{RequeueAfter: utils.StandardRetry}, nil + } + r.status.SetDegraded(operatorv1.ResourceReadError, "Error querying license", err, reqLogger) + return reconcile.Result{RequeueAfter: utils.StandardRetry}, nil + } + + gracePeriod := utils.ParseGracePeriod(license.Status.GracePeriod) + licenseStatus := utils.GetLicenseStatus(license, gracePeriod) + licenseExpired := licenseStatus == utils.LicenseStatusExpired + + // When in the grace period, schedule a requeue so the controller automatically + // transitions to expired state when the grace period elapses. + var graceRequeueAfter time.Duration + if licenseStatus == utils.LicenseStatusInGracePeriod { + reqLogger.Info("License has expired and is within the grace period. Please renew your license to avoid service disruption.") + graceRequeueAfter = time.Until(license.Status.Expiry.Add(gracePeriod)) + } + variant, install, err := utils.GetInstallation(context.Background(), r.client) if err != nil { if errors.IsNotFound(err) { @@ -388,6 +421,7 @@ func (r *ReconcileMonitor) Reconcile(ctx context.Context, request reconcile.Requ OpenShift: r.provider.IsOpenShift(), KubeControllerPort: kubeControllersMetricsPort, FelixPrometheusMetricsEnabled: utils.IsFelixPrometheusMetricsEnabled(felixConfiguration), + LicenseExpired: licenseExpired, } // Render prometheus component @@ -431,6 +465,12 @@ func (r *ReconcileMonitor) Reconcile(ctx context.Context, request reconcile.Requ // Tell the status manager that we're ready to monitor the resources we've told it about and receive statuses. r.status.ReadyToMonitor() + if licenseExpired { + r.status.SetDegraded(operatorv1.ResourceValidationError, + "License is expired - Contact Tigera support or email licensing@tigera.io", nil, reqLogger) + return reconcile.Result{}, nil + } + r.status.ClearDegraded() if !r.status.IsAvailable() { @@ -444,7 +484,7 @@ func (r *ReconcileMonitor) Reconcile(ctx context.Context, request reconcile.Requ return reconcile.Result{}, err } - return reconcile.Result{}, nil + return reconcile.Result{RequeueAfter: graceRequeueAfter}, nil } func fillDefaults(instance *operatorv1.Monitor) { diff --git a/pkg/controller/monitor/monitor_controller_test.go b/pkg/controller/monitor/monitor_controller_test.go index 2d2992badd..9c8df546c3 100644 --- a/pkg/controller/monitor/monitor_controller_test.go +++ b/pkg/controller/monitor/monitor_controller_test.go @@ -17,6 +17,7 @@ package monitor import ( "bytes" "context" + "time" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -96,6 +97,7 @@ var _ = Describe("Monitor controller tests", func() { status: mockStatus, prometheusReady: &utils.ReadyFlag{}, tierWatchReady: &utils.ReadyFlag{}, + licenseAPIReady: &utils.ReadyFlag{}, } // We start off with a 'standard' installation, with nothing special @@ -131,9 +133,18 @@ var _ = Describe("Monitor controller tests", func() { Expect(err).NotTo(HaveOccurred()) Expect(cli.Create(ctx, certificateManager.KeyPair().Secret(common.OperatorNamespace()))).NotTo(HaveOccurred()) + // Create a valid (non-expired) license. + Expect(cli.Create(ctx, &v3.LicenseKey{ + ObjectMeta: metav1.ObjectMeta{Name: "default", CreationTimestamp: metav1.Now()}, + Status: v3.LicenseKeyStatus{ + Expiry: metav1.Time{Time: time.Now().Add(24 * time.Hour)}, + }, + })).NotTo(HaveOccurred()) + // Mark that watches were successful. r.prometheusReady.MarkAsReady() r.tierWatchReady.MarkAsReady() + r.licenseAPIReady.MarkAsReady() }) Context("controller reconciliation", func() { @@ -633,4 +644,71 @@ var _ = Describe("Monitor controller tests", func() { Expect(instance.Status.Conditions[2].ObservedGeneration).To(Equal(generation)) }) }) + + Context("License expiry", func() { + It("should set degraded status when license is expired", func() { + // Replace the valid license with an expired one. + Expect(cli.Delete(ctx, &v3.LicenseKey{ObjectMeta: metav1.ObjectMeta{Name: "default"}})).NotTo(HaveOccurred()) + Expect(cli.Create(ctx, &v3.LicenseKey{ + ObjectMeta: metav1.ObjectMeta{Name: "default", CreationTimestamp: metav1.Now()}, + Status: v3.LicenseKeyStatus{ + Expiry: metav1.Time{Time: time.Now().Add(-24 * time.Hour)}, + }, + })).NotTo(HaveOccurred()) + + mockStatus.On("SetDegraded", operatorv1.ResourceValidationError, + "License is expired - Contact Tigera support or email licensing@tigera.io", mock.Anything, mock.Anything).Return() + + _, err := r.Reconcile(ctx, reconcile.Request{}) + Expect(err).ShouldNot(HaveOccurred()) + + // Verify that ServiceMonitors are not created when license is expired. + sm := &monitoringv1.ServiceMonitor{} + Expect(cli.Get(ctx, client.ObjectKey{Name: monitor.CalicoNodeMonitor, Namespace: common.TigeraPrometheusNamespace}, sm)).To(HaveOccurred()) + Expect(cli.Get(ctx, client.ObjectKey{Name: monitor.ElasticsearchMetrics, Namespace: common.TigeraPrometheusNamespace}, sm)).To(HaveOccurred()) + Expect(cli.Get(ctx, client.ObjectKey{Name: monitor.FluentdMetrics, Namespace: common.TigeraPrometheusNamespace}, sm)).To(HaveOccurred()) + + // Verify that other Prometheus resources are still created. + am := &monitoringv1.Alertmanager{} + Expect(cli.Get(ctx, client.ObjectKey{Name: monitor.CalicoNodeAlertmanager, Namespace: common.TigeraPrometheusNamespace}, am)).NotTo(HaveOccurred()) + p := &monitoringv1.Prometheus{} + Expect(cli.Get(ctx, client.ObjectKey{Name: monitor.CalicoNodePrometheus, Namespace: common.TigeraPrometheusNamespace}, p)).NotTo(HaveOccurred()) + }) + + It("should not set degraded status when license is valid", func() { + _, err := r.Reconcile(ctx, reconcile.Request{}) + Expect(err).ShouldNot(HaveOccurred()) + + // ServiceMonitors should be created with a valid license. + sm := &monitoringv1.ServiceMonitor{} + Expect(cli.Get(ctx, client.ObjectKey{Name: monitor.CalicoNodeMonitor, Namespace: common.TigeraPrometheusNamespace}, sm)).NotTo(HaveOccurred()) + Expect(cli.Get(ctx, client.ObjectKey{Name: monitor.ElasticsearchMetrics, Namespace: common.TigeraPrometheusNamespace}, sm)).NotTo(HaveOccurred()) + Expect(cli.Get(ctx, client.ObjectKey{Name: monitor.FluentdMetrics, Namespace: common.TigeraPrometheusNamespace}, sm)).NotTo(HaveOccurred()) + }) + + It("should requeue when license is in the grace period", func() { + // Replace the valid license with one that expired 1 day ago but has a 90-day grace period. + Expect(cli.Delete(ctx, &v3.LicenseKey{ObjectMeta: metav1.ObjectMeta{Name: "default"}})).NotTo(HaveOccurred()) + Expect(cli.Create(ctx, &v3.LicenseKey{ + ObjectMeta: metav1.ObjectMeta{Name: "default", CreationTimestamp: metav1.Now()}, + Status: v3.LicenseKeyStatus{ + Expiry: metav1.Time{Time: time.Now().Add(-24 * time.Hour)}, + GracePeriod: "90d", + }, + })).NotTo(HaveOccurred()) + + result, err := r.Reconcile(ctx, reconcile.Request{}) + Expect(err).ShouldNot(HaveOccurred()) + + // Should requeue to re-reconcile when the grace period expires. + Expect(result.RequeueAfter).To(BeNumerically(">", 0)) + Expect(result.RequeueAfter).To(BeNumerically("~", 89*24*time.Hour, 1*time.Hour)) + + // ServiceMonitors should still be created during the grace period. + sm := &monitoringv1.ServiceMonitor{} + Expect(cli.Get(ctx, client.ObjectKey{Name: monitor.CalicoNodeMonitor, Namespace: common.TigeraPrometheusNamespace}, sm)).NotTo(HaveOccurred()) + Expect(cli.Get(ctx, client.ObjectKey{Name: monitor.ElasticsearchMetrics, Namespace: common.TigeraPrometheusNamespace}, sm)).NotTo(HaveOccurred()) + Expect(cli.Get(ctx, client.ObjectKey{Name: monitor.FluentdMetrics, Namespace: common.TigeraPrometheusNamespace}, sm)).NotTo(HaveOccurred()) + }) + }) }) diff --git a/pkg/controller/status/status.go b/pkg/controller/status/status.go index 060398b6c1..60a26a57c0 100644 --- a/pkg/controller/status/status.go +++ b/pkg/controller/status/status.go @@ -361,7 +361,11 @@ func (m *statusManager) SetDegraded(reason operator.TigeraStatusReason, msg stri defer m.lock.Unlock() m.degraded = true m.explicitDegradedReason = reason - m.explicitDegradedMsg = fmt.Sprintf("%s: %s", msg, errormsg) + if errormsg != "" { + m.explicitDegradedMsg = fmt.Sprintf("%s: %s", msg, errormsg) + } else { + m.explicitDegradedMsg = msg + } } // ClearDegraded clears degraded state. diff --git a/pkg/controller/status/status_test.go b/pkg/controller/status/status_test.go index f64fb655c1..c503e10508 100644 --- a/pkg/controller/status/status_test.go +++ b/pkg/controller/status/status_test.go @@ -455,7 +455,7 @@ var _ = Describe("Status reporting tests", func() { sm.failing = []string{"This pod has died"} Expect(sm.degradedMessage()).To(Equal("This pod has died")) sm.SetDegraded(operator.ResourceNotFound, "Controller set us degraded", nil, log) - Expect(sm.degradedMessage()).To(Equal("Controller set us degraded: \nThis pod has died")) + Expect(sm.degradedMessage()).To(Equal("Controller set us degraded\nThis pod has died")) }) It("should contain all the NamespacesNames for all the resources added by multiple calls to Set", func() { diff --git a/pkg/controller/utils/license.go b/pkg/controller/utils/license.go new file mode 100644 index 0000000000..d95bacbda3 --- /dev/null +++ b/pkg/controller/utils/license.go @@ -0,0 +1,102 @@ +// Copyright (c) 2026 Tigera, Inc. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package utils + +import ( + "context" + "strconv" + "strings" + "time" + + "github.com/go-logr/logr" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + + "sigs.k8s.io/controller-runtime/pkg/client" + + v3 "github.com/tigera/api/pkg/apis/projectcalico/v3" + + "github.com/tigera/operator/pkg/ctrlruntime" +) + +// LicenseStatus represents the current state of the license with respect to expiry and grace period. +type LicenseStatus int + +const ( + // LicenseStatusValid means the license has not expired. + LicenseStatusValid LicenseStatus = iota + // LicenseStatusInGracePeriod means the license expiry has passed but the grace period has not elapsed. + LicenseStatusInGracePeriod + // LicenseStatusExpired means the license expiry plus the grace period has passed. + LicenseStatusExpired +) + +// WaitToAddLicenseKeyWatch starts a goroutine that waits for the LicenseKey CRD to be available +// and then adds a watch for it on the given controller. +func WaitToAddLicenseKeyWatch(controller ctrlruntime.Controller, c kubernetes.Interface, log logr.Logger, flag *ReadyFlag) { + WaitToAddResourceWatch(controller, c, log, flag, []client.Object{&v3.LicenseKey{TypeMeta: metav1.TypeMeta{Kind: v3.KindLicenseKey}}}) +} + +// FetchLicenseKey returns the license if it has been installed. It's useful +// to prevent rollout of TSEE components that might require it. +// It will return an error if the license is not installed/cannot be read +func FetchLicenseKey(ctx context.Context, cli client.Client) (v3.LicenseKey, error) { + instance := &v3.LicenseKey{} + err := cli.Get(ctx, DefaultInstanceKey, instance) + return *instance, err +} + +// IsFeatureActive return true if the feature is listed in LicenseStatusKey +func IsFeatureActive(license v3.LicenseKey, featureName string) bool { + for _, v := range license.Status.Features { + if v == featureName || v == "all" { + return true + } + } + + return false +} + +// ParseGracePeriod parses a grace period string (e.g. "90d") and returns the +// corresponding duration. Returns 0 if the string is empty or cannot be parsed. +func ParseGracePeriod(gracePeriod string) time.Duration { + if gracePeriod == "" { + return 0 + } + s := strings.TrimSuffix(gracePeriod, "d") + days, err := strconv.Atoi(s) + if err != nil || days < 0 { + return 0 + } + return time.Duration(days) * 24 * time.Hour +} + +// GetLicenseStatus returns the current license status using a single point-in-time check. +// It uses a single time.Now() call to avoid inconsistencies at state boundaries. +func GetLicenseStatus(license v3.LicenseKey, gracePeriod time.Duration) LicenseStatus { + if license.Status.Expiry.IsZero() { + return LicenseStatusValid + } + now := time.Now() + expiry := license.Status.Expiry.Time + if now.After(expiry.Add(gracePeriod)) { + return LicenseStatusExpired + } + if now.After(expiry) { + return LicenseStatusInGracePeriod + } + return LicenseStatusValid +} diff --git a/pkg/controller/utils/license_test.go b/pkg/controller/utils/license_test.go new file mode 100644 index 0000000000..f6ca05d49d --- /dev/null +++ b/pkg/controller/utils/license_test.go @@ -0,0 +1,81 @@ +// Copyright (c) 2026 Tigera, Inc. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package utils + +import ( + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + v3 "github.com/tigera/api/pkg/apis/projectcalico/v3" +) + +var _ = Describe("License helpers", func() { + var license v3.LicenseKey + + BeforeEach(func() { + license = v3.LicenseKey{ + ObjectMeta: metav1.ObjectMeta{Name: "default"}, + } + }) + + Context("ParseGracePeriod", func() { + DescribeTable("should parse grace period strings", + func(input string, expected time.Duration) { + Expect(ParseGracePeriod(input)).To(Equal(expected)) + }, + Entry("90 days", "90d", 90*24*time.Hour), + Entry("30 days", "30d", 30*24*time.Hour), + Entry("0 days", "0d", time.Duration(0)), + Entry("empty string", "", time.Duration(0)), + Entry("invalid string", "abc", time.Duration(0)), + Entry("negative days", "-5d", time.Duration(0)), + Entry("number without d suffix", "90", 90*24*time.Hour), + ) + }) + + Context("GetLicenseStatus", func() { + DescribeTable("should return the correct license status", + func(expiryOffset time.Duration, gracePeriod time.Duration, expected LicenseStatus) { + license.Status.Expiry = metav1.Time{Time: time.Now().Add(expiryOffset)} + Expect(GetLicenseStatus(license, gracePeriod)).To(Equal(expected)) + }, + Entry("valid: expiry in the future, no grace period", + 24*time.Hour, time.Duration(0), LicenseStatusValid), + Entry("valid: expiry in the future, with grace period", + 24*time.Hour, 90*24*time.Hour, LicenseStatusValid), + Entry("expired: expiry in the past, no grace period", + -24*time.Hour, time.Duration(0), LicenseStatusExpired), + Entry("in grace period: expiry in past but within grace period", + -24*time.Hour, 90*24*time.Hour, LicenseStatusInGracePeriod), + Entry("expired: expiry in past and beyond grace period", + -100*24*time.Hour, 90*24*time.Hour, LicenseStatusExpired), + ) + + It("should return valid when expiry is zero", func() { + license.Status.Expiry = metav1.Time{} + Expect(GetLicenseStatus(license, 90*24*time.Hour)).To(Equal(LicenseStatusValid)) + }) + + It("should return expired when expiry is exactly at grace period boundary", func() { + // Set expiry far enough in the past that even with clock skew it's clearly past grace. + license.Status.Expiry = metav1.Time{Time: time.Now().Add(-91 * 24 * time.Hour)} + Expect(GetLicenseStatus(license, 90*24*time.Hour)).To(Equal(LicenseStatusExpired)) + }) + }) +}) diff --git a/pkg/controller/utils/utils.go b/pkg/controller/utils/utils.go index 057d4fbfe4..cc5b83de94 100644 --- a/pkg/controller/utils/utils.go +++ b/pkg/controller/utils/utils.go @@ -257,10 +257,6 @@ func createPeriodicReconcileChannel(period time.Duration) chan event.GenericEven return periodicReconcileEvents } -func WaitToAddLicenseKeyWatch(controller ctrlruntime.Controller, c kubernetes.Interface, log logr.Logger, flag *ReadyFlag) { - WaitToAddResourceWatch(controller, c, log, flag, []client.Object{&v3.LicenseKey{TypeMeta: metav1.TypeMeta{Kind: v3.KindLicenseKey}}}) -} - func WaitToAddClusterInformationWatch(controller ctrlruntime.Controller, c kubernetes.Interface, log logr.Logger, flag *ReadyFlag) { WaitToAddResourceWatch(controller, c, log, flag, []client.Object{&v3.ClusterInformation{TypeMeta: metav1.TypeMeta{Kind: v3.KindClusterInformation}}}) } @@ -358,15 +354,6 @@ func GetLogCollector(ctx context.Context, cli client.Client) (*operatorv1.LogCol return logCollector, nil } -// FetchLicenseKey returns the license if it has been installed. It's useful -// to prevent rollout of TSEE components that might require it. -// It will return an error if the license is not installed/cannot be read -func FetchLicenseKey(ctx context.Context, cli client.Client) (v3.LicenseKey, error) { - instance := &v3.LicenseKey{} - err := cli.Get(ctx, DefaultInstanceKey, instance) - return *instance, err -} - // FetchClusterInformation fetches and returns the clusterinformation. func FetchClusterInformation(ctx context.Context, cli client.Client) (v3.ClusterInformation, error) { instance := &v3.ClusterInformation{} @@ -374,17 +361,6 @@ func FetchClusterInformation(ctx context.Context, cli client.Client) (v3.Cluster return *instance, err } -// IsFeatureActive return true if the feature is listed in LicenseStatusKey -func IsFeatureActive(license v3.LicenseKey, featureName string) bool { - for _, v := range license.Status.Features { - if v == featureName || v == "all" { - return true - } - } - - return false -} - // ValidateCertPair checks if the given secret exists in the given // namespace and if so that it contains key and cert fields. If an // empty string is passed for the keyName argument it is skipped. diff --git a/pkg/render/fluentd.go b/pkg/render/fluentd.go index ee176529a4..4352b88303 100644 --- a/pkg/render/fluentd.go +++ b/pkg/render/fluentd.go @@ -185,6 +185,9 @@ type FluentdConfiguration struct { PacketCapture *operatorv1.PacketCaptureAPI NonClusterHost *operatorv1.NonClusterHost + + // LicenseExpired indicates the license has expired and fluentd DaemonSet should be removed. + LicenseExpired bool } type fluentdComponent struct { @@ -332,7 +335,11 @@ func (c *fluentdComponent) Objects() ([]client.Object, []client.Object) { objs = append(objs, c.packetCaptureApiRole(), c.packetCaptureApiRoleBinding()) } - objs = append(objs, c.daemonset()) + if c.cfg.LicenseExpired { + toDelete = append(toDelete, c.daemonset()) + } else { + objs = append(objs, c.daemonset()) + } if c.cfg.NonClusterHost != nil && c.cfg.OSType == rmeta.OSTypeLinux { objs = append(objs, c.nonClusterHostInputService()) diff --git a/pkg/render/fluentd_test.go b/pkg/render/fluentd_test.go index 6a6ce1e52e..a4627156fd 100644 --- a/pkg/render/fluentd_test.go +++ b/pkg/render/fluentd_test.go @@ -1316,6 +1316,46 @@ var _ = Describe("Tigera Secure Fluentd rendering tests", func() { })) }) }) + + It("should move DaemonSet to toDelete when LicenseExpired is true", func() { + cfg.LicenseExpired = true + component := render.Fluentd(cfg) + Expect(component.ResolveImages(nil)).To(BeNil()) + toCreate, toDelete := component.Objects() + + // DaemonSet should not be in toCreate. + for _, obj := range toCreate { + if ds, ok := obj.(*appsv1.DaemonSet); ok { + Fail("DaemonSet should not be in toCreate when license is expired, but found: " + ds.Name) + } + } + + // DaemonSet should be in toDelete. + found := false + for _, obj := range toDelete { + if ds, ok := obj.(*appsv1.DaemonSet); ok && ds.Name == "fluentd-node" { + found = true + break + } + } + Expect(found).To(BeTrue(), "Expected fluentd-node DaemonSet to be in toDelete") + }) + + It("should include DaemonSet in toCreate when LicenseExpired is false", func() { + cfg.LicenseExpired = false + component := render.Fluentd(cfg) + Expect(component.ResolveImages(nil)).To(BeNil()) + toCreate, _ := component.Objects() + + found := false + for _, obj := range toCreate { + if ds, ok := obj.(*appsv1.DaemonSet); ok && ds.Name == "fluentd-node" { + found = true + break + } + } + Expect(found).To(BeTrue(), "Expected fluentd-node DaemonSet to be in toCreate") + }) }) func setupEKSCloudwatchLogConfig() *render.EksCloudwatchLogConfig { diff --git a/pkg/render/monitor/monitor.go b/pkg/render/monitor/monitor.go index 44836e07d7..b01d02a510 100644 --- a/pkg/render/monitor/monitor.go +++ b/pkg/render/monitor/monitor.go @@ -145,6 +145,7 @@ type Config struct { OpenShift bool KubeControllerPort int FelixPrometheusMetricsEnabled bool + LicenseExpired bool } type monitorComponent struct { @@ -230,12 +231,22 @@ func (mc *monitorComponent) Objects() ([]client.Object, []client.Object) { mc.prometheusServiceClusterRole(), mc.prometheusServiceClusterRoleBinding(), mc.prometheusRule(), + ) + + var toDelete []client.Object + + serviceMonitors := []client.Object{ mc.serviceMonitorCalicoNode(), mc.serviceMonitorElasticsearch(), mc.serviceMonitorFluentd(), mc.serviceMonitorQueryServer(), mc.serviceMonitorCalicoKubeControllers(), - ) + } + if mc.cfg.LicenseExpired { + toDelete = append(toDelete, serviceMonitors...) + } else { + toCreate = append(toCreate, serviceMonitors...) + } if mc.cfg.KeyValidatorConfig != nil { toCreate = append(toCreate, secret.ToRuntimeObjects(mc.cfg.KeyValidatorConfig.RequiredSecrets(common.TigeraPrometheusNamespace)...)...) @@ -254,7 +265,6 @@ func (mc *monitorComponent) Objects() ([]client.Object, []client.Object) { } } - var toDelete []client.Object if mc.cfg.Installation.TyphaMetricsPort != nil { toCreate = append(toCreate, mc.typhaServiceMonitor()) } else { diff --git a/pkg/render/monitor/monitor_test.go b/pkg/render/monitor/monitor_test.go index b09c68aa2a..69f76bf78f 100644 --- a/pkg/render/monitor/monitor_test.go +++ b/pkg/render/monitor/monitor_test.go @@ -981,6 +981,64 @@ var _ = Describe("monitor rendering tests", func() { Expect(servicemonitorObj.Spec.Endpoints[2].ScrapeTimeout).To(BeEquivalentTo("5s")) Expect(*servicemonitorObj.Spec.Endpoints[2].RelabelConfigs[0].Replacement).To(Equal("http")) }) + + It("Should move ServiceMonitors to toDelete when LicenseExpired is true", func() { + cfg.LicenseExpired = true + component := monitor.Monitor(cfg) + Expect(component.ResolveImages(nil)).NotTo(HaveOccurred()) + toCreate, toDelete := component.Objects() + + // ServiceMonitors should not be in toCreate. + for _, obj := range toCreate { + if _, ok := obj.(*monitoringv1.ServiceMonitor); ok { + Fail("ServiceMonitor should not be in toCreate when license is expired, but found: " + obj.GetName()) + } + } + + // ServiceMonitors should be in toDelete. + serviceMonitorNames := []string{ + monitor.CalicoNodeMonitor, + monitor.ElasticsearchMetrics, + monitor.FluentdMetrics, + "calico-api", + "calico-kube-controllers-metrics", + } + for _, name := range serviceMonitorNames { + found := false + for _, obj := range toDelete { + if sm, ok := obj.(*monitoringv1.ServiceMonitor); ok && sm.Name == name { + found = true + break + } + } + Expect(found).To(BeTrue(), "Expected ServiceMonitor %s to be in toDelete", name) + } + }) + + It("Should include ServiceMonitors in toCreate when LicenseExpired is false", func() { + cfg.LicenseExpired = false + component := monitor.Monitor(cfg) + Expect(component.ResolveImages(nil)).NotTo(HaveOccurred()) + toCreate, _ := component.Objects() + + serviceMonitorNames := []string{ + monitor.CalicoNodeMonitor, + monitor.ElasticsearchMetrics, + monitor.FluentdMetrics, + "calico-api", + "calico-kube-controllers-metrics", + } + for _, name := range serviceMonitorNames { + found := false + for _, obj := range toCreate { + if sm, ok := obj.(*monitoringv1.ServiceMonitor); ok && sm.Name == name { + found = true + break + } + } + Expect(found).To(BeTrue(), "Expected ServiceMonitor %s to be in toCreate", name) + } + }) }) // expectedBaseResources These are the expected resources in the most basic setup. From e01e68b842eb496345b9dbb6bcab20435f6b70bc Mon Sep 17 00:00:00 2001 From: Jiawei Huang Date: Sat, 28 Feb 2026 10:03:48 -0800 Subject: [PATCH 2/2] Improve ParseGracePeriod to strictly require "Nd" format ParseGracePeriod now requires the "d" suffix and rejects bare integers or other formats. This makes the parser stricter and aligned with the license controller's output format (e.g. "90d"). Also guards against negative values and uses strings.CutSuffix for cleaner suffix handling. Co-Authored-By: Claude Opus 4.6 --- pkg/controller/utils/license.go | 11 ++++++++--- pkg/controller/utils/license_test.go | 2 +- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/pkg/controller/utils/license.go b/pkg/controller/utils/license.go index d95bacbda3..6ee9fcec9d 100644 --- a/pkg/controller/utils/license.go +++ b/pkg/controller/utils/license.go @@ -70,13 +70,18 @@ func IsFeatureActive(license v3.LicenseKey, featureName string) bool { return false } -// ParseGracePeriod parses a grace period string (e.g. "90d") and returns the -// corresponding duration. Returns 0 if the string is empty or cannot be parsed. +// ParseGracePeriod parses a grace period string in the format "Nd" (e.g. "90d") +// where N is a non-negative number of days. This is the format produced by the +// license controller. Returns 0 if the string is empty, cannot be parsed, or +// represents a negative duration. func ParseGracePeriod(gracePeriod string) time.Duration { if gracePeriod == "" { return 0 } - s := strings.TrimSuffix(gracePeriod, "d") + s, ok := strings.CutSuffix(gracePeriod, "d") + if !ok { + return 0 + } days, err := strconv.Atoi(s) if err != nil || days < 0 { return 0 diff --git a/pkg/controller/utils/license_test.go b/pkg/controller/utils/license_test.go index f6ca05d49d..ae5b4cb3ce 100644 --- a/pkg/controller/utils/license_test.go +++ b/pkg/controller/utils/license_test.go @@ -45,7 +45,7 @@ var _ = Describe("License helpers", func() { Entry("empty string", "", time.Duration(0)), Entry("invalid string", "abc", time.Duration(0)), Entry("negative days", "-5d", time.Duration(0)), - Entry("number without d suffix", "90", 90*24*time.Hour), + Entry("bare number without unit", "90", time.Duration(0)), ) })