Skip to content

Commit

Permalink
Add extended cluster license info (#331)
Browse files Browse the repository at this point in the history
* Add extended cluster license info

* Add additional information and change timestamp type

* Always show inUseFeatures and violations

* Fix up test expectations
  • Loading branch information
andrewstucki authored Dec 3, 2024
1 parent b8d2988 commit 9a0d9e4
Show file tree
Hide file tree
Showing 5 changed files with 235 additions and 5 deletions.
33 changes: 33 additions & 0 deletions operator/api/redpanda/v1alpha2/redpanda_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ package v1alpha2
import (
"encoding/json"
"fmt"
"strconv"
"strings"

"github.com/cockroachdb/errors"
helmv2beta2 "github.com/fluxcd/helm-controller/api/v2beta2"
Expand Down Expand Up @@ -143,6 +145,37 @@ type RedpandaStatus struct {
// decommissioned from the cluster and provides its ordinal number.
// +optional
ManagedDecommissioningNode *int32 `json:"decommissioningNode,omitempty"`

// LicenseStatus contains information about the current state of any
// installed license in the Redpanda cluster.
// +optional
LicenseStatus *RedpandaLicenseStatus `json:"license,omitempty"`
}

type RedpandaLicenseStatus struct {
Violation bool `json:"violation"`
InUseFeatures []string `json:"inUseFeatures"`
// +optional
Expired *bool `json:"expired,omitempty"`
// +optional
Type *string `json:"type,omitempty"`
// +optional
Organization *string `json:"organization,omitempty"`
// +optional
Expiration *metav1.Time `json:"expiration,omitempty"`
}

func (s *RedpandaLicenseStatus) String() string {
expired := "nil"
expiration := "nil"
if s.Expired != nil {
expired = strconv.FormatBool(*s.Expired)
}
if s.Expiration != nil {
expiration = s.Expiration.UTC().Format("Jan 2 2006 MST")
}

return fmt.Sprintf("License Status: Expired(%s), Expiration(%s), Features([%s])", expired, expiration, strings.Join(s.InUseFeatures, ", "))
}

type RemediationStrategy string
Expand Down
44 changes: 44 additions & 0 deletions operator/api/redpanda/v1alpha2/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

48 changes: 48 additions & 0 deletions operator/config/crd/bases/cluster.redpanda.com_redpandas.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9790,6 +9790,30 @@ spec:
reconcile request value, so a change of the annotation value
can be detected.
type: string
license:
description: |-
LicenseStatus contains information about the current state of any
installed license in the Redpanda cluster.
properties:
expiration:
format: date-time
type: string
expired:
type: boolean
inUseFeatures:
items:
type: string
type: array
organization:
type: string
type:
type: string
violation:
type: boolean
required:
- inUseFeatures
- violation
type: object
observedGeneration:
description: Specifies the last observed generation.
format: int64
Expand Down Expand Up @@ -19580,6 +19604,30 @@ spec:
reconcile request value, so a change of the annotation value
can be detected.
type: string
license:
description: |-
LicenseStatus contains information about the current state of any
installed license in the Redpanda cluster.
properties:
expiration:
format: date-time
type: string
expired:
type: boolean
inUseFeatures:
items:
type: string
type: array
organization:
type: string
type:
type: string
violation:
type: boolean
required:
- inUseFeatures
- violation
type: object
observedGeneration:
description: Specifies the last observed generation.
format: int64
Expand Down
51 changes: 51 additions & 0 deletions operator/internal/controller/redpanda/redpanda_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,22 @@ func (r *RedpandaReconciler) reconcileLicense(ctx context.Context, rp *v1alpha2.
return err
}

licenseInfo, err := client.GetLicenseInfo(ctx)
if err != nil {
if internalclient.IsTerminalClientError(err) {
apimeta.SetStatusCondition(rp.GetConditions(), metav1.Condition{
Type: v1alpha2.ClusterLicenseValid,
Status: metav1.ConditionUnknown,
ObservedGeneration: rp.Generation,
Reason: "TerminalError",
Message: err.Error(),
})

return nil
}
return err
}

var message string
var reason string
status := metav1.ConditionUnknown
Expand Down Expand Up @@ -516,6 +532,41 @@ func (r *RedpandaReconciler) reconcileLicense(ctx context.Context, rp *v1alpha2.
Message: message,
})

licenseStatus := func() *v1alpha2.RedpandaLicenseStatus {
inUseFeatures := []string{}
for _, feature := range features.Features {
if feature.Enabled {
inUseFeatures = append(inUseFeatures, feature.Name)
}
}

status := &v1alpha2.RedpandaLicenseStatus{
InUseFeatures: inUseFeatures,
Violation: features.Violation,
}

// make sure we can actually format the extend license properties
if !licenseInfo.Loaded {
return status
}

status.Organization = ptr.To(licenseInfo.Properties.Organization)
status.Type = ptr.To(licenseInfo.Properties.Type)
expirationTime := time.Unix(licenseInfo.Properties.Expires, 0)

// if we have an expiration that is below 0 we are already expired
// so no need to set the expiration time
status.Expired = ptr.To(licenseInfo.Properties.Expires <= 0 || expirationTime.Before(time.Now()))

if !*status.Expired {
status.Expiration = &metav1.Time{Time: expirationTime.UTC()}
}

return status
}

rp.Status.LicenseStatus = licenseStatus()

return nil
}

Expand Down
64 changes: 59 additions & 5 deletions operator/internal/controller/redpanda/redpanda_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -441,37 +441,62 @@ func (s *RedpandaControllerSuite) TestLicense() {
}

cases := []struct {
image image
license bool
expected string
image image
license bool
expected string
expectedLicenseStatus *redpandav1alpha2.RedpandaLicenseStatus
}{{
image: image{
repository: "redpandadata/redpanda-unstable",
tag: "v24.3.1-rc4",
},
license: false,
expected: "Expired",
expectedLicenseStatus: &redpandav1alpha2.RedpandaLicenseStatus{
Violation: false,
InUseFeatures: []string{},
Expired: ptr.To(true),
Type: ptr.To("free_trial"),
Organization: ptr.To("Redpanda Built-In Evaluation Period"),
},
}, {
image: image{
repository: "redpandadata/redpanda-unstable",
tag: "v24.3.1-rc4",
tag: "v24.3.1-rc8",
},
license: true,
expected: "Valid",
expectedLicenseStatus: &redpandav1alpha2.RedpandaLicenseStatus{
Violation: false,
InUseFeatures: []string{},
Expired: ptr.To(false),
// add a 30 day expiration, which is how we handle trial licenses
Expiration: &metav1.Time{Time: time.Now().Add(30 * 24 * time.Hour).UTC()},
Type: ptr.To("free_trial"),
Organization: ptr.To("Redpanda Built-In Evaluation Period"),
},
}, {
image: image{
repository: "redpandadata/redpanda",
tag: "v24.2.9",
},
license: false,
expected: "Not Present",
expectedLicenseStatus: &redpandav1alpha2.RedpandaLicenseStatus{
Violation: false,
InUseFeatures: []string{},
},
}, {
image: image{
repository: "redpandadata/redpanda",
tag: "v24.2.9",
},
license: true,
expected: "Not Present",
expectedLicenseStatus: &redpandav1alpha2.RedpandaLicenseStatus{
Violation: false,
InUseFeatures: []string{},
},
}}

for _, c := range cases {
Expand All @@ -492,6 +517,7 @@ func (s *RedpandaControllerSuite) TestLicense() {
}

var condition metav1.Condition
var licenseStatus *redpandav1alpha2.RedpandaLicenseStatus
s.applyAndWaitFor(func(o client.Object) bool {
rp := o.(*redpandav1alpha2.Redpanda)

Expand All @@ -500,6 +526,7 @@ func (s *RedpandaControllerSuite) TestLicense() {
// grab the first non-unknown status
if cond.Status != metav1.ConditionUnknown {
condition = cond
licenseStatus = rp.Status.LicenseStatus
return true
}
return false
Expand All @@ -512,6 +539,33 @@ func (s *RedpandaControllerSuite) TestLicense() {
message := fmt.Sprintf("%s - %s != %s", name, c.expected, condition.Message)
s.Require().Equal(c.expected, condition.Message, message)

if c.expectedLicenseStatus == nil && licenseStatus != nil {
s.T().Fatalf("%s does not have a nil license %s", name, licenseStatus.String())
}

if c.expectedLicenseStatus != nil {
s.Require().NotNil(licenseStatus, "%s does has a nil license", name)
s.Require().Equal(licenseStatus.Expired, c.expectedLicenseStatus.Expired, "%s license expired field does not match", name)
s.Require().EqualValues(licenseStatus.InUseFeatures, c.expectedLicenseStatus.InUseFeatures, "%s license valid features do not match", name)
s.Require().Equal(licenseStatus.Organization, c.expectedLicenseStatus.Organization, "%s license organization field does not match", name)
s.Require().Equal(licenseStatus.Type, c.expectedLicenseStatus.Type, "%s license type field does not match", name)
s.Require().Equal(licenseStatus.Violation, c.expectedLicenseStatus.Violation, "%s license violation field does not match", name)

// only do the expiration check if the license isn't already expired
if licenseStatus.Expired != nil && !*licenseStatus.Expired {
expectedExpiration := c.expectedLicenseStatus.Expiration.UTC()
actualExpiration := licenseStatus.Expiration.UTC()

rangeFactor := 5 * time.Minute
// add some fudge factor so that we don't fail with flakiness due to tests being run at
// the change of a couple of minutes that causes the date to be rolled over by some factor
if !(expectedExpiration.Add(rangeFactor).After(actualExpiration) &&
expectedExpiration.Add(-rangeFactor).Before(actualExpiration)) {
s.T().Fatalf("%s does not match expected expiration: %s != %s", name, actualExpiration, expectedExpiration)
}
}
}

s.deleteAndWait(rp)
}
}
Expand Down Expand Up @@ -749,7 +803,7 @@ func (s *RedpandaControllerSuite) randString(length int) string {
const alphabet = "abcdefghijklmnopqrstuvwxyz0123456789"

name := ""
for i := 0; i < 6; i++ {
for i := 0; i < length; i++ {
//nolint:gosec // not meant to be a secure random string.
name += string(alphabet[rand.Intn(len(alphabet))])
}
Expand Down

0 comments on commit 9a0d9e4

Please sign in to comment.