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  }