diff --git a/.gitignore b/.gitignore index 52f9d4d..f79aad7 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,7 @@ _testmain.go # Documentation files docs/build/ + + +# Intellij +.idea diff --git a/.goxc.json b/.goxc.json index da1f2c2..3422908 100644 --- a/.goxc.json +++ b/.goxc.json @@ -5,6 +5,6 @@ "Arch": "386 amd64 arm", "Os": "linux darwin", "BuildConstraints": "linux darwin", - "PackageVersion": "0.2.7", + "PackageVersion": "0.3.1", "ConfigVersion": "0.9" } diff --git a/AUTHORS.md b/AUTHORS.md index bf6a0b5..72839f5 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -6,6 +6,8 @@ * [Mthamed](https://github.com/mtahmed) * [Fernando Alvarez](https://github.com/fern4lvarez) +* [Chris Aumann](https://github.com/chr4) +* [Benjamin Reitzammer](https://github.com/nureineide) ## Open-source contributions import diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index 347cf03..5aa1cac 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -1,6 +1,9 @@ { "ImportPath": "github.com/oleiade/trousseau", - "GoVersion": "go1.2.1", + "GoVersion": "go1.3", + "Packages": [ + "./..." + ], "Deps": [ { "ImportPath": "code.google.com/p/go.crypto/cast5", @@ -37,8 +40,8 @@ }, { "ImportPath": "github.com/codegangsta/cli", - "Comment": "1.0.0-39-g8f0575f", - "Rev": "8f0575f520013467eb175a5dd30158ed816eeda9" + "Comment": "1.2.0-24-g7381bc4", + "Rev": "7381bc4e62942763475703c7edd405f1e42adf4f" }, { "ImportPath": "github.com/crowdmob/goamz/aws", @@ -69,6 +72,14 @@ "Comment": "0.1.2-2-g632977f", "Rev": "632977f98cd34d217c4b57d0840ec188b3d3dcaf" }, + { + "ImportPath": "github.com/oleiade/tempura", + "Rev": "1e4f5790d5066b19bd755d71402bb0b41f1e5ff9" + }, + { + "ImportPath": "github.com/stretchr/testify/assert", + "Rev": "5cc789b89e1b0289394556166a1fccd55c93d868" + }, { "ImportPath": "github.com/tmc/keyring", "Rev": "c97358d3b0f2df246afedabacbad24fad4e3246e" diff --git a/Godeps/_workspace/src/github.com/Sirupsen/logrus/.travis.yml b/Godeps/_workspace/src/github.com/Sirupsen/logrus/.travis.yml deleted file mode 100644 index 2efbc54..0000000 --- a/Godeps/_workspace/src/github.com/Sirupsen/logrus/.travis.yml +++ /dev/null @@ -1,7 +0,0 @@ -language: go -go: - - 1.1 - - 1.2 - - tip -before_script: - - go get github.com/stretchr/testify diff --git a/Godeps/_workspace/src/github.com/Sirupsen/logrus/README.md b/Godeps/_workspace/src/github.com/Sirupsen/logrus/README.md deleted file mode 100644 index a58bc9d..0000000 --- a/Godeps/_workspace/src/github.com/Sirupsen/logrus/README.md +++ /dev/null @@ -1,246 +0,0 @@ -# Logrus :walrus: [![Build Status](https://travis-ci.org/Sirupsen/logrus.svg?branch=master)](https://travis-ci.org/Sirupsen/logrus) - -Logrus is a structured logger for Go (golang), completely API compatible with -the standard library logger. [Godoc][godoc]. - -Nicely color-coded in development (when a TTY is attached, otherwise just -plain text): - -![Colored](http://i.imgur.com/PY7qMwd.png) - -With `log.Formatter = new(logrus.JSONFormatter)`, for easy parsing by logstash -or Splunk: - -```json -{"animal":"walrus","level":"info","msg":"A group of walrus emerges from the -ocean","size":10,"time":"2014-03-10 19:57:38.562264131 -0400 EDT"} - -{"level":"warning","msg":"The group's number increased tremendously!", -"number":122,"omg":true,"time":"2014-03-10 19:57:38.562471297 -0400 EDT"} - -{"animal":"walrus","level":"info","msg":"A giant walrus appears!", -"size":10,"time":"2014-03-10 19:57:38.562500591 -0400 EDT"} - -{"animal":"walrus","level":"info","msg":"Tremendously sized cow enters the ocean.", -"size":9,"time":"2014-03-10 19:57:38.562527896 -0400 EDT"} - -{"level":"fatal","msg":"The ice breaks!","number":100,"omg":true, -"time":"2014-03-10 19:57:38.562543128 -0400 EDT"} -``` - -With the default `log.Formatter = new(logrus.TextFormatter)` when a TTY is not -attached: - -```text -time='2014-03-14 13:00:31.751756799 -0400 EDT' level='info' msg='A group of walrus emerges from the ocean' animal='walrus' size=10 -time='2014-03-14 13:00:31.751994265 -0400 EDT' level='warning' msg='The group's number increased tremendously!' omg=true number=122 -time='2014-03-14 13:00:31.752018319 -0400 EDT' level='info' msg='A giant walrus appears!' animal='walrus' size=10 -time='2014-03-14 13:00:31.752034139 -0400 EDT' level='info' msg='Tremendously sized cow enters the ocean.' animal='walrus' size=9 -time='2014-03-14 13:00:31.752048504 -0400 EDT' level='fatal' msg='The ice breaks!' omg=true number=100 -``` - -#### Example - -Note again that Logrus is API compatible with the standardlib logger, so if you -remove the `log` import and create a global `log` variable as below it will just -work. - -```go -package main - -import ( - "github.com/Sirupsen/logrus" -) - -var log = logrus.New() - -func init() { - log.Formatter = new(logrus.JSONFormatter) - log.Formatter = new(logrus.TextFormatter) // default -} - -func main() { - log.WithFields(logrus.Fields{ - "animal": "walrus", - "size": 10, - }).Info("A group of walrus emerges from the ocean") - - log.WithFields(logrus.Fields{ - "omg": true, - "number": 122, - }).Warn("The group's number increased tremendously!") - - log.WithFields(logrus.Fields{ - "omg": true, - "number": 100, - }).Fatal("The ice breaks!") -} -``` - -#### Fields - -Logrus encourages careful, structured logging. It encourages the use of logging -fields instead of long, unparseable error messages. For example, instead of: -`log.Fatalf("Failed to send event %s to topic %s with key %d")`, you should log -the much more discoverable: - -```go -log = logrus.New() - -log.WithFields(logrus.Fields{ - "event": event, - "topic": topic, - "key": key -}).Fatal("Failed to send event") -``` - -We've found this API forces you to think about logging in a way that produces -much more useful logging messages. We've been in countless situations where just -a single added field to a log statement that was already there would've saved us -hours. The `WithFields` call is optional. - -In general, with Logrus using any of the `printf`-family functions should be -seen as a hint you want to add a field, however, you can still use the -`printf`-family functions with Logrus. - -#### Hooks - -You can add hooks for logging levels. For example to send errors to an exception -tracking service on `Error`, `Fatal` and `Panic` or info to StatsD. - -```go -log = logrus.New() -log.Hooks.Add(new(AirbrakeHook)) - -type AirbrakeHook struct{} - -// `Fire()` takes the entry that the hook is fired for. `entry.Data[]` contains -// the fields for the entry. See the Fields section of the README. -func (hook *AirbrakeHook) Fire(entry *logrus.Entry) error { - err := airbrake.Notify(entry.Data["error"].(error)) - if err != nil { - log.WithFields(logrus.Fields{ - "source": "airbrake", - "endpoint": airbrake.Endpoint, - }).Info("Failed to send error to Airbrake") - } - - return nil -} - -// `Levels()` returns a slice of `Levels` the hook is fired for. -func (hook *AirbrakeHook) Levels() []logrus.Level { - return []logrus.Level{ - logrus.Error, - logrus.Fatal, - logrus.Panic, - } -} -``` - -#### Level logging - -Logrus has six logging levels: Debug, Info, Warning, Error, Fatal and Panic. - -```go -log.Debug("Useful debugging information.") -log.Info("Something noteworthy happened!") -log.Warn("You should probably take a look at this.") -log.Error("Something failed but I'm not quitting.") -// Calls os.Exit(1) after logging -log.Fatal("Bye.") -// Calls panic() after logging -log.Panic("I'm bailing.") -``` - -You can set the logging level on a `Logger`, then it will only log entries with -that severity or anything above it: - -```go -// Will log anything that is info or above (warn, error, fatal, panic). Default. -log.Level = logrus.Info -``` - -It may be useful to set `log.Level = logrus.Debug` in a debug or verbose -environment if your application has that. - -#### Entries - -Besides the fields added with `WithField` or `WithFields` some fields are -automatically added to all logging events: - -1. `time`. The timestamp when the entry was created. -2. `msg`. The logging message passed to `{Info,Warn,Error,Fatal,Panic}` after - the `AddFields` call. E.g. `Failed to send event.` -3. `level`. The logging level. E.g. `info`. - -#### Environments - -Logrus has no notion of environment. - -If you wish for hooks and formatters to only be used in specific environments, -you should handle that yourself. For example, if your application has a global -variable `Environment`, which is a string representation of the environment you -could do: - -```go -init() { - // do something here to set environment depending on an environment variable - // or command-line flag - log := logrus.New() - - if Environment == "production" { - log.Formatter = new(logrus.JSONFormatter) - } else { - // The TextFormatter is default, you don't actually have to do this. - log.Formatter = new(logrus.TextFormatter) - } -} -``` - -This configuration is how `logrus` was intended to be used, but JSON in -production is mostly only useful if you do log aggregation with tools like -Splunk or Logstash. - -#### Formatters - -The built-in logging formatters are: - -* `logrus.TextFormatter`. Logs the event in colors if stdout is a tty, otherwise - without colors. - * *Note:* to force colored output when there is no TTY, set the `ForceColors` - field to `true`. -* `logrus.JSONFormatter`. Logs fields as JSON. - -Third party logging formatters: - -* [`zalgo`](https://github.com/aybabtme/logzalgo): invoking the P͉̫o̳̼̊w̖͈̰͎e̬͔̭͂r͚̼̹̲ ̫͓͉̳͈ō̠͕͖̚f̝͍̠ ͕̲̞͖͑Z̖̫̤̫ͪa͉̬͈̗l͖͎g̳̥o̰̥̅!̣͔̲̻͊̄ ̙̘̦̹̦. -* [`l2met`](https://github.com/meatballhat/logrus-formatters): log in [l2met](http://r.32k.io/l2met-introduction) format. - -You can define your formatter by implementing the `Formatter` interface, -requiring a `Format` method. `Format` takes an `*Entry`. `entry.Data` is a -`Fields` type (`map[string]interface{}`) with all your fields as well as the -default ones (see Entries section above): - -```go -type MyJSONFormatter struct { -} - -log.Formatter = new(MyJSONFormatter) - -func (f *JSONFormatter) Format(entry *Entry) ([]byte, error) { - serialized, err := json.Marshal(entry.Data) - if err != nil { - return nil, fmt.Errorf("Failed to marshal fields to JSON, %v", err) - } - return append(serialized, '\n'), nil -} -``` - -#### TODO - -* Performance -* Default fields for an instance and inheritance -* Default available hooks (airbrake, statsd, dump cores) - -[godoc]: https://godoc.org/github.com/Sirupsen/logrus diff --git a/Godeps/_workspace/src/github.com/Sirupsen/logrus/entry.go b/Godeps/_workspace/src/github.com/Sirupsen/logrus/entry.go deleted file mode 100644 index f0e5f76..0000000 --- a/Godeps/_workspace/src/github.com/Sirupsen/logrus/entry.go +++ /dev/null @@ -1,207 +0,0 @@ -package logrus - -import ( - "bytes" - "fmt" - "io" - "os" - "time" -) - -type Entry struct { - logger *Logger - Data Fields -} - -var baseTimestamp time.Time - -func init() { - baseTimestamp = time.Now() -} - -func miniTS() int { - return int(time.Since(baseTimestamp) / time.Second) -} - -func NewEntry(logger *Logger) *Entry { - return &Entry{ - logger: logger, - // Default is three fields, give a little extra room - Data: make(Fields, 5), - } -} - -func (entry *Entry) Reader() (*bytes.Buffer, error) { - serialized, err := entry.logger.Formatter.Format(entry) - return bytes.NewBuffer(serialized), err -} - -func (entry *Entry) String() (string, error) { - reader, err := entry.Reader() - if err != nil { - return "", err - } - - return reader.String(), err -} - -func (entry *Entry) WithField(key string, value interface{}) *Entry { - entry.Data[key] = value - return entry -} - -func (entry *Entry) WithFields(fields Fields) *Entry { - for key, value := range fields { - entry.WithField(key, value) - } - return entry -} - -func (entry *Entry) log(level string, levelInt Level, msg string) string { - entry.Data["time"] = time.Now().String() - entry.Data["level"] = level - entry.Data["msg"] = msg - - reader, err := entry.Reader() - if err != nil { - fmt.Fprintf(os.Stderr, "Failed to obtain reader, %v", err) - } - - if err := entry.logger.Hooks.Fire(levelInt, entry); err != nil { - fmt.Fprintf(os.Stderr, "Failed to fire hook", err) - } - - entry.logger.mu.Lock() - defer entry.logger.mu.Unlock() - - _, err = io.Copy(entry.logger.Out, reader) - if err != nil { - fmt.Fprintf(os.Stderr, "Failed to write to log, %v", err) - } - - return reader.String() -} - -func (entry *Entry) Debug(args ...interface{}) { - if entry.logger.Level >= Debug { - entry.log("debug", Debug, fmt.Sprint(args...)) - entry.logger.Hooks.Fire(Debug, entry) - } -} - -func (entry *Entry) Print(args ...interface{}) { - entry.Info(args...) -} - -func (entry *Entry) Info(args ...interface{}) { - if entry.logger.Level >= Info { - entry.log("info", Info, fmt.Sprint(args...)) - } -} - -func (entry *Entry) Warn(args ...interface{}) { - if entry.logger.Level >= Warn { - entry.log("warning", Warn, fmt.Sprint(args...)) - } -} - -func (entry *Entry) Error(args ...interface{}) { - if entry.logger.Level >= Error { - entry.log("error", Error, fmt.Sprint(args...)) - } -} - -func (entry *Entry) Fatal(args ...interface{}) { - if entry.logger.Level >= Fatal { - entry.log("fatal", Fatal, fmt.Sprint(args...)) - } - os.Exit(1) -} - -func (entry *Entry) Panic(args ...interface{}) { - if entry.logger.Level >= Panic { - msg := entry.log("panic", Panic, fmt.Sprint(args...)) - panic(msg) - } - panic(fmt.Sprint(args...)) -} - -// Entry Printf family functions - -func (entry *Entry) Debugf(format string, args ...interface{}) { - if entry.logger.Level >= Debug { - entry.Debug(fmt.Sprintf(format, args...)) - } -} - -func (entry *Entry) Infof(format string, args ...interface{}) { - if entry.logger.Level >= Info { - entry.Info(fmt.Sprintf(format, args...)) - } -} - -func (entry *Entry) Printf(format string, args ...interface{}) { - entry.Infof(format, args...) -} - -func (entry *Entry) Warnf(format string, args ...interface{}) { - if entry.logger.Level >= Warn { - entry.Warn(fmt.Sprintf(format, args...)) - } -} - -func (entry *Entry) Warningf(format string, args ...interface{}) { - entry.Warnf(format, args...) -} - -func (entry *Entry) Errorf(format string, args ...interface{}) { - if entry.logger.Level >= Error { - entry.Error(fmt.Sprintf(format, args...)) - } -} - -func (entry *Entry) Fatalf(format string, args ...interface{}) { - if entry.logger.Level >= Fatal { - entry.Fatal(fmt.Sprintf(format, args...)) - } -} - -func (entry *Entry) Panicf(format string, args ...interface{}) { - if entry.logger.Level >= Panic { - entry.Panic(fmt.Sprintf(format, args...)) - } -} - -// Entry Println family functions - -func (entry *Entry) Debugln(args ...interface{}) { - entry.Debug(args...) -} - -func (entry *Entry) Infoln(args ...interface{}) { - entry.Info(args...) -} - -func (entry *Entry) Println(args ...interface{}) { - entry.Info(args...) -} - -func (entry *Entry) Warnln(args ...interface{}) { - entry.Warn(args...) -} - -func (entry *Entry) Warningln(args ...interface{}) { - entry.Warn(args...) -} - -func (entry *Entry) Errorln(args ...interface{}) { - entry.Error(args...) -} - -func (entry *Entry) Fatalln(args ...interface{}) { - entry.Fatal(args...) -} - -func (entry *Entry) Panicln(args ...interface{}) { - entry.Panic(args...) -} diff --git a/Godeps/_workspace/src/github.com/Sirupsen/logrus/examples/text.go b/Godeps/_workspace/src/github.com/Sirupsen/logrus/examples/text.go deleted file mode 100644 index b96f833..0000000 --- a/Godeps/_workspace/src/github.com/Sirupsen/logrus/examples/text.go +++ /dev/null @@ -1,37 +0,0 @@ -package main - -import ( - "github.com/Sirupsen/logrus" -) - -func main() { - log := logrus.New() - log.Formatter = new(logrus.JSONFormatter) - - for { - log.WithFields(logrus.Fields{ - "animal": "walrus", - "size": 10, - }).Print("A group of walrus emerges from the ocean") - - log.WithFields(logrus.Fields{ - "omg": true, - "number": 122, - }).Warn("The group's number increased tremendously!") - - log.WithFields(logrus.Fields{ - "animal": "walrus", - "size": 10, - }).Print("A giant walrus appears!") - - log.WithFields(logrus.Fields{ - "animal": "walrus", - "size": 9, - }).Print("Tremendously sized cow enters the ocean.") - - log.WithFields(logrus.Fields{ - "omg": true, - "number": 100, - }).Fatal("The ice breaks!") - } -} diff --git a/Godeps/_workspace/src/github.com/Sirupsen/logrus/formatter.go b/Godeps/_workspace/src/github.com/Sirupsen/logrus/formatter.go deleted file mode 100644 index 3a2eff5..0000000 --- a/Godeps/_workspace/src/github.com/Sirupsen/logrus/formatter.go +++ /dev/null @@ -1,15 +0,0 @@ -package logrus - -// The Formatter interface is used to implement a custom Formatter. It takes an -// `Entry`. It exposes all the fields, including the default ones: -// -// * `entry.Data["msg"]`. The message passed from Info, Warn, Error .. -// * `entry.Data["time"]`. The timestamp. -// * `entry.Data["level"]. The level the entry was logged at. -// -// Any additional fields added with `WithField` or `WithFields` are also in -// `entry.Data`. Format is expected to return an array of bytes which are then -// logged to `logger.Out`. -type Formatter interface { - Format(*Entry) ([]byte, error) -} diff --git a/Godeps/_workspace/src/github.com/Sirupsen/logrus/hooks.go b/Godeps/_workspace/src/github.com/Sirupsen/logrus/hooks.go deleted file mode 100644 index 0da2b36..0000000 --- a/Godeps/_workspace/src/github.com/Sirupsen/logrus/hooks.go +++ /dev/null @@ -1,34 +0,0 @@ -package logrus - -// A hook to be fired when logging on the logging levels returned from -// `Levels()` on your implementation of the interface. Note that this is not -// fired in a goroutine or a channel with workers, you should handle such -// functionality yourself if your call is non-blocking and you don't wish for -// the logging calls for levels returned from `Levels()` to block. -type Hook interface { - Levels() []Level - Fire(*Entry) error -} - -// Internal type for storing the hooks on a logger instance. -type levelHooks map[Level][]Hook - -// Add a hook to an instance of logger. This is called with -// `log.Hooks.Add(new(MyHook))` where `MyHook` implements the `Hook` interface. -func (hooks levelHooks) Add(hook Hook) { - for _, level := range hook.Levels() { - hooks[level] = append(hooks[level], hook) - } -} - -// Fire all the hooks for the passed level. Used by `entry.log` to fire -// appropriate hooks for a log entry. -func (hooks levelHooks) Fire(level Level, entry *Entry) error { - for _, hook := range hooks[level] { - if err := hook.Fire(entry); err != nil { - return err - } - } - - return nil -} diff --git a/Godeps/_workspace/src/github.com/Sirupsen/logrus/json_formatter.go b/Godeps/_workspace/src/github.com/Sirupsen/logrus/json_formatter.go deleted file mode 100644 index cb3489e..0000000 --- a/Godeps/_workspace/src/github.com/Sirupsen/logrus/json_formatter.go +++ /dev/null @@ -1,17 +0,0 @@ -package logrus - -import ( - "encoding/json" - "fmt" -) - -type JSONFormatter struct { -} - -func (f *JSONFormatter) Format(entry *Entry) ([]byte, error) { - serialized, err := json.Marshal(entry.Data) - if err != nil { - return nil, fmt.Errorf("Failed to marshal fields to JSON, %v", err) - } - return append(serialized, '\n'), nil -} diff --git a/Godeps/_workspace/src/github.com/Sirupsen/logrus/logger.go b/Godeps/_workspace/src/github.com/Sirupsen/logrus/logger.go deleted file mode 100644 index a545afd..0000000 --- a/Godeps/_workspace/src/github.com/Sirupsen/logrus/logger.go +++ /dev/null @@ -1,161 +0,0 @@ -package logrus - -import ( - "io" - "os" - "sync" -) - -type Logger struct { - // The logs are `io.Copy`'d to this in a mutex. It's common to set this to a - // file, or leave it default which is `os.Stdout`. You can also set this to - // something more adventorous, such as logging to Kafka. - Out io.Writer - // Hooks for the logger instance. These allow firing events based on logging - // levels and log entries. For example, to send errors to an error tracking - // service, log to StatsD or dump the core on fatal errors. - Hooks levelHooks - // All log entries pass through the formatter before logged to Out. The - // included formatters are `TextFormatter` and `JSONFormatter` for which - // TextFormatter is the default. In development (when a TTY is attached) it - // logs with colors, but to a file it wouldn't. You can easily implement your - // own that implements the `Formatter` interface, see the `README` or included - // formatters for examples. - Formatter Formatter - // The logging level the logger should log at. This is typically (and defaults - // to) `logrus.Info`, which allows Info(), Warn(), Error() and Fatal() to be - // logged. `logrus.Debug` is useful in - Level Level - // Used to sync writing to the log. - mu sync.Mutex -} - -// Creates a new logger. Configuration should be set by changing `Formatter`, -// `Out` and `Hooks` directly on the default logger instance. You can also just -// instantiate your own: -// -// var log = &Logger{ -// Out: os.Stderr, -// Formatter: new(JSONFormatter), -// Hooks: make(levelHooks), -// Level: logrus.Debug, -// } -// -// It's recommended to make this a global instance called `log`. -func New() *Logger { - return &Logger{ - Out: os.Stdout, - Formatter: new(TextFormatter), - Hooks: make(levelHooks), - Level: Info, - } -} - -// Adds a field to the log entry, note that you it doesn't log until you call -// Debug, Print, Info, Warn, Fatal or Panic. It only creates a log entry. -// Ff you want multiple fields, use `WithFields`. -func (logger *Logger) WithField(key string, value interface{}) *Entry { - return NewEntry(logger).WithField(key, value) -} - -// Adds a struct of fields to the log entry. All it does is call `WithField` for -// each `Field`. -func (logger *Logger) WithFields(fields Fields) *Entry { - return NewEntry(logger).WithFields(fields) -} - -func (logger *Logger) Debugf(format string, args ...interface{}) { - NewEntry(logger).Debugf(format, args...) -} - -func (logger *Logger) Infof(format string, args ...interface{}) { - NewEntry(logger).Infof(format, args...) -} - -func (logger *Logger) Printf(format string, args ...interface{}) { - NewEntry(logger).Printf(format, args...) -} - -func (logger *Logger) Warnf(format string, args ...interface{}) { - NewEntry(logger).Warnf(format, args...) -} - -func (logger *Logger) Warningf(format string, args ...interface{}) { - NewEntry(logger).Warnf(format, args...) -} - -func (logger *Logger) Errorf(format string, args ...interface{}) { - NewEntry(logger).Errorf(format, args...) -} - -func (logger *Logger) Fatalf(format string, args ...interface{}) { - NewEntry(logger).Fatalf(format, args...) -} - -func (logger *Logger) Panicf(format string, args ...interface{}) { - NewEntry(logger).Panicf(format, args...) -} - -func (logger *Logger) Debug(args ...interface{}) { - NewEntry(logger).Debug(args...) -} - -func (logger *Logger) Info(args ...interface{}) { - NewEntry(logger).Info(args...) -} - -func (logger *Logger) Print(args ...interface{}) { - NewEntry(logger).Info(args...) -} - -func (logger *Logger) Warn(args ...interface{}) { - NewEntry(logger).Warn(args...) -} - -func (logger *Logger) Warning(args ...interface{}) { - NewEntry(logger).Warn(args...) -} - -func (logger *Logger) Error(args ...interface{}) { - NewEntry(logger).Error(args...) -} - -func (logger *Logger) Fatal(args ...interface{}) { - NewEntry(logger).Fatal(args...) -} - -func (logger *Logger) Panic(args ...interface{}) { - NewEntry(logger).Panic(args...) -} - -func (logger *Logger) Debugln(args ...interface{}) { - NewEntry(logger).Debugln(args...) -} - -func (logger *Logger) Infoln(args ...interface{}) { - NewEntry(logger).Infoln(args...) -} - -func (logger *Logger) Println(args ...interface{}) { - NewEntry(logger).Println(args...) -} - -func (logger *Logger) Warnln(args ...interface{}) { - NewEntry(logger).Warnln(args...) -} - -func (logger *Logger) Warningln(args ...interface{}) { - NewEntry(logger).Warnln(args...) -} - -func (logger *Logger) Errorln(args ...interface{}) { - NewEntry(logger).Errorln(args...) -} - -func (logger *Logger) Fatalln(args ...interface{}) { - NewEntry(logger).Fatalln(args...) -} - -func (logger *Logger) Panicln(args ...interface{}) { - NewEntry(logger).Panicln(args...) -} diff --git a/Godeps/_workspace/src/github.com/Sirupsen/logrus/logrus.go b/Godeps/_workspace/src/github.com/Sirupsen/logrus/logrus.go deleted file mode 100644 index 2376db3..0000000 --- a/Godeps/_workspace/src/github.com/Sirupsen/logrus/logrus.go +++ /dev/null @@ -1,52 +0,0 @@ -package logrus - -import ( - "log" -) - -// Fields type, used to pass to `WithFields`. -type Fields map[string]interface{} - -// Level type -type Level uint8 - -// These are the different logging levels. You can set the logging level to log -// on your instance of logger, obtained with `logrus.New()`. -const ( - // Panic level, highest level of severity. Logs and then calls panic with the - // message passed to Debug, Info, ... - Panic Level = iota - // Fatal level. Logs and then calls `os.Exit(1)`. It will exit even if the - // logging level is set to Panic. - Fatal - // Error level. Logs. Used for errors that should definitely be noted. - // Commonly used for hooks to send errors to an error tracking service. - Error - // Warn level. Non-critical entries that deserve eyes. - Warn - // Info level. General operational entries about what's going on inside the - // application. - Info - // Debug level. Usually only enabled when debugging. Very verbose logging. - Debug -) - -// Won't compile if StdLogger can't be realized by a log.Logger -var _ StdLogger = &log.Logger{} - -// StdLogger is what your logrus-enabled library should take, that way -// it'll accept a stdlib logger and a logrus logger. There's no standard -// interface, this is the closest we get, unfortunately. -type StdLogger interface { - Print(...interface{}) - Printf(string, ...interface{}) - Println(...interface{}) - - Fatal(...interface{}) - Fatalf(string, ...interface{}) - Fatalln(...interface{}) - - Panic(...interface{}) - Panicf(string, ...interface{}) - Panicln(...interface{}) -} diff --git a/Godeps/_workspace/src/github.com/Sirupsen/logrus/logrus_test.go b/Godeps/_workspace/src/github.com/Sirupsen/logrus/logrus_test.go deleted file mode 100644 index 5889db5..0000000 --- a/Godeps/_workspace/src/github.com/Sirupsen/logrus/logrus_test.go +++ /dev/null @@ -1,110 +0,0 @@ -package logrus - -import ( - "bytes" - "encoding/json" - "io/ioutil" - "testing" - "time" - - "github.com/stretchr/testify/assert" -) - -func LogAndAssertJSON(t *testing.T, log func(*Logger), assertions func(fields Fields)) { - var buffer bytes.Buffer - var fields Fields - - logger := New() - logger.Out = &buffer - logger.Formatter = new(JSONFormatter) - - log(logger) - - err := json.Unmarshal(buffer.Bytes(), &fields) - assert.Nil(t, err) - - assertions(fields) -} - -func TestPrint(t *testing.T) { - LogAndAssertJSON(t, func(log *Logger) { - log.Print("test") - }, func(fields Fields) { - assert.Equal(t, fields["msg"], "test") - assert.Equal(t, fields["level"], "info") - }) -} - -func TestInfo(t *testing.T) { - LogAndAssertJSON(t, func(log *Logger) { - log.Info("test") - }, func(fields Fields) { - assert.Equal(t, fields["msg"], "test") - assert.Equal(t, fields["level"], "info") - }) -} - -func TestWarn(t *testing.T) { - LogAndAssertJSON(t, func(log *Logger) { - log.Warn("test") - }, func(fields Fields) { - assert.Equal(t, fields["msg"], "test") - assert.Equal(t, fields["level"], "warning") - }) -} - -type SlowString string - -func (s SlowString) String() string { - time.Sleep(time.Millisecond) - return string(s) -} - -func getLogAtLevel(l Level) *Logger { - log := New() - log.Level = l - log.Out = ioutil.Discard - return log -} - -func BenchmarkLevelDisplayed(b *testing.B) { - log := getLogAtLevel(Info) - for i := 0; i < b.N; i++ { - log.Info(SlowString("foo")) - } -} - -func BenchmarkLevelHidden(b *testing.B) { - log := getLogAtLevel(Info) - for i := 0; i < b.N; i++ { - log.Debug(SlowString("foo")) - } -} - -func BenchmarkLevelfDisplayed(b *testing.B) { - log := getLogAtLevel(Info) - for i := 0; i < b.N; i++ { - log.Infof("%s", SlowString("foo")) - } -} - -func BenchmarkLevelfHidden(b *testing.B) { - log := getLogAtLevel(Info) - for i := 0; i < b.N; i++ { - log.Debugf("%s", SlowString("foo")) - } -} - -func BenchmarkLevellnDisplayed(b *testing.B) { - log := getLogAtLevel(Info) - for i := 0; i < b.N; i++ { - log.Infoln(SlowString("foo")) - } -} - -func BenchmarkLevellnHidden(b *testing.B) { - log := getLogAtLevel(Info) - for i := 0; i < b.N; i++ { - log.Debugln(SlowString("foo")) - } -} diff --git a/Godeps/_workspace/src/github.com/Sirupsen/logrus/text_formatter.go b/Godeps/_workspace/src/github.com/Sirupsen/logrus/text_formatter.go deleted file mode 100644 index 95c3264..0000000 --- a/Godeps/_workspace/src/github.com/Sirupsen/logrus/text_formatter.go +++ /dev/null @@ -1,81 +0,0 @@ -package logrus - -import ( - "fmt" - "os" - "sort" - "strings" - - "github.com/burke/ttyutils" -) - -const ( - nocolor = 0 - red = 31 - green = 32 - yellow = 33 - blue = 34 -) - -type TextFormatter struct { - // Set to true to bypass checking for a TTY before outputting colors. - ForceColors bool -} - -func (f *TextFormatter) Format(entry *Entry) ([]byte, error) { - var serialized []byte - - if f.ForceColors || ttyutils.IsTerminal(os.Stdout.Fd()) { - levelText := strings.ToUpper(entry.Data["level"].(string))[0:4] - - levelColor := blue - - if entry.Data["level"] == "warning" { - levelColor = yellow - } else if entry.Data["level"] == "error" || - entry.Data["level"] == "fatal" || - entry.Data["level"] == "panic" { - levelColor = red - } - - serialized = append(serialized, []byte(fmt.Sprintf("\x1b[%dm%s\x1b[0m[%04d] %-45s ", levelColor, levelText, miniTS(), entry.Data["msg"]))...) - - keys := make([]string, 0) - for k, _ := range entry.Data { - if k != "level" && k != "time" && k != "msg" { - keys = append(keys, k) - } - } - sort.Strings(keys) - first := true - for _, k := range keys { - v := entry.Data[k] - if first { - first = false - } else { - serialized = append(serialized, ' ') - } - serialized = append(serialized, []byte(fmt.Sprintf("\x1b[%dm%s\x1b[0m=%v", levelColor, k, v))...) - } - } else { - serialized = f.AppendKeyValue(serialized, "time", entry.Data["time"].(string)) - serialized = f.AppendKeyValue(serialized, "level", entry.Data["level"].(string)) - serialized = f.AppendKeyValue(serialized, "msg", entry.Data["msg"].(string)) - - for key, value := range entry.Data { - if key != "time" && key != "level" && key != "msg" { - serialized = f.AppendKeyValue(serialized, key, value) - } - } - } - - return append(serialized, '\n'), nil -} - -func (f *TextFormatter) AppendKeyValue(serialized []byte, key, value interface{}) []byte { - if _, ok := value.(string); ok { - return append(serialized, []byte(fmt.Sprintf("%v='%v' ", key, value))...) - } else { - return append(serialized, []byte(fmt.Sprintf("%v=%v ", key, value))...) - } -} diff --git a/Godeps/_workspace/src/github.com/codegangsta/cli/.travis.yml b/Godeps/_workspace/src/github.com/codegangsta/cli/.travis.yml index 2379c61..baf46ab 100644 --- a/Godeps/_workspace/src/github.com/codegangsta/cli/.travis.yml +++ b/Godeps/_workspace/src/github.com/codegangsta/cli/.travis.yml @@ -1,2 +1,6 @@ language: go go: 1.1 + +script: +- go vet ./... +- go test -v ./... diff --git a/Godeps/_workspace/src/github.com/codegangsta/cli/README.md b/Godeps/_workspace/src/github.com/codegangsta/cli/README.md index ff1e584..5c83df6 100644 --- a/Godeps/_workspace/src/github.com/codegangsta/cli/README.md +++ b/Godeps/_workspace/src/github.com/codegangsta/cli/README.md @@ -9,17 +9,17 @@ http://godoc.org/github.com/codegangsta/cli ## Overview Command line apps are usually so tiny that there is absolutely no reason why your code should *not* be self-documenting. Things like generating help text and parsing command flags/options should not hinder productivity when writing a command line app. -This is where cli.go comes into play. cli.go makes command line programming fun, organized, and expressive! +**This is where cli.go comes into play.** cli.go makes command line programming fun, organized, and expressive! ## Installation Make sure you have a working Go environment (go 1.1 is *required*). [See the install instructions](http://golang.org/doc/install.html). -To install cli.go, simply run: +To install `cli.go`, simply run: ``` $ go get github.com/codegangsta/cli ``` -Make sure your PATH includes to the `$GOPATH/bin` directory so your commands can be easily used: +Make sure your `PATH` includes to the `$GOPATH/bin` directory so your commands can be easily used: ``` export PATH=$PATH:$GOPATH/bin ``` @@ -122,7 +122,7 @@ GLOBAL OPTIONS ``` ### Arguments -You can lookup arguments by calling the `Args` function on cli.Context. +You can lookup arguments by calling the `Args` function on `cli.Context`. ``` go ... @@ -137,7 +137,11 @@ Setting and querying flags is simple. ``` go ... app.Flags = []cli.Flag { - cli.StringFlag{"lang", "english", "language for the greeting"}, + cli.StringFlag{ + Name: "lang", + Value: "english", + Usage: "language for the greeting", + }, } app.Action = func(c *cli.Context) { name := "someone" @@ -155,11 +159,30 @@ app.Action = func(c *cli.Context) { #### Alternate Names -You can set alternate (or short) names for flags by providing a comma-delimited list for the Name. e.g. +You can set alternate (or short) names for flags by providing a comma-delimited list for the `Name`. e.g. ``` go app.Flags = []cli.Flag { - cli.StringFlag{"lang, l", "english", "language for the greeting"}, + cli.StringFlag{ + Name: "lang, l", + Value: "english", + Usage: "language for the greeting", + }, +} +``` + +#### Values from the Environment + +You can also have the default value set from the environment via `EnvVar`. e.g. + +``` go +app.Flags = []cli.Flag { + cli.StringFlag{ + Name: "lang, l", + Value: "english", + Usage: "language for the greeting", + EnvVar: "APP_LANG", + }, } ``` @@ -187,9 +210,78 @@ app.Commands = []cli.Command{ println("completed task: ", c.Args().First()) }, }, + { + Name: "template", + ShortName: "r", + Usage: "options for task templates", + Subcommands: []cli.Command{ + { + Name: "add", + Usage: "add a new template", + Action: func(c *cli.Context) { + println("new task template: ", c.Args().First()) + }, + }, + { + Name: "remove", + Usage: "remove an existing template", + Action: func(c *cli.Context) { + println("removed task template: ", c.Args().First()) + }, + }, + }, + }, } ... ``` +### Bash Completion + +You can enable completion commands by setting the `EnableBashCompletion` +flag on the `App` object. By default, this setting will only auto-complete to +show an app's subcommands, but you can write your own completion methods for +the App or its subcommands. +```go +... +var tasks = []string{"cook", "clean", "laundry", "eat", "sleep", "code"} +app := cli.NewApp() +app.EnableBashCompletion = true +app.Commands = []cli.Command{ + { + Name: "complete", + ShortName: "c", + Usage: "complete a task on the list", + Action: func(c *cli.Context) { + println("completed task: ", c.Args().First()) + }, + BashComplete: func(c *cli.Context) { + // This will complete if no args are passed + if len(c.Args()) > 0 { + return + } + for _, t := range tasks { + println(t) + } + }, + } +} +... +``` + +#### To Enable + +Source the `autocomplete/bash_autocomplete` file in your `.bashrc` file while +setting the `PROG` variable to the name of your program: + +`PROG=myprogram source /.../cli/autocomplete/bash_autocomplete` + + +## Contribution Guidelines +Feel free to put up a pull request to fix a bug or maybe add a feature. I will give it a code review and make sure that it does not break backwards compatibility. If I or any other collaborators agree that it is in line with the vision of the project, we will work with you to get the code into a mergeable state and merge it into the master branch. + +If you are have contributed something significant to the project, I will most likely add you as a collaborator. As a collaborator you are given the ability to merge others pull requests. It is very important that new code does not break existing code, so be careful about what code you do choose to merge. If you have any questions feel free to link @codegangsta to the issue in question and we can review it together. + +If you feel like you have contributed to the project but have not yet been added as a collaborator, I probably forgot to add you. Hit @codegangsta up over email and we will get it figured out. + ## About cli.go is written by none other than the [Code Gangsta](http://codegangsta.io) diff --git a/Godeps/_workspace/src/github.com/codegangsta/cli/app.go b/Godeps/_workspace/src/github.com/codegangsta/cli/app.go index a614236..66e541c 100644 --- a/Godeps/_workspace/src/github.com/codegangsta/cli/app.go +++ b/Godeps/_workspace/src/github.com/codegangsta/cli/app.go @@ -20,11 +20,19 @@ type App struct { Commands []Command // List of flags to parse Flags []Flag + // Boolean to enable bash completion commands + EnableBashCompletion bool + // Boolean to hide built-in help command + HideHelp bool + // An action to execute when the bash-completion flag is set + BashComplete func(context *Context) // An action to execute before any subcommands are run, but after the context is ready // If a non-nil error is returned, no subcommands are run Before func(context *Context) error // The action to execute when no subcommands are specified Action func(context *Context) + // Execute this function if the proper command cannot be found + CommandNotFound func(context *Context, command string) // Compilation date Compiled time.Time // Author @@ -46,26 +54,28 @@ func compileTime() time.Time { // Creates a new cli Application with some reasonable defaults for Name, Usage, Version and Action. func NewApp() *App { return &App{ - Name: os.Args[0], - Usage: "A new cli application", - Version: "0.0.0", - Action: helpCommand.Action, - Compiled: compileTime(), - Author: "Author", - Email: "unknown@email", + Name: os.Args[0], + Usage: "A new cli application", + Version: "0.0.0", + BashComplete: DefaultAppComplete, + Action: helpCommand.Action, + Compiled: compileTime(), } } // Entry point to the cli app. Parses the arguments slice and routes to the proper flag/args combination func (a *App) Run(arguments []string) error { // append help to commands - if a.Command(helpCommand.Name) == nil { + if a.Command(helpCommand.Name) == nil && !a.HideHelp { a.Commands = append(a.Commands, helpCommand) + a.appendFlag(HelpFlag) } //append version/help flags - a.appendFlag(BoolFlag{"version, v", "print the version"}) - a.appendFlag(BoolFlag{"help, h", "show help"}) + if a.EnableBashCompletion { + a.appendFlag(BashCompletionFlag) + } + a.appendFlag(VersionFlag) // parse flags set := flagSet(a.Name, a.Flags) @@ -88,6 +98,10 @@ func (a *App) Run(arguments []string) error { return err } + if checkCompletions(context) { + return nil + } + if checkHelp(context) { return nil } @@ -117,6 +131,93 @@ func (a *App) Run(arguments []string) error { return nil } +// Another entry point to the cli app, takes care of passing arguments and error handling +func (a *App) RunAndExitOnError() { + if err := a.Run(os.Args); err != nil { + os.Stderr.WriteString(fmt.Sprintln(err)) + os.Exit(1) + } +} + +// Invokes the subcommand given the context, parses ctx.Args() to generate command-specific flags +func (a *App) RunAsSubcommand(ctx *Context) error { + // append help to commands + if len(a.Commands) > 0 { + if a.Command(helpCommand.Name) == nil && !a.HideHelp { + a.Commands = append(a.Commands, helpCommand) + a.appendFlag(HelpFlag) + } + } + + // append flags + if a.EnableBashCompletion { + a.appendFlag(BashCompletionFlag) + } + + // parse flags + set := flagSet(a.Name, a.Flags) + set.SetOutput(ioutil.Discard) + err := set.Parse(ctx.Args().Tail()) + nerr := normalizeFlags(a.Flags, set) + context := NewContext(a, set, ctx.globalSet) + + if nerr != nil { + fmt.Println(nerr) + if len(a.Commands) > 0 { + ShowSubcommandHelp(context) + } else { + ShowCommandHelp(ctx, context.Args().First()) + } + fmt.Println("") + return nerr + } + + if err != nil { + fmt.Printf("Incorrect Usage.\n\n") + ShowSubcommandHelp(context) + return err + } + + if checkCompletions(context) { + return nil + } + + if len(a.Commands) > 0 { + if checkSubcommandHelp(context) { + return nil + } + } else { + if checkCommandHelp(ctx, context.Args().First()) { + return nil + } + } + + if a.Before != nil { + err := a.Before(context) + if err != nil { + return err + } + } + + args := context.Args() + if args.Present() { + name := args.First() + c := a.Command(name) + if c != nil { + return c.Run(context) + } + } + + // Run default Action + if len(a.Commands) > 0 { + a.Action(context) + } else { + a.Action(ctx) + } + + return nil +} + // Returns the named command on App. Returns nil if the command does not exist func (a *App) Command(name string) *Command { for _, c := range a.Commands { diff --git a/Godeps/_workspace/src/github.com/codegangsta/cli/app_test.go b/Godeps/_workspace/src/github.com/codegangsta/cli/app_test.go index 4e94f9e..81d1174 100644 --- a/Godeps/_workspace/src/github.com/codegangsta/cli/app_test.go +++ b/Godeps/_workspace/src/github.com/codegangsta/cli/app_test.go @@ -2,9 +2,10 @@ package cli_test import ( "fmt" - "github.com/codegangsta/cli" "os" "testing" + + "github.com/codegangsta/cli" ) func ExampleApp() { @@ -24,6 +25,43 @@ func ExampleApp() { // Hello Jeremy } +func ExampleAppSubcommand() { + // set args for examples sake + os.Args = []string{"say", "hi", "english", "--name", "Jeremy"} + app := cli.NewApp() + app.Name = "say" + app.Commands = []cli.Command{ + { + Name: "hello", + ShortName: "hi", + Usage: "use it to see a description", + Description: "This is how we describe hello the function", + Subcommands: []cli.Command{ + { + Name: "english", + ShortName: "en", + Usage: "sends a greeting in english", + Description: "greets someone in english", + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "name", + Value: "Bob", + Usage: "Name of the person to greet", + }, + }, + Action: func(c *cli.Context) { + fmt.Println("Hello,", c.String("name")) + }, + }, + }, + }, + } + + app.Run(os.Args) + // Output: + // Hello, Jeremy +} + func ExampleAppHelp() { // set args for examples sake os.Args = []string{"greet", "h", "describeit"} @@ -35,9 +73,9 @@ func ExampleAppHelp() { } app.Commands = []cli.Command{ { - Name: "describeit", - ShortName: "d", - Usage: "use it to see a description", + Name: "describeit", + ShortName: "d", + Usage: "use it to see a description", Description: "This is how we describe describeit the function", Action: func(c *cli.Context) { fmt.Printf("i like to describe things") @@ -50,12 +88,45 @@ func ExampleAppHelp() { // describeit - use it to see a description // // USAGE: - // command describeit [command options] [arguments...] + // command describeit [arguments...] // // DESCRIPTION: // This is how we describe describeit the function - // - // OPTIONS: +} + +func ExampleAppBashComplete() { + // set args for examples sake + os.Args = []string{"greet", "--generate-bash-completion"} + + app := cli.NewApp() + app.Name = "greet" + app.EnableBashCompletion = true + app.Commands = []cli.Command{ + { + Name: "describeit", + ShortName: "d", + Usage: "use it to see a description", + Description: "This is how we describe describeit the function", + Action: func(c *cli.Context) { + fmt.Printf("i like to describe things") + }, + }, { + Name: "next", + Usage: "next example", + Description: "more stuff to see when generating bash completion", + Action: func(c *cli.Context) { + fmt.Printf("the next example") + }, + }, + } + + app.Run(os.Args) + // Output: + // describeit + // d + // next + // help + // h } func TestApp_Run(t *testing.T) { @@ -186,11 +257,11 @@ func TestApp_ParseSliceFlags(t *testing.T) { var expectedStringSlice = []string{"8.8.8.8", "8.8.4.4"} if !IntsEquals(parsedIntSlice, expectedIntSlice) { - t.Errorf("%s does not match %s", parsedIntSlice, expectedIntSlice) + t.Errorf("%v does not match %v", parsedIntSlice, expectedIntSlice) } if !StrsEquals(parsedStringSlice, expectedStringSlice) { - t.Errorf("%s does not match %s", parsedStringSlice, expectedStringSlice) + t.Errorf("%v does not match %v", parsedStringSlice, expectedStringSlice) } } @@ -278,3 +349,75 @@ func TestAppHelpPrinter(t *testing.T) { t.Errorf("Help printer expected to be called, but was not") } } + +func TestAppVersionPrinter(t *testing.T) { + oldPrinter := cli.VersionPrinter + defer func() { + cli.VersionPrinter = oldPrinter + }() + + var wasCalled = false + cli.VersionPrinter = func(c *cli.Context) { + wasCalled = true + } + + app := cli.NewApp() + ctx := cli.NewContext(app, nil, nil) + cli.ShowVersion(ctx) + + if wasCalled == false { + t.Errorf("Version printer expected to be called, but was not") + } +} + +func TestAppCommandNotFound(t *testing.T) { + beforeRun, subcommandRun := false, false + app := cli.NewApp() + + app.CommandNotFound = func(c *cli.Context, command string) { + beforeRun = true + } + + app.Commands = []cli.Command{ + cli.Command{ + Name: "bar", + Action: func(c *cli.Context) { + subcommandRun = true + }, + }, + } + + app.Run([]string{"command", "foo"}) + + expect(t, beforeRun, true) + expect(t, subcommandRun, false) +} + +func TestGlobalFlagsInSubcommands(t *testing.T) { + subcommandRun := false + app := cli.NewApp() + + app.Flags = []cli.Flag{ + cli.BoolFlag{Name: "debug, d", Usage: "Enable debugging"}, + } + + app.Commands = []cli.Command{ + cli.Command{ + Name: "foo", + Subcommands: []cli.Command{ + { + Name: "bar", + Action: func(c *cli.Context) { + if c.GlobalBool("debug") { + subcommandRun = true + } + }, + }, + }, + }, + } + + app.Run([]string{"command", "-d", "foo", "bar"}) + + expect(t, subcommandRun, true) +} diff --git a/Godeps/_workspace/src/github.com/codegangsta/cli/autocomplete/bash_autocomplete b/Godeps/_workspace/src/github.com/codegangsta/cli/autocomplete/bash_autocomplete new file mode 100644 index 0000000..9b55dd9 --- /dev/null +++ b/Godeps/_workspace/src/github.com/codegangsta/cli/autocomplete/bash_autocomplete @@ -0,0 +1,13 @@ +#! /bin/bash + +_cli_bash_autocomplete() { + local cur prev opts base + COMPREPLY=() + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD-1]}" + opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} --generate-bash-completion ) + COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) + return 0 + } + + complete -F _cli_bash_autocomplete $PROG \ No newline at end of file diff --git a/Godeps/_workspace/src/github.com/codegangsta/cli/autocomplete/zsh_autocomplete b/Godeps/_workspace/src/github.com/codegangsta/cli/autocomplete/zsh_autocomplete new file mode 100644 index 0000000..5430a18 --- /dev/null +++ b/Godeps/_workspace/src/github.com/codegangsta/cli/autocomplete/zsh_autocomplete @@ -0,0 +1,5 @@ +autoload -U compinit && compinit +autoload -U bashcompinit && bashcompinit + +script_dir=$(dirname $0) +source ${script_dir}/bash_autocomplete diff --git a/Godeps/_workspace/src/github.com/codegangsta/cli/cli_test.go b/Godeps/_workspace/src/github.com/codegangsta/cli/cli_test.go index 772e90f..879a793 100644 --- a/Godeps/_workspace/src/github.com/codegangsta/cli/cli_test.go +++ b/Godeps/_workspace/src/github.com/codegangsta/cli/cli_test.go @@ -1,8 +1,9 @@ package cli_test import ( - "github.com/codegangsta/cli" "os" + + "github.com/codegangsta/cli" ) func Example() { @@ -30,3 +31,70 @@ func Example() { app.Run(os.Args) } + +func ExampleSubcommand() { + app := cli.NewApp() + app.Name = "say" + app.Commands = []cli.Command{ + { + Name: "hello", + ShortName: "hi", + Usage: "use it to see a description", + Description: "This is how we describe hello the function", + Subcommands: []cli.Command{ + { + Name: "english", + ShortName: "en", + Usage: "sends a greeting in english", + Description: "greets someone in english", + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "name", + Value: "Bob", + Usage: "Name of the person to greet", + }, + }, + Action: func(c *cli.Context) { + println("Hello, ", c.String("name")) + }, + }, { + Name: "spanish", + ShortName: "sp", + Usage: "sends a greeting in spanish", + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "surname", + Value: "Jones", + Usage: "Surname of the person to greet", + }, + }, + Action: func(c *cli.Context) { + println("Hola, ", c.String("surname")) + }, + }, { + Name: "french", + ShortName: "fr", + Usage: "sends a greeting in french", + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "nickname", + Value: "Stevie", + Usage: "Nickname of the person to greet", + }, + }, + Action: func(c *cli.Context) { + println("Bonjour, ", c.String("nickname")) + }, + }, + }, + }, { + Name: "bye", + Usage: "says goodbye", + Action: func(c *cli.Context) { + println("bye") + }, + }, + } + + app.Run(os.Args) +} diff --git a/Godeps/_workspace/src/github.com/codegangsta/cli/command.go b/Godeps/_workspace/src/github.com/codegangsta/cli/command.go index d05cdf5..5622b38 100644 --- a/Godeps/_workspace/src/github.com/codegangsta/cli/command.go +++ b/Godeps/_workspace/src/github.com/codegangsta/cli/command.go @@ -14,21 +14,43 @@ type Command struct { ShortName string // A short description of the usage of this command Usage string - // A longer explaination of how the command works + // A longer explanation of how the command works Description string + // The function to call when checking for bash command completions + BashComplete func(context *Context) + // An action to execute before any sub-subcommands are run, but after the context is ready + // If a non-nil error is returned, no sub-subcommands are run + Before func(context *Context) error // The function to call when this command is invoked Action func(context *Context) + // List of child commands + Subcommands []Command // List of flags to parse Flags []Flag + // Treat all flags as normal arguments if true + SkipFlagParsing bool + // Boolean to hide built-in help command + HideHelp bool } // Invokes the command given the context, parses ctx.Args() to generate command-specific flags func (c Command) Run(ctx *Context) error { - // append help to flags - c.Flags = append( - c.Flags, - BoolFlag{"help, h", "show help"}, - ) + + if len(c.Subcommands) > 0 || c.Before != nil { + return c.startApp(ctx) + } + + if !c.HideHelp { + // append help to flags + c.Flags = append( + c.Flags, + HelpFlag, + ) + } + + if ctx.App.EnableBashCompletion { + c.Flags = append(c.Flags, BashCompletionFlag) + } set := flagSet(c.Name, c.Flags) set.SetOutput(ioutil.Discard) @@ -42,7 +64,7 @@ func (c Command) Run(ctx *Context) error { } var err error - if firstFlagIndex > -1 { + if firstFlagIndex > -1 && !c.SkipFlagParsing { args := ctx.Args() regularArgs := args[1:firstFlagIndex] flagArgs := args[firstFlagIndex:] @@ -67,9 +89,15 @@ func (c Command) Run(ctx *Context) error { return nerr } context := NewContext(ctx.App, set, ctx.globalSet) + + if checkCommandCompletions(context, c.Name) { + return nil + } + if checkCommandHelp(context, c.Name) { return nil } + context.Command = c c.Action(context) return nil } @@ -78,3 +106,39 @@ func (c Command) Run(ctx *Context) error { func (c Command) HasName(name string) bool { return c.Name == name || c.ShortName == name } + +func (c Command) startApp(ctx *Context) error { + app := NewApp() + + // set the name and usage + app.Name = fmt.Sprintf("%s %s", ctx.App.Name, c.Name) + if c.Description != "" { + app.Usage = c.Description + } else { + app.Usage = c.Usage + } + + // set CommandNotFound + app.CommandNotFound = ctx.App.CommandNotFound + + // set the flags and commands + app.Commands = c.Subcommands + app.Flags = c.Flags + app.HideHelp = c.HideHelp + + // bash completion + app.EnableBashCompletion = ctx.App.EnableBashCompletion + if c.BashComplete != nil { + app.BashComplete = c.BashComplete + } + + // set the actions + app.Before = c.Before + if c.Action != nil { + app.Action = c.Action + } else { + app.Action = helpSubcommand.Action + } + + return app.RunAsSubcommand(ctx) +} diff --git a/Godeps/_workspace/src/github.com/codegangsta/cli/command_test.go b/Godeps/_workspace/src/github.com/codegangsta/cli/command_test.go new file mode 100644 index 0000000..c0f556a --- /dev/null +++ b/Godeps/_workspace/src/github.com/codegangsta/cli/command_test.go @@ -0,0 +1,49 @@ +package cli_test + +import ( + "flag" + "testing" + + "github.com/codegangsta/cli" +) + +func TestCommandDoNotIgnoreFlags(t *testing.T) { + app := cli.NewApp() + set := flag.NewFlagSet("test", 0) + test := []string{"blah", "blah", "-break"} + set.Parse(test) + + c := cli.NewContext(app, set, set) + + command := cli.Command{ + Name: "test-cmd", + ShortName: "tc", + Usage: "this is for testing", + Description: "testing", + Action: func(_ *cli.Context) {}, + } + err := command.Run(c) + + expect(t, err.Error(), "flag provided but not defined: -break") +} + +func TestCommandIgnoreFlags(t *testing.T) { + app := cli.NewApp() + set := flag.NewFlagSet("test", 0) + test := []string{"blah", "blah"} + set.Parse(test) + + c := cli.NewContext(app, set, set) + + command := cli.Command{ + Name: "test-cmd", + ShortName: "tc", + Usage: "this is for testing", + Description: "testing", + Action: func(_ *cli.Context) {}, + SkipFlagParsing: true, + } + err := command.Run(c) + + expect(t, err, nil) +} diff --git a/Godeps/_workspace/src/github.com/codegangsta/cli/context.go b/Godeps/_workspace/src/github.com/codegangsta/cli/context.go index d142442..8b44148 100644 --- a/Godeps/_workspace/src/github.com/codegangsta/cli/context.go +++ b/Godeps/_workspace/src/github.com/codegangsta/cli/context.go @@ -5,6 +5,7 @@ import ( "flag" "strconv" "strings" + "time" ) // Context is a type that is passed through to @@ -13,6 +14,7 @@ import ( // parsed command-line options. type Context struct { App *App + Command Command flagSet *flag.FlagSet globalSet *flag.FlagSet setFlags map[string]bool @@ -20,7 +22,7 @@ type Context struct { // Creates a new context. For use in when invoking an App or Command action. func NewContext(app *App, set *flag.FlagSet, globalSet *flag.FlagSet) *Context { - return &Context{app, set, globalSet, nil} + return &Context{App: app, flagSet: set, globalSet: globalSet} } // Looks up the value of a local int flag, returns 0 if no int flag exists @@ -28,6 +30,11 @@ func (c *Context) Int(name string) int { return lookupInt(name, c.flagSet) } +// Looks up the value of a local time.Duration flag, returns 0 if no time.Duration flag exists +func (c *Context) Duration(name string) time.Duration { + return lookupDuration(name, c.flagSet) +} + // Looks up the value of a local float64 flag, returns 0 if no float64 flag exists func (c *Context) Float64(name string) float64 { return lookupFloat64(name, c.flagSet) @@ -38,6 +45,11 @@ func (c *Context) Bool(name string) bool { return lookupBool(name, c.flagSet) } +// Looks up the value of a local boolT flag, returns false if no bool flag exists +func (c *Context) BoolT(name string) bool { + return lookupBoolT(name, c.flagSet) +} + // Looks up the value of a local string flag, returns "" if no string flag exists func (c *Context) String(name string) string { return lookupString(name, c.flagSet) @@ -53,11 +65,21 @@ func (c *Context) IntSlice(name string) []int { return lookupIntSlice(name, c.flagSet) } +// Looks up the value of a local generic flag, returns nil if no generic flag exists +func (c *Context) Generic(name string) interface{} { + return lookupGeneric(name, c.flagSet) +} + // Looks up the value of a global int flag, returns 0 if no int flag exists func (c *Context) GlobalInt(name string) int { return lookupInt(name, c.globalSet) } +// Looks up the value of a global time.Duration flag, returns 0 if no time.Duration flag exists +func (c *Context) GlobalDuration(name string) time.Duration { + return lookupDuration(name, c.globalSet) +} + // Looks up the value of a global bool flag, returns false if no bool flag exists func (c *Context) GlobalBool(name string) bool { return lookupBool(name, c.globalSet) @@ -78,6 +100,11 @@ func (c *Context) GlobalIntSlice(name string) []int { return lookupIntSlice(name, c.globalSet) } +// Looks up the value of a global generic flag, returns nil if no generic flag exists +func (c *Context) GlobalGeneric(name string) interface{} { + return lookupGeneric(name, c.globalSet) +} + // Determines if the flag was actually set exists func (c *Context) IsSet(name string) bool { if c.setFlags == nil { @@ -89,6 +116,18 @@ func (c *Context) IsSet(name string) bool { return c.setFlags[name] == true } +// Returns a slice of flag names used in this context. +func (c *Context) FlagNames() (names []string) { + for _, flag := range c.Command.Flags { + name := strings.Split(flag.getName(), ",")[0] + if name == "help" { + continue + } + names = append(names, name) + } + return +} + type Args []string // Returns the command line arguments associated with the context. @@ -124,6 +163,15 @@ func (a Args) Present() bool { return len(a) != 0 } +// Swaps arguments at the given indexes +func (a Args) Swap(from, to int) error { + if from >= len(a) || to >= len(a) { + return errors.New("index out of range") + } + a[from], a[to] = a[to], a[from] + return nil +} + func lookupInt(name string, set *flag.FlagSet) int { f := set.Lookup(name) if f != nil { @@ -137,6 +185,18 @@ func lookupInt(name string, set *flag.FlagSet) int { return 0 } +func lookupDuration(name string, set *flag.FlagSet) time.Duration { + f := set.Lookup(name) + if f != nil { + val, err := time.ParseDuration(f.Value.String()) + if err == nil { + return val + } + } + + return 0 +} + func lookupFloat64(name string, set *flag.FlagSet) float64 { f := set.Lookup(name) if f != nil { @@ -179,6 +239,14 @@ func lookupIntSlice(name string, set *flag.FlagSet) []int { return nil } +func lookupGeneric(name string, set *flag.FlagSet) interface{} { + f := set.Lookup(name) + if f != nil { + return f.Value + } + return nil +} + func lookupBool(name string, set *flag.FlagSet) bool { f := set.Lookup(name) if f != nil { @@ -192,6 +260,27 @@ func lookupBool(name string, set *flag.FlagSet) bool { return false } +func lookupBoolT(name string, set *flag.FlagSet) bool { + f := set.Lookup(name) + if f != nil { + val, err := strconv.ParseBool(f.Value.String()) + if err != nil { + return true + } + return val + } + + return false +} + +func copyFlag(name string, ff *flag.Flag, set *flag.FlagSet) { + switch ff.Value.(type) { + case *StringSlice: + default: + set.Set(name, ff.Value.String()) + } +} + func normalizeFlags(flags []Flag, set *flag.FlagSet) error { visited := make(map[string]bool) set.Visit(func(f *flag.Flag) { @@ -217,7 +306,9 @@ func normalizeFlags(flags []Flag, set *flag.FlagSet) error { } for _, name := range parts { name = strings.Trim(name, " ") - set.Set(name, ff.Value.String()) + if !visited[name] { + copyFlag(name, ff, set) + } } } return nil diff --git a/Godeps/_workspace/src/github.com/codegangsta/cli/context_test.go b/Godeps/_workspace/src/github.com/codegangsta/cli/context_test.go index 63f2a70..b2d2412 100644 --- a/Godeps/_workspace/src/github.com/codegangsta/cli/context_test.go +++ b/Godeps/_workspace/src/github.com/codegangsta/cli/context_test.go @@ -2,8 +2,10 @@ package cli_test import ( "flag" - "github.com/codegangsta/cli" "testing" + "time" + + "github.com/codegangsta/cli" ) func TestNewContext(t *testing.T) { @@ -11,9 +13,12 @@ func TestNewContext(t *testing.T) { set.Int("myflag", 12, "doc") globalSet := flag.NewFlagSet("test", 0) globalSet.Int("myflag", 42, "doc") + command := cli.Command{Name: "mycommand"} c := cli.NewContext(nil, set, globalSet) + c.Command = command expect(t, c.Int("myflag"), 12) expect(t, c.GlobalInt("myflag"), 42) + expect(t, c.Command.Name, "mycommand") } func TestContext_Int(t *testing.T) { @@ -23,6 +28,13 @@ func TestContext_Int(t *testing.T) { expect(t, c.Int("myflag"), 12) } +func TestContext_Duration(t *testing.T) { + set := flag.NewFlagSet("test", 0) + set.Duration("myflag", time.Duration(12*time.Second), "doc") + c := cli.NewContext(nil, set, set) + expect(t, c.Duration("myflag"), time.Duration(12*time.Second)) +} + func TestContext_String(t *testing.T) { set := flag.NewFlagSet("test", 0) set.String("myflag", "hello world", "doc") @@ -37,6 +49,13 @@ func TestContext_Bool(t *testing.T) { expect(t, c.Bool("myflag"), false) } +func TestContext_BoolT(t *testing.T) { + set := flag.NewFlagSet("test", 0) + set.Bool("myflag", true, "doc") + c := cli.NewContext(nil, set, set) + expect(t, c.BoolT("myflag"), true) +} + func TestContext_Args(t *testing.T) { set := flag.NewFlagSet("test", 0) set.Bool("myflag", false, "doc") diff --git a/Godeps/_workspace/src/github.com/codegangsta/cli/flag.go b/Godeps/_workspace/src/github.com/codegangsta/cli/flag.go index 38c8c1c..b30bca3 100644 --- a/Godeps/_workspace/src/github.com/codegangsta/cli/flag.go +++ b/Godeps/_workspace/src/github.com/codegangsta/cli/flag.go @@ -3,10 +3,29 @@ package cli import ( "flag" "fmt" + "os" "strconv" "strings" + "time" ) +// This flag enables bash-completion for all commands and subcommands +var BashCompletionFlag = BoolFlag{ + Name: "generate-bash-completion", +} + +// This flag prints the version for the application +var VersionFlag = BoolFlag{ + Name: "version, v", + Usage: "print the version", +} + +// This flag prints the help for all commands and subcommands +var HelpFlag = BoolFlag{ + Name: "help, h", + Usage: "show help", +} + // Flag is a common interface related to parsing flags in cli. // For more advanced flag parsing techniques, it is recomended that // this interface be implemented. @@ -34,6 +53,41 @@ func eachName(longName string, fn func(string)) { } } +// Generic is a generic parseable type identified by a specific flag +type Generic interface { + Set(value string) error + String() string +} + +// GenericFlag is the flag type for types implementing Generic +type GenericFlag struct { + Name string + Value Generic + Usage string + EnvVar string +} + +func (f GenericFlag) String() string { + return withEnvHint(f.EnvVar, fmt.Sprintf("%s%s %v\t`%v` %s", prefixFor(f.Name), f.Name, f.Value, "-"+f.Name+" option -"+f.Name+" option", f.Usage)) +} + +func (f GenericFlag) Apply(set *flag.FlagSet) { + val := f.Value + if f.EnvVar != "" { + if envVal := os.Getenv(f.EnvVar); envVal != "" { + val.Set(envVal) + } + } + + eachName(f.Name, func(name string) { + set.Var(f.Value, name, f.Usage) + }) +} + +func (f GenericFlag) getName() string { + return f.Name +} + type StringSlice []string func (f *StringSlice) Set(value string) error { @@ -50,16 +104,29 @@ func (f *StringSlice) Value() []string { } type StringSliceFlag struct { - Name string - Value *StringSlice - Usage string + Name string + Value *StringSlice + Usage string + EnvVar string } func (f StringSliceFlag) String() string { - return fmt.Sprintf("%s%s %v\t`%v` %s", prefixFor(f.Name), f.Name, f.Value, "-"+f.Name+" option -"+f.Name+" option", f.Usage) + firstName := strings.Trim(strings.Split(f.Name, ",")[0], " ") + pref := prefixFor(firstName) + return withEnvHint(f.EnvVar, fmt.Sprintf("%s '%v'\t%v", prefixedNames(f.Name), pref+firstName+" option "+pref+firstName+" option", f.Usage)) } func (f StringSliceFlag) Apply(set *flag.FlagSet) { + if f.EnvVar != "" { + if envVal := os.Getenv(f.EnvVar); envVal != "" { + newVal := &StringSlice{} + for _, s := range strings.Split(envVal, ",") { + newVal.Set(s) + } + f.Value = newVal + } + } + eachName(f.Name, func(name string) { set.Var(f.Value, name, f.Usage) }) @@ -91,18 +158,32 @@ func (f *IntSlice) Value() []int { } type IntSliceFlag struct { - Name string - Value *IntSlice - Usage string + Name string + Value *IntSlice + Usage string + EnvVar string } func (f IntSliceFlag) String() string { firstName := strings.Trim(strings.Split(f.Name, ",")[0], " ") pref := prefixFor(firstName) - return fmt.Sprintf("%s '%v'\t%v", prefixedNames(f.Name), pref+firstName+" option "+pref+firstName+" option", f.Usage) + return withEnvHint(f.EnvVar, fmt.Sprintf("%s '%v'\t%v", prefixedNames(f.Name), pref+firstName+" option "+pref+firstName+" option", f.Usage)) } func (f IntSliceFlag) Apply(set *flag.FlagSet) { + if f.EnvVar != "" { + if envVal := os.Getenv(f.EnvVar); envVal != "" { + newVal := &IntSlice{} + for _, s := range strings.Split(envVal, ",") { + err := newVal.Set(s) + if err != nil { + fmt.Fprintf(os.Stderr, err.Error()) + } + } + f.Value = newVal + } + } + eachName(f.Name, func(name string) { set.Var(f.Value, name, f.Usage) }) @@ -113,17 +194,28 @@ func (f IntSliceFlag) getName() string { } type BoolFlag struct { - Name string - Usage string + Name string + Usage string + EnvVar string } func (f BoolFlag) String() string { - return fmt.Sprintf("%s\t%v", prefixedNames(f.Name), f.Usage) + return withEnvHint(f.EnvVar, fmt.Sprintf("%s\t%v", prefixedNames(f.Name), f.Usage)) } func (f BoolFlag) Apply(set *flag.FlagSet) { + val := false + if f.EnvVar != "" { + if envVal := os.Getenv(f.EnvVar); envVal != "" { + envValBool, err := strconv.ParseBool(envVal) + if err == nil { + val = envValBool + } + } + } + eachName(f.Name, func(name string) { - set.Bool(name, false, f.Usage) + set.Bool(name, val, f.Usage) }) } @@ -131,17 +223,63 @@ func (f BoolFlag) getName() string { return f.Name } +type BoolTFlag struct { + Name string + Usage string + EnvVar string +} + +func (f BoolTFlag) String() string { + return withEnvHint(f.EnvVar, fmt.Sprintf("%s\t%v", prefixedNames(f.Name), f.Usage)) +} + +func (f BoolTFlag) Apply(set *flag.FlagSet) { + val := true + if f.EnvVar != "" { + if envVal := os.Getenv(f.EnvVar); envVal != "" { + envValBool, err := strconv.ParseBool(envVal) + if err == nil { + val = envValBool + } + } + } + + eachName(f.Name, func(name string) { + set.Bool(name, val, f.Usage) + }) +} + +func (f BoolTFlag) getName() string { + return f.Name +} + type StringFlag struct { - Name string - Value string - Usage string + Name string + Value string + Usage string + EnvVar string } func (f StringFlag) String() string { - return fmt.Sprintf("%s '%v'\t%v", prefixedNames(f.Name), f.Value, f.Usage) + var fmtString string + fmtString = "%s %v\t%v" + + if len(f.Value) > 0 { + fmtString = "%s '%v'\t%v" + } else { + fmtString = "%s %v\t%v" + } + + return withEnvHint(f.EnvVar, fmt.Sprintf(fmtString, prefixedNames(f.Name), f.Value, f.Usage)) } func (f StringFlag) Apply(set *flag.FlagSet) { + if f.EnvVar != "" { + if envVal := os.Getenv(f.EnvVar); envVal != "" { + f.Value = envVal + } + } + eachName(f.Name, func(name string) { set.String(name, f.Value, f.Usage) }) @@ -152,16 +290,26 @@ func (f StringFlag) getName() string { } type IntFlag struct { - Name string - Value int - Usage string + Name string + Value int + Usage string + EnvVar string } func (f IntFlag) String() string { - return fmt.Sprintf("%s '%v'\t%v", prefixedNames(f.Name), f.Value, f.Usage) + return withEnvHint(f.EnvVar, fmt.Sprintf("%s '%v'\t%v", prefixedNames(f.Name), f.Value, f.Usage)) } func (f IntFlag) Apply(set *flag.FlagSet) { + if f.EnvVar != "" { + if envVal := os.Getenv(f.EnvVar); envVal != "" { + envValInt, err := strconv.ParseUint(envVal, 10, 64) + if err == nil { + f.Value = int(envValInt) + } + } + } + eachName(f.Name, func(name string) { set.Int(name, f.Value, f.Usage) }) @@ -171,17 +319,57 @@ func (f IntFlag) getName() string { return f.Name } +type DurationFlag struct { + Name string + Value time.Duration + Usage string + EnvVar string +} + +func (f DurationFlag) String() string { + return withEnvHint(f.EnvVar, fmt.Sprintf("%s '%v'\t%v", prefixedNames(f.Name), f.Value, f.Usage)) +} + +func (f DurationFlag) Apply(set *flag.FlagSet) { + if f.EnvVar != "" { + if envVal := os.Getenv(f.EnvVar); envVal != "" { + envValDuration, err := time.ParseDuration(envVal) + if err == nil { + f.Value = envValDuration + } + } + } + + eachName(f.Name, func(name string) { + set.Duration(name, f.Value, f.Usage) + }) +} + +func (f DurationFlag) getName() string { + return f.Name +} + type Float64Flag struct { - Name string - Value float64 - Usage string + Name string + Value float64 + Usage string + EnvVar string } func (f Float64Flag) String() string { - return fmt.Sprintf("%s '%v'\t%v", prefixedNames(f.Name), f.Value, f.Usage) + return withEnvHint(f.EnvVar, fmt.Sprintf("%s '%v'\t%v", prefixedNames(f.Name), f.Value, f.Usage)) } func (f Float64Flag) Apply(set *flag.FlagSet) { + if f.EnvVar != "" { + if envVal := os.Getenv(f.EnvVar); envVal != "" { + envValFloat, err := strconv.ParseFloat(envVal, 10) + if err == nil { + f.Value = float64(envValFloat) + } + } + } + eachName(f.Name, func(name string) { set.Float64(name, f.Value, f.Usage) }) @@ -212,3 +400,11 @@ func prefixedNames(fullName string) (prefixed string) { } return } + +func withEnvHint(envVar, str string) string { + envText := "" + if envVar != "" { + envText = fmt.Sprintf(" [$%s]", envVar) + } + return str + envText +} diff --git a/Godeps/_workspace/src/github.com/codegangsta/cli/flag_test.go b/Godeps/_workspace/src/github.com/codegangsta/cli/flag_test.go index 28e4582..bc5059c 100644 --- a/Godeps/_workspace/src/github.com/codegangsta/cli/flag_test.go +++ b/Godeps/_workspace/src/github.com/codegangsta/cli/flag_test.go @@ -1,8 +1,13 @@ package cli_test import ( - "github.com/codegangsta/cli" + "fmt" + "os" + "reflect" + "strings" "testing" + + "github.com/codegangsta/cli" ) var boolFlagTests = []struct { @@ -27,16 +32,19 @@ func TestBoolFlagHelpOutput(t *testing.T) { var stringFlagTests = []struct { name string + value string expected string }{ - {"help", "--help ''\t"}, - {"h", "-h ''\t"}, + {"help", "", "--help \t"}, + {"h", "", "-h \t"}, + {"h", "", "-h \t"}, + {"test", "Something", "--test 'Something'\t"}, } func TestStringFlagHelpOutput(t *testing.T) { for _, test := range stringFlagTests { - flag := cli.StringFlag{Name: test.name} + flag := cli.StringFlag{Name: test.name, Value: test.value} output := flag.String() if output != test.expected { @@ -45,6 +53,71 @@ func TestStringFlagHelpOutput(t *testing.T) { } } +func TestStringFlagWithEnvVarHelpOutput(t *testing.T) { + + os.Setenv("APP_FOO", "derp") + for _, test := range stringFlagTests { + flag := cli.StringFlag{Name: test.name, Value: test.value, EnvVar: "APP_FOO"} + output := flag.String() + + if !strings.HasSuffix(output, " [$APP_FOO]") { + t.Errorf("%s does not end with [$APP_FOO]", output) + } + } +} + +var stringSliceFlagTests = []struct { + name string + value *cli.StringSlice + expected string +}{ + {"help", func() *cli.StringSlice { + s := &cli.StringSlice{} + s.Set("") + return s + }(), "--help '--help option --help option'\t"}, + {"h", func() *cli.StringSlice { + s := &cli.StringSlice{} + s.Set("") + return s + }(), "-h '-h option -h option'\t"}, + {"h", func() *cli.StringSlice { + s := &cli.StringSlice{} + s.Set("") + return s + }(), "-h '-h option -h option'\t"}, + {"test", func() *cli.StringSlice { + s := &cli.StringSlice{} + s.Set("Something") + return s + }(), "--test '--test option --test option'\t"}, +} + +func TestStringSliceFlagHelpOutput(t *testing.T) { + + for _, test := range stringSliceFlagTests { + flag := cli.StringSliceFlag{Name: test.name, Value: test.value} + output := flag.String() + + if output != test.expected { + t.Errorf("%q does not match %q", output, test.expected) + } + } +} + +func TestStringSliceFlagWithEnvVarHelpOutput(t *testing.T) { + + os.Setenv("APP_QWWX", "11,4") + for _, test := range stringSliceFlagTests { + flag := cli.StringSliceFlag{Name: test.name, Value: test.value, EnvVar: "APP_QWWX"} + output := flag.String() + + if !strings.HasSuffix(output, " [$APP_QWWX]") { + t.Errorf("%q does not end with [$APP_QWWX]", output) + } + } +} + var intFlagTests = []struct { name string expected string @@ -65,6 +138,92 @@ func TestIntFlagHelpOutput(t *testing.T) { } } +func TestIntFlagWithEnvVarHelpOutput(t *testing.T) { + + os.Setenv("APP_BAR", "2") + for _, test := range intFlagTests { + flag := cli.IntFlag{Name: test.name, EnvVar: "APP_BAR"} + output := flag.String() + + if !strings.HasSuffix(output, " [$APP_BAR]") { + t.Errorf("%s does not end with [$APP_BAR]", output) + } + } +} + +var durationFlagTests = []struct { + name string + expected string +}{ + {"help", "--help '0'\t"}, + {"h", "-h '0'\t"}, +} + +func TestDurationFlagHelpOutput(t *testing.T) { + + for _, test := range durationFlagTests { + flag := cli.DurationFlag{Name: test.name} + output := flag.String() + + if output != test.expected { + t.Errorf("%s does not match %s", output, test.expected) + } + } +} + +func TestDurationFlagWithEnvVarHelpOutput(t *testing.T) { + + os.Setenv("APP_BAR", "2h3m6s") + for _, test := range durationFlagTests { + flag := cli.DurationFlag{Name: test.name, EnvVar: "APP_BAR"} + output := flag.String() + + if !strings.HasSuffix(output, " [$APP_BAR]") { + t.Errorf("%s does not end with [$APP_BAR]", output) + } + } +} + +var intSliceFlagTests = []struct { + name string + value *cli.IntSlice + expected string +}{ + {"help", &cli.IntSlice{}, "--help '--help option --help option'\t"}, + {"h", &cli.IntSlice{}, "-h '-h option -h option'\t"}, + {"h", &cli.IntSlice{}, "-h '-h option -h option'\t"}, + {"test", func() *cli.IntSlice { + i := &cli.IntSlice{} + i.Set("9") + return i + }(), "--test '--test option --test option'\t"}, +} + +func TestIntSliceFlagHelpOutput(t *testing.T) { + + for _, test := range intSliceFlagTests { + flag := cli.IntSliceFlag{Name: test.name, Value: test.value} + output := flag.String() + + if output != test.expected { + t.Errorf("%q does not match %q", output, test.expected) + } + } +} + +func TestIntSliceFlagWithEnvVarHelpOutput(t *testing.T) { + + os.Setenv("APP_SMURF", "42,3") + for _, test := range intSliceFlagTests { + flag := cli.IntSliceFlag{Name: test.name, Value: test.value, EnvVar: "APP_SMURF"} + output := flag.String() + + if !strings.HasSuffix(output, " [$APP_SMURF]") { + t.Errorf("%q does not end with [$APP_SMURF]", output) + } + } +} + var float64FlagTests = []struct { name string expected string @@ -85,6 +244,54 @@ func TestFloat64FlagHelpOutput(t *testing.T) { } } +func TestFloat64FlagWithEnvVarHelpOutput(t *testing.T) { + + os.Setenv("APP_BAZ", "99.4") + for _, test := range float64FlagTests { + flag := cli.Float64Flag{Name: test.name, EnvVar: "APP_BAZ"} + output := flag.String() + + if !strings.HasSuffix(output, " [$APP_BAZ]") { + t.Errorf("%s does not end with [$APP_BAZ]", output) + } + } +} + +var genericFlagTests = []struct { + name string + value cli.Generic + expected string +}{ + {"help", &Parser{}, "--help \t`-help option -help option` "}, + {"h", &Parser{}, "-h \t`-h option -h option` "}, + {"test", &Parser{}, "--test \t`-test option -test option` "}, +} + +func TestGenericFlagHelpOutput(t *testing.T) { + + for _, test := range genericFlagTests { + flag := cli.GenericFlag{Name: test.name} + output := flag.String() + + if output != test.expected { + t.Errorf("%q does not match %q", output, test.expected) + } + } +} + +func TestGenericFlagWithEnvVarHelpOutput(t *testing.T) { + + os.Setenv("APP_ZAP", "3") + for _, test := range genericFlagTests { + flag := cli.GenericFlag{Name: test.name, EnvVar: "APP_ZAP"} + output := flag.String() + + if !strings.HasSuffix(output, " [$APP_ZAP]") { + t.Errorf("%s does not end with [$APP_ZAP]", output) + } + } +} + func TestParseMultiString(t *testing.T) { (&cli.App{ Flags: []cli.Flag{ @@ -101,6 +308,57 @@ func TestParseMultiString(t *testing.T) { }).Run([]string{"run", "-s", "10"}) } +func TestParseMultiStringFromEnv(t *testing.T) { + os.Setenv("APP_COUNT", "20") + (&cli.App{ + Flags: []cli.Flag{ + cli.StringFlag{Name: "count, c", EnvVar: "APP_COUNT"}, + }, + Action: func(ctx *cli.Context) { + if ctx.String("count") != "20" { + t.Errorf("main name not set") + } + if ctx.String("c") != "20" { + t.Errorf("short name not set") + } + }, + }).Run([]string{"run"}) +} + +func TestParseMultiStringSlice(t *testing.T) { + (&cli.App{ + Flags: []cli.Flag{ + cli.StringSliceFlag{Name: "serve, s", Value: &cli.StringSlice{}}, + }, + Action: func(ctx *cli.Context) { + if !reflect.DeepEqual(ctx.StringSlice("serve"), []string{"10", "20"}) { + t.Errorf("main name not set") + } + if !reflect.DeepEqual(ctx.StringSlice("s"), []string{"10", "20"}) { + t.Errorf("short name not set") + } + }, + }).Run([]string{"run", "-s", "10", "-s", "20"}) +} + +func TestParseMultiStringSliceFromEnv(t *testing.T) { + os.Setenv("APP_INTERVALS", "20,30,40") + + (&cli.App{ + Flags: []cli.Flag{ + cli.StringSliceFlag{Name: "intervals, i", Value: &cli.StringSlice{}, EnvVar: "APP_INTERVALS"}, + }, + Action: func(ctx *cli.Context) { + if !reflect.DeepEqual(ctx.StringSlice("intervals"), []string{"20", "30", "40"}) { + t.Errorf("main name not set from env") + } + if !reflect.DeepEqual(ctx.StringSlice("i"), []string{"20", "30", "40"}) { + t.Errorf("short name not set from env") + } + }, + }).Run([]string{"run"}) +} + func TestParseMultiInt(t *testing.T) { a := cli.App{ Flags: []cli.Flag{ @@ -118,6 +376,93 @@ func TestParseMultiInt(t *testing.T) { a.Run([]string{"run", "-s", "10"}) } +func TestParseMultiIntFromEnv(t *testing.T) { + os.Setenv("APP_TIMEOUT_SECONDS", "10") + a := cli.App{ + Flags: []cli.Flag{ + cli.IntFlag{Name: "timeout, t", EnvVar: "APP_TIMEOUT_SECONDS"}, + }, + Action: func(ctx *cli.Context) { + if ctx.Int("timeout") != 10 { + t.Errorf("main name not set") + } + if ctx.Int("t") != 10 { + t.Errorf("short name not set") + } + }, + } + a.Run([]string{"run"}) +} + +func TestParseMultiIntSlice(t *testing.T) { + (&cli.App{ + Flags: []cli.Flag{ + cli.IntSliceFlag{Name: "serve, s", Value: &cli.IntSlice{}}, + }, + Action: func(ctx *cli.Context) { + if !reflect.DeepEqual(ctx.IntSlice("serve"), []int{10, 20}) { + t.Errorf("main name not set") + } + if !reflect.DeepEqual(ctx.IntSlice("s"), []int{10, 20}) { + t.Errorf("short name not set") + } + }, + }).Run([]string{"run", "-s", "10", "-s", "20"}) +} + +func TestParseMultiIntSliceFromEnv(t *testing.T) { + os.Setenv("APP_INTERVALS", "20,30,40") + + (&cli.App{ + Flags: []cli.Flag{ + cli.IntSliceFlag{Name: "intervals, i", Value: &cli.IntSlice{}, EnvVar: "APP_INTERVALS"}, + }, + Action: func(ctx *cli.Context) { + if !reflect.DeepEqual(ctx.IntSlice("intervals"), []int{20, 30, 40}) { + t.Errorf("main name not set from env") + } + if !reflect.DeepEqual(ctx.IntSlice("i"), []int{20, 30, 40}) { + t.Errorf("short name not set from env") + } + }, + }).Run([]string{"run"}) +} + +func TestParseMultiFloat64(t *testing.T) { + a := cli.App{ + Flags: []cli.Flag{ + cli.Float64Flag{Name: "serve, s"}, + }, + Action: func(ctx *cli.Context) { + if ctx.Float64("serve") != 10.2 { + t.Errorf("main name not set") + } + if ctx.Float64("s") != 10.2 { + t.Errorf("short name not set") + } + }, + } + a.Run([]string{"run", "-s", "10.2"}) +} + +func TestParseMultiFloat64FromEnv(t *testing.T) { + os.Setenv("APP_TIMEOUT_SECONDS", "15.5") + a := cli.App{ + Flags: []cli.Flag{ + cli.Float64Flag{Name: "timeout, t", EnvVar: "APP_TIMEOUT_SECONDS"}, + }, + Action: func(ctx *cli.Context) { + if ctx.Float64("timeout") != 15.5 { + t.Errorf("main name not set") + } + if ctx.Float64("t") != 15.5 { + t.Errorf("short name not set") + } + }, + } + a.Run([]string{"run"}) +} + func TestParseMultiBool(t *testing.T) { a := cli.App{ Flags: []cli.Flag{ @@ -134,3 +479,109 @@ func TestParseMultiBool(t *testing.T) { } a.Run([]string{"run", "--serve"}) } + +func TestParseMultiBoolFromEnv(t *testing.T) { + os.Setenv("APP_DEBUG", "1") + a := cli.App{ + Flags: []cli.Flag{ + cli.BoolFlag{Name: "debug, d", EnvVar: "APP_DEBUG"}, + }, + Action: func(ctx *cli.Context) { + if ctx.Bool("debug") != true { + t.Errorf("main name not set from env") + } + if ctx.Bool("d") != true { + t.Errorf("short name not set from env") + } + }, + } + a.Run([]string{"run"}) +} + +func TestParseMultiBoolT(t *testing.T) { + a := cli.App{ + Flags: []cli.Flag{ + cli.BoolTFlag{Name: "serve, s"}, + }, + Action: func(ctx *cli.Context) { + if ctx.BoolT("serve") != true { + t.Errorf("main name not set") + } + if ctx.BoolT("s") != true { + t.Errorf("short name not set") + } + }, + } + a.Run([]string{"run", "--serve"}) +} + +func TestParseMultiBoolTFromEnv(t *testing.T) { + os.Setenv("APP_DEBUG", "0") + a := cli.App{ + Flags: []cli.Flag{ + cli.BoolTFlag{Name: "debug, d", EnvVar: "APP_DEBUG"}, + }, + Action: func(ctx *cli.Context) { + if ctx.BoolT("debug") != false { + t.Errorf("main name not set from env") + } + if ctx.BoolT("d") != false { + t.Errorf("short name not set from env") + } + }, + } + a.Run([]string{"run"}) +} + +type Parser [2]string + +func (p *Parser) Set(value string) error { + parts := strings.Split(value, ",") + if len(parts) != 2 { + return fmt.Errorf("invalid format") + } + + (*p)[0] = parts[0] + (*p)[1] = parts[1] + + return nil +} + +func (p *Parser) String() string { + return fmt.Sprintf("%s,%s", p[0], p[1]) +} + +func TestParseGeneric(t *testing.T) { + a := cli.App{ + Flags: []cli.Flag{ + cli.GenericFlag{Name: "serve, s", Value: &Parser{}}, + }, + Action: func(ctx *cli.Context) { + if !reflect.DeepEqual(ctx.Generic("serve"), &Parser{"10", "20"}) { + t.Errorf("main name not set") + } + if !reflect.DeepEqual(ctx.Generic("s"), &Parser{"10", "20"}) { + t.Errorf("short name not set") + } + }, + } + a.Run([]string{"run", "-s", "10,20"}) +} + +func TestParseGenericFromEnv(t *testing.T) { + os.Setenv("APP_SERVE", "20,30") + a := cli.App{ + Flags: []cli.Flag{ + cli.GenericFlag{Name: "serve, s", Value: &Parser{}, EnvVar: "APP_SERVE"}, + }, + Action: func(ctx *cli.Context) { + if !reflect.DeepEqual(ctx.Generic("serve"), &Parser{"20", "30"}) { + t.Errorf("main name not set from env") + } + if !reflect.DeepEqual(ctx.Generic("s"), &Parser{"20", "30"}) { + t.Errorf("short name not set from env") + } + }, + } + a.Run([]string{"run"}) +} diff --git a/Godeps/_workspace/src/github.com/codegangsta/cli/help.go b/Godeps/_workspace/src/github.com/codegangsta/cli/help.go index 094d01d..5020cb6 100644 --- a/Godeps/_workspace/src/github.com/codegangsta/cli/help.go +++ b/Godeps/_workspace/src/github.com/codegangsta/cli/help.go @@ -14,17 +14,21 @@ var AppHelpTemplate = `NAME: {{.Name}} - {{.Usage}} USAGE: - {{.Name}} [global options] command [command options] [arguments...] + {{.Name}} {{if .Flags}}[global options] {{end}}command{{if .Flags}} [command options]{{end}} [arguments...] VERSION: - {{.Version}} + {{.Version}}{{if or .Author .Email}} + +AUTHOR:{{if .Author}} + {{.Author}}{{if .Email}} - <{{.Email}}>{{end}}{{else}} + {{.Email}}{{end}}{{end}} COMMANDS: {{range .Commands}}{{.Name}}{{with .ShortName}}, {{.}}{{end}}{{ "\t" }}{{.Usage}} - {{end}} + {{end}}{{if .Flags}} GLOBAL OPTIONS: {{range .Flags}}{{.}} - {{end}} + {{end}}{{end}} ` // The text template for the command help topic. @@ -34,14 +38,31 @@ var CommandHelpTemplate = `NAME: {{.Name}} - {{.Usage}} USAGE: - command {{.Name}} [command options] [arguments...] + command {{.Name}}{{if .Flags}} [command options]{{end}} [arguments...]{{if .Description}} DESCRIPTION: - {{.Description}} + {{.Description}}{{end}}{{if .Flags}} + +OPTIONS: + {{range .Flags}}{{.}} + {{end}}{{ end }} +` + +// The text template for the subcommand help topic. +// cli.go uses text/template to render templates. You can +// render custom help text by setting this variable. +var SubcommandHelpTemplate = `NAME: + {{.Name}} - {{.Usage}} + +USAGE: + {{.Name}} command{{if .Flags}} [command options]{{end}} [arguments...] +COMMANDS: + {{range .Commands}}{{.Name}}{{with .ShortName}}, {{.}}{{end}}{{ "\t" }}{{.Usage}} + {{end}}{{if .Flags}} OPTIONS: {{range .Flags}}{{.}} - {{end}} + {{end}}{{end}} ` var helpCommand = Command{ @@ -58,29 +79,86 @@ var helpCommand = Command{ }, } +var helpSubcommand = Command{ + Name: "help", + ShortName: "h", + Usage: "Shows a list of commands or help for one command", + Action: func(c *Context) { + args := c.Args() + if args.Present() { + ShowCommandHelp(c, args.First()) + } else { + ShowSubcommandHelp(c) + } + }, +} + // Prints help for the App var HelpPrinter = printHelp + +// Prints version for the App +var VersionPrinter = printVersion + func ShowAppHelp(c *Context) { HelpPrinter(AppHelpTemplate, c.App) } +// Prints the list of subcommands as the default app completion method +func DefaultAppComplete(c *Context) { + for _, command := range c.App.Commands { + fmt.Println(command.Name) + if command.ShortName != "" { + fmt.Println(command.ShortName) + } + } +} + // Prints help for the given command func ShowCommandHelp(c *Context, command string) { for _, c := range c.App.Commands { if c.HasName(command) { - printHelp(CommandHelpTemplate, c) + HelpPrinter(CommandHelpTemplate, c) return } } - fmt.Printf("No help topic for '%v'\n", command) + if c.App.CommandNotFound != nil { + c.App.CommandNotFound(c, command) + } else { + fmt.Printf("No help topic for '%v'\n", command) + } +} + +// Prints help for the given subcommand +func ShowSubcommandHelp(c *Context) { + HelpPrinter(SubcommandHelpTemplate, c.App) } // Prints the version number of the App func ShowVersion(c *Context) { + VersionPrinter(c) +} + +func printVersion(c *Context) { fmt.Printf("%v version %v\n", c.App.Name, c.App.Version) } +// Prints the lists of commands within a given context +func ShowCompletions(c *Context) { + a := c.App + if a != nil && a.BashComplete != nil { + a.BashComplete(c) + } +} + +// Prints the custom completions for a given command +func ShowCommandCompletions(ctx *Context, command string) { + c := ctx.App.Command(command) + if c != nil && c.BashComplete != nil { + c.BashComplete(ctx) + } +} + func printHelp(templ string, data interface{}) { w := tabwriter.NewWriter(os.Stdout, 0, 8, 1, '\t', 0) t := template.Must(template.New("help").Parse(templ)) @@ -117,3 +195,30 @@ func checkCommandHelp(c *Context, name string) bool { return false } + +func checkSubcommandHelp(c *Context) bool { + if c.GlobalBool("h") || c.GlobalBool("help") { + ShowSubcommandHelp(c) + return true + } + + return false +} + +func checkCompletions(c *Context) bool { + if c.GlobalBool(BashCompletionFlag.Name) && c.App.EnableBashCompletion { + ShowCompletions(c) + return true + } + + return false +} + +func checkCommandCompletions(c *Context, name string) bool { + if c.Bool(BashCompletionFlag.Name) && c.App.EnableBashCompletion { + ShowCommandCompletions(c, name) + return true + } + + return false +} diff --git a/Godeps/_workspace/src/github.com/oleiade/tempura/.gitignore b/Godeps/_workspace/src/github.com/oleiade/tempura/.gitignore new file mode 100644 index 0000000..8365624 --- /dev/null +++ b/Godeps/_workspace/src/github.com/oleiade/tempura/.gitignore @@ -0,0 +1,23 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test diff --git a/Godeps/_workspace/src/github.com/Sirupsen/logrus/LICENSE b/Godeps/_workspace/src/github.com/oleiade/tempura/LICENSE similarity index 88% rename from Godeps/_workspace/src/github.com/Sirupsen/logrus/LICENSE rename to Godeps/_workspace/src/github.com/oleiade/tempura/LICENSE index 57daf58..893ee29 100644 --- a/Godeps/_workspace/src/github.com/Sirupsen/logrus/LICENSE +++ b/Godeps/_workspace/src/github.com/oleiade/tempura/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2012 Simon Eskildsen +Copyright (c) 2014 Théo Crevon Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -9,13 +9,13 @@ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/Godeps/_workspace/src/github.com/oleiade/tempura/README.md b/Godeps/_workspace/src/github.com/oleiade/tempura/README.md new file mode 100644 index 0000000..f472063 --- /dev/null +++ b/Godeps/_workspace/src/github.com/oleiade/tempura/README.md @@ -0,0 +1,4 @@ +tempura +======= + +Temporary files creation and manipulation helpers for Go diff --git a/Godeps/_workspace/src/github.com/oleiade/tempura/tempura.go b/Godeps/_workspace/src/github.com/oleiade/tempura/tempura.go new file mode 100644 index 0000000..582ff16 --- /dev/null +++ b/Godeps/_workspace/src/github.com/oleiade/tempura/tempura.go @@ -0,0 +1,66 @@ +// Package tempura provides temporary files creation and manipulation helpers +// for the purposes of enhancing tests involving files creation. +package tempura + +import ( + "io/ioutil" + "os" +) + +// TempFile represents an open temporary file descriptor +type TempFile struct { + *os.File + + dir string + prefix string +} + +// FromBytes creates a new temporary file in the directory dir with a name beginning with prefix, +// opens the file for reading and writing, writes the provided data into it +// and returns seeks the underlying file object to 0. +// +// If dir is the empty string, TempFile uses the default directory for temporary files (see os.TempDir). +// Multiple programs calling TempFile simultaneously will not choose the same file. +// The caller can use f.Name() to find the pathname of the file. +// It is the caller's responsibility to remove the file when no longer needed. +func FromBytes(dir, prefix string, data []byte) (*TempFile, error) { + var tmp *TempFile = &TempFile{dir: dir, prefix: prefix} + var err error + + tmp.File, err = ioutil.TempFile(dir, prefix) + if err != nil { + return nil, err + } + + _, err = tmp.Write(data) + if err != nil { + return nil, err + } + + tmp.Seek(0, 0) + + return tmp, nil +} + +// Create a new temporary file in the directory dir with a name beginning with prefix, +// writes the provided data into it and returns it's path. +// +// If dir is the empty string, TempFile uses the default directory for temporary files (see os.TempDir). +// Multiple programs calling TempFile simultaneously will not choose the same file. +// It is the caller's responsibility to remove the file when no longer needed. +func Create(dir, prefix string, data []byte) (path string, err error) { + var tmp *os.File + + tmp, err = ioutil.TempFile(dir, prefix) + if err != nil { + return "", err + } + defer tmp.Close() + + _, err = tmp.Write(data) + if err != nil { + return "", err + } + + return tmp.Name(), nil +} diff --git a/Godeps/_workspace/src/github.com/oleiade/tempura/tempura_test.go b/Godeps/_workspace/src/github.com/oleiade/tempura/tempura_test.go new file mode 100644 index 0000000..f61d8a6 --- /dev/null +++ b/Godeps/_workspace/src/github.com/oleiade/tempura/tempura_test.go @@ -0,0 +1,75 @@ +package tempura + +import ( + "fmt" + "io/ioutil" + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +func ExampleFromBytes() { + var tmp *TempFile + var err error + + // Creates a temporary file in /tmp dir, prefixed with "test_" + // and containing three bytes "a", "b", "c" + tmp, err = FromBytes("/tmp", "test_", []byte{'a', 'b', 'c'}) + if err != nil { + // Handle err + } + defer tmp.Close() + defer os.Remove(tmp.Name()) + + // The tmp file descriptor is seeked to 0 and ready to be read/written + data, err := tmp.Read(make([]byte, 3)) + if err != nil { + // Handle error + } + + fmt.Println(string(data)) + // Output: "abc" +} + +func TestFromBytes_creates_a_file(t *testing.T) { + tmp, err := FromBytes("/tmp", "test_tempura_", []byte{'a', 'b', 'c'}) + defer os.Remove(tmp.Name()) + assert.NoError(t, err) + + _, err = os.Stat(tmp.Name()) + assert.NoError(t, err) +} + +func TestFromBytes_returns_an_opened_fd(t *testing.T) { + tmp, _ := FromBytes("/tmp", "test_tempura_", []byte{'a', 'b', 'c'}) + defer os.Remove(tmp.Name()) + + _, err := tmp.Read([]byte{}) + assert.NoError(t, err) +} + +func TestFromBytes_returns_an_opened_seeked_to_zero_fd(t *testing.T) { + input := []byte{'a', 'b', 'c'} + output := make([]byte, 1) + + tmp, _ := FromBytes("/tmp", "test_tempura_", input) + defer os.Remove(tmp.Name()) + n, err := tmp.Read(output) + + assert.NoError(t, err) + assert.Equal(t, n, 1) + assert.Equal(t, output, []byte{'a'}) +} + +func TestCreate_returns_a_valid_path(t *testing.T) { + p, err := Create("/tmp", "test_tempura_", []byte{'a', 'b', 'c'}) + assert.NoError(t, err) + + _, err = os.Stat(p) + assert.NoError(t, err) + + data, err := ioutil.ReadFile(p) + assert.NoError(t, err) + assert.Equal(t, data, []byte{'a', 'b', 'c'}) +} diff --git a/Godeps/_workspace/src/github.com/stretchr/testify/assert/assertions.go b/Godeps/_workspace/src/github.com/stretchr/testify/assert/assertions.go new file mode 100644 index 0000000..72247d0 --- /dev/null +++ b/Godeps/_workspace/src/github.com/stretchr/testify/assert/assertions.go @@ -0,0 +1,537 @@ +package assert + +import ( + "fmt" + "reflect" + "runtime" + "strings" + "time" +) + +// TestingT is an interface wrapper around *testing.T +type TestingT interface { + Errorf(format string, args ...interface{}) +} + +// Comparison a custom function that returns true on success and false on failure +type Comparison func() (success bool) + +/* + Helper functions +*/ + +// ObjectsAreEqual determines if two objects are considered equal. +// +// This function does no assertion of any kind. +func ObjectsAreEqual(expected, actual interface{}) bool { + + if expected == nil || actual == nil { + return expected == actual + } + + if reflect.DeepEqual(expected, actual) { + return true + } + + expectedValue := reflect.ValueOf(expected) + actualValue := reflect.ValueOf(actual) + if expectedValue == actualValue { + return true + } + + // Attempt comparison after type conversion + if actualValue.Type().ConvertibleTo(expectedValue.Type()) && expectedValue == actualValue.Convert(expectedValue.Type()) { + return true + } + + // Last ditch effort + if fmt.Sprintf("%#v", expected) == fmt.Sprintf("%#v", actual) { + return true + } + + return false + +} + +/* CallerInfo is necessary because the assert functions use the testing object +internally, causing it to print the file:line of the assert method, rather than where +the problem actually occured in calling code.*/ + +// CallerInfo returns a string containing the file and line number of the assert call +// that failed. +func CallerInfo() string { + + file := "" + line := 0 + ok := false + + for i := 0; ; i++ { + _, file, line, ok = runtime.Caller(i) + if !ok { + return "" + } + parts := strings.Split(file, "/") + dir := parts[len(parts)-2] + file = parts[len(parts)-1] + if (dir != "assert" && dir != "mock") || file == "mock_test.go" { + break + } + } + + return fmt.Sprintf("%s:%d", file, line) +} + +// getWhitespaceString returns a string that is long enough to overwrite the default +// output from the go testing framework. +func getWhitespaceString() string { + + _, file, line, ok := runtime.Caller(1) + if !ok { + return "" + } + parts := strings.Split(file, "/") + file = parts[len(parts)-1] + + return strings.Repeat(" ", len(fmt.Sprintf("%s:%d: ", file, line))) + +} + +func messageFromMsgAndArgs(msgAndArgs ...interface{}) string { + if len(msgAndArgs) == 0 || msgAndArgs == nil { + return "" + } + if len(msgAndArgs) == 1 { + return msgAndArgs[0].(string) + } + if len(msgAndArgs) > 1 { + return fmt.Sprintf(msgAndArgs[0].(string), msgAndArgs[1:]...) + } + return "" +} + +// Fail reports a failure through +func Fail(t TestingT, failureMessage string, msgAndArgs ...interface{}) bool { + + message := messageFromMsgAndArgs(msgAndArgs...) + + if len(message) > 0 { + t.Errorf("\r%s\r\tLocation:\t%s\n\r\tError:\t\t%s\n\r\tMessages:\t%s\n\r", getWhitespaceString(), CallerInfo(), failureMessage, message) + } else { + t.Errorf("\r%s\r\tLocation:\t%s\n\r\tError:\t\t%s\n\r", getWhitespaceString(), CallerInfo(), failureMessage) + } + + return false +} + +// Implements asserts that an object is implemented by the specified interface. +// +// assert.Implements(t, (*MyInterface)(nil), new(MyObject), "MyObject") +func Implements(t TestingT, interfaceObject interface{}, object interface{}, msgAndArgs ...interface{}) bool { + + interfaceType := reflect.TypeOf(interfaceObject).Elem() + + if !reflect.TypeOf(object).Implements(interfaceType) { + return Fail(t, fmt.Sprintf("Object must implement %v", interfaceType), msgAndArgs...) + } + + return true + +} + +// IsType asserts that the specified objects are of the same type. +func IsType(t TestingT, expectedType interface{}, object interface{}, msgAndArgs ...interface{}) bool { + + if !ObjectsAreEqual(reflect.TypeOf(object), reflect.TypeOf(expectedType)) { + return Fail(t, fmt.Sprintf("Object expected to be of type %v, but was %v", reflect.TypeOf(expectedType), reflect.TypeOf(object)), msgAndArgs...) + } + + return true +} + +// Equal asserts that two objects are equal. +// +// assert.Equal(t, 123, 123, "123 and 123 should be equal") +// +// Returns whether the assertion was successful (true) or not (false). +func Equal(t TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool { + + if !ObjectsAreEqual(expected, actual) { + return Fail(t, fmt.Sprintf("Not equal: %#v != %#v", expected, actual), msgAndArgs...) + } + + return true + +} + +// Exactly asserts that two objects are equal is value and type. +// +// assert.Exactly(t, int32(123), int64(123), "123 and 123 should NOT be equal") +// +// Returns whether the assertion was successful (true) or not (false). +func Exactly(t TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool { + + aType := reflect.TypeOf(expected) + bType := reflect.TypeOf(actual) + + if aType != bType { + return Fail(t, "Types expected to match exactly", "%v != %v", aType, bType) + } + + return Equal(t, expected, actual, msgAndArgs...) + +} + +// NotNil asserts that the specified object is not nil. +// +// assert.NotNil(t, err, "err should be something") +// +// Returns whether the assertion was successful (true) or not (false). +func NotNil(t TestingT, object interface{}, msgAndArgs ...interface{}) bool { + + success := true + + if object == nil { + success = false + } else { + value := reflect.ValueOf(object) + kind := value.Kind() + if kind >= reflect.Chan && kind <= reflect.Slice && value.IsNil() { + success = false + } + } + + if !success { + Fail(t, "Expected not to be nil.", msgAndArgs...) + } + + return success +} + +// isNil checks if a specified object is nil or not, without Failing. +func isNil(object interface{}) bool { + if object == nil { + return true + } + + value := reflect.ValueOf(object) + kind := value.Kind() + if kind >= reflect.Chan && kind <= reflect.Slice && value.IsNil() { + return true + } + + return false +} + +// Nil asserts that the specified object is nil. +// +// assert.Nil(t, err, "err should be nothing") +// +// Returns whether the assertion was successful (true) or not (false). +func Nil(t TestingT, object interface{}, msgAndArgs ...interface{}) bool { + if isNil(object) { + return true + } + return Fail(t, fmt.Sprintf("Expected nil, but got: %#v", object), msgAndArgs...) +} + +var zeros = []interface{}{ + int(0), + int8(0), + int16(0), + int32(0), + int64(0), + uint(0), + uint8(0), + uint16(0), + uint32(0), + uint64(0), + float32(0), + float64(0), +} + +// isEmpty gets whether the specified object is considered empty or not. +func isEmpty(object interface{}) bool { + + if object == nil { + return true + } else if object == "" { + return true + } else if object == false { + return true + } + + for _, v := range zeros { + if object == v { + return true + } + } + + objValue := reflect.ValueOf(object) + + switch objValue.Kind() { + case reflect.Map: + fallthrough + case reflect.Slice, reflect.Chan: + { + return (objValue.Len() == 0) + } + case reflect.Ptr: + { + switch object.(type) { + case *time.Time: + return object.(*time.Time).IsZero() + default: + return false + } + } + } + return false +} + +// Empty asserts that the specified object is empty. I.e. nil, "", false, 0 or either +// a slice or a channel with len == 0. +// +// assert.Empty(t, obj) +// +// Returns whether the assertion was successful (true) or not (false). +func Empty(t TestingT, object interface{}, msgAndArgs ...interface{}) bool { + + pass := isEmpty(object) + if !pass { + Fail(t, fmt.Sprintf("Should be empty, but was %v", object), msgAndArgs...) + } + + return pass + +} + +// NotEmpty asserts that the specified object is NOT empty. I.e. not nil, "", false, 0 or either +// a slice or a channel with len == 0. +// +// if assert.NotEmpty(t, obj) { +// assert.Equal(t, "two", obj[1]) +// } +// +// Returns whether the assertion was successful (true) or not (false). +func NotEmpty(t TestingT, object interface{}, msgAndArgs ...interface{}) bool { + + pass := !isEmpty(object) + if !pass { + Fail(t, fmt.Sprintf("Should NOT be empty, but was %v", object), msgAndArgs...) + } + + return pass + +} + +// True asserts that the specified value is true. +// +// assert.True(t, myBool, "myBool should be true") +// +// Returns whether the assertion was successful (true) or not (false). +func True(t TestingT, value bool, msgAndArgs ...interface{}) bool { + + if value != true { + return Fail(t, "Should be true", msgAndArgs...) + } + + return true + +} + +// False asserts that the specified value is true. +// +// assert.False(t, myBool, "myBool should be false") +// +// Returns whether the assertion was successful (true) or not (false). +func False(t TestingT, value bool, msgAndArgs ...interface{}) bool { + + if value != false { + return Fail(t, "Should be false", msgAndArgs...) + } + + return true + +} + +// NotEqual asserts that the specified values are NOT equal. +// +// assert.NotEqual(t, obj1, obj2, "two objects shouldn't be equal") +// +// Returns whether the assertion was successful (true) or not (false). +func NotEqual(t TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool { + + if ObjectsAreEqual(expected, actual) { + return Fail(t, "Should not be equal", msgAndArgs...) + } + + return true + +} + +// Contains asserts that the specified string contains the specified substring. +// +// assert.Contains(t, "Hello World", "World", "But 'Hello World' does contain 'World'") +// +// Returns whether the assertion was successful (true) or not (false). +func Contains(t TestingT, s, contains string, msgAndArgs ...interface{}) bool { + + if !strings.Contains(s, contains) { + return Fail(t, fmt.Sprintf("\"%s\" does not contain \"%s\"", s, contains), msgAndArgs...) + } + + return true + +} + +// NotContains asserts that the specified string does NOT contain the specified substring. +// +// assert.NotContains(t, "Hello World", "Earth", "But 'Hello World' does NOT contain 'Earth'") +// +// Returns whether the assertion was successful (true) or not (false). +func NotContains(t TestingT, s, contains string, msgAndArgs ...interface{}) bool { + + if strings.Contains(s, contains) { + return Fail(t, fmt.Sprintf("\"%s\" should not contain \"%s\"", s, contains), msgAndArgs...) + } + + return true + +} + +// Condition uses a Comparison to assert a complex condition. +func Condition(t TestingT, comp Comparison, msgAndArgs ...interface{}) bool { + result := comp() + if !result { + Fail(t, "Condition failed!", msgAndArgs...) + } + return result +} + +// PanicTestFunc defines a func that should be passed to the assert.Panics and assert.NotPanics +// methods, and represents a simple func that takes no arguments, and returns nothing. +type PanicTestFunc func() + +// didPanic returns true if the function passed to it panics. Otherwise, it returns false. +func didPanic(f PanicTestFunc) (bool, interface{}) { + + didPanic := false + var message interface{} + func() { + + defer func() { + if message = recover(); message != nil { + didPanic = true + } + }() + + // call the target function + f() + + }() + + return didPanic, message + +} + +// Panics asserts that the code inside the specified PanicTestFunc panics. +// +// assert.Panics(t, func(){ +// GoCrazy() +// }, "Calling GoCrazy() should panic") +// +// Returns whether the assertion was successful (true) or not (false). +func Panics(t TestingT, f PanicTestFunc, msgAndArgs ...interface{}) bool { + + if funcDidPanic, panicValue := didPanic(f); !funcDidPanic { + return Fail(t, fmt.Sprintf("func %#v should panic\n\r\tPanic value:\t%v", f, panicValue), msgAndArgs...) + } + + return true +} + +// NotPanics asserts that the code inside the specified PanicTestFunc does NOT panic. +// +// assert.NotPanics(t, func(){ +// RemainCalm() +// }, "Calling RemainCalm() should NOT panic") +// +// Returns whether the assertion was successful (true) or not (false). +func NotPanics(t TestingT, f PanicTestFunc, msgAndArgs ...interface{}) bool { + + if funcDidPanic, panicValue := didPanic(f); funcDidPanic { + return Fail(t, fmt.Sprintf("func %#v should not panic\n\r\tPanic value:\t%v", f, panicValue), msgAndArgs...) + } + + return true +} + +// WithinDuration asserts that the two times are within duration delta of each other. +// +// assert.WithinDuration(t, time.Now(), time.Now(), 10*time.Second, "The difference should not be more than 10s") +// +// Returns whether the assertion was successful (true) or not (false). +func WithinDuration(t TestingT, expected, actual time.Time, delta time.Duration, msgAndArgs ...interface{}) bool { + + dt := expected.Sub(actual) + if dt < -delta || dt > delta { + return Fail(t, fmt.Sprintf("Max difference between %v and %v allowed is %v, but difference was %v", expected, actual, dt, delta), msgAndArgs...) + } + + return true +} + +/* + Errors +*/ + +// NoError asserts that a function returned no error (i.e. `nil`). +// +// actualObj, err := SomeFunction() +// if assert.NoError(t, err) { +// assert.Equal(t, actualObj, expectedObj) +// } +// +// Returns whether the assertion was successful (true) or not (false). +func NoError(t TestingT, err error, msgAndArgs ...interface{}) bool { + if isNil(err) { + return true + } + + return Fail(t, fmt.Sprintf("No error is expected but got %v", err), msgAndArgs...) +} + +// Error asserts that a function returned an error (i.e. not `nil`). +// +// actualObj, err := SomeFunction() +// if assert.Error(t, err, "An error was expected") { +// assert.Equal(t, err, expectedError) +// } +// +// Returns whether the assertion was successful (true) or not (false). +func Error(t TestingT, err error, msgAndArgs ...interface{}) bool { + + message := messageFromMsgAndArgs(msgAndArgs...) + return NotNil(t, err, "An error is expected but got nil. %s", message) + +} + +// EqualError asserts that a function returned an error (i.e. not `nil`) +// and that it is equal to the provided error. +// +// actualObj, err := SomeFunction() +// if assert.Error(t, err, "An error was expected") { +// assert.Equal(t, err, expectedError) +// } +// +// Returns whether the assertion was successful (true) or not (false). +func EqualError(t TestingT, theError error, errString string, msgAndArgs ...interface{}) bool { + + message := messageFromMsgAndArgs(msgAndArgs...) + if !NotNil(t, theError, "An error is expected but got nil. %s", message) { + return false + } + s := "An error with value \"%s\" is expected but got \"%s\". %s" + return Equal(t, theError.Error(), errString, + s, errString, theError.Error(), message) +} diff --git a/Godeps/_workspace/src/github.com/stretchr/testify/assert/assertions_test.go b/Godeps/_workspace/src/github.com/stretchr/testify/assert/assertions_test.go new file mode 100644 index 0000000..d006169 --- /dev/null +++ b/Godeps/_workspace/src/github.com/stretchr/testify/assert/assertions_test.go @@ -0,0 +1,420 @@ +package assert + +import ( + "errors" + "testing" + "time" +) + +// AssertionTesterInterface defines an interface to be used for testing assertion methods +type AssertionTesterInterface interface { + TestMethod() +} + +// AssertionTesterConformingObject is an object that conforms to the AssertionTesterInterface interface +type AssertionTesterConformingObject struct { +} + +func (a *AssertionTesterConformingObject) TestMethod() { +} + +// AssertionTesterNonConformingObject is an object that does not conform to the AssertionTesterInterface interface +type AssertionTesterNonConformingObject struct { +} + +func TestObjectsAreEqual(t *testing.T) { + + if !ObjectsAreEqual("Hello World", "Hello World") { + t.Error("objectsAreEqual should return true") + } + if !ObjectsAreEqual(123, 123) { + t.Error("objectsAreEqual should return true") + } + if !ObjectsAreEqual(123.5, 123.5) { + t.Error("objectsAreEqual should return true") + } + if !ObjectsAreEqual([]byte("Hello World"), []byte("Hello World")) { + t.Error("objectsAreEqual should return true") + } + if !ObjectsAreEqual(nil, nil) { + t.Error("objectsAreEqual should return true") + } + +} + +func TestImplements(t *testing.T) { + + mockT := new(testing.T) + + if !Implements(mockT, (*AssertionTesterInterface)(nil), new(AssertionTesterConformingObject)) { + t.Error("Implements method should return true: AssertionTesterConformingObject implements AssertionTesterInterface") + } + if Implements(mockT, (*AssertionTesterInterface)(nil), new(AssertionTesterNonConformingObject)) { + t.Error("Implements method should return false: AssertionTesterNonConformingObject does not implements AssertionTesterInterface") + } + +} + +func TestIsType(t *testing.T) { + + mockT := new(testing.T) + + if !IsType(mockT, new(AssertionTesterConformingObject), new(AssertionTesterConformingObject)) { + t.Error("IsType should return true: AssertionTesterConformingObject is the same type as AssertionTesterConformingObject") + } + if IsType(mockT, new(AssertionTesterConformingObject), new(AssertionTesterNonConformingObject)) { + t.Error("IsType should return false: AssertionTesterConformingObject is not the same type as AssertionTesterNonConformingObject") + } + +} + +func TestEqual(t *testing.T) { + + mockT := new(testing.T) + + if !Equal(mockT, "Hello World", "Hello World") { + t.Error("Equal should return true") + } + if !Equal(mockT, 123, 123) { + t.Error("Equal should return true") + } + if !Equal(mockT, 123.5, 123.5) { + t.Error("Equal should return true") + } + if !Equal(mockT, []byte("Hello World"), []byte("Hello World")) { + t.Error("Equal should return true") + } + if !Equal(mockT, nil, nil) { + t.Error("Equal should return true") + } + if !Equal(mockT, int32(123), int64(123)) { + t.Error("Equal should return true") + } + if !Equal(mockT, int64(123), uint64(123)) { + t.Error("Equal should return true") + } + +} + +func TestNotNil(t *testing.T) { + + mockT := new(testing.T) + + if !NotNil(mockT, new(AssertionTesterConformingObject)) { + t.Error("NotNil should return true: object is not nil") + } + if NotNil(mockT, nil) { + t.Error("NotNil should return false: object is nil") + } + +} + +func TestNil(t *testing.T) { + + mockT := new(testing.T) + + if !Nil(mockT, nil) { + t.Error("Nil should return true: object is nil") + } + if Nil(mockT, new(AssertionTesterConformingObject)) { + t.Error("Nil should return false: object is not nil") + } + +} + +func TestTrue(t *testing.T) { + + mockT := new(testing.T) + + if !True(mockT, true) { + t.Error("True should return true") + } + if True(mockT, false) { + t.Error("True should return false") + } + +} + +func TestFalse(t *testing.T) { + + mockT := new(testing.T) + + if !False(mockT, false) { + t.Error("False should return true") + } + if False(mockT, true) { + t.Error("False should return false") + } + +} + +func TestExactly(t *testing.T) { + + mockT := new(testing.T) + + a := float32(1) + b := float64(1) + c := float32(1) + d := float32(2) + + if Exactly(mockT, a, b) { + t.Error("Exactly should return false") + } + if Exactly(mockT, a, d) { + t.Error("Exactly should return false") + } + if !Exactly(mockT, a, c) { + t.Error("Exactly should return true") + } + + if Exactly(mockT, nil, a) { + t.Error("Exactly should return false") + } + if Exactly(mockT, a, nil) { + t.Error("Exactly should return false") + } + +} + +func TestNotEqual(t *testing.T) { + + mockT := new(testing.T) + + if !NotEqual(mockT, "Hello World", "Hello World!") { + t.Error("NotEqual should return true") + } + if !NotEqual(mockT, 123, 1234) { + t.Error("NotEqual should return true") + } + if !NotEqual(mockT, 123.5, 123.55) { + t.Error("NotEqual should return true") + } + if !NotEqual(mockT, []byte("Hello World"), []byte("Hello World!")) { + t.Error("NotEqual should return true") + } + if !NotEqual(mockT, nil, new(AssertionTesterConformingObject)) { + t.Error("NotEqual should return true") + } +} + +func TestContains(t *testing.T) { + + mockT := new(testing.T) + + if !Contains(mockT, "Hello World", "Hello") { + t.Error("Contains should return true: \"Hello World\" contains \"Hello\"") + } + if Contains(mockT, "Hello World", "Salut") { + t.Error("Contains should return false: \"Hello World\" does not contain \"Salut\"") + } + +} + +func TestNotContains(t *testing.T) { + + mockT := new(testing.T) + + if !NotContains(mockT, "Hello World", "Hello!") { + t.Error("NotContains should return true: \"Hello World\" does not contain \"Hello!\"") + } + if NotContains(mockT, "Hello World", "Hello") { + t.Error("NotContains should return false: \"Hello World\" contains \"Hello\"") + } + +} + +func TestDidPanic(t *testing.T) { + + if funcDidPanic, _ := didPanic(func() { + panic("Panic!") + }); !funcDidPanic { + t.Error("didPanic should return true") + } + + if funcDidPanic, _ := didPanic(func() { + }); funcDidPanic { + t.Error("didPanic should return false") + } + +} + +func TestPanics(t *testing.T) { + + mockT := new(testing.T) + + if !Panics(mockT, func() { + panic("Panic!") + }) { + t.Error("Panics should return true") + } + + if Panics(mockT, func() { + }) { + t.Error("Panics should return false") + } + +} + +func TestNotPanics(t *testing.T) { + + mockT := new(testing.T) + + if !NotPanics(mockT, func() { + }) { + t.Error("NotPanics should return true") + } + + if NotPanics(mockT, func() { + panic("Panic!") + }) { + t.Error("NotPanics should return false") + } + +} + +func TestEqual_Funcs(t *testing.T) { + + type f func() int + f1 := func() int { return 1 } + f2 := func() int { return 2 } + + f1Copy := f1 + + Equal(t, f1Copy, f1, "Funcs are the same and should be considered equal") + NotEqual(t, f1, f2, "f1 and f2 are different") + +} + +func TestNoError(t *testing.T) { + + mockT := new(testing.T) + + // start with a nil error + var err error + + True(t, NoError(mockT, err), "NoError should return True for nil arg") + + // now set an error + err = errors.New("some error") + + False(t, NoError(mockT, err), "NoError with error should return False") + +} + +func TestError(t *testing.T) { + + mockT := new(testing.T) + + // start with a nil error + var err error + + False(t, Error(mockT, err), "Error should return False for nil arg") + + // now set an error + err = errors.New("some error") + + True(t, Error(mockT, err), "Error with error should return True") + +} + +func TestEqualError(t *testing.T) { + mockT := new(testing.T) + + // start with a nil error + var err error + False(t, EqualError(mockT, err, ""), + "EqualError should return false for nil arg") + + // now set an error + err = errors.New("some error") + False(t, EqualError(mockT, err, "Not some error"), + "EqualError should return false for different error string") + True(t, EqualError(mockT, err, "some error"), + "EqualError should return true") +} + +func Test_isEmpty(t *testing.T) { + + chWithValue := make(chan struct{}, 1) + chWithValue <- struct{}{} + + True(t, isEmpty("")) + True(t, isEmpty(nil)) + True(t, isEmpty([]string{})) + True(t, isEmpty(0)) + True(t, isEmpty(int32(0))) + True(t, isEmpty(int64(0))) + True(t, isEmpty(false)) + True(t, isEmpty(map[string]string{})) + True(t, isEmpty(new(time.Time))) + True(t, isEmpty(make(chan struct{}))) + False(t, isEmpty("something")) + False(t, isEmpty(errors.New("something"))) + False(t, isEmpty([]string{"something"})) + False(t, isEmpty(1)) + False(t, isEmpty(true)) + False(t, isEmpty(map[string]string{"Hello": "World"})) + False(t, isEmpty(chWithValue)) + +} + +func TestEmpty(t *testing.T) { + + mockT := new(testing.T) + chWithValue := make(chan struct{}, 1) + chWithValue <- struct{}{} + + True(t, Empty(mockT, ""), "Empty string is empty") + True(t, Empty(mockT, nil), "Nil is empty") + True(t, Empty(mockT, []string{}), "Empty string array is empty") + True(t, Empty(mockT, 0), "Zero int value is empty") + True(t, Empty(mockT, false), "False value is empty") + True(t, Empty(mockT, make(chan struct{})), "Channel without values is empty") + + False(t, Empty(mockT, "something"), "Non Empty string is not empty") + False(t, Empty(mockT, errors.New("something")), "Non nil object is not empty") + False(t, Empty(mockT, []string{"something"}), "Non empty string array is not empty") + False(t, Empty(mockT, 1), "Non-zero int value is not empty") + False(t, Empty(mockT, true), "True value is not empty") + False(t, Empty(mockT, chWithValue), "Channel with values is not empty") +} + +func TestNotEmpty(t *testing.T) { + + mockT := new(testing.T) + chWithValue := make(chan struct{}, 1) + chWithValue <- struct{}{} + + False(t, NotEmpty(mockT, ""), "Empty string is empty") + False(t, NotEmpty(mockT, nil), "Nil is empty") + False(t, NotEmpty(mockT, []string{}), "Empty string array is empty") + False(t, NotEmpty(mockT, 0), "Zero int value is empty") + False(t, NotEmpty(mockT, false), "False value is empty") + False(t, NotEmpty(mockT, make(chan struct{})), "Channel without values is empty") + + True(t, NotEmpty(mockT, "something"), "Non Empty string is not empty") + True(t, NotEmpty(mockT, errors.New("something")), "Non nil object is not empty") + True(t, NotEmpty(mockT, []string{"something"}), "Non empty string array is not empty") + True(t, NotEmpty(mockT, 1), "Non-zero int value is not empty") + True(t, NotEmpty(mockT, true), "True value is not empty") + True(t, NotEmpty(mockT, chWithValue), "Channel with values is not empty") +} + +func TestWithinDuration(t *testing.T) { + + mockT := new(testing.T) + a := time.Now() + b := a.Add(10 * time.Second) + + True(t, WithinDuration(mockT, a, b, 10*time.Second), "A 10s difference is within a 10s time difference") + True(t, WithinDuration(mockT, b, a, 10*time.Second), "A 10s difference is within a 10s time difference") + + False(t, WithinDuration(mockT, a, b, 9*time.Second), "A 10s difference is not within a 9s time difference") + False(t, WithinDuration(mockT, b, a, 9*time.Second), "A 10s difference is not within a 9s time difference") + + False(t, WithinDuration(mockT, a, b, -9*time.Second), "A 10s difference is not within a 9s time difference") + False(t, WithinDuration(mockT, b, a, -9*time.Second), "A 10s difference is not within a 9s time difference") + + False(t, WithinDuration(mockT, a, b, -11*time.Second), "A 10s difference is not within a 9s time difference") + False(t, WithinDuration(mockT, b, a, -11*time.Second), "A 10s difference is not within a 9s time difference") +} diff --git a/Godeps/_workspace/src/github.com/stretchr/testify/assert/doc.go b/Godeps/_workspace/src/github.com/stretchr/testify/assert/doc.go new file mode 100644 index 0000000..25f699b --- /dev/null +++ b/Godeps/_workspace/src/github.com/stretchr/testify/assert/doc.go @@ -0,0 +1,74 @@ +// A set of comprehensive testing tools for use with the normal Go testing system. +// +// Example Usage +// +// The following is a complete example using assert in a standard test function: +// import ( +// "testing" +// "github.com/stretchr/testify/assert" +// ) +// +// func TestSomething(t *testing.T) { +// +// var a string = "Hello" +// var b string = "Hello" +// +// assert.Equal(t, a, b, "The two words should be the same.") +// +// } +// +// Assertions +// +// Assertions allow you to easily write test code, and are global funcs in the `assert` package. +// All assertion functions take, as the first argument, the `*testing.T` object provided by the +// testing framework. This allows the assertion funcs to write the failings and other details to +// the correct place. +// +// Every assertion function also takes an optional string message as the final argument, +// allowing custom error messages to be appended to the message the assertion method outputs. +// +// Here is an overview of the assert functions: +// +// assert.Equal(t, expected, actual [, message [, format-args]) +// +// assert.NotEqual(t, notExpected, actual [, message [, format-args]]) +// +// assert.True(t, actualBool [, message [, format-args]]) +// +// assert.False(t, actualBool [, message [, format-args]]) +// +// assert.Nil(t, actualObject [, message [, format-args]]) +// +// assert.NotNil(t, actualObject [, message [, format-args]]) +// +// assert.Empty(t, actualObject [, message [, format-args]]) +// +// assert.NotEmpty(t, actualObject [, message [, format-args]]) +// +// assert.Error(t, errorObject [, message [, format-args]]) +// +// assert.NoError(t, errorObject [, message [, format-args]]) +// +// assert.Implements(t, (*MyInterface)(nil), new(MyObject) [,message [, format-args]]) +// +// assert.IsType(t, expectedObject, actualObject [, message [, format-args]]) +// +// assert.Contains(t, string, substring [, message [, format-args]]) +// +// assert.NotContains(t, string, substring [, message [, format-args]]) +// +// assert.Panics(t, func(){ +// +// // call code that should panic +// +// } [, message [, format-args]]) +// +// assert.NotPanics(t, func(){ +// +// // call code that should not panic +// +// } [, message [, format-args]]) +// +// assert.WithinDuration(t, timeA, timeB, deltaTime, [, message [, format-args]]) + +package assert diff --git a/Godeps/_workspace/src/github.com/stretchr/testify/assert/errors.go b/Godeps/_workspace/src/github.com/stretchr/testify/assert/errors.go new file mode 100644 index 0000000..ac9dc9d --- /dev/null +++ b/Godeps/_workspace/src/github.com/stretchr/testify/assert/errors.go @@ -0,0 +1,10 @@ +package assert + +import ( + "errors" +) + +// AnError is an error instance useful for testing. If the code does not care +// about error specifics, and only needs to return the error for example, this +// error should be used to make the test code more readable. +var AnError = errors.New("assert.AnError general error for testing") diff --git a/History.md b/History.md index b1d095d..92fec8d 100644 --- a/History.md +++ b/History.md @@ -1,3 +1,45 @@ +0.3.1 / 2014-09-10 +================== + +!! Backward Incompatibility !! + +Trousseau data store file format changed, and trousseau >= 0.3.1 are +now incompatible with older version created files. + +Fortunately, trousseau now exposes a 'upgrade' command which will take +care to upgrade your existing data stores. + +So if you are upgrading from former versions, please, upgrade. + +*Features and user experience* + * New data store file format: support for different encryption type and algorithms. Plain and Encrypted sections splitted. + * New upgrade command to automatically upgrade old versions data store to new format. + * Added a rename command to modify a key name + * Added a list-recipients command to easily show data store recipients + * Added a --store global option to select directly from command line data store to be used + * Added bash, zsh, and fish autocompletion rules in scripts/ + * Updated import and export commands to support plain data import/export through a --plain option + * Updated trousseau keys and show commands output so they are now alphabetically sorted + * Fixed trousseau command piped output + * Fixed trousseau dependency management reliability through godep + * Improved command-line accessibility: more obvious behaviors, commands and flags descriptions + * Improved Makefile + + +*Code and design* + * Reduce inter-dependency between trousseau package and cli interactions + * Moved command actions in trousseau package, got rid of cli.Context dependency. + * Replaced (trousseau)cli package with idiomatic cmd/trousseau/* + * Got rid of a ton of useless abstractions. More to go. + * Removed logrus dependency and use stdlib log package instead + * Rename GetStorePath to InferStorePath and add getters/setters on the gStorePath global + * Rename upload* helpers to Helper* + * Move S3 and Scp defaults globals to context.go + * Add a store file path retrieval helper + * Move passphrase handling in context.go + * Remove global passphrase + use getter in cli instead + * Copy the cli interface trousseau package members to a new cli package + 0.3.0 / 2013-04-21 ================== diff --git a/Makefile b/Makefile index d42cb19..7a29ffc 100644 --- a/Makefile +++ b/Makefile @@ -1,18 +1,40 @@ -DEPS = $(go list -f '{{range .TestImports}}{{.}} {{end}}' ./...) +# Base paths +ROOT_DIR := $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST)))) + +# Trousseau version +VERSION=$(awk '/TROUSSEAU_VERSION/ { gsub("\"", ""); print $NF }' ${ROOT_DIR}/constants.go) -all: - @(mkdir -p bin/) - @(bash --norc -i ./scripts/build.sh) +# Commands paths +CMD_DIR := $(ROOT_DIR)/cmd +TROUSSEAU_CMD_DIR = $(CMD_DIR)/trousseau -cov: - @(gocov test ./... | gocov-html > /tmp/coverage.html) - @(open /tmp/coverage.html) +# Binaries paths +BIN_DIR = $(ROOT_DIR)/bin +TROUSSEAU_BIN = $(BIN_DIR)/trousseau + +# Actions +DEPS = $(go list -f '{{range .TestImports}}{{.}} {{end}}' ./...) + +all: deps trousseau + @(mkdir -p $(BIN_DIR)) deps: + @(echo "-> Processing dependencies") @(go get github.com/kr/godep) + @(godep restore) + +trousseau: deps + @(echo "-> Compiling trousseau binary") + @(mkdir -p $(BIN_DIR)) + @(cd $(TROUSSEAU_CMD_DIR) && go build -o $(TROUSSEAU_BIN)) + @(echo "-> trousseau binary created: $(TROUSSEAU_BIN)") test: deps @(go list ./... | xargs -n1 go test) -.PNONY: all cov deps test +format: + @(go fmt ./...) + @(go vet ./...) + +.PNONY: all deps trousseau test format diff --git a/README.md b/README.md index 5aad1e9..6d48a5f 100644 --- a/README.md +++ b/README.md @@ -3,14 +3,16 @@ ## What -*Trousseau* is a **gpg** encrypted key-value store designed to be a *simple*, *safe* and *trustworthy* place for your data. -It stores data in a single multi-recipients encrypted file and can supports both local and remote storage sources (S3, ssh, gist so far) import/export. +*Trousseau* is an encrypted key-value store designed to be a *simple*, *safe* and *trustworthy* place for your data. -Create a *trousseau* store, specify which *gpg* recipients are allowed to open and modify it, add some key-value pairs to it, export it to S3 for example, and re-import it on another device. As simple as that. +It stores data in a single file, encrypted using the openpgp asymetric encryption algorithm. +It can be easily exported and imported to/from multiple remote storages: like S3, an ssh endpoint, gist (ftp, dropbox, and more to come...). +It is able to restrict access to the data store to a set of openpgp recipients. -Whether you're a devops, a paranoid guy living in a bunker, or the random user who seeks a simple way to store it's critical data in secured manner. *Trousseau* can do something for you. +Create a *trousseau* data store, specify which *opengpg* recipients are allowed to open and modify it, add some key-value pairs to it, export it to S3 for example, and re-import it on another device: As simple as that. + +*Secrets are made to be shared, just not with anyone.* Whether you're an admin, a paranoid guy living in a bunker, or a random user who seeks a simple way to store it's critical data in secured manner. *Trousseau* can do something for you. -
## Why Storing, transporting, and sharing sensitive data can be hard, and much more difficult when it comes to automate it. @@ -18,37 +20,27 @@ Storing, transporting, and sharing sensitive data can be hard, and much more dif *Trousseau* was created with private keys transportation and sharing across a servers cluster in mind. However it has proved being useful to anyone who need to store and eventually share a passwords store, bank accounts details or even more sensitive data. -
### Real world use cases -
-#### For the devops out there +#### For the admins out there *Trousseau can be useful to you when it comes to*: -* **Store** sensitive data: Your brand new shiny infrastructure surely relies on many certificates and private keys of different kinds: ssl, rsa, gpg, ... *Trousseau* provides a simple and fine-tuned way to store their content in a single file that you can safely version using your favorite cvs. No more plain certificates and keys in your repositories and configuration files. +* **Store** sensitive data: No more plain certificates and keys in your repositories and configuration files. Your brand new shiny infrastructure surely relies on many certificates and private keys of different kinds: ssl, rsa, gpg, ... *Trousseau* provides a simple and fine-tuned way to store their content in a single file that you can safely version using your favorite cvs. * **Share** passwords, keys and other critical data with co-workers and servers in your cluster in a safe manner. *Trousseau* encrypts its content for the specific recipient you provide it. Only the recipient you intend will be able to import and read-write the *Trousseau* store content. *Trousseau* proved itself to be a great way to share some services passwords with your co-workers too! * **Deploy** keys to your servers in a safe and normative way. Encrypt the trousseau store for each server selectively. -
#### For the common users * **Store** your sensitive data like passwords, bank account details, sex tapes involving you and your teachers or whatever comes to your mind in an encrypted store. * **Sync** your sensitive data store to remote services and easily share it between your unix-like devices. -## It's open-source - -*Trousseau* is open source software under the MIT license. -Any hackers are welcome to supply ideas, features requests, patches, pull requests and so on. -Let's make *Trousseau* awesome! -See **Contribute** section. +## How -
-## Installation +### Installation -
-### Debian and ubuntu +#### Debian and ubuntu A binary debian repository provides *trousseau* packages for *i386*, *x86_64* and *arm* architectures, so you can easily install it. Just add the repository to your sources.list: @@ -63,10 +55,9 @@ And you're ready to go: $ sudo apt-get update && sudo apt-get install trousseau ``` -
-### OSX +#### OSX -#### Homebrew +##### Homebrew If you're using homebrew just proceed to installation using the provided formula: @@ -76,23 +67,20 @@ $ brew install https://raw.githubusercontent.com/oleiade/trousseau/master/trouss *Et voila!* -#### Macports +##### Macports Coming soon (Don't be shy, if you feel like you could do it, just send pull request ;) ) -
-### Build it +#### Build it 1. First, make sure you have a [Go](http://http://golang.org/) language compiler **>= 1.2** (*mandatory*) and [git](http://gitscm.org) installed. 2. Make sure you have the following go system dependencies in your ``$PATH``: ``bzr, svn, hg, git`` 3. Ensure your [GOPATH](http://golang.org/doc/code.html#GOPATH) is properly set. 4. Run ``make`` -
-## Prerequisities +### Prerequisities -
-### Gpg passphrase +#### Gpg passphrase Every decryption operations will require your *gpg* primary key passphrase. As of today, **trousseau** is able to handle your passphrase through multiple ways: @@ -138,29 +126,34 @@ Ultimately, you can pass you gpg passphrase through the command line global opti $ trousseau --passhphrase mysupperdupperpassphrase get abc ``` -### Environment +#### Environment Trousseau behavior can be controlled through the system environment: * *TROUSSEAU_STORE* : if you want to have multiple trousseau data store, set this environment variable to the path of the one you want to use. Default is ``$HOME/.trousseau`` -
## Let's get started -
### Basics First use of **trousseau** requires the data store to be created. A **trousseau** data store is built and maintained for a list of *gpg* recipients who will be the only ones able to decrypt and manipulate it (so don't forget to include yourself ;) ) +Moreover, you can easily create/select multiple stores using whether the ``--store`` global option or the ``TROUSSEAU_STORE`` environment variable mentioned upper in this *README*. -
#### API +##### Commands + * **create** [RECIPIENTS ...] : creates the trousseau encrypted datastore for provided recipients and stores it in `$HOME/.trousseau` +* **upgrade** : Will upgrade your data store to be compatible with a newer version of trousseau. For example, to upgrade a data store created by trousseau <= 0.3.1 to be compatible with trousseau >= 0.3.1 * **meta** : Outputs the store metadata. * **add-recipient** RECIPIENT : Adds a recipient to the store. The recipient will be able to open and modify the store. +* **list-recipients** : Lists the data store recipients. * **remove-recipient** RECIPIENT : Removes a recipient from the store. The recipient will not be able to open or modify the store. -
+##### Global options + +* ``--store`` [PATH] : select an alternative trousseau data store path. This option is really helpful when you want to manipulate multiple stores. + #### First steps with the data store ```bash @@ -174,38 +167,36 @@ Trousseau data store consists in single gpg encrypted file residing in your ``$H ```bash $ cat ~/.trousseau ------BEGIN PGP MESSAGE----- -wcBMA5i2a4x3jHQgAQgAGKAZd5UFauGBMkFz7wi4v4aNTGGpDS81drrevo/Tntdz -rr+PR/GjUlKZxhvG18mr+FuTV6q2DOK3Z0nROs57PLK9Q3ye40Su/Af1vj+LaN4i -AAMK9YVpjKaxz+pciUm8nBDkRxp3CLZ9eA2B+1JBy5HgziHY+7KC/dvaubRv0M0J -qzYvshIYU0urVQt7oO4WYVQbJ1N0OXV3oAzW4bBBs/p6b8KSUlmvHUr+9r4V1KvU -ynpHbp1T2HVPC9uqLgJ+PRjlQ2QsxjezkBntOFMaeMZjq2m2glw90aIGDAPjkMKy -42qQbmdrT3+houqeKUrLcVFNOxevVEZLf8N3Qgo/H9LgAeSroddqYkJzOmknxDzP -MDk+4TaY4Ljge+G7j+CB4iBsIjrgSefl/4ZU30dJ/DHyL5i3lCCGXXAo2eqfJg2w -FZgh+qc8Mbjlz2iMdnC+b8rRwhMTgD1Tyd8vbR1ArPfQh3ThdePwrdyE86CYQZOA -MIBfKgTUpWiAtEhM23melF8H3oznrIKt1ZtDsxJEuBCZ86XlC9TF27XFWbnl7rfK -jF2kqP3DuuBA5d23HprbN6LjDSJeKbXDvc5LetBI7O5y954n3tMWCB9y4EjkpVAx -EWnovjEnnW89uXHaFOBQ4naH4kjg1OHEquCf4Nvgl+S5Pfi875yAKqxxK/+e8GGo -4q8UZC7ho/cA -=t2zr ------END PGP MESSAGE----- +{ + "crypto_type":1, + "crypto_algorithm":0, + "_data":"012ue091ido19d81j2d01029dj1029d1029u401294i ... 1028019k0912djm0129d12" +} +``` + +If you've just updated trousseau to a version marked as implying backward incompatibilities, the ``upgrade`` command is here to help + +```bash +$ trousseau upgrade +Upgrading trousseau data store to version M: success +Upgrading trousseau data store to version N: success +# This is it, your legacy data store has now been upgraded to be compatible with +# your current version of trousseau ``` -
### Manipulating keys Once your trousseau has been created, you're now able to read, write, list, delete its data. Here's how the fun part goes. -
#### API * **get** KEY [--file]: Outputs the stored KEY-value pair, whether on *stdout* or in pointed ``--file`` option path. * **set** KEY [VALUE | --file] : Sets the provided key-value pair in store using provided value or extracting it from path pointed by ``--file`` option. +* **rename** KEY_NAME NEW_NAME : Renames a store key * **del** KEY : Deletes provided key from the store * **keys** : Lists the stored keys * **show** : Lists the stored key-value pairs -
#### You've got the keys ```bash @@ -232,8 +223,16 @@ myuser.ssh.public_key $ trousseau get abc 123 +# What about renaming abc key, just for fun? +$ trousseau rename abc 'my friend jackson' +$ trousseau keys +my friend jackson +easy as +myuser.ssh.public_key + + $ trousseau show -abc: 123 +my friend jackson: 123 easy as: do re mi myuser.ssh.public_key: ssh-rsa 1289eu102ij30192u3e0912e ... @@ -243,7 +242,7 @@ myuser.ssh.public_key: ssh-rsa 1289eu102ij30192u3e0912e $ trousseau get myuser.ssh.public_key --file /home/myuser/id_rsa.pub # Now if you don't need a key anymore, just drop it. -$ trousseau del abc # Now the song lacks something doesn't it? +$ trousseau del 'my friend jackson' # Now the song lacks something doesn't it? ```
@@ -252,13 +251,11 @@ $ trousseau del abc # Now the song lacks something doesn't it? Trousseau was built with data remote storage in mind. Therefore it provides *push* and *pull* actions to export and import the trousseau data store to remote destinations. As of today S3, SSH and gist storages are available (more are to come). -
#### API * **push** : Pushes the trousseau data store to remote storage * **pull** : Pulls the trousseau data store from remote storage -
#### DSN In order to make your life easier trousseau allows you to select your export and import sources using a *DSN*. @@ -274,7 +271,6 @@ In order to make your life easier trousseau allows you to select your export and * **port**: The *aws_region* if you're targeting *s3*. The port to login to using *scp* otherwise. * **path**: The remote path to push to or retrieve from the trousseau file on a ``push`` or ``pull`` action. -
#### S3 Example ```bash @@ -299,7 +295,6 @@ abc: 123 easy as: do re mi ``` -
#### Scp example ```bash @@ -370,7 +365,6 @@ abc: 123 easy as: do re mi ``` -
### Local imports and exports #### API @@ -391,7 +385,6 @@ cousin_machin:isagreatbuddy adams_family:rests in peace, for sure ``` -
### Metadata Trousseau keeps track and exposes all sort of metadata about your store that you can access through the ``meta`` command. @@ -417,7 +410,6 @@ gpg: encrypted with 2048-bit RSA key, ID 4B7D890, created 2013-05-21 {"_meta":{"created_at":"2013-08-12 08:00:20.457477714 +0200 CEST","last_modified_at":"2013-08-12 08:00:20.457586991 +0200 CEST","recipients":["92EDE36B"],"version":"0.1.0"},"data":{}} ``` -
### Adding and removing recipients Okay, so you've created a trousseau data store with two recipients allowed to manipulate it. Now suppose you'd like to add another recipient to be able to open and update the trousseau store; or to remove one. @@ -440,7 +432,6 @@ Recipients: [4B7D890, 869FA4A] TrousseauVersion: 0.1.0c ``` -
## More features to come * Support for Sftp remote storage @@ -449,7 +440,6 @@ TrousseauVersion: 0.1.0c * In a further future I might add TrueCrypt encryption -
## Contribute * Check for open issues or open a fresh issue to start a discussion around a feature idea or a bug. @@ -457,6 +447,14 @@ TrousseauVersion: 0.1.0c * Write tests which show that the bug was fixed or that the feature works as expected. * Send a pull request and bug the maintainer until it gets merged and published. :) Make sure to add yourself to AUTHORS. +## It's open-source + +*Trousseau* is open source software under the MIT license. +Any hackers are welcome to supply ideas, features requests, patches, pull requests and so on. +Let's make *Trousseau* awesome! + +See **Contribute** section. + ## Changelog See [History](https://github.com/oleiade/trousseau/blob/master/History.md) diff --git a/actions.go b/actions.go new file mode 100644 index 0000000..311c128 --- /dev/null +++ b/actions.go @@ -0,0 +1,542 @@ +package trousseau + +import ( + "time" + + "github.com/oleiade/trousseau/dsn" + "os" + "io" + "io/ioutil" + "encoding/json" + "fmt" + "strings" +) + +func CreateAction(recipients []string) { + meta := Meta{ + CreatedAt: time.Now().String(), + LastModifiedAt: time.Now().String(), + Recipients: recipients, + TrousseauVersion: TROUSSEAU_VERSION, + } + store := NewStore(meta) + + tr := Trousseau{ + CryptoType: ASYMMETRIC_ENCRYPTION, + CryptoAlgorithm: GPG_ENCRYPTION, + } + tr.Encrypt(store) + + err := tr.Write(InferStorePath()) + if err != nil { + ErrorLogger.Fatal(err) + } + + InfoLogger.Println("Trousseau data store succesfully created") +} + +func PushAction(destination string, sshPrivateKey string, askPassword bool) { + endpointDsn, err := dsn.Parse(destination) + if err != nil { + ErrorLogger.Fatal(err) + } + + switch endpointDsn.Scheme { + case "s3": + err := endpointDsn.SetDefaults(S3Defaults) + if err != nil { + ErrorLogger.Fatal(err) + } + + err = UploadUsingS3(endpointDsn) + if err != nil { + ErrorLogger.Fatal(err) + } + InfoLogger.Println("Trousseau data store succesfully pushed to s3") + case "scp": + err := endpointDsn.SetDefaults(ScpDefaults) + if err != nil { + ErrorLogger.Fatal(err) + } + + if askPassword == true { + password := PromptForPassword() + endpointDsn.Secret = password + } + + err = UploadUsingScp(endpointDsn, sshPrivateKey) + if err != nil { + ErrorLogger.Fatal(err) + } + InfoLogger.Println("Trousseau data store succesfully pushed to ssh remote storage") + case "gist": + err = UploadUsingGist(endpointDsn) + if err != nil { + ErrorLogger.Fatal(err) + } + InfoLogger.Println("Trousseau data store succesfully pushed to gist") + } +} + +func PullAction(source string, sshPrivateKey string, askPassword bool) { + endpointDsn, err := dsn.Parse(source) + if err != nil { + ErrorLogger.Fatal(err) + } + + switch endpointDsn.Scheme { + case "s3": + err := endpointDsn.SetDefaults(S3Defaults) + if err != nil { + ErrorLogger.Fatal(err) + } + + err = DownloadUsingS3(endpointDsn) + if err != nil { + ErrorLogger.Fatal(err) + } + InfoLogger.Println("Trousseau data store succesfully pulled from S3") + case "scp": + err := endpointDsn.SetDefaults(ScpDefaults) + if err != nil { + ErrorLogger.Fatal(err) + } + + if askPassword == true { + password := PromptForPassword() + endpointDsn.Secret = password + } + + err = DownloadUsingScp(endpointDsn, sshPrivateKey) + if err != nil { + ErrorLogger.Fatal(err) + } + InfoLogger.Println("Trousseau data store succesfully pulled from ssh remote storage") + case "gist": + err = DownloadUsingGist(endpointDsn) + if err != nil { + ErrorLogger.Fatal(err) + } + InfoLogger.Println("Trousseau data store succesfully pulled from gist") + default: + if endpointDsn.Scheme == "" { + ErrorLogger.Fatalf("No dsn scheme supplied") + } else { + ErrorLogger.Fatalf("Invalid dsn scheme supplied: %s", endpointDsn.Scheme) + } + } + + InfoLogger.Println("Trousseau data store succesfully pulled from remote storage") +} + +func ExportAction(to string, plain bool) { + outputFile, err := os.Create(to) + if err != nil { + ErrorLogger.Fatal(err) + } + defer outputFile.Close() + + // Make sure the file is readble/writable only + // by its owner + err = os.Chmod(outputFile.Name(), os.FileMode(0600)) + if err != nil { + ErrorLogger.Fatal(err) + } + + if plain == true { + tr, err := OpenTrousseau(InferStorePath()) + if err != nil { + ErrorLogger.Fatal(err) + } + + store, err := tr.Decrypt() + if err != nil { + ErrorLogger.Fatal(err) + } + + storeBytes, err := json.Marshal(store) + if err != nil { + ErrorLogger.Fatal(err) + } + + err = ioutil.WriteFile(to, storeBytes, os.FileMode(0600)) + if err != nil { + ErrorLogger.Fatal(err) + } + } else { + inputFile, err := os.Open(InferStorePath()) + defer inputFile.Close() + if err != nil { + ErrorLogger.Fatal(err) + } + + _, err = io.Copy(outputFile, inputFile) + if err != nil { + ErrorLogger.Fatal(err) + } + } + + InfoLogger.Println(fmt.Sprintf("Trousseau data store exported to: %s", to)) +} + +func ImportAction(from string, strategy ImportStrategy, plain bool) { + var importedStore *Store = &Store{} + var localFilePath string = InferStorePath() + + localTr, err := OpenTrousseau(localFilePath) + if err != nil { + ErrorLogger.Fatal(err) + } + + localStore, err := localTr.Decrypt() + if err != nil { + ErrorLogger.Fatal(err) + } + + if plain == true { + importedData, err := ioutil.ReadFile(from) + if err != nil { + ErrorLogger.Fatal(err) + } + + err = json.Unmarshal(importedData, importedStore) + if err != nil { + ErrorLogger.Fatal(err) + } + } else { + importedTr, err := OpenTrousseau(from) + if err != nil { + ErrorLogger.Fatal(err) + } + + importedStore, err = importedTr.Decrypt() + if err != nil { + ErrorLogger.Fatal(err) + } + } + + err = ImportStore(importedStore, localStore, strategy) + if err != nil { + ErrorLogger.Fatal(err) + } + + err = localTr.Encrypt(localStore) + if err != nil { + ErrorLogger.Fatal(err) + } + + err = localTr.Write(localFilePath) + if err != nil { + ErrorLogger.Fatal(err) + } + + InfoLogger.Println(fmt.Sprintf("Trousseau data store imported: %s", from)) +} + +func ListRecipientsAction() { + tr, err := OpenTrousseau(InferStorePath()) + if err != nil { + ErrorLogger.Fatal(err) + } + + store, err := tr.Decrypt() + if err != nil { + ErrorLogger.Fatal(err) + } + + recipients := store.Meta.ListRecipients() + for _, r := range recipients { + InfoLogger.Println(r) + } +} + +func AddRecipientAction(recipient string) { + tr, err := OpenTrousseau(InferStorePath()) + if err != nil { + ErrorLogger.Fatal(err) + } + + store, err := tr.Decrypt() + if err != nil { + ErrorLogger.Fatal(err) + } + + err = store.Meta.AddRecipient(recipient) + if err != nil { + ErrorLogger.Fatal(err) + } + + err = tr.Encrypt(store) + if err != nil { + ErrorLogger.Fatal(err) + } + + err = tr.Write(InferStorePath()) + if err != nil { + ErrorLogger.Fatal(err) + } +} + +func RemoveRecipientAction(recipient string) { + tr, err := OpenTrousseau(InferStorePath()) + if err != nil { + ErrorLogger.Fatal(err) + } + + store, err := tr.Decrypt() + if err != nil { + ErrorLogger.Fatal(err) + } + + err = store.Meta.RemoveRecipient(recipient) + if err != nil { + ErrorLogger.Fatal(err) + } + + err = tr.Encrypt(store) + if err != nil { + ErrorLogger.Fatal(err) + } + + err = tr.Write(InferStorePath()) + if err != nil { + ErrorLogger.Fatal(err) + } +} + +func GetAction(key string, filepath string) { + tr, err := OpenTrousseau(InferStorePath()) + if err != nil { + ErrorLogger.Fatal(err) + } + + store, err := tr.Decrypt() + if err != nil { + ErrorLogger.Fatal(err) + } + + value, err := store.Data.Get(key) + if err != nil { + ErrorLogger.Fatal(err) + } + + // If the --file flag is provided + if filepath != "" { + valueBytes, ok := value.(string) + if !ok { + ErrorLogger.Fatal(fmt.Sprintf("unable to write %s value to file", key)) + } + + err := ioutil.WriteFile(filepath, []byte(valueBytes), os.FileMode(0644)) + if err != nil { + ErrorLogger.Fatal(err) + } + } else { + // Use fmt.Print to support patch processes, + // to get the value "as is" without any appended newlines + if isPipe(os.Stdout) { + fmt.Print(value) + } else { + InfoLogger.Println(value) + } + } +} + +func SetAction(key, value, file string) { + // If the --file flag is provided + if file != "" { + // And the file actually exists on file system + if PathExists(file) { + // Then load it's content + fileContent, err := ioutil.ReadFile(file) + if err != nil { + ErrorLogger.Fatal(err) + } + + value = string(fileContent) + } else { + ErrorLogger.Fatalf("Cannot open %s because it doesn't exist", file) + } + } + + tr, err := OpenTrousseau(InferStorePath()) + if err != nil { + ErrorLogger.Fatal(err) + } + + store, err := tr.Decrypt() + if err != nil { + ErrorLogger.Fatal(err) + } + + store.Data.Set(key, value) + + err = tr.Encrypt(store) + if err != nil { + ErrorLogger.Fatal(err) + } + + err = tr.Write(InferStorePath()) + if err != nil { + ErrorLogger.Fatal(err) + } +} + +func RenameAction(src, dest string, overwrite bool) { + tr, err := OpenTrousseau(InferStorePath()) + if err != nil { + ErrorLogger.Fatal(err) + } + + store, err := tr.Decrypt() + if err != nil { + ErrorLogger.Fatal(err) + } + + err = store.Data.Rename(src, dest, overwrite) + if err != nil { + ErrorLogger.Fatal(err) + } + + err = tr.Encrypt(store) + if err != nil { + ErrorLogger.Fatal(err) + } + + err = tr.Write(InferStorePath()) + if err != nil { + ErrorLogger.Fatal(err) + } + +} + +func DelAction(key string) { + tr, err := OpenTrousseau(InferStorePath()) + if err != nil { + ErrorLogger.Fatal(err) + } + + store, err := tr.Decrypt() + if err != nil { + ErrorLogger.Fatal(err) + } + + store.Data.Del(key) + + tr.Encrypt(store) + if err != nil { + ErrorLogger.Fatal(err) + } + + err = tr.Write(InferStorePath()) + if err != nil { + ErrorLogger.Fatal(err) + } +} + +func KeysAction() { + tr, err := OpenTrousseau(InferStorePath()) + if err != nil { + ErrorLogger.Fatal(err) + } + + store, err := tr.Decrypt() + if err != nil { + ErrorLogger.Fatal(err) + } + + keys := store.Data.Keys() + for _, k := range keys { + InfoLogger.Println(k) + } +} + +func ShowAction() { + tr, err := OpenTrousseau(InferStorePath()) + if err != nil { + ErrorLogger.Fatal(err) + } + + store, err := tr.Decrypt() + if err != nil { + ErrorLogger.Fatal(err) + } + + items := store.Data.Items() + for k, v := range items { + InfoLogger.Println(fmt.Sprintf("%s : %s", k, v.(string))) + } +} + +func MetaAction() { + tr, err := OpenTrousseau(InferStorePath()) + if err != nil { + ErrorLogger.Fatal(err) + } + + store, err := tr.Decrypt() + if err != nil { + ErrorLogger.Fatal(err) + } + + InfoLogger.Println(store.Meta) +} + +func UpgradeAction(yes, noBackup bool) { + var proceed string = "n" + + data, err := ioutil.ReadFile(InferStorePath()) + if err != nil { + ErrorLogger.Fatal(err) + } + + version := DiscoverVersion(data, VersionDiscoverClosures) + if version == "" { + fmt.Errorf("Initial store version could not be detected") + } + + newStoreFile, err := UpgradeFrom(version, data, UpgradeClosures) + if err != nil { + ErrorLogger.Fatal(err) + } + + if yes == false { + fmt.Printf("You are about to upgrade trousseau data "+ + "store %s (version %s) up to version %s. Proceed? [Y/n] ", + InferStorePath(), version, TROUSSEAU_VERSION) + _, err = fmt.Scanf("%s", &proceed) + if err != nil { + ErrorLogger.Fatal(err) + } + } + + if strings.ToLower(proceed) == "y" || yes { + // Write a backup of the old store file inplace + if noBackup == false { + err = ioutil.WriteFile(InferStorePath()+".bkp", data, os.FileMode(0700)) + if err != nil { + ErrorLogger.Fatal(err) + } + } + + // Overwrite source legacy store with the new version content + err = ioutil.WriteFile(InferStorePath(), newStoreFile, os.FileMode(0700)) + if err != nil { + ErrorLogger.Fatal(err) + } + } else { + fmt.Println("upgrade cancelled") + } +} + +// Thanks, mrnugget! +// https://github.com/mrnugget/fzz/blob/master/utils.go#L14-L21 +func isPipe(f *os.File) bool { + s, err := f.Stat() + if err != nil { + return false + } + + return s.Mode()&os.ModeNamedPipe != 0 +} + diff --git a/trousseau/cli_helpers.go b/cli_helpers.go similarity index 100% rename from trousseau/cli_helpers.go rename to cli_helpers.go diff --git a/cmd/trousseau/before.go b/cmd/trousseau/before.go new file mode 100644 index 0000000..fd397fb --- /dev/null +++ b/cmd/trousseau/before.go @@ -0,0 +1,48 @@ +package main + +import ( + libcli "github.com/codegangsta/cli" + "github.com/oleiade/trousseau" +) + +func Before(c *libcli.Context) error { + var err error + + err = checkHelp(c) + if err != nil { + return err + } + + err = updateStorePath(c) + if err != nil { + return err + } + + return nil +} + +// checkHelp will print command or app help according to the +// provided context. It is used to bypass the gpg key check +// before the application runs. So users can print the help +// without selecting their master key. +func checkHelp(c *libcli.Context) error { + if c.GlobalBool("h") || c.GlobalBool("help") { + if len(c.Args()) >= 1 { + libcli.ShowCommandHelp(c, c.Args().First()) + } else { + libcli.ShowAppHelp(c) + } + } + + return nil +} + +// updateStorePath selects the default trousseau data store if +// none were provided on the command line +func updateStorePath(c *libcli.Context) error { + if c.String("store") != "" { + trousseau.SetStorePath(c.String("store")) + } + + return nil +} diff --git a/cmd/trousseau/commands.go b/cmd/trousseau/commands.go new file mode 100644 index 0000000..3cfbfef --- /dev/null +++ b/cmd/trousseau/commands.go @@ -0,0 +1,453 @@ +package main + +import ( + "github.com/codegangsta/cli" + "github.com/oleiade/trousseau" + "fmt" + "os" + "strings" + "path/filepath" +) + +func CreateCommand() cli.Command { + return cli.Command{ + Name: "create", + Usage: "Create an encrypted data store", + Description: "The create command will generate an encrypted data store " + + "placed at $HOME/.trousseau.tr or at the location described by " + + "the $TROUSSEAU_HOME environment variable if you provided it.\n\n" + + " Encryption is made using your GPG main identity, and targets the " + + "GPG recipients you provide as the command arguments.\n\n" + + " Examples:\n\n" + + " trousseau create 16DB4F3\n" + + " trousseau create tcrevon@gmail.com\n" + + " export TROUSSEAU_STORE=/tmp/test_trousseau.tr && trousseau create 16DB4F3\n", + Action: func(c *cli.Context) { + if !hasExpectedArgs(c.Args(), 1) { + trousseau.ErrorLogger.Fatal("Invalid number of arguments provided to create command") + } + + var recipients []string = strings.Split(c.Args()[0], ",") + trousseau.CreateAction(recipients) + }, + } +} + +func PushCommand() cli.Command { + return cli.Command{ + Name: "push", + Usage: "Push the encrypted data store to a remote storage", + Description: "The local encrypted data store will be pushed to a remote destination " + + "described by a data source name.\n\n" + + " Trousseau data source name goes as follow:\n\n" + + " {protocol}://{identifier}:{secret}@{host}:{port}/{path}\n\n" + + " Given:\n" + + " * protocol: The remote service target type. Can be one of: s3 or scp\n" + + " * identifier: The login/key/whatever to authenticate trousseau to the remote service. Provide your aws_access_key if you're targeting s3, or your remote login if you're targeting scp\n" + + " * secret: The secret to authenticate trousseau to the remote service. Provide your aws_secret_key if you're targeting s3, or your remote password if you're targeting scp\n" + + " * host: Your bucket name is you're targeting s3. The host to login to using scp otherwise\n" + + " * port: The aws_region if you're targeting s3. The port to login to using scp otherwise\n" + + " * path: The remote path to push to or retrieve from the trousseau file on a push or pull action\n\n" + + " Examples:\n\n" + + " s3://1298u1928eu9182dj19d2:1928u192ijdnh1b2d8@my-super-bucket:eu-west-1/topsecret-trousseau.tr\n" + + " scp://myuser:@myhost.io:6453/topsecret-trousseau.tr (use the password option to supply password)\n" + + " gist://oleiade:1928u3019j2d9812dn0192u490128dj@:/topsecret-trousseau.tr\n", + Action: func(c *cli.Context) { + if !hasExpectedArgs(c.Args(), 1) { + trousseau.ErrorLogger.Fatal("Invalid number of arguments provided to push command") + } + + var destination string = c.Args().First() + trousseau.PushAction(destination, c.String("ssh-private-key"), c.Bool("ask-password")) + }, + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "overwrite", + Usage: "Overwrite any existing remote resource with pushed data", + }, + cli.BoolFlag{ + Name: "password", + Usage: "Prompt for remote host ssh password", + }, + cli.StringFlag{ + Name: "ssh-private-key", + Value: filepath.Join(os.Getenv("HOME"), ".ssh/id_rsa"), + Usage: "Path to the ssh private key to be used when pushing to remote storage via ssh", + }, + }, + } +} + +func PullCommand() cli.Command { + return cli.Command{ + Name: "pull", + Usage: "Pull the encrypted data store from a remote storage", + Description: "The remote encrypted data store described by a data source name " + + "will be pulled and replace the local data store.\n\n" + + " Trousseau data source name goes as follow:\n\n" + + " {protocol}://{identifier}:{secret}@{host}:{port}/{path}\n\n" + + " Given:\n" + + " * protocol: The remote service target type. Can be one of: s3 or scp\n" + + " * identifier: The login/key/whatever to authenticate trousseau to the remote service. Provide your aws_access_key if you're targeting s3, or your remote login if you're targeting scp\n" + + " * secret: The secret to authenticate trousseau to the remote service. Provide your aws_secret_key if you're targeting s3, or your remote password if you're targeting scp\n" + + " * host: Your bucket name is you're targeting s3. The host to login to using scp otherwise\n" + + " * port: The aws_region if you're targeting s3. The port to login to using scp otherwise\n" + + " * path: The remote path to push to or retrieve from the trousseau file on a push or pull action\n\n" + + " Examples:\n\n" + + " s3://1298u1928eu9182dj19d2:1928u192ijdnh1b2d8@my-super-bucket:eu-west-1/topsecret-trousseau.tr\n" + + " scp://myuser:@myhost.io:6453/topsecret-trousseau.tr (use the password option to supply password)\n" + + " gist://oleiade:1928u3019j2d9812dn0192u490128dj@:/topsecret-trousseau.tr\n", + Action: func(c *cli.Context) { + if !hasExpectedArgs(c.Args(), 1) { + trousseau.ErrorLogger.Fatal("Invalid number of arguments provided to pull command") + } + + var source string = c.Args().First() + trousseau.PullAction(source, c.String("ssh-private-key"), c.Bool("ask-password")) + }, + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "overwrite", + Usage: "Overwrite local data store with pulled remote resource", + }, + cli.BoolFlag{ + Name: "password", + Usage: "Prompt for remote host ssh password", + }, + cli.StringFlag{ + Name: "ssh-private-key", + Value: filepath.Join(os.Getenv("HOME"), ".ssh/id_rsa"), + Usage: "Path to the ssh private key to be used when pulling from remote storage via ssh", + }, + }, + } +} + +func ExportCommand() cli.Command { + return cli.Command{ + Name: "export", + Usage: "Export the encrypted data store to a file system location", + Description: "The encrypted data store at the default location ($HOME/.trousseau.tr) or " + + "the one pointed by the $TROUSSEAU_STORE environment variable will be pushed as is " + + "to the filesystem location provided as first argument.", + Action: func(c *cli.Context) { + if !hasExpectedArgs(c.Args(), 1) { + trousseau.ErrorLogger.Fatal("Invalid number of arguments provided to export command") + } + + var to string = c.Args().First() + trousseau.ExportAction(to, c.Bool("plain")) + }, + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "overwrite", + Usage: "Overwrite any existing destination resource", + }, + cli.BoolFlag{ + Name: "plain", + Usage: "Export the plain content of the encrypted data store", + }, + }, + } +} + +func ImportCommand() cli.Command { + return cli.Command{ + Name: "import", + Usage: "Import an encrypted data store from a file system location", + Description: "The encrypted data store at the filesystem location provided as first argument " + + "will be imported to the default trousseau location ($HOME/.trousseau.tr) or " + + "the one pointed by the $TROUSSEAU_STORE environment variable", + Action: func(c *cli.Context) { + if !hasExpectedArgs(c.Args(), 1) { + trousseau.ErrorLogger.Fatal("Invalid number of arguments provided to import command") + } + + var strategy trousseau.ImportStrategy + var yours bool = c.Bool("yours") + var theirs bool = c.Bool("theirs") + var overwrite bool = c.Bool("overwrite") + var activated uint = 0 + + // Ensure two import strategies were not provided at + // the same time. Otherwise, throw an error + for _, flag := range []bool{yours, theirs, overwrite} { + if flag { + activated += 1 + } + if activated >= 2 { + trousseau.ErrorLogger.Fatal("--yours, --theirs and --overwrite options are mutually exclusive") + } + } + + // Return proper ImportStrategy according to + // provided flags + if overwrite == true { + strategy = trousseau.IMPORT_OVERWRITE + } else if theirs == true { + strategy = trousseau.IMPORT_THEIRS + } else { + strategy = trousseau.IMPORT_YOURS + } + + var from string = c.Args().First() + trousseau.ImportAction(from, strategy, c.Bool("plain")) + }, + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "overwrite", + Usage: "Overwrite local data store with imported resource", + }, + cli.BoolFlag{ + Name: "plain", + Usage: "Import the content of the encrypted data store from a plain file", + }, + cli.BoolFlag{ + Name: "theirs", + Usage: "Keep the imported file value", + }, + cli.BoolFlag{ + Name: "yours", + Usage: "Keep your current data store values", + }, + }, + } +} + +func ListRecipientsCommand() cli.Command { + return cli.Command{ + Name: "list-recipients", + Usage: "List the data store encryption recipients", + Action: func(c *cli.Context) { + if !hasExpectedArgs(c.Args(), 0) { + trousseau.ErrorLogger.Fatal("Invalid number of arguments provided to list-recipients command") + } + + trousseau.ListRecipientsAction() + }, + } +} + +func AddRecipientCommand() cli.Command { + return cli.Command{ + Name: "add-recipient", + Usage: "Add a recipient to the encrypted data store", + Description: "Add a valid GPG recipient to the encrypted data store. To proceed you must " + + "make sure the recipient's GPG public key is available in your public keyring (this " + + "can be done by making sure it appears in the 'gpg --list-keys' command's output).\n" + + " And you can whether provide it whether as an openpgp id or by using the email attached " + + "to it's key", + Action: func(c *cli.Context) { + if !hasExpectedArgs(c.Args(), 1) { + trousseau.ErrorLogger.Fatal("Invalid number of arguments provided to add-recipient command") + } + + trousseau.AddRecipientAction(c.Args().First()) + + if c.Bool("verbose") == true { + trousseau.InfoLogger.Println(fmt.Sprintf("Recipient added to trousseau data store: %s", c.Args().First())) + } + }, + } +} + +func RemoveRecipientCommand() cli.Command { + return cli.Command{ + Name: "remove-recipient", + Usage: "Remove a recipient from the encrypted data store", + Action: func(c *cli.Context) { + if !hasExpectedArgs(c.Args(), 1) { + trousseau.ErrorLogger.Fatal("Invalid number of arguments provided to remove-recipient command") + } + + trousseau.RemoveRecipientAction(c.Args().First()) + + if c.Bool("verbose") == true { + fmt.Printf("Recipient removed from trousseau data store: %s", c.Args().First()) + } + + }, + } +} + +func SetCommand() cli.Command { + return cli.Command{ + Name: "set", + Usage: "Set a key value pair in the encrypted data store", + Action: func(c *cli.Context) { + if !hasExpectedArgs(c.Args(), 2) { + trousseau.ErrorLogger.Fatal("Invalid number of arguments provided to set command") + } + + var key string = c.Args().First() + var value string = c.Args()[1] + var file string = c.String("file") + + trousseau.SetAction(key, value, file) + + if c.Bool("verbose") == true { + trousseau.InfoLogger.Println(fmt.Sprintf("%s:%s", key, value)) + } + }, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "file, f", + Usage: "Write key's value to provided file", + }, + }, + } +} + +func GetCommand() cli.Command { + return cli.Command{ + Name: "get", + Usage: "Get a key's value from the encrypted data store", + Action: func(c *cli.Context) { + if !hasExpectedArgs(c.Args(), 1) { + trousseau.ErrorLogger.Fatal("Invalid number of arguments provided to get command") + } + + var key string = c.Args().First() + var file string = c.String("file") + trousseau.GetAction(key, file) + }, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "file, f", + Usage: "Read key's value from provided file", + }, + }, + } +} + +func RenameCommand() cli.Command { + return cli.Command{ + Name: "rename", + Usage: "Rename an encrypted data store's key", + Action: func(c *cli.Context) { + if !hasExpectedArgs(c.Args(), 2) { + trousseau.ErrorLogger.Fatal("Invalid number of arguments provided to rename command") + } + + var src string = c.Args().First() + var dest string = c.Args()[1] + + trousseau.RenameAction(src, dest, c.Bool("overwrite")) + + if c.Bool("verbose") == true { + trousseau.InfoLogger.Println(fmt.Sprintf("renamed: %s to %s", src, dest)) + } + }, + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "overwrite", + Usage: "Override any existing destination key", + }, + }, + } +} + +func DelCommand() cli.Command { + return cli.Command{ + Name: "del", + Usage: "Delete a key value pair from the store", + Action: func(c *cli.Context) { + if !hasExpectedArgs(c.Args(), 1) { + trousseau.ErrorLogger.Fatal("Invalid number of arguments provided to del command") + } + + var key string = c.Args().First() + + trousseau.DelAction(key) + + if c.Bool("verbose") == true { + trousseau.InfoLogger.Println(fmt.Sprintf("deleted: %s", c.Args()[0])) + } + }, + } +} + +func KeysCommand() cli.Command { + return cli.Command{ + Name: "keys", + Usage: "List the encrypted data store keys", + Action: func(c *cli.Context) { + if !hasExpectedArgs(c.Args(), 0) { + trousseau.ErrorLogger.Fatal("Invalid number of arguments provided to keys command") + } + + trousseau.KeysAction() + }, + } +} + +func ShowCommand() cli.Command { + return cli.Command{ + Name: "show", + Usage: "Show the encrypted data store key value pairs", + Action: func(c *cli.Context) { + if !hasExpectedArgs(c.Args(), 0) { + trousseau.ErrorLogger.Fatal("Invalid number of arguments provided to show command") + } + + trousseau.ShowAction() + }, + } +} + +func MetaCommand() cli.Command { + return cli.Command{ + Name: "meta", + Usage: "Show the encrypted data store metadata", + Action: func(c *cli.Context) { + if !hasExpectedArgs(c.Args(), 0) { + trousseau.ErrorLogger.Fatal("Invalid number of arguments provided to meta command") + } + + trousseau.MetaAction() + }, + } +} + +func UpgradeCommand() cli.Command { + return cli.Command{ + Name: "upgrade", + Usage: "Upgrade the encrypted data store to a newer version's file format", + Action: func(c *cli.Context) { + if !hasExpectedArgs(c.Args(), 0) { + trousseau.ErrorLogger.Fatal("Invalid number of arguments provided to upgrade command") + } + + trousseau.UpgradeAction(c.Bool("yes"), c.Bool("no-backup")) + }, + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "yes, y", + Usage: "Answer yes when prompted to trigger the upgrade action", + }, + cli.BoolFlag{ + Name: "no-backup", + Usage: "Don't backup store in the process of upgrading it", + }, + }, + } +} + +// hasExpectedArgs checks whether the number of args are as expected. +func hasExpectedArgs(args []string, expected int) bool { + switch expected { + case -1: + if len(args) > 0 { + return true + } else { + return false + } + default: + if len(args) == expected { + return true + } else { + return false + } + } +} + diff --git a/cmd/trousseau/main.go b/cmd/trousseau/main.go new file mode 100644 index 0000000..2449fe3 --- /dev/null +++ b/cmd/trousseau/main.go @@ -0,0 +1,49 @@ +package main + +import ( + "os" + + "github.com/codegangsta/cli" + "github.com/oleiade/trousseau" +) + +func main() { + app := cli.NewApp() + + app.Name = "trousseau" + app.Author = "oleiade" + app.Email = "tcrevon@gmail.com" + app.Usage = "Create, manage and share an encrypted data store" + app.Version = trousseau.TROUSSEAU_VERSION + app.Commands = []cli.Command{ + CreateCommand(), + SetCommand(), + GetCommand(), + RenameCommand(), + DelCommand(), + KeysCommand(), + ShowCommand(), + ExportCommand(), + ImportCommand(), + PushCommand(), + PullCommand(), + ListRecipientsCommand(), + AddRecipientCommand(), + RemoveRecipientCommand(), + MetaCommand(), + UpgradeCommand(), + } + app.Flags = []cli.Flag{ + cli.BoolFlag{ + Name: "verbose", + Usage: "Set trousseau in verbose mode", + }, + cli.StringFlag{ + Name: "store, s", + Usage: "Path to the trousseau data store to use", + }, + } + + app.Before = Before + app.Run(os.Args) +} diff --git a/trousseau/constants.go b/constants.go similarity index 94% rename from trousseau/constants.go rename to constants.go index ed8d17a..cfcc7ac 100644 --- a/trousseau/constants.go +++ b/constants.go @@ -1,6 +1,6 @@ package trousseau -const TROUSSEAU_VERSION = "0.3.0" +const TROUSSEAU_VERSION = "0.3.1" const ( DEFAULT_STORE_FILENAME = ".trousseau" diff --git a/trousseau/passhrase.go b/context.go similarity index 61% rename from trousseau/passhrase.go rename to context.go index d35102f..218235d 100644 --- a/trousseau/passhrase.go +++ b/context.go @@ -2,10 +2,36 @@ package trousseau import ( "github.com/tmc/keyring" - "log" "os" + "path/filepath" ) +// Global variables defining default values for S3 and scp +// uploads/downloads +var ( + S3Defaults map[string]string = map[string]string{ + "Path": "trousseau.tsk", + } + ScpDefaults map[string]string = map[string]string{ + "Id": os.Getenv("USER"), + "Port": "22", + "Path": "trousseau.tsk", + } +) + +func InferStorePath() string { + envPath := os.Getenv(ENV_TROUSSEAU_STORE) + contextPath := GetStorePath() + + if envPath != "" { + return envPath + } else if contextPath != "" { + return contextPath + } + + return filepath.Join(os.Getenv("HOME"), DEFAULT_STORE_FILENAME) +} + // GetPassphrase attemps to retrieve the user's gpg master // key passphrase using multiple methods. First it will attempt // to retrieve it from the environment, then it will try to fetch @@ -30,17 +56,17 @@ func GetPassphrase() (passphrase string) { // If passphrase was enither found in the environment nor // system keyring manager try to fetch it from gpg-agent if os.Getenv("GPG_AGENT_INFO") != "" { - passphrase, err = GetGpgPassphrase(gMasterGpgId) + passphrase, err = getGpgPassphrase(gMasterGpgId) } if err != nil { - log.Fatal("No passphrase provided. Unable to open data store") + ErrorLogger.Fatal("No passphrase provided. Unable to open data store") } return passphrase } -func GetGpgPassphrase(gpgId string) (string, error) { +func getGpgPassphrase(gpgId string) (string, error) { conn, err := NewGpgAgentConn() if err != nil { return "", err diff --git a/crypto.go b/crypto.go new file mode 100644 index 0000000..f7ad690 --- /dev/null +++ b/crypto.go @@ -0,0 +1,47 @@ +package trousseau + +import ( + "github.com/oleiade/trousseau/crypto/openpgp" +) + +// Declare encryption types +type CryptoType int + +const ( + SYMMETRIC_ENCRYPTION CryptoType = 0 + ASYMMETRIC_ENCRYPTION CryptoType = 1 +) + +// Declare available encryption algorithms +type CryptoAlgorithm int + +const ( + GPG_ENCRYPTION CryptoAlgorithm = 0 + AES_256_ENCRYPTION CryptoAlgorithm = 1 +) + +func DecryptAsymmetricPGP(encryptedData []byte, passphrase string) ([]byte, error) { + // Decrypt store data + decryptionKeys, err := openpgp.ReadSecRing(openpgp.SecringFile) + if err != nil { + return nil, err + } + + plainData, err := openpgp.Decrypt(decryptionKeys, string(encryptedData), passphrase) + if err != nil { + return nil, err + } + + return plainData, nil +} + +func EncryptAsymmetricPGP(plainData []byte, recipients []string) ([]byte, error) { + encryptionKeys, err := openpgp.ReadPubRing(openpgp.PubringFile, recipients) + if err != nil { + return nil, err + } + + encData := openpgp.Encrypt(encryptionKeys, string(plainData)) + + return encData, nil +} diff --git a/crypto/openpgp/constants.go b/crypto/openpgp/constants.go index 8bc7984..423ded5 100644 --- a/crypto/openpgp/constants.go +++ b/crypto/openpgp/constants.go @@ -1,5 +1,10 @@ package openpgp +// Env related constants const ( ENV_MASTER_GPG_ID_KEY = "TROUSSEAU_MASTER_GPG_ID" ) + +// PGP messages related constants +const PGP_MESSAGE_HEADER string = "-----BEGIN PGP MESSAGE-----\n\n" +const PGP_MESSAGE_FOOTER string = "-----END PGP MESSAGE-----\n" diff --git a/crypto/openpgp/globals.go b/crypto/openpgp/globals.go index d5f1bc3..9fe60a0 100644 --- a/crypto/openpgp/globals.go +++ b/crypto/openpgp/globals.go @@ -6,7 +6,7 @@ import ( ) // Gnupg keyrings files -var gPubringFile string = func() string { +var PubringFile string = func() string { envPubring := os.Getenv("GNUPG_PUBRING_PATH") if envPubring != "" { @@ -16,7 +16,7 @@ var gPubringFile string = func() string { return filepath.Join(os.Getenv("HOME"), ".gnupg", "pubring.gpg") }() -var gSecringFile string = func() string { +var SecringFile string = func() string { envSecring := os.Getenv("GNUPG_SECRING_PATH") if envSecring != "" { diff --git a/crypto/openpgp/io.go b/crypto/openpgp/io.go index 1790652..b2a0bf5 100644 --- a/crypto/openpgp/io.go +++ b/crypto/openpgp/io.go @@ -27,7 +27,7 @@ func NewGpgFile(filepath, passphrase string, recipients []string) *GpgFile { // the associated file descriptor has mode O_RDONLY. // If there is an error, it will be of type *PathError. func OpenFile(name string, mode int, passphrase string, recipients []string) (*GpgFile, error) { - f, err := os.OpenFile(name, mode, 0600) + f, err := os.OpenFile(name, mode, os.FileMode(0600)) if err != nil { return nil, err } @@ -51,7 +51,7 @@ func (gf *GpgFile) ReadAll() ([]byte, error) { } // Decrypt store data - decryptionKeys, err := ReadSecRing(gSecringFile) + decryptionKeys, err := ReadSecRing(SecringFile) if err != nil { return nil, err } @@ -84,7 +84,7 @@ func (gf *GpgFile) Read(b []byte) (n int, err error) { // It returns the number of bytes written and an error, if any. // Write returns a non-nil error when n != len(b). func (gf *GpgFile) Write(p []byte) (n int, err error) { - encryptionKeys, err := ReadPubRing(gPubringFile, gf.Recipients) + encryptionKeys, err := ReadPubRing(PubringFile, gf.Recipients) if err != nil { return 0, err } diff --git a/trousseau/download.go b/download.go similarity index 84% rename from trousseau/download.go rename to download.go index 359f363..843766c 100644 --- a/trousseau/download.go +++ b/download.go @@ -4,9 +4,9 @@ import ( "fmt" "github.com/crowdmob/goamz/aws" "github.com/oleiade/trousseau/dsn" + "github.com/oleiade/trousseau/remote/gist" "github.com/oleiade/trousseau/remote/s3" "github.com/oleiade/trousseau/remote/ssh" - "github.com/oleiade/trousseau/remote/gist" ) // downloadUsingS3 executes the whole process of pulling @@ -26,7 +26,7 @@ func DownloadUsingS3(dsn *dsn.Dsn) error { fmt.Errorf("Unable to connect to S3") } - err = s3Storage.Pull(dsn.Path, gStorePath) + err = s3Storage.Pull(dsn.Path, GetStorePath()) if err != nil { return err } @@ -51,7 +51,7 @@ func DownloadUsingScp(dsn *dsn.Dsn, privateKey string) (err error) { return err } - err = scpStorage.Pull(dsn.Path, gStorePath) + err = scpStorage.Pull(dsn.Path, GetStorePath()) if err != nil { return err } @@ -63,13 +63,13 @@ func DownloadUsingScp(dsn *dsn.Dsn, privateKey string) (err error) { // the trousseau data store file from gist remote storage // using the provided scheme informations. func DownloadUsingGist(dsn *dsn.Dsn) (err error) { - gistStorage := gist.NewGistStorage(dsn.Id, dsn.Secret) - gistStorage.Connect() + gistStorage := gist.NewGistStorage(dsn.Id, dsn.Secret) + gistStorage.Connect() - err = gistStorage.Pull(dsn.Path, gStorePath) - if err != nil { - return err - } + err = gistStorage.Pull(dsn.Path, GetStorePath()) + if err != nil { + return err + } - return nil + return nil } diff --git a/globals.go b/globals.go new file mode 100644 index 0000000..5e53a1d --- /dev/null +++ b/globals.go @@ -0,0 +1,19 @@ +package trousseau + +import ( + "os" +) + +// Global data store file path +var gStorePath string +func SetStorePath(storePath string) { gStorePath = storePath } +func GetStorePath() string { return gStorePath } + +// Gnupg trousseau master gpg key id +var gMasterGpgId string = os.Getenv(ENV_MASTER_GPG_ID_KEY) + +// Keyring manager service and username to use in order to +// retrieve trousseau main gpg key passphrase from system +// keyring +//var gKeyringService string = os.Getenv(ENV_KEYRING_SERVICE_KEY) +var gKeyringUser string = os.Getenv(ENV_KEYRING_USER_KEY) diff --git a/trousseau/gpgagent.go b/gpgagent.go similarity index 100% rename from trousseau/gpgagent.go rename to gpgagent.go diff --git a/trousseau/gpgagent_test.go b/gpgagent_test.go similarity index 100% rename from trousseau/gpgagent_test.go rename to gpgagent_test.go diff --git a/trousseau/import.go b/import.go similarity index 83% rename from trousseau/import.go rename to import.go index 8f743e5..32477d5 100644 --- a/trousseau/import.go +++ b/import.go @@ -12,22 +12,23 @@ type ImportStrategy uint32 func ImportStore(src, dest *Store, strategy ImportStrategy) error { switch strategy { case IMPORT_YOURS: - for key, value := range src.Container { - if _, ok := dest.Container[key]; !ok { - dest.Container[key] = value + for key, value := range src.Data { + if _, ok := dest.Data[key]; !ok { + dest.Data[key] = value } } case IMPORT_THEIRS: - for key, value := range src.Container { - dest.Container[key] = value + for key, value := range src.Data { + dest.Data[key] = value } case IMPORT_OVERWRITE: - dest.Container = src.Container + dest.Data = src.Data } return nil } +// TODO : remove func (s *ImportStrategy) FromCliContext(c *cli.Context) error { var yours bool = c.Bool("yours") var theirs bool = c.Bool("theirs") diff --git a/kv.go b/kv.go new file mode 100644 index 0000000..f00c5f6 --- /dev/null +++ b/kv.go @@ -0,0 +1,78 @@ +package trousseau + +import ( + "errors" + "fmt" + "sort" +) + +type KVStore map[string]interface{} + +// Get method fetches a key from the trousseau file store +func (kvs *KVStore) Get(key string) (interface{}, error) { + value, ok := (*kvs)[key] + if !ok { + return "", errors.New(fmt.Sprintf("Key %s does not exist", key)) + } + + return value, nil +} + +// Set method sets a key value pair in the store +func (kvs *KVStore) Set(key string, value interface{}) { + (*kvs)[key] = value +} + +// Del deletes a key value pair from the trousseau file +func (kvs *KVStore) Del(key string) { + delete((*kvs), key) +} + +// Rename a data store key to dest. If overwrite parameter +// is provided with a true value, any existing destination key-value +// pair will be overriden, otherwise (false) an error will be returned. +func (kvs *KVStore) Rename(src, dest string, overwrite bool) error { + + srcValue, ok := (*kvs)[src] + if !ok { + return errors.New(fmt.Sprintf("Source key %s does not exist", src)) + } + + // If destination key already exists, and overwrite flag is + // set to false, then return an error + _, ok = (*kvs)[dest] + if ok && overwrite == false { + return errors.New(fmt.Sprintf("Destination key %s already exists an overwrite flag was not provided.", dest)) + } + + // Otherwise update dest value + kvs.Set(dest, srcValue) + kvs.Del(src) + + return nil +} + +// Keys lists the keys contained in the trousseau store file +func (kvs *KVStore) Keys() []string { + index := 0 + keys := make([]string, len((*kvs))) + + for key, _ := range *kvs { + keys[index] = key + index++ + } + + // Sort in alphabetical order + sort.Strings(keys) + return keys +} + +func (kvs *KVStore) Items() map[string]interface{} { + items := make(map[string]interface{}) + + for key, value := range *kvs { + items[key] = value + } + + return items +} diff --git a/kv_test.go b/kv_test.go new file mode 100644 index 0000000..7734270 --- /dev/null +++ b/kv_test.go @@ -0,0 +1,118 @@ +package trousseau + +import ( + "testing" +) + +func TestKVStoreGet(t *testing.T) { + kvStore := make(KVStore) + kvStore["abc"] = 123 + + v, err := kvStore.Get("abc") + ok(t, err) + equals(t, v, 123) +} + +func TestKVStoreGet_errors_on_non_existing_key(t *testing.T) { + kvStore := make(KVStore) + + v, err := kvStore.Get("easy as") + notOk(t, err) + equals(t, v, "") +} + +func TestKVStoreSet(t *testing.T) { + kvStore := make(KVStore) + + kvStore.Set("abc", 123) + equals(t, kvStore["abc"], 123) +} + +func TestKVStoreDel(t *testing.T) { + kvStore := make(KVStore) + kvStore.Set("abc", 123) + + kvStore.Del("abc") + _, in := kvStore["abc"] + assert(t, in == false, "Expected 'abc' key to be removed from KVStore Container") +} + +func TestKVStoreRename_without_overwrite_and_non_existing_destination_key(t *testing.T) { + kvStore := make(KVStore) + kvStore["abc"] = 123 + + err := kvStore.Rename("abc", "easy as", false) + _, srcIn := kvStore["abc"] + _, destIn := kvStore["easy as"] + + ok(t, err) + assert(t, srcIn == false, "Expected source key to have been removed from KVStore") + assert(t, destIn == true, "Expected destination key to be present in KVStore") + equals(t, kvStore["easy as"], 123) +} + +func TestKVStoreRename_without_overwrite_and_existing_destination_key(t *testing.T) { + kvStore := make(KVStore) + kvStore["abc"] = 123 + kvStore["easy as"] = "do re mi" + + err := kvStore.Rename("abc", "easy as", false) + _, srcIn := kvStore["abc"] + _, destIn := kvStore["easy as"] + + notOk(t, err) + assert(t, srcIn == true, "Expected source key to have been removed from KVStore") + assert(t, destIn == true, "Expected destination key to be present in KVStore") + equals(t, kvStore["easy as"], "do re mi") +} + +func TestKVStoreRename_with_overwrite_and_existing_destination_key(t *testing.T) { + kvStore := make(KVStore) + kvStore["abc"] = 123 + kvStore["easy as"] = "do re mi" + + err := kvStore.Rename("abc", "easy as", true) + _, srcIn := kvStore["abc"] + _, destIn := kvStore["easy as"] + + ok(t, err) + assert(t, srcIn == false, "Expected source key to have been removed from KVStore") + assert(t, destIn == true, "Expected destination key to be present in KVStore") + equals(t, kvStore["easy as"], 123) +} + +func TestKVStoreRename_with_overwrite_and_non_existing_destination_key(t *testing.T) { + kvStore := make(KVStore) + kvStore["abc"] = 123 + + err := kvStore.Rename("abc", "easy as", true) + _, srcIn := kvStore["abc"] + _, destIn := kvStore["easy as"] + + ok(t, err) + assert(t, srcIn == false, "Expected source key to have been removed from KVStore") + assert(t, destIn == true, "Expected destination key to be present in KVStore") + equals(t, kvStore["easy as"], 123) +} + +func TestKVStoreKeys(t *testing.T) { + kvStore := make(KVStore) + kvStore["abc"] = 123 + kvStore["easy as"] = "do re mi" + + keys := kvStore.Keys() + equals(t, keys, []string{"abc", "easy as"}) +} + +func TestKVStoreItems(t *testing.T) { + kvStore := make(KVStore) + kvStore["abc"] = 123 + kvStore["easy as"] = "do re mi" + + items := kvStore.Items() + expected := map[string]interface{}{ + "abc": 123, + "easy as": "do re mi", + } + equals(t, items, expected) +} diff --git a/logger.go b/logger.go new file mode 100644 index 0000000..0d4bd79 --- /dev/null +++ b/logger.go @@ -0,0 +1,10 @@ +package trousseau + +import ( + "log" + "os" +) + +var InfoLogger = log.New(os.Stdout, "", 0) +var ErrorLogger = log.New(os.Stderr, "Error: ", 0) + diff --git a/main.go b/main.go deleted file mode 100644 index 9f02874..0000000 --- a/main.go +++ /dev/null @@ -1,36 +0,0 @@ -package main - -import ( - "github.com/codegangsta/cli" - "github.com/oleiade/trousseau/trousseau" - "os" -) - -func main() { - app := cli.NewApp() - - app.Name = "trousseau" - app.Usage = "handles an encrypted keys store" - app.Version = trousseau.TROUSSEAU_VERSION - app.Commands = []cli.Command{ - trousseau.CreateCommand(), - trousseau.PushCommand(), - trousseau.PullCommand(), - trousseau.ExportCommand(), - trousseau.ImportCommand(), - trousseau.AddRecipientCommand(), - trousseau.RemoveRecipientCommand(), - trousseau.SetCommand(), - trousseau.GetCommand(), - trousseau.DelCommand(), - trousseau.KeysCommand(), - trousseau.ShowCommand(), - trousseau.MetaCommand(), - } - app.Flags = []cli.Flag{ - trousseau.VerboseFlag(), - } - - trousseau.Logger.Formatter = new(trousseau.RawFormatter) - app.Run(os.Args) -} diff --git a/trousseau/meta.go b/meta.go similarity index 79% rename from trousseau/meta.go rename to meta.go index d24007e..5b804c1 100644 --- a/trousseau/meta.go +++ b/meta.go @@ -3,6 +3,7 @@ package trousseau import ( "errors" "fmt" + "reflect" "time" ) @@ -13,26 +14,8 @@ type Meta struct { TrousseauVersion string `json:"version"` } -func (m *Meta) updateLastModificationMarker() { - m.LastModifiedAt = time.Now().String() -} - -func (m *Meta) containsRecipient(recipient string) (status bool, index int) { - for index, r := range m.Recipients { - if r == recipient { - return true, index - } - } - - return false, -1 -} - -func (m *Meta) isLastRecipient(recipient string) bool { - if len(m.Recipients) == 1 { - return true - } - - return false +func (m *Meta) ListRecipients() []string { + return m.Recipients } func (m *Meta) AddRecipient(recipient string) error { @@ -64,3 +47,39 @@ func (m *Meta) RemoveRecipient(recipient string) error { return nil } + +func (m *Meta) updateLastModificationMarker() { + m.LastModifiedAt = time.Now().String() +} + +func (m *Meta) containsRecipient(recipient string) (status bool, index int) { + for index, r := range m.Recipients { + if r == recipient { + return true, index + } + } + + return false, -1 +} + +func (m *Meta) isLastRecipient(recipient string) bool { + if len(m.Recipients) == 1 { + return true + } + + return false +} + +func (m *Meta) String() string { + var repr string + metaType := reflect.TypeOf(*m) + metaValue := reflect.ValueOf(*m) + + for i := 0; i < metaType.NumField(); i++ { + key := metaType.Field(i).Name + value := metaValue.FieldByName(key).Interface() + repr += fmt.Sprintf("%s : %v\n", key, value) + } + + return repr +} diff --git a/remote/gist/constants.go b/remote/gist/constants.go index 46e0623..cbd7e21 100644 --- a/remote/gist/constants.go +++ b/remote/gist/constants.go @@ -1,3 +1,3 @@ package gist -const TOKEN="9c8f97e57b9a5ea6dae59e9762c4deb801fc6b91" +const TOKEN = "9c8f97e57b9a5ea6dae59e9762c4deb801fc6b91" diff --git a/remote/gist/storage.go b/remote/gist/storage.go index b6d5288..13d6b94 100644 --- a/remote/gist/storage.go +++ b/remote/gist/storage.go @@ -1,106 +1,105 @@ package gist import ( - "io/ioutil" - "github.com/google/go-github/github" - "code.google.com/p/goauth2/oauth" + "code.google.com/p/goauth2/oauth" + "github.com/google/go-github/github" + "io/ioutil" ) type GistStorage struct { - connexion *github.Client - transport *oauth.Transport + connexion *github.Client + transport *oauth.Transport - Token string - User string + Token string + User string } func NewGistStorage(user, token string) *GistStorage { - transport := &oauth.Transport{ - Token: &oauth.Token{AccessToken: token}, - } - - return &GistStorage{ - transport: transport, - Token: token, - User: user, - } + transport := &oauth.Transport{ + Token: &oauth.Token{AccessToken: token}, + } + + return &GistStorage{ + transport: transport, + Token: token, + User: user, + } } func (gs *GistStorage) Connect() { - gs.connexion = github.NewClient(gs.transport.Client()) + gs.connexion = github.NewClient(gs.transport.Client()) } // Push exports the local encrypted trousseau data store to // a Github private gist func (gs *GistStorage) Push(localPath, remotePath string) (err error) { - var public bool = false - - // Read the encrypted data store content - fileBytes, err := ioutil.ReadFile(localPath) - if err != nil { - return err - } - fileContent := string(fileBytes) - - // Build a Gist file store - files := map[github.GistFilename]github.GistFile{ - github.GistFilename(remotePath): github.GistFile{ - Content: &fileContent, - }, - } - - // Create a gist representation ready to be - // pushed over network - gist := &github.Gist{ - Public: &public, - Files: files, - } - - // Push the gist to github - _, _, err = gs.connexion.Gists.Create(gist) - if err != nil { - return err - } - - return nil + var public bool = false + + // Read the encrypted data store content + fileBytes, err := ioutil.ReadFile(localPath) + if err != nil { + return err + } + fileContent := string(fileBytes) + + // Build a Gist file store + files := map[github.GistFilename]github.GistFile{ + github.GistFilename(remotePath): github.GistFile{ + Content: &fileContent, + }, + } + + // Create a gist representation ready to be + // pushed over network + gist := &github.Gist{ + Public: &public, + Files: files, + } + + // Push the gist to github + _, _, err = gs.connexion.Gists.Create(gist) + if err != nil { + return err + } + + return nil } // Pull imports the encrypted trousseau data store from // a Github gist func (gs *GistStorage) Pull(remotePath, localPath string) (err error) { - var gist *github.Gist - - // Fetch the user's gists list - gists, _, err := gs.connexion.Gists.List(gs.User, nil) - if err != nil { - return err - } - - // Find a gist containing trousseau data store - for _, g := range gists { - for k, _ := range g.Files { - if string(k) == remotePath { - gist = &g - break - } - } - - if gist != nil { - break - } - } - - // Download the gist file content - gist, _, err = gs.connexion.Gists.Get(*gist.ID) - - // Write the downloaded file content to the local trousseau - // data store file - fileContent := []byte(*gist.Files[github.GistFilename(remotePath)].Content) - err = ioutil.WriteFile(localPath, fileContent, 0600) - if err != nil { - return err - } - - - return nil + var gist *github.Gist + + // Fetch the user's gists list + gists, _, err := gs.connexion.Gists.List(gs.User, nil) + if err != nil { + return err + } + + // Find a gist containing trousseau data store + for _, g := range gists { + for k, _ := range g.Files { + if string(k) == remotePath { + gist = &g + break + } + } + + if gist != nil { + break + } + } + + // Download the gist file content + gist, _, err = gs.connexion.Gists.Get(*gist.ID) + + // Write the downloaded file content to the local trousseau + // data store file + fileContent := []byte(*gist.Files[github.GistFilename(remotePath)].Content) + err = ioutil.WriteFile(localPath, fileContent, 0600) + if err != nil { + return err + } + + return nil } diff --git a/scripts/autocompletion/README.md b/scripts/autocompletion/README.md new file mode 100644 index 0000000..503d603 --- /dev/null +++ b/scripts/autocompletion/README.md @@ -0,0 +1,40 @@ +# Enhancing your trousseau experience with autocompletion + +Thanks to the community, trousseau is shipped with a bunch of autocompletion scripts which will make your experience even more awesome. +Even if you don't know trousseau yet, your shell does and will be able to help you out. + +## Zsh + +To set up zsh autocompletion support for trousseau you will need to: + +1. Setup an autocompletion scripts storage path. Generally `~/.zsh/completion` should make it. Anyway, feel free to use whatever you want and adapting the following procedure. +2. Copy the trousseau zsh autocompletion functions to this path: + ```bash + cp /your/cloned/trousseau/path/scripts/autocompletion/trousseau_autocomplete.zsh ~/.zsh/completion + ``` +3. Let zsh know about the completion path. This is done by setting the `$fpath` variable: + ```bash + fpath=(~/.zsh/completion $fpath) + ``` +4. Activate zsh completion system: + ```bash + autoload -U compinit + compinit + ``` + +Now everything should be working fine, even though you should automate these steps in your `~/.zshrc` + +## Fish shell + +### Installation and configuration + +Just put it in your ``~/.config/fish/completions`` directory. +Yes, as simple as that. + +## Bash + +### Installation and configuration + +Just put it in your ``/etc/bash_completion.d`` folder as ``trousseau`` for example. +That's it. + diff --git a/scripts/autocompletion/autocompletion.bash b/scripts/autocompletion/autocompletion.bash new file mode 100644 index 0000000..8ac1943 --- /dev/null +++ b/scripts/autocompletion/autocompletion.bash @@ -0,0 +1,52 @@ +# This is free and unencumbered software released into the public domain. +# +# Anyone is free to copy, modify, publish, use, compile, sell, or +# distribute this software, either in source code form or as a compiled +# binary, for any purpose, commercial or non-commercial, and by any +# means. +# +# In jurisdictions that recognize copyright laws, the author or authors +# of this software dedicate any and all copyright interest in the +# software to the public domain. We make this dedication for the benefit +# of the public at large and to the detriment of our heirs and +# successors. We intend this dedication to be an overt act of +# relinquishment in perpetuity of all present and future rights to this +# software under copyright law. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. +# +# For more information, please refer to + +_trousseau() { + local cur prev + + cur=${COMP_WORDS[COMP_CWORD]} + prev=${COMP_WORDS[COMP_CWORD-1]} + + case ${COMP_CWORD} in + 1) + COMPREPLY=($(compgen -W "get set rename del keys show meta help create \ + push pull export import \ + add-recipient remove-recipient" ${cur})) + ;; + 2) + case ${prev} in + get|set|del) + local keys=$(trousseau keys) + COMPREPLY=($(compgen -W "${keys[*]}" ${cur})) + ;; + esac + ;; + *) + COMPREPLY=() + ;; + esac +} + +complete -F _trousseau trousseau diff --git a/scripts/autocompletion/autocompletion.fish b/scripts/autocompletion/autocompletion.fish new file mode 100644 index 0000000..83f423a --- /dev/null +++ b/scripts/autocompletion/autocompletion.fish @@ -0,0 +1,57 @@ +# Create command +complete -f -c trousseau -n '__fish_complete_subcommand' -a create -d 'Create a trousseau data store' + + +# Meta command +complete -f -c trousseau -n '__fish_complete_subcommand' -a meta -d 'Show trousseau data store meta data' + + +# Get command +complete -f -c trousseau -n '__fish_complete_subcommand' -a get -d 'Get a key\'s value from the store' + + +# Set command +complete -f -c trousseau -n '__fish_complete_subcommand' -a set -d 'Set a store key-value pair' + +# Rename command +complete -f -c trousseau -n '__fish_complete_subcommand' -a rename -d 'rename an existing key' + +# Del command +complete -f -c trousseau -n '__fish_complete_subcommand' -a del -d 'Remove a key-value pair' + +# Keys command +complete -f -c trousseau -n '__fish_complete_subcommand' -a keys -d 'Lists the store keys' + +# Show command +complete -f -c trousseau -n '__fish_complete_subcommand' -a show -d 'shows trousseau content' + +# Add-recipient command +complete -f -c trousseau -n '__fish_complete_subcommand' -a add-recipient -d 'add a recipient to the encrypted trousseau' + +# Remove-recipient command +complete -f -c trousseau -n '__fish_complete_subcommand' -a remove-recipient -d 'remove a recipient of the encrypted trousseau' + +# Import command +complete -f -c trousseau -n '__fish_complete_subcommand' -a import -d 'Import an encrypted trousseau file content' +complete -f -c trousseau -n '__fish_complete_subcommand import' -l 'overwrite' -d 'Overwrite existing trousseau file' +complete -f -c trousseau -n '__fish_complete_subcommand import' -l 'theirs' -d 'Keep the imported file value' +complete -f -c trousseau -n '__fish_complete_subcommand import' -l 'yours' -d 'Keep your current data store values' + + +# Export command +complete -f -c trousseau -n '__fish_complete_subcommand' -a export -d 'Export the encrypted trousseau to local fs' +complete -f -c trousseau -n '__fish_complete_subcommand export' -l 'overwrite' -d 'Overwrite existing trousseau file' + + +# Push command +complete -f -c trousseau -n '__fish_complete_subcommand' -a push -d 'Push the encrypted data store to remote storage' +complete -f -c trousseau -n '__fish_complete_subcommand push' -l 'overwrite' -d 'Overwrite existing trousseau file' +complete -f -c trousseau -n '__fish_complete_subcommand push' -l 'ask-password' -d 'Prompt for password' +complete -f -c trousseau -n '__fish_complete_subcommand push' -l 'ssh-private-key' -d 'Path to the ssh private key to be used' + + +# Pull command +complete -f -c trousseau -n '__fish_complete_subcommand' -a pull -d 'Push the encrypted data store to remote storage' +complete -f -c trousseau -n '__fish_complete_subcommand pull' -l 'overwrite' -d 'Overwrite existing trousseau file' +complete -f -c trousseau -n '__fish_complete_subcommand pull' -l 'ask-password' -d 'Prompt for password' +complete -f -c trousseau -n '__fish_complete_subcommand pull' -l 'ssh-private-key' -d 'Path to the ssh private key to be used' diff --git a/scripts/autocompletion/autocompletion.zsh b/scripts/autocompletion/autocompletion.zsh new file mode 100644 index 0000000..53cf495 --- /dev/null +++ b/scripts/autocompletion/autocompletion.zsh @@ -0,0 +1,50 @@ +#compdef trousseau +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +_trousseau() { + local curcontext="$curcontext" state line + typeset -A opt_args + + _arguments '1: :->trousseaucmd' \ + '2: :->trousseausubcmd' + + case $state in + trousseaucmd) + # List of available commands + compadd -Q create push pull export import add-recipient remove-recipient set get rename del keys show meta help + ;; + trousseausubcmd) + case $words[2] in + (get|set|del) + # Retrieve get/set autocomplete from "trousseau keys" + keys=("${(@f)$(trousseau keys)}") + compadd -a "$@" -- keys + ;; + *) + # Fallback to standard filename completion + compadd - * + ;; + esac + ;; + *) + # Fallback to standard filename completion + compadd - * + ;; + esac +} + +_trousseau "$@" + diff --git a/scripts/build.sh b/scripts/build.sh deleted file mode 100644 index 9723c68..0000000 --- a/scripts/build.sh +++ /dev/null @@ -1,31 +0,0 @@ -#!/bin/bash -# -# This script builds the application from source. -set -e - -# Get the parent directory of where this script is. -SOURCE="${BASH_SOURCE[0]}" -while [ -h "$SOURCE" ] ; do SOURCE="$(readlink "$SOURCE")"; done -DIR="$( cd -P "$( dirname "$SOURCE" )/.." && pwd )" - -# Get the trousseau package dir -TROUSSEAU_DIR="${DIR}/trousseau" - -# Change into that directory -cd $DIR - -# Get the version -VERSION=$(awk '/TROUSSEAU_VERSION/ { gsub("\"", ""); print $NF }' ${TROUSSEAU_DIR}/constants.go) - -# Install dependencies -echo "--> Installing dependencies" -go get ./... - -# Build! -echo "--> Building" -godep go build -ldflags "-X main.VERSION ${VERSION}" -o bin/trousseau -echo "bin/trousseau${EXTENSION} created" - -# Copy binary to gopath -cp bin/trousseau${EXTENSION} $GOPATH/bin - diff --git a/store.go b/store.go new file mode 100644 index 0000000..3a01c3f --- /dev/null +++ b/store.go @@ -0,0 +1,13 @@ +package trousseau + +type Store struct { + Meta Meta `json:"meta"` + Data KVStore `json:"store"` +} + +func NewStore(meta Meta) *Store { + return &Store{ + Meta: meta, + Data: KVStore{}, + } +} diff --git a/test_helpers.go b/test_helpers.go new file mode 100644 index 0000000..7b07468 --- /dev/null +++ b/test_helpers.go @@ -0,0 +1,44 @@ +package trousseau + +import ( + "fmt" + "path/filepath" + "reflect" + "runtime" + "testing" +) + +// assert fails the test if the condition is false. +func assert(tb testing.TB, condition bool, msg string, v ...interface{}) { + if !condition { + _, file, line, _ := runtime.Caller(1) + fmt.Printf("\033[31m%s:%d: "+msg+"\033[39m\n\n", append([]interface{}{filepath.Base(file), line}, v...)...) + tb.FailNow() + } +} + +// ok fails the test if an err is not nil. +func ok(tb testing.TB, err error) { + if err != nil { + _, file, line, _ := runtime.Caller(1) + fmt.Printf("\033[31m%s:%d: unexpected error: %s\033[39m\n\n", filepath.Base(file), line, err.Error()) + tb.FailNow() + } +} + +func notOk(tb testing.TB, err error) { + if err == nil { + _, file, line, _ := runtime.Caller(1) + fmt.Printf("\033[31m%s:%d: expected error but got nil instead\n\n", filepath.Base(file), line) + tb.FailNow() + } +} + +// equals fails the test if exp is not equal to act. +func equals(tb testing.TB, exp, act interface{}) { + if !reflect.DeepEqual(exp, act) { + _, file, line, _ := runtime.Caller(1) + fmt.Printf("\033[31m%s:%d:\n\n\texp: %#v\n\n\tgot: %#v\033[39m\n\n", filepath.Base(file), line, exp, act) + tb.FailNow() + } +} diff --git a/trousseau.go b/trousseau.go new file mode 100644 index 0000000..b563c11 --- /dev/null +++ b/trousseau.go @@ -0,0 +1,92 @@ +package trousseau + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "os" +) + +type Trousseau struct { + // Crypto public configuration attributes + CryptoType CryptoType `json:"crypto_type"` + CryptoAlgorithm CryptoAlgorithm `json:"crypto_algorithm"` + + // Encrypted data private attribute + Data []byte `json:"_data"` + + // Crypto algorithm to decryption + cryptoMapping map[CryptoAlgorithm]interface{} +} + +func OpenTrousseau(fp string) (*Trousseau, error) { + var trousseau *Trousseau + var content []byte + var err error + + content, err = ioutil.ReadFile(fp) + if err != nil { + return nil, err + } + + err = json.Unmarshal(content, &trousseau) + if err != nil { + return nil, err + } + + return trousseau, nil +} + +func (t *Trousseau) Decrypt() (*Store, error) { + var store Store + + switch t.CryptoAlgorithm { + case GPG_ENCRYPTION: + plainData, err := DecryptAsymmetricPGP(t.Data, GetPassphrase()) + if err != nil { + return nil, err + } + + err = json.Unmarshal(plainData, &store) + if err != nil { + return nil, err + } + default: + return nil, fmt.Errorf("Invalid encryption method provided") + } + + return &store, nil +} + +func (t *Trousseau) Encrypt(store *Store) error { + switch t.CryptoAlgorithm { + case GPG_ENCRYPTION: + plainData, err := json.Marshal(*store) + if err != nil { + return err + } + + t.Data, err = EncryptAsymmetricPGP(plainData, store.Meta.Recipients) + if err != nil { + return err + } + default: + return fmt.Errorf("Invalid encryption method provided") + } + + return nil +} + +func (t *Trousseau) Write(fp string) error { + jsonData, err := json.Marshal(t) + if err != nil { + return err + } + + err = ioutil.WriteFile(fp, jsonData, os.FileMode(0700)) + if err != nil { + return err + } + + return nil +} diff --git a/trousseau.rb b/trousseau.rb index 7493b5c..ed1022a 100644 --- a/trousseau.rb +++ b/trousseau.rb @@ -2,7 +2,7 @@ class Trousseau < Formula homepage 'https://github.com/oleiade/trousseau' - url 'https://github.com/oleiade/trousseau/releases/download/0.3.0/trousseau_0.3.0_darwin_amd64.zip' + url 'https://github.com/oleiade/trousseau/releases/download/0.3.1/trousseau_0.3.1_darwin_amd64.zip' sha1 '' def install diff --git a/trousseau/actions.go b/trousseau/actions.go deleted file mode 100644 index b8fd136..0000000 --- a/trousseau/actions.go +++ /dev/null @@ -1,495 +0,0 @@ -package trousseau - -import ( - "fmt" - "github.com/codegangsta/cli" - "github.com/oleiade/trousseau/crypto" - "github.com/oleiade/trousseau/dsn" - "io" - "io/ioutil" - "log" - "os" - "strings" - "time" -) - -func CreateAction(c *cli.Context) { - if !hasExpectedArgs(c.Args(), 1) { - log.Fatal("Incorrect number of arguments to 'configure' command") - } - - recipients := strings.Split(c.Args()[0], ",") - - opts := &crypto.Options{ - Algorithm: crypto.GPG_ENCRYPTION, - Passphrase: gPasshphrase, - Recipients: recipients, - } - - meta := Meta{ - CreatedAt: time.Now().String(), - LastModifiedAt: time.Now().String(), - Recipients: recipients, - TrousseauVersion: TROUSSEAU_VERSION, - } - - // Create and write empty store file - err := CreateStoreFile(gStorePath, opts, &meta) - if err != nil { - log.Fatal(err) - } - - Logger.Info("Trousseau data store succesfully created") -} - -func PushAction(c *cli.Context) { - if !hasExpectedArgs(c.Args(), 1) { - log.Fatal("Incorrect number of arguments to 'push' command") - } - - endpointDsn, err := dsn.Parse(c.Args()[0]) - if err != nil { - log.Fatal(err) - } - - switch endpointDsn.Scheme { - case "s3": - err := endpointDsn.SetDefaults(gS3Defaults) - if err != nil { - log.Fatal(err) - } - - err = uploadUsingS3(endpointDsn) - if err != nil { - log.Fatal(err) - } - Logger.Info("Trousseau data store succesfully pushed to s3") - case "scp": - privateKey := c.String("ssh-private-key") - - err := endpointDsn.SetDefaults(gScpDefaults) - if err != nil { - log.Fatal(err) - } - - if c.Bool("ask-password") == true { - password := PromptForPassword() - endpointDsn.Secret = password - } - - err = uploadUsingScp(endpointDsn, privateKey) - if err != nil { - log.Fatal(err) - } - Logger.Info("Trousseau data store succesfully pushed to ssh remote storage") - case "gist": - err = uploadUsingGist(endpointDsn) - if err != nil { - log.Fatal(err) - } - Logger.Info("Trousseau data store succesfully pushed to gist") - } -} - -func PullAction(c *cli.Context) { - if !hasExpectedArgs(c.Args(), 1) { - log.Fatal("Incorrect number of arguments to 'pull' command") - } - - endpointDsn, err := dsn.Parse(c.Args()[0]) - if err != nil { - log.Fatal(err) - } - - switch endpointDsn.Scheme { - case "s3": - err := endpointDsn.SetDefaults(gS3Defaults) - if err != nil { - log.Fatal(err) - } - - err = DownloadUsingS3(endpointDsn) - if err != nil { - log.Fatal(err) - } - Logger.Info("Trousseau data store succesfully pulled from S3") - case "scp": - privateKey := c.String("ssh-private-key") - - err := endpointDsn.SetDefaults(gScpDefaults) - if err != nil { - log.Fatal(err) - } - - if c.Bool("ask-password") == true { - password := PromptForPassword() - endpointDsn.Secret = password - } - - err = DownloadUsingScp(endpointDsn, privateKey) - if err != nil { - log.Fatal(err) - } - Logger.Info("Trousseau data store succesfully pulled from ssh remote storage") - case "gist": - err = DownloadUsingGist(endpointDsn) - if err != nil { - log.Fatal(err) - } - Logger.Info("Trousseau data store succesfully pulled from gist") - default: - if endpointDsn.Scheme == "" { - log.Fatalf("No dsn scheme supplied") - } else { - log.Fatalf("Invalid dsn scheme supplied: %s", endpointDsn.Scheme) - } - } - - Logger.Info("Trousseau data store succesfully pulled from remote storage") -} - -func ExportAction(c *cli.Context) { - if !hasExpectedArgs(c.Args(), 1) { - log.Fatal("Incorrect number of arguments to 'export' command") - } - - var err error - var inputFilePath string = gStorePath - var outputFilePath string = c.Args()[0] - - inputFile, err := os.Open(inputFilePath) - defer inputFile.Close() - if err != nil { - log.Fatal(err) - } - - outputFile, err := os.Create(outputFilePath) - defer outputFile.Close() - if err != nil { - log.Fatal(err) - } - - _, err = io.Copy(outputFile, inputFile) - if err != nil { - log.Fatal(err) - } - - Logger.Info(fmt.Sprintf("Trousseau data store exported to: %s", outputFilePath)) -} - -func ImportAction(c *cli.Context) { - if !hasExpectedArgs(c.Args(), 1) { - log.Fatal("Incorrect number of arguments to 'import' command") - } - - var err error - var importedFilePath string = c.Args()[0] - var localFilePath string = gStorePath - var strategy *ImportStrategy = new(ImportStrategy) - - // Transform provided merging startegy flags - // into a proper ImportStrategy byte. - err = strategy.FromCliContext(c) - if err != nil { - log.Fatal(err) - } - - opts := &crypto.Options{ - Algorithm: crypto.GPG_ENCRYPTION, - Passphrase: gPasshphrase, - } - - localStore, err := LoadStore(localFilePath, opts) - if err != nil { - log.Fatal(err) - } - - importedStore, err := LoadStore(importedFilePath, opts) - if err != nil { - log.Fatal(err) - } - - err = ImportStore(importedStore, localStore, *strategy) - if err != nil { - log.Fatal(err) - } - - err = localStore.Sync() - if err != nil { - log.Fatal(err) - } - - Logger.Info(fmt.Sprintf("Trousseau data store imported: %s", importedFilePath)) -} - -func AddRecipientAction(c *cli.Context) { - if !hasExpectedArgs(c.Args(), 1) { - log.Fatal("Incorrect number of arguments to 'add-recipient' command") - } - - recipient := c.Args()[0] - - opts := &crypto.Options{ - Algorithm: crypto.GPG_ENCRYPTION, - Passphrase: gPasshphrase, - } - - store, err := LoadStore(gStorePath, opts) - if err != nil { - log.Fatal(err) - } - - err = store.DataStore.Meta.AddRecipient(recipient) - - err = store.Sync() - if err != nil { - log.Fatal(err) - } - - if c.Bool("verbose") == true { - Logger.Info(fmt.Sprintf("Recipient added to trousseau data store: %s", recipient)) - } -} - -func RemoveRecipientAction(c *cli.Context) { - if !hasExpectedArgs(c.Args(), 1) { - log.Fatal("Incorrect number of arguments to 'remove-recipient' command") - } - - recipient := c.Args()[0] - - opts := &crypto.Options{ - Algorithm: crypto.GPG_ENCRYPTION, - Passphrase: gPasshphrase, - } - - store, err := LoadStore(gStorePath, opts) - if err != nil { - log.Fatal(err) - } - - err = store.Meta.RemoveRecipient(recipient) - if err != nil { - log.Fatal(err) - } - - err = store.Sync() - if err != nil { - log.Fatal(err) - } - - if c.Bool("verbose") == true { - fmt.Printf("Recipient removed from trousseau data store: %s", recipient) - } -} - -func GetAction(c *cli.Context) { - if !hasExpectedArgs(c.Args(), 1) { - log.Fatal("Incorrect number of arguments to 'get' command") - } - - opts := &crypto.Options{ - Algorithm: crypto.GPG_ENCRYPTION, - Passphrase: gPasshphrase, - } - - store, err := LoadStore(gStorePath, opts) - if err != nil { - log.Fatal(err) - } - - value, err := store.Get(c.Args()[0]) - if err != nil { - log.Fatal(err) - } - - // If the --file flag is provided - if c.String("file") != "" { - valueBytes, ok := value.(string) - if !ok { - log.Fatal(fmt.Sprintf("unable to write %s value to file", c.Args()[0])) - } - - err := ioutil.WriteFile(c.String("file"), []byte(valueBytes), 0644) - if err != nil { - log.Fatal(err) - } - } else { - Logger.Info(value) - } -} - -func SetAction(c *cli.Context) { - var key string - var value string - var err error - - // If the --file flag is provided - if c.String("file") != "" && hasExpectedArgs(c.Args(), 1) { - // And the file actually exists on file system - if pathExists(c.String("file")) { - // Then load it's content - fileContent, err := ioutil.ReadFile(c.String("file")) - if err != nil { - log.Fatal(err) - } - - value = string(fileContent) - } else { - log.Fatalf("Cannot open %s because it doesn't exist", c.String("file")) - } - } else if c.String("file") == "" && hasExpectedArgs(c.Args(), 2) { - value = c.Args()[1] - } else { - log.Fatal("Incorrect number of arguments to 'set' command") - } - - key = c.Args()[0] - - opts := &crypto.Options{ - Algorithm: crypto.GPG_ENCRYPTION, - Passphrase: gPasshphrase, - } - - store, err := LoadStore(gStorePath, opts) - if err != nil { - log.Fatal(err) - } - - err = store.Set(key, value) - if err != nil { - log.Fatal(err) - } - - err = store.Sync() - if err != nil { - log.Fatal(err) - } - - if c.Bool("verbose") == true { - Logger.Info(fmt.Sprintf("%s:%s", key, value)) - } -} - -func DelAction(c *cli.Context) { - if !hasExpectedArgs(c.Args(), 1) { - log.Fatal("Incorrect number of arguments to 'del' command") - } - - opts := &crypto.Options{ - Algorithm: crypto.GPG_ENCRYPTION, - Passphrase: gPasshphrase, - } - - store, err := LoadStore(gStorePath, opts) - if err != nil { - log.Fatal(err) - } - - err = store.Del(c.Args()[0]) - if err != nil { - log.Fatal(err) - } - - err = store.Sync() - if err != nil { - log.Fatal(err) - } - - if c.Bool("verbose") == true { - Logger.Info(fmt.Sprintf("deleted: %s", c.Args()[0])) - } -} - -func KeysAction(c *cli.Context) { - if !hasExpectedArgs(c.Args(), 0) { - log.Fatal("Incorrect number of arguments to 'keys' command") - } - - opts := &crypto.Options{ - Algorithm: crypto.GPG_ENCRYPTION, - Passphrase: gPasshphrase, - } - - store, err := LoadStore(gStorePath, opts) - if err != nil { - log.Fatal(err) - } - - keys, err := store.Keys() - if err != nil { - log.Fatal(err) - } else { - for _, k := range keys { - Logger.Info(k) - } - } -} - -func ShowAction(c *cli.Context) { - if !hasExpectedArgs(c.Args(), 0) { - log.Fatal("Incorrect number of arguments to 'show' command") - } - - opts := &crypto.Options{ - Algorithm: crypto.GPG_ENCRYPTION, - Passphrase: gPasshphrase, - } - - store, err := LoadStore(gStorePath, opts) - if err != nil { - log.Fatal(err) - } - - pairs, err := store.Items() - if err != nil { - log.Fatal(err) - } else { - for _, pair := range pairs { - Logger.Info(fmt.Sprintf("%s : %s", pair.Key, pair.Value)) - } - } -} - -func MetaAction(c *cli.Context) { - if !hasExpectedArgs(c.Args(), 0) { - log.Fatal("Incorrect number of arguments to 'meta' command") - } - - opts := &crypto.Options{ - Algorithm: crypto.GPG_ENCRYPTION, - Passphrase: gPasshphrase, - } - - store, err := LoadStore(gStorePath, opts) - if err != nil { - log.Fatal(err) - } - - pairs, err := store.Metadata() - if err != nil { - log.Fatal(err) - } - - for _, pair := range pairs { - Logger.Info(fmt.Sprintf("%s : %s", pair.Key, pair.Value)) - } -} - -// hasExpectedArgs checks whether the number of args are as expected. -func hasExpectedArgs(args []string, expected int) bool { - switch expected { - case -1: - if len(args) > 0 { - return true - } else { - return false - } - default: - if len(args) == expected { - return true - } else { - return false - } - } -} diff --git a/trousseau/commands.go b/trousseau/commands.go deleted file mode 100644 index c78cc40..0000000 --- a/trousseau/commands.go +++ /dev/null @@ -1,150 +0,0 @@ -package trousseau - -import ( - "github.com/codegangsta/cli" -) - -func CreateCommand() cli.Command { - return cli.Command{ - Name: "create", - Usage: "create the trousseau data store", - Action: CreateAction, - } -} - -func PushCommand() cli.Command { - return cli.Command{ - Name: "push", - Usage: "pushes the trousseau to remote storage", - Action: PushAction, - Flags: []cli.Flag{ - OverwriteFlag(), - AskPassword(), - VerboseFlag(), - SshPrivateKeyPathFlag(), - }, - } -} - -func PullCommand() cli.Command { - return cli.Command{ - Name: "pull", - Usage: "pull the trousseau from remote storage", - Action: PullAction, - Flags: []cli.Flag{ - OverwriteFlag(), - AskPassword(), - VerboseFlag(), - SshPrivateKeyPathFlag(), - }, - } -} - -func ExportCommand() cli.Command { - return cli.Command{ - Name: "export", - Usage: "export the encrypted trousseau to local fs", - Action: ExportAction, - Flags: []cli.Flag{ - OverwriteFlag(), - VerboseFlag(), - }, - } -} - -func ImportCommand() cli.Command { - return cli.Command{ - Name: "import", - Usage: "import an encrypted trousseau from local fs", - Action: ImportAction, - Flags: []cli.Flag{ - VerboseFlag(), - OverwriteFlag(), - TheirsFlag(), - YoursFlag(), - }, - } -} - -func AddRecipientCommand() cli.Command { - return cli.Command{ - Name: "add-recipient", - Usage: "add a recipient to the encrypted trousseau", - Action: AddRecipientAction, - Flags: []cli.Flag{ - VerboseFlag(), - }, - } -} - -func RemoveRecipientCommand() cli.Command { - return cli.Command{ - Name: "remove-recipient", - Usage: "remove a recipient of the encrypted trousseau", - Action: RemoveRecipientAction, - Flags: []cli.Flag{ - VerboseFlag(), - }, - } -} - -func SetCommand() cli.Command { - return cli.Command{ - Name: "set", - Usage: "sets a key value pair in the store", - Action: SetAction, - Flags: []cli.Flag{ - FileFlag(), - VerboseFlag(), - }, - } -} - -func GetCommand() cli.Command { - return cli.Command{ - Name: "get", - Usage: "get a value from the trousseau", - Action: GetAction, - Flags: []cli.Flag{ - FileFlag(), - }, - } -} - -func DelCommand() cli.Command { - return cli.Command{ - Name: "del", - Usage: "delete the point key pair from the store", - Action: DelAction, - Flags: []cli.Flag{ - VerboseFlag(), - }, - } -} - -func KeysCommand() cli.Command { - return cli.Command{ - Name: "keys", - Usage: "Lists the store keys", - Action: KeysAction, - Flags: []cli.Flag{ - VerboseFlag(), - }, - } -} - -func ShowCommand() cli.Command { - return cli.Command{ - Name: "show", - Usage: "shows trousseau content", - Action: ShowAction, - } -} - -func MetaCommand() cli.Command { - return cli.Command{ - Name: "meta", - Usage: "shows trousseau metadata", - Action: MetaAction, - } -} diff --git a/trousseau/flags.go b/trousseau/flags.go deleted file mode 100644 index 78e3db4..0000000 --- a/trousseau/flags.go +++ /dev/null @@ -1,65 +0,0 @@ -package trousseau - -import ( - "github.com/codegangsta/cli" - "os" - "path/filepath" -) - -func VerboseFlag() cli.BoolFlag { - return cli.BoolFlag{ - Name: "verbose", - Usage: "Set trousseau in verbose mode", - } -} - -func AskPassword() cli.BoolFlag { - return cli.BoolFlag{ - Name: "ask-password", - Usage: "Prompt for password", - } -} - -func YesFlag() cli.StringFlag { - return cli.StringFlag{ - Name: "yes", - Value: "", - Usage: "Whatever the question is, answers yes", - } -} - -func SshPrivateKeyPathFlag() cli.StringFlag { - return cli.StringFlag{ - Name: "ssh-private-key", - Value: filepath.Join(os.Getenv("HOME"), ".ssh/id_rsa"), - Usage: "Path to the ssh private key to be used", - } -} - -func OverwriteFlag() cli.BoolFlag { - return cli.BoolFlag{ - Name: "overwrite", - Usage: "Overwrite existing trousseau file", - } -} - -func TheirsFlag() cli.BoolFlag { - return cli.BoolFlag{ - Name: "theirs", - Usage: "Keep the imported file value", - } -} - -func YoursFlag() cli.BoolFlag { - return cli.BoolFlag{ - Name: "yours", - Usage: "Keep your current data store values", - } -} - -func FileFlag() cli.StringFlag { - return cli.StringFlag{ - Name: "file", - Usage: "Path to the file to be extracted", - } -} diff --git a/trousseau/globals.go b/trousseau/globals.go deleted file mode 100644 index 53e3d4c..0000000 --- a/trousseau/globals.go +++ /dev/null @@ -1,40 +0,0 @@ -package trousseau - -import ( - "os" - "path/filepath" -) - -// Global data store file path -var gStorePath string = func() string { - envPath := os.Getenv(ENV_TROUSSEAU_STORE) - - if envPath != "" { - return envPath - } - - return filepath.Join(os.Getenv("HOME"), DEFAULT_STORE_FILENAME) -}() - -// Gnupg trousseau master gpg key id -var gMasterGpgId string = os.Getenv(ENV_MASTER_GPG_ID_KEY) -var gPasshphrase string = GetPassphrase() - -// Ssh default identity file path -var gPrivateRsaKeyPath string = filepath.Join(os.Getenv("HOME"), ".ssh", "id_rsa") - -// Keyring manager service and username to use in order to -// retrieve trousseau main gpg key passphrase from system -// keyring -var gKeyringService string = os.Getenv(ENV_KEYRING_SERVICE_KEY) -var gKeyringUser string = os.Getenv(ENV_KEYRING_USER_KEY) - -// S3 and Scp dsn default values -var gS3Defaults map[string]string = map[string]string{ - "Path": "trousseau.tsk", -} -var gScpDefaults map[string]string = map[string]string{ - "Id": os.Getenv("USER"), - "Port": "22", - "Path": "trousseau.tsk", -} diff --git a/trousseau/logging.go b/trousseau/logging.go deleted file mode 100644 index d4e8e1e..0000000 --- a/trousseau/logging.go +++ /dev/null @@ -1,26 +0,0 @@ -package trousseau - -import ( - "fmt" - "github.com/Sirupsen/logrus" -) - -var Logger = logrus.New() - -type RawFormatter struct{} - -func (f *RawFormatter) Format(entry *logrus.Entry) ([]byte, error) { - var serialized []byte - var msg []byte = []byte(entry.Data["msg"].(string)) - serialized = append(serialized, msg...) - - return append(serialized, '\n'), nil -} - -func (f *RawFormatter) AppendKeyValue(serialized []byte, key, value interface{}) []byte { - if _, ok := value.(string); ok { - return append(serialized, []byte(fmt.Sprintf("%v='%v' ", key, value))...) - } else { - return append(serialized, []byte(fmt.Sprintf("%v=%v ", key, value))...) - } -} diff --git a/trousseau/store.go b/trousseau/store.go deleted file mode 100644 index cf92f3b..0000000 --- a/trousseau/store.go +++ /dev/null @@ -1,226 +0,0 @@ -package trousseau - -import ( - "encoding/json" - "errors" - "fmt" - crypto "github.com/oleiade/trousseau/crypto" - openpgp "github.com/oleiade/trousseau/crypto/openpgp" - "os" - "reflect" -) - -type Store struct { - *DataStore - path string - encryptionOpts *crypto.Options -} - -type DataStore struct { - KVStore - Meta Meta `json:"_meta"` - Container map[string]interface{} `json:"data"` -} - -type KVStore interface { - Get(string) (interface{}, error) - Set(string, interface{}) error - Del(string) error - Keys() ([]string, error) - Items() ([]KVPair, error) -} - -type KVPair struct { - Key string - Value interface{} -} - -type Encodable interface { - FromJson([]byte) error - ToJson() ([]byte, error) -} - -// LoadStore reads and decrypt a trousseau data store from the -// provided path, using the provided encryption options. -func LoadStore(path string, opts *crypto.Options) (*Store, error) { - var store *Store = NewStore(path, opts) - var err error = nil - - switch opts.Algorithm { - case crypto.GPG_ENCRYPTION: - var f *openpgp.GpgFile - var jsonData []byte - - f, err = openpgp.OpenFile(path, os.O_RDONLY, opts.Passphrase, opts.Recipients) - if err != nil { - return nil, fmt.Errorf("trousseau data store not found (%s)", err.(*os.PathError).Path) - } - defer f.Close() - - jsonData, err = f.ReadAll() - if err != nil { - return nil, err - } - - err = store.FromJson(jsonData) - if err != nil { - return nil, err - } - default: - return nil, fmt.Errorf("Invalid encryption method provided") - } - - return store, nil -} - -// Sync encrypts the store content and writes it to the disk -func (s *Store) Sync() error { - switch s.encryptionOpts.Algorithm { - case crypto.GPG_ENCRYPTION: - var f *openpgp.GpgFile - var err error - - f, err = openpgp.OpenFile(s.path, - os.O_CREATE|os.O_WRONLY, - s.encryptionOpts.Passphrase, - s.Meta.Recipients) - if err != nil { - return err - } - defer f.Close() - - jsonData, err := s.ToJson() - if err != nil { - return err - } - - _, err = f.Write([]byte(jsonData)) - if err != nil { - return err - } - default: - return fmt.Errorf("Invalid encryption method provided") - } - - return nil -} - -func NewStore(path string, encryptionOpts *crypto.Options) *Store { - return &Store{ - DataStore: NewDataStore(), - path: path, - encryptionOpts: encryptionOpts, - } -} - -func NewKVPair(key string, value interface{}) *KVPair { - return &KVPair{ - Key: key, - Value: value, - } -} - -func NewDataStore() *DataStore { - return &DataStore{ - Container: make(map[string]interface{}), - } -} - -func (ds *DataStore) ToJson() (string, error) { - encodedStore, err := json.Marshal(ds) - if err != nil { - return "", err - } - - return string(encodedStore), nil -} - -func (ds *DataStore) FromJson(jsonData []byte) error { - err := json.Unmarshal(jsonData, &ds) - if err != nil { - return err - } - - return nil -} - -// CreateStoreFile creates a trousseau file at $HOME/.trousseau -func CreateStoreFile(path string, opts *crypto.Options, meta *Meta) (err error) { - // if the store file already exists, return an error - if _, err = os.Stat(path); err == nil { - return errors.New("Store file already exists") - } - - store := NewStore(path, opts) - store.Meta = *meta - - err = store.Sync() - if err != nil { - return err - } - - return nil -} - -// GetStoreKey fetches a key from the trousseau file store -func (ds *DataStore) Get(key string) (interface{}, error) { - value, ok := ds.Container[key] - if !ok { - return "", errors.New(fmt.Sprintf("Key %s does not exist", key)) - } - - return value, nil -} - -// SetStoreKey sets a key value pair in the store -func (ds *DataStore) Set(key string, value interface{}) error { - ds.Container[key] = value - - return nil -} - -// DelStoreKey deletes a key value pair from the trousseau file -func (ds *DataStore) Del(key string) error { - delete(ds.Container, key) - - return nil -} - -// ListStoreKeys lists the keys contained in the trousseau store file -func (ds *DataStore) Keys() ([]string, error) { - index := 0 - keys := make([]string, len(ds.Container)) - - for key, _ := range ds.Container { - keys[index] = key - index++ - } - - return keys, nil -} - -func (ds *DataStore) Items() ([]KVPair, error) { - index := 0 - pairs := make([]KVPair, len(ds.Container)) - - for key, value := range ds.Container { - pairs[index] = *NewKVPair(key, value) - index++ - } - - return pairs, nil -} - -func (s *Store) Metadata() ([]KVPair, error) { - metaType := reflect.TypeOf(s.DataStore.Meta) - metaValue := reflect.ValueOf(s.DataStore.Meta) - pairs := make([]KVPair, metaType.NumField()) - - for i := 0; i < metaType.NumField(); i++ { - key := metaType.Field(i).Name - value := metaValue.FieldByName(key).Interface() - pairs[i] = *NewKVPair(key, value) - } - - return pairs, nil -} diff --git a/trousseau/store_test.go b/trousseau/store_test.go deleted file mode 100644 index 55a7dbc..0000000 --- a/trousseau/store_test.go +++ /dev/null @@ -1,6 +0,0 @@ -package trousseau - -import ( -// "github.com/stretchr/testify/assert" -// "testing" -) diff --git a/trousseau_test.go b/trousseau_test.go new file mode 100644 index 0000000..fddedad --- /dev/null +++ b/trousseau_test.go @@ -0,0 +1,37 @@ +package trousseau + +import ( + "encoding/json" + "testing" + + "github.com/oleiade/tempura" + "os" +) + +func TestOpenTrousseau(t *testing.T) { + testData := make(map[string]interface{}) + testData["crypto_type"] = ASYMMETRIC_ENCRYPTION + testData["crypto_algorithm"] = GPG_ENCRYPTION + testData["_data"] = []byte("abc") + + jsonData, _ := json.Marshal(&testData) + tmp, _ := tempura.FromBytes("/tmp", "trousseau", jsonData) + defer tmp.File.Close() + defer os.Remove(tmp.File.Name()) + + tr, err := OpenTrousseau(tmp.File.Name()) + if err != nil { + t.Fatal(err) + } + + assert(t, tr.CryptoType == ASYMMETRIC_ENCRYPTION, "Wrong encryption type") + assert(t, tr.CryptoAlgorithm == GPG_ENCRYPTION, "Wrong encryption algorithm") + equals(t, tr.Data, []byte("abc")) +} + +func TestOpenTrousseau_returns_err_when_file_does_not_exist(t *testing.T) { + _, err := OpenTrousseau("/does/not/exist") + if err == nil { + t.Fatalf("OpenTrousseau function didn't failed while loading non existing file") + } +} diff --git a/upgrade.go b/upgrade.go new file mode 100644 index 0000000..fa17c40 --- /dev/null +++ b/upgrade.go @@ -0,0 +1,192 @@ +package trousseau + +import ( + "encoding/json" + "fmt" + "github.com/oleiade/trousseau/crypto/openpgp" + "sort" +) + +type VersionMatcher func([]byte) bool +type UpgradeClosure func([]byte) ([]byte, error) + +var UpgradeClosures map[string]UpgradeClosure = map[string]UpgradeClosure{ + "0.3.0": upgradeZeroDotThreeToNext, +} + +var VersionDiscoverClosures map[string]VersionMatcher = map[string]VersionMatcher{ + "0.3.0": isVersionZeroDotThreeDotZero, + "0.3.1": isVersionZeroDotThreeDotOne, +} + +func UpgradeFrom(startVersion string, d []byte, mapping map[string]UpgradeClosure) ([]byte, error) { + var versions []string + var out []byte = d + var err error + + for version, _ := range mapping { + if version >= startVersion { + versions = append(versions, version) + } + } + sort.Strings(versions) + + for idx, version := range versions { + var versionRepr string + + if idx == (len(versions) - 1) { + versionRepr = TROUSSEAU_VERSION + } else { + versionRepr = version + } + + upgradeClosure := mapping[version] + out, err = upgradeClosure(out) + if err != nil { + return nil, fmt.Errorf("Upgrading trousseau data store to version %s: failure\nReason: %s", versionRepr, err.Error()) + } else { + fmt.Printf("Upgrading trousseau data store to version %s: success\n", versionRepr) + } + } + + return out, nil +} + +func DiscoverVersion(d []byte, mapping map[string]VersionMatcher) string { + var versions []string + + for version, _ := range mapping { + versions = append(versions, version) + } + sort.Strings(versions) + + for _, version := range versions { + if mapping[version](d) == true { + return version + } + } + + return "" +} + +func isVersionZeroDotThreeDotZero(d []byte) bool { + if len(d) >= len(openpgp.PGP_MESSAGE_HEADER) && + string(d[0:len(openpgp.PGP_MESSAGE_HEADER)]) == openpgp.PGP_MESSAGE_HEADER { + return true + } + + return false +} + +func isVersionZeroDotThreeDotOne(d []byte) bool { + var zeroDotFourStore map[string]interface{} = make(map[string]interface{}) + var zeroDotFourKeys []string = []string{"crypto_algorithm", "crypto_type", "_data"} + + err := json.Unmarshal(d, &zeroDotFourStore) + if err != nil { + return false + } + + for _, key := range zeroDotFourKeys { + _, in := zeroDotFourStore[key] + if !in { + return false + } + } + + return true +} + +func upgradeZeroDotThreeToNext(d []byte) ([]byte, error) { + var err error + + // Assert input data are in the expected version format + validVersion := isVersionZeroDotThreeDotZero(d) + if !validVersion { + return nil, fmt.Errorf("Provided input data not matching version 0.3 format") + } + + // Declaring and instanciating a type matching + // the 0.3 version store format + legacyStore := struct { + Meta map[string]interface{} `json:"_meta"` + Data map[string]interface{} `json:"data"` + }{ + Meta: make(map[string]interface{}), + Data: make(map[string]interface{}), + } + + // Retrieve secret ring keys from openpgp + decryptionKeys, err := openpgp.ReadSecRing(openpgp.SecringFile) + if err != nil { + return nil, err + } + + // Decrypt store version 0.3 (aka legacy) + plainData, err := openpgp.Decrypt(decryptionKeys, string(d), GetPassphrase()) + if err != nil { + return nil, err + } + + // Unmarshal it's content into the legacyStore + err = json.Unmarshal(plainData, &legacyStore) + if err != nil { + return nil, err + } + + // Declaring and instanciating a type matching + // the 0.4 version store format so we can inject the + // legacy data into it + newStore := struct { + Meta map[string]interface{} `json:"meta"` + Data map[string]interface{} `json:"store"` + }{ + Meta: legacyStore.Meta, + Data: legacyStore.Data, + } + + // Encode it in json + newStoreData, err := json.Marshal(newStore) + if err != nil { + return nil, err + } + + // Retrieve legacyStore recipients + var recipients []string + for _, r := range legacyStore.Meta["recipients"].([]interface{}) { + recipients = append(recipients, r.(string)) + } + + // Read the public openpgp ring to retrieve the recipients public keys + encryptionKeys, err := openpgp.ReadPubRing(openpgp.PubringFile, recipients) + if err != nil { + return nil, err + } + + // Encrypt the encoded newStore content + encryptedData := openpgp.Encrypt(encryptionKeys, string(newStoreData)) + if err != nil { + return nil, err + } + + // Declaring and instanciating a type matching + // the 0.4 version trousseau data store format + // so we can inject the encrypted store into it + newTrousseau := struct { + CryptoAlgorithm CryptoAlgorithm `json:"crypto_algorithm"` + CryptoType CryptoType `json:"crypto_type"` + Data []byte `json:"_data"` + }{ + CryptoAlgorithm: GPG_ENCRYPTION, + CryptoType: ASYMMETRIC_ENCRYPTION, + Data: encryptedData, + } + + // Encode the new trousseau data store + trousseau, err := json.Marshal(newTrousseau) + if err != nil { + return nil, err + } + + return trousseau, nil +} diff --git a/upgrade_test.go b/upgrade_test.go new file mode 100644 index 0000000..8f989c6 --- /dev/null +++ b/upgrade_test.go @@ -0,0 +1,100 @@ +package trousseau + +import ( + "encoding/json" + "github.com/oleiade/trousseau/crypto/openpgp" + "testing" +) + +func TestIsVersionZeroDotThree(t *testing.T) { + data := []byte(openpgp.PGP_MESSAGE_HEADER + + "12kjd091jd192jd0192jd" + + openpgp.PGP_MESSAGE_FOOTER) + + assert(t, + isVersionZeroDotThreeDotZero(data) == true, + "Input test data were suppose to match version 0.3.N") +} + +func TestIsVersionZeroDotThree_fails_with_data_shorter_than_pgp_header(t *testing.T) { + data := []byte("abc123") + assert(t, + isVersionZeroDotThreeDotZero(data) == false, + "Input test data weren't suppose to match version 0.3.N") +} + +func TestIsVersionZeroDotFour(t *testing.T) { + store := map[string]interface{}{ + "crypto_algorithm": 0, + "crypto_type": 1, + "_data": "oqwimdoqiwmd0qwd0iq0wdijqw9d0", + } + + data, err := json.Marshal(store) + if err != nil { + t.Error(err) + } + + assert(t, + isVersionZeroDotThreeDotOne(data) == true, + "Input test data were suppose to match version 0.4.N") +} + +func TestDiscoverVersion_with_only_one_valid_version_in_mapping(t *testing.T) { + var data []byte = []byte(openpgp.PGP_MESSAGE_HEADER + + "12kjd091jd192jd0192jd" + + openpgp.PGP_MESSAGE_FOOTER) + var mapping map[string]VersionMatcher = map[string]VersionMatcher{ + "0.3.0": isVersionZeroDotThreeDotZero, + } + + assert(t, + DiscoverVersion(data, mapping) == "0.3.0", + "Version 0.3.0 was supposed to be discovered") +} + +func TestDiscoverVersion_with_two_valid_versions_in_mapping(t *testing.T) { + var store map[string]interface{} = map[string]interface{}{ + "crypto_algorithm": 0, + "crypto_type": 1, + "_data": "oqwimdoqiwmd0qwd0iq0wdijqw9d0", + } + var mapping map[string]VersionMatcher = map[string]VersionMatcher{ + "0.3.0": isVersionZeroDotThreeDotZero, + "0.4.0": isVersionZeroDotThreeDotOne, + } + + data, err := json.Marshal(store) + if err != nil { + t.Error(err) + } + + assert(t, + DiscoverVersion(data, mapping) == "0.4.0", + "Version 0.4.0 was supposed to be discovered") +} + +func TestDiscoverVersion_with_two_matching_version_returns_the_lowest(t *testing.T) { + var data []byte = []byte(openpgp.PGP_MESSAGE_HEADER + + "12kjd091jd192jd0192jd" + + openpgp.PGP_MESSAGE_FOOTER) + var mapping map[string]VersionMatcher = map[string]VersionMatcher{ + "0.3.0": isVersionZeroDotThreeDotZero, + "0.3.1": isVersionZeroDotThreeDotZero, + } + + assert(t, + DiscoverVersion(data, mapping) == "0.3.0", + "Version 0.3.0 was supposed to be discovered") + +} + +func TestDiscoverVersion_with_no_matching_version(t *testing.T) { + var data []byte = []byte("abc") + var mapping map[string]VersionMatcher = map[string]VersionMatcher{ + "0.3.0": isVersionZeroDotThreeDotZero, + "0.4.0": isVersionZeroDotThreeDotOne, + } + + equals(t, DiscoverVersion(data, mapping), "") +} diff --git a/trousseau/upload.go b/upload.go similarity index 75% rename from trousseau/upload.go rename to upload.go index fbd29f8..fb55870 100644 --- a/trousseau/upload.go +++ b/upload.go @@ -4,15 +4,15 @@ import ( "fmt" "github.com/crowdmob/goamz/aws" "github.com/oleiade/trousseau/dsn" + "github.com/oleiade/trousseau/remote/gist" "github.com/oleiade/trousseau/remote/s3" "github.com/oleiade/trousseau/remote/ssh" - "github.com/oleiade/trousseau/remote/gist" ) // uploadUsingS3 executes the whole process of pushing // the trousseau data store file to s3 remote storage // using the provided environment. -func uploadUsingS3(dsn *dsn.Dsn) error { +func UploadUsingS3(dsn *dsn.Dsn) error { awsAuth := aws.Auth{AccessKey: dsn.Id, SecretKey: dsn.Secret} awsRegion, ok := aws.Regions[dsn.Port] @@ -26,7 +26,7 @@ func uploadUsingS3(dsn *dsn.Dsn) error { return fmt.Errorf("Unable to connect to S3") } - err = s3Storage.Push(gStorePath, dsn.Path) + err = s3Storage.Push(GetStorePath(), dsn.Path) if err != nil { return err } @@ -37,7 +37,7 @@ func uploadUsingS3(dsn *dsn.Dsn) error { // uploadUsingScp executes the whole process of pushing // the trousseau data store file to scp remote storage // using the provided environment. -func uploadUsingScp(dsn *dsn.Dsn, privateKey string) (err error) { +func UploadUsingScp(dsn *dsn.Dsn, privateKey string) (err error) { keychain := new(ssh.Keychain) keychain.AddPEMKey(privateKey) @@ -51,7 +51,7 @@ func uploadUsingScp(dsn *dsn.Dsn, privateKey string) (err error) { return err } - err = scpStorage.Push(gStorePath, dsn.Path) + err = scpStorage.Push(GetStorePath(), dsn.Path) if err != nil { return err } @@ -62,14 +62,14 @@ func uploadUsingScp(dsn *dsn.Dsn, privateKey string) (err error) { // uploadUsingGist executes the whole process of pushing // the trousseau data store file to gist remote storage // using the provided dsn informations. -func uploadUsingGist(dsn *dsn.Dsn) (err error) { - gistStorage := gist.NewGistStorage(dsn.Id, dsn.Secret) - gistStorage.Connect() +func UploadUsingGist(dsn *dsn.Dsn) (err error) { + gistStorage := gist.NewGistStorage(dsn.Id, dsn.Secret) + gistStorage.Connect() - err = gistStorage.Push(gStorePath, dsn.Path) - if err != nil { - return err - } + err = gistStorage.Push(GetStorePath(), dsn.Path) + if err != nil { + return err + } - return nil + return nil } diff --git a/trousseau/utils.go b/utils.go similarity index 86% rename from trousseau/utils.go rename to utils.go index 9f8985f..91d61dd 100644 --- a/trousseau/utils.go +++ b/utils.go @@ -5,7 +5,7 @@ import ( ) // exists returns whether the given file or directory exists or not -func pathExists(path string) bool { +func PathExists(path string) bool { _, err := os.Stat(path) if err == nil { return true