Skip to content

Commit

Permalink
Add regex key search, interactive push and backup
Browse files Browse the repository at this point in the history
 - Listing keys in push is now possible using a regular expression;
 - Add interactive mode for updating configuration;
 - Add backup creation prior to import
 - Template validation can now be skipped for restoring backups
  • Loading branch information
Ivaylo Marinkov committed Mar 20, 2017
1 parent 4c686d3 commit b7bf207
Show file tree
Hide file tree
Showing 14 changed files with 233 additions and 45 deletions.
16 changes: 12 additions & 4 deletions cmd/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ func init() {
}

func buildRun(tmplF string, srcs []map[string]interface{}) error {
out, err := buildConfig(tmplF, srcs)
out, err := buildConfig(tmplF, true, srcs)
if err != nil {
return err
}
Expand All @@ -45,7 +45,7 @@ func buildRun(tmplF string, srcs []map[string]interface{}) error {
return nil
}

func buildConfig(tmplF string, srcs []map[string]interface{}) ([]byte, error) {
func buildConfig(tmplF string, validate bool, srcs []map[string]interface{}) ([]byte, error) {
tmpl, err := os.Open(tmplF)
if err != nil {
return nil, err
Expand All @@ -56,10 +56,18 @@ func buildConfig(tmplF string, srcs []map[string]interface{}) ([]byte, error) {
return nil, err
}

cfg, err := casper.BuildConfig{
var cfg []byte
config := casper.BuildConfig{
Tmlp: tmpl,
Source: source,
}.Build()
}

if validate {
cfg, err = config.Build()
} else {
cfg, err = config.BuildNoValidation()
}

if err != nil {
return nil, err
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ func TestBuildConfig(t *testing.T) {
}

// Build
out, err := buildConfig(tmlpFile.Name(), tc.srcs)
out, err := buildConfig(tmlpFile.Name(), true, tc.srcs)
if tc.ok != (err == nil) {
if err != nil {
t.Fatal("Failed with", err)
Expand Down
4 changes: 3 additions & 1 deletion cmd/consulstorage.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,9 @@ func getChanges(pairs api.KVPairs, config []byte, format, key string) (changes,

kvChanges := diff.KVChanges{}
for _, c := range consulChanges {
if key != "" && key != c.Key {
isRegex, keyRegex := getRegex(key)

if key != "" && (key != c.Key && (isRegex && !keyRegex.MatchString(c.Key))) {
continue
}

Expand Down
3 changes: 2 additions & 1 deletion cmd/diff.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ func init() {
}

func diffRun(tmpl, format, key string, sourcesList []map[string]interface{}, storage string, config map[string]interface{}, pretty bool) error {
out, err := buildConfig(tmpl, sourcesList)
out, err := buildConfig(tmpl, true, sourcesList)
if err != nil {
return err
}
Expand Down Expand Up @@ -80,5 +80,6 @@ func strChanges(cs changes, key string, s storage, pretty bool) string {
}
return fmt.Sprintf("No changes")
}

return s.Diff(cs, pretty)
}
14 changes: 8 additions & 6 deletions cmd/fetch.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ var fetchCmd = &cobra.Command{
config := viper.GetStringMap("storage.config")
format := viper.GetString("format")

return fetchRun(storage, config, format)
cfg, err := fetchRun(storage, config, format)
fmt.Println(*cfg)
return err
},
}

Expand All @@ -27,16 +29,16 @@ func init() {
RootCmd.AddCommand(fetchCmd)
}

func fetchRun(storage string, config map[string]interface{}, format string) error {
func fetchRun(storage string, config map[string]interface{}, format string) (fetched *string, err error) {
s, err := getStorage(storage, config)
if err != nil {
return err
return nil, err
}

cfg, err := s.String(format)
if err != nil {
return err
return nil, err
}
fmt.Println(cfg)
return nil

return &cfg, nil
}
4 changes: 3 additions & 1 deletion cmd/fetch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,12 @@ func TestFetchRun(t *testing.T) {
}

out := caspertest.GetStdout(t, func() {
err = fetchRun("file", config, "jsonraw")
text, err := fetchRun("file", config, "jsonraw")
if err != nil {
t.Fatal(err)
}

fmt.Println(*text)
})

exp := tc.storage + "\n"
Expand Down
10 changes: 10 additions & 0 deletions cmd/filestorage.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,3 +105,13 @@ func (c fileChanges) Len() int {
}
return 1
}

func (c fileChanges) Refine(func(interface{}) bool) interface{} {
return c
}

// SupportsInteractive checks if interactive key
// checking is supported. For fileChanges it is not.
func (c fileChanges) SupportsInteractive() bool {
return false
}
148 changes: 135 additions & 13 deletions cmd/push.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@ package cmd
import (
"bufio"
"fmt"
"io/ioutil"
"os"
"strings"
"time"

"github.com/miracl/casper/lib/diff"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
Expand All @@ -23,11 +26,26 @@ var pushCmd = &cobra.Command{
storage := viper.GetString("storage.type")
config := viper.GetStringMap("storage.config")

noValidation, err := cmd.Flags().GetBool("no-validation")
if err != nil {
return err
}

key, err := cmd.Flags().GetString("key")
if err != nil {
return err
}

interactive, err := cmd.Flags().GetBool("interactive")
if err != nil {
return err
}

backup, err := cmd.Flags().GetBool("backup")
if err != nil {
return err
}

sourcesList, ok := getSliceStringMapIface(viper.Get("sources"))
if !ok {
return errSourceFormat
Expand All @@ -43,50 +61,154 @@ var pushCmd = &cobra.Command{
return err
}

return pushRun(template, format, key, sourcesList, storage, config, force, !plain)
pushConf := &pushConfig{
template, noValidation, format, storage, key,
interactive, backup, &sourcesList, force, !plain,
}

return pushRun(pushConf, config)
},
}

type pushConfig struct {
template string
noValidation bool
format string
storage string
key string
interactive bool
backup bool
sourcesList *[]map[string]interface{}
force bool
pretty bool
}

func init() {
pushCmd.Flags().StringP("template", "t", "", "template file")
pushCmd.Flags().BoolP("no-validation", "n", false, "don't validate input template. Meant for importing from backup")
pushCmd.Flags().StringP("format", "f", "", "format of the template file")
pushCmd.Flags().StringP("key", "k", "", "specific key to push")
pushCmd.Flags().StringP("key", "k", "", "specific key to push, supports a regular expression to match multiple")
pushCmd.Flags().BoolP("interactive", "i", false, "interactive selection for which keys to push")
pushCmd.Flags().Bool("force", false, "push the changes without asking")
pushCmd.Flags().BoolP("plain", "p", false, "disable colorful output")
pushCmd.Flags().BoolP("backup", "b", false, "back up current configuration to file before applying changes")
RootCmd.AddCommand(pushCmd)
}

func pushRun(tmpl, format, key string, sourcesList []map[string]interface{}, storage string, config map[string]interface{}, force, pretty bool) error {
out, err := buildConfig(tmpl, sourcesList)
func pushRun(cmdConf *pushConfig, storageConf map[string]interface{}) error {
out, err := buildConfig(cmdConf.template, !cmdConf.noValidation, *cmdConf.sourcesList)
if err != nil {
return err
}

s, err := getStorage(storage, config)
s, err := getStorage(cmdConf.storage, storageConf)
if err != nil {
return err
}

changes, err := s.GetChanges(out, format, key)
changeset, err := s.GetChanges(out, cmdConf.format, cmdConf.key)
if err != nil {
return err
}

fmt.Println(strChanges(changes, key, s, pretty))
if changes.Len() == 0 {
if changeset.Len() == 0 {
fmt.Println("No changes")
return nil
}

if !force {
if cmdConf.interactive && changeset.SupportsInteractive() {
changeset = filterChangesInteractive(changeset, cmdConf.pretty)
if changeset.Len() == 0 {
fmt.Println("No changes have been selected. Nothing to do.")
return nil
}
}

fmt.Println("\nThe following changes will be applied:")
fmt.Println(strChanges(changeset, cmdConf.key, s, cmdConf.pretty))

if !cmdConf.force {
// prompt for agreement
fmt.Print("Continue[y/N]: ")
input, _ := bufio.NewReader(os.Stdin).ReadString('\n')
if strings.ToLower(strings.TrimRight(input, "\r\n")) != "y" {
if !prompt("Continue [y/N]: ") {
fmt.Println("Canceled")
return nil
}
}

// Save a back-up copy of the current configuration
if cmdConf.backup {
if err := backup(cmdConf, storageConf); err != nil {
return nil
}
}

fmt.Println("Applying changes...")
return s.Push(changes)
if err = s.Push(changeset); err != nil {
return err
}

fmt.Println("Done.")
return nil
}

// filterChangesInteractive prompts the user key-by-key
// if change entries within a set of changes should be applied.
// It then returns the list of changes the user has chosen.
func filterChangesInteractive(changeset changes, pretty bool) changes {
fmt.Print(
"Please select the configuration records you would like to apply.\n" +
"You will be able to confirm the list before applying it.\n\n")

return changeset.Refine(func(a interface{}) bool {
b := a.(diff.KVChange)
var changesDisplay string

if pretty {
changesDisplay = b.Pretty()
} else {
changesDisplay = b.String()
}

return prompt(fmt.Sprintf("\t%s [y/N]: ", changesDisplay))
}).(changes)
}

// prompt prints out question formatted with params and
// returns true if the user enters "Y" using their keyboard.
func prompt(question string, params ...interface{}) bool {
fmt.Printf(question, params...)
input, _ := bufio.NewReader(os.Stdin).ReadString('\n')

return strings.ToLower(strings.TrimRight(input, "\r\n")) == "y"
}

// backup creates a text file containing a service's current configuration.
func backup(cmdConf *pushConfig, storageConf map[string]interface{}) error {
currentConf, err := fetchRun(cmdConf.storage, storageConf, cmdConf.format)
if err != nil {
return err
}

filename, err := saveBackup(currentConf)
if err != nil {
return err
}

fmt.Printf("Backup has been saved as %s\n", filename)
return nil
}

// generateBackupFilename generates a filename for a backup file
// where service configuration is to be stored.
// It follows a <UNIX time>_backup.txt format.
func generateBackupFilename() string {
filenameTemplate := "%d_backup.txt"
return fmt.Sprintf(filenameTemplate, int32(time.Now().Unix()))
}

// saveBackup generates a text file containing the value of
// its content parameter.
func saveBackup(content *string) (filename string, err error) {
filename = generateBackupFilename()
return filename, ioutil.WriteFile(filename, []byte(*content), 0644)
}
Loading

0 comments on commit b7bf207

Please sign in to comment.