Create CLI command for maintenance (#3226)

Creates hidden CLI command to run
maintenance.

Flag names and descriptions can be
updated if something else would fit
better

---

#### Does this PR need a docs update or release note?

- [ ]  Yes, it's included
- [ ] 🕐 Yes, but in a later PR
- [ ]  No

#### Type of change

- [x] 🌻 Feature
- [ ] 🐛 Bugfix
- [ ] 🗺️ Documentation
- [ ] 🤖 Supportability/Tests
- [ ] 💻 CI/Deployment
- [ ] 🧹 Tech Debt/Cleanup

#### Issue(s)

* #3077

#### Test Plan

- [x] 💪 Manual
- [x]  Unit test
- [ ] 💚 E2E
This commit is contained in:
ashmrtn 2023-05-16 14:03:06 -07:00 committed by GitHub
parent 965427d491
commit 089a96d437
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 208 additions and 18 deletions

View File

@ -1,12 +1,21 @@
package repo
import (
"strings"
"github.com/alcionai/clues"
"github.com/spf13/cobra"
"golang.org/x/exp/maps"
"github.com/alcionai/corso/src/cli/print"
"github.com/alcionai/corso/src/cli/utils"
"github.com/alcionai/corso/src/pkg/control/repository"
)
const (
initCommand = "init"
connectCommand = "connect"
initCommand = "init"
connectCommand = "connect"
maintenanceCommand = "maintenance"
)
var repoCommands = []func(cmd *cobra.Command) *cobra.Command{
@ -18,15 +27,24 @@ func AddCommands(cmd *cobra.Command) {
var (
// Get new instances so that setting the context during tests works
// properly.
repoCmd = repoCmd()
initCmd = initCmd()
connectCmd = connectCmd()
repoCmd = repoCmd()
initCmd = initCmd()
connectCmd = connectCmd()
maintenanceCmd = maintenanceCmd()
)
cmd.AddCommand(repoCmd)
repoCmd.AddCommand(initCmd)
repoCmd.AddCommand(connectCmd)
utils.AddCommand(
repoCmd,
maintenanceCmd,
utils.HideCommand(),
utils.MarkPreReleaseCommand())
utils.AddMaintenanceModeFlag(maintenanceCmd)
utils.AddForceMaintenanceFlag(maintenanceCmd)
for _, addRepoTo := range repoCommands {
addRepoTo(initCmd)
addRepoTo(connectCmd)
@ -84,3 +102,65 @@ func connectCmd() *cobra.Command {
func handleConnectCmd(cmd *cobra.Command, args []string) error {
return cmd.Help()
}
func maintenanceCmd() *cobra.Command {
return &cobra.Command{
Use: maintenanceCommand,
Short: "Run maintenance on an existing repository",
Long: `Run maintenance on an existing repository to optimize performance and storage use`,
RunE: handleMaintenanceCmd,
Args: cobra.NoArgs,
}
}
func handleMaintenanceCmd(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()
t, err := getMaintenanceType(utils.MaintenanceModeFV)
if err != nil {
return err
}
r, _, err := utils.GetAccountAndConnect(ctx)
if err != nil {
return print.Only(ctx, err)
}
defer utils.CloseRepo(ctx, r)
m, err := r.NewMaintenance(
ctx,
repository.Maintenance{
Type: t,
Safety: repository.FullMaintenanceSafety,
Force: utils.ForceMaintenanceFV,
})
if err != nil {
return print.Only(ctx, err)
}
err = m.Run(ctx)
if err != nil {
return print.Only(ctx, err)
}
return nil
}
func getMaintenanceType(t string) (repository.MaintenanceType, error) {
res, ok := repository.StringToMaintenanceType[t]
if !ok {
modes := maps.Keys(repository.StringToMaintenanceType)
allButLast := []string{}
for i := 0; i < len(modes)-1; i++ {
allButLast = append(allButLast, string(modes[i]))
}
valuesStr := strings.Join(allButLast, ", ") + " or " + string(modes[len(modes)-1])
return res, clues.New(t + " is an unrecognized maintenance mode; must be one of " + valuesStr)
}
return res, nil
}

41
src/cli/repo/repo_test.go Normal file
View File

@ -0,0 +1,41 @@
package repo
import (
"testing"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"github.com/alcionai/corso/src/internal/tester"
)
type RepoUnitSuite struct {
tester.Suite
}
func TestRepoUnitSuite(t *testing.T) {
suite.Run(t, &RepoUnitSuite{Suite: tester.NewUnitSuite(t)})
}
func (suite *RepoUnitSuite) TestAddRepoCommands() {
t := suite.T()
cmd := &cobra.Command{}
AddCommands(cmd)
var found bool
// This is the repo command.
repoCmds := cmd.Commands()
require.Len(t, repoCmds, 1)
for _, c := range repoCmds[0].Commands() {
if c.Use == maintenanceCommand {
found = true
}
}
assert.True(t, found, "looking for maintenance command")
}

View File

@ -9,6 +9,7 @@ import (
"github.com/spf13/pflag"
"github.com/alcionai/corso/src/internal/common/dttm"
"github.com/alcionai/corso/src/pkg/control/repository"
"github.com/alcionai/corso/src/pkg/path"
"github.com/alcionai/corso/src/pkg/selectors"
)
@ -37,6 +38,9 @@ var (
// for selection of data by category. eg: `--data email,contacts`
CategoryDataFV []string
MaintenanceModeFV string
ForceMaintenanceFV bool
)
// common flag names (eg: FN)
@ -59,6 +63,10 @@ const (
FileCreatedBeforeFN = "file-created-before"
FileModifiedAfterFN = "file-modified-after"
FileModifiedBeforeFN = "file-modified-before"
// Maintenance stuff.
MaintenanceModeFN = "mode"
ForceMaintenanceFN = "force"
)
// well-known flag values
@ -168,6 +176,30 @@ func AddSiteFlag(cmd *cobra.Command) {
"Backup data by site URL; accepts '"+Wildcard+"' to select all sites.")
}
func AddMaintenanceModeFlag(cmd *cobra.Command) {
fs := cmd.Flags()
fs.StringVar(
&MaintenanceModeFV,
MaintenanceModeFN,
repository.CompleteMaintenance.String(),
"Type of maintenance operation to run. Pass '"+
repository.MetadataMaintenance.String()+"' to run a faster maintenance "+
"that does minimal clean-up and optimization. Pass '"+
repository.CompleteMaintenance.String()+"' to fully compact existing "+
"data and delete unused data.")
cobra.CheckErr(fs.MarkHidden(MaintenanceModeFN))
}
func AddForceMaintenanceFlag(cmd *cobra.Command) {
fs := cmd.Flags()
fs.BoolVar(
&ForceMaintenanceFV,
ForceMaintenanceFN,
false,
"Force maintenance. Caution: user must ensure this is not run concurrently on a single repo")
cobra.CheckErr(fs.MarkHidden(ForceMaintenanceFN))
}
type PopulatedFlags map[string]struct{}
func (fs PopulatedFlags) populate(pf *pflag.Flag) {

View File

@ -28,13 +28,15 @@ const (
tenantIDDeprecated = "m365_tenant_hash_deprecated"
// Event Keys
CorsoStart = "Corso Start"
RepoInit = "Repo Init"
RepoConnect = "Repo Connect"
BackupStart = "Backup Start"
BackupEnd = "Backup End"
RestoreStart = "Restore Start"
RestoreEnd = "Restore End"
CorsoStart = "Corso Start"
RepoInit = "Repo Init"
RepoConnect = "Repo Connect"
BackupStart = "Backup Start"
BackupEnd = "Backup End"
RestoreStart = "Restore Start"
RestoreEnd = "Restore End"
MaintenanceStart = "Maintenance Start"
MaintenanceEnd = "Maintenance End"
// Event Data Keys
BackupCreateTime = "backup_creation_time"

View File

@ -7,6 +7,7 @@ import (
"github.com/alcionai/clues"
"github.com/alcionai/corso/src/internal/common/crash"
"github.com/alcionai/corso/src/internal/common/dttm"
"github.com/alcionai/corso/src/internal/events"
"github.com/alcionai/corso/src/internal/kopia"
"github.com/alcionai/corso/src/internal/stats"
@ -49,20 +50,45 @@ func (op *MaintenanceOperation) Run(ctx context.Context) (err error) {
if crErr := crash.Recovery(ctx, recover(), "maintenance"); crErr != nil {
err = crErr
}
// TODO(ashmrtn): Send success/failure usage stat?
op.Results.CompletedAt = time.Now()
}()
op.Results.StartedAt = time.Now()
// TODO(ashmrtn): Send usage statistics?
op.bus.Event(
ctx,
events.MaintenanceStart,
map[string]any{
events.StartTime: op.Results.StartedAt,
})
err = op.operation.kopia.RepoMaintenance(ctx, op.mOpts)
defer func() {
op.bus.Event(
ctx,
events.MaintenanceEnd,
map[string]any{
events.StartTime: op.Results.StartedAt,
events.Duration: op.Results.CompletedAt.Sub(op.Results.StartedAt),
events.EndTime: dttm.Format(op.Results.CompletedAt),
events.Status: op.Status.String(),
events.Resources: op.mOpts.Type.String(),
})
}()
return op.do(ctx)
}
func (op *MaintenanceOperation) do(ctx context.Context) error {
defer func() {
op.Results.CompletedAt = time.Now()
}()
err := op.operation.kopia.RepoMaintenance(ctx, op.mOpts)
if err != nil {
op.Status = Failed
return clues.Wrap(err, "running maintenance operation")
}
op.Status = Completed
return nil
}

View File

@ -26,6 +26,11 @@ const (
MetadataMaintenance // metadata
)
var StringToMaintenanceType = map[string]MaintenanceType{
CompleteMaintenance.String(): CompleteMaintenance,
MetadataMaintenance.String(): MetadataMaintenance,
}
type MaintenanceSafety int
// Can't be reordered as we rely on iota for numbering.
@ -33,5 +38,9 @@ type MaintenanceSafety int
//go:generate stringer -type=MaintenanceSafety -linecomment
const (
FullMaintenanceSafety MaintenanceSafety = iota
//nolint:lll
// Use only if there's no other kopia instances accessing the repo and the
// storage backend is strongly consistent.
// https://github.com/kopia/kopia/blob/f9de453efc198b6e993af8922f953a7e5322dc5f/repo/maintenance/maintenance_safety.go#L42
NoMaintenanceSafety
)