Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Configuration system for knoxite #120

Merged
merged 15 commits into from
Aug 9, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions cmd/knoxite/clone.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ var (
if len(args) < 2 {
return fmt.Errorf("clone needs to know which files and/or directories to work on")
}
cloneOpts = configureStoreOpts(cmd, cloneOpts)

return executeClone(args[0], args[1:], cloneOpts)
},
}
Expand Down
166 changes: 166 additions & 0 deletions cmd/knoxite/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
/*
* knoxite
* Copyright (c) 2020, Nicolas Martin <penguwin@penguwin.eu>
*
* For license see LICENSE
*/
package main

import (
"encoding/json"
"fmt"
"log"
"strconv"
"strings"

"github.com/knoxite/knoxite/config"
"github.com/muesli/gotable"
"github.com/spf13/cobra"
)

var (
configCmd = &cobra.Command{
Use: "config",
Short: "manage configuration",
Long: `The config command manages the knoxite configuration`,
}
configInitCmd = &cobra.Command{
Use: "init",
Short: "initialize a new configuration",
Long: "The init command initializes a new configuration file",
RunE: func(cmd *cobra.Command, args []string) error {
return executeConfigInit()
},
}
configAliasCmd = &cobra.Command{
Use: "alias <alias>",
Short: "Set an alias for the storage backend url to a repository",
Long: `The set command adds an alias for the storage backend url to a repository`,
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) != 1 {
return fmt.Errorf("alias needs an ALIAS to set")
}
return executeConfigAlias(args[0])
},
}
configSetCmd = &cobra.Command{
Use: "set <option> <value>",
Short: "set configuration values for an alias",
Long: "The set command lets you set configuration values for an alias",
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
return fmt.Errorf("set needs to know which option to set")
}
if len(args) < 2 {
return fmt.Errorf("set needs to know which value to set")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This may be the wrong position to critizise this here, since we're doing this in every command - but shouldn't we just print the help text of this command reather than a specific help text? I often had the situation where i typed just the second argument of a command requiring two arguments and was confused when reading that the second argument was missing

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, let's keep this discussion in a separate issue / ticket.

}
return executeConfigSet(args[0], args[1])
},
}
configInfoCmd = &cobra.Command{
Use: "info",
Short: "display information about the configuration file on stdout",
Long: `The info command displays information about the configuration file on stdout`,
RunE: func(cmd *cobra.Command, args []string) error {
return executeConfigInfo()
},
}
configCatCmd = &cobra.Command{
Use: "cat",
Short: "display the configuration file on stdout",
Long: `The cat command displays the configuration file on stdout`,
RunE: func(cmd *cobra.Command, args []string) error {
return executeConfigCat()
},
}
)

func init() {
configCmd.AddCommand(configInitCmd)
configCmd.AddCommand(configSetCmd)
configCmd.AddCommand(configAliasCmd)
configCmd.AddCommand(configInfoCmd)
configCmd.AddCommand(configCatCmd)
RootCmd.AddCommand(configCmd)
}

func executeConfigInit() error {
log.Printf("Writing configuration file to: %s\n", cfg.URL().Path)
return cfg.Save()
}

func executeConfigAlias(alias string) error {
// At first check if the configuration file already exists
cfg.Repositories[alias] = config.RepoConfig{
Url: globalOpts.Repo,
// Compression: utils.CompressionText(knoxite.CompressionNone),
// Tolerance: 0,
// Encryption: utils.EncryptionText(knoxite.EncryptionAES),
}

return cfg.Save()
}

func executeConfigSet(option string, value string) error {
// This probably wont scale for more complex configuration options but works
// fine for now.
parts := strings.Split(option, ".")
if len(parts) != 2 {
return fmt.Errorf("config set needs to work on an alias and a option like this: alias.option")
}

// The first part should be the repos alias
repo, ok := cfg.Repositories[strings.ToLower(parts[0])]
if !ok {
return fmt.Errorf("No alias with name %s found", parts[0])
}

opt := strings.ToLower(parts[1])
switch opt {
case "url":
repo.Url = value
case "compression":
repo.Compression = value
case "encryption":
repo.Encryption = value
case "tolerance":
tol, err := strconv.Atoi(value)
if err != nil {
return fmt.Errorf("Failed to convert %s to uint for the fault tolerance option: %v", opt, err)
}
repo.Tolerance = uint(tol)
default:
return fmt.Errorf("Unknown configuration option: %s", opt)
}
cfg.Repositories[strings.ToLower(parts[0])] = repo

return cfg.Save()
}

func executeConfigInfo() error {
tab := gotable.NewTable(
[]string{"Alias", "Storage URL", "Compression", "Tolerance", "Encryption"},
[]int64{-15, -35, -15, -15, 15},
"No repository configurations found.")

for alias, repo := range cfg.Repositories {
tab.AppendRow([]interface{}{
alias,
repo.Url,
repo.Compression,
fmt.Sprintf("%v", repo.Tolerance),
repo.Encryption,
})
}
return tab.Print()
}

func executeConfigCat() error {
json, err := json.MarshalIndent(cfg, "", " ")
if err != nil {
return err
}

fmt.Printf("%s\n", json)
return nil
}
33 changes: 31 additions & 2 deletions cmd/knoxite/main.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/*
* knoxite
* Copyright (c) 2016-2020, Christian Muehlhaeuser <muesli@gmail.com>
* Copyright (c) 2020, Nicolas Martin <penguwin@penguwin.eu>
*
* For license see LICENSE
*/
Expand All @@ -17,6 +18,7 @@ import (
shutdown "github.com/klauspost/shutdown2"
"github.com/spf13/cobra"

"github.com/knoxite/knoxite/config"
_ "github.com/knoxite/knoxite/storage/azure"
_ "github.com/knoxite/knoxite/storage/backblaze"
_ "github.com/knoxite/knoxite/storage/dropbox"
Expand All @@ -31,15 +33,17 @@ import (

// GlobalOptions holds all those options that can be set for every command
type GlobalOptions struct {
Repo string
Password string
Repo string
Password string
ConfigURL string
}

var (
Version = ""
CommitSHA = ""

globalOpts = GlobalOptions{}
cfg = &config.Config{}

// RootCmd is the core command used for cli-arg parsing
RootCmd = &cobra.Command{
Expand All @@ -60,6 +64,7 @@ func main() {

RootCmd.PersistentFlags().StringVarP(&globalOpts.Repo, "repo", "r", "", "Repository directory to backup to/restore from (default: current working dir)")
RootCmd.PersistentFlags().StringVarP(&globalOpts.Password, "password", "p", "", "Password to use for data encryption")
RootCmd.PersistentFlags().StringVarP(&globalOpts.ConfigURL, "configURL", "C", config.DefaultPath(), "Path to the configuration file")

globalOpts.Repo = os.Getenv("KNOXITE_REPOSITORY")
globalOpts.Password = os.Getenv("KNOXITE_PASSWORD")
Expand All @@ -71,6 +76,7 @@ func main() {
}

func init() {
cobra.OnInitialize(initConfig)
if CommitSHA != "" {
vt := RootCmd.VersionTemplate()
RootCmd.SetVersionTemplate(vt[:len(vt)-1] + " (" + CommitSHA + ")\n")
Expand All @@ -81,3 +87,26 @@ func init() {

RootCmd.Version = Version
}

// initConfig initializes the configuration for knoxite.
// It'll use the the default config url unless specified otherwise via the
// ConfigURL flag.
func initConfig() {
var err error
cfg, err = config.New(globalOpts.ConfigURL)
if err != nil {
log.Fatalf("error reading the config file: %v\n", err)
return
}
if err = cfg.Load(); err != nil {
log.Fatalf("error loading the config file: %v\n", err)
return
}

// There can occur a panic due to an entry assigment in nil map when theres
// no map initialized to store the RepoConfigs. This will prevent this from
// happening:
if cfg.Repositories == nil {
cfg.Repositories = make(map[string]config.RepoConfig)
}
}
71 changes: 8 additions & 63 deletions cmd/knoxite/repository.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/*
* knoxite
* Copyright (c) 2016-2020, Christian Muehlhaeuser <muesli@gmail.com>
* Copyright (c) 2020, Nicolas Martin <penguwin@penguwin.eu>
*
* For license see LICENSE
*/
Expand All @@ -9,26 +10,17 @@ package main

import (
"encoding/json"
"errors"
"fmt"
"io"
"os"
"strings"
"syscall"

shutdown "github.com/klauspost/shutdown2"
"github.com/muesli/crunchy"
"github.com/muesli/gotable"
"github.com/spf13/cobra"
"golang.org/x/crypto/ssh/terminal"

"github.com/knoxite/knoxite"
"github.com/knoxite/knoxite/utils"
)

// Error declarations
var (
ErrPasswordMismatch = errors.New("Passwords did not match")

repoCmd = &cobra.Command{
Use: "repo",
Short: "manage repository",
Expand Down Expand Up @@ -121,7 +113,7 @@ func executeRepoChangePassword() error {
return err
}

password, err := readPasswordTwice("Enter new password:", "Confirm password:")
password, err := utils.ReadPasswordTwice("Enter new password:", "Confirm password:")
penguwin marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return err
}
Expand Down Expand Up @@ -230,73 +222,26 @@ func executeRepoInfo() error {
func openRepository(path, password string) (knoxite.Repository, error) {
if password == "" {
var err error
password, err = readPassword("Enter password:")
password, err = utils.ReadPassword("Enter password:")
if err != nil {
return knoxite.Repository{}, err
}
}

if rep, ok := cfg.Repositories[path]; ok {
return knoxite.OpenRepository(rep.Url, password)
}
return knoxite.OpenRepository(path, password)
}

func newRepository(path, password string) (knoxite.Repository, error) {
if password == "" {
var err error
password, err = readPasswordTwice("Enter a password to encrypt this repository with:", "Confirm password:")
password, err = utils.ReadPasswordTwice("Enter a password to encrypt this repository with:", "Confirm password:")
if err != nil {
return knoxite.Repository{}, err
}
}

return knoxite.NewRepository(path, password)
}

func readPassword(prompt string) (string, error) {
var tty io.WriteCloser
tty, err := os.OpenFile("/dev/tty", os.O_WRONLY, 0)
if err != nil {
tty = os.Stdout
} else {
defer tty.Close()
}

fmt.Fprint(tty, prompt+" ")
buf, err := terminal.ReadPassword(int(syscall.Stdin))
fmt.Fprintln(tty)

return string(buf), err
}

func readPasswordTwice(prompt, promptConfirm string) (string, error) {
pw, err := readPassword(prompt)
if err != nil {
return pw, err
}

crunchErr := crunchy.NewValidator().Check(pw)
if crunchErr != nil {
fmt.Printf("Password is considered unsafe: %v\n", crunchErr)
fmt.Printf("Are you sure you want to use this password (y/N)?: ")
var buf string
_, err = fmt.Scan(&buf)
if err != nil {
return pw, err
}

buf = strings.TrimSpace(buf)
buf = strings.ToLower(buf)
if buf != "y" {
return pw, crunchErr
}
}

pwconfirm, err := readPassword(promptConfirm)
if err != nil {
return pw, err
}
if pw != pwconfirm {
return pw, ErrPasswordMismatch
}

return pw, nil
}