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 }