Skip to content

Commit

Permalink
Merge pull request #271 from zeroae/f-template
Browse files Browse the repository at this point in the history
* Add RenderConfig option to ContainerPilot.

This is useful in AutoPilot applications like MySQL that need to
rewrite the containerpilot.conf file and force a SIGHUP.

* Changed template rendering call syntax.

- containerpilot -template [-config file://] [-out file://]
  • Loading branch information
tgross authored Jan 31, 2017
2 parents 4dec7fb + 1c63b1f commit 46b0387
Show file tree
Hide file tree
Showing 3 changed files with 149 additions and 13 deletions.
55 changes: 42 additions & 13 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,27 +163,35 @@ func (cfg *rawConfig) parseTasks() ([]*tasks.Task, error) {
return tasks, nil
}

// ParseConfig parses a raw config flag
func ParseConfig(configFlag string) (*Config, error) {
if configFlag == "" {
return nil, errors.New("-config flag is required")
// RenderConfig renders the templated config in configFlag to renderFlag.
func RenderConfig(configFlag, renderFlag string) error {
template, err := renderConfigTemplate(configFlag)
if err != nil {
return err
}

var data []byte
if strings.HasPrefix(configFlag, "file://") {
// Save the template, either to stdout or to file://...
if renderFlag == "-" {
fmt.Printf("%s", template)
} else if strings.HasPrefix(renderFlag, "file://") {
var err error
fName := strings.SplitAfter(configFlag, "file://")[1]
if data, err = ioutil.ReadFile(fName); err != nil {
return nil, fmt.Errorf("Could not read config file: %s", err)
fName := strings.SplitAfter(renderFlag, "file://")[1]
if err = ioutil.WriteFile(fName, template, 0644); err != nil {
return fmt.Errorf("Could not write config file: %s", err)
}
} else {
data = []byte(configFlag)
return fmt.Errorf("-render flag is invalid: '%s'", renderFlag)
}

template, err := ApplyTemplate(data)
return nil
}

// ParseConfig parses a raw config flag
func ParseConfig(configFlag string) (*Config, error) {

template, err := renderConfigTemplate(configFlag)
if err != nil {
return nil, fmt.Errorf(
"Could not apply template to config: %v", err)
return nil, err
}
configMap, err := unmarshalConfig(template)
if err != nil {
Expand Down Expand Up @@ -270,6 +278,27 @@ func ParseConfig(configFlag string) (*Config, error) {
return cfg, nil
}

func renderConfigTemplate(configFlag string) ([]byte, error) {
if configFlag == "" {
return nil, errors.New("-config flag is required")
}
var data []byte
if strings.HasPrefix(configFlag, "file://") {
var err error
fName := strings.SplitAfter(configFlag, "file://")[1]
if data, err = ioutil.ReadFile(fName); err != nil {
return nil, fmt.Errorf("Could not read config file: %s", err)
}
} else {
data = []byte(configFlag)
}
template, err := ApplyTemplate(data)
if err != nil {
err = fmt.Errorf("Could not apply template to config: %v", err)
}
return template, err
}

func unmarshalConfig(data []byte) (map[string]interface{}, error) {
var config map[string]interface{}
if err := json.Unmarshal(data, &config); err != nil {
Expand Down
93 changes: 93 additions & 0 deletions config/template_test.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
package config

import (
"io/ioutil"
"os"
"path/filepath"
"reflect"
"strings"
"testing"

// need to import this so that we have a registered backend
_ "github.com/joyent/containerpilot/discovery/consul"
)

func TestParseEnvironment(t *testing.T) {
Expand All @@ -28,6 +35,74 @@ func TestTemplate(t *testing.T) {
validateTemplate(t, "Regex Replace All", `Hello, {{.NAME | regexReplaceAll "[epa]+" "_" }}!`, env, "Hello, T_m_l_t_!")
}

func TestInvalidRenderConfigFile(t *testing.T) {
testRenderExpectError(t, "file:///xxxx", "-",
"Could not read config file: open /xxxx: no such file or directory")
}

func TestInvalidRenderFileConfig(t *testing.T) {
var testJSON = `{"consul": "consul:8500"}`
testRenderExpectError(t, testJSON, "file:///a/b/c/d/e/f.json",
"Could not write config file: open /a/b/c/d/e/f.json: no such file or directory")
}

func TestRenderConfigFileStdout(t *testing.T) {

var testJSON = `{
"consul": "consul:8500",
"backends": [{
"name": "upstreamA",
"poll": 11,
"onChange": "/bin/to/onChangeEvent/for/upstream/A.sh"}]}`

// Render to file
defer os.Remove("testJSON.json")
if err := RenderConfig(testJSON, "file://testJSON.json"); err != nil {
t.Fatalf("Expected no error from renderConfigTemplate but got: %v", err)
}
if exists, err := fileExists("testJSON.json"); !exists || err != nil {
t.Errorf("Expected file testJSON.json to exist.")
}

// Render to stdout
fname := filepath.Join(os.TempDir(), "stdout")
temp, _ := os.Create(fname)
old := os.Stdout
os.Stdout = temp
if err := RenderConfig(testJSON, "-"); err != nil {
t.Fatalf("Expected no error from renderConfigTemplate but got: %v", err)
}
temp.Close()
os.Stdout = old

renderedOut, _ := ioutil.ReadFile(fname)
renderedFile, _ := ioutil.ReadFile("testJSON.json")
if string(renderedOut) != string(renderedFile) {
t.Fatalf("Expected the rendered file and stdout to be identical")
}
}

func TestRenderedConfigIsParseable(t *testing.T) {

var testJSON = `{
"consul": "consul:8500",
"backends": [{
"name": "upstreamA{{.TESTRENDERCONFIGISPARSEABLE}}",
"poll": 11,
"onChange": "/bin/to/onChangeEvent/for/upstream/A.sh"}]}`

os.Setenv("TESTRENDERCONFIGISPARSEABLE", "-ok")
template, _ := renderConfigTemplate(testJSON)
config, err := ParseConfig(string(template))
if err != nil {
t.Fatalf("Unexpected error in ParseConfig: %v", err)
}
name := config.Backends[0].Name
if name != "upstreamA-ok" {
t.Fatalf("Expected Backend[0] name to be upstreamA-ok but got %s", name)
}
}

// Helper Functions

func validateParseEnvironment(t *testing.T, message string, environ []string, expected Environment) {
Expand All @@ -51,3 +126,21 @@ func validateTemplate(t *testing.T, name string, template string, env Environmen
t.Fatalf("%s - Expected %s but got: %s", name, expected, strRes)
}
}

func testRenderExpectError(t *testing.T, testJSON, render, expected string) {
err := RenderConfig(testJSON, render)
if err == nil || !strings.Contains(err.Error(), expected) {
t.Errorf("Expected %s but got %s", expected, err)
}
}

func fileExists(path string) (bool, error) {
_, err := os.Stat(path)
if err == nil {
return true, nil
}
if os.IsNotExist(err) {
return false, nil
}
return true, err
}
14 changes: 14 additions & 0 deletions core/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,16 @@ func LoadApp() (*App, error) {

var configFlag string
var versionFlag bool
var renderFlag string
var templateFlag bool

if !flag.Parsed() {
flag.StringVar(&configFlag, "config", "",
"JSON config or file:// path to JSON config file.")
flag.BoolVar(&templateFlag, "template", false,
"Render template and quit. (default: false)")
flag.StringVar(&renderFlag, "out", "-",
"-(default) for stdout or file:// path where to save rendered JSON config file.")
flag.BoolVar(&versionFlag, "version", false, "Show version identifier and quit.")
flag.Parse()
}
Expand All @@ -77,6 +83,14 @@ func LoadApp() (*App, error) {
if configFlag == "" {
configFlag = os.Getenv("CONTAINERPILOT")
}
if templateFlag {

err := config.RenderConfig(configFlag, renderFlag)
if err != nil {
return nil, err
}
os.Exit(0)
}

os.Setenv("CONTAINERPILOT_PID", fmt.Sprintf("%v", os.Getpid()))
app, err := NewApp(configFlag)
Expand Down

0 comments on commit 46b0387

Please sign in to comment.