diff --git a/.changes/unreleased/Added-20250627-152055.yaml b/.changes/unreleased/Added-20250627-152055.yaml new file mode 100644 index 00000000..08faa1d6 --- /dev/null +++ b/.changes/unreleased/Added-20250627-152055.yaml @@ -0,0 +1,3 @@ +kind: Added +body: Added support for adding providers through mach config directly, without needing plugins +time: 2025-06-27T15:20:55.908740714+02:00 diff --git a/.changes/unreleased/Added-20250708-114044.yaml b/.changes/unreleased/Added-20250708-114044.yaml new file mode 100644 index 00000000..784abec3 --- /dev/null +++ b/.changes/unreleased/Added-20250708-114044.yaml @@ -0,0 +1,3 @@ +kind: Added +body: Added option to set site execution order +time: 2025-07-08T11:40:44.556019581+02:00 diff --git a/go.mod b/go.mod index f438ee79..f5e11d3e 100644 --- a/go.mod +++ b/go.mod @@ -52,6 +52,7 @@ require ( github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/flosch/pongo2/v5 v5.0.0 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-viper/mapstructure/v2 v2.2.1 // indirect diff --git a/go.sum b/go.sum index b6891bba..958d3b99 100644 --- a/go.sum +++ b/go.sum @@ -745,6 +745,8 @@ github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/flosch/pongo2/v5 v5.0.0 h1:ZauMp+iPZzh2aI1QM2UwRb0lXD4BoFcvBuWqefkIuq0= +github.com/flosch/pongo2/v5 v5.0.0/go.mod h1:6ysKu++8ANFXmc3x6uA6iVaS+PKUoDfdX3yPcv8TIzY= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= diff --git a/internal/batcher/batcher.go b/internal/batcher/batcher.go index 4a021e39..aaf852d8 100644 --- a/internal/batcher/batcher.go +++ b/internal/batcher/batcher.go @@ -1,5 +1,57 @@ package batcher -import "github.com/mach-composer/mach-composer-cli/internal/graph" +import ( + "fmt" + "github.com/mach-composer/mach-composer-cli/internal/config" + "github.com/mach-composer/mach-composer-cli/internal/graph" + "slices" +) -type BatchFunc func(g *graph.Graph) map[int][]graph.Node +type BatchFunc func(g *graph.Graph) (map[int][]graph.Node, error) + +type Batcher string + +func Factory(cfg *config.MachConfig) (BatchFunc, error) { + switch cfg.MachComposer.Batcher.Type { + case "": + fallthrough + case "simple": + return simpleBatchFunc(), nil + case "site": + var siteOrder, err = DetermineSiteOrder(cfg) + if err != nil { + return nil, fmt.Errorf("failed determining site order: %w", err) + } + + return siteBatchFunc(siteOrder), nil + default: + return nil, fmt.Errorf("unknown batch type %s", cfg.MachComposer.Batcher.Type) + } +} + +func DetermineSiteOrder(cfg *config.MachConfig) ([]string, error) { + var identifiers = cfg.Sites.Identifiers() + var siteOrder = make([]string, len(identifiers)) + + if len(cfg.MachComposer.Batcher.SiteOrder) > 0 { + // Use the site order from the configuration if provided + siteOrder = cfg.MachComposer.Batcher.SiteOrder + + // Make sure the site order contains the same fields as the identifiers + if len(siteOrder) != len(identifiers) { + return nil, fmt.Errorf("site order length %d does not match identifiers length %d", len(siteOrder), len(identifiers)) + } + for _, siteIdentifier := range siteOrder { + if !slices.Contains(identifiers, siteIdentifier) { + return nil, fmt.Errorf("site order contains siteIdentifier %s that is not in the identifiers list", siteIdentifier) + } + } + + } else { + for i, identifier := range identifiers { + siteOrder[i] = identifier + } + } + + return siteOrder, nil +} diff --git a/internal/batcher/batcher_test.go b/internal/batcher/batcher_test.go new file mode 100644 index 00000000..06208b04 --- /dev/null +++ b/internal/batcher/batcher_test.go @@ -0,0 +1,150 @@ +package batcher + +import ( + "github.com/mach-composer/mach-composer-cli/internal/config" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestReturnsErrorWhenUnknownBatchType(t *testing.T) { + cfg := &config.MachConfig{ + MachComposer: config.MachComposer{ + Batcher: config.Batcher{ + Type: "unknown", + }, + }, + } + + _, err := Factory(cfg) + assert.Error(t, err) + assert.Contains(t, err.Error(), "unknown batch type unknown") +} + +func TestReturnsSimpleBatchFuncWhenTypeIsEmpty(t *testing.T) { + cfg := &config.MachConfig{ + MachComposer: config.MachComposer{ + Batcher: config.Batcher{ + Type: "", + }, + }, + } + + batchFunc, err := Factory(cfg) + assert.NoError(t, err) + assert.NotNil(t, batchFunc) +} + +func TestReturnsSimpleBatchFuncWhenTypeIsSimple(t *testing.T) { + cfg := &config.MachConfig{ + MachComposer: config.MachComposer{ + Batcher: config.Batcher{ + Type: "simple", + }, + }, + } + + batchFunc, err := Factory(cfg) + assert.NoError(t, err) + assert.NotNil(t, batchFunc) +} + +func TestReturnsSiteBatchFunc(t *testing.T) { + cfg := &config.MachConfig{ + MachComposer: config.MachComposer{ + Batcher: config.Batcher{ + Type: "site", + }, + }, + Sites: config.SiteConfigs{ + { + Identifier: "site-1", + }, + { + Identifier: "site-2", + }, + }, + } + + batchFunc, err := Factory(cfg) + assert.NoError(t, err) + assert.NotNil(t, batchFunc) +} + +func TestDetermineSiteOrderReturnsIdentifiersWhenNoSiteOrderProvided(t *testing.T) { + cfg := &config.MachConfig{ + MachComposer: config.MachComposer{ + Batcher: config.Batcher{}, + }, + Sites: config.SiteConfigs{ + {Identifier: "site-1"}, + {Identifier: "site-2"}, + }, + } + order, err := DetermineSiteOrder(cfg) + assert.NoError(t, err) + assert.Equal(t, []string{"site-1", "site-2"}, order) +} + +func TestDetermineSiteOrderReturnsSiteOrderWhenProvided(t *testing.T) { + cfg := &config.MachConfig{ + MachComposer: config.MachComposer{ + Batcher: config.Batcher{ + SiteOrder: []string{"site-2", "site-1"}, + }, + }, + Sites: config.SiteConfigs{ + {Identifier: "site-1"}, + {Identifier: "site-2"}, + }, + } + order, err := DetermineSiteOrder(cfg) + assert.NoError(t, err) + assert.Equal(t, []string{"site-2", "site-1"}, order) +} + +func TestDetermineSiteOrderReturnsErrorWhenSiteOrderLengthMismatch(t *testing.T) { + cfg := &config.MachConfig{ + MachComposer: config.MachComposer{ + Batcher: config.Batcher{ + SiteOrder: []string{"site-1", "site-2"}, + }, + }, + Sites: config.SiteConfigs{ + {Identifier: "site-1"}, + }, + } + order, err := DetermineSiteOrder(cfg) + assert.Error(t, err) + assert.Nil(t, order) + assert.Contains(t, err.Error(), "site order length 2 does not match identifiers length 1") +} + +func TestDetermineSiteOrderReturnsErrorWhenSiteOrderContainsUnknownIdentifier(t *testing.T) { + cfg := &config.MachConfig{ + MachComposer: config.MachComposer{ + Batcher: config.Batcher{ + SiteOrder: []string{"site-1", "unknown-site"}, + }, + }, + Sites: config.SiteConfigs{ + {Identifier: "site-1"}, + {Identifier: "site-2"}, + }, + } + order, err := DetermineSiteOrder(cfg) + assert.Error(t, err) + assert.Nil(t, order) + assert.Contains(t, err.Error(), "site order contains siteIdentifier unknown-site that is not in the identifiers list") +} + +func TestDetermineSiteOrderReturnsEmptyWhenNoSites(t *testing.T) { + cfg := &config.MachConfig{ + MachComposer: config.MachComposer{ + Batcher: config.Batcher{}, + }, + Sites: config.SiteConfigs{}, + } + order, err := DetermineSiteOrder(cfg) + assert.NoError(t, err) + assert.Empty(t, order) +} diff --git a/internal/batcher/naive_batcher.go b/internal/batcher/simple_batcher.go similarity index 70% rename from internal/batcher/naive_batcher.go rename to internal/batcher/simple_batcher.go index 04aabd63..07275021 100644 --- a/internal/batcher/naive_batcher.go +++ b/internal/batcher/simple_batcher.go @@ -2,8 +2,9 @@ package batcher import "github.com/mach-composer/mach-composer-cli/internal/graph" -func NaiveBatchFunc() BatchFunc { - return func(g *graph.Graph) map[int][]graph.Node { +// simpleBatchFunc returns a BatchFunc that batches nodes based on their depth in the graph. +func simpleBatchFunc() BatchFunc { + return func(g *graph.Graph) (map[int][]graph.Node, error) { batches := map[int][]graph.Node{} var sets = map[string][]graph.Path{} @@ -24,6 +25,6 @@ func NaiveBatchFunc() BatchFunc { batches[mx] = append(batches[mx], n) } - return batches + return batches, nil } } diff --git a/internal/batcher/naive_batcher_test.go b/internal/batcher/simple_batcher_test.go similarity index 88% rename from internal/batcher/naive_batcher_test.go rename to internal/batcher/simple_batcher_test.go index 76497521..e884d80b 100644 --- a/internal/batcher/naive_batcher_test.go +++ b/internal/batcher/simple_batcher_test.go @@ -7,7 +7,7 @@ import ( "testing" ) -func TestBatchNodesDepth1(t *testing.T) { +func TestSimpleBatchNodesDepth1(t *testing.T) { ig := graph.New(func(n internalgraph.Node) string { return n.Path() }, graph.Directed(), graph.Tree(), graph.PreventCycles()) start := new(internalgraph.NodeMock) @@ -17,12 +17,13 @@ func TestBatchNodesDepth1(t *testing.T) { g := &internalgraph.Graph{Graph: ig, StartNode: start} - batches := NaiveBatchFunc()(g) + batches, err := simpleBatchFunc()(g) + assert.NoError(t, err) assert.Equal(t, 1, len(batches)) } -func TestBatchNodesDepth2(t *testing.T) { +func TestSimpleBatchNodesDepth2(t *testing.T) { ig := graph.New(func(n internalgraph.Node) string { return n.Path() }, graph.Directed(), graph.Tree(), graph.PreventCycles()) site := new(internalgraph.NodeMock) @@ -43,8 +44,9 @@ func TestBatchNodesDepth2(t *testing.T) { g := &internalgraph.Graph{Graph: ig, StartNode: site} - batches := NaiveBatchFunc()(g) + batches, err := simpleBatchFunc()(g) + assert.NoError(t, err) assert.Equal(t, 2, len(batches)) assert.Equal(t, 1, len(batches[0])) assert.Equal(t, "main/site-1", batches[0][0].Path()) @@ -53,7 +55,7 @@ func TestBatchNodesDepth2(t *testing.T) { assert.Contains(t, batches[1][1].Path(), "component") } -func TestBatchNodesDepth3(t *testing.T) { +func TestSimpleBatchNodesDepth3(t *testing.T) { ig := graph.New(func(n internalgraph.Node) string { return n.Path() }, graph.Directed(), graph.Tree(), graph.PreventCycles()) site := new(internalgraph.NodeMock) @@ -74,8 +76,9 @@ func TestBatchNodesDepth3(t *testing.T) { g := &internalgraph.Graph{Graph: ig, StartNode: site} - batches := NaiveBatchFunc()(g) + batches, err := simpleBatchFunc()(g) + assert.NoError(t, err) assert.Equal(t, 3, len(batches)) assert.Equal(t, 1, len(batches[0])) assert.Equal(t, "main/site-1", batches[0][0].Path()) diff --git a/internal/batcher/site_batcher.go b/internal/batcher/site_batcher.go new file mode 100644 index 00000000..6abe37e6 --- /dev/null +++ b/internal/batcher/site_batcher.go @@ -0,0 +1,71 @@ +package batcher + +import ( + "fmt" + "github.com/mach-composer/mach-composer-cli/internal/graph" + "golang.org/x/exp/maps" +) + +// siteBatchFunc returns a BatchFunc that batches nodes based on their site order before considering their depth in +// the graph. +func siteBatchFunc(siteOrder []string) BatchFunc { + return func(g *graph.Graph) (map[int][]graph.Node, error) { + batches := map[int][]graph.Node{} + + var projects = g.Vertices().Filter(graph.ProjectType) + if len(projects) != 1 { + return nil, fmt.Errorf("expected 1 project, got %d", len(projects)) + } + var project = projects[0] + + var sites = g.Vertices().Filter(graph.SiteType) + + batches[0] = []graph.Node{project} + + for _, siteIdentifier := range siteOrder { + var sets = map[string][]graph.Path{} + var site = sites.FilterByIdentifier(siteIdentifier) + if site == nil { + return nil, fmt.Errorf("site with identifier %s not found", siteIdentifier) + } + + pg, err := g.ExtractSubGraph(site) + if err != nil { + return nil, err + } + + for _, n := range pg.Vertices() { + var route, _ = pg.Routes(n.Path(), site.Path()) + sets[n.Path()] = route + } + + var siteBatches = map[int][]graph.Node{} + + for k, routes := range sets { + var mx int + for _, route := range routes { + if len(route) > mx { + mx = len(route) + } + } + n, _ := pg.Vertex(k) + siteBatches[mx] = append(siteBatches[mx], n) + } + + // Get the highest int in the batches map + var keys = maps.Keys(batches) + var maxKey int + for _, key := range keys { + if key > maxKey { + maxKey = key + } + } + + for k, v := range siteBatches { + batches[maxKey+k+1] = append(batches[maxKey+k+1], v...) + } + } + + return batches, nil + } +} diff --git a/internal/batcher/site_batcher_test.go b/internal/batcher/site_batcher_test.go new file mode 100644 index 00000000..4efd6173 --- /dev/null +++ b/internal/batcher/site_batcher_test.go @@ -0,0 +1,100 @@ +package batcher + +import ( + "github.com/dominikbraun/graph" + internalgraph "github.com/mach-composer/mach-composer-cli/internal/graph" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestSiteBatchFuncReturnsErrorWhenNoProjects(t *testing.T) { + ig := graph.New(func(n internalgraph.Node) string { return n.Path() }, graph.Directed(), graph.Tree(), graph.PreventCycles()) + + site := new(internalgraph.NodeMock) + site.On("Path").Return("main/site-1") + site.On("Type").Return(internalgraph.SiteType) + site.On("Identifier").Return("site-1") + + _ = ig.AddVertex(site) + + g := &internalgraph.Graph{Graph: ig, StartNode: site} + + batches, err := siteBatchFunc([]string{"site-1"})(g) + assert.Nil(t, batches) + assert.Error(t, err) + assert.Contains(t, err.Error(), "expected 1 project") +} + +func TestSiteBatchFuncReturnsErrorWhenMultipleProjects(t *testing.T) { + ig := graph.New(func(n internalgraph.Node) string { return n.Path() }, graph.Directed(), graph.Tree(), graph.PreventCycles()) + + project1 := new(internalgraph.NodeMock) + project1.On("Path").Return("main/project-1") + project1.On("Type").Return(internalgraph.ProjectType) + + project2 := new(internalgraph.NodeMock) + project2.On("Path").Return("main/project-2") + project2.On("Type").Return(internalgraph.ProjectType) + + _ = ig.AddVertex(project1) + _ = ig.AddVertex(project2) + + g := &internalgraph.Graph{Graph: ig, StartNode: project1} + + batches, err := siteBatchFunc([]string{})(g) + assert.Nil(t, batches) + assert.Error(t, err) + assert.Contains(t, err.Error(), "expected 1 project") +} + +func TestSiteBatchFuncReturnsErrorWhenSiteNotFound(t *testing.T) { + ig := graph.New(func(n internalgraph.Node) string { return n.Path() }, graph.Directed(), graph.Tree(), graph.PreventCycles()) + + project := new(internalgraph.NodeMock) + project.On("Path").Return("main/project-1") + project.On("Type").Return(internalgraph.ProjectType) + + _ = ig.AddVertex(project) + + g := &internalgraph.Graph{Graph: ig, StartNode: project} + batches, err := siteBatchFunc([]string{"site-unknown"})(g) + assert.Nil(t, batches) + assert.Error(t, err) + assert.Contains(t, err.Error(), "site with identifier site-unknown not found") +} + +func TestSiteBatchFuncBatchesNodesBySiteOrderAndDepth(t *testing.T) { + ig := graph.New(func(n internalgraph.Node) string { return n.Path() }, graph.Directed(), graph.Tree(), graph.PreventCycles()) + + project := new(internalgraph.NodeMock) + project.On("Path").Return("main/project-1") + project.On("Type").Return(internalgraph.ProjectType) + + site := new(internalgraph.NodeMock) + site.On("Path").Return("main/site-1") + site.On("Type").Return(internalgraph.SiteType) + site.On("Identifier").Return("site-1") + + component := new(internalgraph.NodeMock) + component.On("Path").Return("main/site-1/component-1") + component.On("Type").Return(internalgraph.SiteComponentType) + component.On("Children").Return([]internalgraph.Node{}, nil) + + site.On("Children").Return([]internalgraph.Node{component}, nil) + + _ = ig.AddVertex(project) + _ = ig.AddVertex(site) + _ = ig.AddVertex(component) + + _ = ig.AddEdge("main/project-1", "main/site-1") + _ = ig.AddEdge("main/site-1", "main/site-1/component-1") + + g := &internalgraph.Graph{Graph: ig, StartNode: project} + + batches, err := siteBatchFunc([]string{"site-1"})(g) + assert.NoError(t, err) + assert.NotNil(t, batches) + assert.Equal(t, 3, len(batches)) + assert.Equal(t, "main/project-1", batches[0][0].Path()) + assert.Equal(t, "main/site-1", batches[1][0].Path()) +} diff --git a/internal/cmd/apply.go b/internal/cmd/apply.go index 89d4d06a..7b3d61c9 100644 --- a/internal/cmd/apply.go +++ b/internal/cmd/apply.go @@ -71,11 +71,14 @@ func applyFunc(cmd *cobra.Command, _ []string) error { return err } - r := runner.NewGraphRunner( - batcher.NaiveBatchFunc(), - hash.Factory(cfg), - commonFlags.workers, - ) + b, err := batcher.Factory(cfg) + if err != nil { + return err + } + + h := hash.Factory(cfg) + + r := runner.NewGraphRunner(b, h, commonFlags.workers) return r.TerraformApply(ctx, dg, &runner.ApplyOptions{ ForceInit: applyFlags.forceInit, diff --git a/internal/cmd/init.go b/internal/cmd/init.go index b92c39cb..62958468 100644 --- a/internal/cmd/init.go +++ b/internal/cmd/init.go @@ -57,11 +57,14 @@ func initFunc(cmd *cobra.Command, _ []string) error { return err } - r := runner.NewGraphRunner( - batcher.NaiveBatchFunc(), - hash.Factory(cfg), - commonFlags.workers, - ) + b, err := batcher.Factory(cfg) + if err != nil { + return err + } + + h := hash.Factory(cfg) + + r := runner.NewGraphRunner(b, h, commonFlags.workers) return r.TerraformInit(ctx, dg, &runner.InitOptions{ BufferLogs: initFlags.bufferLogs, diff --git a/internal/cmd/plan.go b/internal/cmd/plan.go index c9d6b66c..d7567101 100644 --- a/internal/cmd/plan.go +++ b/internal/cmd/plan.go @@ -65,11 +65,14 @@ func planFunc(cmd *cobra.Command, _ []string) error { return err } - r := runner.NewGraphRunner( - batcher.NaiveBatchFunc(), - hash.Factory(cfg), - commonFlags.workers, - ) + b, err := batcher.Factory(cfg) + if err != nil { + return err + } + + h := hash.Factory(cfg) + + r := runner.NewGraphRunner(b, h, commonFlags.workers) return r.TerraformPlan(ctx, dg, &runner.PlanOptions{ ForceInit: planFlags.forceInit, diff --git a/internal/cmd/show-plan.go b/internal/cmd/show-plan.go index 060b4ba6..58144aca 100644 --- a/internal/cmd/show-plan.go +++ b/internal/cmd/show-plan.go @@ -54,11 +54,14 @@ func showPlanFunc(cmd *cobra.Command, _ []string) error { return err } - r := runner.NewGraphRunner( - batcher.NaiveBatchFunc(), - hash.Factory(cfg), - commonFlags.workers, - ) + b, err := batcher.Factory(cfg) + if err != nil { + return err + } + + h := hash.Factory(cfg) + + r := runner.NewGraphRunner(b, h, commonFlags.workers) return r.TerraformShow(ctx, dg, &runner.ShowPlanOptions{ ForceInit: showPlanFlags.forceInit, diff --git a/internal/cmd/terraform.go b/internal/cmd/terraform.go index 4974433c..4f8d1f04 100644 --- a/internal/cmd/terraform.go +++ b/internal/cmd/terraform.go @@ -49,11 +49,14 @@ func terraformFunc(cmd *cobra.Command, args []string) error { return err } - r := runner.NewGraphRunner( - batcher.NaiveBatchFunc(), - hash.Factory(cfg), - commonFlags.workers, - ) + b, err := batcher.Factory(cfg) + if err != nil { + return err + } + + h := hash.Factory(cfg) + + r := runner.NewGraphRunner(b, h, commonFlags.workers) return r.TerraformProxy(ctx, dg, &runner.ProxyOptions{ Command: args, diff --git a/internal/cmd/testdata/cases/generate/aws-deployment-type-mixed/expected/main/test-1/component-2/main.tf b/internal/cmd/testdata/cases/generate/aws-deployment-type-mixed/expected/main/test-1/component-2/main.tf index 26f6123b..9b8fed20 100755 --- a/internal/cmd/testdata/cases/generate/aws-deployment-type-mixed/expected/main/test-1/component-2/main.tf +++ b/internal/cmd/testdata/cases/generate/aws-deployment-type-mixed/expected/main/test-1/component-2/main.tf @@ -30,6 +30,10 @@ module "component-2" { variables = { parent_names = [data.terraform_remote_state.test-1.outputs.component-1.name] } + component_version = "test" + environment = "test" + site = "test-1" + tags = { "Component" : "component-2", "Site" : "test-1", "Version" : "test" } } output "component-2" { diff --git a/internal/cmd/testdata/cases/generate/aws-deployment-type-mixed/expected/main/test-1/main.tf b/internal/cmd/testdata/cases/generate/aws-deployment-type-mixed/expected/main/test-1/main.tf index 44d57f53..dd1e4dab 100755 --- a/internal/cmd/testdata/cases/generate/aws-deployment-type-mixed/expected/main/test-1/main.tf +++ b/internal/cmd/testdata/cases/generate/aws-deployment-type-mixed/expected/main/test-1/main.tf @@ -31,7 +31,11 @@ locals { # Component: component-1 module "component-1" { - source = "{{ .PWD }}/testdata/modules/application" + source = "{{ .PWD }}/testdata/modules/application" + component_version = "test" + environment = "test" + site = "test-1" + tags = { "Component" : "component-1", "Site" : "test-1", "Version" : "test" } } output "component-1" { diff --git a/internal/cmd/testdata/cases/generate/aws-deployment-type-site/expected/main/test-1/main.tf b/internal/cmd/testdata/cases/generate/aws-deployment-type-site/expected/main/test-1/main.tf index f7b74efe..c0f44ffe 100755 --- a/internal/cmd/testdata/cases/generate/aws-deployment-type-site/expected/main/test-1/main.tf +++ b/internal/cmd/testdata/cases/generate/aws-deployment-type-site/expected/main/test-1/main.tf @@ -31,7 +31,11 @@ locals { # Component: component-1 module "component-1" { - source = "{{ .PWD }}/testdata/modules/application" + source = "{{ .PWD }}/testdata/modules/application" + component_version = "test" + environment = "test" + site = "test-1" + tags = { "Component" : "component-1", "Site" : "test-1", "Version" : "test" } } output "component-1" { @@ -42,7 +46,11 @@ output "component-1" { # Component: component-2 module "component-2" { - source = "{{ .PWD }}/testdata/modules/application" + source = "{{ .PWD }}/testdata/modules/application" + component_version = "test" + environment = "test" + site = "test-1" + tags = { "Component" : "component-2", "Site" : "test-1", "Version" : "test" } } output "component-2" { diff --git a/internal/cmd/testdata/cases/generate/aws-multisite/expected/main/test-1/component-1/main.tf b/internal/cmd/testdata/cases/generate/aws-multisite/expected/main/test-1/component-1/main.tf index 0ae4f9c7..11a2d990 100755 --- a/internal/cmd/testdata/cases/generate/aws-multisite/expected/main/test-1/component-1/main.tf +++ b/internal/cmd/testdata/cases/generate/aws-multisite/expected/main/test-1/component-1/main.tf @@ -15,7 +15,11 @@ terraform { # Resources # Component: component-1 module "component-1" { - source = "{{ .PWD }}/testdata/modules/application" + source = "{{ .PWD }}/testdata/modules/application" + component_version = "test" + environment = "test" + site = "test-1" + tags = { "Component" : "component-1", "Site" : "test-1", "Version" : "test" } } output "component-1" { diff --git a/internal/cmd/testdata/cases/generate/aws-multisite/expected/main/test-2/component-1/main.tf b/internal/cmd/testdata/cases/generate/aws-multisite/expected/main/test-2/component-1/main.tf index 3be5b9bf..fab87b02 100755 --- a/internal/cmd/testdata/cases/generate/aws-multisite/expected/main/test-2/component-1/main.tf +++ b/internal/cmd/testdata/cases/generate/aws-multisite/expected/main/test-2/component-1/main.tf @@ -15,7 +15,11 @@ terraform { # Resources # Component: component-1 module "component-1" { - source = "{{ .PWD }}/testdata/modules/application" + source = "{{ .PWD }}/testdata/modules/application" + component_version = "test" + environment = "test" + site = "test-2" + tags = { "Component" : "component-1", "Site" : "test-2", "Version" : "test" } } output "component-1" { diff --git a/internal/cmd/testdata/modules/application/main.tf b/internal/cmd/testdata/modules/application/main.tf index 812824ac..be908de4 100644 --- a/internal/cmd/testdata/modules/application/main.tf +++ b/internal/cmd/testdata/modules/application/main.tf @@ -16,6 +16,22 @@ variable "variables" { }) } +variable "component_version" { + type = string +} + +variable "environment" { + type = string +} + +variable "site" { + type = string +} + +variable "tags" { + type = map(string) +} + data "http" "example" { count = var.variables.fail == true ? 1 : 0 url = "fails" diff --git a/internal/cmd/validate.go b/internal/cmd/validate.go index af87d1a2..65ad1d90 100644 --- a/internal/cmd/validate.go +++ b/internal/cmd/validate.go @@ -75,11 +75,14 @@ func validateFunc(cmd *cobra.Command, _ []string) error { return err } - r := runner.NewGraphRunner( - batcher.NaiveBatchFunc(), - hash.NewMemoryMapHandler(), - commonFlags.workers, - ) + b, err := batcher.Factory(cfg) + if err != nil { + return err + } + + h := hash.Factory(cfg) + + r := runner.NewGraphRunner(b, h, commonFlags.workers) return r.TerraformValidate(ctx, dg, &runner.ValidateOptions{ BufferLogs: validateFlags.bufferLogs, diff --git a/internal/config/config.go b/internal/config/config.go index 907331f3..dbf1a999 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -42,6 +42,7 @@ type MachComposer struct { Plugins map[string]MachPluginConfig `yaml:"plugins"` Cloud MachComposerCloud `yaml:"cloud"` Deployment Deployment `yaml:"deployment"` + Batcher Batcher `yaml:"batcher"` } func (mc *MachComposer) CloudEnabled() bool { @@ -70,3 +71,8 @@ type MachPluginConfig struct { Version string `yaml:"version"` Replace string `yaml:"replace"` } + +type Batcher struct { + Type string `yaml:"type"` + SiteOrder []string `yaml:"site_order,omitempty"` +} diff --git a/internal/config/global.go b/internal/config/global.go index 04bd95fb..c3c86aed 100644 --- a/internal/config/global.go +++ b/internal/config/global.go @@ -5,7 +5,9 @@ import ( "github.com/mach-composer/mach-composer-cli/internal/cli" "github.com/mach-composer/mach-composer-cli/internal/config/variable" "github.com/mach-composer/mach-composer-cli/internal/utils" + "github.com/rs/zerolog/log" "gopkg.in/yaml.v3" + "slices" ) type GlobalConfig struct { @@ -19,8 +21,9 @@ type GlobalConfig struct { } type TerraformConfig struct { - Providers map[string]string `yaml:"providers"` - RemoteState map[string]any `yaml:"remote_state"` + Providers map[string]string `yaml:"providers"` + ProviderConfigs ProviderConfigs `yaml:"provider_configs"` + RemoteState map[string]any `yaml:"remote_state"` } func parseGlobalNode(cfg *MachConfig, globalNode *yaml.Node) error { @@ -56,6 +59,20 @@ func parseGlobalNode(cfg *MachConfig, globalNode *yaml.Node) error { } } + if cfg.Global.TerraformConfig != nil { + pluginNames := cfg.Plugins.AllNames() + providerNames, err := cfg.Global.TerraformConfig.ProviderConfigs.Names() + if err != nil { + return fmt.Errorf("failed to get provider names: %w", err) + } + + for _, providerName := range providerNames { + if slices.Contains(pluginNames, providerName) { + log.Warn().Str("plugin", providerName).Str("name", providerName).Msgf("plugin exists with the same name as a provider: %s, this might cause duplicate providers in the generated Terraform code", providerName) + } + } + } + if node, ok := nodes["terraform_config"]; ok { children := MapYamlNodes(node.Content) diff --git a/internal/config/provider.go b/internal/config/provider.go new file mode 100644 index 00000000..8675bd1f --- /dev/null +++ b/internal/config/provider.go @@ -0,0 +1,18 @@ +package config + +type ProviderConfigs []ProviderConfig + +func (pc *ProviderConfigs) Names() ([]string, error) { + var names []string + for _, config := range *pc { + names = append(names, config.Name) + } + return names, nil +} + +type ProviderConfig struct { + Name string `mapstructure:"name"` + Source string `mapstructure:"source"` + Version string `mapstructure:"version"` + Configuration map[string]any `mapstructure:"configuration,omitempty"` +} diff --git a/internal/config/schemas/schema-1.yaml b/internal/config/schemas/schema-1.yaml index 0c0ece53..4a120522 100644 --- a/internal/config/schemas/schema-1.yaml +++ b/internal/config/schemas/schema-1.yaml @@ -37,6 +37,32 @@ definitions: type: string cloud: $ref: "#/definitions/MachComposerCloud" + batcher: + type: object + additionalProperties: true + properties: + type: + description: | + Sets the way the component runs are batched. + + - The default is simple, which means all sites and components will be considered as a single workload. + - The site batching will run sites sequentially, which means that all components of a site will be run + as a separate workload before moving to the next site. The order of execution is determined by the + order in which the sites are defined in the configuration. + type: string + enum: + - simple + - site + default: "simple" + site_order: + description: | + The order in which the sites are processed when using the site batching. This is useful for cases where + you want to process sites in a specific order. If left empty, the sites will be processed in the order + they are defined in the configuration. + type: array + items: + type: string + deployment: $ref: "#/definitions/MachComposerDeployment" plugins: @@ -75,7 +101,6 @@ definitions: required: - environment - terraform_config - - cloud properties: environment: type: string @@ -101,6 +126,11 @@ definitions: patternProperties: "^[a-zA-Z-]+$": type: string + provider_configs: + description: "A list of provider configurations to generate." + type: array + items: + $ref: "#/definitions/ProviderConfig" remote_state: allOf: - type: object @@ -119,6 +149,22 @@ definitions: - terraform_cloud - $ref: "#/definitions/RemoteState" + ProviderConfig: + type: object + description: Provider configuration. + required: + - name + properties: + name: + type: string + source: + type: string + version: + type: string + configuration: + type: object + additionalProperties: true + RemoteState: type: object properties: { } diff --git a/internal/config/site.go b/internal/config/site.go index f839f348..ca77c7c3 100644 --- a/internal/config/site.go +++ b/internal/config/site.go @@ -14,6 +14,14 @@ import ( type SiteConfigs []SiteConfig +func (s *SiteConfigs) Identifiers() []string { + identifiers := make([]string, len(*s)) + for i, site := range *s { + identifiers[i] = site.Identifier + } + return identifiers +} + func (s *SiteConfigs) Get(identifier string) (*SiteConfig, error) { for _, site := range *s { if site.Identifier == identifier { diff --git a/internal/generator/component.go b/internal/generator/component.go index b008acf8..123c436f 100644 --- a/internal/generator/component.go +++ b/internal/generator/component.go @@ -19,6 +19,7 @@ type componentContext struct { ComponentHash string ComponentVariables string ComponentSecrets string + ComponentTags map[string]string SiteName string Environment string SourceType string @@ -76,18 +77,22 @@ func renderSiteComponent(ctx context.Context, cfg *config.MachConfig, n *graph.S } // renderSiteComponentTerraformConfig uses templates/terraform.tmpl to generate a terraform snippet for each component -func renderSiteComponentTerraformConfig(cfg *config.MachConfig, n graph.Node) (string, error) { +func renderSiteComponentTerraformConfig(cfg *config.MachConfig, n *graph.SiteComponent) (string, error) { tpl, err := templates.ReadFile("templates/terraform.tmpl") if err != nil { return "", err } - site := n.(*graph.SiteComponent).SiteConfig - siteComponent := n.(*graph.SiteComponent).SiteComponentConfig - var providers []string - for _, plugin := range cfg.Plugins.Names(siteComponent.Definition.Integrations...) { - content, err := plugin.RenderTerraformProviders(site.Identifier) + + genericProviderRequirements, err := renderRequirements(cfg) + if err != nil { + return "", fmt.Errorf("failed to render generic providers: %w", err) + } + providers = append(providers, genericProviderRequirements...) + + for _, plugin := range cfg.Plugins.Names(n.SiteComponentConfig.Definition.Integrations...) { + content, err := plugin.RenderTerraformProviders(n.SiteConfig.Identifier) if err != nil { return "", fmt.Errorf("plugin %s failed to render providers: %w", plugin.Name, err) } @@ -98,7 +103,7 @@ func renderSiteComponentTerraformConfig(cfg *config.MachConfig, n graph.Node) (s s, ok := cfg.StateRepository.Get(n.Identifier()) if !ok { - return "", fmt.Errorf("state repository does not have a backend for site %s", site.Identifier) + return "", fmt.Errorf("state repository does not have a backend for site %s", n.SiteConfig.Identifier) } bc, err := s.Backend() @@ -113,7 +118,7 @@ func renderSiteComponentTerraformConfig(cfg *config.MachConfig, n graph.Node) (s }{ Providers: providers, BackendConfig: bc, - IncludeSOPS: cfg.Variables.HasEncrypted(site.Identifier), + IncludeSOPS: cfg.Variables.HasEncrypted(n.SiteConfig.Identifier), } return utils.RenderGoTemplate(string(tpl), templateContext) } @@ -125,7 +130,11 @@ func renderSiteComponentResources(cfg *config.MachConfig, n *graph.SiteComponent return "", err } - var resources []string + resources, err := renderProviders(cfg) + if err != nil { + return "", fmt.Errorf("failed to render generic providers: %w", err) + } + for _, plugin := range cfg.Plugins.Names(n.SiteComponentConfig.Definition.Integrations...) { content, err := plugin.RenderTerraformResources(n.SiteConfig.Identifier) if err != nil { @@ -158,11 +167,17 @@ func renderComponentModule(_ context.Context, cfg *config.MachConfig, n *graph.S SiteName: n.SiteConfig.Identifier, Environment: cfg.Global.Environment, Version: n.SiteComponentConfig.Definition.Version, - SourceType: string(sourceType), - PluginResources: []string{}, - PluginVariables: []string{}, - PluginDependsOn: []string{}, - PluginProviders: []string{}, + //TODO: allow setting additional tags via config + ComponentTags: map[string]string{ + "Site": n.SiteConfig.Identifier, + "Component": n.SiteComponentConfig.Name, + "Version": n.SiteComponentConfig.Definition.Version, + }, + SourceType: string(sourceType), + PluginResources: []string{}, + PluginVariables: []string{}, + PluginDependsOn: []string{}, + PluginProviders: []string{}, } for _, plugin := range cfg.Plugins.Names(n.SiteComponentConfig.Definition.Integrations...) { diff --git a/internal/generator/providers.go b/internal/generator/providers.go new file mode 100644 index 00000000..11d108a7 --- /dev/null +++ b/internal/generator/providers.go @@ -0,0 +1,52 @@ +package generator + +import ( + "embed" + "fmt" + "github.com/mach-composer/mach-composer-cli/internal/config" + "github.com/mach-composer/mach-composer-cli/internal/utils" +) + +//go:embed templates/provider.tmpl +var providerTmpl embed.FS + +type RenderProviderConfiguration struct { + Name string + Configuration map[string]any `yaml:"configuration,omitempty"` +} + +func renderProviders(cfg *config.MachConfig) ([]string, error) { + tpl, err := providerTmpl.ReadFile("templates/provider.tmpl") + if err != nil { + return nil, err + } + + siteConfigs := cfg.Global.TerraformConfig.ProviderConfigs + + var renderedProviders []string + for _, provider := range siteConfigs { + // Ensure all values in provider.Configuration are not complex types + for key, value := range provider.Configuration { + if _, ok := value.(map[any]any); ok { + return nil, fmt.Errorf("provider configuration for '%s' contains map value for key '%s'", provider.Name, key) + } + if _, ok := value.([]any); ok { + return nil, fmt.Errorf("provider configuration for '%s' contains slice value for key '%s'", provider.Name, key) + } + } + + data := RenderProviderConfiguration{ + Name: provider.Name, + Configuration: provider.Configuration, + } + + renderedProvider, err := utils.RenderGoTemplate(string(tpl), data) + if err != nil { + return nil, err + } + + renderedProviders = append(renderedProviders, renderedProvider) + } + + return renderedProviders, nil +} diff --git a/internal/generator/requirements.go b/internal/generator/requirements.go new file mode 100644 index 00000000..78532ed2 --- /dev/null +++ b/internal/generator/requirements.go @@ -0,0 +1,42 @@ +package generator + +import ( + "embed" + "github.com/mach-composer/mach-composer-cli/internal/config" + "github.com/mach-composer/mach-composer-plugin-sdk/v2/helpers" +) + +//go:embed templates/requirement.tmpl +var requirementsTmpl embed.FS + +type RenderProviderRequirements struct { + Name string + Source string + Version string +} + +func renderRequirements(cfg *config.MachConfig) ([]string, error) { + tpl, err := requirementsTmpl.ReadFile("templates/requirement.tmpl") + if err != nil { + return nil, err + } + + siteConfigs := cfg.Global.TerraformConfig.ProviderConfigs + + var renderedRequirements []string + for _, provider := range siteConfigs { + providerData := RenderProviderRequirements{ + Name: provider.Name, + Source: provider.Source, + Version: provider.Version, + } + renderedRequirement, err := helpers.RenderGoTemplate(string(tpl), providerData) + if err != nil { + return nil, err + } + + renderedRequirements = append(renderedRequirements, renderedRequirement) + } + + return renderedRequirements, nil +} diff --git a/internal/generator/site.go b/internal/generator/site.go index ba482195..08ef8e7c 100644 --- a/internal/generator/site.go +++ b/internal/generator/site.go @@ -76,6 +76,12 @@ func renderSiteTerraformConfig(cfg *config.MachConfig, n *graph.Site) (string, e } } + requirements, err := renderRequirements(cfg) + if err != nil { + return "", fmt.Errorf("failed to render requirements: %w", err) + } + providers = append(providers, requirements...) + s, ok := cfg.StateRepository.Get(n.Identifier()) if !ok { return "", fmt.Errorf("state repository does not have a backend for %s", n.Identifier()) @@ -116,5 +122,11 @@ func renderSiteResources(cfg *config.MachConfig, n *graph.Site) (string, error) } } + providers, err := renderProviders(cfg) + if err != nil { + return "", fmt.Errorf("failed to render providers: %w", err) + } + resources = append(resources, providers...) + return utils.RenderGoTemplate(string(tpl), resources) } diff --git a/internal/generator/templates/provider.tmpl b/internal/generator/templates/provider.tmpl new file mode 100644 index 00000000..92c0f4c5 --- /dev/null +++ b/internal/generator/templates/provider.tmpl @@ -0,0 +1,21 @@ +provider "{{ .Name }}" { +{{- range $key, $value := .Configuration }} + {{ if isMap $value -}} + {{ $key }} { + {{- range $key2, $value2 := $value }} + {{ if isMap $value2 -}} + {{ $key2 }} { + {{- range $key3, $value3 := $value2 }} + {{ $key3 }} = {{ tfValue $value3 }} + {{- end }} + } + {{- else }} + {{ $key2 }} = {{ tfValue $value2 }} + {{- end }} + {{- end }} + } + {{- else }} + {{ $key }} = {{ tfValue $value }} + {{- end }} +{{- end }} +} diff --git a/internal/generator/templates/requirement.tmpl b/internal/generator/templates/requirement.tmpl new file mode 100644 index 00000000..963517f8 --- /dev/null +++ b/internal/generator/templates/requirement.tmpl @@ -0,0 +1,9 @@ +{{ .Name }} = { +{{- if .Source }} + source = "{{ .Source }}" +{{- end }} + +{{- if .Version }} + version = "{{ .Version }}" +{{- end }} +} diff --git a/internal/generator/templates/site_component.tmpl b/internal/generator/templates/site_component.tmpl index e38451fe..cc3bac0d 100644 --- a/internal/generator/templates/site_component.tmpl +++ b/internal/generator/templates/site_component.tmpl @@ -10,7 +10,6 @@ module "{{ .ComponentName }}" { version = "{{ .Version }}" {{ end }} - {{ if .ComponentVariables }} {{ .ComponentVariables }} {{ end }} @@ -24,6 +23,11 @@ module "{{ .ComponentName }}" { environment = "{{ .Environment }}" site = "{{ .SiteName }}" tags = local.tags +{{ else }} + component_version = "{{ .ComponentVersion }}" + environment = "{{ .Environment }}" + site = "{{ .SiteName }}" + tags = {{ tfValue .ComponentTags }} {{ end }} {{ range $item := .PluginVariables }} diff --git a/internal/graph/graph.go b/internal/graph/graph.go index 49a070af..c8b40a49 100644 --- a/internal/graph/graph.go +++ b/internal/graph/graph.go @@ -10,8 +10,6 @@ type Graph struct { StartNode Node } -type Vertices []Node - // Vertices returns all the vertex that are contained in the graph func (g *Graph) Vertices() Vertices { var vertices Vertices @@ -47,3 +45,63 @@ func (g *Graph) Routes(source, target string) ([]Path, error) { return routes, nil } + +func (g *Graph) ExtractSubGraph(root Node) (*Graph, error) { + // Create a new graph to hold the pruned subgraph + newGraph := &Graph{ + Graph: graph.New(func(n Node) string { return n.Path() }, graph.Directed(), graph.Tree(), graph.PreventCycles()), + StartNode: root, + } + if err := newGraph.AddVertex(root); err != nil { + return nil, err + } + + var addNodeAndChildren func(parent Node) error + addNodeAndChildren = func(parent Node) error { + children, err := parent.Children() + if err != nil { + return err + } + for _, child := range children { + if err := newGraph.AddVertex(child); err != nil { + return err + } + if err := newGraph.AddEdge(parent.Path(), child.Path()); err != nil { + return err + } + if err := addNodeAndChildren(child); err != nil { + return err + } + } + return nil + } + + if err := addNodeAndChildren(root); err != nil { + return nil, err + } + + return newGraph, nil +} + +type Vertices []Node + +func (v Vertices) Filter(t Type) Vertices { + var nv Vertices + + for _, vx := range v { + if vx.Type() == t { + nv = append(nv, vx) + } + } + return nv +} + +func (v Vertices) FilterByIdentifier(identifier string) Node { + for _, vx := range v { + if vx.Identifier() == identifier { + return vx + } + } + + return nil +} diff --git a/internal/graph/mocks.go b/internal/graph/mocks.go index f513e309..6cb8eda3 100644 --- a/internal/graph/mocks.go +++ b/internal/graph/mocks.go @@ -70,6 +70,11 @@ func (n *NodeMock) Parents() ([]Node, error) { return args.Get(0).([]Node), args.Error(1) } +func (n *NodeMock) Children() ([]Node, error) { + args := n.Called() + return args.Get(0).([]Node), args.Error(1) +} + func (n *NodeMock) Independent() bool { //TODO implement me panic("implement me") diff --git a/internal/graph/node.go b/internal/graph/node.go index 1f97c523..a6a7958d 100644 --- a/internal/graph/node.go +++ b/internal/graph/node.go @@ -31,6 +31,9 @@ type Node interface { //Parents returns the direct parents of the node Parents() ([]Node, error) + //Children returns the direct children of the node. This is used to determine the nodes that are dependent on this node. + Children() ([]Node, error) + //Independent returns true if the node can be deployed independently, false otherwise Independent() bool @@ -125,6 +128,26 @@ func (n *baseNode) Parents() ([]Node, error) { return parents, nil } +func (n *baseNode) Children() ([]Node, error) { + pm, err := n.graph.AdjacencyMap() + if err != nil { + return nil, err + } + + eg := pm[n.Path()] + + var children []Node + for _, pathElement := range eg { + p, err := n.graph.Vertex(pathElement.Target) + if err != nil { + return nil, err + } + children = append(children, p) + } + + return children, nil +} + func (n *baseNode) Independent() bool { // Projects and sites are always independent elements if n.typ == ProjectType || n.typ == SiteType { diff --git a/internal/plugins/repository.go b/internal/plugins/repository.go index a4e3885c..13c9aa13 100644 --- a/internal/plugins/repository.go +++ b/internal/plugins/repository.go @@ -55,6 +55,14 @@ func (p *PluginRepository) All() []*PluginHandler { return result } +func (p *PluginRepository) AllNames() []string { + result := make([]string, len(p.handlers)) + for i, key := range pie.Sort(pie.Keys(p.handlers)) { + result[i] = key + } + return result +} + func (p *PluginRepository) Names(names ...string) []PluginHandler { var result []PluginHandler diff --git a/internal/runner/graph.go b/internal/runner/graph.go index 05668e27..b8085b86 100644 --- a/internal/runner/graph.go +++ b/internal/runner/graph.go @@ -123,7 +123,10 @@ func (gr *GraphRunner) run(ctx context.Context, g *graph.Graph, f executorFunc, return err } - batches := gr.batch(g) + batches, err := gr.batch(g) + if err != nil { + return fmt.Errorf("failed to create batches: %w", err) + } keys := maps.Keys(batches) sort.Ints(keys) diff --git a/internal/runner/graph_test.go b/internal/runner/graph_test.go index 62ea6361..f2e88c3b 100644 --- a/internal/runner/graph_test.go +++ b/internal/runner/graph_test.go @@ -5,6 +5,7 @@ import ( "errors" "github.com/mach-composer/mach-composer-cli/internal/batcher" "github.com/mach-composer/mach-composer-cli/internal/cli" + "github.com/mach-composer/mach-composer-cli/internal/config" internalgraph "github.com/mach-composer/mach-composer-cli/internal/graph" "github.com/mach-composer/mach-composer-cli/internal/hash" "github.com/stretchr/testify/assert" @@ -63,7 +64,13 @@ func TestGraphRunnerMultipleLevels(t *testing.T) { hash.Entry{Identifier: "site-1", Hash: "site-1"}, hash.Entry{Identifier: "component-1", Hash: "component-1"}, ) - runner.batch = batcher.NaiveBatchFunc() + runner.batch, _ = batcher.Factory(&config.MachConfig{ + MachComposer: config.MachComposer{ + Batcher: config.Batcher{ + Type: "simple", + }, + }, + }) var called []string @@ -125,7 +132,13 @@ func TestGraphRunnerError(t *testing.T) { runner := GraphRunner{workers: 1} runner.hash = hash.NewMemoryMapHandler() - runner.batch = batcher.NaiveBatchFunc() + runner.batch, _ = batcher.Factory(&config.MachConfig{ + MachComposer: config.MachComposer{ + Batcher: config.Batcher{ + Type: "simple", + }, + }, + }) err := runner.run(context.Background(), graph, func(ctx context.Context, node internalgraph.Node) error { if node.Identifier() == "component-2" { diff --git a/internal/utils/template.go b/internal/utils/template.go index 3f975683..612a8123 100644 --- a/internal/utils/template.go +++ b/internal/utils/template.go @@ -2,11 +2,31 @@ package utils import ( "bytes" + "encoding/json" + "fmt" "text/template" ) func RenderGoTemplate(t string, data any) (string, error) { - tpl, err := template.New("template").Parse(t) + funcMap := template.FuncMap{ + "tfValue": func(v any) string { + switch val := v.(type) { + case string: + return `"` + val + `"` + case int, int64, float64, bool: + return fmt.Sprintf("%v", val) + default: + b, _ := json.Marshal(val) + return string(b) + } + }, + "isMap": func(v any) bool { + _, ok := v.(map[string]any) + return ok + }, + } + + tpl, err := template.New("template").Funcs(funcMap).Parse(t) if err != nil { return "", err }