github.com/kyma-project/kyma-environment-broker@v0.0.1/internal/broker/instance_update.go (about) 1 package broker 2 3 import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "net/http" 8 "strings" 9 "time" 10 11 "github.com/kyma-incubator/compass/components/director/pkg/jsonschema" 12 "github.com/kyma-project/kyma-environment-broker/internal/euaccess" 13 14 "github.com/google/uuid" 15 "github.com/pivotal-cf/brokerapi/v8/domain" 16 "github.com/pivotal-cf/brokerapi/v8/domain/apiresponses" 17 "github.com/sirupsen/logrus" 18 "k8s.io/apimachinery/pkg/util/wait" 19 20 "github.com/kyma-project/kyma-environment-broker/internal" 21 "github.com/kyma-project/kyma-environment-broker/internal/dashboard" 22 "github.com/kyma-project/kyma-environment-broker/internal/ptr" 23 "github.com/kyma-project/kyma-environment-broker/internal/storage" 24 "github.com/kyma-project/kyma-environment-broker/internal/storage/dberr" 25 ) 26 27 type ContextUpdateHandler interface { 28 Handle(instance *internal.Instance, newCtx internal.ERSContext) (bool, error) 29 } 30 31 type UpdateEndpoint struct { 32 config Config 33 log logrus.FieldLogger 34 35 instanceStorage storage.Instances 36 runtimeStates storage.RuntimeStates 37 contextUpdateHandler ContextUpdateHandler 38 brokerURL string 39 processingEnabled bool 40 subAccountMovementEnabled bool 41 42 operationStorage storage.Operations 43 44 updatingQueue Queue 45 46 plansConfig PlansConfig 47 planDefaults PlanDefaults 48 49 dashboardConfig dashboard.Config 50 } 51 52 func NewUpdate(cfg Config, 53 instanceStorage storage.Instances, 54 runtimeStates storage.RuntimeStates, 55 operationStorage storage.Operations, 56 ctxUpdateHandler ContextUpdateHandler, 57 processingEnabled bool, 58 subAccountMovementEnabled bool, 59 queue Queue, 60 plansConfig PlansConfig, 61 planDefaults PlanDefaults, 62 log logrus.FieldLogger, 63 dashboardConfig dashboard.Config, 64 ) *UpdateEndpoint { 65 return &UpdateEndpoint{ 66 config: cfg, 67 log: log.WithField("service", "UpdateEndpoint"), 68 instanceStorage: instanceStorage, 69 runtimeStates: runtimeStates, 70 operationStorage: operationStorage, 71 contextUpdateHandler: ctxUpdateHandler, 72 processingEnabled: processingEnabled, 73 subAccountMovementEnabled: subAccountMovementEnabled, 74 updatingQueue: queue, 75 plansConfig: plansConfig, 76 planDefaults: planDefaults, 77 dashboardConfig: dashboardConfig, 78 } 79 } 80 81 // Update modifies an existing service instance 82 // 83 // PATCH /v2/service_instances/{instance_id} 84 func (b *UpdateEndpoint) Update(_ context.Context, instanceID string, details domain.UpdateDetails, asyncAllowed bool) (domain.UpdateServiceSpec, error) { 85 logger := b.log.WithField("instanceID", instanceID) 86 logger.Infof("Updating instanceID: %s", instanceID) 87 logger.Infof("Updating asyncAllowed: %v", asyncAllowed) 88 logger.Infof("Parameters: '%s'", string(details.RawParameters)) 89 90 instance, err := b.instanceStorage.GetByID(instanceID) 91 if err != nil && dberr.IsNotFound(err) { 92 logger.Errorf("unable to get instance: %s", err.Error()) 93 return domain.UpdateServiceSpec{}, apiresponses.NewFailureResponse(err, http.StatusNotFound, fmt.Sprintf("could not execute update for instanceID %s", instanceID)) 94 } else if err != nil { 95 logger.Errorf("unable to get instance: %s", err.Error()) 96 return domain.UpdateServiceSpec{}, fmt.Errorf("unable to get instance") 97 } 98 logger.Infof("Plan ID/Name: %s/%s", instance.ServicePlanID, PlanNamesMapping[instance.ServicePlanID]) 99 100 var ersContext internal.ERSContext 101 err = json.Unmarshal(details.RawContext, &ersContext) 102 if err != nil { 103 logger.Errorf("unable to decode context: %s", err.Error()) 104 return domain.UpdateServiceSpec{}, fmt.Errorf("unable to unmarshal context") 105 } 106 logger.Infof("Global account ID: %s active: %s", instance.GlobalAccountID, ptr.BoolAsString(ersContext.Active)) 107 logger.Infof("Received context: %s", marshallRawContext(hideSensitiveDataFromRawContext(details.RawContext))) 108 109 // validation of incoming input 110 if err := b.validateWithJsonSchemaValidator(details, instance); err != nil { 111 return domain.UpdateServiceSpec{}, err 112 } 113 114 // If the param contains "expired" - then process expiration (save it in the instance) 115 instance, err = b.processExpirationParam(instance, details, ersContext, logger) 116 if err != nil { 117 logger.Errorf("Unable to update the instance: %s", err.Error()) 118 return domain.UpdateServiceSpec{}, err 119 } 120 121 lastProvisioningOperation, err := b.operationStorage.GetProvisioningOperationByInstanceID(instance.InstanceID) 122 if err != nil { 123 logger.Errorf("cannot fetch provisioning lastProvisioningOperation for instance with ID: %s : %s", instance.InstanceID, err.Error()) 124 return domain.UpdateServiceSpec{}, fmt.Errorf("unable to process the update") 125 } 126 if lastProvisioningOperation.State == domain.Failed && !instance.IsExpired() { 127 return domain.UpdateServiceSpec{}, apiresponses.NewFailureResponse(fmt.Errorf("Unable to process an update of a failed instance"), http.StatusUnprocessableEntity, "") 128 } 129 130 lastDeprovisioningOperation, err := b.operationStorage.GetDeprovisioningOperationByInstanceID(instance.InstanceID) 131 if err != nil && !dberr.IsNotFound(err) { 132 logger.Errorf("cannot fetch deprovisioning for instance with ID: %s : %s", instance.InstanceID, err.Error()) 133 return domain.UpdateServiceSpec{}, fmt.Errorf("unable to process the update") 134 } 135 if err == nil { 136 if !lastDeprovisioningOperation.Temporary && !instance.IsExpired() { 137 // it is not a suspension, but real deprovisioning 138 logger.Warnf("Cannot process update, the instance has started deprovisioning process (operationID=%s)", lastDeprovisioningOperation.Operation.ID) 139 return domain.UpdateServiceSpec{}, apiresponses.NewFailureResponse(fmt.Errorf("Unable to process an update of a deprovisioned instance"), http.StatusUnprocessableEntity, "") 140 } 141 } 142 143 dashboardURL := instance.DashboardURL 144 if b.dashboardConfig.LandscapeURL != "" { 145 dashboardURL = fmt.Sprintf("%s/?kubeconfigID=%s", b.dashboardConfig.LandscapeURL, instanceID) 146 instance.DashboardURL = dashboardURL 147 } 148 149 if b.processingEnabled { 150 instance, suspendStatusChange, err := b.processContext(instance, details, lastProvisioningOperation, logger) 151 if err != nil { 152 return domain.UpdateServiceSpec{}, err 153 } 154 155 // NOTE: KEB currently can't process update parameters in one call along with context update 156 // this block makes it that KEB ignores any parameters updates if context update changed suspension state 157 if !suspendStatusChange && !instance.IsExpired() { 158 return b.processUpdateParameters(instance, details, lastProvisioningOperation, asyncAllowed, ersContext, logger) 159 } 160 } 161 162 return domain.UpdateServiceSpec{ 163 IsAsync: false, 164 DashboardURL: dashboardURL, 165 OperationData: "", 166 Metadata: domain.InstanceMetadata{ 167 Labels: ResponseLabels(*lastProvisioningOperation, *instance, b.config.URL, b.config.EnableKubeconfigURLLabel), 168 }, 169 }, nil 170 } 171 172 func (b *UpdateEndpoint) validateWithJsonSchemaValidator(details domain.UpdateDetails, instance *internal.Instance) error { 173 if len(details.RawParameters) > 0 { 174 planValidator, err := b.getJsonSchemaValidator(instance.Provider, instance.ServicePlanID, instance.ProviderRegion) 175 if err != nil { 176 return fmt.Errorf("while creating plan validator: %w", err) 177 } 178 result, err := planValidator.ValidateString(string(details.RawParameters)) 179 if err != nil { 180 return fmt.Errorf("while executing JSON schema validator: %w", err) 181 } 182 if !result.Valid { 183 return fmt.Errorf("while validating update parameters: %w", result.Error) 184 } 185 } 186 return nil 187 } 188 189 func shouldUpdate(instance *internal.Instance, details domain.UpdateDetails, ersContext internal.ERSContext) bool { 190 if len(details.RawParameters) != 0 { 191 return true 192 } 193 return ersContext.ERSUpdate() 194 } 195 196 func (b *UpdateEndpoint) processUpdateParameters(instance *internal.Instance, details domain.UpdateDetails, lastProvisioningOperation *internal.ProvisioningOperation, asyncAllowed bool, ersContext internal.ERSContext, logger logrus.FieldLogger) (domain.UpdateServiceSpec, error) { 197 if !shouldUpdate(instance, details, ersContext) { 198 logger.Debugf("Parameters not provided, skipping processing update parameters") 199 return domain.UpdateServiceSpec{ 200 IsAsync: false, 201 DashboardURL: instance.DashboardURL, 202 OperationData: "", 203 Metadata: domain.InstanceMetadata{ 204 Labels: ResponseLabels(*lastProvisioningOperation, *instance, b.config.URL, b.config.EnableKubeconfigURLLabel), 205 }, 206 }, nil 207 } 208 // asyncAllowed needed, see https://github.com/openservicebrokerapi/servicebroker/blob/v2.16/spec.md#updating-a-service-instance 209 if !asyncAllowed { 210 return domain.UpdateServiceSpec{}, apiresponses.ErrAsyncRequired 211 } 212 var params internal.UpdatingParametersDTO 213 if len(details.RawParameters) != 0 { 214 err := json.Unmarshal(details.RawParameters, ¶ms) 215 if err != nil { 216 logger.Errorf("unable to unmarshal parameters: %s", err.Error()) 217 return domain.UpdateServiceSpec{}, fmt.Errorf("unable to unmarshal parameters") 218 } 219 logger.Debugf("Updating with params: %+v", params) 220 } 221 222 if params.OIDC.IsProvided() { 223 if err := params.OIDC.Validate(); err != nil { 224 logger.Errorf("invalid OIDC parameters: %s", err.Error()) 225 return domain.UpdateServiceSpec{}, apiresponses.NewFailureResponse(err, http.StatusUnprocessableEntity, err.Error()) 226 } 227 } 228 229 operationID := uuid.New().String() 230 logger = logger.WithField("operationID", operationID) 231 232 logger.Debugf("creating update operation %v", params) 233 operation := internal.NewUpdateOperation(operationID, instance, params) 234 planID := instance.Parameters.PlanID 235 if len(details.PlanID) != 0 { 236 planID = details.PlanID 237 } 238 defaults, err := b.planDefaults(planID, instance.Provider, &instance.Provider) 239 if err != nil { 240 logger.Errorf("unable to obtain plan defaults: %s", err.Error()) 241 return domain.UpdateServiceSpec{}, fmt.Errorf("unable to obtain plan defaults") 242 } 243 var autoscalerMin, autoscalerMax int 244 if defaults.GardenerConfig != nil { 245 p := defaults.GardenerConfig 246 autoscalerMin, autoscalerMax = p.AutoScalerMin, p.AutoScalerMax 247 } 248 if err := operation.ProvisioningParameters.Parameters.AutoScalerParameters.Validate(autoscalerMin, autoscalerMax); err != nil { 249 logger.Errorf("invalid autoscaler parameters: %s", err.Error()) 250 return domain.UpdateServiceSpec{}, apiresponses.NewFailureResponse(err, http.StatusUnprocessableEntity, err.Error()) 251 } 252 err = b.operationStorage.InsertOperation(operation) 253 if err != nil { 254 return domain.UpdateServiceSpec{}, err 255 } 256 257 var updateStorage []string 258 if params.OIDC.IsProvided() { 259 instance.Parameters.Parameters.OIDC = params.OIDC 260 updateStorage = append(updateStorage, "OIDC") 261 } 262 263 if len(params.RuntimeAdministrators) != 0 { 264 newAdministrators := make([]string, 0, len(params.RuntimeAdministrators)) 265 newAdministrators = append(newAdministrators, params.RuntimeAdministrators...) 266 instance.Parameters.Parameters.RuntimeAdministrators = newAdministrators 267 updateStorage = append(updateStorage, "Runtime Administrators") 268 } 269 270 if params.UpdateAutoScaler(&instance.Parameters.Parameters) { 271 updateStorage = append(updateStorage, "Auto Scaler parameters") 272 } 273 if params.MachineType != nil && *params.MachineType != "" { 274 instance.Parameters.Parameters.MachineType = params.MachineType 275 } 276 if len(updateStorage) > 0 { 277 if err := wait.Poll(500*time.Millisecond, 2*time.Second, func() (bool, error) { 278 instance, err = b.instanceStorage.Update(*instance) 279 if err != nil { 280 params := strings.Join(updateStorage, ", ") 281 logger.Warnf("unable to update instance with new %v (%s), retrying", params, err.Error()) 282 return false, nil 283 } 284 return true, nil 285 }); err != nil { 286 response := apiresponses.NewFailureResponse(fmt.Errorf("Update operation failed"), http.StatusInternalServerError, err.Error()) 287 return domain.UpdateServiceSpec{}, response 288 } 289 } 290 logger.Debugf("Adding update operation to the processing queue") 291 b.updatingQueue.Add(operationID) 292 293 return domain.UpdateServiceSpec{ 294 IsAsync: true, 295 DashboardURL: instance.DashboardURL, 296 OperationData: operation.ID, 297 Metadata: domain.InstanceMetadata{ 298 Labels: ResponseLabels(*lastProvisioningOperation, *instance, b.config.URL, b.config.EnableKubeconfigURLLabel), 299 }, 300 }, nil 301 } 302 303 func (b *UpdateEndpoint) processContext(instance *internal.Instance, details domain.UpdateDetails, lastProvisioningOperation *internal.ProvisioningOperation, logger logrus.FieldLogger) (*internal.Instance, bool, error) { 304 var ersContext internal.ERSContext 305 err := json.Unmarshal(details.RawContext, &ersContext) 306 if err != nil { 307 logger.Errorf("unable to decode context: %s", err.Error()) 308 return nil, false, fmt.Errorf("unable to unmarshal context") 309 } 310 logger.Infof("Global account ID: %s active: %s", instance.GlobalAccountID, ptr.BoolAsString(ersContext.Active)) 311 312 lastOp, err := b.operationStorage.GetLastOperation(instance.InstanceID) 313 if err != nil { 314 logger.Errorf("unable to get last operation: %s", err.Error()) 315 return nil, false, fmt.Errorf("failed to process ERS context") 316 } 317 318 // todo: remove the code below when we are sure the ERSContext contains required values. 319 // This code is done because the PATCH request contains only some of fields and that requests made the ERS context empty in the past. 320 existingSMOperatorCredentials := instance.Parameters.ErsContext.SMOperatorCredentials 321 instance.Parameters.ErsContext = lastProvisioningOperation.ProvisioningParameters.ErsContext 322 // but do not change existing SM operator credentials 323 instance.Parameters.ErsContext.SMOperatorCredentials = existingSMOperatorCredentials 324 instance.Parameters.ErsContext.Active, err = b.extractActiveValue(instance.InstanceID, *lastProvisioningOperation) 325 if err != nil { 326 return nil, false, fmt.Errorf("unable to process the update") 327 } 328 instance.Parameters.ErsContext = internal.InheritMissingERSContext(instance.Parameters.ErsContext, lastOp.ProvisioningParameters.ErsContext) 329 instance.Parameters.ErsContext = internal.UpdateInstanceERSContext(instance.Parameters.ErsContext, ersContext) 330 331 changed, err := b.contextUpdateHandler.Handle(instance, ersContext) 332 if err != nil { 333 logger.Errorf("processing context updated failed: %s", err.Error()) 334 return nil, changed, fmt.Errorf("unable to process the update") 335 } 336 337 // copy the Active flag if set 338 if ersContext.Active != nil { 339 instance.Parameters.ErsContext.Active = ersContext.Active 340 } 341 342 if b.subAccountMovementEnabled { 343 if instance.GlobalAccountID != ersContext.GlobalAccountID && ersContext.GlobalAccountID != "" { 344 if instance.SubscriptionGlobalAccountID == "" { 345 instance.SubscriptionGlobalAccountID = instance.GlobalAccountID 346 } 347 instance.GlobalAccountID = ersContext.GlobalAccountID 348 } 349 } 350 351 newInstance, err := b.instanceStorage.Update(*instance) 352 if err != nil { 353 logger.Errorf("processing context updated failed: %s", err.Error()) 354 return nil, changed, fmt.Errorf("unable to process the update") 355 } 356 357 return newInstance, changed, nil 358 } 359 360 func (b *UpdateEndpoint) extractActiveValue(id string, provisioning internal.ProvisioningOperation) (*bool, error) { 361 deprovisioning, dErr := b.operationStorage.GetDeprovisioningOperationByInstanceID(id) 362 if dErr != nil && !dberr.IsNotFound(dErr) { 363 b.log.Errorf("Unable to get deprovisioning operation for the instance %s to check the active flag: %s", id, dErr.Error()) 364 return nil, dErr 365 } 366 // there was no any deprovisioning in the past (any suspension) 367 if deprovisioning == nil { 368 return ptr.Bool(true), nil 369 } 370 371 return ptr.Bool(deprovisioning.CreatedAt.Before(provisioning.CreatedAt)), nil 372 } 373 374 func (b *UpdateEndpoint) isKyma2(instance *internal.Instance) (bool, string, error) { 375 s, err := b.runtimeStates.GetLatestWithKymaVersionByRuntimeID(instance.RuntimeID) 376 if err != nil { 377 return false, "", err 378 } 379 kv := s.GetKymaVersion() 380 return internal.DetermineMajorVersion(kv) == 2, kv, nil 381 } 382 383 // processExpirationParam returns true, if expired 384 func (b *UpdateEndpoint) processExpirationParam(instance *internal.Instance, details domain.UpdateDetails, ers internal.ERSContext, logger logrus.FieldLogger) (*internal.Instance, error) { 385 // if the instance was expired before (in the past), then no need to do any work 386 if instance.IsExpired() { 387 return instance, nil 388 } 389 if len(details.RawParameters) != 0 { 390 var params internal.UpdatingParametersDTO 391 err := json.Unmarshal(details.RawParameters, ¶ms) 392 if err != nil { 393 return instance, err 394 } 395 if params.Expired { 396 if !IsTrialPlan(instance.ServicePlanID) { 397 logger.Warn("Expiration of a non-trial instance is not supported") 398 return instance, apiresponses.NewFailureResponse(fmt.Errorf("expiration of a non-trial instance is not supported"), http.StatusBadRequest, "") 399 } 400 401 if ers.Active == nil || *ers.Active { 402 logger.Warn("Parameter 'expired' requires `context.active=false`") 403 return instance, apiresponses.NewFailureResponse(fmt.Errorf("context.active=false is required for expiration"), http.StatusBadRequest, "") 404 } 405 406 logger.Infof("Saving expiration param for an instance created at %s", instance.CreatedAt) 407 instance.ExpiredAt = ptr.Time(time.Now()) 408 instance, err := b.instanceStorage.Update(*instance) 409 return instance, err 410 } 411 } 412 return instance, nil 413 414 } 415 416 func (b *UpdateEndpoint) getJsonSchemaValidator(provider internal.CloudProvider, planID string, platformRegion string) (JSONSchemaValidator, error) { 417 plans := Plans(b.plansConfig, provider, b.config.IncludeAdditionalParamsInSchema, euaccess.IsEURestrictedAccess(platformRegion), b.config.RegionParameterIsRequired, false) 418 plan := plans[planID] 419 schema := string(Marshal(plan.Schemas.Instance.Update.Parameters)) 420 421 return jsonschema.NewValidatorFromStringSchema(schema) 422 }