podman

Форк
0
570 строк · 14.2 Кб
1
package containers
2

3
import (
4
	"bytes"
5
	"context"
6
	"encoding/binary"
7
	"errors"
8
	"fmt"
9
	"io"
10
	"net"
11
	"net/http"
12
	"net/url"
13
	"os"
14
	"reflect"
15
	"strconv"
16
	"time"
17

18
	"github.com/containers/common/pkg/detach"
19
	"github.com/containers/podman/v5/libpod/define"
20
	"github.com/containers/podman/v5/pkg/bindings"
21
	"github.com/moby/term"
22
	"github.com/sirupsen/logrus"
23
	terminal "golang.org/x/term"
24
)
25

26
// The CloseWriter interface is used to determine whether we can do a  one-sided
27
// close of a hijacked connection.
28
type CloseWriter interface {
29
	CloseWrite() error
30
}
31

32
// Attach attaches to a running container
33
func Attach(ctx context.Context, nameOrID string, stdin io.Reader, stdout io.Writer, stderr io.Writer, attachReady chan bool, options *AttachOptions) error {
34
	if options == nil {
35
		options = new(AttachOptions)
36
	}
37
	isSet := struct {
38
		stdin  bool
39
		stdout bool
40
		stderr bool
41
	}{
42
		stdin:  !(stdin == nil || reflect.ValueOf(stdin).IsNil()),
43
		stdout: !(stdout == nil || reflect.ValueOf(stdout).IsNil()),
44
		stderr: !(stderr == nil || reflect.ValueOf(stderr).IsNil()),
45
	}
46
	// Ensure golang can determine that interfaces are "really" nil
47
	if !isSet.stdin {
48
		stdin = (io.Reader)(nil)
49
	}
50
	if !isSet.stdout {
51
		stdout = (io.Writer)(nil)
52
	}
53
	if !isSet.stderr {
54
		stderr = (io.Writer)(nil)
55
	}
56

57
	conn, err := bindings.GetClient(ctx)
58
	if err != nil {
59
		return err
60
	}
61

62
	// Do we need to wire in stdin?
63
	ctnr, err := Inspect(ctx, nameOrID, new(InspectOptions).WithSize(false))
64
	if err != nil {
65
		return err
66
	}
67

68
	params, err := options.ToParams()
69
	if err != nil {
70
		return err
71
	}
72
	detachKeysInBytes := []byte{}
73
	if options.Changed("DetachKeys") {
74
		params.Add("detachKeys", options.GetDetachKeys())
75

76
		detachKeysInBytes, err = term.ToBytes(options.GetDetachKeys())
77
		if err != nil {
78
			return fmt.Errorf("invalid detach keys: %w", err)
79
		}
80
	}
81
	if isSet.stdin {
82
		params.Add("stdin", "true")
83
	}
84
	if isSet.stdout {
85
		params.Add("stdout", "true")
86
	}
87
	if isSet.stderr {
88
		params.Add("stderr", "true")
89
	}
90

91
	// Unless all requirements are met, don't use "stdin" is a terminal
92
	file, ok := stdin.(*os.File)
93
	outFile, outOk := stdout.(*os.File)
94
	needTTY := ok && outOk && terminal.IsTerminal(int(file.Fd())) && ctnr.Config.Tty
95
	if needTTY {
96
		state, err := setRawTerminal(file)
97
		if err != nil {
98
			return err
99
		}
100
		defer func() {
101
			if err := terminal.Restore(int(file.Fd()), state); err != nil {
102
				logrus.Errorf("Unable to restore terminal: %q", err)
103
			}
104
			logrus.SetFormatter(&logrus.TextFormatter{})
105
		}()
106
	}
107

108
	headers := make(http.Header)
109
	headers.Add("Connection", "Upgrade")
110
	headers.Add("Upgrade", "tcp")
111

112
	var socket net.Conn
113
	socketSet := false
114
	dialContext := conn.Client.Transport.(*http.Transport).DialContext
115
	t := &http.Transport{
116
		DialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
117
			c, err := dialContext(ctx, network, address)
118
			if err != nil {
119
				return nil, err
120
			}
121
			if !socketSet {
122
				socket = c
123
				socketSet = true
124
			}
125
			return c, err
126
		},
127
		IdleConnTimeout: time.Duration(0),
128
	}
129
	conn.Client.Transport = t
130
	response, err := conn.DoRequest(ctx, nil, http.MethodPost, "/containers/%s/attach", params, headers, nameOrID)
131
	if err != nil {
132
		return err
133
	}
134

135
	if !(response.IsSuccess() || response.IsInformational()) {
136
		defer response.Body.Close()
137
		return response.Process(nil)
138
	}
139

140
	if needTTY {
141
		winChange := make(chan os.Signal, 1)
142
		winCtx, winCancel := context.WithCancel(ctx)
143
		defer winCancel()
144
		notifyWinChange(winCtx, winChange, file, outFile)
145
		attachHandleResize(ctx, winCtx, winChange, false, nameOrID, file, outFile)
146
	}
147

148
	// If we are attaching around a start, we need to "signal"
149
	// back that we are in fact attached so that started does
150
	// not execute before we can attach.
151
	if attachReady != nil {
152
		attachReady <- true
153
	}
154

155
	stdoutChan := make(chan error)
156
	stdinChan := make(chan error, 1) // stdin channel should not block
157

158
	if isSet.stdin {
159
		go func() {
160
			logrus.Debugf("Copying STDIN to socket")
161

162
			_, err := detach.Copy(socket, stdin, detachKeysInBytes)
163
			if err != nil && err != define.ErrDetach {
164
				logrus.Errorf("Failed to write input to service: %v", err)
165
			}
166
			if err == nil {
167
				if closeWrite, ok := socket.(CloseWriter); ok {
168
					if err := closeWrite.CloseWrite(); err != nil {
169
						logrus.Warnf("Failed to close STDIN for writing: %v", err)
170
					}
171
				}
172
			}
173
			stdinChan <- err
174
		}()
175
	}
176

177
	buffer := make([]byte, 1024)
178
	if ctnr.Config.Tty {
179
		go func() {
180
			logrus.Debugf("Copying STDOUT of container in terminal mode")
181

182
			if !isSet.stdout {
183
				stdoutChan <- fmt.Errorf("container %q requires stdout to be set", ctnr.ID)
184
			}
185
			// If not multiplex'ed, read from server and write to stdout
186
			_, err := io.Copy(stdout, socket)
187

188
			stdoutChan <- err
189
		}()
190

191
		for {
192
			select {
193
			case err := <-stdoutChan:
194
				if err != nil {
195
					return err
196
				}
197

198
				return nil
199
			case err := <-stdinChan:
200
				if err != nil {
201
					return err
202
				}
203

204
				return nil
205
			}
206
		}
207
	} else {
208
		logrus.Debugf("Copying standard streams of container %q in non-terminal mode", ctnr.ID)
209
		for {
210
			// Read multiplexed channels and write to appropriate stream
211
			fd, l, err := DemuxHeader(socket, buffer)
212
			if err != nil {
213
				if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) {
214
					return nil
215
				}
216
				return err
217
			}
218
			frame, err := DemuxFrame(socket, buffer, l)
219
			if err != nil {
220
				return err
221
			}
222

223
			switch {
224
			case fd == 0:
225
				if isSet.stdout {
226
					if _, err := stdout.Write(frame[0:l]); err != nil {
227
						return err
228
					}
229
				}
230
			case fd == 1:
231
				if isSet.stdout {
232
					if _, err := stdout.Write(frame[0:l]); err != nil {
233
						return err
234
					}
235
				}
236
			case fd == 2:
237
				if isSet.stderr {
238
					if _, err := stderr.Write(frame[0:l]); err != nil {
239
						return err
240
					}
241
				}
242
			case fd == 3:
243
				return fmt.Errorf("from service from stream: %s", frame)
244
			default:
245
				return fmt.Errorf("unrecognized channel '%d' in header, 0-3 supported", fd)
246
			}
247
		}
248
	}
249
}
250

251
// DemuxHeader reads header for stream from server multiplexed stdin/stdout/stderr/2nd error channel
252
func DemuxHeader(r io.Reader, buffer []byte) (fd, sz int, err error) {
253
	n, err := io.ReadFull(r, buffer[0:8])
254
	if err != nil {
255
		return
256
	}
257
	if n < 8 {
258
		err = io.ErrUnexpectedEOF
259
		return
260
	}
261

262
	fd = int(buffer[0])
263
	if fd < 0 || fd > 3 {
264
		err = fmt.Errorf(`channel "%d" found, 0-3 supported: %w`, fd, ErrLostSync)
265
		return
266
	}
267

268
	sz = int(binary.BigEndian.Uint32(buffer[4:8]))
269
	return
270
}
271

272
// DemuxFrame reads contents for frame from server multiplexed stdin/stdout/stderr/2nd error channel
273
func DemuxFrame(r io.Reader, buffer []byte, length int) (frame []byte, err error) {
274
	if len(buffer) < length {
275
		buffer = append(buffer, make([]byte, length-len(buffer)+1)...)
276
	}
277

278
	n, err := io.ReadFull(r, buffer[0:length])
279
	if err != nil {
280
		return nil, err
281
	}
282
	if n < length {
283
		err = io.ErrUnexpectedEOF
284
		return
285
	}
286

287
	return buffer[0:length], nil
288
}
289

290
// ResizeContainerTTY sets container's TTY height and width in characters
291
func ResizeContainerTTY(ctx context.Context, nameOrID string, options *ResizeTTYOptions) error {
292
	if options == nil {
293
		options = new(ResizeTTYOptions)
294
	}
295
	return resizeTTY(ctx, bindings.JoinURL("containers", nameOrID, "resize"), options.Height, options.Width)
296
}
297

298
// ResizeExecTTY sets session's TTY height and width in characters
299
func ResizeExecTTY(ctx context.Context, sessionID string, options *ResizeExecTTYOptions) error {
300
	if options == nil {
301
		options = new(ResizeExecTTYOptions)
302
	}
303
	return resizeTTY(ctx, bindings.JoinURL("exec", sessionID, "resize"), options.Height, options.Width)
304
}
305

306
// resizeTTY set size of TTY of container
307
func resizeTTY(ctx context.Context, endpoint string, height *int, width *int) error {
308
	conn, err := bindings.GetClient(ctx)
309
	if err != nil {
310
		return err
311
	}
312

313
	params := url.Values{}
314
	if height != nil {
315
		params.Set("h", strconv.Itoa(*height))
316
	}
317
	if width != nil {
318
		params.Set("w", strconv.Itoa(*width))
319
	}
320
	params.Set("running", "true")
321
	rsp, err := conn.DoRequest(ctx, nil, http.MethodPost, endpoint, params, nil)
322
	if err != nil {
323
		return err
324
	}
325
	defer rsp.Body.Close()
326

327
	return rsp.Process(nil)
328
}
329

330
type rawFormatter struct {
331
	logrus.TextFormatter
332
}
333

334
func (f *rawFormatter) Format(entry *logrus.Entry) ([]byte, error) {
335
	buffer, err := f.TextFormatter.Format(entry)
336
	if err != nil {
337
		return buffer, err
338
	}
339
	return append(buffer, '\r'), nil
340
}
341

342
// This is intended to not be run as a goroutine, handling resizing for a container
343
// or exec session. It will call resize once and then starts a goroutine which calls resize on winChange
344
func attachHandleResize(ctx, winCtx context.Context, winChange chan os.Signal, isExec bool, id string, file *os.File, outFile *os.File) {
345
	resize := func() {
346
		w, h, err := getTermSize(file, outFile)
347
		if err != nil {
348
			logrus.Warnf("Failed to obtain TTY size: %v", err)
349
		}
350

351
		var resizeErr error
352
		if isExec {
353
			resizeErr = ResizeExecTTY(ctx, id, new(ResizeExecTTYOptions).WithHeight(h).WithWidth(w))
354
		} else {
355
			resizeErr = ResizeContainerTTY(ctx, id, new(ResizeTTYOptions).WithHeight(h).WithWidth(w))
356
		}
357
		if resizeErr != nil {
358
			logrus.Debugf("Failed to resize TTY: %v", resizeErr)
359
		}
360
	}
361

362
	resize()
363

364
	go func() {
365
		for {
366
			select {
367
			case <-winCtx.Done():
368
				return
369
			case <-winChange:
370
				resize()
371
			}
372
		}
373
	}()
374
}
375

376
// Configure the given terminal for raw mode
377
func setRawTerminal(file *os.File) (*terminal.State, error) {
378
	state, err := makeRawTerm(file)
379
	if err != nil {
380
		return nil, err
381
	}
382

383
	logrus.SetFormatter(&rawFormatter{})
384

385
	return state, err
386
}
387

388
// ExecStartAndAttach starts and attaches to a given exec session.
389
func ExecStartAndAttach(ctx context.Context, sessionID string, options *ExecStartAndAttachOptions) error {
390
	if options == nil {
391
		options = new(ExecStartAndAttachOptions)
392
	}
393
	conn, err := bindings.GetClient(ctx)
394
	if err != nil {
395
		return err
396
	}
397

398
	// TODO: Make this configurable (can't use streams' InputStream as it's
399
	// buffered)
400
	terminalFile := os.Stdin
401
	terminalOutFile := os.Stdout
402

403
	logrus.Debugf("Starting & Attaching to exec session ID %q", sessionID)
404

405
	// We need to inspect the exec session first to determine whether to use
406
	// -t.
407
	resp, err := conn.DoRequest(ctx, nil, http.MethodGet, "/exec/%s/json", nil, nil, sessionID)
408
	if err != nil {
409
		return err
410
	}
411
	defer resp.Body.Close()
412

413
	respStruct := new(define.InspectExecSession)
414
	if err := resp.Process(respStruct); err != nil {
415
		return err
416
	}
417
	isTerm := true
418
	if respStruct.ProcessConfig != nil {
419
		isTerm = respStruct.ProcessConfig.Tty
420
	}
421

422
	// If we are in TTY mode, we need to set raw mode for the terminal.
423
	// TODO: Share all of this with Attach() for containers.
424
	needTTY := terminalFile != nil && terminal.IsTerminal(int(terminalFile.Fd())) && isTerm
425

426
	body := struct {
427
		Detach bool   `json:"Detach"`
428
		TTY    bool   `json:"Tty"`
429
		Height uint16 `json:"h"`
430
		Width  uint16 `json:"w"`
431
	}{
432
		Detach: false,
433
		TTY:    needTTY,
434
	}
435

436
	if needTTY {
437
		state, err := setRawTerminal(terminalFile)
438
		if err != nil {
439
			return err
440
		}
441
		defer func() {
442
			if err := terminal.Restore(int(terminalFile.Fd()), state); err != nil {
443
				logrus.Errorf("Unable to restore terminal: %q", err)
444
			}
445
			logrus.SetFormatter(&logrus.TextFormatter{})
446
		}()
447
		w, h, err := getTermSize(terminalFile, terminalOutFile)
448
		if err != nil {
449
			logrus.Warnf("Failed to obtain TTY size: %v", err)
450
		}
451
		body.Width = uint16(w)
452
		body.Height = uint16(h)
453
	}
454

455
	bodyJSON, err := json.Marshal(body)
456
	if err != nil {
457
		return err
458
	}
459

460
	var socket net.Conn
461
	socketSet := false
462
	dialContext := conn.Client.Transport.(*http.Transport).DialContext
463
	t := &http.Transport{
464
		DialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
465
			c, err := dialContext(ctx, network, address)
466
			if err != nil {
467
				return nil, err
468
			}
469
			if !socketSet {
470
				socket = c
471
				socketSet = true
472
			}
473
			return c, err
474
		},
475
		IdleConnTimeout: time.Duration(0),
476
	}
477
	conn.Client.Transport = t
478
	response, err := conn.DoRequest(ctx, bytes.NewReader(bodyJSON), http.MethodPost, "/exec/%s/start", nil, nil, sessionID)
479
	if err != nil {
480
		return err
481
	}
482
	defer response.Body.Close()
483

484
	if !(response.IsSuccess() || response.IsInformational()) {
485
		return response.Process(nil)
486
	}
487

488
	if needTTY {
489
		winChange := make(chan os.Signal, 1)
490
		winCtx, winCancel := context.WithCancel(ctx)
491
		defer winCancel()
492

493
		notifyWinChange(winCtx, winChange, terminalFile, terminalOutFile)
494
		attachHandleResize(ctx, winCtx, winChange, true, sessionID, terminalFile, terminalOutFile)
495
	}
496

497
	if options.GetAttachInput() {
498
		go func() {
499
			logrus.Debugf("Copying STDIN to socket")
500
			_, err := detach.Copy(socket, options.InputStream, []byte{})
501
			if err != nil {
502
				logrus.Errorf("Failed to write input to service: %v", err)
503
			}
504

505
			if closeWrite, ok := socket.(CloseWriter); ok {
506
				logrus.Debugf("Closing STDIN")
507
				if err := closeWrite.CloseWrite(); err != nil {
508
					logrus.Warnf("Failed to close STDIN for writing: %v", err)
509
				}
510
			}
511
		}()
512
	}
513

514
	buffer := make([]byte, 1024)
515
	if isTerm {
516
		logrus.Debugf("Handling terminal attach to exec")
517
		if !options.GetAttachOutput() {
518
			return fmt.Errorf("exec session %s has a terminal and must have STDOUT enabled", sessionID)
519
		}
520
		// If not multiplex'ed, read from server and write to stdout
521
		_, err := detach.Copy(options.GetOutputStream(), socket, []byte{})
522
		if err != nil {
523
			return err
524
		}
525
	} else {
526
		logrus.Debugf("Handling non-terminal attach to exec")
527
		for {
528
			// Read multiplexed channels and write to appropriate stream
529
			fd, l, err := DemuxHeader(socket, buffer)
530
			if err != nil {
531
				if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) {
532
					return nil
533
				}
534
				return err
535
			}
536
			frame, err := DemuxFrame(socket, buffer, l)
537
			if err != nil {
538
				return err
539
			}
540

541
			switch {
542
			case fd == 0:
543
				if options.GetAttachInput() {
544
					// Write STDIN to STDOUT (echoing characters
545
					// typed by another attach session)
546
					if _, err := options.GetOutputStream().Write(frame[0:l]); err != nil {
547
						return err
548
					}
549
				}
550
			case fd == 1:
551
				if options.GetAttachOutput() {
552
					if _, err := options.GetOutputStream().Write(frame[0:l]); err != nil {
553
						return err
554
					}
555
				}
556
			case fd == 2:
557
				if options.GetAttachError() {
558
					if _, err := options.GetErrorStream().Write(frame[0:l]); err != nil {
559
						return err
560
					}
561
				}
562
			case fd == 3:
563
				return fmt.Errorf("from service from stream: %s", frame)
564
			default:
565
				return fmt.Errorf("unrecognized channel '%d' in header, 0-3 supported", fd)
566
			}
567
		}
568
	}
569
	return nil
570
}
571

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.