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 }