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  }