@@ -22,21 +22,31 @@ import (
22
22
"context"
23
23
"fmt"
24
24
"io"
25
+ "net/http"
26
+ "os"
27
+ "strings"
25
28
"time"
26
29
27
30
"github.com/docker/compose-cli/api/compose"
28
31
"github.com/docker/compose-cli/utils"
29
32
"golang.org/x/sync/errgroup"
30
33
corev1 "k8s.io/api/core/v1"
31
34
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
35
+ "k8s.io/apimachinery/pkg/runtime"
32
36
"k8s.io/cli-runtime/pkg/genericclioptions"
33
37
"k8s.io/client-go/kubernetes"
38
+ "k8s.io/client-go/rest"
39
+ "k8s.io/client-go/tools/portforward"
40
+ "k8s.io/client-go/tools/remotecommand"
41
+ "k8s.io/client-go/transport/spdy"
34
42
)
35
43
36
44
// KubeClient API to access kube objects
37
45
type KubeClient struct {
38
46
client * kubernetes.Clientset
39
47
namespace string
48
+ config * rest.Config
49
+ ioStreams genericclioptions.IOStreams
40
50
}
41
51
42
52
// NewKubeClient new kubernetes client
@@ -48,7 +58,7 @@ func NewKubeClient(config genericclioptions.RESTClientGetter) (*KubeClient, erro
48
58
49
59
clientset , err := kubernetes .NewForConfig (restConfig )
50
60
if err != nil {
51
- return nil , err
61
+ return nil , fmt . Errorf ( "failed creating clientset. Error: %+v" , err )
52
62
}
53
63
54
64
namespace , _ , err := config .ToRawKubeConfigLoader ().Namespace ()
@@ -59,9 +69,84 @@ func NewKubeClient(config genericclioptions.RESTClientGetter) (*KubeClient, erro
59
69
return & KubeClient {
60
70
client : clientset ,
61
71
namespace : namespace ,
72
+ config : restConfig ,
73
+ ioStreams : genericclioptions.IOStreams {In : os .Stdin , Out : os .Stdout , ErrOut : os .Stderr },
62
74
}, nil
63
75
}
64
76
77
+ // GetPod retrieves a service pod
78
+ func (kc KubeClient ) GetPod (ctx context.Context , projectName , serviceName string ) (* corev1.Pod , error ) {
79
+ pods , err := kc .client .CoreV1 ().Pods (kc .namespace ).List (ctx , metav1.ListOptions {
80
+ LabelSelector : fmt .Sprintf ("%s=%s" , compose .ProjectTag , projectName ),
81
+ })
82
+ if err != nil {
83
+ return nil , err
84
+ }
85
+ if pods == nil {
86
+ return nil , nil
87
+ }
88
+ var pod corev1.Pod
89
+ for _ , p := range pods .Items {
90
+ service := p .Labels [compose .ServiceTag ]
91
+ if service == serviceName {
92
+ pod = p
93
+ break
94
+ }
95
+ }
96
+ return & pod , nil
97
+ }
98
+
99
+ // Exec executes a command in a container
100
+ func (kc KubeClient ) Exec (ctx context.Context , projectName string , opts compose.RunOptions ) error {
101
+ pod , err := kc .GetPod (ctx , projectName , opts .Service )
102
+ if err != nil || pod == nil {
103
+ return err
104
+ }
105
+ if len (pod .Spec .Containers ) == 0 {
106
+ return fmt .Errorf ("no containers running in pod %s" , pod .Name )
107
+ }
108
+ // get first container in the pod
109
+ container := & pod .Spec .Containers [0 ]
110
+ containerName := container .Name
111
+
112
+ req := kc .client .CoreV1 ().RESTClient ().Post ().
113
+ Resource ("pods" ).
114
+ Name (pod .Name ).
115
+ Namespace (kc .namespace ).
116
+ SubResource ("exec" )
117
+
118
+ option := & corev1.PodExecOptions {
119
+ Container : containerName ,
120
+ Command : opts .Command ,
121
+ Stdin : true ,
122
+ Stdout : true ,
123
+ Stderr : true ,
124
+ TTY : opts .Tty ,
125
+ }
126
+
127
+ if opts .Reader == nil {
128
+ option .Stdin = false
129
+ }
130
+
131
+ scheme := runtime .NewScheme ()
132
+ if err := corev1 .AddToScheme (scheme ); err != nil {
133
+ return fmt .Errorf ("error adding to scheme: %v" , err )
134
+ }
135
+ parameterCodec := runtime .NewParameterCodec (scheme )
136
+ req .VersionedParams (option , parameterCodec )
137
+
138
+ exec , err := remotecommand .NewSPDYExecutor (kc .config , "POST" , req .URL ())
139
+ if err != nil {
140
+ return err
141
+ }
142
+ return exec .Stream (remotecommand.StreamOptions {
143
+ Stdin : opts .Reader ,
144
+ Stdout : opts .Writer ,
145
+ Stderr : opts .Writer ,
146
+ Tty : opts .Tty ,
147
+ })
148
+ }
149
+
65
150
// GetContainers get containers for a given compose project
66
151
func (kc KubeClient ) GetContainers (ctx context.Context , projectName string , all bool ) ([]compose.ContainerSummary , error ) {
67
152
fieldSelector := ""
@@ -76,9 +161,39 @@ func (kc KubeClient) GetContainers(ctx context.Context, projectName string, all
76
161
if err != nil {
77
162
return nil , err
78
163
}
164
+ services := map [string ][]compose.PortPublisher {}
79
165
result := []compose.ContainerSummary {}
80
166
for _ , pod := range pods .Items {
81
- result = append (result , podToContainerSummary (pod ))
167
+ summary := podToContainerSummary (pod )
168
+ serviceName := pod .GetObjectMeta ().GetLabels ()[compose .ServiceTag ]
169
+ ports , ok := services [serviceName ]
170
+ if ! ok {
171
+ s , err := kc .client .CoreV1 ().Services (kc .namespace ).Get (ctx , serviceName , metav1.GetOptions {})
172
+ if err != nil {
173
+ if ! strings .Contains (err .Error (), "not found" ) {
174
+ return nil , err
175
+ }
176
+ result = append (result , summary )
177
+ continue
178
+ }
179
+ ports = []compose.PortPublisher {}
180
+ if s != nil {
181
+ if s .Spec .Type == corev1 .ServiceTypeLoadBalancer {
182
+ if len (s .Status .LoadBalancer .Ingress ) > 0 {
183
+ port := compose.PortPublisher {URL : s .Status .LoadBalancer .Ingress [0 ].IP }
184
+ if len (s .Spec .Ports ) > 0 {
185
+ port .URL = fmt .Sprintf ("%s:%d" , port .URL , s .Spec .Ports [0 ].Port )
186
+ port .TargetPort = s .Spec .Ports [0 ].TargetPort .IntValue ()
187
+ port .Protocol = string (s .Spec .Ports [0 ].Protocol )
188
+ }
189
+ ports = append (ports , port )
190
+ }
191
+ }
192
+ }
193
+ services [serviceName ] = ports
194
+ }
195
+ summary .Publishers = ports
196
+ result = append (result , summary )
82
197
}
83
198
84
199
return result , nil
@@ -161,3 +276,42 @@ func (kc KubeClient) WaitForPodState(ctx context.Context, opts WaitForStatusOpti
161
276
}
162
277
return nil
163
278
}
279
+
280
+ //MapPortsToLocalhost runs a port-forwarder daemon process
281
+ func (kc KubeClient ) MapPortsToLocalhost (ctx context.Context , opts PortMappingOptions ) error {
282
+ stopChannel := make (chan struct {}, 1 )
283
+ readyChannel := make (chan struct {})
284
+
285
+ eg , ctx := errgroup .WithContext (ctx )
286
+ for serviceName , servicePorts := range opts .Services {
287
+ serviceName := serviceName
288
+ servicePorts := servicePorts
289
+ pod , err := kc .GetPod (ctx , opts .ProjectName , serviceName )
290
+ if err != nil {
291
+ return err
292
+ }
293
+ eg .Go (func () error {
294
+ ports := []string {}
295
+ for _ , p := range servicePorts {
296
+ ports = append (ports , fmt .Sprintf ("%d:%d" , p .PublishedPort , p .TargetPort ))
297
+ }
298
+
299
+ req := kc .client .CoreV1 ().RESTClient ().Post ().
300
+ Resource ("pods" ).
301
+ Name (pod .Name ).
302
+ Namespace (kc .namespace ).
303
+ SubResource ("portforward" )
304
+ transport , upgrader , err := spdy .RoundTripperFor (kc .config )
305
+ if err != nil {
306
+ return err
307
+ }
308
+ dialer := spdy .NewDialer (upgrader , & http.Client {Transport : transport }, "POST" , req .URL ())
309
+ fw , err := portforward .New (dialer , ports , stopChannel , readyChannel , os .Stdout , os .Stderr )
310
+ if err != nil {
311
+ return err
312
+ }
313
+ return fw .ForwardPorts ()
314
+ })
315
+ }
316
+ return eg .Wait ()
317
+ }
0 commit comments