Skip to content

Commit a36e6a5

Browse files
committed
Exec collector
1 parent b764050 commit a36e6a5

File tree

9 files changed

+326
-0
lines changed

9 files changed

+326
-0
lines changed

config/crds/troubleshoot.replicated.com_collectors.yaml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,33 @@ spec:
413413
- namespace
414414
- containerPath
415415
type: object
416+
exec:
417+
properties:
418+
args:
419+
items:
420+
type: string
421+
type: array
422+
command:
423+
items:
424+
type: string
425+
type: array
426+
containerName:
427+
type: string
428+
name:
429+
type: string
430+
namespace:
431+
type: string
432+
selector:
433+
items:
434+
type: string
435+
type: array
436+
timeout:
437+
type: string
438+
required:
439+
- name
440+
- selector
441+
- namespace
442+
type: object
416443
http:
417444
properties:
418445
get:

config/crds/troubleshoot.replicated.com_preflights.yaml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -635,6 +635,33 @@ spec:
635635
- namespace
636636
- containerPath
637637
type: object
638+
exec:
639+
properties:
640+
args:
641+
items:
642+
type: string
643+
type: array
644+
command:
645+
items:
646+
type: string
647+
type: array
648+
containerName:
649+
type: string
650+
name:
651+
type: string
652+
namespace:
653+
type: string
654+
selector:
655+
items:
656+
type: string
657+
type: array
658+
timeout:
659+
type: string
660+
required:
661+
- name
662+
- selector
663+
- namespace
664+
type: object
638665
http:
639666
properties:
640667
get:

config/crds/zz_generated.deepcopy.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,11 @@ func (in *Collect) DeepCopyInto(out *Collect) {
353353
*out = new(Run)
354354
(*in).DeepCopyInto(*out)
355355
}
356+
if in.Exec != nil {
357+
in, out := &in.Exec, &out.Exec
358+
*out = new(Exec)
359+
(*in).DeepCopyInto(*out)
360+
}
356361
if in.Copy != nil {
357362
in, out := &in.Copy, &out.Copy
358363
*out = new(Copy)
@@ -626,6 +631,36 @@ func (in *CustomResourceDefinition) DeepCopy() *CustomResourceDefinition {
626631
return out
627632
}
628633

634+
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
635+
func (in *Exec) DeepCopyInto(out *Exec) {
636+
*out = *in
637+
if in.Selector != nil {
638+
in, out := &in.Selector, &out.Selector
639+
*out = make([]string, len(*in))
640+
copy(*out, *in)
641+
}
642+
if in.Command != nil {
643+
in, out := &in.Command, &out.Command
644+
*out = make([]string, len(*in))
645+
copy(*out, *in)
646+
}
647+
if in.Args != nil {
648+
in, out := &in.Args, &out.Args
649+
*out = make([]string, len(*in))
650+
copy(*out, *in)
651+
}
652+
}
653+
654+
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Exec.
655+
func (in *Exec) DeepCopy() *Exec {
656+
if in == nil {
657+
return nil
658+
}
659+
out := new(Exec)
660+
in.DeepCopyInto(out)
661+
return out
662+
}
663+
629664
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
630665
func (in *Get) DeepCopyInto(out *Get) {
631666
*out = *in

config/samples/troubleshoot_v1beta1_collector.yaml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,30 @@ spec:
2323
# command: ["ping"]
2424
# args: ["www.google.com"]
2525
# timeout: 5s
26+
- exec:
27+
name: mysql-vars
28+
selector:
29+
- app=mysql
30+
namespace: default
31+
command: ["mysql"]
32+
args: ["-ureplicated", "-ppassword", "-e", "show processlist"]
33+
timeout: 60m
34+
- exec:
35+
name: hosts
36+
selector:
37+
- app=graphql-api
38+
namespace: default
39+
command: ["cat"]
40+
args: ["/etc/hosts"]
41+
timeout: 60m
42+
- exec:
43+
name: broken
44+
selector:
45+
- app=graphql-api
46+
namespace: default
47+
command: ["cat"]
48+
args: ["/etc/hostdasddsda"]
49+
timeout: 60m
2650
# - copy:
2751
# selector:
2852
# - app=illmannered-cricket-mysql

pkg/apis/troubleshoot/v1beta1/collector_shared.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,16 @@ type Run struct {
3434
ImagePullPolicy string `json:"imagePullPolicy,omitempty" yaml:"imagePullPolicy,omitempty"`
3535
}
3636

37+
type Exec struct {
38+
Name string `json:"name" yaml:"name"`
39+
Selector []string `json:"selector" yaml:"selector"`
40+
Namespace string `json:"namespace" yaml:"namespace"`
41+
ContainerName string `json:"containerName,omitempty" yaml:"containerName,omitempty"`
42+
Command []string `json:"command,omitempty" yaml:"command,omitempty"`
43+
Args []string `json:"args,omitempty" yaml:"args,omitempty"`
44+
Timeout string `json:"timeout,omitempty" yaml:"timeout,omitempty"`
45+
}
46+
3747
type Copy struct {
3848
Selector []string `json:"selector" yaml:"selector"`
3949
Namespace string `json:"namespace" yaml:"namespace"`
@@ -74,6 +84,7 @@ type Collect struct {
7484
Secret *Secret `json:"secret,omitempty" yaml:"secret,omitempty"`
7585
Logs *Logs `json:"logs,omitempty" yaml:"logs,omitempty"`
7686
Run *Run `json:"run,omitempty" yaml:"run,omitempty"`
87+
Exec *Exec `json:"exec,omitempty" yaml:"exec,omitempty"`
7788
Copy *Copy `json:"copy,omitempty" yaml:"copy,omitempty"`
7889
HTTP *HTTP `json:"http,omitempty" yaml:"http,omitempty"`
7990
}

pkg/apis/troubleshoot/v1beta1/zz_generated.deepcopy.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,11 @@ func (in *Collect) DeepCopyInto(out *Collect) {
369369
*out = new(Run)
370370
(*in).DeepCopyInto(*out)
371371
}
372+
if in.Exec != nil {
373+
in, out := &in.Exec, &out.Exec
374+
*out = new(Exec)
375+
(*in).DeepCopyInto(*out)
376+
}
372377
if in.Copy != nil {
373378
in, out := &in.Copy, &out.Copy
374379
*out = new(Copy)
@@ -642,6 +647,36 @@ func (in *CustomResourceDefinition) DeepCopy() *CustomResourceDefinition {
642647
return out
643648
}
644649

650+
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
651+
func (in *Exec) DeepCopyInto(out *Exec) {
652+
*out = *in
653+
if in.Selector != nil {
654+
in, out := &in.Selector, &out.Selector
655+
*out = make([]string, len(*in))
656+
copy(*out, *in)
657+
}
658+
if in.Command != nil {
659+
in, out := &in.Command, &out.Command
660+
*out = make([]string, len(*in))
661+
copy(*out, *in)
662+
}
663+
if in.Args != nil {
664+
in, out := &in.Args, &out.Args
665+
*out = make([]string, len(*in))
666+
copy(*out, *in)
667+
}
668+
}
669+
670+
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Exec.
671+
func (in *Exec) DeepCopy() *Exec {
672+
if in == nil {
673+
return nil
674+
}
675+
out := new(Exec)
676+
in.DeepCopyInto(out)
677+
return out
678+
}
679+
645680
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
646681
func (in *Get) DeepCopyInto(out *Get) {
647682
*out = *in

pkg/collect/collector.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ func (c *Collector) RunCollectorSync() error {
3333
if collect.Run != nil {
3434
return Run(collect.Run, c.Redact)
3535
}
36+
if collect.Exec != nil {
37+
return Exec(collect.Exec, c.Redact)
38+
}
3639
if collect.Copy != nil {
3740
return Copy(collect.Copy, c.Redact)
3841
}

pkg/collect/exec.go

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
package collect
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"errors"
7+
"fmt"
8+
"time"
9+
10+
troubleshootv1beta1 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta1"
11+
corev1 "k8s.io/api/core/v1"
12+
"k8s.io/apimachinery/pkg/runtime"
13+
"k8s.io/client-go/kubernetes"
14+
"k8s.io/client-go/tools/remotecommand"
15+
"sigs.k8s.io/controller-runtime/pkg/client/config"
16+
)
17+
18+
type ExecOutput struct {
19+
Results map[string][]byte `json:"exec/,omitempty"`
20+
}
21+
22+
type execResult struct {
23+
Stdout string `json:"stdout,omitempty"`
24+
Stderr string `json:"stderr,omitempty"`
25+
Error error `json:"error,omitempty"`
26+
}
27+
28+
func Exec(execCollector *troubleshootv1beta1.Exec, redact bool) error {
29+
if execCollector.Timeout == "" {
30+
return execWithoutTimeout(execCollector, redact)
31+
}
32+
33+
timeout, err := time.ParseDuration(execCollector.Timeout)
34+
if err != nil {
35+
return err
36+
}
37+
38+
execChan := make(chan error, 1)
39+
go func() {
40+
execChan <- execWithoutTimeout(execCollector, redact)
41+
}()
42+
43+
select {
44+
case <-time.After(timeout):
45+
return errors.New("timeout")
46+
case err := <-execChan:
47+
return err
48+
}
49+
}
50+
51+
func execWithoutTimeout(execCollector *troubleshootv1beta1.Exec, redact bool) error {
52+
cfg, err := config.GetConfig()
53+
if err != nil {
54+
return err
55+
}
56+
57+
client, err := kubernetes.NewForConfig(cfg)
58+
if err != nil {
59+
return err
60+
}
61+
62+
pods, err := listPodsInSelectors(client, execCollector.Namespace, execCollector.Selector)
63+
if err != nil {
64+
return err
65+
}
66+
67+
execOutput := &ExecOutput{
68+
Results: make(map[string][]byte),
69+
}
70+
71+
for _, pod := range pods {
72+
output, err := getExecOutputs(client, pod, execCollector, redact)
73+
if err != nil {
74+
return err
75+
}
76+
77+
b, err := json.MarshalIndent(output, "", " ")
78+
if err != nil {
79+
return err
80+
}
81+
82+
execOutput.Results[fmt.Sprintf("%s/%s/%s", pod.Namespace, pod.Name, execCollector.Name)] = b
83+
}
84+
85+
if redact {
86+
execOutput, err = execOutput.Redact()
87+
if err != nil {
88+
return err
89+
}
90+
}
91+
92+
b, err := json.MarshalIndent(execOutput, "", " ")
93+
if err != nil {
94+
return err
95+
}
96+
97+
fmt.Printf("%s\n", b)
98+
99+
return nil
100+
}
101+
102+
func getExecOutputs(client *kubernetes.Clientset, pod corev1.Pod, execCollector *troubleshootv1beta1.Exec, doRedact bool) (*execResult, error) {
103+
cfg, err := config.GetConfig()
104+
if err != nil {
105+
return nil, err
106+
}
107+
108+
container := pod.Spec.Containers[0].Name
109+
if execCollector.ContainerName != "" {
110+
container = execCollector.ContainerName
111+
}
112+
113+
req := client.CoreV1().RESTClient().Post().Resource("pods").Name(pod.Name).Namespace(pod.Namespace).SubResource("exec")
114+
scheme := runtime.NewScheme()
115+
if err := corev1.AddToScheme(scheme); err != nil {
116+
return nil, err
117+
}
118+
119+
parameterCodec := runtime.NewParameterCodec(scheme)
120+
req.VersionedParams(&corev1.PodExecOptions{
121+
Command: append(execCollector.Command, execCollector.Args...),
122+
Container: container,
123+
Stdin: true,
124+
Stdout: false,
125+
Stderr: true,
126+
TTY: false,
127+
}, parameterCodec)
128+
129+
exec, err := remotecommand.NewSPDYExecutor(cfg, "POST", req.URL())
130+
if err != nil {
131+
return nil, err
132+
}
133+
134+
stdout := new(bytes.Buffer)
135+
stderr := new(bytes.Buffer)
136+
137+
err = exec.Stream(remotecommand.StreamOptions{
138+
Stdin: nil,
139+
Stdout: stdout,
140+
Stderr: stderr,
141+
Tty: false,
142+
})
143+
144+
return &execResult{
145+
Stdout: stdout.String(),
146+
Stderr: stderr.String(),
147+
Error: err,
148+
}, nil
149+
}
150+
151+
func (r *ExecOutput) Redact() (*ExecOutput, error) {
152+
results, err := redactMap(r.Results)
153+
if err != nil {
154+
return nil, err
155+
}
156+
157+
return &ExecOutput{
158+
Results: results,
159+
}, nil
160+
}

0 commit comments

Comments
 (0)