mirror of
https://github.com/openfaas/faasd.git
synced 2025-06-08 16:06:47 +00:00
262 lines
6.9 KiB
Go
262 lines
6.9 KiB
Go
package cninetwork
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"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 := os.WriteFile(netConfig, []byte(defaultCNIConf), 0644); 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 container name and PID
|
|
func GetIPAddress(container string, PID uint32) (string, error) {
|
|
CNIDir := path.Join(CNIDataDir, defaultNetworkName)
|
|
|
|
files, err := os.ReadDir(CNIDir)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to read CNI dir for container %s: %v", container, err)
|
|
}
|
|
|
|
for _, file := range files {
|
|
// each fileName is an IP address
|
|
fileName := file.Name()
|
|
|
|
resultsFile := filepath.Join(CNIDir, fileName)
|
|
found, err := isCNIResultForPID(resultsFile, container, PID)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if found {
|
|
return fileName, nil
|
|
}
|
|
}
|
|
|
|
return "", fmt.Errorf("unable to get IP address for container: %s", container)
|
|
}
|
|
|
|
// isCNIResultForPID confirms if the CNI result file contains the
|
|
// process name, PID and interface name
|
|
//
|
|
// Example:
|
|
//
|
|
// /var/run/cni/openfaas-cni-bridge/10.62.0.2
|
|
//
|
|
// nats-621
|
|
// eth1
|
|
func isCNIResultForPID(fileName, container string, PID uint32) (bool, error) {
|
|
found := false
|
|
|
|
f, err := os.Open(fileName)
|
|
if err != nil {
|
|
return false, fmt.Errorf("failed to open CNI IP file for %s: %v", fileName, err)
|
|
}
|
|
defer f.Close()
|
|
|
|
reader := bufio.NewReader(f)
|
|
processLine, _ := reader.ReadString('\n')
|
|
if strings.Contains(processLine, fmt.Sprintf("%s-%d", container, PID)) {
|
|
ethNameLine, _ := reader.ReadString('\n')
|
|
if strings.Contains(ethNameLine, defaultIfPrefix) {
|
|
found = true
|
|
}
|
|
}
|
|
|
|
return found, nil
|
|
}
|
|
|
|
// 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
|
|
}
|