github.com/kyma-project/kyma-environment-broker@v0.0.1/internal/runtime/handler.go (about)

     1  package runtime
     2  
     3  import (
     4  	"fmt"
     5  	"net/http"
     6  	"time"
     7  
     8  	"github.com/kyma-project/kyma-environment-broker/internal/ptr"
     9  	"github.com/pivotal-cf/brokerapi/v8/domain"
    10  	"golang.org/x/exp/slices"
    11  
    12  	"github.com/gorilla/mux"
    13  	"github.com/kyma-project/kyma-environment-broker/common/orchestration"
    14  	"github.com/kyma-project/kyma-environment-broker/common/pagination"
    15  	pkg "github.com/kyma-project/kyma-environment-broker/common/runtime"
    16  	"github.com/kyma-project/kyma-environment-broker/internal"
    17  	"github.com/kyma-project/kyma-environment-broker/internal/httputil"
    18  	"github.com/kyma-project/kyma-environment-broker/internal/storage"
    19  	"github.com/kyma-project/kyma-environment-broker/internal/storage/dberr"
    20  	"github.com/kyma-project/kyma-environment-broker/internal/storage/dbmodel"
    21  )
    22  
    23  const numberOfUpgradeOperationsToReturn = 2
    24  
    25  type Handler struct {
    26  	instancesDb     storage.Instances
    27  	operationsDb    storage.Operations
    28  	runtimeStatesDb storage.RuntimeStates
    29  	converter       Converter
    30  
    31  	defaultMaxPage int
    32  }
    33  
    34  func NewHandler(instanceDb storage.Instances, operationDb storage.Operations, runtimeStatesDb storage.RuntimeStates, defaultMaxPage int, defaultRequestRegion string) *Handler {
    35  	return &Handler{
    36  		instancesDb:     instanceDb,
    37  		operationsDb:    operationDb,
    38  		runtimeStatesDb: runtimeStatesDb,
    39  		converter:       NewConverter(defaultRequestRegion),
    40  		defaultMaxPage:  defaultMaxPage,
    41  	}
    42  }
    43  
    44  func (h *Handler) AttachRoutes(router *mux.Router) {
    45  	router.HandleFunc("/runtimes", h.getRuntimes)
    46  }
    47  
    48  func findLastDeprovisioning(operations []internal.Operation) internal.Operation {
    49  	for i := len(operations) - 1; i > 0; i-- {
    50  		o := operations[i]
    51  		if o.Type != internal.OperationTypeDeprovision {
    52  			continue
    53  		}
    54  		if o.State != domain.Succeeded {
    55  			continue
    56  		}
    57  		return o
    58  	}
    59  	return operations[len(operations)-1]
    60  }
    61  
    62  func recreateInstances(operations []internal.Operation) []internal.Instance {
    63  	byInstance := make(map[string][]internal.Operation)
    64  	for _, o := range operations {
    65  		byInstance[o.InstanceID] = append(byInstance[o.InstanceID], o)
    66  	}
    67  	var instances []internal.Instance
    68  	for id, op := range byInstance {
    69  		o := op[0]
    70  		last := findLastDeprovisioning(op)
    71  		instances = append(instances, internal.Instance{
    72  			InstanceID:      id,
    73  			GlobalAccountID: o.GlobalAccountID,
    74  			SubAccountID:    o.SubAccountID,
    75  			RuntimeID:       o.RuntimeID,
    76  			CreatedAt:       o.CreatedAt,
    77  			ServicePlanID:   o.ProvisioningParameters.PlanID,
    78  			DeletedAt:       last.UpdatedAt,
    79  			InstanceDetails: last.InstanceDetails,
    80  			Parameters:      last.ProvisioningParameters,
    81  		})
    82  	}
    83  	return instances
    84  }
    85  
    86  func unionInstances(sets ...[]internal.Instance) (union []internal.Instance) {
    87  	m := make(map[string]internal.Instance)
    88  	for _, s := range sets {
    89  		for _, i := range s {
    90  			if _, exists := m[i.InstanceID]; !exists {
    91  				m[i.InstanceID] = i
    92  			}
    93  		}
    94  	}
    95  	for _, i := range m {
    96  		union = append(union, i)
    97  	}
    98  	return
    99  }
   100  
   101  func (h *Handler) listInstances(filter dbmodel.InstanceFilter) ([]internal.Instance, int, int, error) {
   102  	if slices.Contains(filter.States, dbmodel.InstanceDeprovisioned) {
   103  		// try to list instances where deletion didn't finish successfully
   104  		// entry in the Instances table still exists but has deletion timestamp and contains list of incomplete steps
   105  		deletionAttempted := true
   106  		filter.DeletionAttempted = &deletionAttempted
   107  		instances, instancesCount, instancesTotalCount, _ := h.instancesDb.List(filter)
   108  
   109  		// try to recreate instances from the operations table where entry in the instances table is gone
   110  		opFilter := dbmodel.OperationFilter{}
   111  		opFilter.InstanceFilter = &filter
   112  		opFilter.Page = filter.Page
   113  		opFilter.PageSize = filter.PageSize
   114  		operations, _, _, err := h.operationsDb.ListOperations(opFilter)
   115  		if err != nil {
   116  			return instances, instancesCount, instancesTotalCount, err
   117  		}
   118  		instancesFromOperations := recreateInstances(operations)
   119  
   120  		// return union of both sets of instances
   121  		instancesUnion := unionInstances(instances, instancesFromOperations)
   122  		count := len(instancesFromOperations)
   123  		return instancesUnion, count + instancesCount, count + instancesTotalCount, nil
   124  	}
   125  	return h.instancesDb.List(filter)
   126  }
   127  
   128  func (h *Handler) getRuntimes(w http.ResponseWriter, req *http.Request) {
   129  	toReturn := make([]pkg.RuntimeDTO, 0)
   130  
   131  	pageSize, page, err := pagination.ExtractPaginationConfigFromRequest(req, h.defaultMaxPage)
   132  	if err != nil {
   133  		httputil.WriteErrorResponse(w, http.StatusBadRequest, fmt.Errorf("while getting query parameters: %w", err))
   134  		return
   135  	}
   136  	filter := h.getFilters(req)
   137  	filter.PageSize = pageSize
   138  	filter.Page = page
   139  	opDetail := getOpDetail(req)
   140  	kymaConfig := getBoolParam(pkg.KymaConfigParam, req)
   141  	clusterConfig := getBoolParam(pkg.ClusterConfigParam, req)
   142  
   143  	instances, count, totalCount, err := h.listInstances(filter)
   144  	if err != nil {
   145  		httputil.WriteErrorResponse(w, http.StatusInternalServerError, fmt.Errorf("while fetching instances: %w", err))
   146  		return
   147  	}
   148  
   149  	for _, instance := range instances {
   150  		dto, err := h.converter.NewDTO(instance)
   151  		if err != nil {
   152  			httputil.WriteErrorResponse(w, http.StatusInternalServerError, fmt.Errorf("while converting instance to DTO: %w", err))
   153  			return
   154  		}
   155  
   156  		switch opDetail {
   157  		case pkg.AllOperation:
   158  			err = h.setRuntimeAllOperations(instance, &dto)
   159  		case pkg.LastOperation:
   160  			err = h.setRuntimeLastOperation(instance, &dto)
   161  		}
   162  		if err != nil {
   163  			httputil.WriteErrorResponse(w, http.StatusInternalServerError, err)
   164  			return
   165  		}
   166  
   167  		err = h.determineStatusModifiedAt(&dto)
   168  		if err != nil {
   169  			httputil.WriteErrorResponse(w, http.StatusInternalServerError, err)
   170  			return
   171  		}
   172  		err = h.setRuntimeOptionalAttributes(instance, &dto, kymaConfig, clusterConfig)
   173  		if err != nil {
   174  			httputil.WriteErrorResponse(w, http.StatusInternalServerError, err)
   175  			return
   176  		}
   177  
   178  		toReturn = append(toReturn, dto)
   179  	}
   180  
   181  	runtimePage := pkg.RuntimesPage{
   182  		Data:       toReturn,
   183  		Count:      count,
   184  		TotalCount: totalCount,
   185  	}
   186  	httputil.WriteResponse(w, http.StatusOK, runtimePage)
   187  }
   188  
   189  func (h *Handler) takeLastNonDryRunOperations(oprs []internal.UpgradeKymaOperation) ([]internal.UpgradeKymaOperation, int) {
   190  	toReturn := make([]internal.UpgradeKymaOperation, 0)
   191  	totalCount := 0
   192  	for _, op := range oprs {
   193  		if op.DryRun {
   194  			continue
   195  		}
   196  		if len(toReturn) < numberOfUpgradeOperationsToReturn {
   197  			toReturn = append(toReturn, op)
   198  		}
   199  		totalCount = totalCount + 1
   200  	}
   201  	return toReturn, totalCount
   202  }
   203  
   204  func (h *Handler) takeLastNonDryRunClusterOperations(oprs []internal.UpgradeClusterOperation) ([]internal.UpgradeClusterOperation, int) {
   205  	toReturn := make([]internal.UpgradeClusterOperation, 0)
   206  	totalCount := 0
   207  	for _, op := range oprs {
   208  		if op.DryRun {
   209  			continue
   210  		}
   211  		if len(toReturn) < numberOfUpgradeOperationsToReturn {
   212  			toReturn = append(toReturn, op)
   213  		}
   214  		totalCount = totalCount + 1
   215  	}
   216  	return toReturn, totalCount
   217  }
   218  
   219  func (h *Handler) determineStatusModifiedAt(dto *pkg.RuntimeDTO) error {
   220  	// Determine runtime modifiedAt timestamp based on the last operation of the runtime
   221  	last, err := h.operationsDb.GetLastOperation(dto.InstanceID)
   222  	if err != nil && !dberr.IsNotFound(err) {
   223  		return fmt.Errorf("while fetching last operation for instance %s: %w", dto.InstanceID, err)
   224  	}
   225  	if last != nil {
   226  		dto.Status.ModifiedAt = last.UpdatedAt
   227  	}
   228  	return nil
   229  }
   230  
   231  func (h *Handler) setRuntimeAllOperations(instance internal.Instance, dto *pkg.RuntimeDTO) error {
   232  	provOprs, err := h.operationsDb.ListProvisioningOperationsByInstanceID(instance.InstanceID)
   233  	if err != nil && !dberr.IsNotFound(err) {
   234  		return fmt.Errorf("while fetching provisioning operations list for instance %s: %w", instance.InstanceID, err)
   235  	}
   236  	if len(provOprs) != 0 {
   237  		firstProvOp := &provOprs[len(provOprs)-1]
   238  		lastProvOp := provOprs[0]
   239  		// Set AVS evaluation ID based on the data in the last provisioning operation
   240  		dto.AVSInternalEvaluationID = lastProvOp.InstanceDetails.Avs.AvsEvaluationInternalId
   241  		h.converter.ApplyProvisioningOperation(dto, firstProvOp)
   242  		if len(provOprs) > 1 {
   243  			h.converter.ApplyUnsuspensionOperations(dto, provOprs[:len(provOprs)-1])
   244  		}
   245  	}
   246  
   247  	deprovOprs, err := h.operationsDb.ListDeprovisioningOperationsByInstanceID(instance.InstanceID)
   248  	if err != nil && !dberr.IsNotFound(err) {
   249  		return fmt.Errorf("while fetching deprovisioning operations list for instance %s: %w", instance.InstanceID, err)
   250  	}
   251  	var deprovOp *internal.DeprovisioningOperation
   252  	if len(deprovOprs) != 0 {
   253  		for _, op := range deprovOprs {
   254  			if !op.Temporary {
   255  				deprovOp = &op
   256  				break
   257  			}
   258  		}
   259  	}
   260  	h.converter.ApplyDeprovisioningOperation(dto, deprovOp)
   261  	h.converter.ApplySuspensionOperations(dto, deprovOprs)
   262  
   263  	ukOprs, err := h.operationsDb.ListUpgradeKymaOperationsByInstanceID(instance.InstanceID)
   264  	if err != nil && !dberr.IsNotFound(err) {
   265  		return fmt.Errorf("while fetching upgrade kyma operation for instance %s: %w", instance.InstanceID, err)
   266  	}
   267  	dto.KymaVersion = determineKymaVersion(provOprs, ukOprs)
   268  	ukOprs, totalCount := h.takeLastNonDryRunOperations(ukOprs)
   269  	h.converter.ApplyUpgradingKymaOperations(dto, ukOprs, totalCount)
   270  
   271  	ucOprs, err := h.operationsDb.ListUpgradeClusterOperationsByInstanceID(instance.InstanceID)
   272  	if err != nil && !dberr.IsNotFound(err) {
   273  		return fmt.Errorf("while fetching upgrade cluster operation for instance %s: %w", instance.InstanceID, err)
   274  	}
   275  	ucOprs, totalCount = h.takeLastNonDryRunClusterOperations(ucOprs)
   276  	h.converter.ApplyUpgradingClusterOperations(dto, ucOprs, totalCount)
   277  
   278  	uOprs, err := h.operationsDb.ListUpdatingOperationsByInstanceID(instance.InstanceID)
   279  	if err != nil && !dberr.IsNotFound(err) {
   280  		return fmt.Errorf("while fetching update operation for instance %s: %w", instance.InstanceID, err)
   281  	}
   282  	totalCount = len(uOprs)
   283  	if len(uOprs) > numberOfUpgradeOperationsToReturn {
   284  		uOprs = uOprs[0:numberOfUpgradeOperationsToReturn]
   285  	}
   286  	h.converter.ApplyUpdateOperations(dto, uOprs, totalCount)
   287  
   288  	return nil
   289  }
   290  
   291  func (h *Handler) setRuntimeLastOperation(instance internal.Instance, dto *pkg.RuntimeDTO) error {
   292  	lastOp, err := h.operationsDb.GetLastOperation(instance.InstanceID)
   293  	if err != nil {
   294  		return fmt.Errorf("while fetching last operation instance %s: %w", instance.InstanceID, err)
   295  	}
   296  
   297  	// Set AVS evaluation ID based on the data in the last operation
   298  	dto.AVSInternalEvaluationID = lastOp.InstanceDetails.Avs.AvsEvaluationInternalId
   299  
   300  	switch lastOp.Type {
   301  	case internal.OperationTypeProvision:
   302  		provOps, err := h.operationsDb.ListProvisioningOperationsByInstanceID(instance.InstanceID)
   303  		if err != nil {
   304  			return fmt.Errorf("while fetching provisioning operations for instance %s: %w", instance.InstanceID, err)
   305  		}
   306  		lastProvOp := &provOps[0]
   307  		if len(provOps) > 1 {
   308  			h.converter.ApplyUnsuspensionOperations(dto, []internal.ProvisioningOperation{*lastProvOp})
   309  		} else {
   310  			h.converter.ApplyProvisioningOperation(dto, lastProvOp)
   311  		}
   312  
   313  	case internal.OperationTypeDeprovision:
   314  		deprovOp, err := h.operationsDb.GetDeprovisioningOperationByID(lastOp.ID)
   315  		if err != nil {
   316  			return fmt.Errorf("while fetching deprovisioning operation for instance %s: %w", instance.InstanceID, err)
   317  		}
   318  		if deprovOp.Temporary {
   319  			h.converter.ApplySuspensionOperations(dto, []internal.DeprovisioningOperation{*deprovOp})
   320  		} else {
   321  			h.converter.ApplyDeprovisioningOperation(dto, deprovOp)
   322  		}
   323  
   324  	case internal.OperationTypeUpgradeKyma:
   325  		upgKymaOp, err := h.operationsDb.GetUpgradeKymaOperationByID(lastOp.ID)
   326  		if err != nil {
   327  			return fmt.Errorf("while fetching upgrade kyma operation for instance %s: %w", instance.InstanceID, err)
   328  		}
   329  		h.converter.ApplyUpgradingKymaOperations(dto, []internal.UpgradeKymaOperation{*upgKymaOp}, 1)
   330  
   331  	case internal.OperationTypeUpgradeCluster:
   332  		upgClusterOp, err := h.operationsDb.GetUpgradeClusterOperationByID(lastOp.ID)
   333  		if err != nil {
   334  			return fmt.Errorf("while fetching upgrade cluster operation for instance %s: %w", instance.InstanceID, err)
   335  		}
   336  		h.converter.ApplyUpgradingClusterOperations(dto, []internal.UpgradeClusterOperation{*upgClusterOp}, 1)
   337  
   338  	case internal.OperationTypeUpdate:
   339  		updOp, err := h.operationsDb.GetUpdatingOperationByID(lastOp.ID)
   340  		if err != nil {
   341  			return fmt.Errorf("while fetching update operation for instance %s: %w", instance.InstanceID, err)
   342  		}
   343  		h.converter.ApplyUpdateOperations(dto, []internal.UpdatingOperation{*updOp}, 1)
   344  
   345  	default:
   346  		return fmt.Errorf("unsupported operation type: %s", lastOp.Type)
   347  	}
   348  
   349  	return nil
   350  }
   351  
   352  func (h *Handler) setRuntimeOptionalAttributes(instance internal.Instance, dto *pkg.RuntimeDTO, kymaConfig, clusterConfig bool) error {
   353  	if kymaConfig || clusterConfig {
   354  		states, err := h.runtimeStatesDb.ListByRuntimeID(instance.RuntimeID)
   355  		if err != nil && !dberr.IsNotFound(err) {
   356  			return fmt.Errorf("while fetching runtime states for instance %s: %w", instance.InstanceID, err)
   357  		}
   358  		for _, state := range states {
   359  			if kymaConfig && dto.KymaConfig == nil && state.KymaConfig.Version != "" {
   360  				config := state.KymaConfig
   361  				dto.KymaConfig = &config
   362  			}
   363  			if clusterConfig && dto.ClusterConfig == nil && state.ClusterConfig.Provider != "" {
   364  				config := state.ClusterConfig
   365  				dto.ClusterConfig = &config
   366  			}
   367  			if dto.KymaConfig != nil && dto.ClusterConfig != nil {
   368  				break
   369  			}
   370  		}
   371  	}
   372  
   373  	return nil
   374  }
   375  
   376  func determineKymaVersion(pOprs []internal.ProvisioningOperation, uOprs []internal.UpgradeKymaOperation) string {
   377  	kymaVersion := ""
   378  	kymaVersionSetAt := time.Time{}
   379  
   380  	// Set kyma version from the last provisioning operation
   381  	if len(pOprs) != 0 {
   382  		kymaVersion = pOprs[0].RuntimeVersion.Version
   383  		kymaVersionSetAt = pOprs[0].CreatedAt
   384  	}
   385  
   386  	// Take the last upgrade kyma operation which
   387  	//   - is not dry-run
   388  	//   - is created after the last provisioning operation
   389  	//   - has the kyma version set
   390  	//   - has been processed, i.e. not pending, canceling or canceled
   391  	// Use the last provisioning kyma version if no such upgrade operation was found, or the processed upgrade happened before the last provisioning operation.
   392  	for _, u := range uOprs {
   393  		if !u.DryRun && u.CreatedAt.After(kymaVersionSetAt) && u.RuntimeVersion.Version != "" && u.State != orchestration.Pending && u.State != orchestration.Canceling && u.State != orchestration.Canceled {
   394  			kymaVersion = u.RuntimeVersion.Version
   395  			break
   396  		} else if u.CreatedAt.Before(kymaVersionSetAt) {
   397  			break
   398  		}
   399  	}
   400  
   401  	return kymaVersion
   402  }
   403  
   404  func (h *Handler) getFilters(req *http.Request) dbmodel.InstanceFilter {
   405  	var filter dbmodel.InstanceFilter
   406  	query := req.URL.Query()
   407  	// For optional filter, zero value (nil) is fine if not supplied
   408  	filter.GlobalAccountIDs = query[pkg.GlobalAccountIDParam]
   409  	filter.SubAccountIDs = query[pkg.SubAccountIDParam]
   410  	filter.InstanceIDs = query[pkg.InstanceIDParam]
   411  	filter.RuntimeIDs = query[pkg.RuntimeIDParam]
   412  	filter.Regions = query[pkg.RegionParam]
   413  	filter.Shoots = query[pkg.ShootParam]
   414  	filter.Plans = query[pkg.PlanParam]
   415  	if v, exists := query[pkg.ExpiredParam]; exists && v[0] == "true" {
   416  		filter.Expired = ptr.Bool(true)
   417  	}
   418  	states := query[pkg.StateParam]
   419  	if len(states) == 0 {
   420  		// By default if no state filters are specified, suspended/deprovisioned runtimes are still excluded.
   421  		filter.States = append(filter.States, dbmodel.InstanceNotDeprovisioned)
   422  	} else {
   423  		allState := false
   424  		for _, s := range states {
   425  			switch pkg.State(s) {
   426  			case pkg.StateSucceeded:
   427  				filter.States = append(filter.States, dbmodel.InstanceSucceeded)
   428  			case pkg.StateFailed:
   429  				filter.States = append(filter.States, dbmodel.InstanceFailed)
   430  			case pkg.StateError:
   431  				filter.States = append(filter.States, dbmodel.InstanceError)
   432  			case pkg.StateProvisioning:
   433  				filter.States = append(filter.States, dbmodel.InstanceProvisioning)
   434  			case pkg.StateDeprovisioning:
   435  				filter.States = append(filter.States, dbmodel.InstanceDeprovisioning)
   436  			case pkg.StateUpgrading:
   437  				filter.States = append(filter.States, dbmodel.InstanceUpgrading)
   438  			case pkg.StateUpdating:
   439  				filter.States = append(filter.States, dbmodel.InstanceUpdating)
   440  			case pkg.StateSuspended:
   441  				filter.States = append(filter.States, dbmodel.InstanceDeprovisioned)
   442  			case pkg.StateDeprovisioned:
   443  				filter.States = append(filter.States, dbmodel.InstanceDeprovisioned)
   444  			case pkg.StateDeprovisionIncomplete:
   445  				deletionAttempted := true
   446  				filter.DeletionAttempted = &deletionAttempted
   447  			case pkg.AllState:
   448  				allState = true
   449  			}
   450  		}
   451  		if allState {
   452  			filter.States = nil
   453  		}
   454  	}
   455  
   456  	return filter
   457  }
   458  
   459  func getOpDetail(req *http.Request) pkg.OperationDetail {
   460  	opDetail := pkg.AllOperation
   461  	opDetailParams := req.URL.Query()[pkg.OperationDetailParam]
   462  	for _, p := range opDetailParams {
   463  		opDetailParam := pkg.OperationDetail(p)
   464  		switch opDetailParam {
   465  		case pkg.AllOperation, pkg.LastOperation:
   466  			opDetail = opDetailParam
   467  		}
   468  	}
   469  
   470  	return opDetail
   471  }
   472  
   473  func getBoolParam(param string, req *http.Request) bool {
   474  	requested := false
   475  	params := req.URL.Query()[param]
   476  	for _, p := range params {
   477  		if p == "true" {
   478  			requested = true
   479  			break
   480  		}
   481  	}
   482  
   483  	return requested
   484  }