v0.31.0 refinements

This commit is contained in:
lixen committed 2026-10-10 12:01:54 -04:00
1 parent bfcc15613a
commit 6bd5e12a60
52 files changed
+1948 -683

No files matched your search

+74 -7
View File
@@ -181,13 +181,34 @@ func (c *Composition) pipeline(i int) (*config.PipelineConfig, error) {
// AddPipeline appends an empty pipeline under a name no other has
func (c *Composition) AddPipeline(name string) error {
if name == "" || slices.ContainsFunc(c.Pipelines, func(p config.PipelineConfig) bool { return p.Name == name }) {
return fmt.Errorf("pipeline %q: a name no other pipeline has", name)
if err := c.free(name, -1); err != nil {
return err
}
c.Pipelines = append(c.Pipelines, config.PipelineConfig{Name: name, Flow: &config.FlowConfig{}})
return nil
}
// RenamePipeline gives pipeline i a name no other has
func (c *Composition) RenamePipeline(i int, name string) error {
p, err := c.pipeline(i)
if err == nil {
err = c.free(name, i)
}
if err != nil {
return err
}
p.Name = name
return nil
}
// free refuses a name that is empty or held by a pipeline other than i
func (c *Composition) free(name string, i int) error {
if j := slices.IndexFunc(c.Pipelines, func(p config.PipelineConfig) bool { return p.Name == name }); name == "" || j >= 0 && j != i {
return fmt.Errorf("pipeline %q: a name no other pipeline has", name)
}
return nil
}
func (c *Composition) RemovePipeline(i int) error {
if _, err := c.pipeline(i); err != nil {
return err
@@ -233,6 +254,52 @@ func (c *Composition) Add(pi int, role, typ string) (Node, error) {
return n, nil
}
// Retype gives a source or sink another type of its role, in its place: a
// chosen id stays and an automatic one follows the type. Options of the same
// name and kind carry over to a type on the same network side, where they
// mean the same; across sides host, port, tls and auth do not.
func (c *Composition) Retype(pi int, n Node, typ string) (Node, error) {
p, err := c.pipeline(pi)
if err != nil {
return Node{}, err
}
to, ok := config.LookupPlugin(n.Role, typ)
if !ok {
return Node{}, fmt.Errorf("unknown %s type %q", n.Role, typ)
}
var id, old *string
var opts *map[string]any
var ids []string
switch i := index(p, n); {
case i >= 0 && n.Role == "source":
id, old, opts = &p.PluginSources[i].ID, &p.PluginSources[i].Type, &p.PluginSources[i].Config
for _, s := range p.PluginSources {
ids = append(ids, s.ID)
}
case i >= 0 && n.Role == "sink":
id, old, opts = &p.PluginSinks[i].ID, &p.PluginSinks[i].Type, &p.PluginSinks[i].Config
for _, s := range p.PluginSinks {
ids = append(ids, s.ID)
}
default:
return Node{}, n.missing()
}
from, _ := config.LookupPlugin(n.Role, *old)
kept := map[string]any{}
for _, k := range from.Keys() {
same := func(t config.Key) bool { return t.Name == k.Name && t.Kind == k.Kind }
if v, set := (*opts)[k.Name]; set && from.Side == to.Side && slices.ContainsFunc(to.Keys(), same) {
kept[k.Name] = v
}
}
suffix, auto := strings.CutPrefix(*id, *old)
if n, err := strconv.Atoi(strings.TrimPrefix(suffix, "_")); auto && (suffix == "" || suffix[0] == '_' && err == nil && n > 1) {
*id = config.FreeID(typ, slices.DeleteFunc(ids, func(s string) bool { return s == *id }))
}
*old, *opts = typ, kept
return Node{Role: n.Role, ID: *id}, nil
}
// Remove deletes a source, sink or filter, or turns a flow stage off
func (c *Composition) Remove(pi int, n Node) error {
p, err := c.pipeline(pi)
@@ -353,22 +420,22 @@ func (c *Composition) edit(pi int, n Node, change func(map[string]any) error) er
return plugin(p.PluginSinks[i].Type, &p.PluginSinks[i].Config)
case n.Role == "filters":
filter := &p.Flow.Filters[i]
if err := editStage(reflect.ValueOf(&filter).Elem(), change); err != nil {
if err := editStage(reflect.ValueOf(&filter).Elem(), "", change); err != nil {
return err
}
p.Flow.Filters[i] = *filter
return nil
}
if stage, ok := stages(p.Flow)[n.Role]; ok {
return editStage(stage, change)
return editStage(stage, n.Role, change)
}
return n.missing()
}
// editStage edits a stage's keys as a spec sets them; the table they scan into
// replaces the stage, which turns on
func editStage(stage reflect.Value, change func(map[string]any) error) error {
m := map[string]any{}
// replaces the stage, which turns on as a spec turns it on
func editStage(stage reflect.Value, key string, change func(map[string]any) error) error {
m := config.StageOn(key, nil)
if !stage.IsNil() {
m = config.Values(stage.Interface())
}
+70 -1
View File
@@ -1,6 +1,7 @@
package compose
import (
"fmt"
"os"
"os/exec"
"reflect"
@@ -64,7 +65,7 @@ func TestWrittenFormsStartTheSameComposition(t *testing.T) {
_, err = c.Add(0, "filters", "include")
must(err)
must(c.MoveFilter(0, 1, 0))
must(c.Set(0, Node{Role: "rate_limit"}, "rate", "100"))
must(c.Set(0, Node{Role: "rate_limit"}, "entries_per_second", "100"))
must(c.Set(0, Node{Role: "heartbeat"}, "interval_ms", "5000"))
must(c.Set(0, Node{Role: "heartbeat"}, "enabled", "true"))
must(c.AddPipeline("-b"))
@@ -190,3 +191,71 @@ func TestUnnamedPartsTakeFreeIDs(t *testing.T) {
t.Fatalf("%v\n%s", err, line)
}
}
// Retype keeps the part's place and a chosen id, renames an automatic one,
// and carries the options of the same name and kind only within a side
func TestRetypeKeepsThePlace(t *testing.T) {
c := &Composition{}
if err := c.AddPipeline("p"); err != nil {
t.Fatal(err)
}
for _, typ := range []string{"tcp", "http", "tcp"} {
if _, err := c.Add(0, "sink", typ); err != nil {
t.Fatal(err)
}
}
tcp2, chosen := Node{Role: "sink", ID: "tcp_2"}, Node{Role: "sink", ID: "web"}
c.Pipelines[0].PluginSinks[1].ID = chosen.ID
for _, err := range []error{c.Set(0, tcp2, "port", "9000"), c.Set(0, tcp2, "keep_alive", "false"), c.Set(0, chosen, "port", "80")} {
if err != nil {
t.Fatal(err)
}
}
got, err := c.Retype(0, tcp2, "http")
if err != nil || got.ID != "http" {
t.Fatalf("%+v %v", got, err)
}
if _, err := c.Retype(0, chosen, "tcp_chain"); err != nil {
t.Fatal(err)
}
var parts []string
for _, s := range c.Pipelines[0].PluginSinks {
parts = append(parts, fmt.Sprintf("%s=%s %v", s.ID, s.Type, s.Config))
}
if want := []string{"tcp=tcp map[]", "web=tcp_chain map[]", "http=http map[port:9000]"}; !slices.Equal(parts, want) {
t.Fatalf("%q, want %q", parts, want)
}
}
// A pipeline takes a name no other has, renamed as when added
func TestPipelineNamesAreUnique(t *testing.T) {
c := &Composition{}
for _, name := range []string{"a", "b"} {
if err := c.AddPipeline(name); err != nil {
t.Fatal(err)
}
}
if c.AddPipeline("a") == nil || c.RenamePipeline(1, "a") == nil || c.RenamePipeline(1, "") == nil {
t.Fatal("took a name another pipeline has")
}
if err := c.RenamePipeline(1, "b"); err != nil || c.RenamePipeline(1, "c") != nil || c.Pipelines[1].Name != "c" {
t.Fatalf("%v, %+v", err, c.Pipelines)
}
}
// A stage an edit turns on does what its spec would: a rate limit drops what
// passes its rate, a heartbeat beats
func TestAStageTurnsOnAsItsSpecDoes(t *testing.T) {
c := &Composition{}
if err := c.AddPipeline("p"); err != nil {
t.Fatal(err)
}
for _, err := range []error{c.Set(0, Node{Role: "rate_limit"}, "entries_per_second", "5"), c.Set(0, Node{Role: "heartbeat"}, "interval_ms", "500")} {
if err != nil {
t.Fatal(err)
}
}
if f := c.Pipelines[0].Flow; f.RateLimit.Policy != "drop" || !f.Heartbeat.Enabled {
t.Fatalf("%+v %+v", f.RateLimit, f.Heartbeat)
}
}