github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/configuration/container/container_kill.go (about) 1 /* 2 Copyright (C) 2022-2023 ApeCloud Co., Ltd 3 4 This file is part of KubeBlocks project 5 6 This program is free software: you can redistribute it and/or modify 7 it under the terms of the GNU Affero General Public License as published by 8 the Free Software Foundation, either version 3 of the License, or 9 (at your option) any later version. 10 11 This program is distributed in the hope that it will be useful 12 but WITHOUT ANY WARRANTY; without even the implied warranty of 13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 GNU Affero General Public License for more details. 15 16 You should have received a copy of the GNU Affero General Public License 17 along with this program. If not, see <http://www.gnu.org/licenses/>. 18 */ 19 20 package container 21 22 import ( 23 "context" 24 "fmt" 25 "net" 26 "os" 27 "time" 28 29 "github.com/docker/docker/api/types" 30 "github.com/docker/docker/api/types/filters" 31 dockerapi "github.com/docker/docker/client" 32 "go.uber.org/zap" 33 "google.golang.org/grpc" 34 "google.golang.org/grpc/credentials/insecure" 35 utilerrors "k8s.io/apimachinery/pkg/util/errors" 36 runtimeapi "k8s.io/cri-api/pkg/apis/runtime/v1" 37 38 cfgcore "github.com/1aal/kubeblocks/pkg/configuration/core" 39 "github.com/1aal/kubeblocks/pkg/configuration/util" 40 viper "github.com/1aal/kubeblocks/pkg/viperx" 41 ) 42 43 const ( 44 maxMsgSize = 1024 * 256 // 256k 45 defaultTimeout = 2 * time.Second 46 defaultSignal = "SIGKILL" 47 48 KillContainerSignalEnvName = "KILL_CONTAINER_SIGNAL" 49 ) 50 51 // dockerContainer supports docker cri 52 type dockerContainer struct { 53 dockerEndpoint string 54 logger *zap.SugaredLogger 55 56 dc dockerapi.ContainerAPIClient 57 } 58 59 func init() { 60 if err := viper.BindEnv(KillContainerSignalEnvName); err != nil { 61 fmt.Printf("failed to bind env for viper, env name: [%s]\n", KillContainerSignalEnvName) 62 os.Exit(-2) 63 } 64 65 viper.SetDefault(KillContainerSignalEnvName, defaultSignal) 66 } 67 68 func (d *dockerContainer) Kill(ctx context.Context, containerIDs []string, signal string, _ *time.Duration) error { 69 d.logger.Debugf("docker containers going to be stopped: %v", containerIDs) 70 if signal == "" { 71 signal = defaultSignal 72 } 73 74 allContainer, err := getExistsContainers(ctx, containerIDs, d.dc) 75 if err != nil { 76 return cfgcore.WrapError(err, "failed to search docker container") 77 } 78 79 errs := make([]error, 0, len(containerIDs)) 80 d.logger.Debugf("all containers: %v", util.ToSet(allContainer).AsSlice()) 81 for _, containerID := range containerIDs { 82 d.logger.Infof("stopping docker container: %s", containerID) 83 container, ok := allContainer[containerID] 84 if !ok { 85 d.logger.Infof("docker container[%s] not existed and continue.", containerID) 86 continue 87 } 88 if container.State == "exited" { 89 d.logger.Infof("docker container[%s] exited, status: %s", containerID, container.Status) 90 continue 91 } 92 if err := d.dc.ContainerKill(ctx, containerID, signal); err != nil { 93 errs = append(errs, err) 94 continue 95 } 96 d.logger.Infof("docker container[%s] stopped.", containerID) 97 } 98 if len(errs) > 0 { 99 return utilerrors.NewAggregate(errs) 100 } 101 return nil 102 } 103 104 func getExistsContainers(ctx context.Context, containerIDs []string, dc dockerapi.ContainerAPIClient) (map[string]*types.Container, error) { 105 var ( 106 optionsArgs = filters.NewArgs() 107 allContainer map[string]*types.Container 108 ) 109 110 for _, containerID := range containerIDs { 111 optionsArgs.Add("id", containerID) 112 } 113 114 containers, err := dc.ContainerList(ctx, types.ContainerListOptions{ 115 All: true, 116 Filters: optionsArgs, 117 }) 118 if err != nil { 119 return nil, err 120 } 121 allContainer = make(map[string]*types.Container, len(containerIDs)) 122 for _, c := range containers { 123 allContainer[c.ID] = &c 124 } 125 return allContainer, nil 126 } 127 128 func (d *dockerContainer) Init(ctx context.Context) error { 129 client, err := createDockerClient(d.dockerEndpoint, d.logger) 130 if err != nil { 131 return err 132 } 133 if err := d.ping(ctx, client); err != nil { 134 return err 135 } 136 d.dc = client 137 return nil 138 } 139 140 func (d *dockerContainer) ping(ctx context.Context, cli *dockerapi.Client) error { 141 ping, err := cli.Ping(ctx) 142 if err != nil { 143 return err 144 } 145 d.logger.Infof("create docker client succeed, docker info: %v", ping) 146 return nil 147 } 148 149 func createDockerClient(dockerEndpoint string, logger *zap.SugaredLogger) (*dockerapi.Client, error) { 150 if len(dockerEndpoint) == 0 { 151 dockerEndpoint = dockerapi.DefaultDockerHost 152 } 153 154 logger.Infof("connecting to docker container endpoint: %s", dockerEndpoint) 155 return dockerapi.NewClientWithOpts( 156 dockerapi.WithHost(formatSocketPath(dockerEndpoint)), 157 dockerapi.WithVersion(""), 158 ) 159 } 160 161 // dockerContainer supports docker cri 162 type containerdContainer struct { 163 runtimeEndpoint string 164 logger *zap.SugaredLogger 165 166 backendRuntime runtimeapi.RuntimeServiceClient 167 } 168 169 func (c *containerdContainer) Kill(ctx context.Context, containerIDs []string, signal string, timeout *time.Duration) error { 170 var ( 171 request = &runtimeapi.StopContainerRequest{} 172 errs = make([]error, 0, len(containerIDs)) 173 ) 174 175 switch { 176 case signal == defaultSignal: 177 request.Timeout = 0 178 case timeout != nil: 179 request.Timeout = timeout.Milliseconds() 180 } 181 182 // reference cri-api url: https://github.com/kubernetes/cri-api/blob/master/pkg/apis/runtime/v1/api.proto#L1108 183 // reference containerd url: https://github.com/containerd/containerd/blob/main/pkg/cri/server/container_stop.go#L124 184 for _, containerID := range containerIDs { 185 c.logger.Infof("stopping container: %s", containerID) 186 containers, err := c.backendRuntime.ListContainers(ctx, &runtimeapi.ListContainersRequest{ 187 Filter: &runtimeapi.ContainerFilter{Id: containerID}, 188 }) 189 190 switch { 191 case err != nil: 192 errs = append(errs, err) 193 case containers == nil || len(containers.Containers) == 0: 194 c.logger.Infof("containerd container[%s] not existed and continue.", containerID) 195 case containers.Containers[0].State == runtimeapi.ContainerState_CONTAINER_EXITED: 196 c.logger.Infof("containerd container[%s] not exited and continue.", containerID) 197 default: 198 request.ContainerId = containerID 199 _, err = c.backendRuntime.StopContainer(ctx, request) 200 if err != nil { 201 c.logger.Infof("failed to stop container[%s], error: %v", containerID, err) 202 errs = append(errs, err) 203 continue 204 } 205 c.logger.Infof("docker container[%s] stopped.", containerID) 206 } 207 } 208 209 if len(errs) > 0 { 210 return utilerrors.NewAggregate(errs) 211 } 212 return nil 213 } 214 215 func (c *containerdContainer) Init(ctx context.Context) error { 216 var ( 217 err error 218 conn *grpc.ClientConn 219 endpoints = defaultContainerdEndpoints 220 ) 221 222 if c.runtimeEndpoint != "" { 223 endpoints = []string{formatSocketPath(c.runtimeEndpoint)} 224 } 225 226 for _, endpoint := range endpoints { 227 conn, err = createGrpcConnection(ctx, endpoint) 228 if err != nil { 229 c.logger.Warnf("failed to connect containerd endpoint: %s, error : %v", endpoint, err) 230 } else { 231 c.backendRuntime = runtimeapi.NewRuntimeServiceClient(conn) 232 if err = c.pingCRI(ctx, c.backendRuntime); err != nil { 233 return nil 234 } 235 } 236 } 237 return err 238 } 239 240 func (c *containerdContainer) pingCRI(ctx context.Context, runtime runtimeapi.RuntimeServiceClient) error { 241 status, err := runtime.Status(ctx, &runtimeapi.StatusRequest{ 242 Verbose: true, 243 }) 244 if err != nil { 245 return err 246 } 247 c.logger.Infof("cri status: %v", status) 248 return nil 249 } 250 251 func NewContainerKiller(containerRuntime CRIType, runtimeEndpoint string, logger *zap.SugaredLogger) (ContainerKiller, error) { 252 var ( 253 killer ContainerKiller 254 ) 255 256 if containerRuntime == AutoType { 257 containerRuntime = autoCheckCRIType(defaultContainerdEndpoints, dockerapi.DefaultDockerHost, logger) 258 runtimeEndpoint = "" 259 } 260 261 switch containerRuntime { 262 case DockerType: 263 killer = &dockerContainer{ 264 dockerEndpoint: runtimeEndpoint, 265 logger: logger, 266 } 267 case ContainerdType: 268 killer = &containerdContainer{ 269 runtimeEndpoint: runtimeEndpoint, 270 logger: logger, 271 } 272 default: 273 return nil, cfgcore.MakeError("not supported cri type: %s", containerRuntime) 274 } 275 return killer, nil 276 } 277 278 func autoCheckCRIType(criEndpoints []string, dockerEndpoints string, logger *zap.SugaredLogger) CRIType { 279 for _, f := range criEndpoints { 280 if isSocketFile(f) && hasValidCRISocket(f, logger) { 281 return ContainerdType 282 } 283 } 284 if isSocketFile(dockerEndpoints) { 285 return DockerType 286 } 287 return "" 288 } 289 290 func hasValidCRISocket(sockPath string, logger *zap.SugaredLogger) bool { 291 connection, err := createGrpcConnection(context.Background(), sockPath) 292 if err != nil { 293 logger.Warnf("failed to connect socket path: %s, error: %v", sockPath, err) 294 return false 295 } 296 _ = connection.Close() 297 return true 298 } 299 300 func createGrpcConnection(ctx context.Context, socketAddress string) (*grpc.ClientConn, error) { 301 ctx, cancel := context.WithTimeout(ctx, defaultTimeout) 302 defer cancel() 303 return grpc.DialContext(ctx, socketAddress, 304 grpc.WithTransportCredentials(insecure.NewCredentials()), 305 grpc.WithBlock(), 306 grpc.WithContextDialer(func(ctx context.Context, addr string) (net.Conn, error) { 307 return (&net.Dialer{}).DialContext(ctx, "unix", addr) 308 }), 309 grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(maxMsgSize))) 310 }