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 }