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  }