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  }