github.com/1aal/kubeblocks@v0.0.0-20231107070852-e1c03e598921/pkg/lorry/client/k8sexec_client.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 client
    21  
    22  import (
    23  	"bytes"
    24  	context "context"
    25  	"encoding/json"
    26  	"fmt"
    27  	"io"
    28  	"os"
    29  	"strings"
    30  	"time"
    31  
    32  	"github.com/go-logr/logr"
    33  	"github.com/pkg/errors"
    34  	corev1 "k8s.io/api/core/v1"
    35  	"k8s.io/apimachinery/pkg/runtime/schema"
    36  	"k8s.io/cli-runtime/pkg/genericiooptions"
    37  	"k8s.io/client-go/kubernetes/scheme"
    38  	"k8s.io/client-go/rest"
    39  	"k8s.io/client-go/tools/remotecommand"
    40  	cmdexec "k8s.io/kubectl/pkg/cmd/exec"
    41  	ctrl "sigs.k8s.io/controller-runtime"
    42  
    43  	intctrlutil "github.com/1aal/kubeblocks/pkg/controllerutil"
    44  )
    45  
    46  // K8sExecClient is a mock client for operation, mainly used to hide curl command details.
    47  type K8sExecClient struct {
    48  	lorryClient
    49  	cmdexec.StreamOptions
    50  	Executor       cmdexec.RemoteExecutor
    51  	restConfig     *rest.Config
    52  	restClient     *rest.RESTClient
    53  	lorryPort      int32
    54  	RequestTimeout time.Duration
    55  	logger         logr.Logger
    56  }
    57  
    58  // NewK8sExecClientWithPod create a new OperationHTTPClient with lorry container
    59  func NewK8sExecClientWithPod(pod *corev1.Pod) (*K8sExecClient, error) {
    60  	var (
    61  		err error
    62  	)
    63  	logger := ctrl.Log.WithName("Lorry K8S Exec client")
    64  
    65  	containerName, err := intctrlutil.GetLorryContainerName(pod)
    66  	if err != nil {
    67  		logger.Info("not lorry in the pod, just return nil without error")
    68  		return nil, nil
    69  	}
    70  
    71  	port, err := intctrlutil.GetLorryHTTPPort(pod)
    72  	if err != nil {
    73  		return nil, err
    74  	}
    75  
    76  	streamOptions := cmdexec.StreamOptions{
    77  		IOStreams:     genericiooptions.IOStreams{In: os.Stdin, Out: os.Stdout, ErrOut: os.Stderr},
    78  		Stdin:         true,
    79  		TTY:           true,
    80  		PodName:       pod.Name,
    81  		ContainerName: containerName,
    82  		Namespace:     pod.Namespace,
    83  	}
    84  
    85  	restConfig, err := ctrl.GetConfig()
    86  	if err != nil {
    87  		return nil, errors.Wrap(err, "get k8s config failed")
    88  	}
    89  
    90  	restConfig.GroupVersion = &schema.GroupVersion{Group: "", Version: "v1"}
    91  	restConfig.APIPath = "/api"
    92  	restConfig.NegotiatedSerializer = scheme.Codecs.WithoutConversion()
    93  	restClient, err := rest.RESTClientFor(restConfig)
    94  	if err != nil {
    95  		return nil, errors.Wrap(err, "create k8s client failed")
    96  	}
    97  
    98  	client := &K8sExecClient{
    99  		StreamOptions:  streamOptions,
   100  		lorryPort:      port,
   101  		restConfig:     restConfig,
   102  		restClient:     restClient,
   103  		RequestTimeout: 10 * time.Second,
   104  		logger:         logger,
   105  		Executor:       &cmdexec.DefaultRemoteExecutor{},
   106  	}
   107  	client.lorryClient = lorryClient{requester: client}
   108  	return client, nil
   109  }
   110  
   111  // Request execs lorry operation, this is a blocking operation and use pod EXEC subresource to send a http request to the lorry pod
   112  func (cli *K8sExecClient) Request(ctx context.Context, operation, method string, req map[string]any) (map[string]any, error) {
   113  	var (
   114  		strBuffer bytes.Buffer
   115  		errBuffer bytes.Buffer
   116  		err       error
   117  	)
   118  	curlCmd := fmt.Sprintf("curl --fail-with-body --silent -X %s -H 'Content-Type: application/json' http://localhost:%d/v1.0/%s",
   119  		strings.ToUpper(method), cli.lorryPort, strings.ToLower(operation))
   120  
   121  	if len(req) != 0 {
   122  		jsonData, err := json.Marshal(req)
   123  		if err != nil {
   124  			return nil, err
   125  		}
   126  		// escape single quote
   127  		body := strings.ReplaceAll(string(jsonData), "'", "\\'")
   128  		curlCmd += fmt.Sprintf(" -d '%s'", body)
   129  	}
   130  	cmd := []string{"sh", "-c", curlCmd}
   131  
   132  	// redirect output to strBuffer to be parsed later
   133  	if err = cli.k8sExec(cmd, &strBuffer, &errBuffer); err != nil {
   134  		data := strBuffer.Bytes()
   135  		if len(data) != 0 {
   136  			// curl emits result message to output
   137  			return nil, errors.Wrap(err, string(data))
   138  		}
   139  
   140  		errData := errBuffer.Bytes()
   141  		if len(errData) != 0 {
   142  			return nil, errors.Wrap(err, string(errData))
   143  		}
   144  		return nil, err
   145  	}
   146  
   147  	data := strBuffer.Bytes()
   148  	if len(data) == 0 {
   149  		errData := errBuffer.Bytes()
   150  		if len(errData) != 0 {
   151  			cli.logger.Info("k8s exec error output", "message", string(errData))
   152  			return nil, errors.New(string(errData))
   153  		}
   154  
   155  		return nil, nil
   156  	}
   157  
   158  	result := map[string]any{}
   159  	if err := json.Unmarshal(data, &result); err != nil {
   160  		return nil, errors.Wrap(err, "decode result failed")
   161  	}
   162  	return result, nil
   163  }
   164  
   165  func (cli *K8sExecClient) k8sExec(cmd []string, outWriter io.Writer, errWriter io.Writer) error {
   166  	// ensure we can recover the terminal while attached
   167  	t := cli.SetupTTY()
   168  
   169  	var sizeQueue remotecommand.TerminalSizeQueue
   170  	if t.Raw {
   171  		// this call spawns a goroutine to monitor/update the terminal size
   172  		sizeQueue = t.MonitorSize(t.GetSize())
   173  
   174  		// unset p.Err if it was previously set because both stdout and stderr go over p.Out when tty is
   175  		// true
   176  		cli.ErrOut = nil
   177  	}
   178  
   179  	fn := func() error {
   180  		req := cli.restClient.Post().
   181  			Resource("pods").
   182  			Name(cli.PodName).
   183  			Namespace(cli.Namespace).
   184  			SubResource("exec")
   185  		req.VersionedParams(&corev1.PodExecOptions{
   186  			Container: cli.ContainerName,
   187  			Command:   cmd,
   188  			Stdin:     cli.Stdin,
   189  			Stdout:    outWriter != nil,
   190  			Stderr:    errWriter != nil,
   191  			TTY:       t.Raw,
   192  		}, scheme.ParameterCodec)
   193  
   194  		return cli.Executor.Execute("POST", req.URL(), cli.restConfig, cli.In, outWriter, errWriter, t.Raw, sizeQueue)
   195  	}
   196  
   197  	if err := t.Safe(fn); err != nil {
   198  		return err
   199  	}
   200  	return nil
   201  }