github.com/aporeto-inc/trireme-lib@v10.358.0+incompatible/utils/cri/cri_client_setup_linux.go (about)

     1  // +build linux
     2  
     3  package cri
     4  
     5  import (
     6  	"context"
     7  	"fmt"
     8  	"net"
     9  	"net/url"
    10  	"os"
    11  	"path"
    12  	"regexp"
    13  	"strings"
    14  
    15  	"go.aporeto.io/enforcerd/internal/utils"
    16  	"go.uber.org/zap"
    17  	"google.golang.org/grpc"
    18  	criruntimev1alpha2 "k8s.io/cri-api/pkg/apis/runtime/v1alpha2"
    19  )
    20  
    21  // These are defined like this by Kubernetes. The kubelet will search for them exactly like this.
    22  const (
    23  	criDockerShimEndpoint = "/var/run/dockershim.sock"
    24  	criContainerdEndpoint = "/run/containerd/containerd.sock"
    25  	criCrioEndpoint       = "/var/run/crio/crio.sock"
    26  )
    27  
    28  var (
    29  	nopFunc = func(path string) string { return path }
    30  
    31  	// the following function was earlier utils.GetPathOnHostViaProcRoot but bow that we have proper mounts
    32  	// we will make it a NOP operation.
    33  	getHostPath = nopFunc
    34  )
    35  
    36  // ParseStringFlag parses a flag from a given command
    37  func ParseStringFlag(cmd string, flagRegexp string) *string {
    38  	flags := ParseStringFlags(cmd, flagRegexp)
    39  	if len(flags) > 0 {
    40  		return &flags[0]
    41  	}
    42  	return nil
    43  }
    44  
    45  // flagTemplate captures CLI flags (i.e., 'cmd --some-flag=value', 'cmd --some-flag value', 'cmd --some-flag=valA --some-flag=valB')
    46  const flagTemplate = `(?:%s)(?:=|\s+)(\S+)`
    47  
    48  // ParseStringFlags parses a list of flags from a given command
    49  func ParseStringFlags(cmd string, flagRegexp string) []string {
    50  	var res []string
    51  	expression := fmt.Sprintf(flagTemplate, flagRegexp)
    52  	matches := regexp.MustCompile(expression).FindAllStringSubmatch(cmd, -1)
    53  	for _, tokens := range matches {
    54  		if len(tokens) > 1 {
    55  			res = append(res, strings.Trim(tokens[1], `"'`))
    56  		}
    57  	}
    58  	return res
    59  }
    60  
    61  // BuildProcessRegex returns a regex that should match processes with a name matching the given process regular
    62  // expression
    63  // Remark: procExpression can be a regular expression
    64  func BuildProcessRegex(procExpression string) *regexp.Regexp {
    65  	// Expressions that should be matched by a given procname are:
    66  	// procname -flag1 -flag2
    67  	// /bin/procname -flag1 -flag2
    68  	// /procname -flag1 -flag2
    69  	//
    70  	// Expressions that should NOT be matched are:
    71  	// notprocname -flag1 -flag2
    72  	// /bin/notprocname -flag1 -flag2
    73  	// /bin/procname/notprocname -flag1 -flag2
    74  	// notprocname -flag1 procname
    75  	// notprocname -flag1 -procname
    76  	return regexp.MustCompile(fmt.Sprintf(`^(\S*/)?(%s)( |$)`, procExpression))
    77  }
    78  
    79  // KubeletProcessRegex is the kubelet process regex used to find the kubelet process
    80  // Sometimes it is not the kubelet binary that is used in the system (e.g. Openshift4) but k8s' all-in-one binary: https://github.com/kubernetes/kubernetes/tree/master/cluster/images/hyperkube
    81  // The following is an example of a kubelet cmdline in Openshift4:
    82  // /usr/bin/hyperkube kubelet --config=/etc/kubernetes/kubelet.conf --bootstrap-kubeconfig=/etc/kubernete s/kubeconfig --rotate-certificates --kubeconfig=/var/lib/kubelet/kubeconfig --container-runtime=remote --container-runtime-endpoint=/var/run/crio/crio.s ock --allow-privileged --node-labels=node-role.kubernetes.io/master --minimum-container-ttl-duration=6m0s --client-ca-file=/etc/kubernetes/ca.crt --clou d-provider=aws --anonymous-auth=false --register-with-taints=node-role.kubernetes.io/master=:NoSchedule
    83  var KubeletProcessRegex = BuildProcessRegex("(hyperkube )?kubelet")
    84  
    85  // CriSocket returns the CRI socket path used by kubelet
    86  func CriSocket() (string, error) { //nolint
    87  	procs, err := utils.Processes()
    88  	if err != nil {
    89  		return "", fmt.Errorf("failed to list processes: %v", err)
    90  	}
    91  	for _, proc := range procs {
    92  		if KubeletProcessRegex.MatchString(proc.Cmdline) {
    93  			if sock := ParseStringFlag(proc.Cmdline, "--container-runtime-endpoint"); sock != nil {
    94  				return strings.TrimPrefix(*sock, "unix://"), nil
    95  			}
    96  		}
    97  	}
    98  	return "", nil
    99  }
   100  
   101  // DetectCRIRuntimeEndpoint checks if the unix socket path are present for CRI
   102  func DetectCRIRuntimeEndpoint() (string, Type, error) {
   103  	var retErr error
   104  	isUDSocket := func(path string) (string, error) {
   105  		fileInfo, err := os.Stat(path)
   106  		if err != nil {
   107  			return "", fmt.Errorf("%s not a socket", path)
   108  		}
   109  		if !(fileInfo.Mode()&os.ModeSocket == os.ModeSocket) {
   110  			return "", fmt.Errorf("%s not a socket", path)
   111  		}
   112  		return "unix://" + path, nil
   113  
   114  	}
   115  	runtimes := []string{criDockerShimEndpoint, criContainerdEndpoint, criCrioEndpoint}
   116  	existingCriSockets := []string{}
   117  
   118  	for _, p := range runtimes {
   119  		path := getHostPath(p)
   120  		if addr, err := isUDSocket(path); err == nil {
   121  			zap.L().Debug("cri: detected CRI runtime service socket address", zap.String("socketPathAddress", addr))
   122  			existingCriSockets = append(existingCriSockets, addr)
   123  		} else {
   124  			retErr = err
   125  			zap.L().Debug("cri: socket path unavailable/inaccessible", zap.String("socketPath", path), zap.Error(err))
   126  		}
   127  	}
   128  	zap.L().Debug("The CRI sockets found are:", zap.Strings("paths", existingCriSockets))
   129  	if len(existingCriSockets) > 1 {
   130  		// this should ideally not happen but if it happens then get the kubelet cmdLine CRI
   131  		// now check for the kubelet runtime
   132  		sockaddr, err := CriSocket()
   133  		if err != nil {
   134  			return sockaddr, getCRISocketAddrType(sockaddr), fmt.Errorf("Multiple detection of CRI runtime endpoints, failed to get socketPath from kubelet")
   135  		}
   136  		// If there is no CRI EP on kubelet that means docker is the default, because kubelet's default CRI is docker.
   137  		if sockaddr == "" {
   138  			return getHostPath(criDockerShimEndpoint), TypeDocker, nil
   139  		}
   140  		return sockaddr, getCRISocketAddrType(sockaddr), nil
   141  	} else if len(existingCriSockets) == 1 {
   142  		return existingCriSockets[0], getCRISocketAddrType(existingCriSockets[0]), nil
   143  	}
   144  	// no CRI endpoint present on the system/node, this can happen during restarts
   145  	return "", TypeNone, fmt.Errorf("auto detection of CRI runtime endpoints failed, tested common locations %s, %s", strings.Join(runtimes, ", "), retErr)
   146  }
   147  
   148  func getCRISocketAddrType(sockaddr string) Type {
   149  	if strings.Contains(sockaddr, "crio") {
   150  		return TypeCRIO
   151  	}
   152  	if strings.Contains(sockaddr, "containerd") {
   153  		return TypeContainerD
   154  	}
   155  	if strings.Contains(sockaddr, "docker") {
   156  		return TypeDocker
   157  	}
   158  	return TypeNone
   159  }
   160  
   161  func getCRISocketAddr(criRuntimeEndpoint string) (string, error) {
   162  	var err error
   163  	addr := criRuntimeEndpoint
   164  	if addr == "" {
   165  		addr, _, err = DetectCRIRuntimeEndpoint()
   166  		if err != nil {
   167  			return "", err
   168  		}
   169  	}
   170  	if strings.HasPrefix(addr, "tcp:") {
   171  		return "", fmt.Errorf("tcp endpoints are not supported")
   172  	}
   173  	if !strings.HasPrefix(addr, "unix:") {
   174  		addr = "unix://" + addr
   175  	}
   176  	addr = path.Clean(addr)
   177  
   178  	if strings.Contains(addr, "frakti") {
   179  		return "", fmt.Errorf("frakti runtime is not supported")
   180  	}
   181  
   182  	u, err := url.Parse(addr)
   183  	if err != nil {
   184  		return "", err
   185  	}
   186  	if u.Scheme != "unix" {
   187  		return "", fmt.Errorf("only unix sockets are supported")
   188  	}
   189  
   190  	// NOTE: convoluted, but makes unix socket paths and abstract unix socket socket URLs in gRPC connotation both work.
   191  	// The trouble is that u.Path is only set for "proper" unix socket, and the rest is in u.Opaque
   192  	// This strips "unix:" from the URL again as well as any following potential "//",
   193  	// leaving a single '/' if this was a 'unix:///var/run/...' address
   194  	return strings.TrimPrefix(strings.TrimPrefix(addr, "unix:"), "//"), nil
   195  }
   196  
   197  func connectCRISocket(ctx context.Context, addr string) (*grpc.ClientConn, error) {
   198  	var err error
   199  	var connection *grpc.ClientConn
   200  
   201  	ctx, cancel := context.WithTimeout(ctx, connectTimeout)
   202  	defer cancel()
   203  
   204  	connection, err = grpc.DialContext(
   205  		ctx,
   206  		addr,
   207  		// we want to wait for an initial connection
   208  		grpc.WithBlock(),
   209  		// we do everything like the kubelet: we bump this up to 16MB
   210  		grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(maxMsgSize)),
   211  		// unix socket connection, disable transport security
   212  		grpc.WithInsecure(),
   213  		grpc.WithContextDialer(func(ctx context.Context, addr string) (net.Conn, error) {
   214  			return (&net.Dialer{}).DialContext(ctx, "unix", addr)
   215  		}),
   216  	)
   217  	if err != nil {
   218  		return nil, fmt.Errorf("connection to CRI runtime service failed: %s", err.Error())
   219  	}
   220  	return connection, nil
   221  }
   222  
   223  // NewCRIRuntimeServiceClient takes a CRI socket path and tries to establish a grpc connection to the CRI runtime service.
   224  // On success it is returning an ExtendedRuntimeService interface which is an extended CRI runtime service interface.
   225  func NewCRIRuntimeServiceClient(ctx context.Context, criRuntimeEndpoint string) (ExtendedRuntimeService, error) {
   226  	// build the socket path URL
   227  	addr, err := getCRISocketAddr(criRuntimeEndpoint)
   228  	if err != nil {
   229  		return nil, fmt.Errorf("cri: failed to get socket address: %s", err)
   230  	}
   231  
   232  	// establish the CRI connection
   233  	// once this connection has been established
   234  	// gRPC will take care of reconnections, etc.
   235  	// connections are very much hands-off after that point
   236  	connection, err := connectCRISocket(ctx, addr)
   237  	if err != nil {
   238  		return nil, fmt.Errorf("cri: failed to connect to CRI socket: %s", err)
   239  	}
   240  
   241  	// finally create the extended wrapper
   242  	svc, err := NewCRIExtendedRuntimeServiceWrapper(
   243  		ctx,
   244  		callTimeout,
   245  		criruntimev1alpha2.NewRuntimeServiceClient(connection),
   246  	)
   247  	if err != nil {
   248  		return nil, fmt.Errorf("faile to create extended runtime service wrapper: %s", err.Error())
   249  	}
   250  
   251  	// and return with it
   252  	return svc, nil
   253  }