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