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

     1  package environmentscleanup
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"strings"
     7  	"time"
     8  
     9  	error2 "github.com/kyma-project/kyma-environment-broker/internal/error"
    10  	"github.com/kyma-project/kyma-environment-broker/internal/storage/dberr"
    11  
    12  	"github.com/hashicorp/go-multierror"
    13  	"github.com/kyma-project/kyma-environment-broker/common/gardener"
    14  	"github.com/kyma-project/kyma-environment-broker/internal"
    15  	"github.com/kyma-project/kyma-environment-broker/internal/storage"
    16  	log "github.com/sirupsen/logrus"
    17  	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    18  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    19  )
    20  
    21  const (
    22  	shootAnnotationRuntimeId = "kcp.provisioner.kyma-project.io/runtime-id"
    23  	shootLabelAccountId      = "account"
    24  )
    25  
    26  //go:generate mockery --name=GardenerClient --output=automock
    27  type GardenerClient interface {
    28  	List(context context.Context, opts v1.ListOptions) (*unstructured.UnstructuredList, error)
    29  	Get(ctx context.Context, name string, options v1.GetOptions, subresources ...string) (*unstructured.Unstructured, error)
    30  	Delete(ctx context.Context, name string, options v1.DeleteOptions, subresources ...string) error
    31  	Update(ctx context.Context, obj *unstructured.Unstructured, options v1.UpdateOptions, subresources ...string) (*unstructured.Unstructured, error)
    32  }
    33  
    34  //go:generate mockery --name=BrokerClient --output=automock
    35  type BrokerClient interface {
    36  	Deprovision(instance internal.Instance) (string, error)
    37  }
    38  
    39  //go:generate mockery --name=ProvisionerClient --output=automock
    40  type ProvisionerClient interface {
    41  	DeprovisionRuntime(accountID, runtimeID string) (string, error)
    42  }
    43  
    44  type Service struct {
    45  	gardenerService   GardenerClient
    46  	brokerService     BrokerClient
    47  	instanceStorage   storage.Instances
    48  	logger            *log.Logger
    49  	MaxShootAge       time.Duration
    50  	LabelSelector     string
    51  	provisionerClient ProvisionerClient
    52  }
    53  
    54  type runtime struct {
    55  	ID        string
    56  	AccountID string
    57  }
    58  
    59  func NewService(gardenerClient GardenerClient, brokerClient BrokerClient, provisionerClient ProvisionerClient, instanceStorage storage.Instances, logger *log.Logger, maxShootAge time.Duration, labelSelector string) *Service {
    60  	return &Service{
    61  		gardenerService:   gardenerClient,
    62  		brokerService:     brokerClient,
    63  		instanceStorage:   instanceStorage,
    64  		logger:            logger,
    65  		MaxShootAge:       maxShootAge,
    66  		LabelSelector:     labelSelector,
    67  		provisionerClient: provisionerClient,
    68  	}
    69  }
    70  
    71  func (s *Service) Run() error {
    72  	return s.PerformCleanup()
    73  }
    74  
    75  func (s *Service) PerformCleanup() error {
    76  	runtimesToDelete, shootsToDelete, err := s.getStaleRuntimesByShoots(s.LabelSelector)
    77  	if err != nil {
    78  		s.logger.Error(fmt.Errorf("while getting stale shoots to delete: %w", err))
    79  		return err
    80  	}
    81  
    82  	err = s.cleanupRuntimes(runtimesToDelete)
    83  	if err != nil {
    84  		s.logger.Error(fmt.Errorf("while cleaning runtimes: %w", err))
    85  		return err
    86  	}
    87  
    88  	return s.cleanupShoots(shootsToDelete)
    89  }
    90  
    91  func (s *Service) cleanupRuntimes(runtimes []runtime) error {
    92  	s.logger.Infof("Runtimes to process: %+v", runtimes)
    93  
    94  	if len(runtimes) == 0 {
    95  		return nil
    96  	}
    97  
    98  	return s.cleanUp(runtimes)
    99  }
   100  
   101  func (s *Service) cleanupShoots(shoots []unstructured.Unstructured) error {
   102  	// do not log all shoots as previously - too much info
   103  	s.logger.Infof("Number of shoots to process: %+v", len(shoots))
   104  
   105  	if len(shoots) == 0 {
   106  		return nil
   107  	}
   108  
   109  	for _, shoot := range shoots {
   110  		annotations := shoot.GetAnnotations()
   111  		annotations["confirmation.gardener.cloud/deletion"] = "true"
   112  		shoot.SetAnnotations(annotations)
   113  		_, err := s.gardenerService.Update(context.Background(), &shoot, v1.UpdateOptions{})
   114  		if err != nil {
   115  			s.logger.Error(fmt.Errorf("while annotating shoot with removal confirmation: %w", err))
   116  		}
   117  
   118  		err = s.gardenerService.Delete(context.Background(), shoot.GetName(), v1.DeleteOptions{})
   119  		if err != nil {
   120  			s.logger.Error(fmt.Errorf("while cleaning runtimes: %w", err))
   121  		}
   122  	}
   123  
   124  	return nil
   125  }
   126  
   127  func (s *Service) getStaleRuntimesByShoots(labelSelector string) ([]runtime, []unstructured.Unstructured, error) {
   128  	opts := v1.ListOptions{
   129  		LabelSelector: labelSelector,
   130  	}
   131  	shootList, err := s.gardenerService.List(context.Background(), opts)
   132  	if err != nil {
   133  		return []runtime{}, []unstructured.Unstructured{}, fmt.Errorf("while listing Gardener shoots: %w", err)
   134  	}
   135  
   136  	var runtimes []runtime
   137  	var shoots []unstructured.Unstructured
   138  	for _, shoot := range shootList.Items {
   139  		shootCreationTimestamp := shoot.GetCreationTimestamp()
   140  		shootAge := time.Since(shootCreationTimestamp.Time)
   141  
   142  		if shootAge.Hours() < s.MaxShootAge.Hours() {
   143  			log.Infof("Shoot %q is not older than %f hours with age: %f hours", shoot.GetName(), s.MaxShootAge.Hours(), shootAge.Hours())
   144  			continue
   145  		}
   146  
   147  		log.Infof("Shoot %q is older than %f hours with age: %f hours", shoot.GetName(), s.MaxShootAge.Hours(), shootAge.Hours())
   148  		staleRuntime, err := s.shootToRuntime(shoot)
   149  		if err != nil {
   150  			s.logger.Infof("found a shoot without kcp labels: %v", shoot.GetName())
   151  			shoots = append(shoots, shoot)
   152  			continue
   153  		}
   154  
   155  		runtimes = append(runtimes, *staleRuntime)
   156  	}
   157  
   158  	return runtimes, shoots, nil
   159  }
   160  
   161  func (s *Service) shootToRuntime(st unstructured.Unstructured) (*runtime, error) {
   162  	shoot := gardener.Shoot{st}
   163  	runtimeID, ok := shoot.GetAnnotations()[shootAnnotationRuntimeId]
   164  	if !ok {
   165  		return nil, fmt.Errorf("shoot %q has no runtime-id annotation", shoot.GetName())
   166  	}
   167  
   168  	accountID, ok := shoot.GetLabels()[shootLabelAccountId]
   169  	if !ok {
   170  		return nil, fmt.Errorf("shoot %q has no account label", shoot.GetName())
   171  	}
   172  
   173  	return &runtime{
   174  		ID:        runtimeID,
   175  		AccountID: accountID,
   176  	}, nil
   177  }
   178  
   179  func (s *Service) cleanUp(runtimesToDelete []runtime) error {
   180  	kebInstancesToDelete, err := s.getInstancesForRuntimes(runtimesToDelete)
   181  	if err != nil {
   182  		errMsg := fmt.Errorf("while getting instance IDs for Runtimes: %w", err)
   183  		s.logger.Error(errMsg)
   184  		if !dberr.IsNotFound(err) {
   185  			return errMsg
   186  		}
   187  	}
   188  
   189  	kebResult := s.cleanUpKEBInstances(kebInstancesToDelete)
   190  	provisionerResult := s.cleanUpProvisionerInstances(runtimesToDelete, kebInstancesToDelete)
   191  	result := multierror.Append(kebResult, provisionerResult)
   192  
   193  	if result != nil {
   194  		result.ErrorFormat = func(i []error) string {
   195  			var s []string
   196  			for _, v := range i {
   197  				s = append(s, v.Error())
   198  			}
   199  			return strings.Join(s, ", ")
   200  		}
   201  	}
   202  
   203  	return result.ErrorOrNil()
   204  }
   205  
   206  func (s *Service) getInstancesForRuntimes(runtimesToDelete []runtime) ([]internal.Instance, error) {
   207  
   208  	var runtimeIDsToDelete []string
   209  	for _, runtime := range runtimesToDelete {
   210  		runtimeIDsToDelete = append(runtimeIDsToDelete, runtime.ID)
   211  	}
   212  
   213  	instances, err := s.instanceStorage.FindAllInstancesForRuntimes(runtimeIDsToDelete)
   214  	if err != nil {
   215  		return []internal.Instance{}, err
   216  	}
   217  
   218  	return instances, nil
   219  }
   220  
   221  func (s *Service) cleanUpKEBInstances(instancesToDelete []internal.Instance) *multierror.Error {
   222  	var result *multierror.Error
   223  
   224  	for _, instance := range instancesToDelete {
   225  		s.logger.Infof("Triggering environment deprovisioning for instance ID %q", instance.InstanceID)
   226  		currentErr := s.triggerEnvironmentDeprovisioning(instance)
   227  		if currentErr != nil {
   228  			result = multierror.Append(result, currentErr)
   229  		}
   230  	}
   231  
   232  	return result
   233  }
   234  
   235  func (s *Service) cleanUpProvisionerInstances(runtimesToDelete []runtime, kebInstancesToDelete []internal.Instance) *multierror.Error {
   236  	kebInstanceExists := func(runtimeID string) bool {
   237  		for _, instance := range kebInstancesToDelete {
   238  			if instance.RuntimeID == runtimeID {
   239  				return true
   240  			}
   241  		}
   242  
   243  		return false
   244  	}
   245  
   246  	var result *multierror.Error
   247  
   248  	for _, runtime := range runtimesToDelete {
   249  		if !kebInstanceExists(runtime.ID) {
   250  			s.logger.Infof("Triggering runtime deprovisioning for runtimeID ID %q", runtime.ID)
   251  			err := s.triggerRuntimeDeprovisioning(runtime)
   252  			if err != nil {
   253  				result = multierror.Append(result, err)
   254  			}
   255  		}
   256  	}
   257  
   258  	return result
   259  }
   260  
   261  func (s *Service) triggerRuntimeDeprovisioning(runtime runtime) error {
   262  	operationID, err := s.provisionerClient.DeprovisionRuntime(runtime.AccountID, runtime.ID)
   263  	if error2.IsNotFoundError(err) {
   264  		s.logger.Warnf("Runtime %s does not exists in the provisioner, skipping", runtime.ID)
   265  		return nil
   266  	}
   267  	if err != nil {
   268  		err = fmt.Errorf("while deprovisioning runtime with Provisioner: %w", err)
   269  		s.logger.Error(err)
   270  		return err
   271  	}
   272  
   273  	log.Infof("Successfully send deprovision request to Provisioner, got operation ID %q", operationID)
   274  	return nil
   275  }
   276  
   277  func (s *Service) triggerEnvironmentDeprovisioning(instance internal.Instance) error {
   278  	opID, err := s.brokerService.Deprovision(instance)
   279  	if err != nil {
   280  		err = fmt.Errorf("while triggering deprovisioning for instance ID %q: %w", instance.InstanceID, err)
   281  		s.logger.Error(err)
   282  		return err
   283  	}
   284  
   285  	log.Infof("Successfully send deprovision request to Kyma Environment Broker, got operation ID %q", opID)
   286  	return nil
   287  }