allow connect to create .corso config file (#256)
* allow connect to create .corso config file Current bug: if no .corso config file exists, then repo connect will always fail, even if it has the correct details to build a new config file. Solution: allow connect to build a .corso config file when missing, so long as the operation succeeds otherwise. In tandem, return an error whenever a user attempts to call repo connect with details that do not match the existing .corso config file.
This commit is contained in:
parent
ae778df5ae
commit
8c399a6dc1
80
src/cli/config/account.go
Normal file
80
src/cli/config/account.go
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/alcionai/corso/cli/utils"
|
||||||
|
"github.com/alcionai/corso/pkg/account"
|
||||||
|
"github.com/alcionai/corso/pkg/credentials"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
// prerequisite: readRepoConfig must have been run prior to this to populate the global viper values.
|
||||||
|
func m365ConfigsFromViper(vpr *viper.Viper) (account.M365Config, error) {
|
||||||
|
var m365 account.M365Config
|
||||||
|
|
||||||
|
providerType := vpr.GetString(AccountProviderTypeKey)
|
||||||
|
if providerType != account.ProviderM365.String() {
|
||||||
|
return m365, errors.New("unsupported account provider: " + providerType)
|
||||||
|
}
|
||||||
|
|
||||||
|
m365.TenantID = vpr.GetString(TenantIDKey)
|
||||||
|
return m365, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func m365Overrides(in map[string]string) map[string]string {
|
||||||
|
return map[string]string{
|
||||||
|
account.TenantID: in[account.TenantID],
|
||||||
|
AccountProviderTypeKey: in[AccountProviderTypeKey],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// configureAccount builds a complete account configuration from a mix of
|
||||||
|
// viper properties and manual overrides.
|
||||||
|
func configureAccount(vpr *viper.Viper, readConfigFromViper bool, overrides map[string]string) (account.Account, error) {
|
||||||
|
var (
|
||||||
|
m365Cfg account.M365Config
|
||||||
|
acct account.Account
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
if readConfigFromViper {
|
||||||
|
m365Cfg, err = m365ConfigsFromViper(vpr)
|
||||||
|
if err != nil {
|
||||||
|
return acct, errors.Wrap(err, "reading m365 configs from corso config file")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := mustMatchConfig(vpr, m365Overrides(overrides)); err != nil {
|
||||||
|
return acct, errors.Wrap(err, "verifying m365 configs in corso config file")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// compose the m365 config and credentials
|
||||||
|
m365 := credentials.GetM365()
|
||||||
|
if err := m365.Validate(); err != nil {
|
||||||
|
return acct, errors.Wrap(err, "validating m365 credentials")
|
||||||
|
}
|
||||||
|
|
||||||
|
m365Cfg = account.M365Config{
|
||||||
|
M365: m365,
|
||||||
|
TenantID: first(overrides[account.TenantID], m365Cfg.TenantID, os.Getenv(account.TenantID)),
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure requried properties are present
|
||||||
|
if err := utils.RequireProps(map[string]string{
|
||||||
|
credentials.ClientID: m365Cfg.ClientID,
|
||||||
|
credentials.ClientSecret: m365Cfg.ClientSecret,
|
||||||
|
account.TenantID: m365Cfg.TenantID,
|
||||||
|
}); err != nil {
|
||||||
|
return acct, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// build the account
|
||||||
|
acct, err = account.NewAccount(account.ProviderM365, m365Cfg)
|
||||||
|
if err != nil {
|
||||||
|
return acct, errors.Wrap(err, "retrieving m365 account configuration")
|
||||||
|
}
|
||||||
|
|
||||||
|
return acct, nil
|
||||||
|
}
|
||||||
@ -8,9 +8,7 @@ import (
|
|||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
|
|
||||||
"github.com/alcionai/corso/cli/utils"
|
|
||||||
"github.com/alcionai/corso/pkg/account"
|
"github.com/alcionai/corso/pkg/account"
|
||||||
"github.com/alcionai/corso/pkg/credentials"
|
|
||||||
"github.com/alcionai/corso/pkg/storage"
|
"github.com/alcionai/corso/pkg/storage"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -27,6 +25,12 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func InitConfig(configFilePath string) error {
|
func InitConfig(configFilePath string) error {
|
||||||
|
return initConfigWithViper(viper.GetViper(), configFilePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// initConfigWithViper implements InitConfig, but takes in a viper
|
||||||
|
// struct for testing.
|
||||||
|
func initConfigWithViper(vpr *viper.Viper, configFilePath string) error {
|
||||||
// Configure default config file location
|
// Configure default config file location
|
||||||
if configFilePath == "" {
|
if configFilePath == "" {
|
||||||
// Find home directory.
|
// Find home directory.
|
||||||
@ -36,18 +40,17 @@ func InitConfig(configFilePath string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Search config in home directory with name ".corso" (without extension).
|
// Search config in home directory with name ".corso" (without extension).
|
||||||
viper.AddConfigPath(home)
|
vpr.AddConfigPath(home)
|
||||||
viper.SetConfigType("toml")
|
vpr.SetConfigType("toml")
|
||||||
viper.SetConfigName(".corso")
|
vpr.SetConfigName(".corso")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
// Use a custom file location
|
|
||||||
|
|
||||||
viper.SetConfigFile(configFilePath)
|
vpr.SetConfigFile(configFilePath)
|
||||||
// We also configure the path, type and filename
|
// We also configure the path, type and filename
|
||||||
// because `viper.SafeWriteConfig` needs these set to
|
// because `vpr.SafeWriteConfig` needs these set to
|
||||||
// work correctly (it does not use the configured file)
|
// work correctly (it does not use the configured file)
|
||||||
viper.AddConfigPath(path.Dir(configFilePath))
|
vpr.AddConfigPath(path.Dir(configFilePath))
|
||||||
|
|
||||||
fileName := path.Base(configFilePath)
|
fileName := path.Base(configFilePath)
|
||||||
ext := path.Ext(configFilePath)
|
ext := path.Ext(configFilePath)
|
||||||
@ -55,147 +58,85 @@ func InitConfig(configFilePath string) error {
|
|||||||
return errors.New("config file requires an extension e.g. `toml`")
|
return errors.New("config file requires an extension e.g. `toml`")
|
||||||
}
|
}
|
||||||
fileName = strings.TrimSuffix(fileName, ext)
|
fileName = strings.TrimSuffix(fileName, ext)
|
||||||
viper.SetConfigType(ext[1:])
|
vpr.SetConfigType(ext[1:])
|
||||||
viper.SetConfigName(fileName)
|
vpr.SetConfigName(fileName)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// WriteRepoConfig currently just persists corso config to the config file
|
// WriteRepoConfig currently just persists corso config to the config file
|
||||||
// It does not check for conflicts or existing data.
|
// It does not check for conflicts or existing data.
|
||||||
func WriteRepoConfig(s3Config storage.S3Config, account account.M365Config) error {
|
func WriteRepoConfig(s3Config storage.S3Config, m365Config account.M365Config) error {
|
||||||
|
return writeRepoConfigWithViper(viper.GetViper(), s3Config, m365Config)
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeRepoConfigWithViper implements WriteRepoConfig, but takes in a viper
|
||||||
|
// struct for testing.
|
||||||
|
func writeRepoConfigWithViper(vpr *viper.Viper, s3Config storage.S3Config, m365Config account.M365Config) error {
|
||||||
// Rudimentary support for persisting repo config
|
// Rudimentary support for persisting repo config
|
||||||
// TODO: Handle conflicts, support other config types
|
// TODO: Handle conflicts, support other config types
|
||||||
viper.Set(StorageProviderTypeKey, storage.ProviderS3.String())
|
vpr.Set(StorageProviderTypeKey, storage.ProviderS3.String())
|
||||||
viper.Set(BucketNameKey, s3Config.Bucket)
|
vpr.Set(BucketNameKey, s3Config.Bucket)
|
||||||
viper.Set(EndpointKey, s3Config.Endpoint)
|
vpr.Set(EndpointKey, s3Config.Endpoint)
|
||||||
viper.Set(PrefixKey, s3Config.Prefix)
|
vpr.Set(PrefixKey, s3Config.Prefix)
|
||||||
viper.Set(TenantIDKey, account.TenantID)
|
|
||||||
|
|
||||||
if err := viper.SafeWriteConfig(); err != nil {
|
vpr.Set(AccountProviderTypeKey, account.ProviderM365.String())
|
||||||
|
vpr.Set(TenantIDKey, m365Config.TenantID)
|
||||||
|
|
||||||
|
if err := vpr.SafeWriteConfig(); err != nil {
|
||||||
if _, ok := err.(viper.ConfigFileAlreadyExistsError); ok {
|
if _, ok := err.(viper.ConfigFileAlreadyExistsError); ok {
|
||||||
return viper.GetViper().WriteConfig()
|
return vpr.WriteConfig()
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func readRepoConfig() error {
|
|
||||||
var err error
|
|
||||||
|
|
||||||
if err = viper.ReadInConfig(); err != nil {
|
|
||||||
return errors.Wrap(err, "reading config file: "+viper.ConfigFileUsed())
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// prerequisite: readRepoConfig must have been run prior to this to populate the global viper values.
|
|
||||||
func s3ConfigsFromViper() (storage.S3Config, error) {
|
|
||||||
var s3Config storage.S3Config
|
|
||||||
|
|
||||||
providerType := viper.GetString(StorageProviderTypeKey)
|
|
||||||
if providerType != storage.ProviderS3.String() {
|
|
||||||
return s3Config, errors.New("unsupported storage provider: " + providerType)
|
|
||||||
}
|
|
||||||
|
|
||||||
s3Config.Bucket = viper.GetString(BucketNameKey)
|
|
||||||
s3Config.Endpoint = viper.GetString(EndpointKey)
|
|
||||||
s3Config.Prefix = viper.GetString(PrefixKey)
|
|
||||||
return s3Config, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// prerequisite: readRepoConfig must have been run prior to this to populate the global viper values.
|
|
||||||
func m365ConfigsFromViper() (account.M365Config, error) {
|
|
||||||
var m365 account.M365Config
|
|
||||||
|
|
||||||
providerType := viper.GetString(AccountProviderTypeKey)
|
|
||||||
if providerType != account.ProviderM365.String() {
|
|
||||||
return m365, errors.New("unsupported account provider: " + providerType)
|
|
||||||
}
|
|
||||||
|
|
||||||
m365.TenantID = first(viper.GetString(TenantIDKey), os.Getenv(account.TenantID))
|
|
||||||
return m365, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetStorageAndAccount creates a storage and account instance by mediating all the possible
|
// GetStorageAndAccount creates a storage and account instance by mediating all the possible
|
||||||
// data sources (config file, env vars, flag overrides) and the config file.
|
// data sources (config file, env vars, flag overrides) and the config file.
|
||||||
func GetStorageAndAccount(readFromFile bool, overrides map[string]string) (storage.Storage, account.Account, error) {
|
func GetStorageAndAccount(readFromFile bool, overrides map[string]string) (storage.Storage, account.Account, error) {
|
||||||
|
return getStorageAndAccountWithViper(viper.GetViper(), readFromFile, overrides)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getSorageAndAccountWithViper implements GetSorageAndAccount, but takes in a viper
|
||||||
|
// struct for testing.
|
||||||
|
func getStorageAndAccountWithViper(vpr *viper.Viper, readFromFile bool, overrides map[string]string) (storage.Storage, account.Account, error) {
|
||||||
var (
|
var (
|
||||||
s3Cfg storage.S3Config
|
store storage.Storage
|
||||||
store storage.Storage
|
acct account.Account
|
||||||
m365Cfg account.M365Config
|
err error
|
||||||
acct account.Account
|
|
||||||
err error
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
readConfigFromViper := readFromFile
|
||||||
|
|
||||||
// possibly read the prior config from a .corso file
|
// possibly read the prior config from a .corso file
|
||||||
if readFromFile {
|
if readFromFile {
|
||||||
if err = readRepoConfig(); err != nil {
|
err = vpr.ReadInConfig()
|
||||||
return store, acct, errors.Wrap(err, "reading corso config file")
|
|
||||||
}
|
|
||||||
|
|
||||||
s3Cfg, err = s3ConfigsFromViper()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return store, acct, errors.Wrap(err, "reading s3 configs from corso config file")
|
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
|
||||||
}
|
return store, acct, errors.Wrap(err, "reading corso config file: "+vpr.ConfigFileUsed())
|
||||||
|
}
|
||||||
m365Cfg, err = m365ConfigsFromViper()
|
readConfigFromViper = false
|
||||||
if err != nil {
|
|
||||||
return store, acct, errors.Wrap(err, "reading m365 configs from corso config file")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// compose the m365 account config and credentials
|
acct, err = configureAccount(vpr, readConfigFromViper, overrides)
|
||||||
m365Cfg = account.M365Config{
|
|
||||||
M365: credentials.GetM365(),
|
|
||||||
TenantID: first(overrides[account.TenantID], m365Cfg.TenantID, os.Getenv(account.TenantID)),
|
|
||||||
}
|
|
||||||
acct, err = account.NewAccount(account.ProviderM365, m365Cfg)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return store, acct, errors.Wrap(err, "retrieving m365 account configuration")
|
return store, acct, errors.Wrap(err, "retrieving account configuration details")
|
||||||
}
|
}
|
||||||
|
|
||||||
// compose the s3 storage config and credentials
|
store, err = configureStorage(vpr, readConfigFromViper, overrides)
|
||||||
aws := credentials.GetAWS(overrides)
|
|
||||||
if err := aws.Validate(); err != nil {
|
|
||||||
return storage.Storage{}, acct, errors.Wrap(err, "validating aws credentials")
|
|
||||||
}
|
|
||||||
s3Cfg = storage.S3Config{
|
|
||||||
AWS: aws,
|
|
||||||
Bucket: first(overrides[storage.Bucket], s3Cfg.Bucket),
|
|
||||||
Endpoint: first(overrides[storage.Endpoint], s3Cfg.Endpoint),
|
|
||||||
Prefix: first(overrides[storage.Prefix], s3Cfg.Prefix),
|
|
||||||
}
|
|
||||||
|
|
||||||
// compose the common config and credentials
|
|
||||||
corso := credentials.GetCorso()
|
|
||||||
if err := corso.Validate(); err != nil {
|
|
||||||
return storage.Storage{}, acct, errors.Wrap(err, "validating corso credentials")
|
|
||||||
}
|
|
||||||
cCfg := storage.CommonConfig{
|
|
||||||
Corso: corso,
|
|
||||||
}
|
|
||||||
|
|
||||||
// ensure requried properties are present
|
|
||||||
if err := utils.RequireProps(map[string]string{
|
|
||||||
credentials.AWSAccessKeyID: aws.AccessKey,
|
|
||||||
storage.Bucket: s3Cfg.Bucket,
|
|
||||||
credentials.AWSSecretAccessKey: aws.SecretKey,
|
|
||||||
credentials.AWSSessionToken: aws.SessionToken,
|
|
||||||
credentials.CorsoPassword: corso.CorsoPassword,
|
|
||||||
}); err != nil {
|
|
||||||
return storage.Storage{}, acct, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// return a complete storage
|
|
||||||
s, err := storage.NewStorage(storage.ProviderS3, s3Cfg, cCfg)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return storage.Storage{}, acct, errors.Wrap(err, "configuring repository storage")
|
return store, acct, errors.Wrap(err, "retrieving storage provider details")
|
||||||
}
|
}
|
||||||
return s, acct, nil
|
|
||||||
|
return store, acct, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Helper funcs
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
// returns the first non-zero valued string
|
// returns the first non-zero valued string
|
||||||
func first(vs ...string) string {
|
func first(vs ...string) string {
|
||||||
for _, v := range vs {
|
for _, v := range vs {
|
||||||
@ -205,3 +146,32 @@ func first(vs ...string) string {
|
|||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var constToTomlKeyMap = map[string]string{
|
||||||
|
account.TenantID: TenantIDKey,
|
||||||
|
AccountProviderTypeKey: AccountProviderTypeKey,
|
||||||
|
storage.Bucket: BucketNameKey,
|
||||||
|
storage.Endpoint: EndpointKey,
|
||||||
|
storage.Prefix: PrefixKey,
|
||||||
|
StorageProviderTypeKey: StorageProviderTypeKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
// mustMatchConfig compares the values of each key to their config file value in viper.
|
||||||
|
// If any value differs from the viper value, an error is returned.
|
||||||
|
// values in m that aren't stored in the config are ignored.
|
||||||
|
func mustMatchConfig(vpr *viper.Viper, 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 := constToTomlKeyMap[k]
|
||||||
|
if !ok {
|
||||||
|
continue // m may declare values which aren't stored in the config file
|
||||||
|
}
|
||||||
|
vv := vpr.GetString(tomlK)
|
||||||
|
if v != vv {
|
||||||
|
return errors.New("value of " + k + " (" + v + ") does not match corso configuration value (" + vv + ")")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package config
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@ -11,18 +12,20 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
|
|
||||||
|
ctesting "github.com/alcionai/corso/internal/testing"
|
||||||
"github.com/alcionai/corso/pkg/account"
|
"github.com/alcionai/corso/pkg/account"
|
||||||
|
"github.com/alcionai/corso/pkg/credentials"
|
||||||
"github.com/alcionai/corso/pkg/storage"
|
"github.com/alcionai/corso/pkg/storage"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
configFileTemplate = `
|
configFileTemplate = `
|
||||||
bucket = '%s'
|
` + BucketNameKey + ` = '%s'
|
||||||
endpoint = 's3.amazonaws.com'
|
` + EndpointKey + ` = 's3.amazonaws.com'
|
||||||
prefix = 'test-prefix'
|
` + PrefixKey + ` = 'test-prefix'
|
||||||
provider = 'S3'
|
` + StorageProviderTypeKey + ` = 'S3'
|
||||||
account_provider = 'M365'
|
` + AccountProviderTypeKey + ` = 'M365'
|
||||||
tenantid = '%s'
|
` + TenantIDKey + ` = '%s'
|
||||||
`
|
`
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -35,51 +38,268 @@ func TestConfigSuite(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (suite *ConfigSuite) TestReadRepoConfigBasic() {
|
func (suite *ConfigSuite) TestReadRepoConfigBasic() {
|
||||||
|
var (
|
||||||
|
t = suite.T()
|
||||||
|
vpr = viper.New()
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
b = "read-repo-config-basic-bucket"
|
||||||
|
tID = "6f34ac30-8196-469b-bf8f-d83deadbbbba"
|
||||||
|
)
|
||||||
|
|
||||||
// Generate test config file
|
// Generate test config file
|
||||||
b := "read-repo-config-basic-bucket"
|
|
||||||
tID := "6f34ac30-8196-469b-bf8f-d83deadbbbba"
|
|
||||||
testConfigData := fmt.Sprintf(configFileTemplate, b, tID)
|
testConfigData := fmt.Sprintf(configFileTemplate, b, tID)
|
||||||
testConfigFilePath := path.Join(suite.T().TempDir(), "corso.toml")
|
testConfigFilePath := path.Join(t.TempDir(), "corso.toml")
|
||||||
err := ioutil.WriteFile(testConfigFilePath, []byte(testConfigData), 0700)
|
err := ioutil.WriteFile(testConfigFilePath, []byte(testConfigData), 0700)
|
||||||
assert.NoError(suite.T(), err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Configure viper to read test config file
|
// Configure viper to read test config file
|
||||||
viper.SetConfigFile(testConfigFilePath)
|
vpr.SetConfigFile(testConfigFilePath)
|
||||||
|
|
||||||
// Read and validate config
|
// Read and validate config
|
||||||
err = readRepoConfig()
|
require.NoError(t, vpr.ReadInConfig(), "reading repo config")
|
||||||
require.NoError(suite.T(), err)
|
|
||||||
|
|
||||||
s3Cfg, err := s3ConfigsFromViper()
|
s3Cfg, err := s3ConfigsFromViper(vpr)
|
||||||
require.NoError(suite.T(), err)
|
require.NoError(t, err)
|
||||||
assert.Equal(suite.T(), b, s3Cfg.Bucket)
|
assert.Equal(t, b, s3Cfg.Bucket)
|
||||||
|
|
||||||
m365, err := m365ConfigsFromViper()
|
m365, err := m365ConfigsFromViper(vpr)
|
||||||
require.NoError(suite.T(), err)
|
require.NoError(t, err)
|
||||||
assert.Equal(suite.T(), tID, m365.TenantID)
|
assert.Equal(t, tID, m365.TenantID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *ConfigSuite) TestWriteReadConfig() {
|
func (suite *ConfigSuite) TestWriteReadConfig() {
|
||||||
|
var (
|
||||||
|
t = suite.T()
|
||||||
|
vpr = viper.New()
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
bkt = "write-read-config-bucket"
|
||||||
|
tid = "3c0748d2-470e-444c-9064-1268e52609d5"
|
||||||
|
)
|
||||||
|
|
||||||
// Configure viper to read test config file
|
// Configure viper to read test config file
|
||||||
tempDir := suite.T().TempDir()
|
testConfigFilePath := path.Join(t.TempDir(), "corso.toml")
|
||||||
testConfigFilePath := path.Join(tempDir, "corso.toml")
|
require.NoError(t, initConfigWithViper(vpr, testConfigFilePath), "initializing repo config")
|
||||||
err := InitConfig(testConfigFilePath)
|
|
||||||
assert.NoError(suite.T(), err)
|
|
||||||
|
|
||||||
s3Cfg := storage.S3Config{Bucket: "write-read-config-bucket"}
|
s3Cfg := storage.S3Config{Bucket: bkt}
|
||||||
m365 := account.M365Config{TenantID: "3c0748d2-470e-444c-9064-1268e52609d5"}
|
m365 := account.M365Config{TenantID: tid}
|
||||||
|
|
||||||
err = WriteRepoConfig(s3Cfg, m365)
|
require.NoError(t, writeRepoConfigWithViper(vpr, s3Cfg, m365), "writing repo config")
|
||||||
require.NoError(suite.T(), err)
|
require.NoError(t, vpr.ReadInConfig(), "reading repo config")
|
||||||
|
|
||||||
err = readRepoConfig()
|
readS3Cfg, err := s3ConfigsFromViper(vpr)
|
||||||
require.NoError(suite.T(), err)
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, readS3Cfg.Bucket, s3Cfg.Bucket)
|
||||||
|
|
||||||
readS3Cfg, err := s3ConfigsFromViper()
|
readM365, err := m365ConfigsFromViper(vpr)
|
||||||
require.NoError(suite.T(), err)
|
require.NoError(t, err)
|
||||||
assert.Equal(suite.T(), readS3Cfg.Bucket, s3Cfg.Bucket)
|
assert.Equal(t, readM365.TenantID, m365.TenantID)
|
||||||
|
}
|
||||||
readM365, err := m365ConfigsFromViper()
|
|
||||||
require.NoError(suite.T(), err)
|
func (suite *ConfigSuite) TestMustMatchConfig() {
|
||||||
assert.Equal(suite.T(), readM365.TenantID, m365.TenantID)
|
var (
|
||||||
|
t = suite.T()
|
||||||
|
vpr = viper.New()
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
bkt = "must-match-config-bucket"
|
||||||
|
tid = "dfb12063-7598-458b-85ab-42352c5c25e2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Configure viper to read test config file
|
||||||
|
testConfigFilePath := path.Join(t.TempDir(), "corso.toml")
|
||||||
|
require.NoError(t, initConfigWithViper(vpr, testConfigFilePath), "initializing repo config")
|
||||||
|
|
||||||
|
s3Cfg := storage.S3Config{Bucket: bkt}
|
||||||
|
m365 := account.M365Config{TenantID: tid}
|
||||||
|
|
||||||
|
require.NoError(t, writeRepoConfigWithViper(vpr, s3Cfg, m365), "writing repo config")
|
||||||
|
require.NoError(t, vpr.ReadInConfig(), "reading repo config")
|
||||||
|
|
||||||
|
table := []struct {
|
||||||
|
name string
|
||||||
|
input map[string]string
|
||||||
|
errCheck assert.ErrorAssertionFunc
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "full match",
|
||||||
|
input: map[string]string{
|
||||||
|
storage.Bucket: bkt,
|
||||||
|
account.TenantID: tid,
|
||||||
|
},
|
||||||
|
errCheck: assert.NoError,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty values",
|
||||||
|
input: map[string]string{
|
||||||
|
storage.Bucket: "",
|
||||||
|
account.TenantID: "",
|
||||||
|
},
|
||||||
|
errCheck: assert.NoError,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no overrides",
|
||||||
|
input: map[string]string{},
|
||||||
|
errCheck: assert.NoError,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "nil map",
|
||||||
|
input: nil,
|
||||||
|
errCheck: assert.NoError,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no recognized keys",
|
||||||
|
input: map[string]string{
|
||||||
|
"fnords": "smurfs",
|
||||||
|
"nonsense": "",
|
||||||
|
},
|
||||||
|
errCheck: assert.NoError,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mismatch",
|
||||||
|
input: map[string]string{
|
||||||
|
storage.Bucket: tid,
|
||||||
|
account.TenantID: bkt,
|
||||||
|
},
|
||||||
|
errCheck: assert.Error,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range table {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
test.errCheck(t, mustMatchConfig(vpr, test.input))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
// integration tests
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
|
||||||
|
type ConfigIntegrationSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigIntegrationSuite(t *testing.T) {
|
||||||
|
if err := ctesting.RunOnAny(
|
||||||
|
ctesting.CorsoCITests,
|
||||||
|
ctesting.CorsoCLIConfigTests,
|
||||||
|
); err != nil {
|
||||||
|
t.Skip(err)
|
||||||
|
}
|
||||||
|
suite.Run(t, new(ConfigIntegrationSuite))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *ConfigIntegrationSuite) SetupSuite() {
|
||||||
|
_, err := ctesting.GetRequiredEnvVars(
|
||||||
|
append(
|
||||||
|
ctesting.AWSStorageCredEnvs,
|
||||||
|
ctesting.M365AcctCredEnvs...,
|
||||||
|
)...,
|
||||||
|
)
|
||||||
|
require.NoError(suite.T(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *ConfigIntegrationSuite) TestGetStorageAndAccount() {
|
||||||
|
t := suite.T()
|
||||||
|
vpr := viper.New()
|
||||||
|
|
||||||
|
const (
|
||||||
|
bkt = "get-storage-and-account-bucket"
|
||||||
|
end = "https://get-storage-and-account.com"
|
||||||
|
pfx = "get-storage-and-account-prefix"
|
||||||
|
tid = "3a2faa4e-a882-445c-9d27-f552ef189381"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Configure viper to read test config file
|
||||||
|
testConfigFilePath := path.Join(t.TempDir(), "corso.toml")
|
||||||
|
require.NoError(t, initConfigWithViper(vpr, testConfigFilePath), "initializing repo config")
|
||||||
|
|
||||||
|
s3Cfg := storage.S3Config{
|
||||||
|
Bucket: bkt,
|
||||||
|
Endpoint: end,
|
||||||
|
Prefix: pfx,
|
||||||
|
}
|
||||||
|
m365 := account.M365Config{TenantID: tid}
|
||||||
|
|
||||||
|
require.NoError(t, writeRepoConfigWithViper(vpr, s3Cfg, m365), "writing repo config")
|
||||||
|
require.NoError(t, vpr.ReadInConfig(), "reading repo config")
|
||||||
|
|
||||||
|
st, ac, err := getStorageAndAccountWithViper(vpr, true, nil)
|
||||||
|
require.NoError(t, err, "getting storage and account from config")
|
||||||
|
|
||||||
|
readS3Cfg, err := st.S3Config()
|
||||||
|
require.NoError(t, err, "reading s3 config from storage")
|
||||||
|
assert.Equal(t, readS3Cfg.Bucket, s3Cfg.Bucket)
|
||||||
|
assert.Equal(t, readS3Cfg.Endpoint, s3Cfg.Endpoint)
|
||||||
|
assert.Equal(t, readS3Cfg.Prefix, s3Cfg.Prefix)
|
||||||
|
|
||||||
|
assert.Equal(t, readS3Cfg.AccessKey, os.Getenv(credentials.AWSAccessKeyID))
|
||||||
|
assert.Equal(t, readS3Cfg.SecretKey, os.Getenv(credentials.AWSSecretAccessKey))
|
||||||
|
assert.Equal(t, readS3Cfg.SessionToken, os.Getenv(credentials.AWSSessionToken))
|
||||||
|
|
||||||
|
common, err := st.CommonConfig()
|
||||||
|
require.NoError(t, err, "reading common config from storage")
|
||||||
|
assert.Equal(t, common.CorsoPassword, os.Getenv(credentials.CorsoPassword))
|
||||||
|
|
||||||
|
readM365, err := ac.M365Config()
|
||||||
|
require.NoError(t, err, "reading m365 config from account")
|
||||||
|
assert.Equal(t, readM365.TenantID, m365.TenantID)
|
||||||
|
assert.Equal(t, readM365.ClientID, os.Getenv(credentials.ClientID))
|
||||||
|
assert.Equal(t, readM365.ClientSecret, os.Getenv(credentials.ClientSecret))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *ConfigIntegrationSuite) TestGetStorageAndAccount_noFileOnlyOverrides() {
|
||||||
|
t := suite.T()
|
||||||
|
vpr := viper.New()
|
||||||
|
|
||||||
|
const (
|
||||||
|
bkt = "get-storage-and-account-no-file-bucket"
|
||||||
|
end = "https://get-storage-and-account.com/no-file"
|
||||||
|
pfx = "get-storage-and-account-no-file-prefix"
|
||||||
|
tid = "88f8522b-18e4-4d0f-b514-2d7b34d4c5a1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Configure viper to read test config file
|
||||||
|
s3Cfg := storage.S3Config{
|
||||||
|
Bucket: bkt,
|
||||||
|
Endpoint: end,
|
||||||
|
Prefix: pfx,
|
||||||
|
}
|
||||||
|
m365 := account.M365Config{TenantID: tid}
|
||||||
|
|
||||||
|
overrides := map[string]string{
|
||||||
|
account.TenantID: tid,
|
||||||
|
AccountProviderTypeKey: account.ProviderM365.String(),
|
||||||
|
storage.Bucket: bkt,
|
||||||
|
storage.Endpoint: end,
|
||||||
|
storage.Prefix: pfx,
|
||||||
|
StorageProviderTypeKey: storage.ProviderS3.String(),
|
||||||
|
}
|
||||||
|
|
||||||
|
st, ac, err := getStorageAndAccountWithViper(vpr, false, overrides)
|
||||||
|
require.NoError(t, err, "getting storage and account from config")
|
||||||
|
|
||||||
|
readS3Cfg, err := st.S3Config()
|
||||||
|
require.NoError(t, err, "reading s3 config from storage")
|
||||||
|
assert.Equal(t, readS3Cfg.Bucket, s3Cfg.Bucket)
|
||||||
|
assert.Equal(t, readS3Cfg.Endpoint, s3Cfg.Endpoint)
|
||||||
|
assert.Equal(t, readS3Cfg.Prefix, s3Cfg.Prefix)
|
||||||
|
|
||||||
|
assert.Equal(t, readS3Cfg.AccessKey, os.Getenv(credentials.AWSAccessKeyID))
|
||||||
|
assert.Equal(t, readS3Cfg.SecretKey, os.Getenv(credentials.AWSSecretAccessKey))
|
||||||
|
assert.Equal(t, readS3Cfg.SessionToken, os.Getenv(credentials.AWSSessionToken))
|
||||||
|
|
||||||
|
common, err := st.CommonConfig()
|
||||||
|
require.NoError(t, err, "reading common config from storage")
|
||||||
|
assert.Equal(t, common.CorsoPassword, os.Getenv(credentials.CorsoPassword))
|
||||||
|
|
||||||
|
readM365, err := ac.M365Config()
|
||||||
|
require.NoError(t, err, "reading m365 config from account")
|
||||||
|
assert.Equal(t, readM365.TenantID, m365.TenantID)
|
||||||
|
assert.Equal(t, readM365.ClientID, os.Getenv(credentials.ClientID))
|
||||||
|
assert.Equal(t, readM365.ClientSecret, os.Getenv(credentials.ClientSecret))
|
||||||
}
|
}
|
||||||
|
|||||||
94
src/cli/config/storage.go
Normal file
94
src/cli/config/storage.go
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/alcionai/corso/cli/utils"
|
||||||
|
"github.com/alcionai/corso/pkg/credentials"
|
||||||
|
"github.com/alcionai/corso/pkg/storage"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
// prerequisite: readRepoConfig must have been run prior to this to populate the global viper values.
|
||||||
|
func s3ConfigsFromViper(vpr *viper.Viper) (storage.S3Config, error) {
|
||||||
|
var s3Config storage.S3Config
|
||||||
|
|
||||||
|
providerType := vpr.GetString(StorageProviderTypeKey)
|
||||||
|
if providerType != storage.ProviderS3.String() {
|
||||||
|
return s3Config, errors.New("unsupported storage provider: " + providerType)
|
||||||
|
}
|
||||||
|
|
||||||
|
s3Config.Bucket = vpr.GetString(BucketNameKey)
|
||||||
|
s3Config.Endpoint = vpr.GetString(EndpointKey)
|
||||||
|
s3Config.Prefix = vpr.GetString(PrefixKey)
|
||||||
|
return s3Config, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func s3Overrides(in map[string]string) map[string]string {
|
||||||
|
return map[string]string{
|
||||||
|
storage.Bucket: in[storage.Bucket],
|
||||||
|
storage.Endpoint: in[storage.Endpoint],
|
||||||
|
storage.Prefix: in[storage.Prefix],
|
||||||
|
StorageProviderTypeKey: in[StorageProviderTypeKey],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// configureStorage builds a complete storage configuration from a mix of
|
||||||
|
// viper properties and manual overrides.
|
||||||
|
func configureStorage(vpr *viper.Viper, readConfigFromViper bool, overrides map[string]string) (storage.Storage, error) {
|
||||||
|
var (
|
||||||
|
s3Cfg storage.S3Config
|
||||||
|
store storage.Storage
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
if readConfigFromViper {
|
||||||
|
if s3Cfg, err = s3ConfigsFromViper(vpr); err != nil {
|
||||||
|
return store, errors.Wrap(err, "reading s3 configs from corso config file")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := mustMatchConfig(vpr, s3Overrides(overrides)); err != nil {
|
||||||
|
return store, errors.Wrap(err, "verifying s3 configs in corso config file")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// compose the s3 storage config and credentials
|
||||||
|
aws := credentials.GetAWS(overrides)
|
||||||
|
if err := aws.Validate(); err != nil {
|
||||||
|
return store, errors.Wrap(err, "validating aws credentials")
|
||||||
|
}
|
||||||
|
|
||||||
|
s3Cfg = storage.S3Config{
|
||||||
|
AWS: aws,
|
||||||
|
Bucket: first(overrides[storage.Bucket], s3Cfg.Bucket),
|
||||||
|
Endpoint: first(overrides[storage.Endpoint], s3Cfg.Endpoint),
|
||||||
|
Prefix: first(overrides[storage.Prefix], s3Cfg.Prefix),
|
||||||
|
}
|
||||||
|
|
||||||
|
// compose the common config and credentials
|
||||||
|
corso := credentials.GetCorso()
|
||||||
|
if err := corso.Validate(); err != nil {
|
||||||
|
return store, errors.Wrap(err, "validating corso credentials")
|
||||||
|
}
|
||||||
|
cCfg := storage.CommonConfig{
|
||||||
|
Corso: corso,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure requried properties are present
|
||||||
|
if err := utils.RequireProps(map[string]string{
|
||||||
|
credentials.AWSAccessKeyID: aws.AccessKey,
|
||||||
|
storage.Bucket: s3Cfg.Bucket,
|
||||||
|
credentials.AWSSecretAccessKey: aws.SecretKey,
|
||||||
|
credentials.AWSSessionToken: aws.SessionToken,
|
||||||
|
credentials.CorsoPassword: corso.CorsoPassword,
|
||||||
|
}); err != nil {
|
||||||
|
return storage.Storage{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// build the storage
|
||||||
|
store, err = storage.NewStorage(storage.ProviderS3, s3Cfg, cCfg)
|
||||||
|
if err != nil {
|
||||||
|
return store, errors.Wrap(err, "configuring repository storage")
|
||||||
|
}
|
||||||
|
|
||||||
|
return store, nil
|
||||||
|
}
|
||||||
@ -9,6 +9,7 @@ import (
|
|||||||
|
|
||||||
"github.com/alcionai/corso/cli/config"
|
"github.com/alcionai/corso/cli/config"
|
||||||
"github.com/alcionai/corso/cli/utils"
|
"github.com/alcionai/corso/cli/utils"
|
||||||
|
"github.com/alcionai/corso/pkg/account"
|
||||||
"github.com/alcionai/corso/pkg/credentials"
|
"github.com/alcionai/corso/pkg/credentials"
|
||||||
"github.com/alcionai/corso/pkg/logger"
|
"github.com/alcionai/corso/pkg/logger"
|
||||||
"github.com/alcionai/corso/pkg/repository"
|
"github.com/alcionai/corso/pkg/repository"
|
||||||
@ -63,13 +64,7 @@ func initS3Cmd(cmd *cobra.Command, args []string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
overrides := map[string]string{
|
s, a, err := config.GetStorageAndAccount(false, s3Overrides())
|
||||||
credentials.AWSAccessKeyID: accessKey,
|
|
||||||
storage.Bucket: bucket,
|
|
||||||
storage.Endpoint: endpoint,
|
|
||||||
storage.Prefix: prefix,
|
|
||||||
}
|
|
||||||
s, a, err := config.GetStorageAndAccount(false, overrides)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -123,13 +118,7 @@ func connectS3Cmd(cmd *cobra.Command, args []string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
overrides := map[string]string{
|
s, a, err := config.GetStorageAndAccount(true, s3Overrides())
|
||||||
credentials.AWSAccessKeyID: accessKey,
|
|
||||||
storage.Bucket: bucket,
|
|
||||||
storage.Endpoint: endpoint,
|
|
||||||
storage.Prefix: prefix,
|
|
||||||
}
|
|
||||||
s, a, err := config.GetStorageAndAccount(true, overrides)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -163,3 +152,14 @@ func connectS3Cmd(cmd *cobra.Command, args []string) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func s3Overrides() map[string]string {
|
||||||
|
return map[string]string{
|
||||||
|
config.AccountProviderTypeKey: account.ProviderM365.String(),
|
||||||
|
config.StorageProviderTypeKey: storage.ProviderS3.String(),
|
||||||
|
credentials.AWSAccessKeyID: accessKey,
|
||||||
|
storage.Bucket: bucket,
|
||||||
|
storage.Endpoint: endpoint,
|
||||||
|
storage.Prefix: prefix,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
CorsoCITests = "CORSO_CI_TESTS"
|
CorsoCITests = "CORSO_CI_TESTS"
|
||||||
|
CorsoCLIConfigTests = "CORSO_CLI_CONFIG_TESTS"
|
||||||
CorsoGraphConnectorTests = "CORSO_GRAPH_CONNECTOR_TESTS"
|
CorsoGraphConnectorTests = "CORSO_GRAPH_CONNECTOR_TESTS"
|
||||||
CorsoKopiaWrapperTests = "CORSO_KOPIA_WRAPPER_TESTS"
|
CorsoKopiaWrapperTests = "CORSO_KOPIA_WRAPPER_TESTS"
|
||||||
CorsoRepositoryTests = "CORSO_REPOSITORY_TESTS"
|
CorsoRepositoryTests = "CORSO_REPOSITORY_TESTS"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user