Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Autostart trial #2160

Merged
merged 21 commits into from
Nov 29, 2019
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions pkg/controller/common/license/labels.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ 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"
TrialLicenseStartedAnnotation = "license.k8s.elastic.co/trial-started"
thbkrkr marked this conversation as resolved.
Show resolved Hide resolved
)

// LicenseType is the type of license a resource is describing.
Expand Down
13 changes: 13 additions & 0 deletions pkg/controller/common/license/match.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
11 changes: 9 additions & 2 deletions pkg/controller/common/license/trial.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
thbkrkr marked this conversation as resolved.
Show resolved Hide resolved
)

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")
}
Expand All @@ -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,
Expand Down
1 change: 1 addition & 0 deletions pkg/controller/common/license/trial_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions pkg/controller/elasticsearch/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 license.
thbkrkr marked this conversation as resolved.
Show resolved Hide resolved
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.
Expand Down
11 changes: 11 additions & 0 deletions pkg/controller/elasticsearch/client/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down
5 changes: 5 additions & 0 deletions pkg/controller/elasticsearch/client/v6.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand Down
5 changes: 5 additions & 0 deletions pkg/controller/elasticsearch/client/v7.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 5 additions & 1 deletion pkg/controller/elasticsearch/driver/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
67 changes: 61 additions & 6 deletions pkg/controller/elasticsearch/license/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ import (
"context"
"encoding/json"
"fmt"
"strings"

"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"
Expand All @@ -19,6 +20,11 @@ import (
"k8s.io/apimachinery/pkg/types"
)

// 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,
Expand All @@ -44,17 +50,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.
Expand All @@ -63,7 +75,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{
Expand All @@ -73,6 +85,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
Expand All @@ -82,3 +103,37 @@ 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)
thbkrkr marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return err
}
// Recover from error if the trial was already activated,
// this should not happen because the license has just been checked
if response.Acknowledged && !response.TrialWasStarted &&
strings.Contains(response.ErrorMessage, "Trial was already activated") {
thbkrkr marked this conversation as resolved.
Show resolved Hide resolved
// Trial already activated
return nil
}
if !response.IsSuccess() {
return fmt.Errorf("failed to start trial license: %s", response.ErrorMessage)
}
return nil
}
4 changes: 2 additions & 2 deletions pkg/controller/elasticsearch/license/reconcile.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
}
22 changes: 12 additions & 10 deletions pkg/controller/license/trial/trial_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
}
Expand All @@ -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,
}
}

Expand Down Expand Up @@ -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],
},
},
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
29 changes: 29 additions & 0 deletions test/e2e/es/license_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,32 @@ func TestEnterpriseLicenseSingle(t *testing.T) {
WithSteps(esBuilder.DeletionTestSteps(k)).
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.DeleteEnterpriseTrialLicenseSecret(),
thbkrkr marked this conversation as resolved.
Show resolved Hide resolved
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)
}
Loading