Browse code

Network restore, don't update config to match state

When a network-create request does not specify any IPAM config, on
daemon restart the network needs to be restored with the previously
allocated subnet and gateway.

Those fields were copied from "ipamInfo" (state from the old network)
into "ipamConfig" (user-requested config).

Avoid that by checking for this situation in the IPAM allocation
function - if no subnet/gateway is specified, and there's a value
in "ipamInfo", use it.

Also eliminate some pointer shenanigans (so now my IDE can find the
assignment to Network.ipamInfo).

Signed-off-by: Rob Murray <rob.murray@docker.com>

Rob Murray authored on 2025/10/08 02:16:29
Showing 2 changed files
... ...
@@ -761,30 +761,6 @@ func (c *Controller) reservePools() {
761 761
 		if !doReplayPoolReserve(n) {
762 762
 			continue
763 763
 		}
764
-		// Construct pseudo configs for the auto IP case
765
-		autoIPv4 := (len(n.ipamV4Config) == 0 || (len(n.ipamV4Config) == 1 && n.ipamV4Config[0].PreferredPool == "")) && len(n.ipamV4Info) > 0
766
-		autoIPv6 := (len(n.ipamV6Config) == 0 || (len(n.ipamV6Config) == 1 && n.ipamV6Config[0].PreferredPool == "")) && len(n.ipamV6Info) > 0
767
-		if n.enableIPv4 && autoIPv4 {
768
-			n.ipamV4Config = []*IpamConf{{PreferredPool: n.ipamV4Info[0].Pool.String()}}
769
-		}
770
-		if n.enableIPv6 && autoIPv6 {
771
-			n.ipamV6Config = []*IpamConf{{PreferredPool: n.ipamV6Info[0].Pool.String()}}
772
-		}
773
-		// Account current network gateways
774
-		if n.enableIPv4 {
775
-			for i, cfg := range n.ipamV4Config {
776
-				if cfg.Gateway == "" && n.ipamV4Info[i].Gateway != nil {
777
-					cfg.Gateway = n.ipamV4Info[i].Gateway.IP.String()
778
-				}
779
-			}
780
-		}
781
-		if n.enableIPv6 {
782
-			for i, cfg := range n.ipamV6Config {
783
-				if cfg.Gateway == "" && n.ipamV6Info[i].Gateway != nil {
784
-					cfg.Gateway = n.ipamV6Info[i].Gateway.IP.String()
785
-				}
786
-			}
787
-		}
788 764
 		// Reserve pools
789 765
 		if err := n.ipamAllocate(); err != nil {
790 766
 			log.G(context.TODO()).Warnf("Failed to allocate ipam pool(s) for network %q (%s): %v", n.Name(), n.ID(), err)
... ...
@@ -1510,9 +1510,14 @@ func (n *Network) ipamAllocate() (retErr error) {
1510 1510
 	}
1511 1511
 
1512 1512
 	if n.enableIPv4 {
1513
-		if err := n.ipamAllocateVersion(4, ipam); err != nil {
1513
+		if len(n.ipamV4Config) == 0 {
1514
+			n.ipamV4Config = []*IpamConf{{}}
1515
+		}
1516
+		info, err := n.ipamAllocateVersion(ipam, false, n.ipamV4Config, n.ipamV4Info, n.skipGwAllocIPv4)
1517
+		if err != nil {
1514 1518
 			return err
1515 1519
 		}
1520
+		n.ipamV4Info = info
1516 1521
 		defer func() {
1517 1522
 			if retErr != nil {
1518 1523
 				n.ipamReleaseVersion(4, ipam)
... ...
@@ -1521,123 +1526,129 @@ func (n *Network) ipamAllocate() (retErr error) {
1521 1521
 	}
1522 1522
 
1523 1523
 	if n.enableIPv6 {
1524
-		if err := n.ipamAllocateVersion(6, ipam); err != nil {
1524
+		if len(n.ipamV6Config) == 0 {
1525
+			n.ipamV6Config = []*IpamConf{{}}
1526
+		}
1527
+		info, err := n.ipamAllocateVersion(ipam, true, n.ipamV6Config, n.ipamV6Info, n.skipGwAllocIPv6)
1528
+		if err != nil {
1525 1529
 			return err
1526 1530
 		}
1531
+		n.ipamV6Info = info
1527 1532
 	}
1528 1533
 
1529 1534
 	return nil
1530 1535
 }
1531 1536
 
1532
-func (n *Network) ipamAllocateVersion(ipVer int, ipam ipamapi.Ipam) error {
1533
-	var (
1534
-		cfgList     *[]*IpamConf
1535
-		infoList    *[]*IpamInfo
1536
-		skipGwAlloc bool
1537
-		err         error
1538
-	)
1539
-
1540
-	switch ipVer {
1541
-	case 4:
1542
-		cfgList = &n.ipamV4Config
1543
-		infoList = &n.ipamV4Info
1544
-		skipGwAlloc = n.skipGwAllocIPv4
1545
-	case 6:
1546
-		cfgList = &n.ipamV6Config
1547
-		infoList = &n.ipamV6Info
1548
-		skipGwAlloc = n.skipGwAllocIPv6
1549
-	default:
1550
-		return types.InternalErrorf("incorrect ip version passed to ipam allocate: %d", ipVer)
1551
-	}
1552
-
1553
-	if len(*cfgList) == 0 {
1554
-		*cfgList = []*IpamConf{{}}
1555
-	}
1556
-
1557
-	*infoList = make([]*IpamInfo, len(*cfgList))
1537
+func (n *Network) ipamAllocateVersion(ipam ipamapi.Ipam, v6 bool, ipamConf []*IpamConf, ipamInfo []*IpamInfo, skipGwAlloc bool) (_ []*IpamInfo, retErr error) {
1538
+	var newInfo []*IpamInfo
1558 1539
 
1559
-	log.G(context.TODO()).Debugf("Allocating IPv%d pools for network %s (%s)", ipVer, n.Name(), n.ID())
1540
+	log.G(context.TODO()).WithFields(log.Fields{
1541
+		"network": n.Name(),
1542
+		"nid":     stringid.TruncateID(n.ID()),
1543
+		"ipv6":    v6,
1544
+	}).Debug("Allocating pools for network")
1560 1545
 
1561
-	for i, cfg := range *cfgList {
1562
-		if err = cfg.Validate(); err != nil {
1563
-			return err
1546
+	for i, cfg := range ipamConf {
1547
+		if err := cfg.Validate(); err != nil {
1548
+			return nil, err
1564 1549
 		}
1565
-		d := &IpamInfo{}
1566
-		(*infoList)[i] = d
1567
-
1568
-		d.AddressSpace = n.addrSpace
1569 1550
 
1570 1551
 		var reserved []netip.Prefix
1571 1552
 		if n.Scope() != scope.Global {
1572
-			reserved = netutils.InferReservedNetworks(ipVer == 6)
1553
+			reserved = netutils.InferReservedNetworks(v6)
1554
+		}
1555
+
1556
+		// During network restore, if no subnet was specified in the original network-create
1557
+		// request, use the previously allocated subnet.
1558
+		prefPool := cfg.PreferredPool
1559
+		if prefPool == "" && len(ipamInfo) > i {
1560
+			prefPool = ipamInfo[i].Pool.String()
1573 1561
 		}
1574 1562
 
1575 1563
 		alloc, err := ipam.RequestPool(ipamapi.PoolRequest{
1576 1564
 			AddressSpace: n.addrSpace,
1577
-			Pool:         cfg.PreferredPool,
1565
+			Pool:         prefPool,
1578 1566
 			SubPool:      cfg.SubPool,
1579 1567
 			Options:      n.ipamOptions,
1580 1568
 			Exclude:      reserved,
1581
-			V6:           ipVer == 6,
1569
+			V6:           v6,
1582 1570
 		})
1583 1571
 		if err != nil {
1584
-			return err
1572
+			return nil, err
1585 1573
 		}
1586
-
1587
-		d.PoolID = alloc.PoolID
1588
-		d.Pool = netiputil.ToIPNet(alloc.Pool)
1589
-		d.Meta = alloc.Meta
1590
-
1591 1574
 		defer func() {
1592
-			if err != nil {
1593
-				if err := ipam.ReleasePool(d.PoolID); err != nil {
1594
-					log.G(context.TODO()).Warnf("Failed to release address pool %s after failure to create network %s (%s)", d.PoolID, n.Name(), n.ID())
1575
+			if retErr != nil {
1576
+				if err := ipam.ReleasePool(alloc.PoolID); err != nil {
1577
+					log.G(context.TODO()).WithFields(log.Fields{
1578
+						"network": n.Name(),
1579
+						"nid":     stringid.TruncateID(n.ID()),
1580
+						"pool":    alloc.PoolID,
1581
+						"retErr":  retErr,
1582
+						"error":   err,
1583
+					}).Warn("Failed to release address pool after failure to create network")
1595 1584
 				}
1596 1585
 			}
1597 1586
 		}()
1598 1587
 
1588
+		d := &IpamInfo{
1589
+			PoolID: alloc.PoolID,
1590
+			Meta:   alloc.Meta,
1591
+			IPAMData: driverapi.IPAMData{
1592
+				Pool:         netiputil.ToIPNet(alloc.Pool),
1593
+				AddressSpace: n.addrSpace,
1594
+			},
1595
+		}
1596
+		newInfo = append(newInfo, d)
1597
+
1598
+		// During network restore, if the original network-create request did not specify a
1599
+		// gateway, use the previously allocated gateway.
1600
+		prefGateway := cfg.Gateway
1601
+		if prefGateway == "" && len(ipamInfo) > i && ipamInfo[i].Gateway != nil {
1602
+			prefGateway = ipamInfo[i].Gateway.IP.String()
1603
+		}
1604
+
1599 1605
 		// If there's no user-configured gateway address but the IPAM driver returned a gw when it
1600 1606
 		// set up the pool, use it. (It doesn't need to be requested/reserved in IPAM.)
1601
-		if cfg.Gateway == "" {
1607
+		if prefGateway == "" {
1602 1608
 			if gws, ok := d.Meta[netlabel.Gateway]; ok {
1603 1609
 				if d.Gateway, err = types.ParseCIDR(gws); err != nil {
1604
-					return types.InvalidParameterErrorf("failed to parse gateway address (%v) returned by ipam driver: %v", gws, err)
1610
+					return nil, types.InvalidParameterErrorf("failed to parse gateway address (%v) returned by ipam driver: %v", gws, err)
1605 1611
 				}
1606 1612
 			}
1607 1613
 		}
1608 1614
 
1609 1615
 		// If there's still no gateway, reserve cfg.Gateway if the user specified it. Else,
1610 1616
 		// if the driver wants a gateway, let the IPAM driver select an address.
1611
-		if d.Gateway == nil && (cfg.Gateway != "" || !skipGwAlloc) {
1617
+		if d.Gateway == nil && (prefGateway != "" || !skipGwAlloc) {
1612 1618
 			gatewayOpts := map[string]string{
1613 1619
 				ipamapi.RequestAddressType: netlabel.Gateway,
1614 1620
 			}
1615
-			if d.Gateway, _, err = ipam.RequestAddress(d.PoolID, net.ParseIP(cfg.Gateway), gatewayOpts); err != nil {
1616
-				return types.InternalErrorf("failed to allocate gateway (%v): %v", cfg.Gateway, err)
1621
+			if d.Gateway, _, err = ipam.RequestAddress(d.PoolID, net.ParseIP(prefGateway), gatewayOpts); err != nil {
1622
+				return nil, types.InternalErrorf("failed to allocate gateway (%v): %v", prefGateway, err)
1617 1623
 			}
1618 1624
 		}
1619 1625
 
1620
-		// Auxiliary addresses must be part of the master address pool
1621
-		// If they fall into the container addressable pool, libnetwork will reserve them
1626
+		// Auxiliary addresses must be part of the master address pool.
1627
+		// If they fall into the container addressable pool, reserve them.
1622 1628
 		if cfg.AuxAddresses != nil {
1623 1629
 			var ip net.IP
1624 1630
 			d.IPAMData.AuxAddresses = make(map[string]*net.IPNet, len(cfg.AuxAddresses))
1625 1631
 			for k, v := range cfg.AuxAddresses {
1626 1632
 				if ip = net.ParseIP(v); ip == nil {
1627
-					return types.InvalidParameterErrorf("non parsable secondary ip address (%s:%s) passed for network %s", k, v, n.Name())
1633
+					return nil, types.InvalidParameterErrorf("non parsable secondary ip address (%s:%s) passed for network %s", k, v, n.Name())
1628 1634
 				}
1629 1635
 				if !d.Pool.Contains(ip) {
1630
-					return types.ForbiddenErrorf("auxiliary address: (%s:%s) must belong to the master pool: %s", k, v, d.Pool)
1636
+					return nil, types.ForbiddenErrorf("auxiliary address: (%s:%s) must belong to the master pool: %s", k, v, d.Pool)
1631 1637
 				}
1632 1638
 				// Attempt reservation in the container addressable pool, silent the error if address does not belong to that pool
1633 1639
 				if d.IPAMData.AuxAddresses[k], _, err = ipam.RequestAddress(d.PoolID, ip, nil); err != nil && !errors.Is(err, ipamapi.ErrIPOutOfRange) {
1634
-					return types.InternalErrorf("failed to allocate secondary ip address (%s:%s): %v", k, v, err)
1640
+					return nil, types.InternalErrorf("failed to allocate secondary ip address (%s:%s): %v", k, v, err)
1635 1641
 				}
1636 1642
 			}
1637 1643
 		}
1638 1644
 	}
1639 1645
 
1640
-	return nil
1646
+	return newInfo, nil
1641 1647
 }
1642 1648
 
1643 1649
 func (n *Network) ipamRelease() {