Repository navigation
Expand file tree
/
Copy pathnative_backend.go
More file actions
146 lines (129 loc) · 4.17 KB
/
Copy pathnative_backend.go
File metadata and controls
146 lines (129 loc) · 4.17 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"net/http"
"net/http/httptest"
"os"
"os/signal"
"sync"
"syscall"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/ssm"
"github.com/mmmorris1975/ssm-session-client/datachannel"
"golang.org/x/term"
)
// NativeBackend connects to ECS containers using the ssm-session-client
// library directly, without requiring the external session-manager-plugin binary.
type NativeBackend struct {
awsCfg aws.Config
}
// Connect establishes a session to an ECS container via the SSM data channel.
// It uses Open() (via a mock SSM endpoint returning the pre-existing session)
// to properly initialize internal message buffers and the outbound queue processor,
// which are required for reliable bidirectional communication.
func (b *NativeBackend) Connect(streamUrl, tokenValue string, verbose bool) error {
// Suppress internal library log output (ssm-session-client uses log.Printf)
if !verbose {
prevWriter := log.Writer()
prevFlags := log.Flags()
prevPrefix := log.Prefix()
log.SetOutput(io.Discard)
defer func() {
log.SetOutput(prevWriter)
log.SetFlags(prevFlags)
log.SetPrefix(prevPrefix)
}()
}
c, err := openDataChannelWithSession(b.awsCfg, streamUrl, tokenValue)
if err != nil {
return err
}
defer c.Close()
fd := int(os.Stdin.Fd())
var restoreOnce sync.Once
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
if term.IsTerminal(fd) {
oldState, err := term.MakeRaw(fd)
if err == nil {
defer restoreOnce.Do(func() { term.Restore(fd, oldState) })
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
defer signal.Stop(sigCh)
go func() {
<-sigCh
restoreOnce.Do(func() { term.Restore(fd, oldState) })
cancel()
}()
}
sendTerminalSize(c)
go watchTerminalSize(ctx, c)
}
// stdin -> datachannel in background (will be abandoned when output finishes)
go func() {
io.Copy(c, os.Stdin)
}()
// datachannel -> stdout (blocks until session closes)
_, err = io.Copy(os.Stdout, c)
cancel()
if err != nil && !errors.Is(err, io.EOF) {
return err
}
return nil
}
// openDataChannelWithSession creates a fully initialized SsmDataChannel using
// a pre-existing session (StreamUrl/TokenValue from ECS ExecuteCommand).
// It routes the SSM StartSession API call through a local mock endpoint that
// returns the existing session data, allowing Open() to do its full initialization
// (message buffers, outbound queue processor) before connecting to the real websocket.
// The provided aws.Config is reused (from ECSClient) to avoid a second credential
// resolution, with only the SSM endpoint overridden to point at the mock server.
func openDataChannelWithSession(baseCfg aws.Config, streamUrl, tokenValue string) (*datachannel.SsmDataChannel, error) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
resp := struct {
SessionId string `json:"SessionId"`
StreamUrl string `json:"StreamUrl"`
TokenValue string `json:"TokenValue"`
}{
SessionId: "ecssh-native",
StreamUrl: streamUrl,
TokenValue: tokenValue,
}
w.Header().Set("Content-Type", "application/x-amz-json-1.1")
json.NewEncoder(w).Encode(resp)
}))
defer ts.Close()
// Copy the base config and override only the SSM endpoint
cfg := baseCfg.Copy()
cfg.EndpointResolverWithOptions = aws.EndpointResolverWithOptionsFunc(
func(service, region string, options ...interface{}) (aws.Endpoint, error) {
if service == ssm.ServiceID {
return aws.Endpoint{
URL: ts.URL,
SigningRegion: region,
}, nil
}
return aws.Endpoint{}, &aws.EndpointNotFoundError{}
},
)
c := new(datachannel.SsmDataChannel)
if err := c.Open(cfg, &ssm.StartSessionInput{
Target: aws.String("ecssh-native"),
}); err != nil {
return nil, fmt.Errorf("failed to open session: %w", err)
}
return c, nil
}
func sendTerminalSize(c *datachannel.SsmDataChannel) {
width, height, err := term.GetSize(int(os.Stdout.Fd()))
if err != nil {
return
}
// term.GetSize returns (width, height) but SetTerminalSize expects (rows, cols)
c.SetTerminalSize(uint32(height), uint32(width))
}