// serviceFunc returns or accumulates health service dependencies. func serviceFunc(brain *Brain, used, missing map[string]dep.Dependency) func(...string) ([]*dep.HealthService, error) { return func(s ...string) ([]*dep.HealthService, error) { result := make([]*dep.HealthService, 0) if len(s) == 0 || s[0] == "" { return result, nil } d, err := dep.ParseHealthServices(s...) if err != nil { return nil, err } addDependency(used, d) if value, ok := brain.Recall(d); ok { return value.([]*dep.HealthService), nil } addDependency(missing, d) return result, nil } }
func TestServiceFunc_missingData(t *testing.T) { d, err := dep.ParseHealthServices("non-existing") if err != nil { t.Fatal(err) } brain := NewBrain() used := make(map[string]dep.Dependency) missing := make(map[string]dep.Dependency) f := serviceFunc(brain, used, missing) result, err := f("non-existing") if err != nil { t.Fatal(err) } expected := []*dep.HealthService{} if !reflect.DeepEqual(result, expected) { t.Errorf("expected %q to be %q", result, expected) } if _, ok := used[d.HashCode()]; !ok { t.Errorf("expected dep to be used") } if _, ok := missing[d.HashCode()]; !ok { t.Errorf("expected dep to be missing") } }
func TestRun_executesCommand(t *testing.T) { outFile := test.CreateTempfile(nil, t) os.Remove(outFile.Name()) defer os.Remove(outFile.Name()) inTemplate := test.CreateTempfile([]byte(` {{ range service "consul@nyc1"}}{{ end }} `), t) defer test.DeleteTempfile(inTemplate, t) outTemplate := test.CreateTempfile(nil, t) defer test.DeleteTempfile(outTemplate, t) config := DefaultConfig() config.Merge(&Config{ ConfigTemplates: []*ConfigTemplate{ &ConfigTemplate{ Source: inTemplate.Name(), Destination: outTemplate.Name(), Command: fmt.Sprintf("echo 'foo' > %s", outFile.Name()), CommandTimeout: 1 * time.Second, }, }, }) runner, err := NewRunner(config, false, false) if err != nil { t.Fatal(err) } d, err := dep.ParseHealthServices("consul@nyc1") if err != nil { t.Fatal(err) } data := []*dep.HealthService{ &dep.HealthService{ Node: "consul", Address: "1.2.3.4", ID: "consul@nyc1", Name: "consul", }, } runner.dependencies[d.HashCode()] = d runner.watcher.ForceWatching(d, true) runner.Receive(d, data) if err := runner.Run(); err != nil { t.Fatal(err) } _, err = os.Stat(outFile.Name()) if err != nil { t.Fatal(err) } }
func TestRender_sameContentsDoesNotExecuteCommand(t *testing.T) { outFile := test.CreateTempfile(nil, t) os.Remove(outFile.Name()) defer os.Remove(outFile.Name()) inTemplate := test.CreateTempfile([]byte(` {{ range service "consul@nyc1" }}{{.Node}}{{ end }} `), t) defer test.DeleteTempfile(inTemplate, t) outTemplate := test.CreateTempfile([]byte(` consul1consul2 `), t) defer test.DeleteTempfile(outTemplate, t) config := DefaultConfig() config.Merge(&Config{ ConfigTemplates: []*ConfigTemplate{ &ConfigTemplate{ Source: inTemplate.Name(), Destination: outTemplate.Name(), Command: fmt.Sprintf("echo 'foo' > %s", outFile.Name()), }, }, }) runner, err := NewRunner(config, false, false) if err != nil { t.Fatal(err) } d, err := dep.ParseHealthServices("consul@nyc1") if err != nil { t.Fatal(err) } data := []*dep.HealthService{ &dep.HealthService{Node: "consul1"}, &dep.HealthService{Node: "consul2"}, } runner.Receive(d, data) if err := runner.Run(); err != nil { t.Fatal(err) } _, err = os.Stat(outFile.Name()) if !os.IsNotExist(err) { t.Fatalf("expected command to not be run") } }
func TestRun_dry(t *testing.T) { in := test.CreateTempfile([]byte(` {{ range service "consul@nyc1" }}{{.Node}}{{ end }} `), t) defer test.DeleteTempfile(in, t) config := DefaultConfig() config.Merge(&Config{ ConfigTemplates: []*ConfigTemplate{ &ConfigTemplate{ Source: in.Name(), Destination: "/out/file.txt", }, }, }) runner, err := NewRunner(config, true, false) if err != nil { t.Fatal(err) } d, err := dep.ParseHealthServices("consul@nyc1") if err != nil { t.Fatal(err) } data := []*dep.HealthService{ &dep.HealthService{Node: "consul1"}, &dep.HealthService{Node: "consul2"}, } runner.dependencies[d.HashCode()] = d runner.watcher.ForceWatching(d, true) runner.Receive(d, data) buff := gatedio.NewByteBuffer() runner.outStream, runner.errStream = buff, buff if err := runner.Run(); err != nil { t.Fatal(err) } actual := bytes.TrimSpace(buff.Bytes()) expected := bytes.TrimSpace([]byte(` > /out/file.txt consul1consul2 `)) if !bytes.Equal(actual, expected) { t.Errorf("expected \n%q\n to equal \n%q\n", actual, expected) } }
func TestDedup_UpdateDeps(t *testing.T) { t.Parallel() // Create a template in := test.CreateTempfile([]byte(` {{ range service "consul" }}{{.Node}}{{ end }} `), t) defer test.DeleteTempfile(in, t) tmpl, err := NewTemplate(in.Name()) if err != nil { t.Fatalf("err: %v", err) } consul, dedup := testDedupManager(t, []*Template{tmpl}) defer consul.Stop() // Start dedup if err := dedup.Start(); err != nil { t.Fatalf("err: %v", err) } defer dedup.Stop() // Wait until we are leader select { case <-dedup.UpdateCh(): case <-time.After(2 * time.Second): t.Fatalf("timeout") } // Create the dependency dep, err := dependency.ParseHealthServices("consul") if err != nil { t.Fatalf("err: %v", err) } // Inject data into the brain dedup.brain.Remember(dep, 123) // Update the dependencies err = dedup.UpdateDeps(tmpl, []dependency.Dependency{dep}) if err != nil { t.Fatalf("err: %v", err) } }
func TestRun_removesUnusedDependencies(t *testing.T) { in := test.CreateTempfile([]byte(nil), t) defer test.DeleteTempfile(in, t) config := DefaultConfig() config.Merge(&Config{ ConfigTemplates: []*ConfigTemplate{ &ConfigTemplate{Source: in.Name()}, }, }) runner, err := NewRunner(config, true, false) if err != nil { t.Fatal(err) } d, err := dep.ParseHealthServices("consul@nyc2") if err != nil { t.Fatal(err) } runner.dependencies = map[string]dep.Dependency{"consul@nyc2": d} if err := runner.Run(); err != nil { t.Fatal(err) } if len(runner.dependencies) != 0 { t.Errorf("expected %d to be %d", len(runner.dependencies), 0) } if runner.watcher.Watching(d) { t.Errorf("expected watcher to stop watching dependency") } if _, ok := runner.brain.Recall(d); ok { t.Errorf("expected brain to forget dependency") } }
func TestServiceFunc_hasData(t *testing.T) { d, err := dep.ParseHealthServices("existing") if err != nil { t.Fatal(err) } data := []*dep.HealthService{ &dep.HealthService{Node: "a"}, &dep.HealthService{Node: "b"}, } brain := NewBrain() brain.Remember(d, data) used := make(map[string]dep.Dependency) missing := make(map[string]dep.Dependency) f := serviceFunc(brain, used, missing) result, err := f("existing") if err != nil { t.Fatal(err) } expected := data if !reflect.DeepEqual(result, expected) { t.Errorf("expected %q to be %q", result, expected) } if len(missing) != 0 { t.Errorf("expected missing to have 0 elements, but had %d", len(missing)) } if _, ok := used[d.HashCode()]; !ok { t.Errorf("expected dep to be used") } }
func TestExecute_multipass(t *testing.T) { in := test.CreateTempfile([]byte(` {{ range ls "services" }}{{.Key}}:{{ range service .Key }} {{.Node}} {{.Address}}:{{.Port}}{{ end }} {{ end }} `), t) defer test.DeleteTempfile(in, t) tmpl, err := NewTemplate(in.Name()) if err != nil { t.Fatal(err) } brain := NewBrain() used, missing, result, err := tmpl.Execute(brain) if err != nil { t.Fatal(err) } if num := len(missing); num != 1 { t.Errorf("expected 1 missing, got: %d", num) } if num := len(used); num != 1 { t.Errorf("expected 1 used, got: %d", num) } expected := bytes.TrimSpace([]byte("")) result = bytes.TrimSpace(result) if !bytes.Equal(result, expected) { t.Errorf("expected %q to be %q", result, expected) } // Receive data for the key prefix dependency d1, err := dep.ParseStoreKeyPrefix("services") brain.Remember(d1, []*dep.KeyPair{ &dep.KeyPair{Key: "webapp", Value: "1"}, &dep.KeyPair{Key: "database", Value: "1"}, }) used, missing, result, err = tmpl.Execute(brain) if err != nil { t.Fatal(err) } if num := len(missing); num != 2 { t.Errorf("expected 2 missing, got: %d", num) } if num := len(used); num != 3 { t.Errorf("expected 3 used, got: %d", num) } expected = bytes.TrimSpace([]byte(` webapp: database: `)) result = bytes.TrimSpace(result) if !bytes.Equal(result, expected) { t.Errorf("expected \n%q\n to be \n%q\n", result, expected) } // Receive data for the services d2, err := dep.ParseHealthServices("webapp") brain.Remember(d2, []*dep.HealthService{ &dep.HealthService{Node: "web01", Address: "1.2.3.4", Port: 1234}, }) d3, err := dep.ParseHealthServices("database") brain.Remember(d3, []*dep.HealthService{ &dep.HealthService{Node: "db01", Address: "5.6.7.8", Port: 5678}, }) used, missing, result, err = tmpl.Execute(brain) if err != nil { t.Fatal(err) } if num := len(missing); num != 0 { t.Errorf("expected 0 missing, got: %d", num) } if num := len(used); num != 3 { t.Errorf("expected 3 used, got: %d", num) } expected = bytes.TrimSpace([]byte(` webapp: web01 1.2.3.4:1234 database: db01 5.6.7.8:5678 `)) result = bytes.TrimSpace(result) if !bytes.Equal(result, expected) { t.Errorf("expected \n%q\n to be \n%q\n", result, expected) } }
func TestExecute_renders(t *testing.T) { // Stub out the time. now = func() time.Time { return time.Unix(0, 0).UTC() } in := test.CreateTempfile([]byte(` API Functions ------------- datacenters:{{ range datacenters }} {{.}}{{ end }} file: {{ file "/path/to/file" }} key: {{ key "config/redis/maxconns" }} ls:{{ range ls "config/redis" }} {{.Key}}={{.Value}}{{ end }} node:{{ with node }} {{.Node.Node}}{{ range .Services}} {{.Service}}{{ end }}{{ end }} nodes:{{ range nodes }} {{.Node}}{{ end }} service:{{ range service "webapp" }} {{.Address}}{{ end }} service (any):{{ range service "webapp" "any" }} {{.Address}}{{ end }} service (tag.Contains):{{ range service "webapp" }}{{ if .Tags.Contains "production" }} {{.Node}}{{ end }}{{ end }} services:{{ range services }} {{.Name}}{{ end }} tree:{{ range tree "config/redis" }} {{.Key}}={{.Value}}{{ end }} vault: {{ with vault "secret/foo/bar" }}{{.Data.zip}}{{ end }} Helper Functions ---------------- byKey:{{ range $key, $pairs := tree "config/redis" | byKey }} {{$key}}:{{ range $pairs }} {{.Key}}={{.Value}}{{ end }}{{ end }} byTag (health service):{{ range $tag, $services := service "webapp" | byTag }} {{$tag}}:{{ range $services }} {{.Address}}{{ end }}{{ end }} byTag (catalog services):{{ range $tag, $services := services | byTag }} {{$tag}}:{{ range $services }} {{.Name}}{{ end }}{{ end }} contains:{{ range service "webapp" }}{{ if .Tags | contains "production" }} {{.Node}}{{ end }}{{ end }} env: {{ env "foo" }} explode:{{ range $k, $v := tree "config/redis" | explode }} {{$k}}{{$v}}{{ end }} in:{{ range service "webapp" }}{{ if in .Tags "production" }} {{.Node}}{{ end }}{{ end }} loop:{{ range loop 3 }} test{{ end }} loop(i):{{ range $i := loop 5 8 }} test{{$i}}{{ end }} join: {{ "a,b,c" | split "," | join ";" }} parseBool: {{"true" | parseBool}} parseFloat: {{"1.2" | parseFloat}} parseInt: {{"-1" | parseInt}} parseJSON (string):{{ range $key, $value := "{\"foo\": \"bar\"}" | parseJSON }} {{$key}}={{$value}}{{ end }} parseJSON (file):{{ range $key, $value := file "/path/to/json/file" | parseJSON }} {{$key}}={{$value}}{{ end }} parseJSON (env):{{ range $key, $value := env "json" | parseJSON }} {{$key}}={{$value}}{{ end }} parseUint: {{"1" | parseUint}} plugin: {{ file "/path/to/json/file" | plugin "echo" }} timestamp: {{ timestamp }} timestamp (formatted): {{ timestamp "2006-01-02" }} regexMatch: {{ file "/path/to/file" | regexMatch ".*[cont][a-z]+" }} regexMatch: {{ file "/path/to/file" | regexMatch "v[0-9]*" }} regexReplaceAll: {{ file "/path/to/file" | regexReplaceAll "\\w" "x" }} replaceAll: {{ file "/path/to/file" | replaceAll "some" "this" }} split:{{ range "a,b,c" | split "," }} {{.}}{{end}} toLower: {{ file "/path/to/file" | toLower }} toJSON: {{ tree "config/redis" | explode | toJSON }} toJSONPretty: {{ tree "config/redis" | explode | toJSONPretty }} toTitle: {{ file "/path/to/file" | toTitle }} toUpper: {{ file "/path/to/file" | toUpper }} toYAML: {{ tree "config/redis" | explode | toYAML }} Math Functions -------------- add:{{ 2 | add 2 }} subtract:{{ 2 | subtract 2 }} multiply:{{ 2 | multiply 2 }} divide:{{ 2 | divide 2 }} `), t) defer test.DeleteTempfile(in, t) tmpl, err := NewTemplate(in.Name()) if err != nil { t.Fatal(err) } brain := NewBrain() var d dep.Dependency d, err = dep.ParseDatacenters() if err != nil { t.Fatal(err) } brain.Remember(d, []string{"dc1", "dc2"}) d, err = dep.ParseFile("/path/to/file") if err != nil { t.Fatal(err) } brain.Remember(d, "some content") d, err = dep.ParseStoreKey("config/redis/maxconns") if err != nil { t.Fatal(err) } brain.Remember(d, "5") d, err = dep.ParseStoreKeyPrefix("config/redis") if err != nil { t.Fatal(err) } brain.Remember(d, []*dep.KeyPair{ &dep.KeyPair{Key: "", Value: ""}, &dep.KeyPair{Key: "admin/port", Value: "1134"}, &dep.KeyPair{Key: "maxconns", Value: "5"}, &dep.KeyPair{Key: "minconns", Value: "2"}, }) d, err = dep.ParseCatalogNode() if err != nil { t.Fatal(err) } brain.Remember(d, &dep.NodeDetail{ Node: &dep.Node{Node: "node1"}, Services: dep.NodeServiceList([]*dep.NodeService{ &dep.NodeService{ Service: "service1", }, }), }) d, err = dep.ParseCatalogNodes("") if err != nil { t.Fatal(err) } brain.Remember(d, []*dep.Node{ &dep.Node{Node: "node1"}, &dep.Node{Node: "node2"}, }) d, err = dep.ParseHealthServices("webapp") if err != nil { t.Fatal(err) } brain.Remember(d, []*dep.HealthService{ &dep.HealthService{ Node: "node1", Address: "1.2.3.4", Tags: []string{"release"}, }, &dep.HealthService{ Node: "node2", Address: "5.6.7.8", Tags: []string{"release", "production"}, }, &dep.HealthService{ Node: "node3", Address: "9.10.11.12", Tags: []string{"production"}, }, }) d, err = dep.ParseHealthServices("webapp", "any") if err != nil { t.Fatal(err) } brain.Remember(d, []*dep.HealthService{ &dep.HealthService{Node: "node1", Address: "1.2.3.4"}, &dep.HealthService{Node: "node2", Address: "5.6.7.8"}, }) d, err = dep.ParseCatalogServices("") if err != nil { t.Fatal(err) } brain.Remember(d, []*dep.CatalogService{ &dep.CatalogService{ Name: "service1", Tags: []string{"production"}, }, &dep.CatalogService{ Name: "service2", Tags: []string{"release", "production"}, }, }) d, err = dep.ParseVaultSecret("secret/foo/bar") if err != nil { t.Fatal(err) } brain.Remember(d, &dep.Secret{ LeaseID: "abcd1234", LeaseDuration: 120, Renewable: true, Data: map[string]interface{}{"zip": "zap"}, }) if err := os.Setenv("foo", "bar"); err != nil { t.Fatal(err) } d, err = dep.ParseFile("/path/to/json/file") if err != nil { t.Fatal(err) } brain.Remember(d, `{"foo": "bar"}`) if err := os.Setenv("json", `{"foo": "bar"}`); err != nil { t.Fatal(err) } _, _, result, err := tmpl.Execute(brain) if err != nil { t.Fatal(err) } expected := []byte(` API Functions ------------- datacenters: dc1 dc2 file: some content key: 5 ls: maxconns=5 minconns=2 node: node1 service1 nodes: node1 node2 service: 1.2.3.4 5.6.7.8 9.10.11.12 service (any): 1.2.3.4 5.6.7.8 service (tag.Contains): node2 node3 services: service1 service2 tree: admin/port=1134 maxconns=5 minconns=2 vault: zap Helper Functions ---------------- byKey: admin: port=1134 byTag (health service): production: 5.6.7.8 9.10.11.12 release: 1.2.3.4 5.6.7.8 byTag (catalog services): production: service1 service2 release: service2 contains: node2 node3 env: bar explode: adminmap[port:1134] maxconns5 minconns2 in: node2 node3 loop: test test test loop(i): test5 test6 test7 join: a;b;c parseBool: true parseFloat: 1.2 parseInt: -1 parseJSON (string): foo=bar parseJSON (file): foo=bar parseJSON (env): foo=bar parseUint: 1 plugin: {"foo": "bar"} timestamp: 1970-01-01T00:00:00Z timestamp (formatted): 1970-01-01 regexMatch: true regexMatch: false regexReplaceAll: xxxx xxxxxxx replaceAll: this content split: a b c toLower: some content toJSON: {"admin":{"port":"1134"},"maxconns":"5","minconns":"2"} toJSONPretty: { "admin": { "port": "1134" }, "maxconns": "5", "minconns": "2" } toTitle: Some Content toUpper: SOME CONTENT toYAML: admin: port: "1134" maxconns: "5" minconns: "2" Math Functions -------------- add:4 subtract:0 multiply:4 divide:1 `) if !bytes.Equal(result, expected) { t.Errorf("expected %s to be %s", result, expected) } }
func TestRun_doesNotExecuteCommandMoreThanOnce(t *testing.T) { outFile := test.CreateTempfile(nil, t) os.Remove(outFile.Name()) defer os.Remove(outFile.Name()) inTemplate := test.CreateTempfile([]byte(` {{ range service "consul@nyc1"}}{{ end }} `), t) defer test.DeleteTempfile(inTemplate, t) outTemplateA := test.CreateTempfile(nil, t) defer test.DeleteTempfile(outTemplateA, t) outTemplateB := test.CreateTempfile(nil, t) defer test.DeleteTempfile(outTemplateB, t) config := DefaultConfig() config.Merge(&Config{ ConfigTemplates: []*ConfigTemplate{ &ConfigTemplate{ Source: inTemplate.Name(), Destination: outTemplateA.Name(), Command: fmt.Sprintf("echo 'foo' >> %s", outFile.Name()), }, &ConfigTemplate{ Source: inTemplate.Name(), Destination: outTemplateB.Name(), Command: fmt.Sprintf("echo 'foo' >> %s", outFile.Name()), }, }, }) runner, err := NewRunner(config, false, false) if err != nil { t.Fatal(err) } d, err := dep.ParseHealthServices("consul@nyc1") if err != nil { t.Fatal(err) } data := []*dep.HealthService{ &dep.HealthService{ Node: "consul", Address: "1.2.3.4", ID: "consul@nyc1", Name: "consul", }, } runner.dependencies[d.HashCode()] = d runner.Receive(d, data) if err := runner.Run(); err != nil { t.Fatal(err) } _, err = os.Stat(outFile.Name()) if err != nil { t.Fatal(err) } output, err := ioutil.ReadFile(outFile.Name()) if err != nil { t.Fatal(err) } if strings.Count(string(output), "foo") > 1 { t.Fatalf("expected command to be run once.") } }
func TestRun_multipleTemplatesRunsCommands(t *testing.T) { in1 := test.CreateTempfile([]byte(` {{ range service "consul@nyc1" }}{{.Node}}{{ end }} `), t) defer test.DeleteTempfile(in1, t) in2 := test.CreateTempfile([]byte(` {{range service "consul@nyc2"}}{{.Node}}{{ end }} `), t) defer test.DeleteTempfile(in2, t) out1 := test.CreateTempfile(nil, t) test.DeleteTempfile(out1, t) out2 := test.CreateTempfile(nil, t) test.DeleteTempfile(out2, t) touch1, err := ioutil.TempFile(os.TempDir(), "touch1-") if err != nil { t.Fatal(err) } os.Remove(touch1.Name()) defer os.Remove(touch1.Name()) touch2, err := ioutil.TempFile(os.TempDir(), "touch2-") if err != nil { t.Fatal(err) } os.Remove(touch2.Name()) defer os.Remove(touch2.Name()) config := DefaultConfig() config.Merge(&Config{ ConfigTemplates: []*ConfigTemplate{ &ConfigTemplate{ Source: in1.Name(), Destination: out1.Name(), Command: fmt.Sprintf("touch %s", touch1.Name()), }, &ConfigTemplate{ Source: in2.Name(), Destination: out2.Name(), Command: fmt.Sprintf("touch %s", touch2.Name()), }, }, }) runner, err := NewRunner(config, false, false) if err != nil { t.Fatal(err) } d, err := dep.ParseHealthServices("consul@nyc1") if err != nil { t.Fatal(err) } data := []*dep.HealthService{ &dep.HealthService{Node: "consul1"}, &dep.HealthService{Node: "consul2"}, } runner.dependencies[d.HashCode()] = d runner.Receive(d, data) if err := runner.Run(); err != nil { t.Fatal(err) } if _, err := os.Stat(touch1.Name()); err != nil { t.Errorf("expected first command to run, but did not: %s", err) } if _, err := os.Stat(touch2.Name()); err == nil { t.Errorf("expected second command to not run, but touch exists") } }
func TestDedup_FollowerUpdate(t *testing.T) { t.Parallel() // Create a template in := test.CreateTempfile([]byte(` {{ range service "consul" }}{{.Node}}{{ end }} `), t) defer test.DeleteTempfile(in, t) tmpl, err := NewTemplate(in.Name()) if err != nil { t.Fatalf("err: %v", err) } consul, dedup1 := testDedupManager(t, []*Template{tmpl}) defer consul.Stop() dedup2 := testDedupFollower(t, dedup1) // Start dedups if err := dedup1.Start(); err != nil { t.Fatalf("err: %v", err) } defer dedup1.Stop() if err := dedup2.Start(); err != nil { t.Fatalf("err: %v", err) } defer dedup2.Stop() // Wait until we have a leader var leader, follow *DedupManager select { case <-dedup1.UpdateCh(): if dedup1.IsLeader(tmpl) { leader = dedup1 follow = dedup2 } case <-dedup2.UpdateCh(): if dedup2.IsLeader(tmpl) { leader = dedup2 follow = dedup1 } case <-time.After(2 * time.Second): t.Fatalf("timeout") } // Create the dependency dep, err := dependency.ParseHealthServices("consul") if err != nil { t.Fatalf("err: %v", err) } // Inject data into the brain leader.brain.Remember(dep, 123) // Update the dependencies err = leader.UpdateDeps(tmpl, []dependency.Dependency{dep}) if err != nil { t.Fatalf("err: %v", err) } // Follower should get an update select { case <-follow.UpdateCh(): case <-time.After(2 * time.Second): t.Fatalf("timeout") } // Recall from the brain data, ok := follow.brain.Recall(dep) if !ok { t.Fatalf("missing data") } if data != 123 { t.Fatalf("bad: %v", data) } }