corso/src/pkg/storage/storage.go
Abhishek Pandey d2c73827cb
Add CLI for local storage (#4243)
<!-- PR description-->

New commands to initialize & connect to a repo on local or network attached storage.
* `repo init filesystem --path /tmp/repo`
* `repo connect filesystem --path /tmp/repo`

Includes basic unit & e2e tests. More coverage to be added in a following PR to keep the size contained.

**Updates:**
* Added Repo path sanitization i.e. handle relative paths, make paths cross platform compatible, etc. 
* Removed retention artifacts, not supported for filesystem storage.
* cli docs - auto updated.
* Manually tested with all corso backup/restore/export commands.

**Doesn't include** 
1. Symlinks
2. User ids wiring into repo.
3. Repos documentation update - in an upcoming PR.
4. Prefix support -> kopia doesn't support prefixes for `filesystem` storage
5. More E2E tests. 

---

#### 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

<!--- Please check the type of change your PR introduces: --->
- [x] 🌻 Feature
- [ ] 🐛 Bugfix
- [ ] 🗺️ Documentation
- [ ] 🤖 Supportability/Tests
- [ ] 💻 CI/Deployment
- [ ] 🧹 Tech Debt/Cleanup

#### Issue(s)

<!-- Can reference multiple issues. Use one of the following "magic words" - "closes, fixes" to auto-close the Github issue. -->
* #1416 

#### Test Plan

<!-- How will this be tested prior to merging.-->
- [x] 💪 Manual
- [x]  Unit test
- [x] 💚 E2E
2023-09-19 09:46:54 +00:00

176 lines
4.1 KiB
Go

package storage
import (
"fmt"
"github.com/alcionai/clues"
"github.com/spf13/cast"
"github.com/alcionai/corso/src/internal/common"
)
type ProviderType int
//go:generate stringer -type=ProviderType -linecomment
const (
ProviderUnknown ProviderType = 0 // Unknown Provider
ProviderS3 ProviderType = 1 // S3
ProviderFilesystem ProviderType = 2 // Filesystem
)
var StringToProviderType = map[string]ProviderType{
ProviderUnknown.String(): ProviderUnknown,
ProviderS3.String(): ProviderS3,
ProviderFilesystem.String(): ProviderFilesystem,
}
const (
StorageProviderTypeKey = "provider"
)
// storage parsing errors
var (
errMissingRequired = clues.New("missing required storage configuration")
)
// Storage defines a storage provider, along with any configuration
// required to set up or communicate with that provider.
type Storage struct {
Provider ProviderType
Config map[string]string
// TODO: These are AWS S3 specific -> move these out
SessionTags map[string]string
Role string
SessionName string
SessionDuration string
}
// NewStorage aggregates all the supplied configurations into a single configuration.
func NewStorage(p ProviderType, cfgs ...common.StringConfigurer) (Storage, error) {
cs, err := common.UnionStringConfigs(cfgs...)
return Storage{
Provider: p,
Config: cs,
}, err
}
// NewStorageUsingRole supports specifying an AWS IAM role the storage provider
// should assume.
func NewStorageUsingRole(
p ProviderType,
roleARN string,
sessionName string,
sessionTags map[string]string,
duration string,
cfgs ...common.StringConfigurer,
) (Storage, error) {
cs, err := common.UnionStringConfigs(cfgs...)
return Storage{
Provider: p,
Config: cs,
Role: roleARN,
SessionTags: sessionTags,
SessionName: sessionName,
SessionDuration: duration,
}, err
}
// Helper for parsing the values in a config object.
// If the value is nil or not a string, returns an empty string.
func orEmptyString(v any) string {
defer func() {
r := recover()
if r != nil {
fmt.Printf("panic recovery casting %v to string\n", v)
}
}()
if v == nil {
return ""
}
return v.(string)
}
func (s Storage) StorageConfig() (Configurer, error) {
switch s.Provider {
case ProviderS3:
return buildS3ConfigFromMap(s.Config)
case ProviderFilesystem:
return buildFilesystemConfigFromMap(s.Config)
}
return nil, clues.New("unsupported storage provider: " + s.Provider.String())
}
func NewStorageConfig(provider ProviderType) (Configurer, error) {
switch provider {
case ProviderS3:
return &S3Config{}, nil
case ProviderFilesystem:
return &FilesystemConfig{}, nil
}
return nil, clues.New("unsupported storage provider: " + provider.String())
}
type Getter interface {
Get(key string) any
}
type Setter interface {
Set(key string, value any)
}
// WriteConfigToStorer writes config key value pairs to provided store.
type WriteConfigToStorer interface {
WriteConfigToStore(
s Setter,
)
}
type Configurer interface {
common.StringConfigurer
// ApplyOverrides fetches config from file, processes overrides
// from sources like environment variables and flags, and updates the
// underlying configuration accordingly.
ApplyConfigOverrides(
g Getter,
readConfigFromStore bool,
matchFromConfig bool,
overrides map[string]string,
) error
WriteConfigToStorer
}
// mustMatchConfig compares the values of each key to their config file value in store.
// If any value differs from the store value, an error is returned.
// values in m that aren't stored in the config are ignored.
func mustMatchConfig(
g Getter,
tomlMap map[string]string,
m map[string]string,
) error {
for k, v := range m {
if len(v) == 0 {
continue // empty variables will get caught by configuration validators, if necessary
}
tomlK, ok := tomlMap[k]
if !ok {
continue // m may declare values which aren't stored in the config file
}
vv := cast.ToString(g.Get(tomlK))
if v != vv {
return clues.New("value of " + k + " (" + v + ") does not match corso configuration value (" + vv + ")")
}
}
return nil
}