mirror of
https://github.com/openfaas/faasd.git
synced 2025-06-10 00:46:47 +00:00
This is an optimization that uses the results cache created by CNI on the filesystem to store and fetch IP addresses for containers in the core services and for functions. As part of the change, the dependency on the syscall code from Weave net has been removed, and the code should compile on MacOS again. Updates and rebases the work in #38 by carlosedp Tested in the original PR, further testing in the incoming PR. Signed-off-by: Alex Ellis (OpenFaaS Ltd) <alexellis2@gmail.com>
235 lines
6.4 KiB
Go
235 lines
6.4 KiB
Go
package cninetwork
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"log"
|
|
"net"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/containerd/containerd"
|
|
gocni "github.com/containerd/go-cni"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
const (
|
|
// CNIBinDir describes the directory where the CNI binaries are stored
|
|
CNIBinDir = "/opt/cni/bin"
|
|
|
|
// CNIConfDir describes the directory where the CNI plugin's configuration is stored
|
|
CNIConfDir = "/etc/cni/net.d"
|
|
|
|
// NetNSPathFmt gives the path to the a process network namespace, given the pid
|
|
NetNSPathFmt = "/proc/%d/ns/net"
|
|
|
|
// CNIDataDir is the directory CNI stores allocated IP for containers
|
|
CNIDataDir = "/var/run/cni"
|
|
|
|
// defaultCNIConfFilename is the vanity filename of default CNI configuration file
|
|
defaultCNIConfFilename = "10-openfaas.conflist"
|
|
|
|
// defaultNetworkName names the "docker-bridge"-like CNI plugin-chain installed when no other CNI configuration is present.
|
|
// This value appears in iptables comments created by CNI.
|
|
defaultNetworkName = "openfaas-cni-bridge"
|
|
|
|
// defaultBridgeName is the default bridge device name used in the defaultCNIConf
|
|
defaultBridgeName = "openfaas0"
|
|
|
|
// defaultSubnet is the default subnet used in the defaultCNIConf -- this value is set to not collide with common container networking subnets:
|
|
defaultSubnet = "10.62.0.0/16"
|
|
|
|
// defaultIfPrefix is the interface name to be created in the container
|
|
defaultIfPrefix = "eth"
|
|
)
|
|
|
|
// defaultCNIConf is a CNI configuration that enables network access to containers (docker-bridge style)
|
|
var defaultCNIConf = fmt.Sprintf(`
|
|
{
|
|
"cniVersion": "0.4.0",
|
|
"name": "%s",
|
|
"plugins": [
|
|
{
|
|
"type": "bridge",
|
|
"bridge": "%s",
|
|
"isGateway": true,
|
|
"ipMasq": true,
|
|
"ipam": {
|
|
"type": "host-local",
|
|
"subnet": "%s",
|
|
"dataDir": "%s",
|
|
"routes": [
|
|
{ "dst": "0.0.0.0/0" }
|
|
]
|
|
}
|
|
},
|
|
{
|
|
"type": "firewall"
|
|
}
|
|
]
|
|
}
|
|
`, defaultNetworkName, defaultBridgeName, defaultSubnet, CNIDataDir)
|
|
|
|
// InitNetwork writes configlist file and initializes CNI network
|
|
func InitNetwork() (gocni.CNI, error) {
|
|
|
|
log.Printf("Writing network config...\n")
|
|
if !dirExists(CNIConfDir) {
|
|
if err := os.MkdirAll(CNIConfDir, 0755); err != nil {
|
|
return nil, fmt.Errorf("cannot create directory: %s", CNIConfDir)
|
|
}
|
|
}
|
|
|
|
netConfig := path.Join(CNIConfDir, defaultCNIConfFilename)
|
|
if err := ioutil.WriteFile(netConfig, []byte(defaultCNIConf), 644); err != nil {
|
|
return nil, fmt.Errorf("cannot write network config: %s", defaultCNIConfFilename)
|
|
}
|
|
|
|
// Initialize CNI library
|
|
cni, err := gocni.New(
|
|
gocni.WithPluginConfDir(CNIConfDir),
|
|
gocni.WithPluginDir([]string{CNIBinDir}),
|
|
gocni.WithInterfacePrefix(defaultIfPrefix),
|
|
)
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error initializing cni: %s", err)
|
|
}
|
|
|
|
// Load the cni configuration
|
|
if err := cni.Load(gocni.WithLoNetwork, gocni.WithConfListFile(filepath.Join(CNIConfDir, defaultCNIConfFilename))); err != nil {
|
|
return nil, fmt.Errorf("failed to load cni configuration: %v", err)
|
|
}
|
|
|
|
return cni, nil
|
|
}
|
|
|
|
// CreateCNINetwork creates a CNI network interface and attaches it to the context
|
|
func CreateCNINetwork(ctx context.Context, cni gocni.CNI, task containerd.Task, labels map[string]string) (*gocni.CNIResult, error) {
|
|
id := netID(task)
|
|
netns := netNamespace(task)
|
|
result, err := cni.Setup(ctx, id, netns, gocni.WithLabels(labels))
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "Failed to setup network for task %q: %v", id, err)
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// DeleteCNINetwork deletes a CNI network based on task ID and Pid
|
|
func DeleteCNINetwork(ctx context.Context, cni gocni.CNI, client *containerd.Client, name string) error {
|
|
container, containerErr := client.LoadContainer(ctx, name)
|
|
if containerErr == nil {
|
|
task, err := container.Task(ctx, nil)
|
|
if err != nil {
|
|
log.Printf("[Delete] unable to find task for container: %s\n", name)
|
|
return nil
|
|
}
|
|
|
|
log.Printf("[Delete] removing CNI network for: %s\n", task.ID())
|
|
|
|
id := netID(task)
|
|
netns := netNamespace(task)
|
|
|
|
if err := cni.Remove(ctx, id, netns); err != nil {
|
|
return errors.Wrapf(err, "Failed to remove network for task: %q, %v", id, err)
|
|
}
|
|
log.Printf("[Delete] removed: %s from namespace: %s, ID: %s\n", name, netns, id)
|
|
|
|
return nil
|
|
}
|
|
|
|
return errors.Wrapf(containerErr, "Unable to find container: %s, error: %s", name, containerErr)
|
|
}
|
|
|
|
// GetIPAddress returns the IP address from container based on name and PID
|
|
func GetIPAddress(name string, PID uint32) (string, error) {
|
|
processName := fmt.Sprintf("%s-%d", name, PID)
|
|
CNIDir := path.Join(CNIDataDir, defaultNetworkName)
|
|
|
|
files, err := ioutil.ReadDir(CNIDir)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to read CNI dir for container %s: %v", name, err)
|
|
}
|
|
|
|
for _, file := range files {
|
|
f, err := os.Open(filepath.Join(CNIDir, file.Name()))
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to open CNI IP file for %s/%s: %v", CNIDir, file.Name(), err)
|
|
}
|
|
reader := bufio.NewReader(f)
|
|
text, _ := reader.ReadString('\n')
|
|
if strings.Contains(text, processName) {
|
|
i, _ := reader.ReadString('\n')
|
|
if strings.Contains(i, defaultIfPrefix) {
|
|
f.Close()
|
|
return file.Name(), nil
|
|
}
|
|
}
|
|
f.Close()
|
|
}
|
|
return "", fmt.Errorf("unable to get IP address for container %s", name)
|
|
}
|
|
|
|
// CNIGateway returns the gateway for default subnet
|
|
func CNIGateway() (string, error) {
|
|
ip, _, err := net.ParseCIDR(defaultSubnet)
|
|
if err != nil {
|
|
return "", fmt.Errorf("error formatting gateway for network %s", defaultSubnet)
|
|
}
|
|
ip = ip.To4()
|
|
ip[3] = 1
|
|
return ip.String(), nil
|
|
}
|
|
|
|
// netID generates the network IF based on task name and task PID
|
|
func netID(task containerd.Task) string {
|
|
return fmt.Sprintf("%s-%d", task.ID(), task.Pid())
|
|
}
|
|
|
|
// netNamespace generates the namespace path based on task PID.
|
|
func netNamespace(task containerd.Task) string {
|
|
return fmt.Sprintf(NetNSPathFmt, task.Pid())
|
|
}
|
|
|
|
func dirEmpty(dirname string) (isEmpty bool) {
|
|
if !dirExists(dirname) {
|
|
return
|
|
}
|
|
|
|
f, err := os.Open(dirname)
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer func() { _ = f.Close() }()
|
|
|
|
// If the first file is EOF, the directory is empty
|
|
if _, err = f.Readdir(1); err == io.EOF {
|
|
isEmpty = true
|
|
}
|
|
return isEmpty
|
|
}
|
|
|
|
func dirExists(dirname string) bool {
|
|
exists, info := pathExists(dirname)
|
|
if !exists {
|
|
return false
|
|
}
|
|
|
|
return info.IsDir()
|
|
}
|
|
|
|
func pathExists(path string) (bool, os.FileInfo) {
|
|
info, err := os.Stat(path)
|
|
if os.IsNotExist(err) {
|
|
return false, nil
|
|
}
|
|
|
|
return true, info
|
|
}
|