Skip to content

Commit f73a579

Browse files
committed
support k8s pod
1 parent 9c15da4 commit f73a579

12 files changed

+405
-68
lines changed

README.md

+7-3
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,15 @@ GoShell is a simple terminal GUI client, written in Go,via [Fyne](https://fyne.i
1616
![GoShell SSH](screenshot/ssh-conf.png)
1717
### Docker Config
1818
![GoShell Docker](screenshot/docker-conf.png)
19-
### Docker Container
19+
### Docker Select Container
2020
![GoShell Docker](screenshot/docker-container.png)
21+
### K8S Config
22+
![GoShell Docker](screenshot/k8s-conf.png)
23+
### K8S Select Container
24+
![GoShell Docker](screenshot/k8s-container.png)
2125

2226
# TODOs
2327

2428
- UI/UX optimization
25-
- Configuration encryption
26-
- Supports K8S pod.
29+
- Configuration encryption
30+
- ~~Supports K8S pod.~~

config.go

+9-5
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@ import (
66
"fyne.io/fyne/v2/container"
77
"fyne.io/fyne/v2/dialog"
88
"fyne.io/fyne/v2/widget"
9-
"github.com/jinzhu/copier"
109
"log"
1110
)
1211

1312
type Config interface {
1413
Name() string
1514
Type() string
15+
Load(string) error
1616
Data() interface{}
1717
Form() *widget.Form
1818
Term(*Window)
@@ -97,15 +97,19 @@ func (w *Window) load() {
9797
var cfg Config
9898
switch t {
9999
case "ssh":
100-
cfg = &SSHConfig{}
100+
cfg = &SSHConfig{data: &SSHConfigData{}}
101101
case "docker":
102-
cfg = &DockerConfig{}
102+
cfg = &DockerConfig{data: &DockerConfigData{}}
103103
case "k8s":
104-
cfg = &K8SConfig{}
104+
cfg = &K8SConfig{data: &K8SConfigData{}}
105105
default:
106106
continue
107107
}
108-
err := copier.Copy(cfg, data)
108+
bytes, err := json.Marshal(data)
109+
if err != nil {
110+
continue
111+
}
112+
err = cfg.Load(string(bytes))
109113
if err != nil {
110114
continue
111115
}

docker.go

+20-9
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,19 @@ package main
22

33
import (
44
"context"
5+
"encoding/json"
56
"fyne.io/fyne/v2"
67
"fyne.io/fyne/v2/container"
78
"fyne.io/fyne/v2/dialog"
89
"fyne.io/fyne/v2/widget"
910
"github.com/docker/docker/api/types"
1011
"github.com/docker/docker/client"
11-
"github.com/fyne-io/terminal"
1212
)
1313

1414
type DockerConfigData struct {
15-
Name string
16-
Type string
17-
Host string
15+
Name string `json:"name,omitempty"`
16+
Type string `json:"type,omitempty"`
17+
Host string `json:"host,omitempty"`
1818
}
1919

2020
type DockerConfig struct {
@@ -27,9 +27,19 @@ func (c *DockerConfig) Name() string {
2727
}
2828

2929
func (c *DockerConfig) Type() string {
30-
return "ssh"
30+
return "docker"
3131
}
3232

33+
func (c *DockerConfig) Load(s string) error {
34+
data := &DockerConfigData{}
35+
36+
err := json.Unmarshal([]byte(s), data)
37+
if err != nil {
38+
return err
39+
}
40+
c.data = data
41+
return nil
42+
}
3343
func (c *DockerConfig) Data() interface{} {
3444
return c.data
3545
}
@@ -102,18 +112,19 @@ func (c *DockerConfig) Term(win *Window) {
102112
win.showError(err)
103113
return
104114
}
105-
term := terminal.New()
115+
116+
term := NewTerm(c.Name(), c)
117+
106118
go func() {
107119
defer attach.Close()
108-
err = term.RunWithConnection(attach.Conn, attach.Reader)
120+
err = term.RunWithConnection(attach.Conn)
109121
if err != nil {
110122
win.showError(err)
111123
return
112124
}
113125
}()
114126

115-
tab := &Term{name: c.Name(), term: term}
116-
win.AddTermTab(tab)
127+
win.AddTermTab(term)
117128
if dlg != nil {
118129
dlg.Hide()
119130
}

go.mod

+5-3
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,13 @@ require (
1010
github.com/docker/docker v17.12.0-ce-rc1.0.20201201034508-7d75c1d40d88+incompatible
1111
github.com/docker/go-units v0.5.0 // indirect
1212
github.com/fyne-io/terminal v0.0.0-20210419192104-b6a609f9c1bd
13-
github.com/gorilla/mux v1.8.0 // indirect
14-
github.com/jinzhu/copier v0.3.5
1513
github.com/sirupsen/logrus v1.9.0 // indirect
1614
github.com/srwiley/oksvg v0.0.0-20210320200257-875f767ac39a // indirect
17-
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5
15+
github.com/tk103331/stream v1.0.2
16+
golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd
1817
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9 // indirect
1918
google.golang.org/grpc v1.49.0 // indirect
19+
k8s.io/api v0.22.5
20+
k8s.io/apimachinery v0.22.5
21+
k8s.io/client-go v0.22.5
2022
)

go.sum

+26-5
Large diffs are not rendered by default.

k8s.go

+203-8
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,31 @@
11
package main
22

33
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"fyne.io/fyne/v2"
8+
"fyne.io/fyne/v2/dialog"
49
"fyne.io/fyne/v2/widget"
10+
"github.com/fyne-io/terminal"
11+
"github.com/tk103331/stream"
12+
corev1 "k8s.io/api/core/v1"
13+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
14+
"k8s.io/client-go/kubernetes"
15+
"k8s.io/client-go/kubernetes/scheme"
16+
"k8s.io/client-go/rest"
17+
"k8s.io/client-go/tools/remotecommand"
518
)
619

720
type K8SConfigData struct {
8-
Name string
9-
Type string
10-
Server string
11-
Token string
12-
User string
13-
Pswd string
21+
Name string `json:"name,omitempty"`
22+
Type string `json:"type,omitempty"`
23+
Server string `json:"server,omitempty"`
24+
Token string `json:"token,omitempty"`
1425
}
1526
type K8SConfig struct {
1627
data *K8SConfigData
28+
onOk func()
1729
}
1830

1931
func (c *K8SConfig) Name() string {
@@ -23,19 +35,202 @@ func (c *K8SConfig) Name() string {
2335
func (c *K8SConfig) Type() string {
2436
return "k8s"
2537
}
38+
func (c *K8SConfig) Load(s string) error {
39+
data := &K8SConfigData{}
2640

41+
err := json.Unmarshal([]byte(s), data)
42+
if err != nil {
43+
return err
44+
}
45+
c.data = data
46+
return nil
47+
}
2748
func (c *K8SConfig) Data() interface{} {
2849
return c.data
2950
}
3051

3152
func (c *K8SConfig) Form() *widget.Form {
32-
return widget.NewForm()
53+
nameEntry := widget.NewEntry()
54+
serverEntry := widget.NewEntry()
55+
tokenEntry := widget.NewEntry()
56+
tokenEntry.MultiLine = true
57+
tokenEntry.Wrapping = fyne.TextWrapBreak
58+
data := c.data
59+
if data != nil {
60+
nameEntry.Text = data.Name
61+
nameEntry.Disable()
62+
serverEntry.Text = data.Server
63+
tokenEntry.Text = data.Token
64+
}
65+
c.onOk = func() {
66+
if c.data == nil {
67+
c.data = &K8SConfigData{Type: c.Type()}
68+
}
69+
c.data.Name = nameEntry.Text
70+
c.data.Server = serverEntry.Text
71+
c.data.Token = tokenEntry.Text
72+
}
73+
return widget.NewForm([]*widget.FormItem{
74+
widget.NewFormItem("Name", nameEntry),
75+
widget.NewFormItem("Server", serverEntry),
76+
widget.NewFormItem("Token", tokenEntry),
77+
}...)
3378
}
3479

3580
func (c *K8SConfig) OnOk() {
81+
c.onOk()
82+
}
3683

84+
type ExecOpt struct {
85+
Namespace string
86+
PodName string
87+
Container string
3788
}
3889

3990
func (c *K8SConfig) Term(win *Window) {
40-
panic("implement me")
91+
cfg := c.data
92+
restCfg := &rest.Config{
93+
Host: cfg.Server,
94+
BearerToken: cfg.Token,
95+
BearerTokenFile: "",
96+
TLSClientConfig: rest.TLSClientConfig{
97+
Insecure: true,
98+
},
99+
}
100+
clientset, err := kubernetes.NewForConfig(restCfg)
101+
if err != nil {
102+
win.showError(err)
103+
return
104+
}
105+
namespaceList, err := clientset.CoreV1().Namespaces().List(context.Background(), metav1.ListOptions{})
106+
if err != nil {
107+
win.showError(err)
108+
return
109+
}
110+
s, _ := stream.New(namespaceList.Items)
111+
namespaces := make([]string, 0)
112+
s.Map(func(n corev1.Namespace) string {
113+
return n.ObjectMeta.Name
114+
}).ToSlice(&namespaces)
115+
116+
execOpt := &ExecOpt{}
117+
118+
containerSelect := widget.NewSelect(namespaces, func(container string) {
119+
execOpt.Container = container
120+
})
121+
122+
podSelect := widget.NewSelect(namespaces, func(pod string) {
123+
execOpt.PodName = pod
124+
containerSelect.ClearSelected()
125+
containerSelect.Options = []string{}
126+
127+
go func() {
128+
pod, err2 := clientset.CoreV1().Pods(execOpt.Namespace).Get(context.Background(), pod, metav1.GetOptions{})
129+
if err2 != nil {
130+
win.showError(err2)
131+
return
132+
}
133+
s, _ := stream.New(pod.Spec.Containers)
134+
containers := make([]string, 0)
135+
s.Map(func(n corev1.Container) string {
136+
return n.Name
137+
}).Limit(20).ToSlice(&containers)
138+
containerSelect.Options = containers
139+
}()
140+
141+
})
142+
143+
namespaceSelect := widget.NewSelect(namespaces, func(namespace string) {
144+
execOpt.Namespace = namespace
145+
podSelect.ClearSelected()
146+
podSelect.Options = []string{}
147+
148+
go func() {
149+
podList, err2 := clientset.CoreV1().Pods(namespace).List(context.Background(), metav1.ListOptions{})
150+
if err2 != nil {
151+
win.showError(err2)
152+
return
153+
}
154+
s, _ := stream.New(podList.Items)
155+
pods := make([]string, 0)
156+
s.Map(func(n corev1.Pod) string {
157+
return n.ObjectMeta.Name
158+
}).Limit(20).ToSlice(&pods)
159+
podSelect.Options = pods
160+
}()
161+
})
162+
namespaceSelect.Resize(fyne.Size{Height: 200})
163+
164+
form := widget.NewForm(widget.NewFormItem("Namespace", namespaceSelect), widget.NewFormItem("Pod", podSelect), widget.NewFormItem("Container", containerSelect))
165+
dlg := dialog.NewCustomConfirm("Select a container", "Connect", "Cancel", form, func(b bool) {
166+
if b {
167+
req := clientset.CoreV1().RESTClient().Post().
168+
Resource("pods").
169+
Name(execOpt.PodName).
170+
Namespace(execOpt.Namespace).
171+
SubResource("exec").
172+
VersionedParams(&corev1.PodExecOptions{
173+
Container: execOpt.Container,
174+
Command: []string{"bash"},
175+
Stdin: true,
176+
Stdout: true,
177+
Stderr: true,
178+
TTY: true,
179+
}, scheme.ParameterCodec)
180+
181+
executor, err := remotecommand.NewSPDYExecutor(restCfg, "POST", req.URL())
182+
if err != nil {
183+
win.showError(err)
184+
return
185+
}
186+
187+
termCfgChan := make(chan terminal.Config)
188+
189+
term := NewTerm(execOpt.PodName, c)
190+
191+
writer, reader := term.StartWithPipe(func(err error) {
192+
if err != nil {
193+
fmt.Println(err.Error())
194+
win.showError(err)
195+
}
196+
})
197+
198+
go func() {
199+
200+
err = executor.Stream(remotecommand.StreamOptions{
201+
Stdin: reader,
202+
Stdout: writer,
203+
Stderr: writer,
204+
Tty: true,
205+
TerminalSizeQueue: TermConfigSizeQueue(termCfgChan),
206+
})
207+
if err != nil {
208+
win.showError(err)
209+
return
210+
}
211+
}()
212+
213+
term.AddConfigListener(func(config *terminal.Config) {
214+
if config != nil {
215+
go func() {
216+
termCfgChan <- *config
217+
}()
218+
}
219+
})
220+
win.AddTermTab(term)
221+
}
222+
}, win.win)
223+
dlg.Resize(fyne.Size{Width: 400})
224+
dlg.Show()
225+
}
226+
227+
type TermConfigSizeQueue chan terminal.Config
228+
229+
func (t TermConfigSizeQueue) Next() *remotecommand.TerminalSize {
230+
cfg := <-t
231+
return &remotecommand.TerminalSize{Width: uint16(cfg.Columns), Height: uint16(cfg.Rows)}
232+
}
233+
234+
func (t TermConfigSizeQueue) Send(cfg terminal.Config) {
235+
t <- cfg
41236
}

0 commit comments

Comments
 (0)