pkg/controller/elasticsearch/license/apply.go (119 lines of code) (raw):
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
// or more contributor license agreements. Licensed under the Elastic License 2.0;
// you may not use this file except in compliance with the Elastic License 2.0.
package license
import (
"context"
"encoding/json"
"fmt"
pkgerrors "github.com/pkg/errors"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/types"
esv1 "github.com/elastic/cloud-on-k8s/v3/pkg/apis/elasticsearch/v1"
commonlicense "github.com/elastic/cloud-on-k8s/v3/pkg/controller/common/license"
esclient "github.com/elastic/cloud-on-k8s/v3/pkg/controller/elasticsearch/client"
"github.com/elastic/cloud-on-k8s/v3/pkg/utils/k8s"
ulog "github.com/elastic/cloud-on-k8s/v3/pkg/utils/log"
)
// isTrial returns true if an Elasticsearch license is of the trial type
func isTrial(l esclient.License) bool {
return l.Type == string(esclient.ElasticsearchLicenseTypeTrial)
}
// isECKManagedTrial returns true if this is a trial started via the internal trial mechanism in the operator. We use an
// empty license with only the type field populated to indicate to the Elasticsearch controller that a self-generated
// trial in Elasticsearch should be started. This can be done only once.
func isECKManagedTrial(l esclient.License) bool {
return isTrial(l) && l.Signature == "" && l.UID == "" && l.ExpiryDateInMillis == 0 && l.StartDateInMillis == 0
}
// isBasic returns true if an Elasticsearch license is of the basic type
func isBasic(l esclient.License) bool {
return l.Type == string(esclient.ElasticsearchLicenseTypeBasic)
}
func applyLinkedLicense(
ctx context.Context,
c k8s.Client,
esCluster types.NamespacedName,
updater esclient.LicenseClient,
currentLicense esclient.License,
) error {
// get the expected license
// the underlying assumption here is that either a user or a
// license controller has created a cluster license in the
// namespace of this cluster following the cluster-license naming
// convention
var license corev1.Secret
err := c.Get(ctx,
types.NamespacedName{
Namespace: esCluster.Namespace,
Name: esv1.LicenseSecretName(esCluster.Name),
},
&license,
)
if err != nil && !apierrors.IsNotFound(err) {
return err
}
if err != nil && apierrors.IsNotFound(err) {
// no license expected, let's look at the current cluster license
switch {
case isBasic(currentLicense):
// nothing to do
return nil
case isTrial(currentLicense):
// Elasticsearch reports a trial license, but there's no ECK enterprise trial requested.
// This can be the case if:
// - an ECK trial was started previously, then stopped (secret removed)
// - the user manually started a trial at the stack level (eg. by clicking a button in Kibana when
// trying to access a commercial feature). While this is not a supported use case,
// we tolerate it to avoid a bad user experience because trials can only be started once.
ulog.FromContext(ctx).V(1).Info("Preserving existing stack-level trial license",
"namespace", esCluster.Namespace, "es_name", esCluster.Name)
return nil
default:
// revert the current license to basic
return startBasic(ctx, updater)
}
}
bytes, err := commonlicense.FetchLicenseData(license.Data)
if err != nil {
return err
}
var desired esclient.License
err = json.Unmarshal(bytes, &desired)
if err != nil {
return pkgerrors.Wrap(err, "no valid license found in license secret")
}
return updateLicense(ctx, esCluster, updater, currentLicense, desired)
}
func startBasic(ctx context.Context, updater esclient.LicenseClient) error {
_, err := updater.StartBasic(ctx)
if err != nil && esclient.IsForbidden(err) {
// ES returns 403 + acknowledged: true (which we don't parse in case of error) if we are already in basic mode
return nil
}
return pkgerrors.Wrap(err, "failed to revert to basic")
}
// updateLicense make the call to Elasticsearch to set the license. This function exists mainly to facilitate testing.
func updateLicense(
ctx context.Context,
esCluster types.NamespacedName,
updater esclient.LicenseClient,
current esclient.License,
desired esclient.License,
) error {
if current.UID == desired.UID || (isTrial(current) && current.Type == desired.Type) {
return nil // we are done already applied
}
request := esclient.LicenseUpdateRequest{
Licenses: []esclient.License{
desired,
},
}
if isECKManagedTrial(desired) {
// start a self-generated trial in Elasticsearch, this can only be done once.
return pkgerrors.Wrap(startTrial(ctx, updater, esCluster), "failed to start trial")
}
response, err := updater.UpdateLicense(ctx, request)
if err != nil {
return pkgerrors.Wrap(err, fmt.Sprintf("failed to update license to %s", desired.Type))
}
if !response.IsSuccess() {
return pkgerrors.Errorf("failed to apply license: %s", response.LicenseStatus)
}
return nil
}
// startTrial starts the trial license after checking that the trial is not yet activated by directly hitting the
// Elasticsearch API.
func startTrial(ctx context.Context, c esclient.LicenseClient, esCluster types.NamespacedName) error {
response, err := c.StartTrial(ctx)
log := ulog.FromContext(ctx)
if err != nil && esclient.IsForbidden(err) {
log.Info("failed to start trial most likely because trial was activated previously",
"err", err.Error(),
"namespace", esCluster.Namespace,
"name", esCluster.Name,
)
return nil
}
if response.IsSuccess() {
log.Info(
"Elasticsearch trial license activated",
"namespace", esCluster.Namespace,
"name", esCluster.Name,
)
}
return err
}