- adding a exec option to the control flags, the command is executabed whenever the data is changed
This commit is contained in:
parent
8b46a367e3
commit
be9e86d915
|
@ -42,6 +42,8 @@ type config struct {
|
||||||
resources *VaultResources
|
resources *VaultResources
|
||||||
// the interval for producing statistics
|
// the interval for producing statistics
|
||||||
statsInterval time.Duration
|
statsInterval time.Duration
|
||||||
|
// the timeout for a exec command
|
||||||
|
execTimeout time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
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.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.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.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")
|
flag.Var(options.resources, "cn", "a resource to retrieve and monitor from vault")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
154
formats.go
Normal file
154
formats.go
Normal 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)
|
||||||
|
}
|
4
main.go
4
main.go
|
@ -26,7 +26,7 @@ import (
|
||||||
|
|
||||||
const (
|
const (
|
||||||
Prog = "vault-sidekick"
|
Prog = "vault-sidekick"
|
||||||
Version = "v0.0.7"
|
Version = "v0.0.8"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
@ -64,7 +64,7 @@ func main() {
|
||||||
case evt := <-updates:
|
case evt := <-updates:
|
||||||
glog.V(10).Infof("recieved an update from the resource: %s", evt.Resource)
|
glog.V(10).Infof("recieved an update from the resource: %s", evt.Resource)
|
||||||
go func(r VaultEvent) {
|
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)
|
glog.Errorf("failed to write out the update, error: %s", err)
|
||||||
}
|
}
|
||||||
}(evt)
|
}(evt)
|
||||||
|
|
3
tests/runme.sh
Executable file
3
tests/runme.sh
Executable file
|
@ -0,0 +1,3 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
echo "running me: $@" >> runme.log
|
181
utils.go
181
utils.go
|
@ -17,7 +17,6 @@ limitations under the License.
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
@ -30,6 +29,7 @@ import (
|
||||||
|
|
||||||
"github.com/golang/glog"
|
"github.com/golang/glog"
|
||||||
"gopkg.in/yaml.v2"
|
"gopkg.in/yaml.v2"
|
||||||
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -155,144 +155,57 @@ func fileExists(filename string) (bool, error) {
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// writeResourceContent is responsible for generating the specific content from the resource
|
// processResource is responsible for generating the specific content from the resource
|
||||||
// rn : a point to the vault resource
|
// rn : a point to the vault resource
|
||||||
// data : a map of the related secret associated to the 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
|
// step: determine the resource path
|
||||||
resourcePath := rn.GetFilename()
|
filename := rn.GetFilename()
|
||||||
if !strings.HasPrefix(resourcePath, "/") {
|
if !strings.HasPrefix(filename, "/") {
|
||||||
resourcePath = fmt.Sprintf("%s/%s", options.outputDir, filepath.Base(resourcePath))
|
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)
|
// step: check if we need to execute a command
|
||||||
|
if rn.execPath != "" {
|
||||||
if rn.format == "yaml" {
|
glog.V(10).Infof("executing the command: %s for resouce: %s", rn.execPath, rn.path)
|
||||||
// marshall the content to yaml
|
cmd := exec.Command(rn.execPath, filename)
|
||||||
content, err := yaml.Marshal(data)
|
cmd.Start()
|
||||||
if err != nil {
|
timer := time.AfterFunc(options.execTimeout, func() {
|
||||||
return err
|
if err := cmd.Process.Kill(); err != nil {
|
||||||
}
|
glog.Errorf("failed to kill the command, pid: %d, error: %s", cmd.Process.Pid, 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: write the file
|
// step: wait for the command to finish
|
||||||
if err := writeFile(filename, []byte(fmt.Sprintf("%s", content))); err != nil {
|
err = cmd.Wait()
|
||||||
glog.Errorf("failed to write resource: %s, elemment: %s, filename: %s, error: %s", rn, suffix, filename, err)
|
timer.Stop()
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if rn.format == "csv" {
|
return err
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
8
vault.go
8
vault.go
|
@ -325,7 +325,7 @@ func (r VaultService) get(rn *watchedResource) (err error) {
|
||||||
// step: perform a request to vault
|
// step: perform a request to vault
|
||||||
switch rn.resource.resource {
|
switch rn.resource.resource {
|
||||||
case "raw":
|
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 {
|
for k, v := range rn.resource.options {
|
||||||
request.Params.Add(k, v)
|
request.Params.Add(k, v)
|
||||||
}
|
}
|
||||||
|
@ -341,10 +341,10 @@ func (r VaultService) get(rn *watchedResource) (err error) {
|
||||||
}
|
}
|
||||||
// step: construct a secret from the response
|
// step: construct a secret from the response
|
||||||
secret = &api.Secret{
|
secret = &api.Secret{
|
||||||
LeaseID: "raw",
|
LeaseID: "raw",
|
||||||
Renewable: false,
|
Renewable: false,
|
||||||
Data: map[string]interface{}{
|
Data: map[string]interface{}{
|
||||||
"content" : fmt.Sprintf("%s", content),
|
"content": fmt.Sprintf("%s", content),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
if rn.resource.update > 0 {
|
if rn.resource.update > 0 {
|
||||||
|
|
|
@ -37,10 +37,12 @@ const (
|
||||||
optionsRevokeDelay = "delay"
|
optionsRevokeDelay = "delay"
|
||||||
// optionUpdate overrides the lease of the resource
|
// optionUpdate overrides the lease of the resource
|
||||||
optionUpdate = "update"
|
optionUpdate = "update"
|
||||||
|
// optionsExec executes something on a change
|
||||||
|
optionExec = "exec"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
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
|
// a map of valid resource to retrieve from vault
|
||||||
validResources = map[string]bool{
|
validResources = map[string]bool{
|
||||||
|
@ -86,6 +88,8 @@ type VaultResource struct {
|
||||||
filename string
|
filename string
|
||||||
// the template file
|
// the template file
|
||||||
templateFile string
|
templateFile string
|
||||||
|
// the path to an exec to run on a change
|
||||||
|
execPath string
|
||||||
// additional options to the resource
|
// additional options to the resource
|
||||||
options map[string]string
|
options map[string]string
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
return fmt.Errorf("the renewal option: %s is invalid, should be a boolean", value)
|
||||||
}
|
}
|
||||||
rn.renewable = choice
|
rn.renewable = choice
|
||||||
|
case optionExec:
|
||||||
|
rn.execPath = value
|
||||||
case optionFilename:
|
case optionFilename:
|
||||||
rn.filename = value
|
rn.filename = value
|
||||||
case optionTemplatePath:
|
case optionTemplatePath:
|
||||||
|
|
Reference in a new issue