pkg/controller/common/license/trial.go (141 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" "crypto/rand" "crypto/rsa" "crypto/x509" "errors" "fmt" "time" pkgerrors "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/uuid" "github.com/elastic/cloud-on-k8s/v3/pkg/utils/chrono" ulog "github.com/elastic/cloud-on-k8s/v3/pkg/utils/log" ) const ( ECKLicenseIssuer = "Elastic k8s operator" TrialStatusSecretKey = "trial-status" TrialPubkeyKey = "pubkey" TrialActivationKey = "in-trial-activation" TrialLicenseSecretName = "trial.k8s.elastic.co/secret-name" //nolint:gosec TrialLicenseSecretNamespace = "trial.k8s.elastic.co/secret-namespace" //nolint:gosec ) // TrialState captures the in-memory representation of the trial status. type TrialState struct { privateKey *rsa.PrivateKey publicKey *rsa.PublicKey } // NewTrialState creates a new trial state based on a new RSA key pair. func NewTrialState() (TrialState, error) { key, err := newTrialKey() if err != nil { return TrialState{}, err } return TrialState{ privateKey: key, publicKey: &key.PublicKey, }, nil } // NewTrialStateFromStatus reconstructs trial state from a trial status secret. func NewTrialStateFromStatus(trialStatus corev1.Secret) (TrialState, error) { // reinstate pubkey from status secret e.g. after operator restart pubKeyBytes := trialStatus.Data[TrialPubkeyKey] key, err := ParsePubKey(pubKeyBytes) if err != nil { return TrialState{}, err } return TrialState{ publicKey: key, }, nil } // IsTrialStarted returns true if a trial has been successfully started at some point in the past. func (tk *TrialState) IsTrialStarted() bool { return tk.publicKey != nil && tk.privateKey == nil } // IsEmpty returns true on an empty state struct. func (tk *TrialState) IsEmpty() bool { return tk.privateKey == nil && tk.publicKey == nil } // InitTrialLicense initialises and signs the given license based on the current state. func (tk *TrialState) InitTrialLicense(ctx context.Context, l *EnterpriseLicense) error { if tk.privateKey == nil { return errors.New("trial has already been activated") } if l == nil { return errors.New("license is nil") } if err := populateTrialLicense(l); err != nil { return pkgerrors.Wrap(err, "failed to populate trial license") } ulog.FromContext(ctx).Info("Starting enterprise trial", "start", l.StartTime(), "end", l.ExpiryTime()) // sign trial license signer := NewSigner(tk.privateKey) sig, err := signer.Sign(*l) if err != nil { return pkgerrors.Wrap(err, "failed to sign license") } l.License.Signature = string(sig) return nil } // CompleteTrialActivation should be called once a trial license has been successfully generated and verified. // Returns false if the trial activation had been completed previously. func (tk *TrialState) CompleteTrialActivation() bool { if tk.privateKey == nil { return false } tk.privateKey = nil return true } // LicenseVerifier returns a verifier based on the current state/public key func (tk *TrialState) LicenseVerifier() *Verifier { return &Verifier{PublicKey: tk.publicKey} } // ExpectedTrialStatus creates the expected state of the trial status secret for the given trial state for reconciliation purposes. func ExpectedTrialStatus(operatorNamespace string, license types.NamespacedName, state TrialState) (corev1.Secret, error) { if state.IsEmpty() { return corev1.Secret{}, errors.New("cannot create trial status from uninitialised trial state") } pubkeyBytes, err := x509.MarshalPKIXPublicKey(state.publicKey) if err != nil { return corev1.Secret{}, err } secret := corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Namespace: operatorNamespace, Name: TrialStatusSecretKey, Annotations: map[string]string{ TrialLicenseSecretName: license.Name, TrialLicenseSecretNamespace: license.Namespace, }, }, Data: map[string][]byte{ TrialPubkeyKey: pubkeyBytes, }, } if !state.IsTrialStarted() { secret.Data[TrialActivationKey] = []byte("true") } return secret, nil } func newTrialKey() (*rsa.PrivateKey, error) { rnd := rand.Reader trialKey, err := rsa.GenerateKey(rnd, 2048) if err != nil { return nil, fmt.Errorf("while generating trial key %w", err) } return trialKey, nil } // populateTrialLicense adds missing fields to a trial license. func populateTrialLicense(l *EnterpriseLicense) error { if !l.IsTrial() { return pkgerrors.Errorf("%s for %s is not a trial license", l.License.UID, l.License.IssuedTo) } if l.License.Issuer == "" { l.License.Issuer = ECKLicenseIssuer } if l.License.IssuedTo == "" { l.License.IssuedTo = "Unknown" } if l.License.UID == "" { l.License.UID = string(uuid.NewUUID()) } if l.License.StartDateInMillis == 0 || l.License.ExpiryDateInMillis == 0 { setStartAndExpiry(l, time.Now()) } return nil } // setStartAndExpiry sets the issue, start and end dates for a trial. func setStartAndExpiry(l *EnterpriseLicense, from time.Time) { l.License.StartDateInMillis = chrono.ToMillis(from) l.License.IssueDateInMillis = l.License.StartDateInMillis l.License.ExpiryDateInMillis = chrono.ToMillis(from.Add(24 * time.Hour * 30)) }