From 56e4f515a9a681c143520eef0a857a73f56450da Mon Sep 17 00:00:00 2001 From: Ben Whaley Date: Sun, 29 Sep 2019 16:56:18 -0700 Subject: [PATCH] Add support for .ssmshrc configuration file --- Gopkg.lock | 22 ++++ Gopkg.toml | 4 + README.md | 207 ++++++++++++++++++------------- aws/aws.go | 20 +++ commands/commands.go | 1 + commands/cp.go | 1 + commands/decrypt.go | 24 +++- commands/key.go | 46 +++++++ commands/ls.go | 4 - commands/put.go | 41 ++++-- config/config.go | 45 +++++++ parameterstore/parameterstore.go | 101 ++++++++++----- ssmsh.go | 27 ++-- 13 files changed, 400 insertions(+), 143 deletions(-) create mode 100644 aws/aws.go create mode 100644 commands/key.go create mode 100644 config/config.go diff --git a/Gopkg.lock b/Gopkg.lock index d694c745..864342cd 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -114,6 +114,27 @@ pruneopts = "" revision = "a129542de9ae0895210abff9c95d67a1f33cb93d" +[[projects]] + digest = "1:bb864e9881b2c241fc6348ba5ed57e6ccf8a675903f1f5c3d81c1b3d7cc4b8f8" + name = "gopkg.in/gcfg.v1" + packages = [ + ".", + "scanner", + "token", + "types", + ] + pruneopts = "" + revision = "61b2c08bc8f6068f7c5ca684372f9a6cb1c45ebe" + version = "v1.2.3" + +[[projects]] + digest = "1:ceec7e96590fb8168f36df4795fefe17051d4b0c2acc7ec4e260d8138c4dafac" + name = "gopkg.in/warnings.v0" + packages = ["."] + pruneopts = "" + revision = "ec4a0fea49c7b46c2aeb0b51aac55779c607e52b" + version = "v0.1.2" + [solve-meta] analyzer-name = "dep" analyzer-version = 1 @@ -124,6 +145,7 @@ "github.com/aws/aws-sdk-go/service/ssm", "github.com/aws/aws-sdk-go/service/ssm/ssmiface", "github.com/mattn/go-shellwords", + "gopkg.in/gcfg.v1", ] solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index 347c9673..09690ef6 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -28,3 +28,7 @@ [[constraint]] branch = "master" name = "github.com/aws/aws-sdk-go" + +[[constraint]] + version = "v1.2.3" + name = "gopkg.in/gcfg.v1" diff --git a/README.md b/README.md index 617fcd39..f080a4b6 100644 --- a/README.md +++ b/README.md @@ -17,142 +17,180 @@ ssmsh is an interactive shell for the EC2 Parameter Store. Features: 1. Download [here](https://github.com/bwhaley/ssmsh/releases) or clone and build from this repo. 2. Set up [AWS credentials](http://docs.aws.amazon.com/sdk-for-go/v1/developer-guide/configuring-sdk.html#specifying-credentials). +## Configuration + +You can set up a `.ssmshrc` to configure `ssmsh`. By default, `ssmsh` will load `~/.ssmshrc` if it exists. Use the `-config` argument to set a different path. + +```bash +[default] +type=SecureString +overwrite=true +decrypt=true +profile=my-profile +region=us-east-1 +key=3example-89a6-4880-b544-73ad3db2ff3b +``` + +A few notes on configuration: +* When setting the region, the `AWS_REGION` env var takes top priority, followed by the setting in `.ssmshrc`, followed by the value set in the AWS profile (if configured) +* When setting the profile, the `AWS_PROFILE` env var takes top priority, followed by the setting in `.ssmshrc` +* If you set a KMS key, it will only work in the region where that key is located. You can use the `key` command while in the shell to change the key. + ## Usage ### Help ```bash -/>help +/> help Commands: - cd change your relative location within the parameter store - clear clear the screen - cp copy source to dest - decrypt toggle parameter decryption - exit exit the program - get get parameters - help display help - history get parameter history - ls list parameters - mv move parameters - policy create named parameter policy - profile switch the active AWS credentials profile - put set parameter - region change region - rm remove parameters +cd change your relative location within the parameter store +clear clear the screen +cp copy source to dest +decrypt toggle parameter decryption +exit exit the program +get get parameters +help display help +history get parameter history +key set the KMS key +ls list parameters +mv move parameters +policy create named parameter policy +profile switch to a different AWS IAM profile +put set parameter +region change region +rm remove parameters ``` + ### List contents of a path +Note: Listing a large number of parameters may take a long time because the maximum number of results per API call is 10. Press ^C to interrupt if a listing is taking too long. Example usage: ```bash -/>ls /House -Lannister/ -Stark/ -Targaryen/ +/> ls +dev/ +/> ls -r +/dev/app/url +/dev/db/password +/dev/db/username +/> ls /dev/app +url +/dev> ``` ### Change dir and list from current working dir ```bash -/>cd /House -/House>ls -Lannister/ -Stark/ -Targaryen/ +/> cd /dev +/dev> ls +app/ +db/ +/dev> ``` -### Get parameter +### Get a parameter ```bash -/>cd /House/Stark -/House/Stark>get JonSnow +/> get /dev/db/username [{ - Name: "/House/Stark/JonSnow", - Type: "String", - Value: "Bastard", - Version: 2 -}] -``` - -### Get encrypted parameters -```bash -/>cd /House/Stark -/House/Stark>get VerySecretInformation -[{ - Name: "/House/Stark/VerySecretInformation", + ARN: "arn:aws:ssm:us-east-1:012345678901:parameter/dev/db/username", + LastModifiedDate: 2019-09-29 23:22:19 +0000 UTC, + Name: "/dev/db/username", Type: "SecureString", - Value: "AQICAHhBW4N+....", + Value: "foo", Version: 1 }] -/House/Stark>decrypt -Decrypt is true -/House/Stark>get VerySecretInformation +/> cd /dev/db +/dev/db> get ../app/url [{ - Name: "/House/Stark/VerySecretInformation", + ARN: "arn:aws:ssm:us-east-1:318677964956:parameter/dev/app/url", + LastModifiedDate: 2019-09-29 23:22:49 +0000 UTC, + Name: "/dev/app/url", Type: "SecureString", - Value: "The three-eyed raven lives.", + Value: "https://www.example.com", Version: 1 }] +/dev/db> +``` + +### Toggle decryption for SecureString parameters +```bash +/> decrypt +Decrypt is false +/> decrypt true +Decrypt is true +/> ``` ### Get parameter history ```bash -/>history /House/Stark/JonSnow +/> history /dev/app/url [{ - Description: "Bastard son of Eddard", - LastModifiedDate: 2017-11-06 23:59:02 +0000 UTC, - LastModifiedUser: "bwhaley", - Name: "/House/Stark/JonSnow", - Type: "String", - Value: "Bastard", + KeyId: "alias/aws/ssm", + Labels: [], + LastModifiedDate: 2019-09-29 23:22:49 +0000 UTC, + LastModifiedUser: "arn:aws:iam::318677964956:root", + Name: "/dev/app/url", + Policies: [], + Tier: "Standard", + Type: "SecureString", + Value: "https://www.example.com", Version: 1 -} { - Description: "Bastard son of Eddard Stark, man of the Night's Watch", - LastModifiedDate: 2017-11-06 23:59:05 +0000 UTC, - LastModifiedUser: "bwhaley", - Name: "/House/Stark/JonSnow", - Type: "String", - Value: "Bastard", - Version: 2 }] ``` ### Copy a parameter ```bash -/> cp /House/Stark/SansaStark /House/Lannister/SansaStark +/> cp /dev/app/url /test/app/url +/> ls -r /dev/app /test/app +/dev/app: +/dev/app/url +/test/app: +/test/app/url ``` ### Copy an entire hierarchy ```bash -/> cp -R /House/Stark /House/Targaryen +/> cp -r /dev /test +/> ls -r /test +/test/app/url +/test/db/password +/test/db/username ``` ### Remove parameters ```bash -/> rm /House/Stark/EddardStark -/> cd /House/Stark -/House/Stark> rm -r ../Lannister +/> rm /test/app/url +/> ls -r /test +/test/db/password +/test/db/username +/> rm -r /test +/> ls -r /test +/> ``` ### Put new parameters ```bash +Multiline: /> put Input options. End with a blank line. -... name=/House/Targaryen/DaenerysTargaryen -... value="Khaleesi" +... name=/dev/app/domain +... value="www.example.com" ... type=String -... description="Mother of Dragons" +... description="The domain of the app in dev" ... /> ``` -Alternatively: +Single line version: ```bash -/> put name=/House/Targaryen/Daenerys value="Khaleesi" type=String description="Mother of Dragons" +/> put name=/dev/app/domain value="www.example.com" type=String description="The domain of the app in dev" ``` ### Advanced parameters with policies +Use [parameter policies](https://docs.aws.amazon.com/systems-manager/latest/userguide/parameter-store-policies.html) to do things like expire (automatically delete) parameters at a specified time: ```bash -/> policy RobbStarkExpiration Expiration(Timestamp=2013-03-31T21:00:00.000Z) +/> policy urlExpiration Expiration(Timestamp=2013-03-31T21:00:00.000Z) /> policy ReminderPolicy ExpirationNotification(Before=30,Unit=days) NoChangeNotification(After=7,Unit=days) -/> put name=/House/Stark/Robb value="King in the North" type=String policies=[RobbStarkExpiration,ReminderPolicy] +/> put name=/dev/app/url value="www.example.com" type=String policies=[urlExpiration,ReminderPolicy] ``` -### Switch profile +### Switch AWS profile +Switches to another profile as configured in `~/.aws/config`. ```bash /> profile default @@ -170,19 +208,21 @@ eu-central-1 ``` ### Operate on other regions - +A few examples of working with regions. ```bash -/> put region=eu-central-1 name=/House/Targaryen/DaenerysTargaryen value="Khaleesi" type=String description="Mother of Dragons" -/> cp -r us-west-2:/House/Stark/ eu-central-1:/House/Targaryen -/> get eu-central-1:/House/Stark/JonSnow us-west-2:/House/Stark/JonSnow +/> put region=eu-central-1 name=/dev/app/domain value="www.example.com" type=String description="The domain of the app in dev" +/> cp -r us-east-1:/dev us-west-2:/dev +/> ls -r us-west-2:/dev +/> region us-east-2 +/> get us-west-2:/dev/db/username us-east-1:/dev/db/password ``` ### Read commands in batches ```bash $ cat << EOF > commands.txt -put name=/House/Targaryen/DaenerysTargaryen value="Khaleesi" type=String description="Mother of Dragons" -rm /House/Stark/RobStark -cp -R /House/Baratheon /House/Lannister +put name=/dev/app/domain value="www.example.com" type=String description="The domain of the app in dev" +rm /dev/app/domain +cp -r /dev /test EOF $ ssmsh -file commands.txt $ cat commands.txt | ssmsh -file - # Read commands from STDIN @@ -190,7 +230,7 @@ $ cat commands.txt | ssmsh -file - # Read commands from STDIN ### Inline commands ``` -$ ssmsh put name=/House/Lannister/CerseiLannister value="Noble" description="Daughter of Tywin" type=string +$ ssmsh put name=/dev/app/domain value="www.example.com" type=String description="The domain of the app in dev" ``` ## todo (maybe) @@ -198,7 +238,6 @@ $ ssmsh put name=/House/Lannister/CerseiLannister value="Noble" description="Dau * [ ] Release via homebrew * [ ] Copy between accounts using profiles * [ ] Find parameter -* [ ] update parameter (put with fewer required fields) * [ ] Integration w/ CloudWatch Events for scheduled parameter updates * [ ] Export/import * [ ] Support globbing and/or regex diff --git a/aws/aws.go b/aws/aws.go new file mode 100644 index 00000000..36bbf21b --- /dev/null +++ b/aws/aws.go @@ -0,0 +1,20 @@ +package aws + +import ( + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" +) + +func NewSession(region, profile string) *session.Session { + return session.Must( + session.NewSessionWithOptions( + session.Options{ + SharedConfigState: session.SharedConfigEnable, + Config: aws.Config{ + Region: aws.String(region), + }, + Profile: profile, + }, + ), + ) +} diff --git a/commands/commands.go b/commands/commands.go index 43763136..c73f7fb0 100644 --- a/commands/commands.go +++ b/commands/commands.go @@ -21,6 +21,7 @@ func Init(_shell *ishell.Shell, _ps *parameterstore.ParameterStore) { registerCommand("decrypt", "toggle parameter decryption", decrypt, decryptUsage) registerCommand("get", "get parameters", get, getUsage) registerCommand("history", "get parameter history", history, historyUsage) + registerCommand("key", "set the KMS key", key, keyUsage) registerCommand("ls", "list parameters", ls, lsUsage) registerCommand("mv", "move parameters", mv, mvUsage) registerCommand("policy", "create named parameter policy", policy, policyUsage) diff --git a/commands/cp.go b/commands/cp.go index 7ab5955d..378309a1 100644 --- a/commands/cp.go +++ b/commands/cp.go @@ -15,6 +15,7 @@ func cp(c *ishell.Context) { if len(paths) != 2 { shell.Println("Expected src and dst") shell.Println(cpUsage) + return } err := ps.Copy(parsePath(paths[0]), parsePath(paths[1]), recurse) if err != nil { diff --git a/commands/decrypt.go b/commands/decrypt.go index c561a17b..59531c42 100644 --- a/commands/decrypt.go +++ b/commands/decrypt.go @@ -1,6 +1,8 @@ package commands import ( + "strconv" + "github.com/abiosoft/ishell" ) @@ -9,8 +11,26 @@ decrypt usage: decrypt Toggles decryption of SecureString parameter values. Default is false. ` -// decrypt toggles parameter decryption for SecureString values +const decryptError = "value for decrypt must be boolean" + +// decrypt determines parameter decryption for SecureString values func decrypt(c *ishell.Context) { - ps.Decrypt = !ps.Decrypt + if len(c.Args) == 1 { + v, err := strconv.ParseBool(c.Args[0]) + if err != nil { + shell.Println(decryptError) + } + + switch v { + case true: + ps.Decrypt = true + case false: + ps.Decrypt = false + default: + shell.Println(decryptError) + } + } else if len(c.Args) > 1 { + shell.Println(decryptError) + } shell.Println("Decrypt is", ps.Decrypt) } diff --git a/commands/key.go b/commands/key.go new file mode 100644 index 00000000..04c82ec1 --- /dev/null +++ b/commands/key.go @@ -0,0 +1,46 @@ +package commands + +import ( + "fmt" + + "github.com/abiosoft/ishell" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/kms" + saws "github.com/bwhaley/ssmsh/aws" +) + +const keyUsage string = ` +key ARN|ID +Set the KMS key ARN (or ID) to use with SecureString parameters +` + +func key(c *ishell.Context) { + if len(c.Args) != 1 { + shell.Println(keyUsage) + } + if err := checkKey(c.Args[0]); err != nil { + shell.Println(err) + } + ps.Key = c.Args[0] +} + +func checkKey(key string) (err error) { + client := kms.New(saws.NewSession(ps.Region, ps.Profile)) + input := kms.ListKeysInput{} + for { + resp, err := client.ListKeys(&input) + if err != nil { + return err + } + for _, keyEntry := range resp.Keys { + if aws.StringValue(keyEntry.KeyId) == key || aws.StringValue(keyEntry.KeyArn) == key { + return nil + } + } + if resp.NextMarker == nil { + break + } + input.Marker = resp.NextMarker + } + return fmt.Errorf("key %s not found in this region", key) +} diff --git a/commands/ls.go b/commands/ls.go index cbdcdfde..c28b986f 100644 --- a/commands/ls.go +++ b/commands/ls.go @@ -24,10 +24,6 @@ func ls(c *ishell.Context) { if len(paths) == 0 { paths = append(paths, ps.Cwd) } - if ps.Cwd == parameterstore.Delimiter { - shell.Println("Warning: Listing a large number of parameters may take a long time.") - shell.Println("Press ^C to interrupt.") - } for _, p := range paths { pathList, err = list(p, recurse) if err != nil { diff --git a/commands/put.go b/commands/put.go index 810bf02c..9fe70ab2 100644 --- a/commands/put.go +++ b/commands/put.go @@ -43,7 +43,15 @@ var putParamRegion string func put(c *ishell.Context) { var err error var resp *ssm.PutParameterOutput + putParamInput = ssm.PutParameterInput{} + err = setDefaults(&putParamInput) + if err != nil { + shell.Println(err) + return + } + + // Read args for values var r bool if len(c.Args) == 0 { r = multiLinePut() @@ -53,15 +61,14 @@ func put(c *ishell.Context) { if !r { return } + if putParamInput.Name == nil || putParamInput.Value == nil || putParamInput.Type == nil { shell.Println("Error: name, type and value are required.") return } - if putParamRegion == "" { - putParamRegion = ps.Region - } + resp, err = ps.Put(&putParamInput, putParamRegion) if err != nil { shell.Println("Error: ", err) @@ -73,11 +80,27 @@ func put(c *ishell.Context) { } } +// setDefaults sets parameter settings according to the defaults +func setDefaults(param *ssm.PutParameterInput) (err error) { + param.SetOverwrite(ps.Overwrite) + if ps.Key != "" { + param.SetKeyId(ps.Key) + } + param.SetType(ps.Type) + err = validateType(ps.Type) + if err != nil { + return err + } + putParamRegion = ps.Region + return nil +} + func multiLinePut() bool { // Set the prompt explicitly rather than use SetMultiPrompt // due to the unexpected 2nd line behavior shell.SetPrompt("... ") defer setPrompt(ps.Cwd) + shell.Println("Input options. End with a blank line.") str := shell.ReadMultiLinesFunc(putOptions) if str == "" { @@ -160,27 +183,27 @@ func validateValue(s string) (err error) { func validateName(s string) (err error) { if strings.HasPrefix(s, parameterstore.Delimiter) { - putParamInput.Name = aws.String(s) + putParamInput.SetName(s) } else { - putParamInput.Name = aws.String(ps.Cwd + parameterstore.Delimiter + s) + putParamInput.SetName(ps.Cwd + parameterstore.Delimiter + s) } return nil } func validateDescription(s string) (err error) { - putParamInput.Description = aws.String(s) + putParamInput.SetDescription(s) return nil } // TODO validate key func validateKey(s string) (err error) { - putParamInput.KeyId = aws.String(s) + putParamInput.SetKeyId(s) return nil } // TODO validate pattern func validatePattern(s string) (err error) { - putParamInput.AllowedPattern = aws.String(s) + putParamInput.SetAllowedPattern(s) return nil } @@ -190,7 +213,7 @@ func validateOverwrite(s string) (err error) { shell.Println("overwrite must be true or false") return err } - putParamInput.Overwrite = aws.Bool(overwrite) + putParamInput.SetOverwrite(overwrite) return nil } diff --git a/config/config.go b/config/config.go new file mode 100644 index 00000000..2181ec50 --- /dev/null +++ b/config/config.go @@ -0,0 +1,45 @@ +package config + +import ( + "os" + "path/filepath" + + gcfg "gopkg.in/gcfg.v1" +) + +const DefaultConfigFileName = ".ssmshrc" + +// Config holds the default shell configuration +type Config struct { + Default struct { + Decrypt bool + Key string + Profile string + Region string + Overwrite bool + Type string + } +} + +// ReadConfig reads ssmsh configuration from a given file +func ReadConfig(cfgFile string) (Config, error) { + if cfgFile == "" { + homeDir, err := os.UserHomeDir() + if err != nil { + return Config{}, err + } + cfgFile = filepath.Join(homeDir, DefaultConfigFileName) + } + + if _, err := os.Stat(cfgFile); os.IsNotExist(err) { + // Config file is not required, just return an empty config + return Config{}, nil + } + + var cfg Config + err := gcfg.ReadFileInto(&cfg, cfgFile) + if err != nil { + return Config{}, err + } + return cfg, nil +} diff --git a/parameterstore/parameterstore.go b/parameterstore/parameterstore.go index f81d152f..057fa13b 100644 --- a/parameterstore/parameterstore.go +++ b/parameterstore/parameterstore.go @@ -8,9 +8,10 @@ import ( "strings" "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/ssm" "github.com/aws/aws-sdk-go/service/ssm/ssmiface" + saws "github.com/bwhaley/ssmsh/aws" + "github.com/bwhaley/ssmsh/config" ) // Delimiter is the parameter path separator character @@ -18,40 +19,56 @@ const Delimiter = "/" // ParameterStore represents the current state and preferences of the shell type ParameterStore struct { - Confirm bool // TODO Prompt for confirmation to delete or overwrite - Cwd string // The current working directory in the hierarchy - Decrypt bool // Decrypt values retrieved from Get - Key string // The KMS key to use for SecureString parameters - Region string // AWS region on which to operate - Profile string // Profile to use from .aws/[config|credentials] - Clients map[string]ssmiface.SSMAPI // per-region SSM clients + Cwd string // The current working directory in the hierarchy + Decrypt bool // Decrypt values retrieved from Get + Type string // Default parameter type (String, SecureString, StringList) + Key string // The KMS key to use for SecureString parameters + Region string // AWS region on which to operate + Overwrite bool // Whether or not to overwrite parameters + Profile string // Profile to use from .aws/[config|credentials] + Clients map[string]ssmiface.SSMAPI // per-region SSM clients } -func newSession(region, profile string) *session.Session { - return session.Must( - session.NewSessionWithOptions( - session.Options{ - SharedConfigState: session.SharedConfigEnable, - Config: aws.Config{ - Region: aws.String(region), - }, - Profile: profile, - }, - ), - ) +// SetConfig sets the shels configuration state +func (ps *ParameterStore) SetDefaults(cfg config.Config) { + ps.Decrypt = cfg.Default.Decrypt + ps.Overwrite = cfg.Default.Overwrite + + // The value in the $AWS_PROFILE env var is most preferred + ps.Profile = os.Getenv("AWS_PROFILE") + + // Profile setting via ssmsh config file is second + if ps.Profile == "" { + ps.Profile = cfg.Default.Profile + } + + // Fall back to the default profile + if ps.Profile == "" { + ps.Profile = "default" + } + + if cfg.Default.Key != "" { + ps.Key = cfg.Default.Key + } + + if cfg.Default.Type != "" { + ps.Type = cfg.Default.Type + } + + // The value in the $AWS_REGION env var is most preferred + ps.Region = os.Getenv("AWS_REGION") + + if ps.Region == "" { + ps.Region = cfg.Default.Region + } } // NewParameterStore initializes a ParameterStore with default values func (ps *ParameterStore) NewParameterStore() error { - ps.Confirm = false ps.Cwd = Delimiter - ps.Decrypt = false + ps.Clients = make(map[string]ssmiface.SSMAPI) - ps.Profile = os.Getenv("AWS_PROFILE") - if ps.Profile == "" { - ps.Profile = "default" - } - ps.Clients[ps.Region] = ssm.New(newSession(ps.Region, ps.Profile)) + ps.Clients[ps.Region] = ssm.New(saws.NewSession(ps.Region, ps.Profile)) // Check for a non-existent parameter to validate credentials & permissions _, err := ps.Get([]string{Delimiter}, ps.Region) @@ -63,7 +80,7 @@ func (ps *ParameterStore) NewParameterStore() error { // InitClient initializes an SSM client in a given region func (ps *ParameterStore) InitClient(region string) { - ps.Clients[region] = ssm.New(newSession(region, ps.Profile)) + ps.Clients[region] = ssm.New(saws.NewSession(region, ps.Profile)) } // ParameterPath abstracts a parameter to include some metadata @@ -96,17 +113,24 @@ type ListResult struct { // List displays the parameters in a given path // Behavior is vaguely similar to UNIX ls func (ps *ParameterStore) List(ppath ParameterPath, recurse bool, lr chan ListResult, quit chan bool) { + results := []string{} + path := ppath.Name region := ppath.Region // Check for parameters under this path path = fqp(path, ps.Cwd) - results := []string{} + + // To find all the paths, the call to GetParametersByPath is always recursive + // The results are culled later to present just the top-level results params := &ssm.GetParametersByPathInput{ Path: aws.String(path), Recursive: aws.Bool(true), WithDecryption: aws.Bool(ps.Decrypt), } for { + // Interrupt this loop with SIGQUIT + // GetParametersByPath returns max 10 results at a time. For paths with many + // parameters this can take a long time. select { case <-quit: return @@ -165,6 +189,7 @@ func (ps *ParameterStore) Remove(params []ParameterPath, recurse bool) (err erro return ps.deleteByRegion(parametersToDelete) } +// recursiveDelete deletes all the parameters under a given path func (ps *ParameterStore) recursiveDelete(path ParameterPath) (err error) { var parametersToDelete []ParameterPath additionalParams := &ssm.GetParametersByPathInput{ @@ -299,9 +324,8 @@ func (ps *ParameterStore) Move(src, dst ParameterPath) error { // Copy duplicates a parameter from src to dst func (ps *ParameterStore) Copy(src, dst ParameterPath, recurse bool) error { - src.Name = fqp(src.Name, ps.Cwd) - dst.Name = fqp(dst.Name, ps.Cwd) var srcIsParameter, dstIsParameter, srcIsPath, dstIsPath bool + if !ps.Decrypt { // Decryption required for copy ps.Decrypt = true @@ -309,14 +333,20 @@ func (ps *ParameterStore) Copy(src, dst ParameterPath, recurse bool) error { ps.Decrypt = false }() } + + src.Name = fqp(src.Name, ps.Cwd) + dst.Name = fqp(dst.Name, ps.Cwd) + srcIsParameter = ps.isParameter(src) if !srcIsParameter { srcIsPath = ps.isPath(src) } + dstIsParameter = ps.isParameter(dst) if !dstIsParameter { dstIsPath = ps.isPath(dst) } + if srcIsParameter && !dstIsPath { return ps.copyParameter(src, dst) } else if srcIsParameter && dstIsPath { @@ -332,9 +362,10 @@ func (ps *ParameterStore) Copy(src, dst ParameterPath, recurse bool) error { } return ps.copyPathToPath(true, src, dst) } - return fmt.Errorf("%s is not a path or parameter", src) + return fmt.Errorf("%s is not a path or parameter", src.Name) } +// copyParameter copies one parameter to a new name func (ps *ParameterStore) copyParameter(src, dst ParameterPath) error { if !ps.isParameter(src) { return errors.New("source must be a parameter: " + src.Name) @@ -354,7 +385,7 @@ func (ps *ParameterStore) copyParameter(src, dst ParameterPath) error { KeyId: pLatest.KeyId, Description: pLatest.Description, AllowedPattern: pLatest.AllowedPattern, - Overwrite: aws.Bool(true), // TODO Prompt for overwrite + Overwrite: aws.Bool(ps.Overwrite), } _, err = ps.Put(putParamInput, dst.Region) if err != nil { @@ -363,12 +394,14 @@ func (ps *ParameterStore) copyParameter(src, dst ParameterPath) error { return nil } +// copyParameterToPath copies a parameter to a given path (preserving the parameter name) func (ps *ParameterStore) copyParameterToPath(srcParam, dstPath ParameterPath) error { srcParamElements := strings.Split(srcParam.Name, Delimiter) dstPath.Name = dstPath.Name + Delimiter + srcParamElements[len(srcParamElements)-1] return ps.copyParameter(srcParam, dstPath) } +// copyPathToPath copies the parameters at a source path to a new destination path func (ps *ParameterStore) copyPathToPath(newPath bool, srcPath, dstPath ParameterPath) error { /* 1) Get all source parameters @@ -452,7 +485,7 @@ func (ps *ParameterStore) inputPaths(paths []string) []*string { return _paths } -// fqp cleans a provided path +// fqp (fully qualified path) cleans a provided path // relative paths are prefixed with cwd // TODO Support regex or globbing func fqp(path string, cwd string) string { diff --git a/ssmsh.go b/ssmsh.go index d74d67ab..81126e0d 100644 --- a/ssmsh.go +++ b/ssmsh.go @@ -9,6 +9,7 @@ import ( "github.com/abiosoft/ishell" "github.com/bwhaley/ssmsh/commands" + "github.com/bwhaley/ssmsh/config" "github.com/bwhaley/ssmsh/parameterstore" "github.com/mattn/go-shellwords" ) @@ -16,30 +17,36 @@ import ( var Version string func main() { - var version bool - _fn := flag.String("file", "", "Read commands from file (use - for stdin)") - flag.BoolVar(&version, "version", false, "Display the current version") + cfgFile := flag.String("config", "", "Load configuration from the specified file") + file := flag.String("file", "", "Read commands from file (use - for stdin)") + version := flag.Bool("version", false, "Display the current version") flag.Parse() - if version { + if *version { fmt.Println("Version", Version) os.Exit(0) } + cfg, err := config.ReadConfig(*cfgFile) + if err != nil { + fmt.Printf("Error reading configuration file %s: %s\n", *cfgFile, err) + os.Exit(1) + } + shell := ishell.New() var ps parameterstore.ParameterStore - err := ps.NewParameterStore() + ps.SetDefaults(cfg) + err = ps.NewParameterStore() if err != nil { shell.Println("Error initializing session. Is your authentication correct?", err) os.Exit(1) } commands.Init(shell, &ps) - fn := *_fn - if fn == "-" { - processStdin(shell, fn) - } else if fn != "" { - processFile(shell, fn) + if *file == "-" { + processStdin(shell, *file) + } else if *file != "" { + processFile(shell, *file) } else if len(flag.Args()) > 1 { shell.Process(flag.Args()...) } else {