From e1841a84286526762705bf67dcf4176a349882d2 Mon Sep 17 00:00:00 2001 From: Ousama Ben Younes Date: Sat, 11 Jul 2026 08:23:19 +0000 Subject: [PATCH 1/2] 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 1c08e1501e..78eb3bed24 100644 --- a/pkg/compose/create.go +++ b/pkg/compose/create.go @@ -1433,7 +1433,7 @@ func (s *composeService) createNetwork(ctx context.Context, n *types.NetworkConf 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{} } @@ -1441,6 +1441,10 @@ func (s *composeService) createNetwork(ctx context.Context, n *types.NetworkConf 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 5480b006e9..b539f04818 100644 --- a/pkg/compose/executor_test.go +++ b/pkg/compose/executor_test.go @@ -24,6 +24,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" @@ -39,6 +40,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) @@ -84,6 +94,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 d35b2c80f6ec2dedb26d500bb9820c13af2d2774 Mon Sep 17 00:00:00 2001 From: Ousama Ben Younes Date: Thu, 16 Jul 2026 16:13:34 +0000 Subject: [PATCH 2/2] Address IPAM review feedback Signed-off-by: Ousama Ben Younes --- pkg/compose/create.go | 27 +++---------------- pkg/compose/executor_test.go | 52 +++++++++++++++++------------------- 2 files changed, 29 insertions(+), 50 deletions(-) diff --git a/pkg/compose/create.go b/pkg/compose/create.go index 78eb3bed24..40bd0586e7 100644 --- a/pkg/compose/create.go +++ b/pkg/compose/create.go @@ -1403,7 +1403,7 @@ func buildImageOptions(image *types.ServiceVolumeImage) *mount.ImageOptions { // name-matched networks is decided by the reconciler from the observed state. func (s *composeService) createNetwork(ctx context.Context, n *types.NetworkConfig) error { 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) @@ -1413,8 +1413,9 @@ func (s *composeService) createNetwork(ctx context.Context, n *types.NetworkConf 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) @@ -1433,26 +1434,6 @@ func (s *composeService) createNetwork(ctx context.Context, n *types.NetworkConf 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 b539f04818..2da523e3de 100644 --- a/pkg/compose/executor_test.go +++ b/pkg/compose/executor_test.go @@ -41,12 +41,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 = "ipam-option" + ipamOptionsValue = "enabled" ) func newTestService(t *testing.T) (*composeService, *mocks.MockAPIClient) { @@ -63,34 +65,34 @@ 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}, } // createNetwork issues a plain NetworkCreate (divergence/reuse decisions are // made by the reconciler from the observed state, not here). - 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) } @@ -98,7 +100,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, @@ -106,34 +108,30 @@ 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()). - 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) }