Skip to content

Commit

Permalink
feat(cli): new storage config input from file when creating a repo (#…
Browse files Browse the repository at this point in the history
…2756)

* feat(cli): new storage config input from file when creating a repo

* feat(cli): allow token from stdin

* fix(cli): improve code coverage

---------

Co-authored-by: Shikhar Mall <small@kopia.io>
  • Loading branch information
Shrekster and Shikhar Mall committed Jul 27, 2023
1 parent 83b88d8 commit e1c44f7
Show file tree
Hide file tree
Showing 5 changed files with 113 additions and 14 deletions.
50 changes: 41 additions & 9 deletions cli/command_repository_connect_from_config.go
Expand Up @@ -2,6 +2,8 @@ package cli

import (
"context"
"io"
"os"

"github.com/alecthomas/kingpin/v2"
"github.com/pkg/errors"
Expand All @@ -13,33 +15,45 @@ import (
type storageFromConfigFlags struct {
connectFromConfigFile string
connectFromConfigToken string
connectFromTokenFile string
connectFromTokenStdin bool

sps StorageProviderServices
}

func (c *storageFromConfigFlags) Setup(sps StorageProviderServices, cmd *kingpin.CmdClause) {
cmd.Flag("file", "Path to the configuration file").StringVar(&c.connectFromConfigFile)
cmd.Flag("token", "Configuration token").StringVar(&c.connectFromConfigToken)
cmd.Flag("token-file", "Path to the configuration token file").StringVar(&c.connectFromTokenFile)
cmd.Flag("token-stdin", "Read configuration token from stdin").BoolVar(&c.connectFromTokenStdin)

c.sps = sps
}

func (c *storageFromConfigFlags) Connect(ctx context.Context, isCreate bool, formatVersion int) (blob.Storage, error) {
_ = formatVersion

if isCreate {
return nil, errors.New("not supported")
}

if c.connectFromConfigFile != "" {
if !isCreate && c.connectFromConfigFile != "" {
return c.connectToStorageFromConfigFile(ctx)
}

if c.connectFromConfigToken != "" {
return c.connectToStorageFromConfigToken(ctx)
return c.connectToStorageFromConfigToken(ctx, c.connectFromConfigToken)
}

if c.connectFromTokenFile != "" {
return c.connectToStorageFromStorageConfigFile(ctx)
}

if c.connectFromTokenStdin {
return c.connectToStorageFromStorageConfigStdin(ctx)
}

if isCreate {
return nil, errors.New("one of --token-file, --token-stdin or --token must be provided")
}

return nil, errors.New("either --file or --token must be provided")
return nil, errors.New("one of --file, --token-file, --token-stdin or --token must be provided")
}

func (c *storageFromConfigFlags) connectToStorageFromConfigFile(ctx context.Context) (blob.Storage, error) {
Expand All @@ -56,8 +70,8 @@ func (c *storageFromConfigFlags) connectToStorageFromConfigFile(ctx context.Cont
return blob.NewStorage(ctx, *cfg.Storage, false)
}

func (c *storageFromConfigFlags) connectToStorageFromConfigToken(ctx context.Context) (blob.Storage, error) {
ci, pass, err := repo.DecodeToken(c.connectFromConfigToken)
func (c *storageFromConfigFlags) connectToStorageFromConfigToken(ctx context.Context, token string) (blob.Storage, error) {
ci, pass, err := repo.DecodeToken(token)
if err != nil {
return nil, errors.Wrap(err, "invalid token")
}
Expand All @@ -69,3 +83,21 @@ func (c *storageFromConfigFlags) connectToStorageFromConfigToken(ctx context.Con
//nolint:wrapcheck
return blob.NewStorage(ctx, ci, false)
}

func (c *storageFromConfigFlags) connectToStorageFromStorageConfigFile(ctx context.Context) (blob.Storage, error) {
tokenData, err := os.ReadFile(c.connectFromTokenFile)
if err != nil {
return nil, errors.Wrap(err, "unable to open token file")
}

return c.connectToStorageFromConfigToken(ctx, string(tokenData))
}

func (c *storageFromConfigFlags) connectToStorageFromStorageConfigStdin(ctx context.Context) (blob.Storage, error) {
tokenData, err := io.ReadAll(c.sps.stdin())
if err != nil {
return nil, errors.Wrap(err, "unable to read token from stdin")
}

return c.connectToStorageFromConfigToken(ctx, string(tokenData))
}
4 changes: 0 additions & 4 deletions cli/command_repository_create.go
Expand Up @@ -57,10 +57,6 @@ func (c *commandRepositoryCreate) setup(svc advancedAppServices, parent commandP
c.out.setup(svc)

for _, prov := range svc.storageProviders() {
if prov.Name == "from-config" {
continue
}

// Set up 'create' subcommand
f := prov.NewFlags()
cc := cmd.Command(prov.Name, "Create repository in "+prov.Description)
Expand Down
63 changes: 63 additions & 0 deletions cli/command_repository_create_test.go
@@ -0,0 +1,63 @@
package cli_test

import (
"os"
"path"
"strings"
"testing"

"github.com/kopia/kopia/repo"
"github.com/kopia/kopia/repo/blob"
"github.com/kopia/kopia/repo/blob/filesystem"
"github.com/kopia/kopia/tests/testenv"

"github.com/stretchr/testify/require"
)

func TestRepositoryCreateWithConfigFile(t *testing.T) {
env := testenv.NewCLITest(t, nil, testenv.NewInProcRunner(t))

_, stderr := env.RunAndExpectFailure(t, "repo", "create", "from-config", "--file", path.Join(env.ConfigDir, "does_not_exist.config"))
require.Contains(t, stderr, "can't connect to storage: one of --token-file, --token-stdin or --token must be provided")

_, stderr = env.RunAndExpectFailure(t, "repo", "connect", "from-config")
require.Contains(t, stderr, "can't connect to storage: one of --file, --token-file, --token-stdin or --token must be provided")

_, stderr = env.RunAndExpectFailure(t, "repo", "create", "from-config", "--token", "bad-token")
require.Contains(t, stderr, "can't connect to storage: invalid token: unable to decode token")

storageCfgFName := path.Join(env.ConfigDir, "storage-config.json")
ci := blob.ConnectionInfo{
Type: "filesystem",
Config: filesystem.Options{Path: env.RepoDir},
}
token, err := repo.EncodeToken("12345678", ci)
require.Nil(t, err)

// expect failure before writing to file
_, stderr = env.RunAndExpectFailure(t, "repo", "create", "from-config", "--token-file", storageCfgFName)
require.Contains(t, strings.Join(stderr, "\n"), "can't connect to storage: unable to open token file")

require.Nil(t, os.WriteFile(storageCfgFName, []byte(token), 0o600))

defer os.Remove(storageCfgFName) //nolint:errcheck,gosec

env.RunAndExpectSuccess(t, "repo", "create", "from-config", "--token-file", storageCfgFName)
}

func TestRepositoryCreateWithConfigFromStdin(t *testing.T) {
runner := testenv.NewInProcRunner(t)
env := testenv.NewCLITest(t, nil, runner)

ci := blob.ConnectionInfo{
Type: "filesystem",
Config: filesystem.Options{Path: env.RepoDir},
}
token, err := repo.EncodeToken("12345678", ci)
require.Nil(t, err)

// set stdin
runner.SetNextStdin(strings.NewReader(token))

env.RunAndExpectSuccess(t, "repo", "create", "from-config", "--token-stdin")
}
2 changes: 2 additions & 0 deletions cli/storage_providers.go
Expand Up @@ -2,6 +2,7 @@ package cli

import (
"context"
"io"

"github.com/alecthomas/kingpin/v2"

Expand All @@ -15,6 +16,7 @@ type StorageProviderServices interface {
EnvName(s string) string
setPasswordFromToken(pwd string)
storageProviders() []StorageProvider
stdin() io.Reader
}

// StorageFlags is implemented by cli storage providers which need to support a
Expand Down
8 changes: 7 additions & 1 deletion repo/token.go
Expand Up @@ -18,9 +18,15 @@ type tokenInfo struct {
// Token returns an opaque token that contains repository connection information
// and optionally the provided password.
func (r *directRepository) Token(password string) (string, error) {
return EncodeToken(password, r.blobs.ConnectionInfo())
}

// EncodeToken returns an opaque token that contains the given connection information
// and optionally the provided password.
func EncodeToken(password string, ci blob.ConnectionInfo) (string, error) {
ti := &tokenInfo{
Version: "1",
Storage: r.blobs.ConnectionInfo(),
Storage: ci,
Password: password,
}

Expand Down

0 comments on commit e1c44f7

Please sign in to comment.