Add hidden flags for setting retention during repo init (#3932)

Add, parse, and configure info about retention
when initializing a repo. These flags are
currently marked as hidden

Medium-term, they can be used in longevity
tests when making the repo for the first time
(we can configure this out of band if needed
though)

Long-term, they can be exposed to the user
for immutable backup support

---

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

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

#### Type of change

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

#### Issue(s)

* #3799

#### Test Plan

- [x] 💪 Manual
- [x]  Unit test
- [ ] 💚 E2E
This commit is contained in:
ashmrtn 2023-08-04 12:08:04 -07:00 committed by GitHub
parent ab2aa0d54e
commit 179edfc08e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 316 additions and 11 deletions

View File

@ -0,0 +1,50 @@
package flags
import (
"time"
"github.com/spf13/cobra"
"github.com/alcionai/corso/src/pkg/control/repository"
)
const (
RetentionModeFN = "retention-mode"
RetentionDurationFN = "retention-duration"
ExtendRetentionFN = "extend-retention"
)
var (
RetentionModeFV string
RetentionDurationFV time.Duration
ExtendRetentionFV bool
)
// AddRetentionConfigFlags adds the retention config flag set.
func AddRetentionConfigFlags(cmd *cobra.Command) {
fs := cmd.Flags()
fs.StringVar(
&RetentionModeFV,
RetentionModeFN,
repository.NoRetention.String(),
"Sets object locking mode (if any) to use in remote storage: "+
repository.NoRetention.String()+", "+
repository.GovernanceRetention.String()+", or "+
repository.ComplianceRetention.String())
cobra.CheckErr(fs.MarkHidden(RetentionModeFN))
fs.DurationVar(
&RetentionDurationFV,
RetentionDurationFN,
time.Duration(0),
"Set the amount of time to lock individual objects in remote storage")
cobra.CheckErr(fs.MarkHidden(RetentionDurationFN))
fs.BoolVar(
&ExtendRetentionFV,
ExtendRetentionFN,
false,
"Extends object locks during maintenance. "+
"Extends locks by the most recently set value of "+RetentionDurationFN)
cobra.CheckErr(fs.MarkHidden(ExtendRetentionFN))
}

View File

@ -15,7 +15,6 @@ import (
"github.com/alcionai/corso/src/cli/utils" "github.com/alcionai/corso/src/cli/utils"
"github.com/alcionai/corso/src/internal/events" "github.com/alcionai/corso/src/internal/events"
"github.com/alcionai/corso/src/pkg/account" "github.com/alcionai/corso/src/pkg/account"
rep "github.com/alcionai/corso/src/pkg/control/repository"
"github.com/alcionai/corso/src/pkg/credentials" "github.com/alcionai/corso/src/pkg/credentials"
"github.com/alcionai/corso/src/pkg/repository" "github.com/alcionai/corso/src/pkg/repository"
"github.com/alcionai/corso/src/pkg/storage" "github.com/alcionai/corso/src/pkg/storage"
@ -50,7 +49,10 @@ func addS3Commands(cmd *cobra.Command) *cobra.Command {
switch cmd.Use { switch cmd.Use {
case initCommand: case initCommand:
c, fs = utils.AddCommand(cmd, s3InitCmd()) init := s3InitCmd()
flags.AddRetentionConfigFlags(init)
c, fs = utils.AddCommand(cmd, init)
case connectCommand: case connectCommand:
c, fs = utils.AddCommand(cmd, s3ConnectCmd()) c, fs = utils.AddCommand(cmd, s3ConnectCmd())
} }
@ -133,6 +135,11 @@ func initS3Cmd(cmd *cobra.Command, args []string) error {
opt := utils.ControlWithConfig(cfg) opt := utils.ControlWithConfig(cfg)
retentionOpts, err := utils.MakeRetentionOpts(cmd)
if err != nil {
return Only(ctx, err)
}
// SendStartCorsoEvent uses distict ID as tenant ID because repoID is still not generated // SendStartCorsoEvent uses distict ID as tenant ID because repoID is still not generated
utils.SendStartCorsoEvent( utils.SendStartCorsoEvent(
ctx, ctx,
@ -159,13 +166,12 @@ func initS3Cmd(cmd *cobra.Command, args []string) error {
return Only(ctx, clues.Wrap(err, "Failed to parse m365 account config")) return Only(ctx, clues.Wrap(err, "Failed to parse m365 account config"))
} }
// TODO(ashmrtn): Wire to flags for retention during repo init.
r, err := repository.Initialize( r, err := repository.Initialize(
ctx, ctx,
cfg.Account, cfg.Account,
cfg.Storage, cfg.Storage,
opt, opt,
rep.Retention{}) retentionOpts)
if err != nil { if err != nil {
if succeedIfExists && errors.Is(err, repository.ErrorRepoAlreadyExists) { if succeedIfExists && errors.Is(err, repository.ErrorRepoAlreadyExists) {
return nil return nil

View File

@ -0,0 +1,87 @@
package utils
import (
"time"
"github.com/alcionai/clues"
"github.com/spf13/cobra"
"github.com/alcionai/corso/src/cli/flags"
"github.com/alcionai/corso/src/internal/common/ptr"
"github.com/alcionai/corso/src/pkg/control/repository"
)
type retentionCfgOpts struct {
Mode string
Duration time.Duration
Extend bool
Populated flags.PopulatedFlags
}
func makeRetentionCfgOpts(cmd *cobra.Command) retentionCfgOpts {
return retentionCfgOpts{
Mode: flags.RetentionModeFV,
Duration: flags.RetentionDurationFV,
Extend: flags.ExtendRetentionFV,
// Populated contains the list of flags that appear in the command,
// according to pflags. Use this to differentiate between an "empty" and a
// "missing" value.
Populated: flags.GetPopulatedFlags(cmd),
}
}
// validate checks common restore flags for correctness.
func (opts retentionCfgOpts) validate() error {
// Mode defaults to a valid value when pulled from flags. If coming from an
// empty struct will return invalid error.
if _, ok := repository.ValidRetentionModeNames()[opts.Mode]; !ok {
return clues.New("invalid retention mode " + opts.Mode)
}
// TODO(ashmrtn): Add an upper bound check?
if opts.Duration < 0 {
return clues.New("negative retention duration")
}
return nil
}
// MakeRetentionConfig converts the current retentionCfgOpts into a
// repository.Retention struct for use in lower-layers of corso.
func (opts retentionCfgOpts) makeRetentionOpts() (repository.Retention, error) {
retention := repository.Retention{}
if err := opts.validate(); err != nil {
return retention, clues.Stack(err)
}
// Only populate the fields that the user passed so that we don't accidentally
// change retention values without meaning to. Even if the user passed the
// same value as the default for the flag it gets marked as populated.
if _, ok := opts.Populated[flags.RetentionModeFN]; ok {
mode, ok := repository.ValidRetentionModeNames()[opts.Mode]
if !ok {
// Not sure how we'd get here since we validate above, but just in case.
return retention, clues.New("invalid retention mode " + opts.Mode)
}
retention.Mode = ptr.To(mode)
}
if _, ok := opts.Populated[flags.RetentionDurationFN]; ok {
retention.Duration = ptr.To(opts.Duration)
}
if _, ok := opts.Populated[flags.ExtendRetentionFN]; ok {
retention.Extend = ptr.To(opts.Extend)
}
return retention, nil
}
func MakeRetentionOpts(cmd *cobra.Command) (repository.Retention, error) {
opts, err := makeRetentionCfgOpts(cmd).makeRetentionOpts()
return opts, clues.Stack(err).OrNil()
}

View File

@ -0,0 +1,156 @@
package utils_test
import (
"testing"
"time"
"github.com/alcionai/clues"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"github.com/alcionai/corso/src/cli/flags"
"github.com/alcionai/corso/src/cli/utils"
"github.com/alcionai/corso/src/internal/common/ptr"
"github.com/alcionai/corso/src/internal/tester"
"github.com/alcionai/corso/src/pkg/control/repository"
)
type RetentionCfgUnitSuite struct {
tester.Suite
}
func TestRetentionCfgUnitSuite(t *testing.T) {
suite.Run(t, &RetentionCfgUnitSuite{Suite: tester.NewUnitSuite(t)})
}
func (suite *RetentionCfgUnitSuite) TestMakeRetentionOpts() {
table := []struct {
name string
flags map[string]string
expectErr assert.ErrorAssertionFunc
expect repository.Retention
}{
{
name: "Nothing Set",
expectErr: assert.NoError,
},
{
name: "Invalid Mode",
flags: map[string]string{
flags.RetentionModeFN: "foo",
},
expectErr: assert.Error,
},
{
name: "Negative Duration",
flags: map[string]string{
flags.RetentionDurationFN: "-5h",
},
expectErr: assert.Error,
},
{
name: "Only Governance Mode",
flags: map[string]string{
flags.RetentionModeFN: "governance",
},
expectErr: assert.NoError,
expect: repository.Retention{
Mode: ptr.To(repository.GovernanceRetention),
},
},
{
name: "Only Compliance Mode",
flags: map[string]string{
flags.RetentionModeFN: "compliance",
},
expectErr: assert.NoError,
expect: repository.Retention{
Mode: ptr.To(repository.ComplianceRetention),
},
},
{
name: "Only No Retention Mode",
flags: map[string]string{
flags.RetentionModeFN: "none",
},
expectErr: assert.NoError,
expect: repository.Retention{
Mode: ptr.To(repository.NoRetention),
},
},
{
name: "Mode And Duration",
flags: map[string]string{
flags.RetentionModeFN: "governance",
flags.RetentionDurationFN: "48h",
},
expectErr: assert.NoError,
expect: repository.Retention{
Mode: ptr.To(repository.GovernanceRetention),
Duration: ptr.To(time.Hour * 48),
},
},
{
name: "Mode And Extend",
flags: map[string]string{
flags.RetentionModeFN: "governance",
flags.ExtendRetentionFN: "false",
},
expectErr: assert.NoError,
expect: repository.Retention{
Mode: ptr.To(repository.GovernanceRetention),
Extend: ptr.To(false),
},
},
{
name: "Duration And Extend",
flags: map[string]string{
flags.RetentionDurationFN: "48h",
flags.ExtendRetentionFN: "true",
},
expectErr: assert.NoError,
expect: repository.Retention{
Duration: ptr.To(time.Hour * 48),
Extend: ptr.To(true),
},
},
{
name: "All Set",
flags: map[string]string{
flags.RetentionModeFN: "governance",
flags.RetentionDurationFN: "48h",
flags.ExtendRetentionFN: "true",
},
expectErr: assert.NoError,
expect: repository.Retention{
Mode: ptr.To(repository.GovernanceRetention),
Duration: ptr.To(time.Hour * 48),
Extend: ptr.To(true),
},
},
}
for _, test := range table {
suite.Run(test.name, func() {
t := suite.T()
cmd := &cobra.Command{}
flags.AddRetentionConfigFlags(cmd)
fs := cmd.Flags()
for fn, fv := range test.flags {
require.NoError(t, fs.Set(fn, fv), "setting flag values")
}
result, err := utils.MakeRetentionOpts(cmd)
test.expectErr(t, err, "parsing flags into struct: %v", clues.ToCore(err))
if err != nil {
return
}
assert.Equal(t, test.expect, result)
})
}
}

View File

@ -54,16 +54,22 @@ const (
type RetentionMode int type RetentionMode int
// Can't be reordered as we rely on iota for numbering.
//
//go:generate stringer -type=RetentionMode -linecomment //go:generate stringer -type=RetentionMode -linecomment
const ( const (
UnknownRetention RetentionMode = 0 UnknownRetention RetentionMode = 0
NoRetention RetentionMode = 1 NoRetention RetentionMode = 1 // none
GovernanceRetention RetentionMode = 2 GovernanceRetention RetentionMode = 2 // governance
ComplianceRetention RetentionMode = 3 ComplianceRetention RetentionMode = 3 // compliance
) )
func ValidRetentionModeNames() map[string]RetentionMode {
return map[string]RetentionMode{
NoRetention.String(): NoRetention,
GovernanceRetention.String(): GovernanceRetention,
ComplianceRetention.String(): ComplianceRetention,
}
}
// Retention contains various options for configuring the retention mode. Takes // Retention contains various options for configuring the retention mode. Takes
// pointers instead of values so that we can tell the difference between an // pointers instead of values so that we can tell the difference between an
// unset value and a set but invalid value. This allows for partial // unset value and a set but invalid value. This allows for partial

View File

@ -14,9 +14,9 @@ func _() {
_ = x[ComplianceRetention-3] _ = x[ComplianceRetention-3]
} }
const _RetentionMode_name = "UnknownRetentionNoRetentionGovernanceRetentionComplianceRetention" const _RetentionMode_name = "UnknownRetentionnonegovernancecompliance"
var _RetentionMode_index = [...]uint8{0, 16, 27, 46, 65} var _RetentionMode_index = [...]uint8{0, 16, 20, 30, 40}
func (i RetentionMode) String() string { func (i RetentionMode) String() string {
if i < 0 || i >= RetentionMode(len(_RetentionMode_index)-1) { if i < 0 || i >= RetentionMode(len(_RetentionMode_index)-1) {