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, &params)
   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, &params)
   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  }