sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/pkg/pod-utils/wrapper/options.go (about) 1 /* 2 Copyright 2018 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package wrapper 18 19 import ( 20 "context" 21 "errors" 22 "flag" 23 "fmt" 24 "os" 25 "path/filepath" 26 "strconv" 27 "strings" 28 "time" 29 30 "github.com/fsnotify/fsnotify" 31 "github.com/sirupsen/logrus" 32 ) 33 34 // Options exposes the configuration options 35 // used when wrapping test execution 36 type Options struct { 37 // Args is the process and args to run 38 Args []string `json:"args,omitempty"` 39 40 // ContainerName will contain the name of the container 41 // for the wrapped test process 42 ContainerName string `json:"container_name,omitempty"` 43 44 // ProcessLog will contain std{out,err} from the 45 // wrapped test process 46 ProcessLog string `json:"process_log"` 47 48 // MarkerFile will be written with the exit code 49 // of the test process or an internal error code 50 // if the entrypoint fails. 51 MarkerFile string `json:"marker_file"` 52 53 // MetadataFile is a file generated by the job, 54 // and contains job metadata info like node image 55 // versions for rendering in other tools like 56 // testgrid/gubernator. 57 // Prow will parse the file and merge it into 58 // the `metadata` field in finished.json 59 MetadataFile string `json:"metadata_file"` 60 } 61 62 type MarkerResult struct { 63 ReturnCode int 64 Err error 65 } 66 67 // AddFlags adds flags to the FlagSet that populate 68 // the wrapper options struct provided. 69 func (o *Options) AddFlags(fs *flag.FlagSet) { 70 fs.StringVar(&o.ProcessLog, "process-log", "", "path to the log where stdout and stderr are streamed for the process we execute") 71 fs.StringVar(&o.MarkerFile, "marker-file", "", "file we write the return code of the process we execute once it has finished running") 72 fs.StringVar(&o.MetadataFile, "metadata-file", "", "path to the metadata file generated from the job") 73 } 74 75 // Validate ensures that the set of options are 76 // self-consistent and valid 77 func (o *Options) Validate() error { 78 if o.ProcessLog == "" { 79 return errors.New("no log file specified with --process-log") 80 } 81 82 if o.MarkerFile == "" { 83 return errors.New("no marker file specified with --marker-file") 84 } 85 86 return nil 87 } 88 89 func WaitForMarkers(ctx context.Context, paths ...string) map[string]MarkerResult { 90 91 results := make(map[string]MarkerResult) 92 93 if len(paths) == 0 { 94 return results 95 } 96 97 for _, path := range paths { 98 if _, err := os.Stat(path); !os.IsNotExist(err) { 99 results[path] = readMarkerFile(path) 100 } 101 } 102 watcher, err := fsnotify.NewWatcher() 103 if err != nil { 104 populateMapWithError(results, fmt.Errorf("new fsnotify watch: %w", err), paths...) 105 return results 106 } 107 defer watcher.Close() 108 109 // we are assuming that all marker files will be written to the same directory. 110 // this should be the case since all marker files are written to the "logs" VolumeMount 111 dir := filepath.Dir(paths[0]) 112 for _, path := range paths { 113 if filepath.Dir(path) != dir { 114 populateMapWithError(results, fmt.Errorf("marker files are not all written to the same directory"), paths...) 115 return results 116 } 117 } 118 119 if err := watcher.Add(dir); err != nil { 120 populateMapWithError(results, fmt.Errorf("add %s to fsnotify watch: %w", dir, err), paths...) 121 return results 122 } 123 124 ticker := time.NewTicker(10 * time.Second) 125 defer ticker.Stop() 126 127 for len(results) < len(paths) { 128 select { 129 case <-ctx.Done(): 130 populateMapWithError(results, fmt.Errorf("cancelled: %w", ctx.Err()), paths...) 131 return results 132 case event := <-watcher.Events: 133 for _, path := range paths { 134 if event.Name == path && event.Op&fsnotify.Create == fsnotify.Create { 135 results[path] = readMarkerFile(path) 136 } 137 } 138 case err := <-watcher.Errors: 139 logrus.WithError(err).Warn("fsnotify watch error") 140 case <-ticker.C: 141 for _, path := range paths { 142 if _, err := os.Stat(path); !os.IsNotExist(err) { 143 results[path] = readMarkerFile(path) 144 } 145 } 146 } 147 } 148 return results 149 150 } 151 152 func populateMapWithError(markerMap map[string]MarkerResult, err error, paths ...string) { 153 for _, path := range paths { 154 if _, exists := markerMap[path]; !exists { 155 markerMap[path] = MarkerResult{-1, err} 156 } 157 } 158 } 159 160 func readMarkerFile(path string) MarkerResult { 161 returnCodeData, err := os.ReadFile(path) 162 if err != nil { 163 return MarkerResult{-1, fmt.Errorf("bad read: %w", err)} 164 } 165 returnCode, err := strconv.Atoi(strings.TrimSpace(string(returnCodeData))) 166 if err != nil { 167 return MarkerResult{-1, fmt.Errorf("invalid return code: %w", err)} 168 } 169 return MarkerResult{returnCode, nil} 170 }