pod.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551
  1. // Copyright 2019 Yunion
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. package pod
  15. import (
  16. "context"
  17. "fmt"
  18. "path/filepath"
  19. "strconv"
  20. "strings"
  21. "time"
  22. "google.golang.org/grpc"
  23. "google.golang.org/grpc/credentials/insecure"
  24. runtimeapi "k8s.io/cri-api/pkg/apis/runtime/v1"
  25. "yunion.io/x/jsonutils"
  26. "yunion.io/x/log"
  27. "yunion.io/x/pkg/errors"
  28. "yunion.io/x/onecloud/pkg/util/exec"
  29. "yunion.io/x/onecloud/pkg/util/procutils"
  30. )
  31. type CRI interface {
  32. Version(ctx context.Context) (*runtimeapi.VersionResponse, error)
  33. ListPods(ctx context.Context, opts ListPodOptions) ([]*runtimeapi.PodSandbox, error)
  34. RunPod(ctx context.Context, podConfig *runtimeapi.PodSandboxConfig, runtimeHandler string) (string, error)
  35. StopPod(ctx context.Context, req *runtimeapi.StopPodSandboxRequest) error
  36. RemovePod(ctx context.Context, podId string) error
  37. CreateContainer(ctx context.Context, podId string, podConfig *runtimeapi.PodSandboxConfig, ctrConfig *runtimeapi.ContainerConfig, withPull bool) (string, error)
  38. StartContainer(ctx context.Context, id string) error
  39. StopContainer(ctx context.Context, ctrId string, timeout int64, tryRemove bool, force bool) error
  40. RemoveContainer(ctx context.Context, ctrId string) error
  41. RunContainers(ctx context.Context, podConfig *runtimeapi.PodSandboxConfig, containerConfigs []*runtimeapi.ContainerConfig, runtimeHandler string) (*RunContainersResponse, error)
  42. ListContainers(ctx context.Context, opts ListContainerOptions) ([]*runtimeapi.Container, error)
  43. ContainerStatus(ctx context.Context, ctrId string) (*runtimeapi.ContainerStatusResponse, error)
  44. ListImages(ctx context.Context, filter *runtimeapi.ImageFilter) ([]*runtimeapi.Image, error)
  45. PullImage(ctx context.Context, req *runtimeapi.PullImageRequest) (*runtimeapi.PullImageResponse, error)
  46. ImageStatus(ctx context.Context, req *runtimeapi.ImageStatusRequest) (*runtimeapi.ImageStatusResponse, error)
  47. ExecSync(ctx context.Context, ctrId string, command []string, timeout int64) (*ExecSyncResponse, error)
  48. // lower layer client
  49. GetImageClient() runtimeapi.ImageServiceClient
  50. GetRuntimeClient() runtimeapi.RuntimeServiceClient
  51. }
  52. type ListContainerOptions struct {
  53. // Id of container
  54. Id string
  55. // PodId of container
  56. PodId string
  57. // Regular expression pattern to match pod or container
  58. NameRegexp string
  59. // Regular expression pattern to match the pod namespace
  60. PodNamespaceRegexp string
  61. // State of the sandbox
  62. State string
  63. // Show verbose info for the sandbox
  64. Verbose bool
  65. // Labels are selectors for the sandbox
  66. Labels map[string]string
  67. // Image ued by the container
  68. Image string
  69. }
  70. type crictl struct {
  71. endpoint string
  72. timeout time.Duration
  73. conn *grpc.ClientConn
  74. imgCli runtimeapi.ImageServiceClient
  75. runCli runtimeapi.RuntimeServiceClient
  76. }
  77. func NewCRI(endpoint string, timeout time.Duration) (CRI, error) {
  78. ctx, cancel := context.WithTimeout(context.Background(), timeout)
  79. defer cancel()
  80. var dialOpts []grpc.DialOption
  81. maxMsgSize := 1024 * 1024 * 16
  82. dialOpts = append(dialOpts,
  83. grpc.WithTransportCredentials(insecure.NewCredentials()),
  84. grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(maxMsgSize)))
  85. conn, err := grpc.DialContext(ctx, endpoint, dialOpts...)
  86. if err != nil {
  87. return nil, errors.Wrapf(err, "Connect remote endpoint %q failed", endpoint)
  88. }
  89. imgCli := runtimeapi.NewImageServiceClient(conn)
  90. runCli := runtimeapi.NewRuntimeServiceClient(conn)
  91. return &crictl{
  92. endpoint: endpoint,
  93. timeout: timeout,
  94. conn: conn,
  95. imgCli: imgCli,
  96. runCli: runCli,
  97. }, nil
  98. }
  99. func (c crictl) GetImageClient() runtimeapi.ImageServiceClient {
  100. return c.imgCli
  101. }
  102. func (c crictl) GetRuntimeClient() runtimeapi.RuntimeServiceClient {
  103. return c.runCli
  104. }
  105. func (c crictl) Version(ctx context.Context) (*runtimeapi.VersionResponse, error) {
  106. return c.GetRuntimeClient().Version(ctx, &runtimeapi.VersionRequest{})
  107. }
  108. func (c crictl) ListImages(ctx context.Context, filter *runtimeapi.ImageFilter) ([]*runtimeapi.Image, error) {
  109. ctx, cancel := context.WithTimeout(ctx, c.timeout)
  110. defer cancel()
  111. resp, err := c.GetImageClient().ListImages(ctx, &runtimeapi.ListImagesRequest{
  112. Filter: filter,
  113. })
  114. if err != nil {
  115. return nil, errors.Wrapf(err, "ListImages with filter %s", filter.String())
  116. }
  117. return resp.Images, nil
  118. }
  119. func (c crictl) RunPod(ctx context.Context, podConfig *runtimeapi.PodSandboxConfig, runtimeHandler string) (string, error) {
  120. req := &runtimeapi.RunPodSandboxRequest{
  121. Config: podConfig,
  122. RuntimeHandler: runtimeHandler,
  123. }
  124. log.Infof("RunPodSandboxRequest: %v", req)
  125. r, err := c.GetRuntimeClient().RunPodSandbox(ctx, req)
  126. if err != nil {
  127. return "", errors.Wrapf(err, "RunPod with request: %s", req.String())
  128. }
  129. return r.GetPodSandboxId(), nil
  130. }
  131. func (c crictl) StopPod(ctx context.Context, req *runtimeapi.StopPodSandboxRequest) error {
  132. _, err := c.GetRuntimeClient().StopPodSandbox(ctx, req)
  133. if err != nil {
  134. return errors.Wrap(err, "StopPodSandbox")
  135. }
  136. return nil
  137. }
  138. // PullImageWithSandbox sends a PullImageRequest to the server and parses
  139. // the returned PullImageResponses.
  140. func (c crictl) PullImageWithSandbox(ctx context.Context, image string, auth *runtimeapi.AuthConfig, sandbox *runtimeapi.PodSandboxConfig, ann map[string]string) (*runtimeapi.PullImageResponse, error) {
  141. req := &runtimeapi.PullImageRequest{
  142. Image: &runtimeapi.ImageSpec{
  143. Image: image,
  144. Annotations: ann,
  145. },
  146. Auth: auth,
  147. SandboxConfig: sandbox,
  148. }
  149. log.Infof("PullImageRequest: %v", req)
  150. r, err := c.GetImageClient().PullImage(ctx, req)
  151. if err != nil {
  152. return nil, errors.Wrapf(err, "PullImage with %s", req)
  153. }
  154. return r, nil
  155. }
  156. // CreateContainer sends a CreateContainerRequest to the server, and parses
  157. // the returned CreateContainerResponse.
  158. func (c crictl) CreateContainer(ctx context.Context,
  159. podId string, podConfig *runtimeapi.PodSandboxConfig,
  160. ctrConfig *runtimeapi.ContainerConfig, withPull bool) (string, error) {
  161. req := &runtimeapi.CreateContainerRequest{
  162. PodSandboxId: podId,
  163. Config: ctrConfig,
  164. SandboxConfig: podConfig,
  165. }
  166. image := ctrConfig.GetImage().GetImage()
  167. // When there is a withPull request or the image default mode is to
  168. // pull-image-on-create(true) and no-pull was not set we pull the image when
  169. // they ask for a create as a helper on the cli to reduce extra steps. As a
  170. // reminder if the image is already in cache only the manifest will be pulled
  171. // down to verify.
  172. if withPull {
  173. // Try to pull the image before container creation
  174. ann := ctrConfig.GetImage().GetAnnotations()
  175. resp, err := c.PullImageWithSandbox(ctx, image, nil, nil, ann)
  176. if err != nil {
  177. return "", errors.Wrap(err, "PullImageWithSandbox")
  178. }
  179. log.Infof("Pull image %s", resp.String())
  180. }
  181. log.Debugf("CreateContainerRequest pod %s: %v", podId, req)
  182. r, err := c.GetRuntimeClient().CreateContainer(ctx, req)
  183. if err != nil {
  184. return "", errors.Wrapf(err, "CreateContainer with: %s", req)
  185. }
  186. return r.GetContainerId(), nil
  187. }
  188. // StartContainer sends a StartContainerRequest to the server, and parses
  189. // the returned StartContainerResponse.
  190. func (c crictl) StartContainer(ctx context.Context, id string) error {
  191. if id == "" {
  192. return errors.Error("Id can't be empty")
  193. }
  194. if _, err := c.GetRuntimeClient().StartContainer(ctx, &runtimeapi.StartContainerRequest{
  195. ContainerId: id,
  196. }); err != nil {
  197. return errors.Wrapf(err, "StartContainer %s", id)
  198. }
  199. return nil
  200. }
  201. type RunContainersResponse struct {
  202. PodId string `json:"pod_id"`
  203. ContainerIds []string `json:"container_ids"`
  204. }
  205. // RunContainers starts containers in the provided pod sandbox
  206. func (c crictl) RunContainers(
  207. ctx context.Context, podConfig *runtimeapi.PodSandboxConfig, containerConfigs []*runtimeapi.ContainerConfig, runtimeHandler string) (*RunContainersResponse, error) {
  208. // Create the pod
  209. podId, err := c.RunPod(ctx, podConfig, runtimeHandler)
  210. if err != nil {
  211. return nil, errors.Wrap(err, "RunPod")
  212. }
  213. ret := &RunContainersResponse{
  214. PodId: podId,
  215. ContainerIds: make([]string, 0),
  216. }
  217. // Create the containers
  218. for idx, ctr := range containerConfigs {
  219. // Create the container
  220. ctrId, err := c.CreateContainer(ctx, podId, podConfig, ctr, true)
  221. if err != nil {
  222. return nil, errors.Wrapf(err, "CreateContainer %d", idx)
  223. }
  224. // Start the container
  225. if err := c.StartContainer(ctx, ctrId); err != nil {
  226. return nil, errors.Wrapf(err, "StarContainer %d", idx)
  227. }
  228. ret.ContainerIds = append(ret.ContainerIds, ctrId)
  229. }
  230. return ret, nil
  231. }
  232. func (c crictl) ListContainers(ctx context.Context, opts ListContainerOptions) ([]*runtimeapi.Container, error) {
  233. filter := &runtimeapi.ContainerFilter{
  234. Id: opts.Id,
  235. PodSandboxId: opts.PodId,
  236. }
  237. st := &runtimeapi.ContainerStateValue{}
  238. if opts.State != "" {
  239. st.State = runtimeapi.ContainerState_CONTAINER_UNKNOWN
  240. switch strings.ToLower(opts.State) {
  241. case "created":
  242. st.State = runtimeapi.ContainerState_CONTAINER_CREATED
  243. case "running":
  244. st.State = runtimeapi.ContainerState_CONTAINER_RUNNING
  245. case "exited":
  246. st.State = runtimeapi.ContainerState_CONTAINER_EXITED
  247. case "unknown":
  248. st.State = runtimeapi.ContainerState_CONTAINER_UNKNOWN
  249. default:
  250. return nil, fmt.Errorf("unsupported state: %q", opts.State)
  251. }
  252. filter.State = st
  253. }
  254. if opts.Labels != nil {
  255. filter.LabelSelector = opts.Labels
  256. }
  257. req := &runtimeapi.ListContainersRequest{
  258. Filter: filter,
  259. }
  260. r, err := c.GetRuntimeClient().ListContainers(ctx, req)
  261. if err != nil {
  262. return nil, errors.Wrap(err, "ListContainers")
  263. }
  264. return r.Containers, nil
  265. }
  266. type ListPodOptions struct {
  267. Id string
  268. State string
  269. }
  270. func (c crictl) ListPods(ctx context.Context, opts ListPodOptions) ([]*runtimeapi.PodSandbox, error) {
  271. filter := &runtimeapi.PodSandboxFilter{
  272. Id: opts.Id,
  273. }
  274. if opts.State != "" {
  275. st := &runtimeapi.PodSandboxStateValue{}
  276. st.State = runtimeapi.PodSandboxState_SANDBOX_NOTREADY
  277. switch strings.ToLower(opts.State) {
  278. case "ready":
  279. st.State = runtimeapi.PodSandboxState_SANDBOX_READY
  280. case "notready":
  281. st.State = runtimeapi.PodSandboxState_SANDBOX_NOTREADY
  282. default:
  283. return nil, errors.Errorf("Invalid state: %q", opts.State)
  284. }
  285. filter.State = st
  286. }
  287. req := &runtimeapi.ListPodSandboxRequest{
  288. Filter: filter,
  289. }
  290. ret, err := c.GetRuntimeClient().ListPodSandbox(ctx, req)
  291. if err != nil {
  292. return nil, errors.Wrap(err, "ListPodSandbox")
  293. }
  294. return ret.Items, nil
  295. }
  296. func (c crictl) RemovePod(ctx context.Context, podId string) error {
  297. maxTries := 10
  298. interval := 5 * time.Second
  299. errs := []error{}
  300. for tries := 0; tries < maxTries; tries++ {
  301. err := func() error {
  302. ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
  303. defer cancel()
  304. return c.removePod(ctx, podId)
  305. }()
  306. if err == nil {
  307. return nil
  308. }
  309. if strings.Contains(err.Error(), "code = NotFound") {
  310. return nil
  311. }
  312. dur := interval * time.Duration(tries+1)
  313. log.Warningf("try to remove pod %s after %s: %v", podId, dur, err)
  314. errs = append(errs, errors.Wrapf(err, "try %d", tries))
  315. time.Sleep(dur)
  316. }
  317. return errors.NewAggregate(errs)
  318. }
  319. func (c crictl) removePod(ctx context.Context, podId string) error {
  320. if _, err := c.GetRuntimeClient().RemovePodSandbox(ctx, &runtimeapi.RemovePodSandboxRequest{
  321. PodSandboxId: podId,
  322. }); err != nil {
  323. return errors.Wrap(err, "RemovePodSandbox")
  324. }
  325. return nil
  326. }
  327. func (c crictl) stopContainerWithRetry(ctx context.Context, ctrId string, timeout int64, maxTries int) error {
  328. interval := 5 * time.Second
  329. errs := []error{}
  330. for tries := 0; tries < maxTries; tries++ {
  331. err := func() error {
  332. ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
  333. defer cancel()
  334. return c.stopContainer(ctx, ctrId, timeout)
  335. }()
  336. if err == nil {
  337. return nil
  338. }
  339. if strings.Contains(err.Error(), "code = NotFound") {
  340. return nil
  341. }
  342. dur := interval * time.Duration(tries+1)
  343. log.Warningf("try to restop container %s after %s, timeout: %d: %v", ctrId, dur, timeout, err)
  344. // set timeout to 0 to stop forcely
  345. timeout = 0
  346. errs = append(errs, errors.Wrapf(err, "try %d", tries))
  347. time.Sleep(dur)
  348. }
  349. return errors.NewAggregate(errs)
  350. }
  351. func (c crictl) StopContainer(ctx context.Context, ctrId string, timeout int64, tryRemove bool, force bool) error {
  352. errs := []error{}
  353. isStopped := false
  354. if force {
  355. if err := c.forceKillContainer(ctx, ctrId); err != nil {
  356. log.Infof("force kill container %s error: %v", ctrId, err)
  357. errs = append(errs, errors.Wrap(err, "forceKillContainer"))
  358. } else {
  359. isStopped = true
  360. }
  361. } else {
  362. maxTries := 10
  363. if err := c.stopContainerWithRetry(ctx, ctrId, timeout, maxTries); err != nil {
  364. errs = append(errs, errors.Wrap(err, "stopContainer"))
  365. } else {
  366. isStopped = true
  367. }
  368. }
  369. // 尝试 force kill 容器
  370. if !force && !isStopped {
  371. if err := c.forceKillContainer(ctx, ctrId); err != nil {
  372. log.Infof("try force kill container %s error: %v", ctrId, err)
  373. errs = append(errs, errors.Wrap(err, "retry forceKillContainer"))
  374. } else {
  375. isStopped = true
  376. }
  377. }
  378. if tryRemove {
  379. // try force remove container
  380. ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
  381. defer cancel()
  382. if err := c.RemoveContainer(ctx, ctrId); err != nil {
  383. errs = append(errs, errors.Wrapf(err, "try remove container %s", ctrId))
  384. } else {
  385. return nil
  386. }
  387. }
  388. if isStopped {
  389. return nil
  390. }
  391. return errors.NewAggregate(errs)
  392. }
  393. func (c crictl) stopContainer(ctx context.Context, ctrId string, timeout int64) error {
  394. if _, err := c.GetRuntimeClient().StopContainer(ctx, &runtimeapi.StopContainerRequest{
  395. ContainerId: ctrId,
  396. Timeout: timeout,
  397. }); err != nil {
  398. return errors.Wrap(err, "StopContainer")
  399. }
  400. return nil
  401. }
  402. func (c crictl) forceKillContainer(ctx context.Context, ctrId string) error {
  403. cs, err := c.containerStatus(ctx, ctrId, true)
  404. if err != nil {
  405. return errors.Wrap(err, "get containerStatus")
  406. }
  407. info := cs.GetInfo()
  408. infoStr := info["info"]
  409. if infoStr == "" {
  410. return errors.Errorf("empty info: %s", infoStr)
  411. }
  412. infoObj, err := jsonutils.ParseString(infoStr)
  413. if err != nil {
  414. return errors.Wrapf(err, "invalid info: %s", infoStr)
  415. }
  416. pidInt, err := infoObj.Int("pid")
  417. if err != nil {
  418. return errors.Wrapf(err, "get pid from %s", infoObj)
  419. }
  420. pid := fmt.Sprintf("%d", pidInt)
  421. // get ppid
  422. pStatusFile := filepath.Join("/proc", pid, "task", pid, "status")
  423. out, err := procutils.NewRemoteCommandAsFarAsPossible("sh", "-c", fmt.Sprintf("cat %s | grep PPid: | awk '{print $2}'", pStatusFile)).Output()
  424. if err != nil {
  425. return errors.Wrapf(err, "get ppid from %s, out: %s", pStatusFile, out)
  426. }
  427. ppidStr := strings.TrimSpace(string(out))
  428. ppid, err := strconv.Atoi(ppidStr)
  429. if err != nil {
  430. return errors.Wrapf(err, "invalid ppid str %s from %s", ppidStr, pStatusFile)
  431. }
  432. ppCmdlineFile := filepath.Join("/proc", ppidStr, "cmdline")
  433. ppCmdline, err := procutils.NewRemoteCommandAsFarAsPossible("cat", ppCmdlineFile).Output()
  434. if err != nil {
  435. return errors.Wrapf(err, "get cmdline from %s, out: %s", ppCmdlineFile, ppCmdline)
  436. }
  437. log.Infof("try to kill container %s, pid %s parent process(%d): %s", ctrId, pid, ppid, ppCmdline)
  438. killOut, err := procutils.NewRemoteCommandAsFarAsPossible("kill", "-9", ppidStr).Output()
  439. if err != nil {
  440. killErr := errors.Wrapf(err, "kill -9 %s, out: %s", ppidStr, killOut)
  441. log.Errorf("kill container %s, pid %s parent process(%d): %s, error: %v", ctrId, pid, ppid, ppCmdline, killErr)
  442. return killErr
  443. }
  444. if err := c.stopContainerWithRetry(ctx, ctrId, 0, 5); err != nil {
  445. return errors.Wrapf(err, "stop container %s after kill parent process", ctrId)
  446. }
  447. return nil
  448. }
  449. func (c crictl) RemoveContainer(ctx context.Context, ctrId string) error {
  450. _, err := c.GetRuntimeClient().RemoveContainer(ctx, &runtimeapi.RemoveContainerRequest{
  451. ContainerId: ctrId,
  452. })
  453. if err != nil {
  454. return errors.Wrap(err, "RemoveContainer")
  455. }
  456. return nil
  457. }
  458. func (c crictl) ContainerStatus(ctx context.Context, ctrId string) (*runtimeapi.ContainerStatusResponse, error) {
  459. return c.containerStatus(ctx, ctrId, false)
  460. }
  461. func (c crictl) containerStatus(ctx context.Context, ctrId string, verbose bool) (*runtimeapi.ContainerStatusResponse, error) {
  462. req := &runtimeapi.ContainerStatusRequest{
  463. ContainerId: ctrId,
  464. Verbose: verbose,
  465. }
  466. return c.GetRuntimeClient().ContainerStatus(ctx, req)
  467. }
  468. func (c crictl) PullImage(ctx context.Context, req *runtimeapi.PullImageRequest) (*runtimeapi.PullImageResponse, error) {
  469. resp, err := c.GetImageClient().PullImage(ctx, req)
  470. if err != nil {
  471. return nil, errors.Wrapf(err, "PullImage")
  472. }
  473. return resp, nil
  474. }
  475. func (c crictl) ImageStatus(ctx context.Context, req *runtimeapi.ImageStatusRequest) (*runtimeapi.ImageStatusResponse, error) {
  476. return c.GetImageClient().ImageStatus(ctx, req)
  477. }
  478. type ExecSyncResponse struct {
  479. Stdout []byte
  480. Stderr []byte
  481. ExitCode int32
  482. }
  483. func (c crictl) ExecSync(ctx context.Context, ctrId string, command []string, timeout int64) (*ExecSyncResponse, error) {
  484. cli := c.GetRuntimeClient()
  485. resp, err := cli.ExecSync(ctx, &runtimeapi.ExecSyncRequest{
  486. ContainerId: ctrId,
  487. Cmd: command,
  488. Timeout: timeout,
  489. })
  490. if err != nil {
  491. return nil, errors.Wrapf(err, "exec sync container %s with command: %v", ctrId, command)
  492. }
  493. if resp.GetExitCode() != 0 {
  494. return nil, exec.CodeExitError{
  495. Err: errors.Errorf("stdout: %s, stderr: %s, exited: %d", resp.GetStdout(), resp.GetStderr(), resp.GetExitCode()),
  496. Code: int(resp.ExitCode),
  497. }
  498. }
  499. return &ExecSyncResponse{
  500. Stdout: resp.Stdout,
  501. Stderr: resp.Stderr,
  502. ExitCode: resp.ExitCode,
  503. }, nil
  504. }