-
Notifications
You must be signed in to change notification settings - Fork 719
/
trial_controller.go
283 lines (253 loc) · 10.3 KB
/
trial_controller.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
// 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 trial
import (
"bytes"
"context"
"fmt"
"reflect"
"sync/atomic"
"time"
pkgerrors "github.com/pkg/errors"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/tools/record"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller"
"sigs.k8s.io/controller-runtime/pkg/handler"
"sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"sigs.k8s.io/controller-runtime/pkg/source"
"github.com/elastic/cloud-on-k8s/pkg/controller/common"
licensing "github.com/elastic/cloud-on-k8s/pkg/controller/common/license"
"github.com/elastic/cloud-on-k8s/pkg/controller/common/operator"
"github.com/elastic/cloud-on-k8s/pkg/controller/common/reconciler"
"github.com/elastic/cloud-on-k8s/pkg/utils/k8s"
ulog "github.com/elastic/cloud-on-k8s/pkg/utils/log"
)
const (
name = "trial-controller"
EULAValidationMsg = `Please set the annotation elastic.co/eula to "accepted" to accept the EULA`
trialOnlyOnceMsg = "trial can be started only once"
)
var (
log = ulog.Log.WithName(name)
userFriendlyMsgs = map[licensing.LicenseStatus]string{
licensing.LicenseStatusInvalid: "trial license signature invalid",
licensing.LicenseStatusExpired: "trial license expired",
}
)
// ReconcileTrials reconciles Enterprise trial licenses.
type ReconcileTrials struct {
k8s.Client
recorder record.EventRecorder
// iteration is the number of times this controller has run its Reconcile method.
iteration int64
trialState licensing.TrialState
operatorNamespace string
}
// Reconcile watches a trial status secret. If it finds a trial license it checks whether a trial has been started.
// If not it starts the trial period if the user has expressed intent to do so.
// If a trial is already running it validates the trial license.
func (r *ReconcileTrials) Reconcile(ctx context.Context, request reconcile.Request) (reconcile.Result, error) {
// atomically update the iteration to support concurrent runs.
currentIteration := atomic.AddInt64(&r.iteration, 1)
iterationStartTime := time.Now()
log.Info("Start reconcile iteration", "iteration", currentIteration, "namespace", request.Namespace, "secret_name", request.Name)
defer func() {
log.Info("End reconcile iteration", "iteration", currentIteration, "took", time.Since(iterationStartTime), "namespace", request.Namespace, "secret_name", request.Name)
}()
secret, license, err := licensing.TrialLicense(r, request.NamespacedName)
if err != nil && errors.IsNotFound(err) {
log.Info("Trial license secret has been deleted by user, but trial had been started previously.")
return reconcile.Result{}, nil
}
if err != nil {
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)
}
// 1. reconcile trial status secret
if err := r.reconcileTrialStatus(request.NamespacedName, license); err != nil {
return reconcile.Result{}, pkgerrors.Wrap(err, "while reconciling trial status")
}
// 2. reconcile the trial license itself
trialLicensePopulated := license.IsMissingFields() == nil
licenseStatus := r.validateLicense(license)
switch {
case !trialLicensePopulated && r.trialState.IsTrialStarted():
// user wants to start a trial for the second time
return r.invalidOperation(secret, trialOnlyOnceMsg)
case !trialLicensePopulated && !r.trialState.IsTrialStarted():
// user wants to init a trial for the first time
return r.initTrialLicense(secret, license)
case trialLicensePopulated && !validLicense(licenseStatus):
// existing license is invalid (expired or tampered with)
return r.invalidOperation(secret, userFriendlyMsgs[licenseStatus])
case trialLicensePopulated && validLicense(licenseStatus) && !r.trialState.IsTrialStarted():
// valid license, let's consider the trial started and complete the activation
return r.completeTrialActivation(request.NamespacedName)
case trialLicensePopulated && validLicense(licenseStatus) && r.trialState.IsTrialStarted():
// all good nothing to do
}
return reconcile.Result{}, nil
}
func (r *ReconcileTrials) reconcileTrialStatus(licenseName types.NamespacedName, license licensing.EnterpriseLicense) error {
var trialStatus corev1.Secret
err := r.Get(context.Background(), types.NamespacedName{Namespace: r.operatorNamespace, Name: licensing.TrialStatusSecretKey}, &trialStatus)
if errors.IsNotFound(err) {
if r.trialState.IsEmpty() {
// we have no state in memory nor in the status secret: start the activation process
if err := r.startTrialActivation(); err != nil {
return err
}
}
// we have state in memory but the status secret is missing: recreate it
trialStatus, err = licensing.ExpectedTrialStatus(r.operatorNamespace, licenseName, r.trialState)
if err != nil {
return fmt.Errorf("while creating expected trial status %w", err)
}
return r.Create(context.Background(), &trialStatus)
}
if err != nil {
return fmt.Errorf("while fetching trial status %w", err)
}
// the status secret is there but we don't have anything in memory: recover the state
if r.trialState.IsEmpty() {
recoveredState, err := recoverState(license, trialStatus)
if err != nil {
return err
}
r.trialState = recoveredState
}
// if trial status exists, but we need to update it because:
// - has been tampered with
// - we need to complete the trial activation because if failed on a previous attempt
// - we just regenerated the state after a crash
expected, err := licensing.ExpectedTrialStatus(r.operatorNamespace, licenseName, r.trialState)
if err != nil {
return err
}
if reflect.DeepEqual(expected.Data, trialStatus.Data) {
return nil
}
trialStatus.Data = expected.Data
return r.Update(context.Background(), &trialStatus)
}
func recoverState(license licensing.EnterpriseLicense, trialStatus corev1.Secret) (licensing.TrialState, error) {
// allow new trial state only if we don't have license that looks like it has been populated previously
allowNewState := license.IsMissingFields() != nil
// create new keys if the operator failed just before the trial was started
trialActivationInProgress := bytes.Equal(trialStatus.Data[licensing.TrialActivationKey], []byte("true"))
if trialActivationInProgress && allowNewState {
return licensing.NewTrialState()
}
// otherwise just recover the public key
return licensing.NewTrialStateFromStatus(trialStatus)
}
func (r *ReconcileTrials) startTrialActivation() error {
state, err := licensing.NewTrialState()
if err != nil {
return err
}
r.trialState = state
return nil
}
func (r *ReconcileTrials) completeTrialActivation(license types.NamespacedName) (reconcile.Result, error) {
if r.trialState.CompleteTrialActivation() {
expectedStatus, err := licensing.ExpectedTrialStatus(r.operatorNamespace, license, r.trialState)
if err != nil {
return reconcile.Result{}, err
}
_, err = reconciler.ReconcileSecret(r, expectedStatus, nil)
return reconcile.Result{}, err
}
return reconcile.Result{}, nil
}
func (r *ReconcileTrials) initTrialLicense(secret corev1.Secret, license licensing.EnterpriseLicense) (reconcile.Result, error) {
if err := r.trialState.InitTrialLicense(&license); err != nil {
return reconcile.Result{}, err
}
return reconcile.Result{}, licensing.UpdateEnterpriseLicense(r, secret, license)
}
func (r *ReconcileTrials) invalidOperation(secret corev1.Secret, msg string) (reconcile.Result, error) {
setValidationMsg(&secret, msg)
return reconcile.Result{}, r.Update(context.Background(), &secret)
}
func validLicense(status licensing.LicenseStatus) bool {
return status == licensing.LicenseStatusValid
}
func (r *ReconcileTrials) validateLicense(license licensing.EnterpriseLicense) licensing.LicenseStatus {
return r.trialState.LicenseVerifier().Valid(license, time.Now())
}
func validateEULA(trialSecret corev1.Secret) string {
if licensing.IsEnterpriseTrial(trialSecret) &&
trialSecret.Annotations[licensing.EULAAnnotation] != licensing.EULAAcceptedValue {
return EULAValidationMsg
}
return ""
}
func setValidationMsg(secret *corev1.Secret, violation string) {
if secret.Annotations == nil {
secret.Annotations = map[string]string{}
}
log.Info("trial license invalid", "reason", violation)
secret.Annotations[licensing.LicenseInvalidAnnotation] = violation
}
func newReconciler(mgr manager.Manager, params operator.Parameters) *ReconcileTrials {
return &ReconcileTrials{
Client: mgr.GetClient(),
recorder: mgr.GetEventRecorderFor(name),
operatorNamespace: params.OperatorNamespace,
}
}
func addWatches(c controller.Controller) error {
// Watch the trial status secret and the enterprise trial licenses as well
return c.Watch(&source.Kind{Type: &corev1.Secret{}}, handler.EnqueueRequestsFromMapFunc(func(obj client.Object) []reconcile.Request {
secret, ok := obj.(*corev1.Secret)
if !ok {
log.Error(fmt.Errorf("object of type %T in secret watch", obj), "dropping event due to type error")
}
if licensing.IsEnterpriseTrial(*secret) {
return []reconcile.Request{
{
NamespacedName: types.NamespacedName{
Namespace: obj.GetNamespace(),
Name: obj.GetName(),
},
},
}
}
if obj.GetName() != licensing.TrialStatusSecretKey {
return nil
}
return []reconcile.Request{
{
NamespacedName: types.NamespacedName{
Namespace: secret.Annotations[licensing.TrialLicenseSecretNamespace],
Name: secret.Annotations[licensing.TrialLicenseSecretName],
},
},
}
}),
)
}
// Add creates a new Trial Controller and adds it to the Manager with default RBAC. The Manager will set fields on the Controller
// and Start it when the Manager is Started.
func Add(mgr manager.Manager, params operator.Parameters) error {
r := newReconciler(mgr, params)
c, err := common.NewController(mgr, name, r, params)
if err != nil {
return err
}
return addWatches(c)
}
var _ reconcile.Reconciler = &ReconcileTrials{}