github.com/munnerz/test-infra@v0.0.0-20190108210205-ce3d181dc989/testgrid/cmd/configurator/main.go (about) 1 /* 2 Copyright 2016 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 main 18 19 import ( 20 "context" 21 "errors" 22 "flag" 23 "fmt" 24 "io/ioutil" 25 "log" 26 "net/url" 27 "os" 28 "strings" 29 "time" 30 31 "k8s.io/test-infra/testgrid/util/gcs" 32 33 "cloud.google.com/go/storage" 34 ) 35 36 type multiString []string 37 38 func (m multiString) String() string { 39 return strings.Join(m, ",") 40 } 41 42 func (m *multiString) Set(v string) error { 43 *m = strings.Split(v, ",") 44 return nil 45 } 46 47 type options struct { 48 creds string 49 inputs multiString 50 oneshot bool 51 output string 52 printText bool 53 validateConfigFile bool 54 } 55 56 func gatherOptions() (options, error) { 57 o := options{} 58 flag.StringVar(&o.creds, "gcp-service-account", "", "/path/to/gcp/creds (use local creds if empty") 59 flag.BoolVar(&o.oneshot, "oneshot", false, "Write proto once and exit instead of monitoring --yaml files for changes") 60 flag.StringVar(&o.output, "output", "", "write proto to gs://bucket/obj or /local/path") 61 flag.BoolVar(&o.printText, "print-text", false, "print generated proto in text format to stdout") 62 flag.BoolVar(&o.validateConfigFile, "validate-config-file", false, "validate that the given config files are syntactically correct and exit (proto is not written anywhere)") 63 flag.Var(&o.inputs, "yaml", "comma-separated list of input YAML files") 64 flag.Parse() 65 if len(o.inputs) == 0 || o.inputs[0] == "" { 66 return o, errors.New("--yaml must include at least one file") 67 } 68 69 if !o.printText && !o.validateConfigFile && o.output == "" { 70 return o, errors.New("--print-text or --output=gs://path required") 71 } 72 if o.validateConfigFile && o.output != "" { 73 return o, errors.New("--validate-config-file doesn't write the proto anywhere") 74 } 75 return o, nil 76 } 77 78 // announceChanges watches for changes to files and writes one of them to the channel 79 func announceChanges(ctx context.Context, paths []string, channel chan []string) { 80 defer close(channel) 81 modified := map[string]time.Time{} 82 for _, p := range paths { 83 modified[p] = time.Time{} // Never seen 84 } 85 86 // TODO(fejta): consider waiting for a notification rather than polling 87 // but performance isn't that big a deal here. 88 for { 89 var changed []string 90 for p, last := range modified { 91 select { 92 case <-ctx.Done(): 93 return 94 default: 95 } 96 switch info, err := os.Stat(p); { 97 case os.IsNotExist(err) && !last.IsZero(): 98 // File deleted 99 modified[p] = time.Time{} 100 changed = append(changed, p) 101 case err != nil: 102 log.Printf("Error reading %s: %v", p, err) 103 default: 104 if t := info.ModTime(); t.After(last) { 105 changed = append(changed, p) 106 modified[p] = t 107 } 108 } 109 } 110 if len(changed) > 0 { 111 select { 112 case <-ctx.Done(): 113 return 114 case channel <- changed: 115 } 116 } else { 117 time.Sleep(1 * time.Second) 118 } 119 } 120 } 121 122 func readConfig(paths []string) (*Config, error) { 123 var c Config 124 for _, file := range paths { 125 b, err := ioutil.ReadFile(file) 126 if err != nil { 127 return nil, fmt.Errorf("failed to read %s: %v", file, err) 128 } 129 if err = c.Update(b); err != nil { 130 return nil, fmt.Errorf("failed to merge %s into config: %v", file, err) 131 } 132 } 133 return &c, nil 134 } 135 136 func write(ctx context.Context, client *storage.Client, path string, bytes []byte) error { 137 u, err := url.Parse(path) 138 if err != nil { 139 return fmt.Errorf("invalid url %s: %v", path, err) 140 } 141 if u.Scheme != "gs" { 142 return ioutil.WriteFile(path, bytes, 0644) 143 } 144 var p gcs.Path 145 if err = p.SetURL(u); err != nil { 146 return err 147 } 148 return gcs.Upload(ctx, client, p, bytes) 149 } 150 151 func doOneshot(ctx context.Context, client *storage.Client, opt options) error { 152 // Ignore what changed for now and just recompute everything 153 c, err := readConfig(opt.inputs) 154 if err != nil { 155 return fmt.Errorf("could not read config: %v", err) 156 } 157 158 // Print proto if requested 159 if opt.printText { 160 if err := c.MarshalText(os.Stdout); err != nil { 161 return fmt.Errorf("could not print config: %v", err) 162 } 163 } 164 165 // Write proto if requested 166 if opt.output != "" { 167 b, err := c.MarshalBytes() 168 if err == nil { 169 err = write(ctx, client, opt.output, b) 170 } 171 if err != nil { 172 return fmt.Errorf("could not write config: %v", err) 173 } 174 } 175 return nil 176 } 177 178 func main() { 179 // Parse flags 180 opt, err := gatherOptions() 181 if err != nil { 182 log.Fatalf("Bad flags: %v", err) 183 } 184 185 ctx := context.Background() 186 187 // Config file validation only 188 if opt.validateConfigFile { 189 if err := doOneshot(ctx, nil, opt); err != nil { 190 log.Fatalf("FAIL: %v", err) 191 } 192 log.Println("Config validated successfully") 193 return 194 } 195 196 // Setup GCS client 197 client, err := gcs.ClientWithCreds(ctx, opt.creds) 198 if err != nil { 199 log.Fatalf("Failed to create storage client: %v", err) 200 } 201 202 // Oneshot mode, write config and exit 203 if opt.oneshot { 204 if err := doOneshot(ctx, client, opt); err != nil { 205 log.Fatalf("FAIL: %v", err) 206 } 207 return 208 } 209 210 // Service mode, monitor input files for changes 211 channel := make(chan []string) 212 // Monitor files for changes 213 go announceChanges(ctx, opt.inputs, channel) 214 215 // Wait for changed files 216 for changes := range channel { 217 log.Printf("Changed: %v", changes) 218 log.Println("Writing config...") 219 if err := doOneshot(ctx, client, opt); err != nil { 220 log.Printf("FAIL: %v", err) 221 continue 222 } 223 log.Printf("Wrote config to %s", opt.output) 224 } 225 }