github.com/cozy/cozy-stack@v0.0.0-20240327093429-939e4a21320e/model/instance/lifecycle/destroy.go (about) 1 package lifecycle 2 3 import ( 4 "errors" 5 "net/http" 6 "net/url" 7 "time" 8 9 "github.com/cozy/cozy-stack/model/account" 10 "github.com/cozy/cozy-stack/model/app" 11 "github.com/cozy/cozy-stack/model/instance" 12 job "github.com/cozy/cozy-stack/model/job" 13 "github.com/cozy/cozy-stack/pkg/config/config" 14 "github.com/cozy/cozy-stack/pkg/consts" 15 "github.com/cozy/cozy-stack/pkg/couchdb" 16 "github.com/cozy/cozy-stack/pkg/logger" 17 "github.com/cozy/cozy-stack/pkg/mail" 18 "github.com/labstack/echo/v4" 19 ) 20 21 func AskDeletion(inst *instance.Instance) error { 22 clouderies := config.GetConfig().Clouderies 23 var cloudery config.ClouderyConfig 24 cloudery, ok := clouderies[inst.ContextName] 25 if !ok { 26 cloudery = clouderies[config.DefaultInstanceContext] 27 } 28 29 api := cloudery.API 30 clouderyURL := api.URL 31 clouderyToken := api.Token 32 33 u, err := url.Parse(clouderyURL) 34 if err != nil { 35 return err 36 } 37 u.Path = "/api/admin/instances/" + url.PathEscape(inst.UUID) 38 39 req, err := http.NewRequest(http.MethodDelete, u.String(), nil) 40 if err != nil { 41 return err 42 } 43 req.Header.Add(echo.HeaderAuthorization, "Bearer "+clouderyToken) 44 res, err := managerHTTPClient.Do(req) 45 if err != nil { 46 return err 47 } 48 return res.Body.Close() 49 } 50 51 // Destroy is used to remove the instance. All the data linked to this 52 // instance will be permanently deleted. 53 func Destroy(domain string) error { 54 domain, err := validateDomain(domain) 55 if err != nil { 56 return err 57 } 58 inst, err := instance.GetFromCouch(domain) 59 if err != nil { 60 return err 61 } 62 63 // Check that we don't try to run twice the deletion of accounts 64 if inst.Deleting { 65 return instance.ErrDeletionAlreadyRequested 66 } 67 inst.Deleting = true 68 if err := instance.Update(inst); err != nil { 69 return err 70 } 71 72 // Deleting accounts manually to invoke the "account deletion hook" which may 73 // launch a worker in order to clean the account. 74 if err := deleteAccounts(inst); err != nil { 75 sendAlert(inst, err) 76 return err 77 } 78 79 // Reload the instance, it can have been updated in CouchDB if the instance 80 // had at least one account and was not up-to-date for its indexes/views. 81 inst, err = instance.GetFromCouch(domain) 82 if err != nil { 83 return err 84 } 85 inst.Deleting = false 86 _ = instance.Update(inst) 87 88 removeTriggers(inst) 89 90 if err = couchdb.DeleteAllDBs(inst); err != nil { 91 inst.Logger().Errorf("Could not delete all CouchDB databases: %s", err.Error()) 92 return err 93 } 94 95 if err = inst.VFS().Delete(); err != nil { 96 inst.Logger().Errorf("Could not delete VFS: %s", err.Error()) 97 return err 98 } 99 100 err = instance.Delete(inst) 101 if couchdb.IsConflictError(err) { 102 // We may need to try again as CouchDB can return an old version of 103 // this document when we have concurrent updates for indexes/views 104 // version and deleting flag. 105 time.Sleep(3 * time.Second) 106 inst, errg := instance.GetFromCouch(domain) 107 if couchdb.IsNotFoundError(errg) { 108 err = nil 109 } else if inst != nil { 110 err = instance.Delete(inst) 111 } 112 } 113 return err 114 } 115 116 func deleteAccounts(inst *instance.Instance) error { 117 var accounts []*account.Account 118 if err := couchdb.GetAllDocs(inst, consts.Accounts, nil, &accounts); err != nil { 119 if couchdb.IsNoDatabaseError(err) { 120 return nil 121 } 122 return err 123 } 124 125 var toClean []account.CleanEntry 126 for _, acc := range accounts { 127 // Accounts that are not tied to a konnector must not be deleted, and 128 // the aggregator accounts in particular. 129 slug := acc.AccountType 130 if slug == "" { 131 continue 132 } 133 man, err := app.GetKonnectorBySlug(inst, slug) 134 if errors.Is(err, app.ErrNotFound) { 135 copier := app.Copier(consts.KonnectorType, inst) 136 installer, erri := app.NewInstaller(inst, copier, 137 &app.InstallerOptions{ 138 Operation: app.Install, 139 Type: consts.KonnectorType, 140 SourceURL: "registry://" + slug + "/stable", 141 Slug: slug, 142 Registries: inst.Registries(), 143 }, 144 ) 145 if erri == nil { 146 if appManifest, erri := installer.RunSync(); erri == nil { 147 man = appManifest.(*app.KonnManifest) 148 err = nil 149 } 150 } 151 } 152 if err != nil { 153 return err 154 } 155 entry := account.CleanEntry{ 156 Account: acc, 157 Triggers: nil, // We don't care, the triggers will all be deleted a bit later 158 ManifestOnDelete: man.OnDeleteAccount() != "", 159 Slug: slug, 160 } 161 toClean = append(toClean, entry) 162 } 163 if len(toClean) == 0 { 164 return nil 165 } 166 167 return account.CleanAndWait(inst, toClean) 168 } 169 170 func sendAlert(inst *instance.Instance, e error) { 171 alert := config.GetConfig().AlertAddr 172 if alert == "" { 173 return 174 } 175 addr := &mail.Address{ 176 Name: "Support", 177 Email: alert, 178 } 179 values := map[string]interface{}{ 180 "Domain": inst.Domain, 181 "Error": e.Error(), 182 } 183 msg, err := job.NewMessage(mail.Options{ 184 Mode: mail.ModeFromUser, 185 To: []*mail.Address{addr}, 186 TemplateName: "alert_account", 187 TemplateValues: values, 188 Layout: mail.CozyCloudLayout, 189 Locale: consts.DefaultLocale, 190 }) 191 if err == nil { 192 _, _ = job.System().PushJob(inst, &job.JobRequest{ 193 WorkerType: "sendmail", 194 Message: msg, 195 }) 196 } 197 } 198 199 func removeTriggers(inst *instance.Instance) { 200 sched := job.System() 201 triggers, err := sched.GetAllTriggers(inst) 202 if err == nil { 203 for _, t := range triggers { 204 if err = sched.DeleteTrigger(inst, t.Infos().TID); err != nil { 205 logger.WithDomain(inst.Domain).Errorf( 206 "Failed to remove trigger: %s", err) 207 } 208 } 209 } 210 }