diff --git a/pkg/controller/common/license/labels.go b/pkg/controller/common/license/labels.go index 41b68042b0..6fc42206f7 100644 --- a/pkg/controller/common/license/labels.go +++ b/pkg/controller/common/license/labels.go @@ -11,12 +11,11 @@ import ( const ( // LicenseLabelName is a label pointing to the name of the source enterprise license. - LicenseLabelName = "license.k8s.elastic.co/name" - LicenseLabelType = "license.k8s.elastic.co/type" - Type = "license" - EULAAnnotation = "elastic.co/eula" - EULAAcceptedValue = "accepted" - LicenseInvalidAnnotation = "license.k8s.elastic.co/invalid" + LicenseLabelName = "license.k8s.elastic.co/name" + LicenseLabelType = "license.k8s.elastic.co/type" + Type = "license" + EULAAnnotation = "elastic.co/eula" + EULAAcceptedValue = "accepted" ) // LicenseType is the type of license a resource is describing. diff --git a/pkg/controller/common/license/match.go b/pkg/controller/common/license/match.go index bdd0867f60..e821f2a462 100644 --- a/pkg/controller/common/license/match.go +++ b/pkg/controller/common/license/match.go @@ -68,6 +68,19 @@ func filterValid(now time.Time, licenses []EnterpriseLicense, filter func(Enterp if !ok { continue } + + // Shortcut if it's a trial license + if el.IsTrial() { + return []licenseWithTimeLeft{{ + // For a trial, only the type is used, the license will be generated by ES + license: client.License{ + Type: string(ElasticsearchLicenseTypeTrial), + }, + parentUID: el.License.UID, + remaining: el.ExpiryTime().Sub(now), + }} + } + for _, l := range el.License.ClusterLicenses { if l.License.IsValid(now) { filtered = append(filtered, licenseWithTimeLeft{ diff --git a/pkg/controller/common/license/trial.go b/pkg/controller/common/license/trial.go index 0eb6b42487..90b4192b42 100644 --- a/pkg/controller/common/license/trial.go +++ b/pkg/controller/common/license/trial.go @@ -23,9 +23,12 @@ import ( const ( TrialStatusSecretKey = "trial-status" TrialPubkeyKey = "pubkey" + + TrialLicenseSecretName = "trial.k8s.elastic.co/secret-name" // nolint + TrialLicenseSecretNamespace = "trial.k8s.elastic.co/secret-namespace" // nolint ) -func InitTrial(c k8s.Client, secret corev1.Secret, l *EnterpriseLicense) (*rsa.PublicKey, error) { +func InitTrial(c k8s.Client, operatorNamespace string, secret corev1.Secret, l *EnterpriseLicense) (*rsa.PublicKey, error) { if l == nil { return nil, errors.New("license is nil") } @@ -51,11 +54,15 @@ func InitTrial(c k8s.Client, secret corev1.Secret, l *EnterpriseLicense) (*rsa.P } trialStatus := corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ - Namespace: secret.Namespace, + Namespace: operatorNamespace, Name: TrialStatusSecretKey, Labels: map[string]string{ LicenseLabelName: l.License.UID, }, + Annotations: map[string]string{ + TrialLicenseSecretName: secret.Name, + TrialLicenseSecretNamespace: secret.Namespace, + }, }, Data: map[string][]byte{ TrialPubkeyKey: pubkeyBytes, diff --git a/pkg/controller/common/license/trial_test.go b/pkg/controller/common/license/trial_test.go index 22ef7d5193..f125766b3e 100644 --- a/pkg/controller/common/license/trial_test.go +++ b/pkg/controller/common/license/trial_test.go @@ -101,6 +101,7 @@ func TestInitTrial(t *testing.T) { t.Run(tt.name, func(t *testing.T) { got, err := InitTrial( tt.args.c, + "elastic-system", corev1.Secret{ ObjectMeta: v1.ObjectMeta{ Namespace: "elastic-system", diff --git a/pkg/controller/elasticsearch/client/client.go b/pkg/controller/elasticsearch/client/client.go index 3d3f4c7ef4..fd71ee8b65 100644 --- a/pkg/controller/elasticsearch/client/client.go +++ b/pkg/controller/elasticsearch/client/client.go @@ -85,6 +85,8 @@ type Client interface { GetNodesStats(ctx context.Context) (NodesStats, error) // GetLicense returns the currently applied license. Can be empty. GetLicense(ctx context.Context) (License, error) + // StartTrial starts a 30-day trial period (which gives access to platinum features). + StartTrial(ctx context.Context) (StartTrialResponse, error) // UpdateLicense attempts to update cluster license with the given licenses. UpdateLicense(ctx context.Context, licenses LicenseUpdateRequest) (LicenseUpdateResponse, error) // AddVotingConfigExclusions sets the transient and persistent setting of the same name in cluster settings. diff --git a/pkg/controller/elasticsearch/client/model.go b/pkg/controller/elasticsearch/client/model.go index 70f101bc41..62139c054d 100644 --- a/pkg/controller/elasticsearch/client/model.go +++ b/pkg/controller/elasticsearch/client/model.go @@ -280,6 +280,17 @@ func (lr LicenseUpdateResponse) IsSuccess() bool { return lr.LicenseStatus == "valid" } +// StartTrialResponse is the response to the start trial API call. +type StartTrialResponse struct { + Acknowledged bool `json:"acknowledged"` + TrialWasStarted bool `json:"trial_was_started"` + ErrorMessage string `json:"error_message"` +} + +func (sr StartTrialResponse) IsSuccess() bool { + return sr.Acknowledged && sr.TrialWasStarted +} + // LicenseResponse is the response to GET _xpack/license. Licenses won't contain signature. type LicenseResponse struct { License License `json:"license"` diff --git a/pkg/controller/elasticsearch/client/v6.go b/pkg/controller/elasticsearch/client/v6.go index 58e40a3893..779fbe4e7f 100644 --- a/pkg/controller/elasticsearch/client/v6.go +++ b/pkg/controller/elasticsearch/client/v6.go @@ -94,6 +94,11 @@ func (c *clientV6) UpdateLicense(ctx context.Context, licenses LicenseUpdateRequ return response, c.post(ctx, "/_xpack/license", licenses, &response) } +func (c *clientV6) StartTrial(ctx context.Context) (StartTrialResponse, error) { + var response StartTrialResponse + return response, c.post(ctx, "/_xpack/license/start_trial?acknowledge=true", nil, &response) +} + func (c *clientV6) AddVotingConfigExclusions(ctx context.Context, nodeNames []string, timeout string) error { return errors.New("Not supported in Elasticsearch 6.x") } diff --git a/pkg/controller/elasticsearch/client/v7.go b/pkg/controller/elasticsearch/client/v7.go index 086d122d28..1b9dba955c 100644 --- a/pkg/controller/elasticsearch/client/v7.go +++ b/pkg/controller/elasticsearch/client/v7.go @@ -27,6 +27,11 @@ func (c *clientV7) UpdateLicense(ctx context.Context, licenses LicenseUpdateRequ return response, c.post(ctx, "/_license", licenses, &response) } +func (c *clientV7) StartTrial(ctx context.Context) (StartTrialResponse, error) { + var response StartTrialResponse + return response, c.post(ctx, "/_license/start_trial?acknowledge=true", nil, &response) +} + func (c *clientV7) AddVotingConfigExclusions(ctx context.Context, nodeNames []string, timeout string) error { if timeout == "" { timeout = DefaultVotingConfigExclusionsTimeout diff --git a/pkg/controller/elasticsearch/driver/driver.go b/pkg/controller/elasticsearch/driver/driver.go index 43a9bb0172..9433149fad 100644 --- a/pkg/controller/elasticsearch/driver/driver.go +++ b/pkg/controller/elasticsearch/driver/driver.go @@ -189,13 +189,17 @@ func (d *defaultDriver) Reconcile() *reconciler.Results { results.Apply( "reconcile-cluster-license", func() (controller.Result, error) { + if !esReachable { + return defaultRequeue, nil + } + err := license.Reconcile( d.Client, d.ES, esClient, observedState.ClusterLicense, ) - if err != nil && esReachable { + if err != nil { d.ReconcileState.AddEvent( corev1.EventTypeWarning, events.EventReasonUnexpected, diff --git a/pkg/controller/elasticsearch/license/apply.go b/pkg/controller/elasticsearch/license/apply.go index 4eeabc97f8..b66052b6c3 100644 --- a/pkg/controller/elasticsearch/license/apply.go +++ b/pkg/controller/elasticsearch/license/apply.go @@ -10,15 +10,23 @@ import ( "fmt" "github.com/elastic/cloud-on-k8s/pkg/apis/elasticsearch/v1beta1" - common_license "github.com/elastic/cloud-on-k8s/pkg/controller/common/license" + commonlicense "github.com/elastic/cloud-on-k8s/pkg/controller/common/license" esclient "github.com/elastic/cloud-on-k8s/pkg/controller/elasticsearch/client" "github.com/elastic/cloud-on-k8s/pkg/utils/k8s" pkgerrors "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/types" + logf "sigs.k8s.io/controller-runtime/pkg/log" ) +var log = logf.Log.WithName("elasticsearch-license") + +// isTrial returns true if an Elasticsearch license is of the trial type +func isTrial(l *esclient.License) bool { + return l != nil && l.Type == string(commonlicense.ElasticsearchLicenseTypeTrial) +} + func applyLinkedLicense( c k8s.Client, esCluster types.NamespacedName, @@ -44,17 +52,23 @@ func applyLinkedLicense( return err } - bytes, err := common_license.FetchLicenseData(license.Data) + bytes, err := commonlicense.FetchLicenseData(license.Data) if err != nil { return err } - var lic esclient.License - err = json.Unmarshal(bytes, &lic) + var desired esclient.License + err = json.Unmarshal(bytes, &desired) if err != nil { return pkgerrors.Wrap(err, "no valid license found in license secret") } - return updater(lic) + + err = updater(desired) + if err != nil { + return err + } + + return nil } // updateLicense make the call to Elasticsearch to set the license. This function exists mainly to facilitate testing. @@ -63,7 +77,7 @@ func updateLicense( current *esclient.License, desired esclient.License, ) error { - if current != nil && current.UID == desired.UID { + if current != nil && (current.UID == desired.UID || (isTrial(current) && current.Type == desired.Type)) { return nil // we are done already applied } request := esclient.LicenseUpdateRequest{ @@ -73,6 +87,15 @@ func updateLicense( } ctx, cancel := context.WithTimeout(context.Background(), esclient.DefaultReqTimeout) defer cancel() + + if isTrial(&desired) { + err := startTrial(c) + if err != nil { + return err + } + return nil + } + response, err := c.UpdateLicense(ctx, request) if err != nil { return err @@ -82,3 +105,32 @@ func updateLicense( } return nil } + +// startTrial starts the trial license after checking that the trial is not yet activated by directly hitting the +// Elasticsearch API. +func startTrial(c esclient.Client) error { + ctx, cancel := context.WithTimeout(context.Background(), esclient.DefaultReqTimeout) + defer cancel() + + // Check the current license + license, err := c.GetLicense(ctx) + if err != nil { + return err + } + if isTrial(&license) { + // Trial already activated + return nil + } + + // Let's start the trial + response, err := c.StartTrial(ctx) + if err != nil { + return err + } + if !response.IsSuccess() { + return fmt.Errorf("failed to start trial license: %s", response.ErrorMessage) + } + + log.Info("Trial license started") + return nil +} diff --git a/pkg/controller/elasticsearch/license/reconcile.go b/pkg/controller/elasticsearch/license/reconcile.go index 45052b6c69..85668c00ac 100644 --- a/pkg/controller/elasticsearch/license/reconcile.go +++ b/pkg/controller/elasticsearch/license/reconcile.go @@ -18,7 +18,7 @@ func Reconcile( current *esclient.License, ) error { clusterName := k8s.ExtractNamespacedName(&esCluster) - return applyLinkedLicense(c, clusterName, func(license esclient.License) error { - return updateLicense(clusterClient, current, license) + return applyLinkedLicense(c, clusterName, func(desired esclient.License) error { + return updateLicense(clusterClient, current, desired) }) } diff --git a/pkg/controller/license/trial/trial_controller.go b/pkg/controller/license/trial/trial_controller.go index 3c79c08ba1..762438e3ef 100644 --- a/pkg/controller/license/trial/trial_controller.go +++ b/pkg/controller/license/trial/trial_controller.go @@ -44,8 +44,9 @@ type ReconcileTrials struct { scheme *runtime.Scheme recorder record.EventRecorder // iteration is the number of times this controller has run its Reconcile method. - iteration int64 - trialPubKey *rsa.PublicKey + iteration int64 + trialPubKey *rsa.PublicKey + operatorNamespace string } // Reconcile watches a trial status secret. If it finds a trial license it checks whether a trial has been started. @@ -78,7 +79,7 @@ func (r *ReconcileTrials) Reconcile(request reconcile.Request) (reconcile.Result // 1. fetch trial status secret var trialStatus corev1.Secret - err = r.Get(types.NamespacedName{Namespace: request.Namespace, Name: licensing.TrialStatusSecretKey}, &trialStatus) + err = r.Get(types.NamespacedName{Namespace: r.operatorNamespace, Name: licensing.TrialStatusSecretKey}, &trialStatus) if errors.IsNotFound(err) { // 2. if not present create one + finalizer err := r.initTrial(secret, license) @@ -107,7 +108,7 @@ func (r *ReconcileTrials) initTrial(secret corev1.Secret, l licensing.Enterprise return nil } - trialPubKey, err := licensing.InitTrial(r, secret, &l) + trialPubKey, err := licensing.InitTrial(r, r.operatorNamespace, secret, &l) if err != nil { return err } @@ -132,11 +133,12 @@ func (r *ReconcileTrials) reconcileTrialStatus(trialStatus corev1.Secret) error } -func newReconciler(mgr manager.Manager, _ operator.Parameters) *ReconcileTrials { +func newReconciler(mgr manager.Manager, params operator.Parameters) *ReconcileTrials { return &ReconcileTrials{ - Client: k8s.WrapClient(mgr.GetClient()), - scheme: mgr.GetScheme(), - recorder: mgr.GetEventRecorderFor(name), + Client: k8s.WrapClient(mgr.GetClient()), + scheme: mgr.GetScheme(), + recorder: mgr.GetEventRecorderFor(name), + operatorNamespace: params.OperatorNamespace, } } @@ -171,8 +173,8 @@ func add(mgr manager.Manager, r *ReconcileTrials) error { return []reconcile.Request{ { NamespacedName: types.NamespacedName{ - Namespace: obj.Meta.GetNamespace(), - Name: string(licensing.LicenseTypeEnterpriseTrial), + Namespace: secret.Annotations[licensing.TrialLicenseSecretNamespace], + Name: secret.Annotations[licensing.TrialLicenseSecretName], }, }, } diff --git a/pkg/controller/license/trial/trial_controller_integration_test.go b/pkg/controller/license/trial/trial_controller_integration_test.go index e602241a60..4cdc32da7a 100644 --- a/pkg/controller/license/trial/trial_controller_integration_test.go +++ b/pkg/controller/license/trial/trial_controller_integration_test.go @@ -30,7 +30,7 @@ func TestMain(m *testing.M) { } func TestReconcile(t *testing.T) { - c, stop := test.StartManager(t, Add, operator.Parameters{}) + c, stop := test.StartManager(t, Add, operator.Parameters{OperatorNamespace: operatorNs}) defer stop() now := time.Now() diff --git a/test/e2e/es/license_test.go b/test/e2e/es/license_test.go index a1a0d1b2ae..620b2b7b9e 100644 --- a/test/e2e/es/license_test.go +++ b/test/e2e/es/license_test.go @@ -47,5 +47,35 @@ func TestEnterpriseLicenseSingle(t *testing.T) { ), }). WithSteps(esBuilder.DeletionTestSteps(k)). + WithStep(licenseTestContext.DeleteEnterpriseLicenseSecret()). RunSequential(t) } + +func TestEnterpriseTrialLicense(t *testing.T) { + esBuilder := elasticsearch.NewBuilder("test-es-trial-license"). + WithESMasterDataNodes(1, elasticsearch.DefaultResources) + + var licenseTestContext elasticsearch.LicenseTestContext + + initStepsFn := func(k *test.K8sClient) test.StepList { + return test.StepList{ + { + Name: "Create license test context", + Test: func(t *testing.T) { + licenseTestContext = elasticsearch.NewLicenseTestContext(k, esBuilder.Elasticsearch) + }, + }, + licenseTestContext.DeleteEnterpriseLicenseSecret(), + licenseTestContext.CreateEnterpriseTrialLicenseSecret(), + } + } + + stepsFn := func(k *test.K8sClient) test.StepList { + return test.StepList{ + licenseTestContext.Init(), + licenseTestContext.CheckElasticsearchLicense(license.ElasticsearchLicenseTypeTrial), + } + } + + test.Sequence(initStepsFn, stepsFn, esBuilder).RunSequential(t) +} diff --git a/test/e2e/test/elasticsearch/steps_license.go b/test/e2e/test/elasticsearch/steps_license.go index 9904373275..4ee0eac897 100644 --- a/test/e2e/test/elasticsearch/steps_license.go +++ b/test/e2e/test/elasticsearch/steps_license.go @@ -91,10 +91,32 @@ func (ltctx *LicenseTestContext) CreateEnterpriseLicenseSecret(licenseBytes []by } } +func (ltctx *LicenseTestContext) CreateEnterpriseTrialLicenseSecret() test.Step { + return test.Step{ + Name: "Creating enterprise trial license secret", + Test: func(t *testing.T) { + sec := corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: test.Ctx().ManagedNamespace(0), + Name: licenseSecretName, + Labels: map[string]string{ + license.LicenseLabelType: string(license.LicenseTypeEnterpriseTrial), + }, + Annotations: map[string]string{ + license.EULAAnnotation: license.EULAAcceptedValue, + }, + }, + } + require.NoError(t, ltctx.k.Client.Create(&sec)) + }, + } +} + func (ltctx *LicenseTestContext) DeleteEnterpriseLicenseSecret() test.Step { return test.Step{ - Name: "Removing any test enterprise licenses", + Name: "Removing any test enterprise license secrets", Test: func(t *testing.T) { + // Delete operator license secret sec := corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Namespace: test.Ctx().ManagedNamespace(0), @@ -102,6 +124,14 @@ func (ltctx *LicenseTestContext) DeleteEnterpriseLicenseSecret() test.Step { }, } _ = ltctx.k.Client.Delete(&sec) + // Delete operator trial status secret + sec = corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: test.Ctx().GlobalOperator.Namespace, + Name: license.TrialStatusSecretKey, + }, + } + _ = ltctx.k.Client.Delete(&sec) }, } }