Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .changes/unreleased/Added-20250627-152055.yaml
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions .changes/unreleased/Added-20250708-114044.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
kind: Added
body: Added option to set site execution order
time: 2025-07-08T11:40:44.556019581+02:00
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
56 changes: 54 additions & 2 deletions internal/batcher/batcher.go
Original file line number Diff line number Diff line change
@@ -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
}
150 changes: 150 additions & 0 deletions internal/batcher/batcher_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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{}
Expand All @@ -24,6 +25,6 @@ func NaiveBatchFunc() BatchFunc {
batches[mx] = append(batches[mx], n)
}

return batches
return batches, nil
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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())
Expand All @@ -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)
Expand All @@ -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())
Expand Down
71 changes: 71 additions & 0 deletions internal/batcher/site_batcher.go
Original file line number Diff line number Diff line change
@@ -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
}
}
Loading
Loading