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:
parent
965427d491
commit
089a96d437
@ -1,12 +1,21 @@
|
|||||||
package repo
|
package repo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/alcionai/clues"
|
||||||
"github.com/spf13/cobra"
|
"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 (
|
const (
|
||||||
initCommand = "init"
|
initCommand = "init"
|
||||||
connectCommand = "connect"
|
connectCommand = "connect"
|
||||||
|
maintenanceCommand = "maintenance"
|
||||||
)
|
)
|
||||||
|
|
||||||
var repoCommands = []func(cmd *cobra.Command) *cobra.Command{
|
var repoCommands = []func(cmd *cobra.Command) *cobra.Command{
|
||||||
@ -18,15 +27,24 @@ func AddCommands(cmd *cobra.Command) {
|
|||||||
var (
|
var (
|
||||||
// Get new instances so that setting the context during tests works
|
// Get new instances so that setting the context during tests works
|
||||||
// properly.
|
// properly.
|
||||||
repoCmd = repoCmd()
|
repoCmd = repoCmd()
|
||||||
initCmd = initCmd()
|
initCmd = initCmd()
|
||||||
connectCmd = connectCmd()
|
connectCmd = connectCmd()
|
||||||
|
maintenanceCmd = maintenanceCmd()
|
||||||
)
|
)
|
||||||
|
|
||||||
cmd.AddCommand(repoCmd)
|
cmd.AddCommand(repoCmd)
|
||||||
repoCmd.AddCommand(initCmd)
|
repoCmd.AddCommand(initCmd)
|
||||||
repoCmd.AddCommand(connectCmd)
|
repoCmd.AddCommand(connectCmd)
|
||||||
|
|
||||||
|
utils.AddCommand(
|
||||||
|
repoCmd,
|
||||||
|
maintenanceCmd,
|
||||||
|
utils.HideCommand(),
|
||||||
|
utils.MarkPreReleaseCommand())
|
||||||
|
utils.AddMaintenanceModeFlag(maintenanceCmd)
|
||||||
|
utils.AddForceMaintenanceFlag(maintenanceCmd)
|
||||||
|
|
||||||
for _, addRepoTo := range repoCommands {
|
for _, addRepoTo := range repoCommands {
|
||||||
addRepoTo(initCmd)
|
addRepoTo(initCmd)
|
||||||
addRepoTo(connectCmd)
|
addRepoTo(connectCmd)
|
||||||
@ -84,3 +102,65 @@ func connectCmd() *cobra.Command {
|
|||||||
func handleConnectCmd(cmd *cobra.Command, args []string) error {
|
func handleConnectCmd(cmd *cobra.Command, args []string) error {
|
||||||
return cmd.Help()
|
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
41
src/cli/repo/repo_test.go
Normal 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")
|
||||||
|
}
|
||||||
@ -9,6 +9,7 @@ import (
|
|||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
|
|
||||||
"github.com/alcionai/corso/src/internal/common/dttm"
|
"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/path"
|
||||||
"github.com/alcionai/corso/src/pkg/selectors"
|
"github.com/alcionai/corso/src/pkg/selectors"
|
||||||
)
|
)
|
||||||
@ -37,6 +38,9 @@ var (
|
|||||||
|
|
||||||
// for selection of data by category. eg: `--data email,contacts`
|
// for selection of data by category. eg: `--data email,contacts`
|
||||||
CategoryDataFV []string
|
CategoryDataFV []string
|
||||||
|
|
||||||
|
MaintenanceModeFV string
|
||||||
|
ForceMaintenanceFV bool
|
||||||
)
|
)
|
||||||
|
|
||||||
// common flag names (eg: FN)
|
// common flag names (eg: FN)
|
||||||
@ -59,6 +63,10 @@ const (
|
|||||||
FileCreatedBeforeFN = "file-created-before"
|
FileCreatedBeforeFN = "file-created-before"
|
||||||
FileModifiedAfterFN = "file-modified-after"
|
FileModifiedAfterFN = "file-modified-after"
|
||||||
FileModifiedBeforeFN = "file-modified-before"
|
FileModifiedBeforeFN = "file-modified-before"
|
||||||
|
|
||||||
|
// Maintenance stuff.
|
||||||
|
MaintenanceModeFN = "mode"
|
||||||
|
ForceMaintenanceFN = "force"
|
||||||
)
|
)
|
||||||
|
|
||||||
// well-known flag values
|
// well-known flag values
|
||||||
@ -168,6 +176,30 @@ func AddSiteFlag(cmd *cobra.Command) {
|
|||||||
"Backup data by site URL; accepts '"+Wildcard+"' to select all sites.")
|
"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{}
|
type PopulatedFlags map[string]struct{}
|
||||||
|
|
||||||
func (fs PopulatedFlags) populate(pf *pflag.Flag) {
|
func (fs PopulatedFlags) populate(pf *pflag.Flag) {
|
||||||
|
|||||||
@ -28,13 +28,15 @@ const (
|
|||||||
tenantIDDeprecated = "m365_tenant_hash_deprecated"
|
tenantIDDeprecated = "m365_tenant_hash_deprecated"
|
||||||
|
|
||||||
// Event Keys
|
// Event Keys
|
||||||
CorsoStart = "Corso Start"
|
CorsoStart = "Corso Start"
|
||||||
RepoInit = "Repo Init"
|
RepoInit = "Repo Init"
|
||||||
RepoConnect = "Repo Connect"
|
RepoConnect = "Repo Connect"
|
||||||
BackupStart = "Backup Start"
|
BackupStart = "Backup Start"
|
||||||
BackupEnd = "Backup End"
|
BackupEnd = "Backup End"
|
||||||
RestoreStart = "Restore Start"
|
RestoreStart = "Restore Start"
|
||||||
RestoreEnd = "Restore End"
|
RestoreEnd = "Restore End"
|
||||||
|
MaintenanceStart = "Maintenance Start"
|
||||||
|
MaintenanceEnd = "Maintenance End"
|
||||||
|
|
||||||
// Event Data Keys
|
// Event Data Keys
|
||||||
BackupCreateTime = "backup_creation_time"
|
BackupCreateTime = "backup_creation_time"
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import (
|
|||||||
"github.com/alcionai/clues"
|
"github.com/alcionai/clues"
|
||||||
|
|
||||||
"github.com/alcionai/corso/src/internal/common/crash"
|
"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/events"
|
||||||
"github.com/alcionai/corso/src/internal/kopia"
|
"github.com/alcionai/corso/src/internal/kopia"
|
||||||
"github.com/alcionai/corso/src/internal/stats"
|
"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 {
|
if crErr := crash.Recovery(ctx, recover(), "maintenance"); crErr != nil {
|
||||||
err = crErr
|
err = crErr
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(ashmrtn): Send success/failure usage stat?
|
|
||||||
|
|
||||||
op.Results.CompletedAt = time.Now()
|
|
||||||
}()
|
}()
|
||||||
|
|
||||||
op.Results.StartedAt = 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 {
|
if err != nil {
|
||||||
|
op.Status = Failed
|
||||||
return clues.Wrap(err, "running maintenance operation")
|
return clues.Wrap(err, "running maintenance operation")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
op.Status = Completed
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,6 +26,11 @@ const (
|
|||||||
MetadataMaintenance // metadata
|
MetadataMaintenance // metadata
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var StringToMaintenanceType = map[string]MaintenanceType{
|
||||||
|
CompleteMaintenance.String(): CompleteMaintenance,
|
||||||
|
MetadataMaintenance.String(): MetadataMaintenance,
|
||||||
|
}
|
||||||
|
|
||||||
type MaintenanceSafety int
|
type MaintenanceSafety int
|
||||||
|
|
||||||
// Can't be reordered as we rely on iota for numbering.
|
// Can't be reordered as we rely on iota for numbering.
|
||||||
@ -33,5 +38,9 @@ type MaintenanceSafety int
|
|||||||
//go:generate stringer -type=MaintenanceSafety -linecomment
|
//go:generate stringer -type=MaintenanceSafety -linecomment
|
||||||
const (
|
const (
|
||||||
FullMaintenanceSafety MaintenanceSafety = iota
|
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
|
NoMaintenanceSafety
|
||||||
)
|
)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user