Merge pull request #14 from gambol99/exec_option

Exec option
This commit is contained in:
Rohith 2016-03-17 14:07:09 +00:00
commit 9e9a3a60a5
9 changed files with 227 additions and 141 deletions

View file

@ -1,3 +1,10 @@
#### **Version v0.0.8**
##### FEATURES
* Adding an exec option to the control set, the command is called whenever a change is made on the resource with a
condfigurable timeout (default to 60s)
-cn=secret:platform/secrets/se2:fmt=yaml,exec=tests/runme.sh,update=1s
#### **Version v0.0.7**

View file

@ -42,6 +42,8 @@ type config struct {
resources *VaultResources
// the interval for producing statistics
statsInterval time.Duration
// the timeout for a exec command
execTimeout time.Duration
}
var (
@ -60,6 +62,7 @@ func init() {
flag.BoolVar(&options.tlsVerify, "tls-skip-verify", false, "whether to check and verify the vault service certificate")
flag.StringVar(&options.vaultCaFile, "ca-cert", "", "the path to the file container the CA used to verify the vault service")
flag.DurationVar(&options.statsInterval, "stats", time.Duration(1)*time.Hour, "the interval to produce statistics on the accessed resources")
flag.DurationVar(&options.execTimeout, "exec-timeout", time.Duration(60) * time.Second, "the timeout applied to commands on the exec option")
flag.Var(options.resources, "cn", "a resource to retrieve and monitor from vault")
}

154
formats.go Normal file
View file

@ -0,0 +1,154 @@
/*
Copyright 2015 Home Office All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"strings"
"github.com/golang/glog"
"gopkg.in/yaml.v2"
)
func writeIniFile(filename string, data map[string]interface{}) error {
var buf bytes.Buffer
for key, val := range data {
buf.WriteString(fmt.Sprintf("%s = %v\n", key, val))
}
return writeFile(filename, buf.Bytes())
}
func writeCSVFile(filename string, data map[string]interface{}) error {
var buf bytes.Buffer
for key, val := range data {
buf.WriteString(fmt.Sprintf("%s,%v\n", key, val))
}
return writeFile(filename, buf.Bytes())
}
func writeYAMLFile(filename string, data map[string]interface{}) error {
// marshall the content to yaml
content, err := yaml.Marshal(data)
if err != nil {
return err
}
return writeFile(filename, content)
}
func writeEnvFile(filename string, data map[string]interface{}) error {
var buf bytes.Buffer
for key, val := range data {
buf.WriteString(fmt.Sprintf("%s=%v\n", strings.ToUpper(key), val))
}
return writeFile(filename, buf.Bytes())
}
func writeCertificateFile(filename string, data map[string]interface{}) error {
files := map[string]string{
"certificate": "crt",
"issuing_ca": "ca",
"private_key": "key",
}
for key, suffix := range files {
filename := fmt.Sprintf("%s.%s", filename, suffix)
content, found := data[key]
if !found {
glog.Errorf("didn't find the certification option: %s in the resource: %s", key, filename)
continue
}
// step: write the file
if err := writeFile(filename, []byte(fmt.Sprintf("%s", content))); err != nil {
glog.Errorf("failed to write resource: %s, elemment: %s, filename: %s, error: %s", filename, suffix, filename, err)
continue
}
}
return nil
}
func writeCertificateBundleFile(filename string, data map[string]interface{}) error {
certificateFile := fmt.Sprintf("%s.crt", filename)
caFile := fmt.Sprintf("%s.ca", filename)
certificate := fmt.Sprintf("%s\n\n%s", data["certificate"], data["private_key"])
ca := fmt.Sprintf("%s", data["issuing_ca"])
if err := writeFile(certificateFile, []byte(certificate)); err != nil {
glog.Errorf("failed to write the bundled certificate file, error: %s", err)
return err
}
if err := writeFile(caFile, []byte(ca)); err != nil {
glog.Errorf("failed to write the ca certificate file, errro: %s", err)
return err
}
return nil
}
func writeTxtFile(filename string, data map[string]interface{}) error {
keys := getKeys(data)
if len(keys) > 1 {
// 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", filename, suffix)
if err := writeFile(filename, []byte(fmt.Sprintf("%v", content))); err != nil {
glog.Errorf("failed to write resource: %s, elemment: %s, filename: %s, error: %s",
filename, suffix, filename, err)
continue
}
}
return nil
}
// step: we only have the one key, so will write plain
value, _ := data[keys[0]]
content := []byte(fmt.Sprintf("%s", value))
return writeFile(filename, content)
}
func writeJSONFile(filename string, data map[string]interface{}) error {
content, err := json.MarshalIndent(data, "", " ")
if err != nil {
return err
}
return writeFile(filename, content)
}
// writeFile ... writes the content to a file .. dah
// filename : the path to the file
// content : the content to be written
func writeFile(filename string, content []byte) error {
if options.dryRun {
glog.Infof("dry-run: filename: %s, content:", filename)
fmt.Printf("%s\n", string(content))
return nil
}
glog.V(3).Infof("saving the file: %s", filename)
return ioutil.WriteFile(filename, content, 0660)
}

View file

@ -26,7 +26,7 @@ import (
const (
Prog = "vault-sidekick"
Version = "v0.0.7"
Version = "v0.0.8"
)
func main() {
@ -64,7 +64,7 @@ func main() {
case evt := <-updates:
glog.V(10).Infof("recieved an update from the resource: %s", evt.Resource)
go func(r VaultEvent) {
if err := writeResource(evt.Resource, evt.Secret); err != nil {
if err := processResource(evt.Resource, evt.Secret); err != nil {
glog.Errorf("failed to write out the update, error: %s", err)
}
}(evt)

3
tests/runme.sh Executable file
View file

@ -0,0 +1,3 @@
#!/bin/bash
echo "running me: $@" >> runme.log

181
utils.go
View file

@ -17,7 +17,6 @@ limitations under the License.
package main
import (
"bytes"
"encoding/json"
"flag"
"fmt"
@ -30,6 +29,7 @@ import (
"github.com/golang/glog"
"gopkg.in/yaml.v2"
"os/exec"
"path/filepath"
)
@ -155,144 +155,57 @@ func fileExists(filename string) (bool, error) {
return true, nil
}
// writeResourceContent is responsible for generating the specific content from the resource
// rn : a point to the vault resource
// processResource is responsible for generating the specific content from the resource
// rn : a point to the vault resource
// data : a map of the related secret associated to the resource
func writeResource(rn *VaultResource, data map[string]interface{}) error {
func processResource(rn *VaultResource, data map[string]interface{}) (err error) {
// step: determine the resource path
resourcePath := rn.GetFilename()
if !strings.HasPrefix(resourcePath, "/") {
resourcePath = fmt.Sprintf("%s/%s", options.outputDir, filepath.Base(resourcePath))
filename := rn.GetFilename()
if !strings.HasPrefix(filename, "/") {
filename = fmt.Sprintf("%s/%s", options.outputDir, filepath.Base(filename))
}
// step: format and write the file
switch rn.format {
case "yaml":
fallthrough
case "yml":
err = writeYAMLFile(filename, data)
case "json":
err = writeJSONFile(filename, data)
case "ini":
err = writeIniFile(filename, data)
case "csv":
err = writeCSVFile(filename, data)
case "env":
err = writeEnvFile(filename, data)
case "cert":
err = writeCertificateFile(filename, data)
case "txt":
err = writeTxtFile(filename, data)
case "bundle":
err = writeCertificateBundleFile(filename, data)
default:
return fmt.Errorf("unknown output format: %s", rn.format)
}
// step: check for an error
if err != nil {
return err
}
glog.V(10).Infof("writing the resource: %s, format: %s", resourcePath, rn.format)
if rn.format == "yaml" {
// marshall the content to yaml
content, err := yaml.Marshal(data)
if err != nil {
return err
}
return writeFile(resourcePath, content)
}
if rn.format == "ini" {
var buf bytes.Buffer
for key, val := range data {
buf.WriteString(fmt.Sprintf("%s = %v\n", key, val))
}
return writeFile(resourcePath, buf.Bytes())
}
if rn.format == "bundle" {
certificateFile := fmt.Sprintf("%s.crt", resourcePath)
caFile := fmt.Sprintf("%s.ca", resourcePath)
certificate := fmt.Sprintf("%s\n\n%s", data["certificate"], data["private_key"])
ca := fmt.Sprintf("%s", data["issuing_ca"])
if err := writeFile(certificateFile, []byte(certificate)); err != nil {
glog.Errorf("failed to write the bundled certificate file, error: %s", err)
return err
}
if err := writeFile(caFile, []byte(ca)); err != nil {
glog.Errorf("failed to write the ca certificate file, errro: %s", err)
return err
}
return nil
}
if rn.format == "env" {
var buf bytes.Buffer
for key, val := range data {
buf.WriteString(fmt.Sprintf("%s=%v\n", strings.ToUpper(key), val))
}
return writeFile(resourcePath, buf.Bytes())
}
if rn.format == "cert" {
files := map[string]string{
"certificate": "crt",
"issuing_ca": "ca",
"private_key": "key",
}
for key, suffix := range files {
filename := fmt.Sprintf("%s.%s", resourcePath, suffix)
content, found := data[key]
if !found {
glog.Errorf("didn't find the certification option: %s in the resource: %s", key, rn.path)
continue
// step: check if we need to execute a command
if rn.execPath != "" {
glog.V(10).Infof("executing the command: %s for resouce: %s", rn.execPath, rn.path)
cmd := exec.Command(rn.execPath, filename)
cmd.Start()
timer := time.AfterFunc(options.execTimeout, func() {
if err := cmd.Process.Kill(); err != nil {
glog.Errorf("failed to kill the command, pid: %d, error: %s", cmd.Process.Pid, err)
}
// step: write the file
if err := writeFile(filename, []byte(fmt.Sprintf("%s", content))); err != nil {
glog.Errorf("failed to write resource: %s, elemment: %s, filename: %s, error: %s", rn, suffix, filename, err)
continue
}
}
return nil
})
// step: wait for the command to finish
err = cmd.Wait()
timer.Stop()
}
if rn.format == "csv" {
var buf bytes.Buffer
for key, val := range data {
buf.WriteString(fmt.Sprintf("%s,%v\n", key, val))
}
return writeFile(resourcePath, buf.Bytes())
}
if rn.format == "txt" {
keys := getKeys(data)
if len(keys) > 1 {
// 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("%v", content))); err != nil {
glog.Errorf("failed to write resource: %s, elemment: %s, filename: %s, error: %s",
rn, suffix, filename, err)
continue
}
}
return nil
}
// step: we only have the one key, so will write plain
value, _ := data[keys[0]]
content := []byte(fmt.Sprintf("%s", value))
return writeFile(resourcePath, content)
}
if rn.format == "json" {
content, err := json.MarshalIndent(data, "", " ")
if err != nil {
return err
}
return writeFile(resourcePath, content)
}
return fmt.Errorf("unknown output format: %s", rn.format)
}
// writeFile ... writes the content to a file .. dah
// filename : the path to the file
// content : the content to be written
func writeFile(filename string, content []byte) error {
if options.dryRun {
glog.Infof("dry-run: filename: %s, content:", filename)
fmt.Printf("%s\n", string(content))
return nil
}
glog.V(3).Infof("saving the file: %s", filename)
return ioutil.WriteFile(filename, content, 0660)
return err
}

View file

@ -325,7 +325,7 @@ func (r VaultService) get(rn *watchedResource) (err error) {
// step: perform a request to vault
switch rn.resource.resource {
case "raw":
request := r.client.NewRequest("GET", "/v1/" + rn.resource.path)
request := r.client.NewRequest("GET", "/v1/"+rn.resource.path)
for k, v := range rn.resource.options {
request.Params.Add(k, v)
}
@ -341,10 +341,10 @@ func (r VaultService) get(rn *watchedResource) (err error) {
}
// step: construct a secret from the response
secret = &api.Secret{
LeaseID: "raw",
Renewable: false,
LeaseID: "raw",
Renewable: false,
Data: map[string]interface{}{
"content" : fmt.Sprintf("%s", content),
"content": fmt.Sprintf("%s", content),
},
}
if rn.resource.update > 0 {

View file

@ -37,10 +37,12 @@ const (
optionsRevokeDelay = "delay"
// optionUpdate overrides the lease of the resource
optionUpdate = "update"
// optionsExec executes something on a change
optionExec = "exec"
)
var (
resourceFormatRegex = regexp.MustCompile("^(yaml|json|env|ini|txt|cert|bundle|csv)$")
resourceFormatRegex = regexp.MustCompile("^(yaml|yml|json|env|ini|txt|cert|bundle|csv)$")
// a map of valid resource to retrieve from vault
validResources = map[string]bool{
@ -86,6 +88,8 @@ type VaultResource struct {
filename string
// the template file
templateFile string
// the path to an exec to run on a change
execPath string
// additional options to the resource
options map[string]string
}

View file

@ -96,6 +96,8 @@ func (r *VaultResources) Set(value string) error {
return fmt.Errorf("the renewal option: %s is invalid, should be a boolean", value)
}
rn.renewable = choice
case optionExec:
rn.execPath = value
case optionFilename:
rn.filename = value
case optionTemplatePath: