-
Notifications
You must be signed in to change notification settings - Fork 113
/
service.go
389 lines (313 loc) · 14.3 KB
/
service.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
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
package provisioning
import (
"time"
gardener_Types "github.com/gardener/gardener/pkg/apis/core/v1beta1"
"github.com/hashicorp/go-version"
"github.com/kyma-project/control-plane/components/provisioner/internal/apperrors"
"github.com/kyma-project/control-plane/components/provisioner/internal/model"
"github.com/kyma-project/control-plane/components/provisioner/internal/operations/queue"
"github.com/kyma-project/control-plane/components/provisioner/internal/persistence/dberrors"
"github.com/kyma-project/control-plane/components/provisioner/internal/provisioning/persistence/dbsession"
"github.com/kyma-project/control-plane/components/provisioner/internal/util"
uuid "github.com/kyma-project/control-plane/components/provisioner/internal/uuid"
"github.com/kyma-project/control-plane/components/provisioner/pkg/gqlschema"
log "github.com/sirupsen/logrus"
)
type DynamicKubeconfigProvider interface {
FetchFromRequest(shootName string) ([]byte, error)
}
//go:generate mockery --name=Service
type Service interface {
ProvisionRuntime(config gqlschema.ProvisionRuntimeInput, tenant, subAccount string) (*gqlschema.OperationStatus, apperrors.AppError)
DeprovisionRuntime(id string) (string, apperrors.AppError)
UpgradeGardenerShoot(id string, input gqlschema.UpgradeShootInput) (*gqlschema.OperationStatus, apperrors.AppError)
ReconnectRuntimeAgent(id string) (string, apperrors.AppError)
RuntimeStatus(id string) (*gqlschema.RuntimeStatus, apperrors.AppError)
RuntimeOperationStatus(id string) (*gqlschema.OperationStatus, apperrors.AppError)
}
//go:generate mockery --name=Provisioner
type Provisioner interface {
ProvisionCluster(cluster model.Cluster, operationId string) apperrors.AppError
DeprovisionCluster(cluster model.Cluster, operationId string) (model.Operation, apperrors.AppError)
UpgradeCluster(clusterID string, upgradeConfig model.GardenerConfig) apperrors.AppError
}
//go:generate mockery --name=ShootProvider
type ShootProvider interface {
Get(runtimeID string, tenant string) (gardener_Types.Shoot, apperrors.AppError)
}
type service struct {
inputConverter InputConverter
graphQLConverter GraphQLConverter
shootProvider ShootProvider
dynamicKubeconfigProvider DynamicKubeconfigProvider
dbSessionFactory dbsession.Factory
provisioner Provisioner
uuidGenerator uuid.UUIDGenerator
provisioningQueue queue.OperationQueue
deprovisioningQueue queue.OperationQueue
upgradeQueue queue.OperationQueue
shootUpgradeQueue queue.OperationQueue
hibernationQueue queue.OperationQueue
}
func NewProvisioningService(
inputConverter InputConverter,
graphQLConverter GraphQLConverter,
factory dbsession.Factory,
provisioner Provisioner,
generator uuid.UUIDGenerator,
shootProvider ShootProvider,
provisioningQueue queue.OperationQueue,
deprovisioningQueue queue.OperationQueue,
shootUpgradeQueue queue.OperationQueue,
dynamicKubeconfigProvider DynamicKubeconfigProvider,
) Service {
return &service{
inputConverter: inputConverter,
graphQLConverter: graphQLConverter,
dbSessionFactory: factory,
provisioner: provisioner,
uuidGenerator: generator,
provisioningQueue: provisioningQueue,
deprovisioningQueue: deprovisioningQueue,
shootUpgradeQueue: shootUpgradeQueue,
shootProvider: shootProvider,
dynamicKubeconfigProvider: dynamicKubeconfigProvider,
}
}
func (r *service) ProvisionRuntime(config gqlschema.ProvisionRuntimeInput, tenant, subAccount string) (*gqlschema.OperationStatus, apperrors.AppError) {
var runtimeID string
runtimeID = r.uuidGenerator.New()
log.Infof("Assigned new ID for provisioned Runtime: %s ", runtimeID)
cluster, err := r.inputConverter.ProvisioningInputToCluster(runtimeID, config, tenant, subAccount)
if err != nil {
return nil, err
}
dbSession, dberr := r.dbSessionFactory.NewSessionWithinTransaction()
if dberr != nil {
return nil, dberr
}
defer dbSession.RollbackUnlessCommitted()
// Try to set provisioning started before triggering it (which is hard to interrupt) to verify all unique constraints
operation, dberr := r.setProvisioningStarted(dbSession, runtimeID, cluster)
if dberr != nil {
return nil, dberr
}
err = r.provisioner.ProvisionCluster(cluster, operation.ID)
if err != nil {
return nil, err.Append("Failed to start provisioning")
}
dberr = dbSession.Commit()
if dberr != nil {
return nil, dberr
}
log.Infof("KymaConfig not provided. Starting provisioning steps for runtime %s without installation", cluster.ID)
r.provisioningQueue.Add(operation.ID)
return r.graphQLConverter.OperationStatusToGQLOperationStatus(operation), nil
}
func (r *service) DeprovisionRuntime(id string) (string, apperrors.AppError) {
session := r.dbSessionFactory.NewReadWriteSession()
appErr := r.verifyLastOperationFinished(session, id)
if appErr != nil {
return "", appErr
}
cluster, dberr := session.GetCluster(id)
if dberr != nil {
return "", dberr
}
operation, appErr := r.provisioner.DeprovisionCluster(cluster, r.uuidGenerator.New())
if appErr != nil {
return "", apperrors.Internal("Failed to start deprovisioning: %s", appErr.Error()).SetComponent(appErr.Component()).SetReason(appErr.Reason())
}
dberr = session.InsertOperation(operation)
if dberr != nil {
return "", dberr
}
log.Infof("Starting deprovisioning steps for runtime %s without installation", cluster.ID)
r.deprovisioningQueue.Add(operation.ID)
return operation.ID, nil
}
func (r *service) UpgradeGardenerShoot(runtimeID string, input gqlschema.UpgradeShootInput) (*gqlschema.OperationStatus, apperrors.AppError) {
log.Infof("Starting Upgrade of Gardener Shoot for Runtime '%s'...", runtimeID)
if input.GardenerConfig == nil {
return &gqlschema.OperationStatus{}, apperrors.Internal("Error: Gardener config is nil")
}
session := r.dbSessionFactory.NewReadSession()
err := r.verifyLastOperationFinished(session, runtimeID)
if err != nil {
return &gqlschema.OperationStatus{}, err
}
cluster, dberr := session.GetCluster(runtimeID)
if dberr != nil {
return &gqlschema.OperationStatus{}, apperrors.Internal("Failed to find shoot cluster to upgrade in database: %s", dberr.Error())
}
gardenerConfig, err := r.inputConverter.UpgradeShootInputToGardenerConfig(*input.GardenerConfig, cluster.ClusterConfig)
if err != nil {
return &gqlschema.OperationStatus{}, err.Append("Failed to convert GardenerClusterUpgradeConfig: %s", err.Error())
}
shoot, err := r.shootProvider.Get(runtimeID, cluster.Tenant)
if err != nil {
return &gqlschema.OperationStatus{}, err.Append("Failed to get shoot")
}
// This is a workaround for a problem with Kubernetes auto upgrade. If Kubernetes gets updated the current Kubernetes version is obtained for the shoot and stored in the database.
shouldTakeShootKubernetesVersion, err := isVersionHigher(shoot.Spec.Kubernetes.Version, gardenerConfig.KubernetesVersion)
if err != nil {
return &gqlschema.OperationStatus{}, err.Append("Failed to check if the shoot kubernetes version is higher than the config one")
}
if shouldTakeShootKubernetesVersion {
log.Infof("Kubernetes version in shoot was higher than the version provided in UpgradeGardenerShoot. Version fetched from the shoot will be used :%s.", shoot.Spec.Kubernetes.Version)
gardenerConfig.KubernetesVersion = shoot.Spec.Kubernetes.Version
}
// This is a workaround for the possible manual modification of the Shoot Spec Extensions. If ShootNetworkingFilterDisabled is modified manually, Provisioner should use the actual value.
shootNetworkingFilterDisabled := getShootNetworkingFilterDisabled(shoot.Spec.Extensions)
if input.GardenerConfig.ShootNetworkingFilterDisabled == nil && shootNetworkingFilterDisabled != nil {
log.Warnf("ShootNetworkingFilter extension was different than the one provided in UpgradeGardenerShoot. Value fetched from the shoot will be used: %t.", *shootNetworkingFilterDisabled)
gardenerConfig.ShootNetworkingFilterDisabled = shootNetworkingFilterDisabled
}
// Validate provider specific changes to the shoot
err = gardenerConfig.GardenerProviderConfig.ValidateShootConfigChange(&shoot)
if err != nil {
return &gqlschema.OperationStatus{}, err.Append("Invalid gardener provider config change")
}
txSession, dbErr := r.dbSessionFactory.NewSessionWithinTransaction()
if dbErr != nil {
return &gqlschema.OperationStatus{}, apperrors.Internal("Failed to start database transaction: %s", dbErr.Error())
}
defer txSession.RollbackUnlessCommitted()
operation, gardError := r.setGardenerShootUpgradeStarted(txSession, cluster, gardenerConfig, input.Administrators)
if gardError != nil {
return &gqlschema.OperationStatus{}, apperrors.Internal("Failed to set shoot upgrade started: %s", gardError.Error())
}
err = r.provisioner.UpgradeCluster(cluster.ID, gardenerConfig)
if err != nil {
return &gqlschema.OperationStatus{}, apperrors.Internal("Failed to upgrade Cluster: %s", err.Error())
}
dbErr = txSession.Commit()
if dbErr != nil {
return &gqlschema.OperationStatus{}, apperrors.Internal("Failed to commit upgrade transaction: %s", dbErr.Error())
}
r.shootUpgradeQueue.Add(operation.ID)
return r.graphQLConverter.OperationStatusToGQLOperationStatus(operation), nil
}
func (r *service) verifyLastOperationFinished(session dbsession.ReadSession, runtimeId string) apperrors.AppError {
lastOperation, dberr := session.GetLastOperation(runtimeId)
if dberr != nil {
return dberr.Append("failed to get last operation")
}
if lastOperation.State == model.InProgress {
return apperrors.BadRequest("cannot start new operation for %s Runtime while previous one is in progress", runtimeId)
}
return nil
}
func (r *service) ReconnectRuntimeAgent(string) (string, apperrors.AppError) {
return "", nil
}
func (r *service) RuntimeStatus(runtimeID string) (*gqlschema.RuntimeStatus, apperrors.AppError) {
runtimeStatus, dberr := r.getRuntimeStatus(runtimeID)
if dberr != nil {
return nil, dberr.Append("failed to get Runtime Status")
}
return r.graphQLConverter.RuntimeStatusToGraphQLStatus(runtimeStatus), nil
}
func (r *service) RuntimeOperationStatus(operationID string) (*gqlschema.OperationStatus, apperrors.AppError) {
readSession := r.dbSessionFactory.NewReadSession()
operation, dberr := readSession.GetOperation(operationID)
if dberr != nil {
return nil, dberr.Append("failed to get Runtime Operation Status")
}
return r.graphQLConverter.OperationStatusToGQLOperationStatus(operation), nil
}
func (r *service) getRuntimeStatus(runtimeID string) (model.RuntimeStatus, apperrors.AppError) {
session := r.dbSessionFactory.NewReadSession()
operation, err := session.GetLastOperation(runtimeID)
if err != nil {
return model.RuntimeStatus{}, err
}
cluster, err := session.GetCluster(runtimeID)
if err != nil {
return model.RuntimeStatus{}, err
}
kubeconfig, fetchErr := r.dynamicKubeconfigProvider.FetchFromRequest(cluster.ClusterConfig.Name)
if fetchErr != nil {
return model.RuntimeStatus{}, apperrors.Internal("unable to fetch kubeconfig: %s", fetchErr)
}
cluster.Kubeconfig = util.PtrTo(string(kubeconfig))
return model.RuntimeStatus{
LastOperationStatus: operation,
RuntimeConfiguration: cluster,
}, nil
}
func (r *service) setProvisioningStarted(dbSession dbsession.WriteSession, runtimeID string, cluster model.Cluster) (model.Operation, dberrors.Error) {
timestamp := time.Now()
cluster.CreationTimestamp = timestamp
if err := dbSession.InsertCluster(cluster); err != nil {
return model.Operation{}, dberrors.Internal("Failed to set provisioning started: %s", err)
}
if err := dbSession.InsertGardenerConfig(cluster.ClusterConfig); err != nil {
return model.Operation{}, dberrors.Internal("Failed to set provisioning started: %s", err)
}
provisioningMode := model.Provision
operation, err := r.setOperationStarted(dbSession, runtimeID, provisioningMode, model.WaitingForClusterDomain, timestamp, "Provisioning started")
if err != nil {
return model.Operation{}, err.Append("Failed to set provisioning started: %s")
}
return operation, nil
}
func (r *service) setGardenerShootUpgradeStarted(txSession dbsession.WriteSession, currentCluster model.Cluster, gardenerConfig model.GardenerConfig, administrators []string) (model.Operation, error) {
log.Infof("Starting Upgrade of Gardener Shoot operation")
dberr := txSession.UpdateGardenerClusterConfig(gardenerConfig)
if dberr != nil {
return model.Operation{}, dberrors.Internal("Failed to set Shoot Upgrade started: %s", dberr.Error())
}
dberr = txSession.InsertAdministrators(currentCluster.ID, administrators)
if dberr != nil {
return model.Operation{}, dberrors.Internal("Failed to set Shoot Upgrade started: %s", dberr.Error())
}
operation, dbError := r.setOperationStarted(txSession, currentCluster.ID, model.UpgradeShoot, model.WaitingForShootNewVersion, time.Now(), "Starting Gardener Shoot upgrade")
if dbError != nil {
return model.Operation{}, dbError.Append("Failed to start operation of Gardener Shoot upgrade %s", dbError.Error())
}
return operation, nil
}
func (r *service) setOperationStarted(
dbSession dbsession.WriteSession,
runtimeID string,
operationType model.OperationType,
operationStage model.OperationStage,
timestamp time.Time,
message string) (model.Operation, dberrors.Error) {
id := r.uuidGenerator.New()
operation := model.Operation{
ID: id,
Type: operationType,
StartTimestamp: timestamp,
State: model.InProgress,
Message: message,
ClusterID: runtimeID,
Stage: operationStage,
LastTransition: ×tamp,
}
err := dbSession.InsertOperation(operation)
if err != nil {
return model.Operation{}, err.Append("failed to insert operation")
}
return operation, nil
}
func isVersionHigher(version1, version2 string) (bool, apperrors.AppError) {
parsedVersion1, err := version.NewVersion(version1)
if err != nil {
return false, apperrors.Internal("Failed to parse \"%s\" as a version", version1)
}
parsedVersion2, err := version.NewVersion(version2)
if err != nil {
return false, apperrors.Internal("Failed to parse \"%s\" as a version", version2)
}
return parsedVersion1.GreaterThan(parsedVersion2), nil
}
func getShootNetworkingFilterDisabled(extensions []gardener_Types.Extension) *bool {
for _, extension := range extensions {
if extension.Type == model.ShootNetworkingFilterExtensionType {
return extension.Disabled
}
}
return nil
}