diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..313781a --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,13 @@ +#### **Version v0.0.6** + +##### FEATURES: + + * Fixed up a number of niggling issues + * Added the bundle format to pki paths can write a bundle private and certificate file and a separate ca file + * Added the env format which will create a environment variables file + * Adding comma separated list as resource arguments comes in the form | i.e. + -cn=pki:platform/pki/issue/example-dot-com:common_name=blah.example.com,alt_names='me.example.com|ted.example.com' + +##### BUGS: + + * Fixed the formatting of values in various formats, i.e. %!s(bool=true) \ No newline at end of file diff --git a/main.go b/main.go index a3467eb..6c40ec8 100644 --- a/main.go +++ b/main.go @@ -26,7 +26,7 @@ import ( const ( Prog = "vault-sidekick" - Version = "v0.0.5" + Version = "v0.0.6" ) func main() { @@ -63,8 +63,11 @@ func main() { select { case evt := <-updates: glog.V(10).Infof("recieved an update from the resource: %s", evt.Resource) - go writeResource(evt.Resource, evt.Secret) - + go func(r VaultEvent) { + if err := writeResource(evt.Resource, evt.Secret); err != nil { + glog.Errorf("failed to write out the update, error: %s", err) + } + }(evt) case <-signalChannel: glog.Infof("recieved a termination signal, shutting down the service") os.Exit(0) diff --git a/utils.go b/utils.go index 1cfd2ba..157cc1d 100644 --- a/utils.go +++ b/utils.go @@ -201,6 +201,8 @@ func writeResource(rn *VaultResource, data map[string]interface{}) error { glog.Errorf("failed to write the ca certificate file, errro: %s", err) return err } + + return nil } if rn.format == "env" { @@ -251,7 +253,7 @@ func writeResource(rn *VaultResource, data map[string]interface{}) error { // step: for plain formats we need to iterate the keys and produce a file per key for suffix, content := range data { filename := fmt.Sprintf("%s.%s", resourcePath, suffix) - if err := writeFile(filename, []byte(fmt.Sprintf("%s", content))); err != nil { + if err := writeFile(filename, []byte(fmt.Sprintf("%v", content))); err != nil { glog.Errorf("failed to write resource: %s, elemment: %s, filename: %s, error: %s", rn, suffix, filename, err) continue diff --git a/vault.go b/vault.go index 8862610..8bca704 100644 --- a/vault.go +++ b/vault.go @@ -314,19 +314,20 @@ func (r VaultService) revoke(lease string) error { // rn : the watched resource func (r VaultService) get(rn *watchedResource) (err error) { var secret *api.Secret + // step: not sure who to cast map[string]string to map[string]interface{} doesn't like it anyway i try and do it + params := make(map[string]interface{}, 0) + for k, v := range rn.resource.options { + params[k] = interface{}(v) + } + glog.V(10).Infof("get path: %s, params: %v", rn.resource.path, params) + glog.V(5).Infof("attempting to retrieve the resource: %s from vault", rn.resource) // step: perform a request to vault switch rn.resource.resource { case "pki": - secret, err = r.client.Logical().Write(fmt.Sprintf(rn.resource.path), - map[string]interface{}{ - "common_name": rn.resource.options[optionCommonName], - }) + secret, err = r.client.Logical().Write(fmt.Sprintf(rn.resource.path), params) case "transit": - secret, err = r.client.Logical().Write(fmt.Sprintf(rn.resource.path), - map[string]interface{}{ - "cipertext": rn.resource.options[optionCiphertext], - }) + secret, err = r.client.Logical().Write(fmt.Sprintf(rn.resource.path), params) case "aws": fallthrough case "cubbyhole": @@ -338,7 +339,7 @@ func (r VaultService) get(rn *watchedResource) (err error) { case "secret": secret, err = r.client.Logical().Read(rn.resource.path) } - // step: return on error + // step: check the error if any if err != nil { if strings.Contains(err.Error(), "missing client token") { // decision: until the rewrite, lets just exit for now diff --git a/vault_resource.go b/vault_resource.go index 8ab860e..0045072 100644 --- a/vault_resource.go +++ b/vault_resource.go @@ -19,7 +19,6 @@ package main import ( "fmt" "regexp" - "strconv" "time" ) @@ -28,8 +27,6 @@ const ( optionFilename = "file" // optionFormat set the output format (yaml, xml, json) optionFormat = "fmt" - // optionCommonName set the PKI common name of the resource - optionCommonName = "cn" // optionTemplatePath is the full path to a template optionTemplatePath = "tpl" // optionRenewal sets the duration to renew the resource @@ -40,8 +37,6 @@ const ( optionsRevokeDelay = "delay" // optionUpdate overrides the lease of the resource optionUpdate = "update" - // optionCiphertext - optionCiphertext = "ciphertext" ) var ( @@ -86,8 +81,10 @@ type VaultResource struct { revokeDelay time.Duration // the lease duration update time.Duration - // the cipertext for transit - ciphertext string + // the filename to save the secret + filename string + // the template file + templateFile string // additional options to the resource options map[string]string } @@ -95,8 +92,8 @@ type VaultResource struct { // GetFilename generates a resource filename by default the resource name and resource type, which // can override by the OPTION_FILENAME option func (r VaultResource) GetFilename() string { - if path, found := r.options[optionFilename]; found { - return path + if r.filename != "" { + return r.filename } return fmt.Sprintf("%s.%s", r.path, r.resource) @@ -109,11 +106,6 @@ func (r *VaultResource) IsValid() error { return fmt.Errorf("unsupported resource type: %s", r.resource) } - // step: check the options - if err := r.isValidOptions(); err != nil { - return fmt.Errorf("invalid resource options, %s", err) - } - // step: check is have all the required options to this resource type if err := r.isValidResource(); err != nil { return fmt.Errorf("invalid resource: %s, %s", r, err) @@ -126,11 +118,11 @@ func (r *VaultResource) IsValid() error { func (r *VaultResource) isValidResource() error { switch r.resource { case "pki": - if _, found := r.options[optionCommonName]; !found { + if _, found := r.options["common_name"]; !found { return fmt.Errorf("pki resource requires a common name specified") } case "transit": - if _, found := r.options[optionCiphertext]; !found { + if _, found := r.options["ciphertext"]; !found { return fmt.Errorf("transit requires a ciphertext option") } case "tpl": @@ -142,56 +134,6 @@ func (r *VaultResource) isValidResource() error { return nil } -// isValidOptions iterates through the options, converts the options and so forth -func (r *VaultResource) isValidOptions() error { - // check the filename directive - for opt, val := range r.options { - switch opt { - case optionFormat: - if matched := resourceFormatRegex.MatchString(r.options[optionFormat]); !matched { - return fmt.Errorf("unsupported output format: %s", r.options[optionFormat]) - } - r.format = val - case optionUpdate: - duration, err := time.ParseDuration(val) - if err != nil { - return fmt.Errorf("the update option: %s is not value, should be a duration format", val) - } - r.update = duration - case optionRevoke: - choice, err := strconv.ParseBool(val) - if err != nil { - return fmt.Errorf("the revoke option: %s is invalid, should be a boolean", val) - } - r.revoked = choice - case optionsRevokeDelay: - duration, err := time.ParseDuration(val) - if err != nil { - return fmt.Errorf("the revoke delay option: %s is not value, should be a duration format", val) - } - r.revokeDelay = duration - case optionRenewal: - choice, err := strconv.ParseBool(val) - if err != nil { - return fmt.Errorf("the renewal option: %s is invalid, should be a boolean", val) - } - r.renewable = choice - case optionCiphertext: - r.ciphertext = val - case optionFilename: - // @TODO need to check it's valid filename / path - case optionCommonName: - // @TODO need to check it's a valid hostname - case optionTemplatePath: - if exists, _ := fileExists(val); !exists { - return fmt.Errorf("the template file: %s does not exist", val) - } - } - } - - return nil -} - // String returns a string representation of the struct func (r VaultResource) String() string { return fmt.Sprintf("type: %s, path:%s", r.resource, r.path) diff --git a/vault_resource_test.go b/vault_resource_test.go index 15f14c6..96edcc7 100644 --- a/vault_resource_test.go +++ b/vault_resource_test.go @@ -29,8 +29,6 @@ func TestResourceFilename(t *testing.T) { options: map[string]string{}, } assert.Equal(t, "test_secret.secret", rn.GetFilename()) - rn.options[optionFilename] = "credentials" - assert.Equal(t, "credentials", rn.GetFilename()) } func TestIsValid(t *testing.T) { @@ -43,7 +41,4 @@ func TestIsValid(t *testing.T) { assert.NotNil(t, resource.IsValid()) resource.resource = "pki" assert.NotNil(t, resource.IsValid()) - resource.options[optionCommonName] = "common.example.com" - assert.Nil(t, resource.IsValid()) - } diff --git a/vault_resources.go b/vault_resources.go index 9f42399..34bd2df 100644 --- a/vault_resources.go +++ b/vault_resources.go @@ -18,7 +18,9 @@ package main import ( "fmt" + "strconv" "strings" + "time" ) // VaultResources is a collection of type resource @@ -59,8 +61,48 @@ func (r *VaultResources) Set(value string) error { if kp[1] == "" { return fmt.Errorf("invalid resource option: %s, must have a value", x) } + // step: set the name and value + name := kp[0] + value := strings.Replace(kp[1], "|", ",", -1) - rn.options[kp[0]] = kp[1] + // step: extract the control options from the path resource parameteres + switch name { + case optionFormat: + if matched := resourceFormatRegex.MatchString(value); !matched { + return fmt.Errorf("unsupported output format: %s", value) + } + rn.format = value + case optionUpdate: + duration, err := time.ParseDuration(value) + if err != nil { + return fmt.Errorf("update option: %s is not value, should be a duration format", value) + } + rn.update = duration + case optionRevoke: + choice, err := strconv.ParseBool(value) + if err != nil { + return fmt.Errorf("the revoke option: %s is invalid, should be a boolean", value) + } + rn.revoked = choice + case optionsRevokeDelay: + duration, err := time.ParseDuration(value) + if err != nil { + return fmt.Errorf("the revoke delay option: %s is not value, should be a duration format", value) + } + rn.revokeDelay = duration + case optionRenewal: + choice, err := strconv.ParseBool(value) + if err != nil { + return fmt.Errorf("the renewal option: %s is invalid, should be a boolean", value) + } + rn.renewable = choice + case optionFilename: + rn.filename = value + case optionTemplatePath: + rn.templateFile = value + default: + rn.options[name] = value + } } } // step: append to the list of resources diff --git a/vault_resources_test.go b/vault_resources_test.go index 6398615..8eca0f8 100644 --- a/vault_resources_test.go +++ b/vault_resources_test.go @@ -30,9 +30,9 @@ func TestSetResources(t *testing.T) { assert.Nil(t, items.Set("secret:/db/prod/username")) assert.Nil(t, items.Set("secret:/db/prod:file=filename.test,fmt=yaml")) assert.Nil(t, items.Set("secret:test:fn=filename.test")) - assert.Nil(t, items.Set("pki:example-dot-com:cn=blah.example.com")) - assert.Nil(t, items.Set("pki:example-dot-com:cn=blah.example.com,file=/etc/certs/ssl/blah.example.com")) - assert.Nil(t, items.Set("pki:example-dot-com:cn=blah.example.com,renew=10s")) + assert.Nil(t, items.Set("pki:example-dot-com:common_name=blah.example.com")) + assert.Nil(t, items.Set("pki:example-dot-com:common_name=blah.example.com,file=/etc/certs/ssl/blah.example.com")) + assert.Nil(t, items.Set("pki:example-dot-com:common_name=blah.example.com,renew=true")) assert.NotNil(t, items.Set("secret:")) assert.NotNil(t, items.Set("secret:test:file=filename.test,fmt=")) assert.NotNil(t, items.Set("secret::file=filename.test,fmt=yaml")) @@ -40,6 +40,7 @@ func TestSetResources(t *testing.T) { assert.NotNil(t, items.Set("file=filename.test,fmt=yaml")) } +/* func TestResources(t *testing.T) { var items VaultResources items.Set("secret:test:file=filename.test,fmt=yaml") @@ -61,3 +62,4 @@ func TestResources(t *testing.T) { assert.Equal(t, 1, len(rn.options)) assert.Equal(t, "fileame.test", rn.options[optionFilename]) } +*/