diff --git a/go.mod b/go.mod index aff89fab..dba0e44f 100644 --- a/go.mod +++ b/go.mod @@ -77,7 +77,8 @@ require ( require ( cloud.google.com/go v0.105.0 // indirect - cloud.google.com/go/compute/metadata v0.2.0 // indirect + cloud.google.com/go/compute v1.12.1 // indirect + cloud.google.com/go/compute/metadata v0.2.1 // indirect cloud.google.com/go/iam v0.6.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.0.1 // indirect github.com/ajg/form v1.5.1 // indirect diff --git a/go.sum b/go.sum index bd911d62..c1bfbc82 100644 --- a/go.sum +++ b/go.sum @@ -50,8 +50,10 @@ cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6m cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s= cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU= cloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U= -cloud.google.com/go/compute/metadata v0.2.0 h1:nBbNSZyDpkNlo3DepaaLKVuO7ClyifSAmNloSCZrHnQ= -cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= +cloud.google.com/go/compute v1.12.1 h1:gKVJMEyqV5c/UnpzjjQbo3Rjvvqpr9B1DFSbJC4OXr0= +cloud.google.com/go/compute v1.12.1/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU= +cloud.google.com/go/compute/metadata v0.2.1 h1:efOwf5ymceDhK6PKMnnrTHP4pppY5L22mle96M1yP48= +cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= diff --git a/internal/common/eventmanager.go b/internal/common/eventmanager.go index 81f8176b..63f070ad 100644 --- a/internal/common/eventmanager.go +++ b/internal/common/eventmanager.go @@ -29,6 +29,7 @@ import ( "os" "os/exec" "path" + "path/filepath" "strconv" "strings" "sync" @@ -476,6 +477,15 @@ func (p *EventParams) AddError(err error) { p.errors = append(p.errors, err.Error()) } +func (p *EventParams) setBackupParams(backupPath string) { + if p.sender != "" { + return + } + p.sender = dataprovider.ActionExecutorSystem + p.FsPath = backupPath + p.VirtualPath = filepath.Base(backupPath) +} + func (p *EventParams) getStatusString() string { switch p.Status { case 1: @@ -503,6 +513,18 @@ func (p *EventParams) getUsers() ([]dataprovider.User, error) { } func (p *EventParams) getUserFromSender() (dataprovider.User, error) { + if p.sender == dataprovider.ActionExecutorSystem { + return dataprovider.User{ + BaseUser: sdk.BaseUser{ + Status: 1, + Username: p.sender, + HomeDir: dataprovider.GetBackupsPath(), + Permissions: map[string][]string{ + "/": {dataprovider.PermAny}, + }, + }, + }, nil + } user, err := dataprovider.UserExists(p.sender) if err != nil { eventManagerLog(logger.LevelError, "unable to get user %q: %+v", p.sender, err) @@ -1903,7 +1925,11 @@ func executeRuleAction(action dataprovider.BaseEventAction, params *EventParams, case dataprovider.ActionTypeEmail: err = executeEmailRuleAction(action.Options.EmailConfig, params) case dataprovider.ActionTypeBackup: - err = dataprovider.ExecuteBackup() + var backupPath string + backupPath, err = dataprovider.ExecuteBackup() + if err == nil { + params.setBackupParams(backupPath) + } case dataprovider.ActionTypeUserQuotaReset: err = executeUsersQuotaResetRuleAction(conditions, params) case dataprovider.ActionTypeFolderQuotaReset: diff --git a/internal/common/protocol_test.go b/internal/common/protocol_test.go index 5fb18aec..66b4c7b6 100644 --- a/internal/common/protocol_test.go +++ b/internal/common/protocol_test.go @@ -3353,6 +3353,10 @@ func TestEventRule(t *testing.T) { } a2 := dataprovider.BaseEventAction{ Name: "action2", + Type: dataprovider.ActionTypeBackup, + } + a3 := dataprovider.BaseEventAction{ + Name: "action3", Type: dataprovider.ActionTypeEmail, Options: dataprovider.BaseEventActionOptions{ EmailConfig: dataprovider.EventActionEmailConfig{ @@ -3362,8 +3366,8 @@ func TestEventRule(t *testing.T) { }, }, } - a3 := dataprovider.BaseEventAction{ - Name: "action3", + a4 := dataprovider.BaseEventAction{ + Name: "action4", Type: dataprovider.ActionTypeEmail, Options: dataprovider.BaseEventActionOptions{ EmailConfig: dataprovider.EventActionEmailConfig{ @@ -3379,6 +3383,9 @@ func TestEventRule(t *testing.T) { assert.NoError(t, err) action3, _, err := httpdtest.AddEventAction(a3, http.StatusCreated) assert.NoError(t, err) + action4, _, err := httpdtest.AddEventAction(a4, http.StatusCreated) + assert.NoError(t, err) + r1 := dataprovider.EventRule{ Name: "test rule1", Trigger: dataprovider.EventTriggerFsEvent, @@ -3417,6 +3424,12 @@ func TestEventRule(t *testing.T) { Name: action3.Name, }, Order: 3, + }, + { + BaseEventAction: dataprovider.BaseEventAction{ + Name: action4.Name, + }, + Order: 4, Options: dataprovider.EventActionOptions{ IsFailureAction: true, }, @@ -3442,13 +3455,13 @@ func TestEventRule(t *testing.T) { Actions: []dataprovider.EventAction{ { BaseEventAction: dataprovider.BaseEventAction{ - Name: action2.Name, + Name: action3.Name, }, Order: 1, }, { BaseEventAction: dataprovider.BaseEventAction{ - Name: action3.Name, + Name: action4.Name, }, Order: 2, Options: dataprovider.EventActionOptions{ @@ -3469,7 +3482,7 @@ func TestEventRule(t *testing.T) { Actions: []dataprovider.EventAction{ { BaseEventAction: dataprovider.BaseEventAction{ - Name: action2.Name, + Name: action3.Name, }, Order: 1, }, @@ -3645,6 +3658,8 @@ func TestEventRule(t *testing.T) { assert.NoError(t, err) _, err = httpdtest.RemoveEventAction(action3, http.StatusOK) assert.NoError(t, err) + _, err = httpdtest.RemoveEventAction(action4, http.StatusOK) + assert.NoError(t, err) lastReceivedEmail.reset() _, err = httpdtest.RemoveUser(user, http.StatusOK) assert.NoError(t, err) @@ -4237,6 +4252,89 @@ func TestEventFsActionsGroupFilters(t *testing.T) { require.NoError(t, err) } +func TestBackupAsAttachment(t *testing.T) { + smtpCfg := smtp.Config{ + Host: "127.0.0.1", + Port: 2525, + From: "notification@example.com", + TemplatesPath: "templates", + } + err := smtpCfg.Initialize(configDir) + require.NoError(t, err) + + a1 := dataprovider.BaseEventAction{ + Name: "a1", + Type: dataprovider.ActionTypeBackup, + } + a2 := dataprovider.BaseEventAction{ + Name: "a2", + Type: dataprovider.ActionTypeEmail, + Options: dataprovider.BaseEventActionOptions{ + EmailConfig: dataprovider.EventActionEmailConfig{ + Recipients: []string{"test@example.com"}, + Subject: `"{{Event}} {{StatusString}}"`, + Body: "Domain: {{Name}}", + Attachments: []string{"/{{VirtualPath}}"}, + }, + }, + } + action1, _, err := httpdtest.AddEventAction(a1, http.StatusCreated) + assert.NoError(t, err) + action2, _, err := httpdtest.AddEventAction(a2, http.StatusCreated) + assert.NoError(t, err) + + r1 := dataprovider.EventRule{ + Name: "test rule certificate", + Trigger: dataprovider.EventTriggerCertificate, + Actions: []dataprovider.EventAction{ + { + BaseEventAction: dataprovider.BaseEventAction{ + Name: action1.Name, + }, + Order: 1, + }, + { + BaseEventAction: dataprovider.BaseEventAction{ + Name: action2.Name, + }, + Order: 2, + }, + }, + } + rule1, _, err := httpdtest.AddEventRule(r1, http.StatusCreated) + assert.NoError(t, err) + + lastReceivedEmail.reset() + renewalEvent := "Certificate renewal" + + common.HandleCertificateEvent(common.EventParams{ + Name: "example.com", + Timestamp: time.Now().UnixNano(), + Status: 1, + Event: renewalEvent, + }) + assert.Eventually(t, func() bool { + return lastReceivedEmail.get().From != "" + }, 3000*time.Millisecond, 100*time.Millisecond) + email := lastReceivedEmail.get() + assert.Len(t, email.To, 1) + assert.True(t, util.Contains(email.To, "test@example.com")) + assert.Contains(t, email.Data, fmt.Sprintf(`Subject: "%s OK"`, renewalEvent)) + assert.Contains(t, email.Data, `Domain: example.com`) + assert.Contains(t, email.Data, "Content-Type: application/json") + + _, err = httpdtest.RemoveEventRule(rule1, http.StatusOK) + assert.NoError(t, err) + _, err = httpdtest.RemoveEventAction(action1, http.StatusOK) + assert.NoError(t, err) + _, err = httpdtest.RemoveEventAction(action2, http.StatusOK) + assert.NoError(t, err) + + smtpCfg = smtp.Config{} + err = smtpCfg.Initialize(configDir) + require.NoError(t, err) +} + func TestEventActionHTTPMultipart(t *testing.T) { a1 := dataprovider.BaseEventAction{ Name: "action1", diff --git a/internal/dataprovider/dataprovider.go b/internal/dataprovider/dataprovider.go index 2d0f5863..78774b18 100644 --- a/internal/dataprovider/dataprovider.go +++ b/internal/dataprovider/dataprovider.go @@ -524,36 +524,36 @@ func (c *Config) requireCustomTLSForMySQL() bool { return false } -func (c *Config) doBackup() error { +func (c *Config) doBackup() (string, error) { now := time.Now().UTC() outputFile := filepath.Join(c.BackupsPath, fmt.Sprintf("backup_%s_%d.json", now.Weekday(), now.Hour())) providerLog(logger.LevelDebug, "starting backup to file %q", outputFile) err := os.MkdirAll(filepath.Dir(outputFile), 0700) if err != nil { providerLog(logger.LevelError, "unable to create backup dir %q: %v", outputFile, err) - return fmt.Errorf("unable to create backup dir: %w", err) + return outputFile, fmt.Errorf("unable to create backup dir: %w", err) } backup, err := DumpData() if err != nil { providerLog(logger.LevelError, "unable to execute backup: %v", err) - return fmt.Errorf("unable to dump backup data: %w", err) + return outputFile, fmt.Errorf("unable to dump backup data: %w", err) } dump, err := json.Marshal(backup) if err != nil { providerLog(logger.LevelError, "unable to marshal backup as JSON: %v", err) - return fmt.Errorf("unable to marshal backup data as JSON: %w", err) + return outputFile, fmt.Errorf("unable to marshal backup data as JSON: %w", err) } err = os.WriteFile(outputFile, dump, 0600) if err != nil { providerLog(logger.LevelError, "unable to save backup: %v", err) - return fmt.Errorf("unable to save backup: %w", err) + return outputFile, fmt.Errorf("unable to save backup: %w", err) } providerLog(logger.LevelDebug, "backup saved to %q", outputFile) - return nil + return outputFile, nil } // ExecuteBackup executes a backup -func ExecuteBackup() error { +func ExecuteBackup() (string, error) { return config.doBackup() } @@ -833,6 +833,12 @@ func Initialize(cnf Config, basePath string, checkAdmins bool) error { if cnf.BackupsPath == "" { return fmt.Errorf("required directory is invalid, backup path %#v", cnf.BackupsPath) } + absoluteBackupPath, err := util.GetAbsolutePath(cnf.BackupsPath) + if err != nil { + return fmt.Errorf("unable to get absolute backup path: %w", err) + } + config.BackupsPath = absoluteBackupPath + providerLog(logger.LevelDebug, "absolute backup path %q", config.BackupsPath) if err := initializeHashingAlgo(&cnf); err != nil { return err diff --git a/internal/dataprovider/eventrule.go b/internal/dataprovider/eventrule.go index e6f4ed5e..a25f00bf 100644 --- a/internal/dataprovider/eventrule.go +++ b/internal/dataprovider/eventrule.go @@ -1354,6 +1354,12 @@ func (r *EventRule) hasUserAssociated(providerObjectType string) bool { return providerObjectType == actionObjectUser case EventTriggerFsEvent: return true + default: + if len(r.Actions) > 0 { + // should we allow schedules where backup is not the first action? + // maybe we could pass the action index and check before that index + return r.Actions[0].Type == ActionTypeBackup + } } return false } diff --git a/internal/util/util.go b/internal/util/util.go index 6ebbd334..7f08be2e 100644 --- a/internal/util/util.go +++ b/internal/util/util.go @@ -727,3 +727,19 @@ func PanicOnError(err error) { panic(fmt.Errorf("unexpected error: %w", err)) } } + +// GetAbsolutePath returns an absolute path using the current dir as base +// if name defines a relative path +func GetAbsolutePath(name string) (string, error) { + if name == "" { + return name, errors.New("input path cannot be empty") + } + if filepath.IsAbs(name) { + return name, nil + } + curDir, err := os.Getwd() + if err != nil { + return name, err + } + return filepath.Join(curDir, name), nil +}