@@ -3,18 +3,24 @@ package collect
33import (
44 "bytes"
55 "encoding/json"
6+ "fmt"
7+ "os"
68 "os/exec"
79 "path/filepath"
810 "strings"
911
1012 "github.com/pkg/errors"
1113 troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
14+ "k8s.io/klog/v2"
1215)
1316
1417type HostRunInfo struct {
15- Command string `json:"command"`
16- ExitCode string `json:"exitCode"`
17- Error string `json:"error"`
18+ Command string `json:"command"`
19+ ExitCode string `json:"exitCode"`
20+ Error string `json:"error"`
21+ OutputDir string `json:"outputDir"`
22+ Input string `json:"input"`
23+ Env []string `json:"env"`
1824}
1925
2026type CollectHostRun struct {
@@ -31,20 +37,83 @@ func (c *CollectHostRun) IsExcluded() (bool, error) {
3137}
3238
3339func (c * CollectHostRun ) Collect (progressChan chan <- interface {}) (map [string ][]byte , error ) {
34- runHostCollector := c .hostCollector
40+ var (
41+ cmdOutputTempDir string
42+ cmdInputTempDir string
43+ bundleOutputRelativePath string
44+ )
3545
36- cmd := exec .Command (runHostCollector .Command , runHostCollector .Args ... )
46+ runHostCollector := c .hostCollector
47+ collectorName := runHostCollector .CollectorName
48+ if collectorName == "" {
49+ collectorName = "run-host"
50+ }
3751
38- var stdout , stderr bytes.Buffer
39- cmd .Stdout = & stdout
40- cmd .Stderr = & stderr
52+ cmd := exec .Command (c .attemptToConvertCmdToAbsPath (), runHostCollector .Args ... )
4153
42- runInfo := HostRunInfo {
54+ klog .V (2 ).Infof ("Run host collector command: %q" , cmd .String ())
55+ runInfo := & HostRunInfo {
4356 Command : cmd .String (),
4457 ExitCode : "0" ,
4558 }
4659
47- err := cmd .Run ()
60+ err := c .processEnvVars (cmd )
61+ if err != nil {
62+ return nil , errors .Wrap (err , "failed to parse env variable" )
63+ }
64+
65+ // Create a working directory for the command
66+ wkdir , err := os .MkdirTemp ("" , collectorName )
67+ defer os .RemoveAll (wkdir )
68+ if err != nil {
69+ return nil , errors .Wrap (err , "failed to create temp dir for host run" )
70+ }
71+ // Change the working directory for the command to ensure the command
72+ // does not polute the parent/caller working directory
73+ cmd .Dir = wkdir
74+
75+ // if we choose to save result for the command run
76+ if runHostCollector .OutputDir != "" {
77+ cmdOutputTempDir = filepath .Join (wkdir , runHostCollector .OutputDir )
78+ err = os .MkdirAll (cmdOutputTempDir , 0755 )
79+ if err != nil {
80+ return nil , errors .New (fmt .Sprintf ("failed to create dir for: %s" , runHostCollector .OutputDir ))
81+ }
82+ cmd .Env = append (cmd .Env ,
83+ fmt .Sprintf ("TS_WORKSPACE_DIR=%s" , cmdOutputTempDir ),
84+ )
85+ }
86+
87+ if runHostCollector .Input != nil {
88+ cmdInputTempDir = filepath .Join (wkdir , "input" )
89+ err = os .MkdirAll (cmdInputTempDir , 0755 )
90+ if err != nil {
91+ return nil , errors .New ("failed to create temp dir for host run input" )
92+ }
93+ for inFilename , inFileContent := range runHostCollector .Input {
94+ if strings .Contains (inFilename , "/" ) {
95+ return nil , errors .New ("Input filename contains '/'" )
96+ }
97+ cmdInputFilePath := filepath .Join (cmdInputTempDir , inFilename )
98+ err = os .WriteFile (cmdInputFilePath , []byte (inFileContent ), 0644 )
99+ if err != nil {
100+ return nil , errors .Wrap (err , fmt .Sprintf ("failed to write input file: %s to temp directory" , inFilename ))
101+ }
102+ }
103+ cmd .Env = append (cmd .Env ,
104+ fmt .Sprintf ("TS_INPUT_DIR=%s" , cmdInputTempDir ),
105+ )
106+ }
107+
108+ collectorRelativePath := filepath .Join ("host-collectors/run-host" , collectorName )
109+
110+ runInfo .Env = cmd .Env
111+
112+ var stdout , stderr bytes.Buffer
113+ cmd .Stdout = & stdout
114+ cmd .Stderr = & stderr
115+
116+ err = cmd .Run ()
48117 if err != nil {
49118 if werr , ok := err .(* exec.ExitError ); ok {
50119 runInfo .ExitCode = strings .TrimPrefix (werr .Error (), "exit status " )
@@ -54,10 +123,7 @@ func (c *CollectHostRun) Collect(progressChan chan<- interface{}) (map[string][]
54123 }
55124 }
56125
57- collectorName := c .hostCollector .CollectorName
58- if collectorName == "" {
59- collectorName = "run-host"
60- }
126+ output := NewResult ()
61127 resultInfo := filepath .Join ("host-collectors/run-host" , collectorName + "-info.json" )
62128 result := filepath .Join ("host-collectors/run-host" , collectorName + ".txt" )
63129
@@ -66,14 +132,89 @@ func (c *CollectHostRun) Collect(progressChan chan<- interface{}) (map[string][]
66132 return nil , errors .Wrap (err , "failed to marshal run host result" )
67133 }
68134
69- output := NewResult ()
70135 output .SaveResult (c .BundlePath , resultInfo , bytes .NewBuffer (b ))
71136 output .SaveResult (c .BundlePath , result , bytes .NewBuffer (stdout .Bytes ()))
137+ // walkthrough the output directory and save result for each file
138+ if runHostCollector .OutputDir != "" {
139+ runInfo .OutputDir = runHostCollector .OutputDir
140+ bundleOutputRelativePath = filepath .Join (collectorRelativePath , runHostCollector .OutputDir )
141+ klog .V (2 ).Infof ("Saving command output to %q in bundle" , bundleOutputRelativePath )
142+ output .SaveResults (c .BundlePath , bundleOutputRelativePath , cmdOutputTempDir )
143+ }
144+
145+ return output , nil
146+ }
147+
148+ func (c * CollectHostRun ) processEnvVars (cmd * exec.Cmd ) error {
149+ runHostCollector := c .hostCollector
150+
151+ if runHostCollector .IgnoreParentEnvs {
152+ klog .V (2 ).Info ("Not inheriting the environment variables!" )
153+ if runHostCollector .InheritEnvs != nil {
154+ klog .V (2 ).Infof ("The following environment variables will not be loaded to the command: [%s]" ,
155+ strings .Join (runHostCollector .InheritEnvs , "," ))
156+ }
157+ // clears the parent env vars
158+ cmd .Env = []string {}
159+ populateGuaranteedEnvVars (cmd )
160+ } else if runHostCollector .InheritEnvs != nil {
161+ for _ , key := range runHostCollector .InheritEnvs {
162+ envVal , found := os .LookupEnv (key )
163+ if ! found {
164+ return errors .New (fmt .Sprintf ("inherit env variable is not found: %s" , key ))
165+ }
166+ cmd .Env = append (cmd .Env ,
167+ fmt .Sprintf ("%s=%s" , key , envVal ))
168+ }
169+ populateGuaranteedEnvVars (cmd )
170+ } else {
171+ cmd .Env = os .Environ ()
172+ }
72173
73- runHostOutput := map [string ][]byte {
74- resultInfo : b ,
75- result : stdout .Bytes (),
174+ if runHostCollector .Env != nil {
175+ for i := range runHostCollector .Env {
176+ parts := strings .Split (runHostCollector .Env [i ], "=" )
177+ if len (parts ) == 2 && parts [0 ] != "" && parts [1 ] != "" {
178+ cmd .Env = append (cmd .Env ,
179+ fmt .Sprintf ("%s" , runHostCollector .Env [i ]))
180+ } else {
181+ return errors .New (fmt .Sprintf ("env variable entry is missing '=' : %s" , runHostCollector .Env [i ]))
182+ }
183+ }
184+ }
185+
186+ return nil
187+ }
188+
189+ func populateGuaranteedEnvVars (cmd * exec.Cmd ) {
190+ guaranteedEnvs := []string {"PATH" , "KUBECONFIG" , "PWD" }
191+ for _ , key := range guaranteedEnvs {
192+ guaranteedEnvVal , found := os .LookupEnv (key )
193+ if found {
194+ cmd .Env = append (cmd .Env ,
195+ fmt .Sprintf ("%s=%s" , key , guaranteedEnvVal ))
196+ }
197+ }
198+ }
199+
200+ // attemptToConvertCmdToAbsPath checks if the command is a file path or command name
201+ // If it is a file path, it will return the absolute path else
202+ // it will return the command name as is and leave the resolution to cmd.Run()
203+ // This enables passing commands using relative paths e.g. "./my-command"
204+ // which is not possible with cmd.Run() since the child process runs
205+ // in a different working directory
206+ func (c * CollectHostRun ) attemptToConvertCmdToAbsPath () string {
207+ // Attempt to check if the command is file path or command name
208+ cmdAbsPath , err := filepath .Abs (c .hostCollector .Command )
209+ if err != nil {
210+ return c .hostCollector .Command
211+ }
212+
213+ // Check if the file exists
214+ _ , err = os .Stat (cmdAbsPath )
215+ if err != nil {
216+ return c .hostCollector .Command
76217 }
77218
78- return runHostOutput , nil
219+ return cmdAbsPath
79220}
0 commit comments