<commit_before>package localclient

import (
	"net"

	"github.com/ligato/vpp-agent/clientv1/defaultplugins"
	"github.com/ligato/vpp-agent/clientv1/linux"

	"github.com/ligato/vpp-agent/plugins/defaultplugins/aclplugin/model/acl"
	"github.com/ligato/vpp-agent/plugins/defaultplugins/ifplugin/model/bfd"
	vpp_intf "github.com/ligato/vpp-agent/plugins/defaultplugins/ifplugin/model/interfaces"
	"github.com/ligato/vpp-agent/plugins/defaultplugins/l2plugin/model/l2"
	"github.com/ligato/vpp-agent/plugins/defaultplugins/l3plugin/model/l3"
	linux_intf "github.com/ligato/vpp-agent/plugins/linuxplugin/ifplugin/model/interfaces"
	linux_l3 "github.com/ligato/vpp-agent/plugins/linuxplugin/l3plugin/model/l3"
)

// MockDataChangeDSL is mock for DataChangeDSL.
type MockDataChangeDSL struct {
	// List of transaction operations in the order as they were called.
	Ops []TxnOp

	txnTracker *TxnTracker
	txn        *Txn
}

// newMockDataChangeDSL is a constructor for MockDataChangeDSL.
func newMockDataChangeDSL(tracker *TxnTracker, transaction *Txn) *MockDataChangeDSL {
	return &MockDataChangeDSL{txnTracker: tracker, txn: transaction}
}

// MockPutDSL is a mock for PutDSL.
type MockPutDSL struct {
	parent *MockDataChangeDSL
}

// MockDeleteDSL is a mock for DeleteDSL.
type MockDeleteDSL struct {
	parent *MockDataChangeDSL
}

// Put initiates a chained sequence of data change DSL statements declaring
// new or changing existing configurable objects.
func (dsl *MockDataChangeDSL) Put() linux.PutDSL {
	return &MockPutDSL{dsl}
}

// Delete initiates a chained sequence of data change DSL statements
// removing existing configurable objects.
func (dsl *MockDataChangeDSL) Delete() linux.DeleteDSL {
	return &MockDeleteDSL{dsl}
}

// Send commits the transaction into the mock DB.
func (dsl *MockDataChangeDSL) Send() defaultplugins.Reply {
	err := dsl.txnTracker.commit(dsl.txn)
	return &Reply{err}
}

// apply reflects the effect of transaction operations into the mock DB.
func (dsl *MockDataChangeDSL) apply() {
	for _, op := range dsl.Ops {
		if op.Value != nil {
			dsl.txnTracker.AppliedConfig[op.Key] = op.Value
		} else {
			_, exists := dsl.txnTracker.AppliedConfig[op.Key]
			if exists {
				delete(dsl.txnTracker.AppliedConfig, op.Key)
			}
		}
	}
}

// Interface adds a mock request to create or update VPP network interface.
func (dsl *MockPutDSL) VppInterface(val *vpp_intf.Interfaces_Interface) linux.PutDSL {
	op := TxnOp{Key: vpp_intf.InterfaceKey(val.Name), Value: val}
	dsl.parent.Ops = append(dsl.parent.Ops, op)
	return dsl
}

// BfdSession adds a mock request to create or update bidirectional forwarding
// detection session.
func (dsl *MockPutDSL) BfdSession(val *bfd.SingleHopBFD_Session) linux.PutDSL {
	op := TxnOp{Key: bfd.SessionKey(val.Interface), Value: val}
	dsl.parent.Ops = append(dsl.parent.Ops, op)
	return dsl
}

// BfdAuthKeys adds a mock request to create or update bidirectional forwarding
// detection key.
func (dsl *MockPutDSL) BfdAuthKeys(val *bfd.SingleHopBFD_Key) linux.PutDSL {
	op := TxnOp{Key: bfd.AuthKeysKey(string(val.Id)), Value: val}
	dsl.parent.Ops = append(dsl.parent.Ops, op)
	return dsl
}

// BfdEchoFunction adds a mock request to create or update bidirectional
// forwarding detection echo function.
func (dsl *MockPutDSL) BfdEchoFunction(val *bfd.SingleHopBFD_EchoFunction) linux.PutDSL {
	op := TxnOp{Key: bfd.EchoFunctionKey(val.EchoSourceInterface), Value: val}
	dsl.parent.Ops = append(dsl.parent.Ops, op)
	return dsl
}

// BD adds a mock request to create or update VPP Bridge Domain.
func (dsl *MockPutDSL) BD(val *l2.BridgeDomains_BridgeDomain) linux.PutDSL {
	op := TxnOp{Key: l2.BridgeDomainKey(val.Name), Value: val}
	dsl.parent.Ops = append(dsl.parent.Ops, op)
	return dsl
}

// BDFIB adds a mock request to create or update VPP L2 Forwarding Information
// Base.
func (dsl *MockPutDSL) BDFIB(val *l2.FibTableEntries_FibTableEntry) linux.PutDSL {
	op := TxnOp{Key: l2.FibKey(val.BridgeDomain, val.PhysAddress), Value: val}
	dsl.parent.Ops = append(dsl.parent.Ops, op)
	return dsl
}

// XConnect adds a mock request to create or update VPP Cross Connect.
func (dsl *MockPutDSL) XConnect(val *l2.XConnectPairs_XConnectPair) linux.PutDSL {
	op := TxnOp{Key: l2.XConnectKey(val.ReceiveInterface), Value: val}
	dsl.parent.Ops = append(dsl.parent.Ops, op)
	return dsl
}

// StaticRoute adds a mock request to create or update VPP L3 Static Route.
func (dsl *MockPutDSL) StaticRoute(val *l3.StaticRoutes_Route) linux.PutDSL {
	_, dstAddr, _ := net.ParseCIDR(val.DstIpAddr)
	op := TxnOp{Key: l3.RouteKey(val.VrfId, dstAddr, val.NextHopAddr), Value: val}
	dsl.parent.Ops = append(dsl.parent.Ops, op)
	return dsl
}

// ACL adds a mock request to create or update VPP Access Control List.
func (dsl *MockPutDSL) ACL(val *acl.AccessLists_Acl) linux.PutDSL {
	op := TxnOp{Key: acl.Key(val.AclName), Value: val}
	dsl.parent.Ops = append(dsl.parent.Ops, op)
	return dsl
}

// LinuxInterface adds a mock request to create or update Linux network interface.
func (dsl *MockPutDSL) LinuxInterface(val *linux_intf.LinuxInterfaces_Interface) linux.PutDSL {
	op := TxnOp{Key: linux_intf.InterfaceKey(val.Name), Value: val}
	dsl.parent.Ops = append(dsl.parent.Ops, op)
	return dsl
}

func (dsl *MockPutDSL) LinuxArpEntry(val *linux_l3.LinuxStaticArpEntries_ArpEntry) linux.PutDSL {
	op := TxnOp{Key: linux_l3.StaticArpKey(val.Name), Value: val}
	dsl.parent.Ops = append(dsl.parent.Ops, op)
	return dsl
}

func (dsl *MockPutDSL) LinuxRoute(val *linux_l3.LinuxStaticRoutes_Route) linux.PutDSL {
	op := TxnOp{Key: linux_l3.StaticRouteKey(val.Name), Value: val}
	dsl.parent.Ops = append(dsl.parent.Ops, op)
	return dsl
}

// Delete changes the DSL mode to allow removal of an existing configuration.
func (dsl *MockPutDSL) Delete() linux.DeleteDSL {
	return &MockDeleteDSL{dsl.parent}
}

// Send commits the transaction into the mock DB.
func (dsl *MockPutDSL) Send() defaultplugins.Reply {
	return dsl.parent.Send()
}

// Interface adds a mock request to delete an existing VPP network interface.
func (dsl *MockDeleteDSL) VppInterface(interfaceName string) linux.DeleteDSL {
	op := TxnOp{Key: vpp_intf.InterfaceKey(interfaceName)}
	dsl.parent.Ops = append(dsl.parent.Ops, op)
	return dsl
}

// BfdSession adds a mock request to delete an existing bidirectional forwarding
// detection session.
func (dsl *MockDeleteDSL) BfdSession(bfdSessionIfaceName string) linux.DeleteDSL {
	op := TxnOp{Key: bfd.SessionKey(bfdSessionIfaceName)}
	dsl.parent.Ops = append(dsl.parent.Ops, op)
	return dsl
}

// BfdAuthKeys adds a mock request to delete an existing bidirectional forwarding
// detection key.
func (dsl *MockDeleteDSL) BfdAuthKeys(bfdKeyName string) linux.DeleteDSL {
	op := TxnOp{Key: bfd.AuthKeysKey(bfdKeyName)}
	dsl.parent.Ops = append(dsl.parent.Ops, op)
	return dsl
}

// BfdEchoFunction adds a mock request to delete an existing bidirectional
// forwarding detection echo function.
func (dsl *MockDeleteDSL) BfdEchoFunction(bfdEchoName string) linux.DeleteDSL {
	op := TxnOp{Key: bfd.EchoFunctionKey(bfdEchoName)}
	dsl.parent.Ops = append(dsl.parent.Ops, op)
	return dsl
}

// BD adds a mock request to delete an existing VPP Bridge Domain.
func (dsl *MockDeleteDSL) BD(bdName string) linux.DeleteDSL {
	op := TxnOp{Key: l2.BridgeDomainKey(bdName)}
	dsl.parent.Ops = append(dsl.parent.Ops, op)
	return dsl
}

// BDFIB adds a mock request to delete an existing VPP L2 Forwarding Information
// Base.
func (dsl *MockDeleteDSL) BDFIB(bdName string, mac string) linux.DeleteDSL {
	op := TxnOp{Key: l2.FibKey(bdName, mac)}
	dsl.parent.Ops = append(dsl.parent.Ops, op)
	return dsl
}

// XConnect adds a mock request to delete an existing VPP Cross Connect.
func (dsl *MockDeleteDSL) XConnect(rxIfName string) linux.DeleteDSL {
	op := TxnOp{Key: l2.XConnectKey(rxIfName)}
	dsl.parent.Ops = append(dsl.parent.Ops, op)
	return dsl
}

// StaticRoute adds a mock request to delete an existing VPP L3 Static Route..
func (dsl *MockDeleteDSL) StaticRoute(vrf uint32, dstAddrInput *net.IPNet, nextHopAddr net.IP) linux.DeleteDSL {
	op := TxnOp{Key: l3.RouteKey(vrf, dstAddrInput, nextHopAddr.String())}
	dsl.parent.Ops = append(dsl.parent.Ops, op)
	return dsl
}

// ACL adds a mock request to delete an existing VPP Access Control List.
func (dsl *MockDeleteDSL) ACL(aclName string) linux.DeleteDSL {
	op := TxnOp{Key: acl.Key(aclName)}
	dsl.parent.Ops = append(dsl.parent.Ops, op)
	return dsl
}

// LinuxInterface adds a mock request to delete an existing Linux network
// interface.
func (dsl *MockDeleteDSL) LinuxInterface(ifName string) linux.DeleteDSL {
	op := TxnOp{Key: linux_intf.InterfaceKey(ifName)}
	dsl.parent.Ops = append(dsl.parent.Ops, op)
	return dsl
}

func (dsl *MockDeleteDSL) LinuxArpEntry(val *linux_l3.LinuxStaticArpEntries_ArpEntry) linux.DeleteDSL {
	op := TxnOp{Key: linux_l3.StaticArpKey(val.Name), Value: val}
	dsl.parent.Ops = append(dsl.parent.Ops, op)
	return dsl
}

func (dsl *MockDeleteDSL) LinuxRoute(val *linux_l3.LinuxStaticRoutes_Route) linux.DeleteDSL {
	op := TxnOp{Key: linux_l3.StaticRouteKey(val.Name), Value: val}
	dsl.parent.Ops = append(dsl.parent.Ops, op)
	return dsl
}

// Put changes the DSL mode to allow configuration editing.
func (dsl *MockDeleteDSL) Put() linux.PutDSL {
	return &MockPutDSL{dsl.parent}
}

// Send commits the transaction into the mock DB.
func (dsl *MockDeleteDSL) Send() defaultplugins.Reply {
	return dsl.parent.Send()
}

// Reply interface allows to wait for a reply to previously called Send() and
// extract the result from it (success/error).
type Reply struct {
	err error
}

// ReceiveReply waits for a reply to previously called Send() and returns
// the result (error or nil).
func (dsl Reply) ReceiveReply() error {
	return dsl.err
}
<commit_msg>Adjust mock<commit_after>package localclient

import (
	"net"

	"github.com/ligato/vpp-agent/clientv1/defaultplugins"
	"github.com/ligato/vpp-agent/clientv1/linux"

	"github.com/ligato/vpp-agent/plugins/defaultplugins/aclplugin/model/acl"
	"github.com/ligato/vpp-agent/plugins/defaultplugins/ifplugin/model/bfd"
	vpp_intf "github.com/ligato/vpp-agent/plugins/defaultplugins/ifplugin/model/interfaces"
	"github.com/ligato/vpp-agent/plugins/defaultplugins/l2plugin/model/l2"
	"github.com/ligato/vpp-agent/plugins/defaultplugins/l3plugin/model/l3"
	linux_intf "github.com/ligato/vpp-agent/plugins/linuxplugin/ifplugin/model/interfaces"
	linux_l3 "github.com/ligato/vpp-agent/plugins/linuxplugin/l3plugin/model/l3"
)

// MockDataChangeDSL is mock for DataChangeDSL.
type MockDataChangeDSL struct {
	// List of transaction operations in the order as they were called.
	Ops []TxnOp

	txnTracker *TxnTracker
	txn        *Txn
}

// newMockDataChangeDSL is a constructor for MockDataChangeDSL.
func newMockDataChangeDSL(tracker *TxnTracker, transaction *Txn) *MockDataChangeDSL {
	return &MockDataChangeDSL{txnTracker: tracker, txn: transaction}
}

// MockPutDSL is a mock for PutDSL.
type MockPutDSL struct {
	parent *MockDataChangeDSL
}

// MockDeleteDSL is a mock for DeleteDSL.
type MockDeleteDSL struct {
	parent *MockDataChangeDSL
}

// Put initiates a chained sequence of data change DSL statements declaring
// new or changing existing configurable objects.
func (dsl *MockDataChangeDSL) Put() linux.PutDSL {
	return &MockPutDSL{dsl}
}

// Delete initiates a chained sequence of data change DSL statements
// removing existing configurable objects.
func (dsl *MockDataChangeDSL) Delete() linux.DeleteDSL {
	return &MockDeleteDSL{dsl}
}

// Send commits the transaction into the mock DB.
func (dsl *MockDataChangeDSL) Send() defaultplugins.Reply {
	err := dsl.txnTracker.commit(dsl.txn)
	return &Reply{err}
}

// apply reflects the effect of transaction operations into the mock DB.
func (dsl *MockDataChangeDSL) apply() {
	for _, op := range dsl.Ops {
		if op.Value != nil {
			dsl.txnTracker.AppliedConfig[op.Key] = op.Value
		} else {
			_, exists := dsl.txnTracker.AppliedConfig[op.Key]
			if exists {
				delete(dsl.txnTracker.AppliedConfig, op.Key)
			}
		}
	}
}

// Interface adds a mock request to create or update VPP network interface.
func (dsl *MockPutDSL) VppInterface(val *vpp_intf.Interfaces_Interface) linux.PutDSL {
	op := TxnOp{Key: vpp_intf.InterfaceKey(val.Name), Value: val}
	dsl.parent.Ops = append(dsl.parent.Ops, op)
	return dsl
}

// BfdSession adds a mock request to create or update bidirectional forwarding
// detection session.
func (dsl *MockPutDSL) BfdSession(val *bfd.SingleHopBFD_Session) linux.PutDSL {
	op := TxnOp{Key: bfd.SessionKey(val.Interface), Value: val}
	dsl.parent.Ops = append(dsl.parent.Ops, op)
	return dsl
}

// BfdAuthKeys adds a mock request to create or update bidirectional forwarding
// detection key.
func (dsl *MockPutDSL) BfdAuthKeys(val *bfd.SingleHopBFD_Key) linux.PutDSL {
	op := TxnOp{Key: bfd.AuthKeysKey(string(val.Id)), Value: val}
	dsl.parent.Ops = append(dsl.parent.Ops, op)
	return dsl
}

// BfdEchoFunction adds a mock request to create or update bidirectional
// forwarding detection echo function.
func (dsl *MockPutDSL) BfdEchoFunction(val *bfd.SingleHopBFD_EchoFunction) linux.PutDSL {
	op := TxnOp{Key: bfd.EchoFunctionKey(val.EchoSourceInterface), Value: val}
	dsl.parent.Ops = append(dsl.parent.Ops, op)
	return dsl
}

// BD adds a mock request to create or update VPP Bridge Domain.
func (dsl *MockPutDSL) BD(val *l2.BridgeDomains_BridgeDomain) linux.PutDSL {
	op := TxnOp{Key: l2.BridgeDomainKey(val.Name), Value: val}
	dsl.parent.Ops = append(dsl.parent.Ops, op)
	return dsl
}

// BDFIB adds a mock request to create or update VPP L2 Forwarding Information
// Base.
func (dsl *MockPutDSL) BDFIB(val *l2.FibTableEntries_FibTableEntry) linux.PutDSL {
	op := TxnOp{Key: l2.FibKey(val.BridgeDomain, val.PhysAddress), Value: val}
	dsl.parent.Ops = append(dsl.parent.Ops, op)
	return dsl
}

// XConnect adds a mock request to create or update VPP Cross Connect.
func (dsl *MockPutDSL) XConnect(val *l2.XConnectPairs_XConnectPair) linux.PutDSL {
	op := TxnOp{Key: l2.XConnectKey(val.ReceiveInterface), Value: val}
	dsl.parent.Ops = append(dsl.parent.Ops, op)
	return dsl
}

// StaticRoute adds a mock request to create or update VPP L3 Static Route.
func (dsl *MockPutDSL) StaticRoute(val *l3.StaticRoutes_Route) linux.PutDSL {
	_, dstAddr, _ := net.ParseCIDR(val.DstIpAddr)
	op := TxnOp{Key: l3.RouteKey(val.VrfId, dstAddr, val.NextHopAddr), Value: val}
	dsl.parent.Ops = append(dsl.parent.Ops, op)
	return dsl
}

// ACL adds a mock request to create or update VPP Access Control List.
func (dsl *MockPutDSL) ACL(val *acl.AccessLists_Acl) linux.PutDSL {
	op := TxnOp{Key: acl.Key(val.AclName), Value: val}
	dsl.parent.Ops = append(dsl.parent.Ops, op)
	return dsl
}

// LinuxInterface adds a mock request to create or update Linux network interface.
func (dsl *MockPutDSL) LinuxInterface(val *linux_intf.LinuxInterfaces_Interface) linux.PutDSL {
	op := TxnOp{Key: linux_intf.InterfaceKey(val.Name), Value: val}
	dsl.parent.Ops = append(dsl.parent.Ops, op)
	return dsl
}

func (dsl *MockPutDSL) LinuxArpEntry(val *linux_l3.LinuxStaticArpEntries_ArpEntry) linux.PutDSL {
	op := TxnOp{Key: linux_l3.StaticArpKey(val.Name), Value: val}
	dsl.parent.Ops = append(dsl.parent.Ops, op)
	return dsl
}

func (dsl *MockPutDSL) LinuxRoute(val *linux_l3.LinuxStaticRoutes_Route) linux.PutDSL {
	op := TxnOp{Key: linux_l3.StaticRouteKey(val.Name), Value: val}
	dsl.parent.Ops = append(dsl.parent.Ops, op)
	return dsl
}

// Delete changes the DSL mode to allow removal of an existing configuration.
func (dsl *MockPutDSL) Delete() linux.DeleteDSL {
	return &MockDeleteDSL{dsl.parent}
}

// Send commits the transaction into the mock DB.
func (dsl *MockPutDSL) Send() defaultplugins.Reply {
	return dsl.parent.Send()
}

// Interface adds a mock request to delete an existing VPP network interface.
func (dsl *MockDeleteDSL) VppInterface(interfaceName string) linux.DeleteDSL {
	op := TxnOp{Key: vpp_intf.InterfaceKey(interfaceName)}
	dsl.parent.Ops = append(dsl.parent.Ops, op)
	return dsl
}

// BfdSession adds a mock request to delete an existing bidirectional forwarding
// detection session.
func (dsl *MockDeleteDSL) BfdSession(bfdSessionIfaceName string) linux.DeleteDSL {
	op := TxnOp{Key: bfd.SessionKey(bfdSessionIfaceName)}
	dsl.parent.Ops = append(dsl.parent.Ops, op)
	return dsl
}

// BfdAuthKeys adds a mock request to delete an existing bidirectional forwarding
// detection key.
func (dsl *MockDeleteDSL) BfdAuthKeys(bfdKey uint32) linux.DeleteDSL {
	op := TxnOp{Key: bfd.AuthKeysKey(string(bfdKey))}
	dsl.parent.Ops = append(dsl.parent.Ops, op)
	return dsl
}

// BfdEchoFunction adds a mock request to delete an existing bidirectional
// forwarding detection echo function.
func (dsl *MockDeleteDSL) BfdEchoFunction(bfdEchoName string) linux.DeleteDSL {
	op := TxnOp{Key: bfd.EchoFunctionKey(bfdEchoName)}
	dsl.parent.Ops = append(dsl.parent.Ops, op)
	return dsl
}

// BD adds a mock request to delete an existing VPP Bridge Domain.
func (dsl *MockDeleteDSL) BD(bdName string) linux.DeleteDSL {
	op := TxnOp{Key: l2.BridgeDomainKey(bdName)}
	dsl.parent.Ops = append(dsl.parent.Ops, op)
	return dsl
}

// BDFIB adds a mock request to delete an existing VPP L2 Forwarding Information
// Base.
func (dsl *MockDeleteDSL) BDFIB(bdName string, mac string) linux.DeleteDSL {
	op := TxnOp{Key: l2.FibKey(bdName, mac)}
	dsl.parent.Ops = append(dsl.parent.Ops, op)
	return dsl
}

// XConnect adds a mock request to delete an existing VPP Cross Connect.
func (dsl *MockDeleteDSL) XConnect(rxIfName string) linux.DeleteDSL {
	op := TxnOp{Key: l2.XConnectKey(rxIfName)}
	dsl.parent.Ops = append(dsl.parent.Ops, op)
	return dsl
}

// StaticRoute adds a mock request to delete an existing VPP L3 Static Route..
func (dsl *MockDeleteDSL) StaticRoute(vrf uint32, dstAddrInput *net.IPNet, nextHopAddr net.IP) linux.DeleteDSL {
	op := TxnOp{Key: l3.RouteKey(vrf, dstAddrInput, nextHopAddr.String())}
	dsl.parent.Ops = append(dsl.parent.Ops, op)
	return dsl
}

// ACL adds a mock request to delete an existing VPP Access Control List.
func (dsl *MockDeleteDSL) ACL(aclName string) linux.DeleteDSL {
	op := TxnOp{Key: acl.Key(aclName)}
	dsl.parent.Ops = append(dsl.parent.Ops, op)
	return dsl
}

// LinuxInterface adds a mock request to delete an existing Linux network
// interface.
func (dsl *MockDeleteDSL) LinuxInterface(ifName string) linux.DeleteDSL {
	op := TxnOp{Key: linux_intf.InterfaceKey(ifName)}
	dsl.parent.Ops = append(dsl.parent.Ops, op)
	return dsl
}

func (dsl *MockDeleteDSL) LinuxArpEntry(val *linux_l3.LinuxStaticArpEntries_ArpEntry) linux.DeleteDSL {
	op := TxnOp{Key: linux_l3.StaticArpKey(val.Name), Value: val}
	dsl.parent.Ops = append(dsl.parent.Ops, op)
	return dsl
}

func (dsl *MockDeleteDSL) LinuxRoute(val *linux_l3.LinuxStaticRoutes_Route) linux.DeleteDSL {
	op := TxnOp{Key: linux_l3.StaticRouteKey(val.Name), Value: val}
	dsl.parent.Ops = append(dsl.parent.Ops, op)
	return dsl
}

// Put changes the DSL mode to allow configuration editing.
func (dsl *MockDeleteDSL) Put() linux.PutDSL {
	return &MockPutDSL{dsl.parent}
}

// Send commits the transaction into the mock DB.
func (dsl *MockDeleteDSL) Send() defaultplugins.Reply {
	return dsl.parent.Send()
}

// Reply interface allows to wait for a reply to previously called Send() and
// extract the result from it (success/error).
type Reply struct {
	err error
}

// ReceiveReply waits for a reply to previously called Send() and returns
// the result (error or nil).
func (dsl Reply) ReceiveReply() error {
	return dsl.err
}
<|endoftext|>


<commit_before>package netup

import (
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"path/filepath"
	"regexp"
	"time"

	"github.com/koron/go-arch"
	"github.com/koron/go-github"
)

var (
	errSourceNotModified = errors.New("source not modified")

	errGithubNoRelease       = errors.New("absence of github release")
	errGithubNoAssets        = errors.New("no matched assets in github release")
	errGithubIncompleteAsset = errors.New("incomplete github asset")
)

type progressFunc func(curr, max int64)

// Source describes source of update.
type Source interface {
	// download downloads source file to outdir, return its path name.
	// if pivot is not zero, this checks changes of source from pivot.
	download(outdir string, pivot time.Time, f progressFunc) (path string, err error)

	stripCount() int

	name() string

	// String returns a string to represent source.
	String() string
}

// DirectSource represents direct ZIP source.
type DirectSource struct {
	Name  string
	URL   string
	Strip int
}

var _ Source = (*DirectSource)(nil)

func (ds *DirectSource) download(d string, p time.Time, f progressFunc) (string, error) {
	return download(ds.URL, d, p, f)
}

func (ds *DirectSource) stripCount() int {
	return ds.Strip
}

func (ds *DirectSource) name() string {
	return ds.Name
}

func (ds *DirectSource) String() string {
	return fmt.Sprintf("direct: URL=%s", ds.URL)
}

// GithubSource represents project source on GitHub.
type GithubSource struct {
	Name    string
	User    string
	Project string
	NamePat *regexp.Regexp
	Strip   int
}

var _ Source = (*GithubSource)(nil)

func (gs *GithubSource) download(d string, p time.Time, f progressFunc) (string, error) {
	a, err := gs.fetchAsset()
	if err != nil {
		return "", err
	}
	if !p.IsZero() && p.After(a.UpdatedAt) {
		return "", errSourceNotModified
	}
	msgPrintln("found newer release on GitHub")
	return download(a.DownloadURL, d, p, f)
}

func (gs *GithubSource) stripCount() int {
	return gs.Strip
}

func (gs *GithubSource) name() string {
	return gs.Name
}

func (gs *GithubSource) fetchAsset() (*github.Asset, error) {
	r, err := github.Latest(gs.User, gs.Project)
	if err != nil {
		return nil, err
	}
	if r.Draft || r.PreRelease {
		return nil, errGithubNoRelease
	}
	var t *github.Asset
	for _, a := range r.Assets {
		if gs.NamePat.MatchString(a.Name) {
			t = &a
			break
		}
	}
	if t == nil {
		return nil, errGithubNoAssets
	}
	if t.State != "uploaded" {
		return nil, errGithubIncompleteAsset
	}
	return t, nil
}

func (gs *GithubSource) String() string {
	return fmt.Sprintf("GitHub: %s/%s pattern=%s",
		gs.User, gs.Project, gs.NamePat.String())
}

func downloadFilepath(inURL, outdir string) (string, error) {
	u, err := url.Parse(inURL)
	if err != nil {
		return "", err
	}
	return filepath.Join(outdir, filepath.Base(u.Path)), nil
}

var downloadTimeout = 5 * time.Minute

func downloadAsFile(inURL, outPath string, pivot time.Time, pf progressFunc) error {
	req, err := http.NewRequest("GET", inURL, nil)
	if err != nil {
		return err
	}
	if !pivot.IsZero() {
		t := pivot.UTC().Format(http.TimeFormat)
		req.Header.Set("If-Modified-Since", t)
	}
	logInfo("download URL %s as file %s", inURL, outPath)
	msgPrintf("download %s\n", inURL)
	client := http.Client{Timeout: downloadTimeout}
	resp, err := client.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	switch resp.StatusCode {
	case http.StatusOK:
		return saveBody(outPath, resp, pf)
	case http.StatusNotModified:
		return errSourceNotModified
	default:
		return fmt.Errorf("unexpected response: %s", resp.Status)
	}
}

// download downloads URL and saves as a file to outdir, return its path name.
// if pivot is not zero, this checks changes of source after pivot.
func download(inURL, outdir string, pivot time.Time, f progressFunc) (string, error) {
	path, err := downloadFilepath(inURL, outdir)
	if err != nil {
		return "", err
	}
	if err := downloadAsFile(inURL, path, pivot, f); err != nil {
		return "", err
	}
	return path, nil
}

func saveBody(outPath string, resp *http.Response, pf progressFunc) error {
	f, err := os.Create(outPath)
	if err != nil {
		return err
	}
	defer f.Close()
	w := &progressWriter{w: f, f: pf, m: resp.ContentLength}
	if _, err := io.Copy(w, resp.Body); err != nil {
		return err
	}
	return nil
}

type progressWriter struct {
	w    io.Writer
	f    progressFunc
	n, m int64
}

func (w *progressWriter) Write(p []byte) (int, error) {
	n, err := w.w.Write(p)
	w.n += int64(n)
	if w.f != nil {
		w.f(w.n, w.m)
	}
	return n, err
}

// SourcePack is the map arch.CPU to source.
type SourcePack map[arch.CPU]Source
<commit_msg>coniditional request of Github<commit_after>package netup

import (
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"path/filepath"
	"regexp"
	"time"

	"github.com/koron/go-arch"
	"github.com/koron/go-github"
)

var (
	errSourceNotModified = errors.New("source not modified")

	errGithubNoRelease       = errors.New("absence of github release")
	errGithubNoAssets        = errors.New("no matched assets in github release")
	errGithubIncompleteAsset = errors.New("incomplete github asset")
)

type progressFunc func(curr, max int64)

// Source describes source of update.
type Source interface {
	// download downloads source file to outdir, return its path name.
	// if pivot is not zero, this checks changes of source from pivot.
	download(outdir string, pivot time.Time, f progressFunc) (path string, err error)

	stripCount() int

	name() string

	// String returns a string to represent source.
	String() string
}

// DirectSource represents direct ZIP source.
type DirectSource struct {
	Name  string
	URL   string
	Strip int
}

var _ Source = (*DirectSource)(nil)

func (ds *DirectSource) download(d string, p time.Time, f progressFunc) (string, error) {
	return download(ds.URL, d, p, f)
}

func (ds *DirectSource) stripCount() int {
	return ds.Strip
}

func (ds *DirectSource) name() string {
	return ds.Name
}

func (ds *DirectSource) String() string {
	return fmt.Sprintf("direct: URL=%s", ds.URL)
}

// GithubSource represents project source on GitHub.
type GithubSource struct {
	Name    string
	User    string
	Project string
	NamePat *regexp.Regexp
	Strip   int
}

var _ Source = (*GithubSource)(nil)

func (gs *GithubSource) download(d string, p time.Time, f progressFunc) (string, error) {
	a, err := gs.fetchAsset(p)
	if err != nil {
		return "", err
	}
	if !p.IsZero() && p.After(a.UpdatedAt) {
		return "", errSourceNotModified
	}
	msgPrintln("found newer release on GitHub")
	return download(a.DownloadURL, d, p, f)
}

func (gs *GithubSource) stripCount() int {
	return gs.Strip
}

func (gs *GithubSource) name() string {
	return gs.Name
}

func (gs *GithubSource) fetchAsset(pivot time.Time) (*github.Asset, error) {
	r, err := github.LatestIfModifiedSince(gs.User, gs.Project, pivot)
	if err != nil {
		if err == github.ErrNotModified {
			err = errSourceNotModified
		}
		return nil, err
	}
	if r.Draft || r.PreRelease {
		return nil, errGithubNoRelease
	}
	var t *github.Asset
	for _, a := range r.Assets {
		if gs.NamePat.MatchString(a.Name) {
			t = &a
			break
		}
	}
	if t == nil {
		return nil, errGithubNoAssets
	}
	if t.State != "uploaded" {
		return nil, errGithubIncompleteAsset
	}
	return t, nil
}

func (gs *GithubSource) String() string {
	return fmt.Sprintf("GitHub: %s/%s pattern=%s",
		gs.User, gs.Project, gs.NamePat.String())
}

func downloadFilepath(inURL, outdir string) (string, error) {
	u, err := url.Parse(inURL)
	if err != nil {
		return "", err
	}
	return filepath.Join(outdir, filepath.Base(u.Path)), nil
}

var downloadTimeout = 5 * time.Minute

func downloadAsFile(inURL, outPath string, pivot time.Time, pf progressFunc) error {
	req, err := http.NewRequest("GET", inURL, nil)
	if err != nil {
		return err
	}
	if !pivot.IsZero() {
		t := pivot.UTC().Format(http.TimeFormat)
		req.Header.Set("If-Modified-Since", t)
	}
	logInfo("download URL %s as file %s", inURL, outPath)
	msgPrintf("download %s\n", inURL)
	client := http.Client{Timeout: downloadTimeout}
	resp, err := client.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	switch resp.StatusCode {
	case http.StatusOK:
		return saveBody(outPath, resp, pf)
	case http.StatusNotModified:
		return errSourceNotModified
	default:
		return fmt.Errorf("unexpected response: %s", resp.Status)
	}
}

// download downloads URL and saves as a file to outdir, return its path name.
// if pivot is not zero, this checks changes of source after pivot.
func download(inURL, outdir string, pivot time.Time, f progressFunc) (string, error) {
	path, err := downloadFilepath(inURL, outdir)
	if err != nil {
		return "", err
	}
	if err := downloadAsFile(inURL, path, pivot, f); err != nil {
		return "", err
	}
	return path, nil
}

func saveBody(outPath string, resp *http.Response, pf progressFunc) error {
	f, err := os.Create(outPath)
	if err != nil {
		return err
	}
	defer f.Close()
	w := &progressWriter{w: f, f: pf, m: resp.ContentLength}
	if _, err := io.Copy(w, resp.Body); err != nil {
		return err
	}
	return nil
}

type progressWriter struct {
	w    io.Writer
	f    progressFunc
	n, m int64
}

func (w *progressWriter) Write(p []byte) (int, error) {
	n, err := w.w.Write(p)
	w.n += int64(n)
	if w.f != nil {
		w.f(w.n, w.m)
	}
	return n, err
}

// SourcePack is the map arch.CPU to source.
type SourcePack map[arch.CPU]Source
<|endoftext|>


<commit_before>package manager

import (
	"errors"
	"fmt"
	"strings"

	v3 "github.com/rancher/types/apis/management.cattle.io/v3"
	client "github.com/rancher/types/client/management/v3"
	"github.com/sirupsen/logrus"
	kerrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
)

func hasAllUpdates(catalog *v3.Catalog) bool {
	upgraded := v3.CatalogConditionUpgraded.IsTrue(catalog)
	diskCached := v3.CatalogConditionDiskCached.IsTrue(catalog)
	return upgraded && diskCached
}

func isUpToDate(commit string, catalog *v3.Catalog) bool {
	commitsEqual := commit == catalog.Status.Commit
	updated := hasAllUpdates(catalog)
	return commitsEqual && updated
}

func setRefreshed(catalog *v3.Catalog) bool {
	logrus.Debugf("Catalog %s is already up to date", catalog.Name)
	if !v3.CatalogConditionRefreshed.IsTrue(catalog) {
		v3.CatalogConditionRefreshed.True(catalog)
		v3.CatalogConditionRefreshed.Reason(catalog, "")
		v3.CatalogConditionRefreshed.Message(catalog, "")
		return true
	}
	return false
}

func setRefreshedError(catalog *v3.Catalog, err error) {
	v3.CatalogConditionRefreshed.False(catalog)
	v3.CatalogConditionRefreshed.ReasonAndMessageFromError(catalog, err)
}

func (m *Manager) deleteTemplates(key string, namespace string) error {
	templates, err := m.getTemplateMap(key, namespace)
	if err != nil {
		return err
	}
	tvToDelete := map[string]struct{}{}
	for _, t := range templates {
		tvs, err := m.getTemplateVersion(t.Name, namespace)
		if err != nil {
			//if template version doesn't exist continue to delete template
			if strings.Contains(err.Error(), "invalid label value") {
				continue
			}
			return err
		}
		for k := range tvs {
			tvToDelete[k] = struct{}{}
		}
	}
	go func() {
		for {
			for k := range templates {
				if err := m.templateClient.DeleteNamespaced(namespace, k, &metav1.DeleteOptions{}); err != nil && !kerrors.IsNotFound(err) {
					logrus.Warnf("Deleting template %v doesn't succeed. Continue loop", k)
					continue
				}
			}
			for k := range tvToDelete {
				if err := m.templateVersionClient.DeleteNamespaced(namespace, k, &metav1.DeleteOptions{}); err != nil && !kerrors.IsNotFound(err) {
					logrus.Warnf("Deleting templateVersion %v doesn't succeed. Continue loop", k)
					continue
				}
			}
			break
		}
	}()
	return nil
}

func getCatalogType(cmt *CatalogInfo) string {
	if cmt.projectCatalog == nil && cmt.clusterCatalog == nil {
		return client.CatalogType
	} else if cmt.projectCatalog != nil {
		return client.ProjectCatalogType
	} else {
		return client.ClusterCatalogType
	}
}

func (m *Manager) updateCatalogInfo(cmt *CatalogInfo, catalogType string, templateName string, condition bool, updateOnly bool) (*CatalogInfo, error) {
	var obj runtime.Object
	if condition {
		switch catalogType {
		case client.CatalogType:
			obj = runtime.Object(cmt.catalog)
		case client.ProjectCatalogType:
			obj = runtime.Object(cmt.projectCatalog)
		case client.ClusterCatalogType:
			obj = runtime.Object(cmt.clusterCatalog)
		default:
			return cmt, fmt.Errorf("incorrect catalog type")
		}
		v3.CatalogConditionRefreshed.Unknown(obj)
		if templateName != "" {
			v3.CatalogConditionRefreshed.Message(obj, fmt.Sprintf("syncing catalog %v", cmt.catalog.Name))
		} else {
			v3.CatalogConditionRefreshed.Message(obj, fmt.Sprintf(""))
		}
	}

	if updateOnly {
		switch catalogType {
		case client.CatalogType:
			if _, err := m.catalogClient.Update(cmt.catalog); err != nil {
				return nil, err
			}
		case client.ProjectCatalogType:
			if _, err := m.projectCatalogClient.Update(cmt.projectCatalog); err != nil {
				return nil, err
			}
		case client.ClusterCatalogType:
			if _, err := m.clusterCatalogClient.Update(cmt.clusterCatalog); err != nil {
				return nil, err
			}
		default:
			return cmt, fmt.Errorf("incorrect catalog type")
		}
		return cmt, nil
	}

	switch catalogType {
	case client.CatalogType:
		catalog := cmt.catalog
		if newCatalog, err := m.catalogClient.Update(cmt.catalog); err == nil {
			catalog = newCatalog
		} else {
			catalog, _ = m.catalogClient.Get(catalog.Name, metav1.GetOptions{})
		}
		cmt.catalog = catalog
	case client.ProjectCatalogType:
		projectCatalog := cmt.projectCatalog
		if newCatalog, err := m.projectCatalogClient.Update(projectCatalog); err == nil {
			projectCatalog = newCatalog
		} else {
			projectCatalog, _ = m.projectCatalogClient.Get(projectCatalog.Name, metav1.GetOptions{})
		}
		cmt.catalog = &projectCatalog.Catalog
		cmt.projectCatalog = projectCatalog
	case client.ClusterCatalogType:
		clusterCatalog := cmt.clusterCatalog
		if newCatalog, err := m.clusterCatalogClient.Update(clusterCatalog); err == nil {
			clusterCatalog = newCatalog
		} else {
			clusterCatalog, _ = m.clusterCatalogClient.Get(clusterCatalog.Name, metav1.GetOptions{})
		}
		cmt.catalog = &clusterCatalog.Catalog
		cmt.clusterCatalog = clusterCatalog
	default:
		return cmt, fmt.Errorf("incorrect catalog type")
	}

	return cmt, nil
}

func setCatalogErrorState(cmt *CatalogInfo, catalog *v3.Catalog, projectCatalog *v3.ProjectCatalog, clusterCatalog *v3.ClusterCatalog) {
	v3.CatalogConditionRefreshed.False(catalog)
	v3.CatalogConditionRefreshed.Message(catalog, fmt.Sprintf("Error syncing catalog %v", catalog.Name))
	v3.CatalogConditionProcessed.True(catalog)
	cmt.catalog = catalog
	cmt.projectCatalog = projectCatalog
	cmt.clusterCatalog = clusterCatalog
}

func setCatalogIgnoreErrorState(commit string, cmt *CatalogInfo, catalog *v3.Catalog, projectCatalog *v3.ProjectCatalog, clusterCatalog *v3.ClusterCatalog, message string) {
	v3.CatalogConditionProcessed.False(catalog)
	v3.CatalogConditionProcessed.Message(catalog, message)
	v3.CatalogConditionRefreshed.Message(catalog, "")
	v3.CatalogConditionProcessed.ReasonAndMessageFromError(catalog, errors.New(message))
	v3.CatalogConditionRefreshed.True(catalog)
	catalog.Status.Commit = commit
	if projectCatalog != nil {
		projectCatalog.Catalog = *catalog
	} else if clusterCatalog != nil {
		clusterCatalog.Catalog = *catalog
	}
	cmt.catalog = catalog
	cmt.projectCatalog = projectCatalog
	cmt.clusterCatalog = clusterCatalog
}

func setTraverseCompleted(catalog *v3.Catalog) {
	v3.CatalogConditionUpgraded.True(catalog)
	v3.CatalogConditionDiskCached.True(catalog)
	v3.CatalogConditionProcessed.True(catalog)
	v3.CatalogConditionProcessed.Message(catalog, "")
	v3.CatalogConditionProcessed.Reason(catalog, "")
}

// Using Helm standards to make a qualified name to be used for template name, see link below
// General Helm Chart conventions should be followed as we will not correct all potential issues
// https://github.com/helm/helm/blob/9b42702a4bced339ff424a78ad68dd6be6e1a80a/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_fullname.tpl
func getValidTemplateName(catalogName, chartName string) string {
	templateName := fmt.Sprintf("%s-%s", catalogName, chartName)
	templateName = strings.ToLower(templateName)
	templateName = strings.TrimSuffix(templateName, "-")
	return templateName
}

// Using Helm standards to make a label to be used for the template version name, see link below
// General Helm Chart conventions should be followed as we will not correct all potential issues
// https://github.com/helm/helm/blob/3582b03a91bb994aa4d33a7bc50de5205f734c7a/pkg/chartutil/create.go
func getValidTemplateNameWithVersion(templateName, version string) string {
	label := fmt.Sprintf("%s-%s", templateName, version)
	label = strings.ReplaceAll(label, "+", "-")
	label = strings.TrimSuffix(label, "-")
	return label
}
<commit_msg>Support uppercase characters in version strings<commit_after>package manager

import (
	"errors"
	"fmt"
	"strings"

	v3 "github.com/rancher/types/apis/management.cattle.io/v3"
	client "github.com/rancher/types/client/management/v3"
	"github.com/sirupsen/logrus"
	kerrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
)

func hasAllUpdates(catalog *v3.Catalog) bool {
	upgraded := v3.CatalogConditionUpgraded.IsTrue(catalog)
	diskCached := v3.CatalogConditionDiskCached.IsTrue(catalog)
	return upgraded && diskCached
}

func isUpToDate(commit string, catalog *v3.Catalog) bool {
	commitsEqual := commit == catalog.Status.Commit
	updated := hasAllUpdates(catalog)
	return commitsEqual && updated
}

func setRefreshed(catalog *v3.Catalog) bool {
	logrus.Debugf("Catalog %s is already up to date", catalog.Name)
	if !v3.CatalogConditionRefreshed.IsTrue(catalog) {
		v3.CatalogConditionRefreshed.True(catalog)
		v3.CatalogConditionRefreshed.Reason(catalog, "")
		v3.CatalogConditionRefreshed.Message(catalog, "")
		return true
	}
	return false
}

func setRefreshedError(catalog *v3.Catalog, err error) {
	v3.CatalogConditionRefreshed.False(catalog)
	v3.CatalogConditionRefreshed.ReasonAndMessageFromError(catalog, err)
}

func (m *Manager) deleteTemplates(key string, namespace string) error {
	templates, err := m.getTemplateMap(key, namespace)
	if err != nil {
		return err
	}
	tvToDelete := map[string]struct{}{}
	for _, t := range templates {
		tvs, err := m.getTemplateVersion(t.Name, namespace)
		if err != nil {
			//if template version doesn't exist continue to delete template
			if strings.Contains(err.Error(), "invalid label value") {
				continue
			}
			return err
		}
		for k := range tvs {
			tvToDelete[k] = struct{}{}
		}
	}
	go func() {
		for {
			for k := range templates {
				if err := m.templateClient.DeleteNamespaced(namespace, k, &metav1.DeleteOptions{}); err != nil && !kerrors.IsNotFound(err) {
					logrus.Warnf("Deleting template %v doesn't succeed. Continue loop", k)
					continue
				}
			}
			for k := range tvToDelete {
				if err := m.templateVersionClient.DeleteNamespaced(namespace, k, &metav1.DeleteOptions{}); err != nil && !kerrors.IsNotFound(err) {
					logrus.Warnf("Deleting templateVersion %v doesn't succeed. Continue loop", k)
					continue
				}
			}
			break
		}
	}()
	return nil
}

func getCatalogType(cmt *CatalogInfo) string {
	if cmt.projectCatalog == nil && cmt.clusterCatalog == nil {
		return client.CatalogType
	} else if cmt.projectCatalog != nil {
		return client.ProjectCatalogType
	} else {
		return client.ClusterCatalogType
	}
}

func (m *Manager) updateCatalogInfo(cmt *CatalogInfo, catalogType string, templateName string, condition bool, updateOnly bool) (*CatalogInfo, error) {
	var obj runtime.Object
	if condition {
		switch catalogType {
		case client.CatalogType:
			obj = runtime.Object(cmt.catalog)
		case client.ProjectCatalogType:
			obj = runtime.Object(cmt.projectCatalog)
		case client.ClusterCatalogType:
			obj = runtime.Object(cmt.clusterCatalog)
		default:
			return cmt, fmt.Errorf("incorrect catalog type")
		}
		v3.CatalogConditionRefreshed.Unknown(obj)
		if templateName != "" {
			v3.CatalogConditionRefreshed.Message(obj, fmt.Sprintf("syncing catalog %v", cmt.catalog.Name))
		} else {
			v3.CatalogConditionRefreshed.Message(obj, fmt.Sprintf(""))
		}
	}

	if updateOnly {
		switch catalogType {
		case client.CatalogType:
			if _, err := m.catalogClient.Update(cmt.catalog); err != nil {
				return nil, err
			}
		case client.ProjectCatalogType:
			if _, err := m.projectCatalogClient.Update(cmt.projectCatalog); err != nil {
				return nil, err
			}
		case client.ClusterCatalogType:
			if _, err := m.clusterCatalogClient.Update(cmt.clusterCatalog); err != nil {
				return nil, err
			}
		default:
			return cmt, fmt.Errorf("incorrect catalog type")
		}
		return cmt, nil
	}

	switch catalogType {
	case client.CatalogType:
		catalog := cmt.catalog
		if newCatalog, err := m.catalogClient.Update(cmt.catalog); err == nil {
			catalog = newCatalog
		} else {
			catalog, _ = m.catalogClient.Get(catalog.Name, metav1.GetOptions{})
		}
		cmt.catalog = catalog
	case client.ProjectCatalogType:
		projectCatalog := cmt.projectCatalog
		if newCatalog, err := m.projectCatalogClient.Update(projectCatalog); err == nil {
			projectCatalog = newCatalog
		} else {
			projectCatalog, _ = m.projectCatalogClient.Get(projectCatalog.Name, metav1.GetOptions{})
		}
		cmt.catalog = &projectCatalog.Catalog
		cmt.projectCatalog = projectCatalog
	case client.ClusterCatalogType:
		clusterCatalog := cmt.clusterCatalog
		if newCatalog, err := m.clusterCatalogClient.Update(clusterCatalog); err == nil {
			clusterCatalog = newCatalog
		} else {
			clusterCatalog, _ = m.clusterCatalogClient.Get(clusterCatalog.Name, metav1.GetOptions{})
		}
		cmt.catalog = &clusterCatalog.Catalog
		cmt.clusterCatalog = clusterCatalog
	default:
		return cmt, fmt.Errorf("incorrect catalog type")
	}

	return cmt, nil
}

func setCatalogErrorState(cmt *CatalogInfo, catalog *v3.Catalog, projectCatalog *v3.ProjectCatalog, clusterCatalog *v3.ClusterCatalog) {
	v3.CatalogConditionRefreshed.False(catalog)
	v3.CatalogConditionRefreshed.Message(catalog, fmt.Sprintf("Error syncing catalog %v", catalog.Name))
	v3.CatalogConditionProcessed.True(catalog)
	cmt.catalog = catalog
	cmt.projectCatalog = projectCatalog
	cmt.clusterCatalog = clusterCatalog
}

func setCatalogIgnoreErrorState(commit string, cmt *CatalogInfo, catalog *v3.Catalog, projectCatalog *v3.ProjectCatalog, clusterCatalog *v3.ClusterCatalog, message string) {
	v3.CatalogConditionProcessed.False(catalog)
	v3.CatalogConditionProcessed.Message(catalog, message)
	v3.CatalogConditionRefreshed.Message(catalog, "")
	v3.CatalogConditionProcessed.ReasonAndMessageFromError(catalog, errors.New(message))
	v3.CatalogConditionRefreshed.True(catalog)
	catalog.Status.Commit = commit
	if projectCatalog != nil {
		projectCatalog.Catalog = *catalog
	} else if clusterCatalog != nil {
		clusterCatalog.Catalog = *catalog
	}
	cmt.catalog = catalog
	cmt.projectCatalog = projectCatalog
	cmt.clusterCatalog = clusterCatalog
}

func setTraverseCompleted(catalog *v3.Catalog) {
	v3.CatalogConditionUpgraded.True(catalog)
	v3.CatalogConditionDiskCached.True(catalog)
	v3.CatalogConditionProcessed.True(catalog)
	v3.CatalogConditionProcessed.Message(catalog, "")
	v3.CatalogConditionProcessed.Reason(catalog, "")
}

// Using Helm standards to make a qualified name to be used for template name, see link below
// General Helm Chart conventions should be followed as we will not correct all potential issues
// https://github.com/helm/helm/blob/9b42702a4bced339ff424a78ad68dd6be6e1a80a/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_fullname.tpl
func getValidTemplateName(catalogName, chartName string) string {
	templateName := fmt.Sprintf("%s-%s", catalogName, chartName)
	templateName = strings.ToLower(templateName)
	templateName = strings.TrimSuffix(templateName, "-")
	return templateName
}

// Using Helm standards to make a label to be used for the template version name, see link below
// General Helm Chart conventions should be followed as we will not correct all potential issues
// https://github.com/helm/helm/blob/3582b03a91bb994aa4d33a7bc50de5205f734c7a/pkg/chartutil/create.go
func getValidTemplateNameWithVersion(templateName, version string) string {
	label := fmt.Sprintf("%s-%s", templateName, version)
	label = strings.ReplaceAll(label, "+", "-")
	label = strings.TrimSuffix(label, "-")
	label = strings.ToLower(label)
	return label
}
<|endoftext|>


<commit_before>/*
Copyright 2017 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package sync

import (
	"context"
	"fmt"
	"net"
	"time"

	"github.com/golang/glog"

	"k8s.io/api/core/v1"
	"k8s.io/kubernetes/pkg/controller/node/ipam/cidrset"
)

const (
	// InvalidPodCIDR is the event recorded when a node is found with an
	// invalid PodCIDR.
	InvalidPodCIDR = "CloudCIDRAllocatorInvalidPodCIDR"
	// InvalidModeEvent is the event recorded when the CIDR range cannot be
	// sync'd due to the cluster running in the wrong mode.
	InvalidModeEvent = "CloudCIDRAllocatorInvalidMode"
	// MismatchEvent is the event recorded when the CIDR range allocated in the
	// node spec does not match what has been allocated in the cloud.
	MismatchEvent = "CloudCIDRAllocatorMismatch"
)

// cloudAlias is the interface to the cloud platform APIs.
type cloudAlias interface {
	// Alias returns the IP alias for the node.
	Alias(ctx context.Context, nodeName string) (*net.IPNet, error)
	// AddAlias adds an alias to the node.
	AddAlias(ctx context.Context, nodeName string, cidrRange *net.IPNet) error
}

// kubeAPI is the interface to the Kubernetes APIs.
type kubeAPI interface {
	// Node returns the spec for the Node object.
	Node(ctx context.Context, name string) (*v1.Node, error)
	// UpdateNodePodCIDR updates the PodCIDR in the Node spec.
	UpdateNodePodCIDR(ctx context.Context, node *v1.Node, cidrRange *net.IPNet) error
	// UpdateNodeNetworkUnavailable updates the network unavailable status for the node.
	UpdateNodeNetworkUnavailable(nodeName string, unavailable bool) error
	// EmitNodeWarningEvent emits an event for the given node.
	EmitNodeWarningEvent(nodeName, reason, fmt string, args ...interface{})
}

// controller is the interface to the controller.
type controller interface {
	// ReportResult updates the controller with the result of the latest
	// sync operation.
	ReportResult(err error)
	// ResyncTimeout returns the amount of time to wait before retrying
	// a sync with a node.
	ResyncTimeout() time.Duration
}

// NodeSyncMode is the mode the cloud CIDR allocator runs in.
type NodeSyncMode string

var (
	// SyncFromCloud is the mode that synchronizes the IP allocation from the cloud
	// platform to the node.
	SyncFromCloud NodeSyncMode = "SyncFromCloud"
	// SyncFromCluster is the mode that synchronizes the IP allocation determined
	// by the k8s controller to the cloud provider.
	SyncFromCluster NodeSyncMode = "SyncFromCluster"
)

// IsValidMode returns true if the given mode is valid.
func IsValidMode(m NodeSyncMode) bool {
	switch m {
	case SyncFromCloud:
	case SyncFromCluster:
	default:
		return false
	}
	return true
}

// NodeSync synchronizes the state for a single node in the cluster.
type NodeSync struct {
	c          controller
	cloudAlias cloudAlias
	kubeAPI    kubeAPI
	mode       NodeSyncMode
	nodeName   string
	opChan     chan syncOp
	set        *cidrset.CidrSet
}

// New returns a new syncer for a given node.
func New(c controller, cloudAlias cloudAlias, kubeAPI kubeAPI, mode NodeSyncMode, nodeName string, set *cidrset.CidrSet) *NodeSync {
	return &NodeSync{
		c:          c,
		cloudAlias: cloudAlias,
		kubeAPI:    kubeAPI,
		mode:       mode,
		nodeName:   nodeName,
		opChan:     make(chan syncOp, 1),
		set:        set,
	}
}

// Loop runs the sync loop for a given node. done is an optional channel that
// is closed when the Loop() returns.
func (sync *NodeSync) Loop(done chan struct{}) {
	glog.V(2).Infof("Starting sync loop for node %q", sync.nodeName)

	defer func() {
		if done != nil {
			close(done)
		}
	}()

	timeout := sync.c.ResyncTimeout()
	delayTimer := time.NewTimer(timeout)
	glog.V(4).Infof("Resync node %q in %v", sync.nodeName, timeout)

	for {
		select {
		case op, more := <-sync.opChan:
			if !more {
				glog.V(2).Infof("Stopping sync loop")
				return
			}
			sync.c.ReportResult(op.run(sync))
			if !delayTimer.Stop() {
				<-delayTimer.C
			}
		case <-delayTimer.C:
			glog.V(4).Infof("Running resync for node %q", sync.nodeName)
			sync.c.ReportResult((&updateOp{}).run(sync))
		}

		timeout := sync.c.ResyncTimeout()
		delayTimer.Reset(timeout)
		glog.V(4).Infof("Resync node %q in %v", sync.nodeName, timeout)
	}
}

// Update causes an update operation on the given node. If node is nil, then
// the syncer will fetch the node spec from the API server before syncing.
//
// This method is safe to call from multiple goroutines.
func (sync *NodeSync) Update(node *v1.Node) {
	sync.opChan <- &updateOp{node}
}

// Delete performs the sync operations necessary to remove the node from the
// IPAM state.
//
// This method is safe to call from multiple goroutines.
func (sync *NodeSync) Delete(node *v1.Node) {
	sync.opChan <- &deleteOp{node}
	close(sync.opChan)
}

// syncOp is the interface for generic sync operation.
type syncOp interface {
	// run the requested sync operation.
	run(sync *NodeSync) error
}

// updateOp handles creation and updates of a node.
type updateOp struct {
	node *v1.Node
}

func (op *updateOp) String() string {
	if op.node == nil {
		return fmt.Sprintf("updateOp(nil)")
	}
	return fmt.Sprintf("updateOp(%q,%v)", op.node.Name, op.node.Spec.PodCIDR)
}

func (op *updateOp) run(sync *NodeSync) error {
	glog.V(3).Infof("Running updateOp %+v", op)

	ctx := context.Background()

	if op.node == nil {
		glog.V(3).Infof("Getting node spec for %q", sync.nodeName)
		node, err := sync.kubeAPI.Node(ctx, sync.nodeName)
		if err != nil {
			glog.Errorf("Error getting node %q spec: %v", sync.nodeName, err)
			return err
		}
		op.node = node
	}

	aliasRange, err := sync.cloudAlias.Alias(ctx, sync.nodeName)
	if err != nil {
		glog.Errorf("Error getting cloud alias for node %q: %v", sync.nodeName, err)
		return err
	}

	switch {
	case op.node.Spec.PodCIDR == "" && aliasRange == nil:
		err = op.allocateRange(ctx, sync, op.node)
	case op.node.Spec.PodCIDR == "" && aliasRange != nil:
		err = op.updateNodeFromAlias(ctx, sync, op.node, aliasRange)
	case op.node.Spec.PodCIDR != "" && aliasRange == nil:
		err = op.updateAliasFromNode(ctx, sync, op.node)
	case op.node.Spec.PodCIDR != "" && aliasRange != nil:
		err = op.validateRange(ctx, sync, op.node, aliasRange)
	}

	return err
}

// validateRange checks that the allocated range and the alias range
// match.
func (op *updateOp) validateRange(ctx context.Context, sync *NodeSync, node *v1.Node, aliasRange *net.IPNet) error {
	if node.Spec.PodCIDR != aliasRange.String() {
		glog.Errorf("Inconsistency detected between node PodCIDR and node alias (%v != %v)",
			node.Spec.PodCIDR, aliasRange)
		sync.kubeAPI.EmitNodeWarningEvent(node.Name, MismatchEvent,
			"Node.Spec.PodCIDR != cloud alias (%v != %v)", node.Spec.PodCIDR, aliasRange)
		// User intervention is required in this case, as this is most likely due
		// to the user mucking around with their VM aliases on the side.
	} else {
		glog.V(4).Infof("Node %q CIDR range %v is matches cloud assignment", node.Name, node.Spec.PodCIDR)
	}
	return nil
}

// updateNodeFromAlias updates the the node from the cloud allocated
// alias.
func (op *updateOp) updateNodeFromAlias(ctx context.Context, sync *NodeSync, node *v1.Node, aliasRange *net.IPNet) error {
	if sync.mode != SyncFromCloud {
		glog.Warningf("Detect mode %q while expect to sync from cloud", sync.mode
	}

	glog.V(2).Infof("Updating node spec with alias range, node.PodCIDR = %v", aliasRange)

	if err := sync.set.Occupy(aliasRange); err != nil {
		glog.Errorf("Error occupying range %v for node %v", aliasRange, sync.nodeName)
		return err
	}

	if err := sync.kubeAPI.UpdateNodePodCIDR(ctx, node, aliasRange); err != nil {
		glog.Errorf("Could not update node %q PodCIDR to %v: %v", node.Name, aliasRange, err)
		return err
	}

	glog.V(2).Infof("Node %q PodCIDR set to %v", node.Name, aliasRange)

	if err := sync.kubeAPI.UpdateNodeNetworkUnavailable(node.Name, false); err != nil {
		glog.Errorf("Could not update node NetworkUnavailable status to false: %v", err)
		return err
	}

	glog.V(2).Infof("Updated node %q PodCIDR from cloud alias %v", node.Name, aliasRange)

	return nil
}

// updateAliasFromNode updates the cloud alias given the node allocation.
func (op *updateOp) updateAliasFromNode(ctx context.Context, sync *NodeSync, node *v1.Node) error {
	if sync.mode != SyncFromCluster {
		glog.Warningf("Detect mode %q while expect to sync from cluster", sync.mode)
	}

	_, aliasRange, err := net.ParseCIDR(node.Spec.PodCIDR)
	if err != nil {
		glog.Errorf("Could not parse PodCIDR (%q) for node %q: %v",
			node.Spec.PodCIDR, node.Name, err)
		return err
	}

	if err := sync.set.Occupy(aliasRange); err != nil {
		glog.Errorf("Error occupying range %v for node %v", aliasRange, sync.nodeName)
		return err
	}

	if err := sync.cloudAlias.AddAlias(ctx, node.Name, aliasRange); err != nil {
		glog.Errorf("Could not add alias %v for node %q: %v", aliasRange, node.Name, err)
		return err
	}

	if err := sync.kubeAPI.UpdateNodeNetworkUnavailable(node.Name, false); err != nil {
		glog.Errorf("Could not update node NetworkUnavailable status to false: %v", err)
		return err
	}

	glog.V(2).Infof("Updated node %q cloud alias with node spec, node.PodCIDR = %v",
		node.Name, node.Spec.PodCIDR)

	return nil
}

// allocateRange allocates a new range and updates both the cloud
// platform and the node allocation.
func (op *updateOp) allocateRange(ctx context.Context, sync *NodeSync, node *v1.Node) error {
	if sync.mode != SyncFromCluster {
		sync.kubeAPI.EmitNodeWarningEvent(node.Name, InvalidModeEvent,
			"Cannot allocate CIDRs in mode %q", sync.mode)
		return fmt.Errorf("controller cannot allocate CIDRS in mode %q", sync.mode)
	}

	cidrRange, err := sync.set.AllocateNext()
	if err != nil {
		return err
	}
	// If addAlias returns a hard error, cidrRange will be leaked as there
	// is no durable record of the range. The missing space will be
	// recovered on the next restart of the controller.
	if err := sync.cloudAlias.AddAlias(ctx, node.Name, cidrRange); err != nil {
		glog.Errorf("Could not add alias %v for node %q: %v", cidrRange, node.Name, err)
		return err
	}

	if err := sync.kubeAPI.UpdateNodePodCIDR(ctx, node, cidrRange); err != nil {
		glog.Errorf("Could not update node %q PodCIDR to %v: %v", node.Name, cidrRange, err)
		return err
	}

	if err := sync.kubeAPI.UpdateNodeNetworkUnavailable(node.Name, false); err != nil {
		glog.Errorf("Could not update node NetworkUnavailable status to false: %v", err)
		return err
	}

	glog.V(2).Infof("Allocated PodCIDR %v for node %q", cidrRange, node.Name)

	return nil
}

// deleteOp handles deletion of a node.
type deleteOp struct {
	node *v1.Node
}

func (op *deleteOp) String() string {
	if op.node == nil {
		return fmt.Sprintf("deleteOp(nil)")
	}
	return fmt.Sprintf("deleteOp(%q,%v)", op.node.Name, op.node.Spec.PodCIDR)
}

func (op *deleteOp) run(sync *NodeSync) error {
	glog.V(3).Infof("Running deleteOp %+v", op)
	if op.node.Spec.PodCIDR == "" {
		glog.V(2).Infof("Node %q was deleted, node had no PodCIDR range assigned", op.node.Name)
		return nil
	}

	_, cidrRange, err := net.ParseCIDR(op.node.Spec.PodCIDR)
	if err != nil {
		glog.Errorf("Deleted node %q has an invalid podCIDR %q: %v",
			op.node.Name, op.node.Spec.PodCIDR, err)
		sync.kubeAPI.EmitNodeWarningEvent(op.node.Name, InvalidPodCIDR,
			"Node %q has an invalid PodCIDR: %q", op.node.Name, op.node.Spec.PodCIDR)
		return nil
	}

	sync.set.Release(cidrRange)
	glog.V(2).Infof("Node %q was deleted, releasing CIDR range %v",
		op.node.Name, op.node.Spec.PodCIDR)

	return nil
}
<commit_msg>Add a missing ) to glog.Waringf.<commit_after>/*
Copyright 2017 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package sync

import (
	"context"
	"fmt"
	"net"
	"time"

	"github.com/golang/glog"

	"k8s.io/api/core/v1"
	"k8s.io/kubernetes/pkg/controller/node/ipam/cidrset"
)

const (
	// InvalidPodCIDR is the event recorded when a node is found with an
	// invalid PodCIDR.
	InvalidPodCIDR = "CloudCIDRAllocatorInvalidPodCIDR"
	// InvalidModeEvent is the event recorded when the CIDR range cannot be
	// sync'd due to the cluster running in the wrong mode.
	InvalidModeEvent = "CloudCIDRAllocatorInvalidMode"
	// MismatchEvent is the event recorded when the CIDR range allocated in the
	// node spec does not match what has been allocated in the cloud.
	MismatchEvent = "CloudCIDRAllocatorMismatch"
)

// cloudAlias is the interface to the cloud platform APIs.
type cloudAlias interface {
	// Alias returns the IP alias for the node.
	Alias(ctx context.Context, nodeName string) (*net.IPNet, error)
	// AddAlias adds an alias to the node.
	AddAlias(ctx context.Context, nodeName string, cidrRange *net.IPNet) error
}

// kubeAPI is the interface to the Kubernetes APIs.
type kubeAPI interface {
	// Node returns the spec for the Node object.
	Node(ctx context.Context, name string) (*v1.Node, error)
	// UpdateNodePodCIDR updates the PodCIDR in the Node spec.
	UpdateNodePodCIDR(ctx context.Context, node *v1.Node, cidrRange *net.IPNet) error
	// UpdateNodeNetworkUnavailable updates the network unavailable status for the node.
	UpdateNodeNetworkUnavailable(nodeName string, unavailable bool) error
	// EmitNodeWarningEvent emits an event for the given node.
	EmitNodeWarningEvent(nodeName, reason, fmt string, args ...interface{})
}

// controller is the interface to the controller.
type controller interface {
	// ReportResult updates the controller with the result of the latest
	// sync operation.
	ReportResult(err error)
	// ResyncTimeout returns the amount of time to wait before retrying
	// a sync with a node.
	ResyncTimeout() time.Duration
}

// NodeSyncMode is the mode the cloud CIDR allocator runs in.
type NodeSyncMode string

var (
	// SyncFromCloud is the mode that synchronizes the IP allocation from the cloud
	// platform to the node.
	SyncFromCloud NodeSyncMode = "SyncFromCloud"
	// SyncFromCluster is the mode that synchronizes the IP allocation determined
	// by the k8s controller to the cloud provider.
	SyncFromCluster NodeSyncMode = "SyncFromCluster"
)

// IsValidMode returns true if the given mode is valid.
func IsValidMode(m NodeSyncMode) bool {
	switch m {
	case SyncFromCloud:
	case SyncFromCluster:
	default:
		return false
	}
	return true
}

// NodeSync synchronizes the state for a single node in the cluster.
type NodeSync struct {
	c          controller
	cloudAlias cloudAlias
	kubeAPI    kubeAPI
	mode       NodeSyncMode
	nodeName   string
	opChan     chan syncOp
	set        *cidrset.CidrSet
}

// New returns a new syncer for a given node.
func New(c controller, cloudAlias cloudAlias, kubeAPI kubeAPI, mode NodeSyncMode, nodeName string, set *cidrset.CidrSet) *NodeSync {
	return &NodeSync{
		c:          c,
		cloudAlias: cloudAlias,
		kubeAPI:    kubeAPI,
		mode:       mode,
		nodeName:   nodeName,
		opChan:     make(chan syncOp, 1),
		set:        set,
	}
}

// Loop runs the sync loop for a given node. done is an optional channel that
// is closed when the Loop() returns.
func (sync *NodeSync) Loop(done chan struct{}) {
	glog.V(2).Infof("Starting sync loop for node %q", sync.nodeName)

	defer func() {
		if done != nil {
			close(done)
		}
	}()

	timeout := sync.c.ResyncTimeout()
	delayTimer := time.NewTimer(timeout)
	glog.V(4).Infof("Resync node %q in %v", sync.nodeName, timeout)

	for {
		select {
		case op, more := <-sync.opChan:
			if !more {
				glog.V(2).Infof("Stopping sync loop")
				return
			}
			sync.c.ReportResult(op.run(sync))
			if !delayTimer.Stop() {
				<-delayTimer.C
			}
		case <-delayTimer.C:
			glog.V(4).Infof("Running resync for node %q", sync.nodeName)
			sync.c.ReportResult((&updateOp{}).run(sync))
		}

		timeout := sync.c.ResyncTimeout()
		delayTimer.Reset(timeout)
		glog.V(4).Infof("Resync node %q in %v", sync.nodeName, timeout)
	}
}

// Update causes an update operation on the given node. If node is nil, then
// the syncer will fetch the node spec from the API server before syncing.
//
// This method is safe to call from multiple goroutines.
func (sync *NodeSync) Update(node *v1.Node) {
	sync.opChan <- &updateOp{node}
}

// Delete performs the sync operations necessary to remove the node from the
// IPAM state.
//
// This method is safe to call from multiple goroutines.
func (sync *NodeSync) Delete(node *v1.Node) {
	sync.opChan <- &deleteOp{node}
	close(sync.opChan)
}

// syncOp is the interface for generic sync operation.
type syncOp interface {
	// run the requested sync operation.
	run(sync *NodeSync) error
}

// updateOp handles creation and updates of a node.
type updateOp struct {
	node *v1.Node
}

func (op *updateOp) String() string {
	if op.node == nil {
		return fmt.Sprintf("updateOp(nil)")
	}
	return fmt.Sprintf("updateOp(%q,%v)", op.node.Name, op.node.Spec.PodCIDR)
}

func (op *updateOp) run(sync *NodeSync) error {
	glog.V(3).Infof("Running updateOp %+v", op)

	ctx := context.Background()

	if op.node == nil {
		glog.V(3).Infof("Getting node spec for %q", sync.nodeName)
		node, err := sync.kubeAPI.Node(ctx, sync.nodeName)
		if err != nil {
			glog.Errorf("Error getting node %q spec: %v", sync.nodeName, err)
			return err
		}
		op.node = node
	}

	aliasRange, err := sync.cloudAlias.Alias(ctx, sync.nodeName)
	if err != nil {
		glog.Errorf("Error getting cloud alias for node %q: %v", sync.nodeName, err)
		return err
	}

	switch {
	case op.node.Spec.PodCIDR == "" && aliasRange == nil:
		err = op.allocateRange(ctx, sync, op.node)
	case op.node.Spec.PodCIDR == "" && aliasRange != nil:
		err = op.updateNodeFromAlias(ctx, sync, op.node, aliasRange)
	case op.node.Spec.PodCIDR != "" && aliasRange == nil:
		err = op.updateAliasFromNode(ctx, sync, op.node)
	case op.node.Spec.PodCIDR != "" && aliasRange != nil:
		err = op.validateRange(ctx, sync, op.node, aliasRange)
	}

	return err
}

// validateRange checks that the allocated range and the alias range
// match.
func (op *updateOp) validateRange(ctx context.Context, sync *NodeSync, node *v1.Node, aliasRange *net.IPNet) error {
	if node.Spec.PodCIDR != aliasRange.String() {
		glog.Errorf("Inconsistency detected between node PodCIDR and node alias (%v != %v)",
			node.Spec.PodCIDR, aliasRange)
		sync.kubeAPI.EmitNodeWarningEvent(node.Name, MismatchEvent,
			"Node.Spec.PodCIDR != cloud alias (%v != %v)", node.Spec.PodCIDR, aliasRange)
		// User intervention is required in this case, as this is most likely due
		// to the user mucking around with their VM aliases on the side.
	} else {
		glog.V(4).Infof("Node %q CIDR range %v is matches cloud assignment", node.Name, node.Spec.PodCIDR)
	}
	return nil
}

// updateNodeFromAlias updates the the node from the cloud allocated
// alias.
func (op *updateOp) updateNodeFromAlias(ctx context.Context, sync *NodeSync, node *v1.Node, aliasRange *net.IPNet) error {
	if sync.mode != SyncFromCloud {
		glog.Warningf("Detect mode %q while expect to sync from cloud", sync.mode)
	}

	glog.V(2).Infof("Updating node spec with alias range, node.PodCIDR = %v", aliasRange)

	if err := sync.set.Occupy(aliasRange); err != nil {
		glog.Errorf("Error occupying range %v for node %v", aliasRange, sync.nodeName)
		return err
	}

	if err := sync.kubeAPI.UpdateNodePodCIDR(ctx, node, aliasRange); err != nil {
		glog.Errorf("Could not update node %q PodCIDR to %v: %v", node.Name, aliasRange, err)
		return err
	}

	glog.V(2).Infof("Node %q PodCIDR set to %v", node.Name, aliasRange)

	if err := sync.kubeAPI.UpdateNodeNetworkUnavailable(node.Name, false); err != nil {
		glog.Errorf("Could not update node NetworkUnavailable status to false: %v", err)
		return err
	}

	glog.V(2).Infof("Updated node %q PodCIDR from cloud alias %v", node.Name, aliasRange)

	return nil
}

// updateAliasFromNode updates the cloud alias given the node allocation.
func (op *updateOp) updateAliasFromNode(ctx context.Context, sync *NodeSync, node *v1.Node) error {
	if sync.mode != SyncFromCluster {
		glog.Warningf("Detect mode %q while expect to sync from cluster", sync.mode)
	}

	_, aliasRange, err := net.ParseCIDR(node.Spec.PodCIDR)
	if err != nil {
		glog.Errorf("Could not parse PodCIDR (%q) for node %q: %v",
			node.Spec.PodCIDR, node.Name, err)
		return err
	}

	if err := sync.set.Occupy(aliasRange); err != nil {
		glog.Errorf("Error occupying range %v for node %v", aliasRange, sync.nodeName)
		return err
	}

	if err := sync.cloudAlias.AddAlias(ctx, node.Name, aliasRange); err != nil {
		glog.Errorf("Could not add alias %v for node %q: %v", aliasRange, node.Name, err)
		return err
	}

	if err := sync.kubeAPI.UpdateNodeNetworkUnavailable(node.Name, false); err != nil {
		glog.Errorf("Could not update node NetworkUnavailable status to false: %v", err)
		return err
	}

	glog.V(2).Infof("Updated node %q cloud alias with node spec, node.PodCIDR = %v",
		node.Name, node.Spec.PodCIDR)

	return nil
}

// allocateRange allocates a new range and updates both the cloud
// platform and the node allocation.
func (op *updateOp) allocateRange(ctx context.Context, sync *NodeSync, node *v1.Node) error {
	if sync.mode != SyncFromCluster {
		sync.kubeAPI.EmitNodeWarningEvent(node.Name, InvalidModeEvent,
			"Cannot allocate CIDRs in mode %q", sync.mode)
		return fmt.Errorf("controller cannot allocate CIDRS in mode %q", sync.mode)
	}

	cidrRange, err := sync.set.AllocateNext()
	if err != nil {
		return err
	}
	// If addAlias returns a hard error, cidrRange will be leaked as there
	// is no durable record of the range. The missing space will be
	// recovered on the next restart of the controller.
	if err := sync.cloudAlias.AddAlias(ctx, node.Name, cidrRange); err != nil {
		glog.Errorf("Could not add alias %v for node %q: %v", cidrRange, node.Name, err)
		return err
	}

	if err := sync.kubeAPI.UpdateNodePodCIDR(ctx, node, cidrRange); err != nil {
		glog.Errorf("Could not update node %q PodCIDR to %v: %v", node.Name, cidrRange, err)
		return err
	}

	if err := sync.kubeAPI.UpdateNodeNetworkUnavailable(node.Name, false); err != nil {
		glog.Errorf("Could not update node NetworkUnavailable status to false: %v", err)
		return err
	}

	glog.V(2).Infof("Allocated PodCIDR %v for node %q", cidrRange, node.Name)

	return nil
}

// deleteOp handles deletion of a node.
type deleteOp struct {
	node *v1.Node
}

func (op *deleteOp) String() string {
	if op.node == nil {
		return fmt.Sprintf("deleteOp(nil)")
	}
	return fmt.Sprintf("deleteOp(%q,%v)", op.node.Name, op.node.Spec.PodCIDR)
}

func (op *deleteOp) run(sync *NodeSync) error {
	glog.V(3).Infof("Running deleteOp %+v", op)
	if op.node.Spec.PodCIDR == "" {
		glog.V(2).Infof("Node %q was deleted, node had no PodCIDR range assigned", op.node.Name)
		return nil
	}

	_, cidrRange, err := net.ParseCIDR(op.node.Spec.PodCIDR)
	if err != nil {
		glog.Errorf("Deleted node %q has an invalid podCIDR %q: %v",
			op.node.Name, op.node.Spec.PodCIDR, err)
		sync.kubeAPI.EmitNodeWarningEvent(op.node.Name, InvalidPodCIDR,
			"Node %q has an invalid PodCIDR: %q", op.node.Name, op.node.Spec.PodCIDR)
		return nil
	}

	sync.set.Release(cidrRange)
	glog.V(2).Infof("Node %q was deleted, releasing CIDR range %v",
		op.node.Name, op.node.Spec.PodCIDR)

	return nil
}
<|endoftext|>


<commit_before>// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 Authors of Cilium

package linuxrouting

import (
	"errors"
	"fmt"
	"net"

	"github.com/cilium/cilium/pkg/datapath/linux/linux_defaults"
	"github.com/cilium/cilium/pkg/datapath/linux/route"
	iputil "github.com/cilium/cilium/pkg/ip"
	ipamOption "github.com/cilium/cilium/pkg/ipam/option"
	"github.com/cilium/cilium/pkg/logging"
	"github.com/cilium/cilium/pkg/logging/logfields"
	"github.com/cilium/cilium/pkg/mac"
	"github.com/cilium/cilium/pkg/node"
	"github.com/cilium/cilium/pkg/option"

	"github.com/sirupsen/logrus"
	"github.com/vishvananda/netlink"
)

var (
	log = logging.DefaultLogger.WithField(logfields.LogSubsys, "linux-routing")
)

// Configure sets up the rules and routes needed when running in ENI or
// Azure IPAM mode.
// These rules and routes direct egress traffic out of the interface and
// ingress traffic back to the endpoint (`ip`). The compat flag controls which
// egress priority to consider when deleting the egress rules (see
// option.Config.EgressMultiHomeIPRuleCompat).
//
// ip: The endpoint IP address to direct traffic out / from interface.
// info: The interface routing info used to create rules and routes.
// mtu: The interface MTU.
func (info *RoutingInfo) Configure(ip net.IP, mtu int, compat bool) error {
	if ip.To4() == nil {
		log.WithFields(logrus.Fields{
			"endpointIP": ip,
		}).Warning("Unable to configure rules and routes because IP is not an IPv4 address")
		return errors.New("IP not compatible")
	}

	ifindex, err := retrieveIfIndexFromMAC(info.MasterIfMAC, mtu)
	if err != nil {
		return fmt.Errorf("unable to find ifindex for interface MAC: %s", err)
	}

	ipWithMask := net.IPNet{
		IP:   ip,
		Mask: net.CIDRMask(32, 32),
	}

	// On ingress, route all traffic to the endpoint IP via the main routing
	// table. Egress rules are created in a per-ENI routing table.
	if err := route.ReplaceRule(route.Rule{
		Priority: linux_defaults.RulePriorityIngress,
		To:       &ipWithMask,
		Table:    route.MainTable,
	}); err != nil {
		return fmt.Errorf("unable to install ip rule: %s", err)
	}

	var egressPriority, tableID int
	if compat {
		egressPriority = linux_defaults.RulePriorityEgress
		tableID = ifindex
	} else {
		egressPriority = linux_defaults.RulePriorityEgressv2
		tableID = computeTableIDFromIfaceNumber(info.InterfaceNumber)
	}

	if info.Masquerade {
		// Lookup a VPC specific table for all traffic from an endpoint to the
		// CIDR configured for the VPC on which the endpoint has the IP on.
		for _, cidr := range info.IPv4CIDRs {
			if err := route.ReplaceRule(route.Rule{
				Priority: egressPriority,
				From:     &ipWithMask,
				To:       &cidr,
				Table:    tableID,
			}); err != nil {
				return fmt.Errorf("unable to install ip rule: %s", err)
			}
		}
	} else {
		// Lookup a VPC specific table for all traffic from an endpoint.
		if err := route.ReplaceRule(route.Rule{
			Priority: egressPriority,
			From:     &ipWithMask,
			Table:    tableID,
		}); err != nil {
			return fmt.Errorf("unable to install ip rule: %s", err)
		}
	}

	// Nexthop route to the VPC or subnet gateway
	//
	// Note: This is a /32 route to avoid any L2. The endpoint does no L2
	// either.
	if err := netlink.RouteReplace(&netlink.Route{
		LinkIndex: ifindex,
		Dst:       &net.IPNet{IP: info.IPv4Gateway, Mask: net.CIDRMask(32, 32)},
		Scope:     netlink.SCOPE_LINK,
		Table:     tableID,
	}); err != nil {
		return fmt.Errorf("unable to add L2 nexthop route: %s", err)
	}

	// Default route to the VPC or subnet gateway
	if err := netlink.RouteReplace(&netlink.Route{
		Dst:   &net.IPNet{IP: net.IPv4zero, Mask: net.CIDRMask(0, 32)},
		Table: tableID,
		Gw:    info.IPv4Gateway,
	}); err != nil {
		return fmt.Errorf("unable to add L2 nexthop route: %s", err)
	}

	return nil
}

// Delete removes the ingress and egress rules that control traffic for
// endpoints. Note that the routes referenced by the rules are not deleted as
// they can be reused when another endpoint is created on the same node. The
// compat flag controls which egress priority to consider when deleting the
// egress rules (see option.Config.EgressMultiHomeIPRuleCompat).
//
// Note that one or more IPs may share the same route table, as identified by
// the interface number of the corresponding device. This function only removes
// the ingress and egress rules to disconnect the per-ENI egress routes from a
// specific local IP, and does not remove the corresponding route table as
// other IPs may still be using that table.
//
// The search for both the ingress & egress rule corresponding to this IP is a
// best-effort based on the respective priority that Cilium uses, which we
// assume full control over. The search for the ingress rule is more likely to
// succeed (albeit very rarely that egress deletion fails) because we are able
// to perform a narrower search on the rule because we know it references the
// main routing table. Due to multiple routing CIDRs, there might be more than
// one egress rule. Deletion of any rule only proceeds if the rule matches
// the IP & priority. If more than one rule matches, then deletion is skipped.
func Delete(ip net.IP, compat bool) error {
	if ip.To4() == nil {
		log.WithFields(logrus.Fields{
			"endpointIP": ip,
		}).Warning("Unable to delete rules because IP is not an IPv4 address")
		return errors.New("IP not compatible")
	}
	ipWithMask := net.IPNet{
		IP:   ip,
		Mask: net.CIDRMask(32, 32),
	}

	scopedLog := log.WithFields(logrus.Fields{
		"ip": ipWithMask.String(),
	})

	// Ingress rules
	ingress := route.Rule{
		Priority: linux_defaults.RulePriorityIngress,
		To:       &ipWithMask,
		Table:    route.MainTable,
	}
	if err := deleteRule(ingress); err != nil {
		return fmt.Errorf("unable to delete ingress rule from main table with ip %s: %v", ipWithMask.String(), err)
	}

	scopedLog.WithField("rule", ingress).Debug("Deleted ingress rule")

	priority := linux_defaults.RulePriorityEgressv2
	if compat {
		priority = linux_defaults.RulePriorityEgress
	}

	// Egress rules
	if info := node.GetRouterInfo(); info != nil && option.Config.IPAM == ipamOption.IPAMENI {
		ipv4CIDRs := info.GetIPv4CIDRs()
		cidrs := make([]*net.IPNet, 0, len(ipv4CIDRs))
		for i := range ipv4CIDRs {
			cidrs = append(cidrs, &ipv4CIDRs[i])
		}
		// Coalesce CIDRs into minimum set needed for route rules
		// This code here mirrors interfaceAdd() in cilium-cni/interface.go
		// and must be kept in sync when modified
		routingCIDRs, _ := iputil.CoalesceCIDRs(cidrs)
		for _, cidr := range routingCIDRs {
			egress := route.Rule{
				Priority: priority,
				From:     &ipWithMask,
				To:       cidr,
			}
			if err := deleteRule(egress); err != nil {
				return fmt.Errorf("unable to delete egress rule with ip %s: %w", ipWithMask.String(), err)
			}
			scopedLog.WithField(logfields.Rule, egress).Debug("Deleted egress rule")
		}
	} else {
		egress := route.Rule{
			Priority: priority,
			From:     &ipWithMask,
		}
		if err := deleteRule(egress); err != nil {
			return fmt.Errorf("unable to delete egress rule with ip %s: %w", ipWithMask.String(), err)
		}
		scopedLog.WithField(logfields.Rule, egress).Debug("Deleted egress rule")
	}

	return nil
}

// SetupRules installs routing rules based on the passed attributes. It accounts
// for option.Config.EgressMultiHomeIPRuleCompat while configuring the rules.
func SetupRules(from, to *net.IPNet, mac string, ifaceNum int) error {
	var (
		prio    int
		tableId int
	)

	if option.Config.EgressMultiHomeIPRuleCompat {
		prio = linux_defaults.RulePriorityEgress
		ifindex, err := retrieveIfaceIdxFromMAC(mac)
		if err != nil {
			return fmt.Errorf("unable to find ifindex for interface MAC: %w", err)
		}
		tableId = ifindex
	} else {
		prio = linux_defaults.RulePriorityEgressv2
		tableId = computeTableIDFromIfaceNumber(ifaceNum)
	}
	return route.ReplaceRule(route.Rule{
		Priority: prio,
		From:     from,
		To:       to,
		Table:    tableId,
	})
}

// RetrieveIfaceNameFromMAC finds the corresponding device name for a
// given MAC address.
func RetrieveIfaceNameFromMAC(mac string) (string, error) {
	iface, err := retrieveIfaceFromMAC(mac)
	if err != nil {
		err = fmt.Errorf("failed to get iface name with MAC %w", err)
		return "", err
	}
	return iface.Attrs().Name, nil
}

func deleteRule(r route.Rule) error {
	rules, err := route.ListRules(netlink.FAMILY_V4, &r)
	if err != nil {
		return err
	}

	length := len(rules)
	switch {
	case length > 1:
		log.WithFields(logrus.Fields{
			"candidates": rules,
			"rule":       r,
		}).Warning("Found too many rules matching, skipping deletion")
		return errors.New("unexpected number of rules found to delete")
	case length == 1:
		return route.DeleteRule(r)
	}

	log.WithFields(logrus.Fields{
		"rule": r,
	}).Warning("No rule matching found")

	return errors.New("no rule found to delete")
}

// retrieveIfIndexFromMAC finds the corresponding device index (ifindex) for a
// given MAC address, excluding Linux slave devices. This is useful for
// creating rules and routes in order to specify the table. When the ifindex is
// found, the device is brought up and its MTU is set.
func retrieveIfIndexFromMAC(mac mac.MAC, mtu int) (int, error) {
	var link netlink.Link

	links, err := netlink.LinkList()
	if err != nil {
		return -1, fmt.Errorf("unable to list interfaces: %w", err)
	}

	for _, l := range links {
		// Linux slave devices have the same MAC address as their master
		// device, but we want the master device.
		if l.Attrs().Slave != nil {
			continue
		}
		if l.Attrs().HardwareAddr.String() == mac.String() {
			if link != nil {
				return -1, fmt.Errorf("several interfaces found with MAC %s: %s and %s", mac, link.Attrs().Name, l.Attrs().Name)
			}
			link = l
		}
	}

	if link == nil {
		return -1, fmt.Errorf("interface with MAC %s not found", mac)
	}

	if err = netlink.LinkSetMTU(link, mtu); err != nil {
		return -1, fmt.Errorf("unable to change MTU of link %s to %d: %w", link.Attrs().Name, mtu, err)
	}
	if err = netlink.LinkSetUp(link); err != nil {
		return -1, fmt.Errorf("unable to up link %s: %w", link.Attrs().Name, err)
	}

	return link.Attrs().Index, nil
}

// computeTableIDFromIfaceNumber returns a computed per-ENI route table ID for the given
// ENI interface number.
func computeTableIDFromIfaceNumber(num int) int {
	return linux_defaults.RouteTableInterfacesOffset + num
}

// retrieveIfaceIdxFromMAC finds the corresponding interface index for a
// given MAC address.
// It returns -1 as the index for error conditions.
func retrieveIfaceIdxFromMAC(mac string) (int, error) {
	iface, err := retrieveIfaceFromMAC(mac)
	if err != nil {
		err = fmt.Errorf("failed to get iface index with MAC %w", err)
		return -1, err
	}
	return iface.Attrs().Index, nil
}

// retrieveIfaceFromFromMAC finds the corresponding interface for a
// given MAC address.
func retrieveIfaceFromMAC(mac string) (link netlink.Link, err error) {
	var links []netlink.Link

	links, err = netlink.LinkList()
	if err != nil {
		err = fmt.Errorf("unable to list interfaces: %w", err)
		return
	}
	for _, l := range links {
		if l.Attrs().HardwareAddr.String() == mac {
			link = l
			return
		}
	}

	err = fmt.Errorf("interface with MAC not found")
	return
}
<commit_msg>routing: Fix incorrect detection of Linux slave devices<commit_after>// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 Authors of Cilium

package linuxrouting

import (
	"errors"
	"fmt"
	"net"

	"github.com/cilium/cilium/pkg/datapath/linux/linux_defaults"
	"github.com/cilium/cilium/pkg/datapath/linux/route"
	iputil "github.com/cilium/cilium/pkg/ip"
	ipamOption "github.com/cilium/cilium/pkg/ipam/option"
	"github.com/cilium/cilium/pkg/logging"
	"github.com/cilium/cilium/pkg/logging/logfields"
	"github.com/cilium/cilium/pkg/mac"
	"github.com/cilium/cilium/pkg/node"
	"github.com/cilium/cilium/pkg/option"

	"github.com/sirupsen/logrus"
	"github.com/vishvananda/netlink"
	"golang.org/x/sys/unix"
)

var (
	log = logging.DefaultLogger.WithField(logfields.LogSubsys, "linux-routing")
)

// Configure sets up the rules and routes needed when running in ENI or
// Azure IPAM mode.
// These rules and routes direct egress traffic out of the interface and
// ingress traffic back to the endpoint (`ip`). The compat flag controls which
// egress priority to consider when deleting the egress rules (see
// option.Config.EgressMultiHomeIPRuleCompat).
//
// ip: The endpoint IP address to direct traffic out / from interface.
// info: The interface routing info used to create rules and routes.
// mtu: The interface MTU.
func (info *RoutingInfo) Configure(ip net.IP, mtu int, compat bool) error {
	if ip.To4() == nil {
		log.WithFields(logrus.Fields{
			"endpointIP": ip,
		}).Warning("Unable to configure rules and routes because IP is not an IPv4 address")
		return errors.New("IP not compatible")
	}

	ifindex, err := retrieveIfIndexFromMAC(info.MasterIfMAC, mtu)
	if err != nil {
		return fmt.Errorf("unable to find ifindex for interface MAC: %s", err)
	}

	ipWithMask := net.IPNet{
		IP:   ip,
		Mask: net.CIDRMask(32, 32),
	}

	// On ingress, route all traffic to the endpoint IP via the main routing
	// table. Egress rules are created in a per-ENI routing table.
	if err := route.ReplaceRule(route.Rule{
		Priority: linux_defaults.RulePriorityIngress,
		To:       &ipWithMask,
		Table:    route.MainTable,
	}); err != nil {
		return fmt.Errorf("unable to install ip rule: %s", err)
	}

	var egressPriority, tableID int
	if compat {
		egressPriority = linux_defaults.RulePriorityEgress
		tableID = ifindex
	} else {
		egressPriority = linux_defaults.RulePriorityEgressv2
		tableID = computeTableIDFromIfaceNumber(info.InterfaceNumber)
	}

	if info.Masquerade {
		// Lookup a VPC specific table for all traffic from an endpoint to the
		// CIDR configured for the VPC on which the endpoint has the IP on.
		for _, cidr := range info.IPv4CIDRs {
			if err := route.ReplaceRule(route.Rule{
				Priority: egressPriority,
				From:     &ipWithMask,
				To:       &cidr,
				Table:    tableID,
			}); err != nil {
				return fmt.Errorf("unable to install ip rule: %s", err)
			}
		}
	} else {
		// Lookup a VPC specific table for all traffic from an endpoint.
		if err := route.ReplaceRule(route.Rule{
			Priority: egressPriority,
			From:     &ipWithMask,
			Table:    tableID,
		}); err != nil {
			return fmt.Errorf("unable to install ip rule: %s", err)
		}
	}

	// Nexthop route to the VPC or subnet gateway
	//
	// Note: This is a /32 route to avoid any L2. The endpoint does no L2
	// either.
	if err := netlink.RouteReplace(&netlink.Route{
		LinkIndex: ifindex,
		Dst:       &net.IPNet{IP: info.IPv4Gateway, Mask: net.CIDRMask(32, 32)},
		Scope:     netlink.SCOPE_LINK,
		Table:     tableID,
	}); err != nil {
		return fmt.Errorf("unable to add L2 nexthop route: %s", err)
	}

	// Default route to the VPC or subnet gateway
	if err := netlink.RouteReplace(&netlink.Route{
		Dst:   &net.IPNet{IP: net.IPv4zero, Mask: net.CIDRMask(0, 32)},
		Table: tableID,
		Gw:    info.IPv4Gateway,
	}); err != nil {
		return fmt.Errorf("unable to add L2 nexthop route: %s", err)
	}

	return nil
}

// Delete removes the ingress and egress rules that control traffic for
// endpoints. Note that the routes referenced by the rules are not deleted as
// they can be reused when another endpoint is created on the same node. The
// compat flag controls which egress priority to consider when deleting the
// egress rules (see option.Config.EgressMultiHomeIPRuleCompat).
//
// Note that one or more IPs may share the same route table, as identified by
// the interface number of the corresponding device. This function only removes
// the ingress and egress rules to disconnect the per-ENI egress routes from a
// specific local IP, and does not remove the corresponding route table as
// other IPs may still be using that table.
//
// The search for both the ingress & egress rule corresponding to this IP is a
// best-effort based on the respective priority that Cilium uses, which we
// assume full control over. The search for the ingress rule is more likely to
// succeed (albeit very rarely that egress deletion fails) because we are able
// to perform a narrower search on the rule because we know it references the
// main routing table. Due to multiple routing CIDRs, there might be more than
// one egress rule. Deletion of any rule only proceeds if the rule matches
// the IP & priority. If more than one rule matches, then deletion is skipped.
func Delete(ip net.IP, compat bool) error {
	if ip.To4() == nil {
		log.WithFields(logrus.Fields{
			"endpointIP": ip,
		}).Warning("Unable to delete rules because IP is not an IPv4 address")
		return errors.New("IP not compatible")
	}
	ipWithMask := net.IPNet{
		IP:   ip,
		Mask: net.CIDRMask(32, 32),
	}

	scopedLog := log.WithFields(logrus.Fields{
		"ip": ipWithMask.String(),
	})

	// Ingress rules
	ingress := route.Rule{
		Priority: linux_defaults.RulePriorityIngress,
		To:       &ipWithMask,
		Table:    route.MainTable,
	}
	if err := deleteRule(ingress); err != nil {
		return fmt.Errorf("unable to delete ingress rule from main table with ip %s: %v", ipWithMask.String(), err)
	}

	scopedLog.WithField("rule", ingress).Debug("Deleted ingress rule")

	priority := linux_defaults.RulePriorityEgressv2
	if compat {
		priority = linux_defaults.RulePriorityEgress
	}

	// Egress rules
	if info := node.GetRouterInfo(); info != nil && option.Config.IPAM == ipamOption.IPAMENI {
		ipv4CIDRs := info.GetIPv4CIDRs()
		cidrs := make([]*net.IPNet, 0, len(ipv4CIDRs))
		for i := range ipv4CIDRs {
			cidrs = append(cidrs, &ipv4CIDRs[i])
		}
		// Coalesce CIDRs into minimum set needed for route rules
		// This code here mirrors interfaceAdd() in cilium-cni/interface.go
		// and must be kept in sync when modified
		routingCIDRs, _ := iputil.CoalesceCIDRs(cidrs)
		for _, cidr := range routingCIDRs {
			egress := route.Rule{
				Priority: priority,
				From:     &ipWithMask,
				To:       cidr,
			}
			if err := deleteRule(egress); err != nil {
				return fmt.Errorf("unable to delete egress rule with ip %s: %w", ipWithMask.String(), err)
			}
			scopedLog.WithField(logfields.Rule, egress).Debug("Deleted egress rule")
		}
	} else {
		egress := route.Rule{
			Priority: priority,
			From:     &ipWithMask,
		}
		if err := deleteRule(egress); err != nil {
			return fmt.Errorf("unable to delete egress rule with ip %s: %w", ipWithMask.String(), err)
		}
		scopedLog.WithField(logfields.Rule, egress).Debug("Deleted egress rule")
	}

	return nil
}

// SetupRules installs routing rules based on the passed attributes. It accounts
// for option.Config.EgressMultiHomeIPRuleCompat while configuring the rules.
func SetupRules(from, to *net.IPNet, mac string, ifaceNum int) error {
	var (
		prio    int
		tableId int
	)

	if option.Config.EgressMultiHomeIPRuleCompat {
		prio = linux_defaults.RulePriorityEgress
		ifindex, err := retrieveIfaceIdxFromMAC(mac)
		if err != nil {
			return fmt.Errorf("unable to find ifindex for interface MAC: %w", err)
		}
		tableId = ifindex
	} else {
		prio = linux_defaults.RulePriorityEgressv2
		tableId = computeTableIDFromIfaceNumber(ifaceNum)
	}
	return route.ReplaceRule(route.Rule{
		Priority: prio,
		From:     from,
		To:       to,
		Table:    tableId,
	})
}

// RetrieveIfaceNameFromMAC finds the corresponding device name for a
// given MAC address.
func RetrieveIfaceNameFromMAC(mac string) (string, error) {
	iface, err := retrieveIfaceFromMAC(mac)
	if err != nil {
		err = fmt.Errorf("failed to get iface name with MAC %w", err)
		return "", err
	}
	return iface.Attrs().Name, nil
}

func deleteRule(r route.Rule) error {
	rules, err := route.ListRules(netlink.FAMILY_V4, &r)
	if err != nil {
		return err
	}

	length := len(rules)
	switch {
	case length > 1:
		log.WithFields(logrus.Fields{
			"candidates": rules,
			"rule":       r,
		}).Warning("Found too many rules matching, skipping deletion")
		return errors.New("unexpected number of rules found to delete")
	case length == 1:
		return route.DeleteRule(r)
	}

	log.WithFields(logrus.Fields{
		"rule": r,
	}).Warning("No rule matching found")

	return errors.New("no rule found to delete")
}

// retrieveIfIndexFromMAC finds the corresponding device index (ifindex) for a
// given MAC address, excluding Linux slave devices. This is useful for
// creating rules and routes in order to specify the table. When the ifindex is
// found, the device is brought up and its MTU is set.
func retrieveIfIndexFromMAC(mac mac.MAC, mtu int) (int, error) {
	var link netlink.Link

	links, err := netlink.LinkList()
	if err != nil {
		return -1, fmt.Errorf("unable to list interfaces: %w", err)
	}

	for _, l := range links {
		// Linux slave devices have the same MAC address as their master
		// device, but we want the master device.
		if l.Attrs().RawFlags&unix.IFF_SLAVE != 0 {
			continue
		}
		if l.Attrs().HardwareAddr.String() == mac.String() {
			if link != nil {
				return -1, fmt.Errorf("several interfaces found with MAC %s: %s and %s", mac, link.Attrs().Name, l.Attrs().Name)
			}
			link = l
		}
	}

	if link == nil {
		return -1, fmt.Errorf("interface with MAC %s not found", mac)
	}

	if err = netlink.LinkSetMTU(link, mtu); err != nil {
		return -1, fmt.Errorf("unable to change MTU of link %s to %d: %w", link.Attrs().Name, mtu, err)
	}
	if err = netlink.LinkSetUp(link); err != nil {
		return -1, fmt.Errorf("unable to up link %s: %w", link.Attrs().Name, err)
	}

	return link.Attrs().Index, nil
}

// computeTableIDFromIfaceNumber returns a computed per-ENI route table ID for the given
// ENI interface number.
func computeTableIDFromIfaceNumber(num int) int {
	return linux_defaults.RouteTableInterfacesOffset + num
}

// retrieveIfaceIdxFromMAC finds the corresponding interface index for a
// given MAC address.
// It returns -1 as the index for error conditions.
func retrieveIfaceIdxFromMAC(mac string) (int, error) {
	iface, err := retrieveIfaceFromMAC(mac)
	if err != nil {
		err = fmt.Errorf("failed to get iface index with MAC %w", err)
		return -1, err
	}
	return iface.Attrs().Index, nil
}

// retrieveIfaceFromFromMAC finds the corresponding interface for a
// given MAC address.
func retrieveIfaceFromMAC(mac string) (link netlink.Link, err error) {
	var links []netlink.Link

	links, err = netlink.LinkList()
	if err != nil {
		err = fmt.Errorf("unable to list interfaces: %w", err)
		return
	}
	for _, l := range links {
		if l.Attrs().HardwareAddr.String() == mac {
			link = l
			return
		}
	}

	err = fmt.Errorf("interface with MAC not found")
	return
}
<|endoftext|>