From d3605a3471fc11286f7be827ffab03d6069459e5 Mon Sep 17 00:00:00 2001 From: Ousama Ben Younes Date: Sat, 11 Jul 2026 08:23:19 +0000 Subject: [PATCH 1/3] Pass IPAM options when creating networks Signed-off-by: Ousama Ben Younes --- pkg/compose/create.go | 6 +++- pkg/compose/executor_test.go | 53 ++++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/pkg/compose/create.go b/pkg/compose/create.go index 6d15f28f28d..635bc55bc1f 100644 --- a/pkg/compose/create.go +++ b/pkg/compose/create.go @@ -1436,7 +1436,7 @@ func (s *composeService) resolveOrCreateNetwork(ctx context.Context, project *ty EnableIPv4: n.EnableIPv4, } - if n.Ipam.Driver != "" || len(n.Ipam.Config) > 0 { + if n.Ipam.Driver != "" || len(n.Ipam.Config) > 0 || len(n.Ipam.Options) > 0 { createOpts.IPAM = &network.IPAM{} } @@ -1444,6 +1444,10 @@ func (s *composeService) resolveOrCreateNetwork(ctx context.Context, project *ty createOpts.IPAM.Driver = n.Ipam.Driver } + if len(n.Ipam.Options) > 0 { + createOpts.IPAM.Options = n.Ipam.Options + } + for _, ipamConfig := range n.Ipam.Config { c, err := parseIPAMPool(ipamConfig) if err != nil { diff --git a/pkg/compose/executor_test.go b/pkg/compose/executor_test.go index 5f4ab88049c..c65b58c670c 100644 --- a/pkg/compose/executor_test.go +++ b/pkg/compose/executor_test.go @@ -23,6 +23,7 @@ import ( "github.com/compose-spec/compose-go/v2/types" "github.com/moby/moby/api/types/container" + "github.com/moby/moby/api/types/network" "github.com/moby/moby/client" "go.uber.org/mock/gomock" "gotest.tools/v3/assert" @@ -38,6 +39,15 @@ func (noopEventProcessor) Start(_ context.Context, _ string) {} func (noopEventProcessor) On(_ ...api.Resource) {} func (noopEventProcessor) Done(_ string, _ bool) {} +const ( + ipamOptionsProjectName = "test" + ipamOptionsNetworkKey = "default" + ipamOptionsNetworkName = "test_default" + ipamOptionsKey = "test" + ipamOptionsValue = "1" + ipamOptionsCreatedNetID = "net1" +) + func newTestService(t *testing.T) (*composeService, *mocks.MockAPIClient) { t.Helper() mockCtrl := gomock.NewController(t) @@ -86,6 +96,49 @@ func TestExecutePlanCreateNetwork(t *testing.T) { assert.NilError(t, err) } +func TestExecutePlanCreateNetworkWithIPAMOptions(t *testing.T) { + svc, apiClient := newTestService(t) + + nw := types.NetworkConfig{ + Name: ipamOptionsNetworkName, + Ipam: types.IPAMConfig{ + Options: types.Options{ + ipamOptionsKey: ipamOptionsValue, + }, + }, + } + project := &types.Project{ + Name: ipamOptionsProjectName, + Networks: types.Networks{ipamOptionsNetworkKey: nw}, + } + + apiClient.EXPECT().NetworkInspect(gomock.Any(), ipamOptionsNetworkName, gomock.Any()). + Return(client.NetworkInspectResult{}, notFoundError{}) + apiClient.EXPECT().NetworkList(gomock.Any(), gomock.Any()). + Return(client.NetworkListResult{}, nil) + apiClient.EXPECT().NetworkCreate(gomock.Any(), ipamOptionsNetworkName, gomock.Any()). + DoAndReturn(func(_ context.Context, _ string, opts client.NetworkCreateOptions) (client.NetworkCreateResult, error) { + assert.DeepEqual(t, opts.IPAM, &network.IPAM{ + Options: map[string]string{ + ipamOptionsKey: ipamOptionsValue, + }, + }) + return client.NetworkCreateResult{ID: ipamOptionsCreatedNetID}, nil + }) + + plan := &Plan{} + plan.addNode(Operation{ + Type: OpCreateNetwork, + ResourceID: "network:" + ipamOptionsNetworkKey, + Cause: "not found", + Name: nw.Name, + Network: &nw, + }, "") + + err := svc.executePlan(t.Context(), project, emptyObservedState(ipamOptionsProjectName), plan) + assert.NilError(t, err) +} + func TestExecutePlanStopRemoveContainer(t *testing.T) { svc, apiClient := newTestService(t) From 078eacefcbd88add410de43d36c483bf487697e1 Mon Sep 17 00:00:00 2001 From: Ousama Ben Younes Date: Thu, 16 Jul 2026 16:13:34 +0000 Subject: [PATCH 2/3] Address IPAM review feedback Signed-off-by: Ousama Ben Younes --- pkg/compose/create.go | 27 +++---------------- pkg/compose/executor_test.go | 52 +++++++++++++++++++----------------- 2 files changed, 31 insertions(+), 48 deletions(-) diff --git a/pkg/compose/create.go b/pkg/compose/create.go index 635bc55bc1f..55c6cc4e436 100644 --- a/pkg/compose/create.go +++ b/pkg/compose/create.go @@ -1406,7 +1406,7 @@ func (s *composeService) resolveOrCreateNetwork(ctx context.Context, project *ty } var ipam *network.IPAM - if n.Ipam.Config != nil { + if n.Ipam.Driver != "" || len(n.Ipam.Config) > 0 || len(n.Ipam.Options) > 0 { var config []network.IPAMConfig for _, pool := range n.Ipam.Config { c, err := parseIPAMPool(pool) @@ -1416,8 +1416,9 @@ func (s *composeService) resolveOrCreateNetwork(ctx context.Context, project *ty config = append(config, c) } ipam = &network.IPAM{ - Driver: n.Ipam.Driver, - Config: config, + Driver: n.Ipam.Driver, + Config: config, + Options: n.Ipam.Options, } } hash, err := NetworkHash(n) @@ -1436,26 +1437,6 @@ func (s *composeService) resolveOrCreateNetwork(ctx context.Context, project *ty EnableIPv4: n.EnableIPv4, } - if n.Ipam.Driver != "" || len(n.Ipam.Config) > 0 || len(n.Ipam.Options) > 0 { - createOpts.IPAM = &network.IPAM{} - } - - if n.Ipam.Driver != "" { - createOpts.IPAM.Driver = n.Ipam.Driver - } - - if len(n.Ipam.Options) > 0 { - createOpts.IPAM.Options = n.Ipam.Options - } - - for _, ipamConfig := range n.Ipam.Config { - c, err := parseIPAMPool(ipamConfig) - if err != nil { - return "", err - } - createOpts.IPAM.Config = append(createOpts.IPAM.Config, c) - } - networkEventName := fmt.Sprintf("Network %s", n.Name) s.events.On(creatingEvent(networkEventName)) diff --git a/pkg/compose/executor_test.go b/pkg/compose/executor_test.go index c65b58c670c..40961d5c866 100644 --- a/pkg/compose/executor_test.go +++ b/pkg/compose/executor_test.go @@ -40,12 +40,14 @@ func (noopEventProcessor) On(_ ...api.Resource) {} func (noopEventProcessor) Done(_ string, _ bool) {} const ( - ipamOptionsProjectName = "test" - ipamOptionsNetworkKey = "default" - ipamOptionsNetworkName = "test_default" - ipamOptionsKey = "test" - ipamOptionsValue = "1" - ipamOptionsCreatedNetID = "net1" + executorTestProjectName = "test" + executorTestNetworkKey = "default" + executorTestNetworkName = "test_default" + executorTestNetworkResource = "network:" + executorTestNetworkKey + executorTestNotFoundCause = "not found" + executorTestCreatedNetworkID = "net1" + ipamOptionsKey = "test" + ipamOptionsValue = "1" ) func newTestService(t *testing.T) (*composeService, *mocks.MockAPIClient) { @@ -62,37 +64,37 @@ func newTestService(t *testing.T) (*composeService, *mocks.MockAPIClient) { func TestExecutePlanEmpty(t *testing.T) { svc, _ := newTestService(t) - err := svc.executePlan(t.Context(), &types.Project{Name: "test"}, emptyObservedState("test"), &Plan{}) + err := svc.executePlan(t.Context(), &types.Project{Name: executorTestProjectName}, emptyObservedState(executorTestProjectName), &Plan{}) assert.NilError(t, err) } func TestExecutePlanCreateNetwork(t *testing.T) { svc, apiClient := newTestService(t) - nw := types.NetworkConfig{Name: "test_default"} + nw := types.NetworkConfig{Name: executorTestNetworkName} project := &types.Project{ - Name: "test", - Networks: types.Networks{"default": nw}, + Name: executorTestProjectName, + Networks: types.Networks{executorTestNetworkKey: nw}, } // ensureNetwork: inspect → not found, list → empty, create - apiClient.EXPECT().NetworkInspect(gomock.Any(), "test_default", gomock.Any()). + apiClient.EXPECT().NetworkInspect(gomock.Any(), executorTestNetworkName, gomock.Any()). Return(client.NetworkInspectResult{}, notFoundError{}) apiClient.EXPECT().NetworkList(gomock.Any(), gomock.Any()). Return(client.NetworkListResult{}, nil) - apiClient.EXPECT().NetworkCreate(gomock.Any(), "test_default", gomock.Any()). - Return(client.NetworkCreateResult{ID: "net1"}, nil) + apiClient.EXPECT().NetworkCreate(gomock.Any(), executorTestNetworkName, gomock.Any()). + Return(client.NetworkCreateResult{ID: executorTestCreatedNetworkID}, nil) plan := &Plan{} plan.addNode(Operation{ Type: OpCreateNetwork, - ResourceID: "network:default", - Cause: "not found", + ResourceID: executorTestNetworkResource, + Cause: executorTestNotFoundCause, Name: nw.Name, Network: &nw, }, "") - err := svc.executePlan(t.Context(), project, emptyObservedState("test"), plan) + err := svc.executePlan(t.Context(), project, emptyObservedState(executorTestProjectName), plan) assert.NilError(t, err) } @@ -100,7 +102,7 @@ func TestExecutePlanCreateNetworkWithIPAMOptions(t *testing.T) { svc, apiClient := newTestService(t) nw := types.NetworkConfig{ - Name: ipamOptionsNetworkName, + Name: executorTestNetworkName, Ipam: types.IPAMConfig{ Options: types.Options{ ipamOptionsKey: ipamOptionsValue, @@ -108,34 +110,34 @@ func TestExecutePlanCreateNetworkWithIPAMOptions(t *testing.T) { }, } project := &types.Project{ - Name: ipamOptionsProjectName, - Networks: types.Networks{ipamOptionsNetworkKey: nw}, + Name: executorTestProjectName, + Networks: types.Networks{executorTestNetworkKey: nw}, } - apiClient.EXPECT().NetworkInspect(gomock.Any(), ipamOptionsNetworkName, gomock.Any()). + apiClient.EXPECT().NetworkInspect(gomock.Any(), executorTestNetworkName, gomock.Any()). Return(client.NetworkInspectResult{}, notFoundError{}) apiClient.EXPECT().NetworkList(gomock.Any(), gomock.Any()). Return(client.NetworkListResult{}, nil) - apiClient.EXPECT().NetworkCreate(gomock.Any(), ipamOptionsNetworkName, gomock.Any()). + apiClient.EXPECT().NetworkCreate(gomock.Any(), executorTestNetworkName, gomock.Any()). DoAndReturn(func(_ context.Context, _ string, opts client.NetworkCreateOptions) (client.NetworkCreateResult, error) { assert.DeepEqual(t, opts.IPAM, &network.IPAM{ Options: map[string]string{ ipamOptionsKey: ipamOptionsValue, }, }) - return client.NetworkCreateResult{ID: ipamOptionsCreatedNetID}, nil + return client.NetworkCreateResult{ID: executorTestCreatedNetworkID}, nil }) plan := &Plan{} plan.addNode(Operation{ Type: OpCreateNetwork, - ResourceID: "network:" + ipamOptionsNetworkKey, - Cause: "not found", + ResourceID: executorTestNetworkResource, + Cause: executorTestNotFoundCause, Name: nw.Name, Network: &nw, }, "") - err := svc.executePlan(t.Context(), project, emptyObservedState(ipamOptionsProjectName), plan) + err := svc.executePlan(t.Context(), project, emptyObservedState(executorTestProjectName), plan) assert.NilError(t, err) } From 7d1e1078e768492eb61c62811c1f05f4e2cd9e3b Mon Sep 17 00:00:00 2001 From: Ousama Ben Younes Date: Thu, 16 Jul 2026 18:57:06 +0000 Subject: [PATCH 3/3] Use distinct IPAM option test values Signed-off-by: Ousama Ben Younes --- pkg/compose/executor_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/compose/executor_test.go b/pkg/compose/executor_test.go index 40961d5c866..a1cd2e0a75b 100644 --- a/pkg/compose/executor_test.go +++ b/pkg/compose/executor_test.go @@ -46,8 +46,8 @@ const ( executorTestNetworkResource = "network:" + executorTestNetworkKey executorTestNotFoundCause = "not found" executorTestCreatedNetworkID = "net1" - ipamOptionsKey = "test" - ipamOptionsValue = "1" + ipamOptionsKey = "ipam-option" + ipamOptionsValue = "enabled" ) func newTestService(t *testing.T) (*composeService, *mocks.MockAPIClient) {