FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

feat: add migration utility · linagora/cozy-stack@f459d8d · GitHub

Repository navigation

Commit f459d8d

Browse files
committed
feat: add migration utility
1 parent 82227fc commit f459d8d

9 files changed

Lines changed: 304 additions & 25 deletions

File tree

‎cmd/fix.go‎

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"github.com/cozy/cozy-stack/client"
1717
"github.com/cozy/cozy-stack/client/request"
1818
"github.com/cozy/cozy-stack/model/contact"
19+
"github.com/cozy/cozy-stack/model/instance/lifecycle"
1920
"github.com/cozy/cozy-stack/model/vfs"
2021
"github.com/cozy/cozy-stack/pkg/consts"
2122

@@ -427,10 +428,69 @@ this instance are correctly set.
427428
},
428429
}
429430

431+
var internalEmailsFixer = &cobra.Command{
432+
Use: "internal-emails",
433+
Short: "Backfill the internal email of all the instances",
434+
Long: `
435+
This fixer sets the internal email of each instance from its settings email.
436+
It skips organization instances and keeps an internal email already set. Emails
437+
found on several instances are listed for manual resolution and left unset.
438+
If a settings email can't be read, nothing is written and the command fails.
439+
`,
440+
Example: `$ cozy-stack fix internal-emails --dry-run`,
441+
RunE: func(cmd *cobra.Command, args []string) error {
442+
if !dryRunFlag && !forceFlag {
443+
if err := askForConfirmation("Set the internal email on all instances?"); err != nil {
444+
return err
445+
}
446+
}
447+
c := newAdminClient()
448+
q := url.Values{}
449+
if dryRunFlag {
450+
q.Set("dry_run", "true")
451+
}
452+
res, err := c.Req(&request.Options{
453+
Method: "POST",
454+
Path: "/instances/fixers/internal-emails",
455+
Queries: q,
456+
})
457+
if err != nil {
458+
return err
459+
}
460+
defer res.Body.Close()
461+
var report lifecycle.InternalEmailsReport
462+
if err := json.NewDecoder(res.Body).Decode(&report); err != nil {
463+
return err
464+
}
465+
for email, domains := range report.Duplicates {
466+
fmt.Fprintf(os.Stdout, "Duplicate %s: %s\n", email, strings.Join(domains, ", "))
467+
}
468+
for _, domain := range report.MissingSettings {
469+
fmt.Fprintf(os.Stdout, "Missing settings: %s\n", domain)
470+
}
471+
for _, e := range report.Errors {
472+
fmt.Fprintf(os.Stderr, "Error: %s\n", e)
473+
}
474+
updated := "Updated"
475+
if dryRunFlag {
476+
updated = "Would update"
477+
}
478+
fmt.Fprintf(os.Stdout, "Scanned: %d\nMissing settings: %d\nEmpty emails: %d\nDuplicate groups: %d\n%s: %d\nSkipped: %d\nErrors: %d\n",
479+
report.Scanned, len(report.MissingSettings), report.EmptyEmails, len(report.Duplicates),
480+
updated, report.Updated, report.Skipped, len(report.Errors))
481+
if len(report.Errors) > 0 {
482+
return fmt.Errorf("%d errors, see above", len(report.Errors))
483+
}
484+
return nil
485+
},
486+
}
487+
430488
func init() {
431489
thumbnailsFixer.Flags().BoolVar(&dryRunFlag, "dry-run", false, "Dry run")
432490
thumbnailsFixer.Flags().BoolVar(&withMetadataFlag, "with-metadata", false, "Recalculate images metadata")
433491
redisFixer.Flags().BoolVar(&forceFlag, "force", false, "Do not ask for confirmation before fixing redis on all instances")
492+
internalEmailsFixer.Flags().BoolVar(&dryRunFlag, "dry-run", false, "Report what would change without writing")
493+
internalEmailsFixer.Flags().BoolVar(&forceFlag, "force", false, "Do not ask for confirmation before writing")
434494

435495
fixerCmdGroup.AddCommand(jobsFixer)
436496
fixerCmdGroup.AddCommand(mimeFixerCmd)
@@ -442,6 +502,7 @@ func init() {
442502
fixerCmdGroup.AddCommand(serviceTriggersFixer)
443503
fixerCmdGroup.AddCommand(sharingsMovedFixer)
444504
fixerCmdGroup.AddCommand(indexesFixer)
505+
fixerCmdGroup.AddCommand(internalEmailsFixer)
445506

446507
RootCmd.AddCommand(fixerCmdGroup)
447508
}

‎docs/admin.md‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,48 @@ Delete the accounts which are not linked to a konnector
425425
POST /instances/alice.cozy.localhost/fixers/orphan-account HTTP/1.1
426426
```
427427

428+
### POST /instances/fixers/internal-emails
429+
430+
Set the internal email of every instance from its settings email. Organization
431+
instances are skipped, and an internal email already set is kept. Emails found
432+
on several instances are listed in `duplicates` and left unset, for manual
433+
resolution. If a settings email can't be read, nothing is updated, since
434+
duplicates can't be ruled out. Each scanned instance is counted once in
435+
`missing_settings`, `empty_emails`, `updated`, `skipped` or `errors`.
436+
437+
#### Query-String
438+
439+
| Parameter | Description |
440+
| --------- | ---------------------------------------------------- |
441+
| dry_run | `true` to report what would be updated, but not save |
442+
443+
#### Request
444+
445+
```http
446+
POST /instances/fixers/internal-emails?dry_run=true HTTP/1.1
447+
```
448+
449+
#### Response
450+
451+
```http
452+
HTTP/1.1 200 OK
453+
Content-Type: application/json
454+
```
455+
456+
```json
457+
{
458+
"scanned": 4,
459+
"missing_settings": [],
460+
"empty_emails": 0,
461+
"duplicates": {
462+
"bob@example.com": ["bob.cozy.localhost", "bob2.cozy.localhost"]
463+
},
464+
"updated": 1,
465+
"skipped": 3,
466+
"errors": []
467+
}
468+
```
469+
428470
### POST /instances/:domain/export
429471

430472
Starts an export for the given instance. The CouchDB documents will be saved in

‎docs/cli/cozy-stack_fix.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ cozy-stack fix <command> [flags]
2727
* [cozy-stack](cozy-stack.md) - cozy-stack is the main command
2828
* [cozy-stack fix contact-emails](cozy-stack_fix_contact-emails.md) - Detect and try to fix invalid emails on contacts
2929
* [cozy-stack fix indexes](cozy-stack_fix_indexes.md) - Rebuild the CouchDB views and indexes
30+
* [cozy-stack fix internal-emails](cozy-stack_fix_internal-emails.md) - Backfill the internal email of all the instances
3031
* [cozy-stack fix jobs](cozy-stack_fix_jobs.md) - Take a look at the consistency of the jobs
3132
* [cozy-stack fix mime](cozy-stack_fix_mime.md) - Fix the class computed from the mime-type
3233
* [cozy-stack fix orphan-account](cozy-stack_fix_orphan-account.md) - Remove the orphan accounts
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
## cozy-stack fix internal-emails
2+
3+
Backfill the internal email of all the instances
4+
5+
### Synopsis
6+
7+
8+
This fixer sets the internal email of each instance from its settings email.
9+
It skips organization instances and keeps an internal email already set. Emails
10+
found on several instances are listed for manual resolution and left unset.
11+
If a settings email can't be read, nothing is written and the command fails.
12+
13+
14+
```
15+
cozy-stack fix internal-emails [flags]
16+
```
17+
18+
### Examples
19+
20+
```
21+
$ cozy-stack fix internal-emails --dry-run
22+
```
23+
24+
### Options
25+
26+
```
27+
--dry-run Report what would change without writing
28+
--force Do not ask for confirmation before writing
29+
-h, --help help for internal-emails
30+
```
31+
32+
### Options inherited from parent commands
33+
34+
```
35+
--admin-host string administration server host (default "localhost")
36+
--admin-port int administration server port (default 6060)
37+
-c, --config string configuration file (default "$HOME/.cozy.yaml")
38+
--host string server host (default "localhost")
39+
-p, --port int server port (default 8080)
40+
```
41+
42+
### SEE ALSO
43+
44+
* [cozy-stack fix](cozy-stack_fix.md) - A set of tools to fix issues or migrate content.
45+

‎model/instance/lifecycle/internal_email.go‎

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,74 @@ func GetInstanceByInternalEmail(email string) (*instance.Instance, error) {
4141
return nil, fmt.Errorf("internal email %s is on several instances", email)
4242
}
4343
}
44+
45+
// InternalEmailsReport sums up a backfill of internal emails.
46+
type InternalEmailsReport struct {
47+
Scanned int `json:"scanned"`
48+
MissingSettings []string `json:"missing_settings"`
49+
EmptyEmails int `json:"empty_emails"`
50+
Duplicates map[string][]string `json:"duplicates"`
51+
Updated int `json:"updated"`
52+
Skipped int `json:"skipped"`
53+
Errors []string `json:"errors"`
54+
}
55+
56+
// BackfillInternalEmails sets the internal email from the settings email. It
57+
// writes nothing if a settings read fails, as it may hide a duplicate.
58+
func BackfillInternalEmails(insts []*instance.Instance, dryRun bool) *InternalEmailsReport {
59+
report := &InternalEmailsReport{
60+
MissingSettings: []string{},
61+
Duplicates: map[string][]string{},
62+
Errors: []string{},
63+
}
64+
byEmail := make(map[string][]*instance.Instance)
65+
for _, inst := range insts {
66+
report.Scanned++
67+
if inst.IsOrganizationInstance() && inst.InternalEmail == "" {
68+
report.Skipped++
69+
continue
70+
}
71+
email := inst.InternalEmail
72+
if email == "" {
73+
var err error
74+
email, err = inst.SettingsEMail()
75+
if couchdb.IsNotFoundError(err) {
76+
report.MissingSettings = append(report.MissingSettings, inst.Domain)
77+
continue
78+
}
79+
if err != nil {
80+
report.Errors = append(report.Errors, fmt.Sprintf("%s: %s", inst.Domain, err))
81+
continue
82+
}
83+
}
84+
if email = utils.NormalizeEmail(email); email == "" {
85+
report.EmptyEmails++
86+
continue
87+
}
88+
byEmail[email] = append(byEmail[email], inst)
89+
}
90+
91+
scanFailed := len(report.Errors) > 0
92+
for email, owners := range byEmail {
93+
if len(owners) > 1 {
94+
for _, owner := range owners {
95+
report.Duplicates[email] = append(report.Duplicates[email], owner.Domain)
96+
}
97+
report.Skipped += len(owners)
98+
continue
99+
}
100+
inst := owners[0]
101+
if inst.InternalEmail == email || scanFailed {
102+
report.Skipped++
103+
continue
104+
}
105+
if !dryRun {
106+
if err := SetInternalEmail(inst, email); err != nil {
107+
report.Errors = append(report.Errors, fmt.Sprintf("%s: %s", inst.Domain, err))
108+
continue
109+
}
110+
}
111+
report.Updated++
112+
}
113+
return report
114+
}

‎model/instance/lifecycle/lifecycle_test.go‎

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ package lifecycle_test
22

33
import (
44
"bytes"
5+
"fmt"
6+
"net/url"
7+
"strings"
58
"testing"
69
"time"
710

@@ -494,6 +497,69 @@ func TestLifecycle(t *testing.T) {
494497
assert.Equal(t, instance.TOSNone, deadline)
495498
})
496499

500+
t.Run("BackfillInternalEmails", func(t *testing.T) {
501+
suffix := fmt.Sprintf("%d.example", time.Now().UnixNano())
502+
create := func(slug, email string) *instance.Instance {
503+
t.Helper()
504+
domain := slug + "-" + suffix
505+
inst, err := lifecycle.Create(&lifecycle.Options{Domain: domain, Email: email})
506+
require.NoError(t, err)
507+
t.Cleanup(func() { _ = lifecycle.Destroy(domain) })
508+
return inst
509+
}
510+
internalEmail := func(inst *instance.Instance) string {
511+
t.Helper()
512+
got, err := lifecycle.GetInstance(inst.Domain)
513+
require.NoError(t, err)
514+
return got.InternalEmail
515+
}
516+
517+
insts := []*instance.Instance{
518+
create("alice", " Alice@"+suffix),
519+
create("bob", "bob@"+suffix),
520+
create("bob2", "BOB@"+suffix),
521+
create("nobody", ""),
522+
}
523+
524+
report := lifecycle.BackfillInternalEmails(insts, true)
525+
assert.Equal(t, 4, report.Scanned)
526+
assert.Equal(t, 1, report.Updated)
527+
assert.Equal(t, 2, report.Skipped)
528+
assert.Equal(t, 1, report.EmptyEmails)
529+
assert.ElementsMatch(t, []string{insts[1].Domain, insts[2].Domain}, report.Duplicates["bob@"+suffix])
530+
assert.Empty(t, report.Errors)
531+
assert.Empty(t, internalEmail(insts[0]), "dry run must not write")
532+
533+
report = lifecycle.BackfillInternalEmails(insts, false)
534+
assert.Equal(t, 1, report.Updated)
535+
assert.Equal(t, "alice@"+suffix, internalEmail(insts[0]))
536+
assert.Empty(t, internalEmail(insts[1]))
537+
assert.Empty(t, internalEmail(insts[2]))
538+
539+
orgSlug := "org-" + strings.TrimSuffix(suffix, ".example")
540+
org, err := lifecycle.Create(&lifecycle.Options{Domain: orgSlug + ".example", OrgID: orgSlug})
541+
require.NoError(t, err)
542+
t.Cleanup(func() { _ = lifecycle.Destroy(org.Domain) })
543+
require.True(t, org.IsOrganizationInstance())
544+
require.NoError(t, lifecycle.SetInternalEmail(org, "erin@"+suffix))
545+
erin := create("erin", "erin@"+suffix)
546+
report = lifecycle.BackfillInternalEmails([]*instance.Instance{org, erin}, false)
547+
assert.Len(t, report.Duplicates["erin@"+suffix], 2)
548+
assert.Empty(t, internalEmail(erin))
549+
550+
// A failed settings read blocks every write.
551+
clusters := config.GetConfig().CouchDB.Clusters
552+
t.Cleanup(func() { config.GetConfig().CouchDB.Clusters = clusters })
553+
unreachable, _ := url.Parse("http://127.0.0.1:1/")
554+
config.GetConfig().CouchDB.Clusters = append(clusters[:len(clusters):len(clusters)], config.CouchDBCluster{URL: unreachable})
555+
broken := &instance.Instance{Domain: "broken-" + suffix, CouchCluster: len(clusters)}
556+
dave := create("dave", "dave@"+suffix)
557+
report = lifecycle.BackfillInternalEmails([]*instance.Instance{dave, broken}, false)
558+
assert.Len(t, report.Errors, 1)
559+
assert.Equal(t, 0, report.Updated)
560+
assert.Empty(t, internalEmail(dave))
561+
})
562+
497563
t.Run("InstanceDestroy", func(t *testing.T) {
498564
_ = lifecycle.Destroy("test.cozycloud.cc")
499565

‎web/instances/fixers.go‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,3 +355,12 @@ func indexesFixer(c echo.Context) error {
355355

356356
return c.NoContent(http.StatusNoContent)
357357
}
358+
359+
func internalEmailsFixer(c echo.Context) error {
360+
insts, err := instance.List()
361+
if err != nil {
362+
return wrapError(err)
363+
}
364+
dryRun := c.QueryParam("dry_run") == "true"
365+
return c.JSON(http.StatusOK, lifecycle.BackfillInternalEmails(insts, dryRun))
366+
}

‎web/instances/instances.go‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -825,4 +825,5 @@ func Routes(router *echo.Group) {
825825
router.POST("/:domain/fixers/sharings-moved", sharingsMovedFixer)
826826
router.POST("/:domain/fixers/service-triggers", serviceTriggersFixer)
827827
router.POST("/:domain/fixers/indexes", indexesFixer)
828+
router.POST("/fixers/internal-emails", internalEmailsFixer)
828829
}

0 commit comments

Comments
 (0)

Back | FazBrowse Home | New Git URL