diff --git a/cmd/licensing-info/main.go b/cmd/licensing-info/main.go index 92f7dc6979..a04611d2a1 100644 --- a/cmd/licensing-info/main.go +++ b/cmd/licensing-info/main.go @@ -6,6 +6,7 @@ package main import ( "encoding/json" + "flag" "fmt" "log" @@ -25,7 +26,7 @@ import ( // // Example of use: // -// > go run cmd/licensing-info/main.go +// > go run cmd/licensing-info/main.go -operator-namespace // { // "timestamp": "2019-12-17T11:56:02+01:00", // "license_level": "basic", @@ -35,7 +36,10 @@ import ( // func main() { - licensingInfo, err := license.NewResourceReporter(newK8sClient()).Get() + var operatorNamespace string + flag.StringVar(&operatorNamespace, "operator-namespace", "elastic-system", "indicates the namespace where the operator is deployed") + flag.Parse() + licensingInfo, err := license.NewResourceReporter(newK8sClient(), operatorNamespace).Get() if err != nil { log.Fatal(err, "Failed to get licensing info") } diff --git a/cmd/manager/main.go b/cmd/manager/main.go index c4382c6e32..aa79f61ebe 100644 --- a/cmd/manager/main.go +++ b/cmd/manager/main.go @@ -375,8 +375,8 @@ func execute() { go func() { time.Sleep(10 * time.Second) // wait some arbitrary time for the manager to start mgr.GetCache().WaitForCacheSync(nil) // wait until k8s client cache is initialized - r := licensing.NewResourceReporter(mgr.GetClient()) - r.Start(operatorNamespace, licensing.ResourceReporterFrequency) + r := licensing.NewResourceReporter(mgr.GetClient(), operatorNamespace) + r.Start(licensing.ResourceReporterFrequency) }() log.Info("Starting the manager", "uuid", operatorInfo.OperatorUUID, diff --git a/docs/operating-eck/licensing.asciidoc b/docs/operating-eck/licensing.asciidoc index 254818727c..3a6cbb5d47 100644 --- a/docs/operating-eck/licensing.asciidoc +++ b/docs/operating-eck/licensing.asciidoc @@ -24,7 +24,7 @@ metadata: name: eck-trial-license namespace: elastic-system labels: - license.k8s.elastic.co/type: enterprise-trial + license.k8s.elastic.co/type: enterprise_trial annotations: elastic.co/eula: accepted <1> EOF @@ -38,7 +38,7 @@ At the end of the trial period, the Platinum and Enterprise features operate in [float] == Add a license -If you have a valid Enterprise subscription you will receive a license as a JSON file. +If you have a valid Enterprise subscription or a trial license extension, you will receive a license as a JSON file. NOTE: Please note that ECK will only accept Enterprise licenses. You can not apply a single Platinum or Gold cluster license to ECK. diff --git a/pkg/controller/common/license/check.go b/pkg/controller/common/license/check.go index 48d9586c6f..e8932e4f2c 100644 --- a/pkg/controller/common/license/check.go +++ b/pkg/controller/common/license/check.go @@ -37,7 +37,7 @@ func NewLicenseChecker(client k8s.Client, operatorNamespace string) Checker { } func (lc *checker) publicKeyFor(l EnterpriseLicense) ([]byte, error) { - if !l.IsTrial() { + if !l.IsECKManagedTrial() { return lc.publicKey, nil } var signatureSec corev1.Secret @@ -87,9 +87,6 @@ func (lc *checker) EnterpriseFeaturesEnabled() (bool, error) { // Valid returns true if the given Enterprise license is valid or an error if any. func (lc *checker) Valid(l EnterpriseLicense) (bool, error) { - if l.IsTrial() { - return true, nil - } pk, err := lc.publicKeyFor(l) if err != nil { return false, errors.Wrap(err, "while loading signature secret") diff --git a/pkg/controller/common/license/check_test.go b/pkg/controller/common/license/check_test.go index b2af7952e4..93774bdc3c 100644 --- a/pkg/controller/common/license/check_test.go +++ b/pkg/controller/common/license/check_test.go @@ -13,8 +13,11 @@ import ( "github.com/elastic/cloud-on-k8s/pkg/utils/k8s" "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" ) +const testNS = "test-system" + func TestChecker_EnterpriseFeaturesEnabled(t *testing.T) { privKey, err := x509.ParsePKCS1PrivateKey(privateKeyFixture) require.NoError(t, err) @@ -25,10 +28,29 @@ func TestChecker_EnterpriseFeaturesEnabled(t *testing.T) { signatureBytes, err := NewSigner(privKey).Sign(validLicenseFixture) require.NoError(t, err) + trialState, err := NewTrialState() + require.NoError(t, err) + validTrialLicenseFixture := emptyTrialLicenseFixture + require.NoError(t, trialState.InitTrialLicense(&validTrialLicenseFixture)) + + validLegacyTrialFixture := EnterpriseLicense{ + License: LicenseSpec{ + Type: LicenseTypeLegacyTrial, + }, + } + require.NoError(t, trialState.InitTrialLicense(&validLegacyTrialFixture)) + + expiredTrialLicense := validTrialLicenseFixture + expiredTrialLicense.License.ExpiryDateInMillis = chrono.ToMillis(time.Now().Add(-1 * time.Hour)) + expiredTrialSignatureBytes, err := NewSigner(trialState.privateKey).Sign(expiredTrialLicense) + require.NoError(t, err) + + statusSecret, err := ExpectedTrialStatus(testNS, types.NamespacedName{}, trialState) + require.NoError(t, err) + type fields struct { - initialObjects []runtime.Object - operatorNamespace string - publicKey []byte + initialObjects []runtime.Object + publicKey []byte } tests := []struct { name string @@ -39,18 +61,48 @@ func TestChecker_EnterpriseFeaturesEnabled(t *testing.T) { { name: "valid license: OK", fields: fields{ - initialObjects: asRuntimeObjects(validLicenseFixture, signatureBytes), - operatorNamespace: "test-system", - publicKey: publicKeyBytesFixture(t), + initialObjects: asRuntimeObjects(validLicenseFixture, signatureBytes), + publicKey: publicKeyBytesFixture(t), }, want: true, }, + { + name: "valid trial: OK", + fields: fields{ + initialObjects: []runtime.Object{asRuntimeObject(validTrialLicenseFixture), &statusSecret}, + }, + want: true, + wantErr: false, + }, + { + name: "valid legacy trial: OK", + fields: fields{ + initialObjects: []runtime.Object{asRuntimeObject(validTrialLicenseFixture), &statusSecret}, + }, + want: true, + wantErr: false, + }, + { + name: "invalid trial: FAIL", + fields: fields{ + initialObjects: []runtime.Object{asRuntimeObject(emptyTrialLicenseFixture), &statusSecret}, + }, + want: false, + wantErr: false, + }, + { + name: "expired trial: FAIL", + fields: fields{ + initialObjects: append(asRuntimeObjects(expiredTrialLicense, expiredTrialSignatureBytes), &statusSecret), + }, + want: false, + wantErr: false, + }, { name: "invalid signature: FAIL", fields: fields{ - initialObjects: asRuntimeObjects(validLicenseFixture, []byte{}), - operatorNamespace: "test-system", - publicKey: publicKeyBytesFixture(t), + initialObjects: asRuntimeObjects(validLicenseFixture, []byte{}), + publicKey: publicKeyBytesFixture(t), }, want: false, wantErr: false, @@ -58,8 +110,7 @@ func TestChecker_EnterpriseFeaturesEnabled(t *testing.T) { { name: "no public key: FAIL", fields: fields{ - initialObjects: asRuntimeObjects(validLicenseFixture, signatureBytes), - operatorNamespace: "test-system", + initialObjects: asRuntimeObjects(validLicenseFixture, signatureBytes), }, want: false, wantErr: true, @@ -69,7 +120,7 @@ func TestChecker_EnterpriseFeaturesEnabled(t *testing.T) { t.Run(tt.name, func(t *testing.T) { lc := &checker{ k8sClient: k8s.WrappedFakeClient(tt.fields.initialObjects...), - operatorNamespace: tt.fields.operatorNamespace, + operatorNamespace: testNS, publicKey: tt.fields.publicKey, } got, err := lc.EnterpriseFeaturesEnabled() @@ -93,11 +144,14 @@ func Test_CurrentEnterpriseLicense(t *testing.T) { require.NoError(t, err) validLicense := asRuntimeObjects(validLicenseFixture, signatureBytes) - validTrialLicenseFixture := trialLicenseFixture - validTrialLicenseFixture.License.ExpiryDateInMillis = chrono.ToMillis(time.Now().Add(1 * time.Hour)) - trialSignatureBytes, err := NewSigner(privKey).Sign(validTrialLicenseFixture) + trialState, err := NewTrialState() + require.NoError(t, err) + validTrialLicenseFixture := emptyTrialLicenseFixture + require.NoError(t, trialState.InitTrialLicense(&validTrialLicenseFixture)) + validTrialLicense := asRuntimeObject(validTrialLicenseFixture) + + statusSecret, err := ExpectedTrialStatus(testNS, types.NamespacedName{}, trialState) require.NoError(t, err) - validTrialLicense := asRuntimeObjects(validTrialLicenseFixture, trialSignatureBytes) type fields struct { initialObjects []runtime.Object @@ -126,7 +180,7 @@ func Test_CurrentEnterpriseLicense(t *testing.T) { { name: "get valid trial enterprise license: OK", fields: fields{ - initialObjects: validTrialLicense, + initialObjects: []runtime.Object{validTrialLicense, &statusSecret}, operatorNamespace: "test-system", publicKey: publicKeyBytesFixture(t), }, @@ -137,7 +191,7 @@ func Test_CurrentEnterpriseLicense(t *testing.T) { { name: "get valid enterprise license among two licenses: OK", fields: fields{ - initialObjects: append(validLicense, validTrialLicense...), + initialObjects: append(validLicense, validTrialLicense), operatorNamespace: "test-system", publicKey: publicKeyBytesFixture(t), }, diff --git a/pkg/controller/common/license/crud.go b/pkg/controller/common/license/crud.go index 84cedf4703..77806f8efd 100644 --- a/pkg/controller/common/license/crud.go +++ b/pkg/controller/common/license/crud.go @@ -71,11 +71,11 @@ func TrialLicense(c k8s.Client, nsn types.NamespacedName) (corev1.Secret, Enterp } // CreateTrialLicense creates en empty secret with the correct meta data to start an enterprise trial -func CreateTrialLicense(c k8s.Client, namespace string) error { +func CreateTrialLicense(c k8s.Client, nsn types.NamespacedName) error { return c.Create(&corev1.Secret{ ObjectMeta: v1.ObjectMeta{ - Name: string(LicenseTypeEnterpriseTrial), - Namespace: namespace, + Name: nsn.Name, + Namespace: nsn.Namespace, Labels: map[string]string{ common.TypeLabelName: Type, LicenseLabelType: string(LicenseTypeEnterpriseTrial), diff --git a/pkg/controller/common/license/detection.go b/pkg/controller/common/license/detection.go index fd537114bf..611e134792 100644 --- a/pkg/controller/common/license/detection.go +++ b/pkg/controller/common/license/detection.go @@ -21,13 +21,11 @@ func isLicenseType(secret corev1.Secret, licenseType OperatorLicenseType) bool { // IsEnterpriseTrial returns true if the given secret is a wrapper for an Enterprise Trial license func IsEnterpriseTrial(secret corev1.Secret) bool { - return isLicenseType(secret, LicenseTypeEnterpriseTrial) -} - -func IsEnterpriseLicense(secret corev1.Secret) bool { - return isLicenseType(secret, LicenseTypeEnterprise) + // we need to support legacy trial license secrets for backwards compatibility + return isLicenseType(secret, LicenseTypeEnterpriseTrial) || isLicenseType(secret, LicenseTypeLegacyTrial) } +// IsOperatorLicense returns true if the given secret is a wrapper for an operator license. func IsOperatorLicense(secret corev1.Secret) bool { scope, hasLabel := secret.Labels[LicenseLabelScope] return hasLabel && scope == string(LicenseScopeOperator) diff --git a/pkg/controller/common/license/fixtures_test.go b/pkg/controller/common/license/fixtures_test.go index b27d007ec9..2cd423c10f 100644 --- a/pkg/controller/common/license/fixtures_test.go +++ b/pkg/controller/common/license/fixtures_test.go @@ -49,7 +49,7 @@ var ( } ) -var trialLicenseFixture = EnterpriseLicense{ +var emptyTrialLicenseFixture = EnterpriseLicense{ License: LicenseSpec{ Type: LicenseTypeEnterpriseTrial, }, @@ -60,27 +60,31 @@ func withSignature(l EnterpriseLicense, sig []byte) EnterpriseLicense { return l } -func asRuntimeObjects(l EnterpriseLicense, sig []byte) []runtime.Object { - bytes, err := json.Marshal(withSignature(l, sig)) +func asRuntimeObject(l EnterpriseLicense) runtime.Object { + bytes, err := json.Marshal(l) if err != nil { panic(err) } - return []runtime.Object{ - &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "test-system", - Name: fmt.Sprintf("test-%s-license", string(l.License.Type)), - Labels: map[string]string{ - common.TypeLabelName: Type, - LicenseLabelScope: string(LicenseScopeOperator), - LicenseLabelType: string(l.License.Type), - }, - }, - Data: map[string][]byte{ - FileName: bytes, + return &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test-system", + Name: fmt.Sprintf("test-%s-license", string(l.License.Type)), + Labels: map[string]string{ + common.TypeLabelName: Type, + LicenseLabelScope: string(LicenseScopeOperator), + LicenseLabelType: string(l.License.Type), }, }, + Data: map[string][]byte{ + FileName: bytes, + }, + } +} + +func asRuntimeObjects(l EnterpriseLicense, sig []byte) []runtime.Object { + return []runtime.Object{ + asRuntimeObject(withSignature(l, sig)), } } diff --git a/pkg/controller/common/license/model.go b/pkg/controller/common/license/model.go index aaf2e70a0d..d9c8f54f8d 100644 --- a/pkg/controller/common/license/model.go +++ b/pkg/controller/common/license/model.go @@ -16,7 +16,9 @@ type OperatorLicenseType string const ( LicenseTypeEnterprise OperatorLicenseType = "enterprise" - LicenseTypeEnterpriseTrial OperatorLicenseType = "enterprise-trial" + LicenseTypeEnterpriseTrial OperatorLicenseType = "enterprise_trial" + // LicenseTypeLegacyTrial earlier versions of ECK used this as the trial identifier + LicenseTypeLegacyTrial OperatorLicenseType = "enterprise-trial" ) type ElasticsearchLicense struct { @@ -47,8 +49,9 @@ type LicenseSpec struct { // EnterpriseLicenseTypeOrder license types mapped to ints in increasing order of feature sets for sorting purposes. var EnterpriseLicenseTypeOrder = map[OperatorLicenseType]int{ - LicenseTypeEnterpriseTrial: 0, - LicenseTypeEnterprise: 1, + LicenseTypeLegacyTrial: 0, + LicenseTypeEnterpriseTrial: 1, + LicenseTypeEnterprise: 2, } // StartTime is the date as of which this license is valid. @@ -69,7 +72,12 @@ func (l EnterpriseLicense) IsValid(instant time.Time) bool { // IsTrial returns true if this is a self-generated trial license. func (l EnterpriseLicense) IsTrial() bool { - return l.License.Type == LicenseTypeEnterpriseTrial + return l.License.Type == LicenseTypeEnterpriseTrial || l.License.Type == LicenseTypeLegacyTrial +} + +// IsECKManagedTrial returns true if this license has been issued by ECK or if this is an empty license that ECK can fill in. +func (l EnterpriseLicense) IsECKManagedTrial() bool { + return l.License.Issuer == ECKLicenseIssuer || l.License.Issuer == "" } // IsMissingFields returns an error if any of the required fields are missing. Expected state on trial licenses. diff --git a/pkg/controller/common/license/model_test.go b/pkg/controller/common/license/model_test.go index 5cb56a20f6..17588ff98b 100644 --- a/pkg/controller/common/license/model_test.go +++ b/pkg/controller/common/license/model_test.go @@ -136,6 +136,48 @@ var expectedLicenseSpec = EnterpriseLicense{ }, } +var expectedLicenseSpecV4 = EnterpriseLicense{ + License: LicenseSpec{ + UID: "840F0DB6-1906-452E-98C7-6F94E6012CD7", + IssueDateInMillis: 1548115200000, + ExpiryDateInMillis: 1561247999999, + IssuedTo: "test org", + Issuer: "test issuer", + StartDateInMillis: 1548115200000, + Type: "enterprise", + MaxResourceUnits: 20, + Signature: "test signature", + ClusterLicenses: []ElasticsearchLicense{ + { + License: client.License{ + UID: "73117B2A-FEEA-4FEC-B8F6-49D764E9F1DA", + IssueDateInMillis: 1548115200000, + ExpiryDateInMillis: 1561247999999, + IssuedTo: "test org", + Issuer: "test issuer", + StartDateInMillis: 1548115200000, + MaxNodes: 100, + Type: "platinum", + Signature: "test signature platinum", + }, + }, + { + License: client.License{ + UID: "57E312E2-6EA0-49D0-8E65-AA5017742ACF", + IssueDateInMillis: 1548115200000, + ExpiryDateInMillis: 1561247999999, + IssuedTo: "test org", + Issuer: "test issuer", + StartDateInMillis: 1548115200000, + MaxResourceUnits: 50, + Type: "enterprise", + Signature: "test signature enterprise", + }, + }, + }, + }, +} + func Test_unmarshalModel(t *testing.T) { _ = controllerscheme.SetupScheme() type args struct { @@ -155,7 +197,7 @@ func Test_unmarshalModel(t *testing.T) { wantErr: true, }, { - name: "valid input: OK", + name: "valid input: license v3 OK", args: args{ licenseFile: "testdata/test-license.json", }, @@ -166,6 +208,18 @@ func Test_unmarshalModel(t *testing.T) { } }, }, + { + name: "valid input: license v4 OK", + args: args{ + licenseFile: "testdata/test-license-v4.json", + }, + wantErr: false, + assertion: func(el EnterpriseLicense) { + if diff := deep.Equal(el, expectedLicenseSpecV4); diff != nil { + t.Error(diff) + } + }, + }, } for _, tt := range tests { diff --git a/pkg/controller/common/license/testdata/test-license-v4.json b/pkg/controller/common/license/testdata/test-license-v4.json new file mode 100644 index 0000000000..e6d48dc629 --- /dev/null +++ b/pkg/controller/common/license/testdata/test-license-v4.json @@ -0,0 +1,41 @@ +{ + "license": { + "uid": "840F0DB6-1906-452E-98C7-6F94E6012CD7", + "type": "enterprise", + "issue_date_in_millis": 1548115200000, + "start_date_in_millis": 1548115200000, + "expiry_date_in_millis": 1561247999999, + "max_resource_units": 20, + "issued_to": "test org", + "issuer": "test issuer", + "signature": "test signature", + "cluster_licenses": [ + { + "license": { + "uid": "73117B2A-FEEA-4FEC-B8F6-49D764E9F1DA", + "type": "platinum", + "issue_date_in_millis": 1548115200000, + "expiry_date_in_millis": 1561247999999, + "max_nodes": 100, + "issued_to": "test org", + "issuer": "test issuer", + "signature": "test signature platinum", + "start_date_in_millis": 1548115200000 + } + }, + { + "license": { + "uid": "57E312E2-6EA0-49D0-8E65-AA5017742ACF", + "type": "enterprise", + "issue_date_in_millis": 1548115200000, + "expiry_date_in_millis": 1561247999999, + "max_resource_units": 50, + "issued_to": "test org", + "issuer": "test issuer", + "signature": "test signature enterprise", + "start_date_in_millis": 1548115200000 + } + } + ] + } +} \ No newline at end of file diff --git a/pkg/controller/common/license/trial.go b/pkg/controller/common/license/trial.go index 58e5eb4d37..ffecfc2297 100644 --- a/pkg/controller/common/license/trial.go +++ b/pkg/controller/common/license/trial.go @@ -21,6 +21,8 @@ import ( ) const ( + ECKLicenseIssuer = "Elastic k8s operator" + TrialStatusSecretKey = "trial-status" TrialPubkeyKey = "pubkey" TrialActivationKey = "in-trial-activation" @@ -152,7 +154,7 @@ func populateTrialLicense(l *EnterpriseLicense) error { return pkgerrors.Errorf("%s for %s is not a trial license", l.License.UID, l.License.IssuedTo) } if l.License.Issuer == "" { - l.License.Issuer = "Elastic k8s operator" + l.License.Issuer = ECKLicenseIssuer } if l.License.IssuedTo == "" { l.License.IssuedTo = "Unknown" diff --git a/pkg/controller/common/license/trial_test.go b/pkg/controller/common/license/trial_test.go index 9937afef1b..ce3f6649d2 100644 --- a/pkg/controller/common/license/trial_test.go +++ b/pkg/controller/common/license/trial_test.go @@ -122,6 +122,22 @@ func TestPopulateTrialLicense(t *testing.T) { }, wantErr: false, }, + { + // technically this code path should not be possible: we use the new type when creating a new trial + // and we don't repopulate existing licenses + name: "legacy trial still supported", + args: args{ + l: &EnterpriseLicense{ + License: LicenseSpec{ + Type: LicenseTypeLegacyTrial, + }, + }, + }, + assertions: func(l EnterpriseLicense) { + require.NoError(t, l.IsMissingFields()) + }, + wantErr: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/pkg/controller/common/license/verifier_test.go b/pkg/controller/common/license/verifier_test.go index fea82186f2..4e73e4b66c 100644 --- a/pkg/controller/common/license/verifier_test.go +++ b/pkg/controller/common/license/verifier_test.go @@ -57,6 +57,15 @@ func TestLicenseVerifier_ValidSignature(t *testing.T) { }, wantErr: true, }, + { + name: "empty signature", + args: licenseFixtureV4, + verifyInput: func(l EnterpriseLicense) EnterpriseLicense { + l.License.Signature = "" + return l + }, + wantErr: true, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/pkg/controller/license/trial/trial_controller.go b/pkg/controller/license/trial/trial_controller.go index c84e0ce219..1e3d6cb938 100644 --- a/pkg/controller/license/trial/trial_controller.go +++ b/pkg/controller/license/trial/trial_controller.go @@ -73,6 +73,11 @@ func (r *ReconcileTrials) Reconcile(request reconcile.Request) (reconcile.Result return reconcile.Result{}, pkgerrors.Wrap(err, "while fetching trial license") } + if !license.IsECKManagedTrial() { + // ignore externally generated licenses + return reconcile.Result{}, nil + } + validationMsg := validateEULA(secret) if validationMsg != "" { return r.invalidOperation(secret, validationMsg) diff --git a/pkg/controller/license/trial/trial_controller_integration_test.go b/pkg/controller/license/trial/trial_controller_integration_test.go index 4cdc32da7a..39ddc5c0fc 100644 --- a/pkg/controller/license/trial/trial_controller_integration_test.go +++ b/pkg/controller/license/trial/trial_controller_integration_test.go @@ -25,6 +25,11 @@ import ( const operatorNs = "elastic-system" +var testLicenseNSN = types.NamespacedName{ + Namespace: operatorNs, + Name: "eck-trial-license", +} + func TestMain(m *testing.M) { test.RunWithK8s(m) } @@ -36,7 +41,7 @@ func TestReconcile(t *testing.T) { now := time.Now() // Create trial initialisation is controlled via config - require.NoError(t, license.CreateTrialLicense(c, operatorNs)) + require.NoError(t, license.CreateTrialLicense(c, testLicenseNSN)) checker := license.NewLicenseChecker(c, operatorNs) // test trial initialisation on create validateTrialStatus(t, checker, true) @@ -54,9 +59,12 @@ func TestReconcile(t *testing.T) { Namespace: operatorNs, Name: license.TrialStatusSecretKey, } - require.NoError(t, c.Get(trialStatusKey, &trialStatus)) - trialStatus.Data[license.TrialPubkeyKey] = []byte("foobar") - require.NoError(t, c.Update(&trialStatus)) + // retry in case of edit conflict with reconciliation loop + test.RetryUntilSuccess(t, func() error { + require.NoError(t, c.Get(trialStatusKey, &trialStatus)) + trialStatus.Data[license.TrialPubkeyKey] = []byte("foobar") + return c.Update(&trialStatus) + }) test.RetryUntilSuccess(t, func() error { require.NoError(t, c.Get(trialStatusKey, &trialStatus)) if bytes.Equal(trialStatus.Data[license.TrialPubkeyKey], []byte("foobar")) { @@ -66,9 +74,9 @@ func TestReconcile(t *testing.T) { }) // Delete the trial license - require.NoError(t, deleteTrial(c, string(license.LicenseTypeEnterpriseTrial))) + require.NoError(t, deleteTrial(c)) // recreate it with modified validity + 1 year - license.CreateTrialLicense(c, operatorNs) + require.NoError(t, license.CreateTrialLicense(c, testLicenseNSN)) // expect an invalid license validateTrialStatus(t, checker, false) // ClusterLicense should be GC'ed but can't be tested here @@ -95,9 +103,9 @@ func validateTrialDuration(t *testing.T, license license.EnterpriseLicense, now assert.True(t, endDelta <= precision, "end date should be within %v, but was %v", precision, endDelta) } -func deleteTrial(c k8s.Client, name string) error { +func deleteTrial(c k8s.Client) error { var trialLicense corev1.Secret - if err := c.Get(types.NamespacedName{Namespace: operatorNs, Name: name}, &trialLicense); err != nil { + if err := c.Get(testLicenseNSN, &trialLicense); err != nil { return err } return c.Delete(&trialLicense) diff --git a/pkg/controller/license/trial/trial_controller_test.go b/pkg/controller/license/trial/trial_controller_test.go index 4382d21e91..73ac0b83ee 100644 --- a/pkg/controller/license/trial/trial_controller_test.go +++ b/pkg/controller/license/trial/trial_controller_test.go @@ -7,6 +7,7 @@ package trial import ( "encoding/json" "fmt" + "strings" "testing" "time" @@ -57,7 +58,7 @@ func trialStatusSecretSample(t *testing.T, state licensing.TrialState) *corev1.S func trialLicenseBytes() []byte { return []byte(fmt.Sprintf( - `{"license": {"uid": "x", "type": "enterprise-trial", "issue_date_in_millis": 1, "expiry_date_in_millis": %d, "issued_to": "x", "issuer": "x", "start_date_in_millis": 1, "cluster_licenses": null, "Version": 0}}`, + `{"license": {"uid": "x", "type": "enterprise_trial", "issue_date_in_millis": 1, "expiry_date_in_millis": %d, "issued_to": "x", "issuer": "Elastic k8s operator", "start_date_in_millis": 1, "cluster_licenses": null, "Version": 0}}`, chrono.ToMillis(time.Now().Add(24*time.Hour)), // simulate a license still valid for 24 hours )) } @@ -261,6 +262,16 @@ func TestReconcileTrials_Reconcile(t *testing.T) { wantErr: false, assertions: requireValidationMsg("trial license signature invalid"), }, + { + name: "externally generated licenses are ignored", + fields: fields{ + Client: k8s.WrappedFakeClient(trialLicenseSecretSample(true, map[string][]byte{ + "license": []byte(strings.ReplaceAll(string(trialLicenseBytes()), licensing.ECKLicenseIssuer, "Some other issuer")), + })), + }, + wantErr: false, + assertions: requireNoValidationMsg, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/pkg/license/license.go b/pkg/license/license.go index d24ccfd800..d3edbbd30f 100644 --- a/pkg/license/license.go +++ b/pkg/license/license.go @@ -73,16 +73,16 @@ func (r LicensingResolver) ToInfo(totalMemory resource.Quantity) (LicensingInfo, } // Save updates or creates licensing information in a config map -func (r LicensingResolver) Save(info LicensingInfo, operatorNs string) error { +func (r LicensingResolver) Save(info LicensingInfo) error { data, err := info.toMap() if err != nil { return err } - log.V(1).Info("Saving", "namespace", operatorNs, "configmap_name", licensingCfgMapName, "license_info", info) + log.V(1).Info("Saving", "namespace", r.operatorNs, "configmap_name", licensingCfgMapName, "license_info", info) cm := corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ - Namespace: operatorNs, + Namespace: r.operatorNs, Name: licensingCfgMapName, Labels: map[string]string{ common.TypeLabelName: Type, diff --git a/pkg/license/reporter.go b/pkg/license/reporter.go index bbe9e8adca..f930761ff3 100644 --- a/pkg/license/reporter.go +++ b/pkg/license/reporter.go @@ -25,29 +25,30 @@ type ResourceReporter struct { } // NewResourceReporter returns a new ResourceReporter -func NewResourceReporter(client client.Client) ResourceReporter { +func NewResourceReporter(client client.Client, operatorNs string) ResourceReporter { c := k8s.WrapClient(client) return ResourceReporter{ aggregator: Aggregator{ client: c, }, licensingResolver: LicensingResolver{ - client: c, + client: c, + operatorNs: operatorNs, }, } } // Start starts to report the licensing information repeatedly at regular intervals -func (r ResourceReporter) Start(operatorNs string, refreshPeriod time.Duration) { +func (r ResourceReporter) Start(refreshPeriod time.Duration) { // report once as soon as possible to not wait the first tick - err := r.Report(operatorNs) + err := r.Report() if err != nil { log.Error(err, "Failed to report licensing information") } ticker := time.NewTicker(refreshPeriod) for range ticker.C { - err := r.Report(operatorNs) + err := r.Report() if err != nil { log.Error(err, "Failed to report licensing information") } @@ -55,13 +56,13 @@ func (r ResourceReporter) Start(operatorNs string, refreshPeriod time.Duration) } // Report reports the licensing information in a config map -func (r ResourceReporter) Report(operatorNs string) error { +func (r ResourceReporter) Report() error { licensingInfo, err := r.Get() if err != nil { return err } - return r.licensingResolver.Save(licensingInfo, operatorNs) + return r.licensingResolver.Save(licensingInfo) } // Get aggregates managed resources and returns the licensing information diff --git a/pkg/license/reporter_test.go b/pkg/license/reporter_test.go index 0cc7b665bd..f6b6cb2dd9 100644 --- a/pkg/license/reporter_test.go +++ b/pkg/license/reporter_test.go @@ -12,16 +12,21 @@ import ( apmv1 "github.com/elastic/cloud-on-k8s/pkg/apis/apm/v1" esv1 "github.com/elastic/cloud-on-k8s/pkg/apis/elasticsearch/v1" kbv1 "github.com/elastic/cloud-on-k8s/pkg/apis/kibana/v1" + commonlicense "github.com/elastic/cloud-on-k8s/pkg/controller/common/license" essettings "github.com/elastic/cloud-on-k8s/pkg/controller/elasticsearch/settings" kbconfig "github.com/elastic/cloud-on-k8s/pkg/controller/kibana/config" "github.com/elastic/cloud-on-k8s/pkg/utils/k8s" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" ) +const operatorNs = "test-system" + func Test_Get(t *testing.T) { es := esv1.Elasticsearch{ Spec: esv1.ElasticsearchSpec{ @@ -30,7 +35,7 @@ func Test_Get(t *testing.T) { }}, }, } - licensingInfo, err := NewResourceReporter(k8s.FakeClient(&es)).Get() + licensingInfo, err := NewResourceReporter(k8s.FakeClient(&es), operatorNs).Get() assert.NoError(t, err) assert.Equal(t, "21.47GB", licensingInfo.TotalManagedMemory) assert.Equal(t, "1", licensingInfo.EnterpriseResourceUnits) @@ -56,7 +61,7 @@ func Test_Get(t *testing.T) { }}, }, } - licensingInfo, err = NewResourceReporter(k8s.FakeClient(&es)).Get() + licensingInfo, err = NewResourceReporter(k8s.FakeClient(&es), operatorNs).Get() assert.NoError(t, err) assert.Equal(t, "644.25GB", licensingInfo.TotalManagedMemory) assert.Equal(t, "11", licensingInfo.EnterpriseResourceUnits) @@ -80,7 +85,7 @@ func Test_Get(t *testing.T) { }}, }, } - licensingInfo, err = NewResourceReporter(k8s.FakeClient(&es)).Get() + licensingInfo, err = NewResourceReporter(k8s.FakeClient(&es), operatorNs).Get() assert.NoError(t, err) assert.Equal(t, "171.80GB", licensingInfo.TotalManagedMemory) assert.Equal(t, "3", licensingInfo.EnterpriseResourceUnits) @@ -104,7 +109,7 @@ func Test_Get(t *testing.T) { }}, }, } - licensingInfo, err = NewResourceReporter(k8s.FakeClient(&es)).Get() + licensingInfo, err = NewResourceReporter(k8s.FakeClient(&es), operatorNs).Get() assert.NoError(t, err) assert.Equal(t, "171.80GB", licensingInfo.TotalManagedMemory) assert.Equal(t, "3", licensingInfo.EnterpriseResourceUnits) @@ -114,7 +119,7 @@ func Test_Get(t *testing.T) { Count: 100, }, } - licensingInfo, err = NewResourceReporter(k8s.FakeClient(&kb)).Get() + licensingInfo, err = NewResourceReporter(k8s.FakeClient(&kb), operatorNs).Get() assert.NoError(t, err) assert.Equal(t, "107.37GB", licensingInfo.TotalManagedMemory) assert.Equal(t, "2", licensingInfo.EnterpriseResourceUnits) @@ -138,7 +143,7 @@ func Test_Get(t *testing.T) { }, }, } - licensingInfo, err = NewResourceReporter(k8s.FakeClient(&kb)).Get() + licensingInfo, err = NewResourceReporter(k8s.FakeClient(&kb), operatorNs).Get() assert.NoError(t, err) assert.Equal(t, "214.75GB", licensingInfo.TotalManagedMemory) assert.Equal(t, "4", licensingInfo.EnterpriseResourceUnits) @@ -160,7 +165,7 @@ func Test_Get(t *testing.T) { }, }, } - licensingInfo, err = NewResourceReporter(k8s.FakeClient(&kb)).Get() + licensingInfo, err = NewResourceReporter(k8s.FakeClient(&kb), operatorNs).Get() assert.NoError(t, err) assert.Equal(t, "204.80GB", licensingInfo.TotalManagedMemory) assert.Equal(t, "4", licensingInfo.EnterpriseResourceUnits) @@ -175,13 +180,12 @@ func Test_Start(t *testing.T) { kb := kbv1.Kibana{Spec: kbv1.KibanaSpec{Count: 2}} apm := apmv1.ApmServer{Spec: apmv1.ApmServerSpec{Count: 2}} k8sClient := k8s.FakeClient(&es, &kb, &apm) - operatorNs := "test-system" refreshPeriod := 1 * time.Second waitFor := 10 * refreshPeriod tick := refreshPeriod / 2 // start the resource reporter - go NewResourceReporter(k8sClient).Start(operatorNs, refreshPeriod) + go NewResourceReporter(k8sClient, operatorNs).Start(refreshPeriod) // check that the licensing config map exists assert.Eventually(t, func() bool { @@ -219,4 +223,46 @@ func Test_Start(t *testing.T) { cm.Data["enterprise_resource_units"] == "3" && cm.Data["total_managed_memory"] == "175.02GB" }, waitFor, tick) + + startTrial(t, k8sClient) + // check that the license level has been updated + assert.Eventually(t, func() bool { + var cm corev1.ConfigMap + err := k8sClient.Get(context.Background(), types.NamespacedName{ + Namespace: operatorNs, + Name: licensingCfgMapName, + }, &cm) + if err != nil { + return false + } + return cm.Data["timestamp"] != "" && + cm.Data["eck_license_level"] == string(commonlicense.LicenseTypeEnterpriseTrial) && + cm.Data["enterprise_resource_units"] == "3" && + cm.Data["total_managed_memory"] == "175.02GB" + }, waitFor, tick) + +} + +func startTrial(t *testing.T, k8sClient client.Client) { + // start a trial + trialState, err := commonlicense.NewTrialState() + require.NoError(t, err) + wrappedClient := k8s.WrapClient(k8sClient) + licenseNSN := types.NamespacedName{ + Namespace: operatorNs, + Name: "eck-trial", + } + // simulate user kicking off the trial activation + require.NoError(t, commonlicense.CreateTrialLicense(wrappedClient, licenseNSN)) + // fetch user created license + licenseSecret, license, err := commonlicense.TrialLicense(wrappedClient, licenseNSN) + require.NoError(t, err) + // fill in and sign + require.NoError(t, trialState.InitTrialLicense(&license)) + status, err := commonlicense.ExpectedTrialStatus(operatorNs, licenseNSN, trialState) + require.NoError(t, err) + // persist status + require.NoError(t, wrappedClient.Create(&status)) + // persist updated license + require.NoError(t, commonlicense.UpdateEnterpriseLicense(wrappedClient, licenseSecret, license)) }