github.com/GoogleCloudPlatform/testgrid@v0.0.174/cmd/tabulator/main.go (about)

     1  /*
     2  Copyright 2022 The TestGrid 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  	"runtime"
    24  	"strings"
    25  	"time"
    26  
    27  	gpubsub "cloud.google.com/go/pubsub"
    28  	"github.com/GoogleCloudPlatform/testgrid/pkg/pubsub"
    29  	"github.com/GoogleCloudPlatform/testgrid/pkg/tabulator"
    30  	"github.com/GoogleCloudPlatform/testgrid/util"
    31  	"github.com/GoogleCloudPlatform/testgrid/util/gcs"
    32  	"github.com/GoogleCloudPlatform/testgrid/util/metrics/prometheus"
    33  	"github.com/sirupsen/logrus"
    34  	"google.golang.org/api/option"
    35  )
    36  
    37  // options configures the updater
    38  type options struct {
    39  	config              gcs.Path // gs://path/to/config/proto
    40  	persistQueue        gcs.Path
    41  	creds               string
    42  	confirm             bool
    43  	useTabAlertSettings bool
    44  	calculateStats      bool
    45  	extendState         bool
    46  	groups              util.Strings
    47  	readConcurrency     int
    48  	writeConcurrency    int
    49  	wait                time.Duration
    50  	gridPathPrefix      string
    51  	tabStatePathPrefix  string
    52  	pubsub              string
    53  
    54  	debug    bool
    55  	trace    bool
    56  	jsonLogs bool
    57  }
    58  
    59  // validate ensures reasonable options
    60  func (o *options) validate() error {
    61  	if o.config.String() == "" {
    62  		return errors.New("empty --config")
    63  	}
    64  	if o.writeConcurrency < 1 {
    65  		o.writeConcurrency = 4 * runtime.NumCPU()
    66  	}
    67  	if o.readConcurrency < 1 {
    68  		o.readConcurrency = (o.writeConcurrency / 2) + 1
    69  	}
    70  
    71  	return nil
    72  }
    73  
    74  // gatherOptions reads options from flags
    75  func gatherOptions() options {
    76  	var o options
    77  
    78  	flag.Var(&o.config, "config", "gs://path/to/config.pb")
    79  	flag.Var(&o.persistQueue, "persist-queue", "Load previous queue state from gs://path/to/queue-state.json and regularly save to it thereafter")
    80  	flag.StringVar(&o.creds, "gcp-service-account", "", "/path/to/gcp/creds (use local creds if empty)")
    81  	flag.BoolVar(&o.confirm, "confirm", false, "Upload data if set")
    82  	flag.Var(&o.groups, "group", "Only update named test group if set (repeateable)")
    83  	flag.BoolVar(&o.useTabAlertSettings, "tab-alerts", false, "Use newer tab settings while caculating alerts")
    84  	flag.BoolVar(&o.calculateStats, "column-stats", true, "Calculates stats for broken columns")
    85  	flag.BoolVar(&o.extendState, "extend", false, "Extend tab state instead of replacing it")
    86  
    87  	flag.IntVar(&o.readConcurrency, "read-concurrency", 0, "Manually define the number of groups to read and hold in memory at once if non-zero")
    88  	flag.IntVar(&o.writeConcurrency, "concurrency", 0, "Manually define the number of tabs to concurrently update if non-zero")
    89  	flag.IntVar(&o.writeConcurrency, "write-concurrency", 0, "alias for --concurrency")
    90  	flag.DurationVar(&o.wait, "wait", 0, "Ensure at least this much time has passed since the last loop (exit if zero).")
    91  
    92  	flag.StringVar(&o.gridPathPrefix, "grid-path", "grid", "Read grid states under this GCS path.")
    93  	flag.StringVar(&o.tabStatePathPrefix, "tab-state-path", "tabs", "Write tab states under this GCS path.")
    94  	flag.StringVar(&o.pubsub, "pubsub", "", "listen for test group updates at project/subscription")
    95  
    96  	flag.BoolVar(&o.debug, "debug", false, "Log debug lines if set")
    97  	flag.BoolVar(&o.trace, "trace", false, "Log trace and debug lines if set")
    98  	flag.BoolVar(&o.jsonLogs, "json-logs", false, "Uses a json logrus formatter when set")
    99  
   100  	flag.Parse()
   101  	return o
   102  }
   103  
   104  func main() {
   105  	opt := gatherOptions()
   106  	if err := opt.validate(); err != nil {
   107  		logrus.Fatalf("Invalid flags: %v", err)
   108  	}
   109  	if !opt.confirm {
   110  		logrus.Warning("--confirm=false (DRY-RUN): will not write to gcs")
   111  	}
   112  	switch {
   113  	case opt.trace:
   114  		logrus.SetLevel(logrus.TraceLevel)
   115  	case opt.debug:
   116  		logrus.SetLevel(logrus.DebugLevel)
   117  	}
   118  
   119  	if opt.jsonLogs {
   120  		logrus.SetFormatter(&logrus.JSONFormatter{})
   121  	}
   122  	logrus.SetReportCaller(true)
   123  
   124  	ctx, cancel := context.WithCancel(context.Background())
   125  	defer cancel()
   126  
   127  	storageClient, err := gcs.ClientWithCreds(ctx, opt.creds)
   128  	if err != nil {
   129  		logrus.WithError(err).Fatal("Failed to create storage client")
   130  	}
   131  	defer storageClient.Close()
   132  
   133  	client := gcs.NewClient(storageClient)
   134  
   135  	logrus.WithFields(logrus.Fields{
   136  		"read":  opt.readConcurrency,
   137  		"write": opt.writeConcurrency,
   138  	}).Info("Configured concurrency")
   139  
   140  	fixers := make([]tabulator.Fixer, 0, 2)
   141  
   142  	fixer, err := gcsFixer(ctx, opt.pubsub, opt.config, opt.gridPathPrefix, opt.creds)
   143  	if err != nil {
   144  		logrus.WithError(err).WithField("subscription", opt.pubsub).Fatal("Failed to configure pubsub")
   145  	}
   146  	if fixer != nil {
   147  		fixers = append(fixers, fixer)
   148  	}
   149  	if path := opt.persistQueue; path.String() != "" {
   150  		const freq = time.Minute
   151  		ticker := time.NewTicker(freq)
   152  		log := logrus.WithField("frequency", freq)
   153  		fixers = append(fixers, tabulator.FixPersistent(log, client, path, ticker.C))
   154  	}
   155  
   156  	mets := tabulator.CreateMetrics(prometheus.NewFactory())
   157  
   158  	opts := &tabulator.UpdateOptions{
   159  		ConfigPath:          opt.config,
   160  		ReadConcurrency:     opt.readConcurrency,
   161  		WriteConcurrency:    opt.writeConcurrency,
   162  		GridPathPrefix:      opt.gridPathPrefix,
   163  		TabsPathPrefix:      opt.tabStatePathPrefix,
   164  		AllowedGroups:       opt.groups.Strings(),
   165  		Confirm:             opt.confirm,
   166  		CalculateStats:      opt.calculateStats,
   167  		UseTabAlertSettings: opt.useTabAlertSettings,
   168  		ExtendState:         opt.extendState,
   169  		Freq:                opt.wait,
   170  	}
   171  
   172  	if err := tabulator.Update(ctx, client, mets, opts, fixers...); err != nil {
   173  		logrus.WithError(err).Error("Could not tabulate")
   174  	}
   175  }
   176  
   177  func gcsFixer(ctx context.Context, projectSub string, configPath gcs.Path, gridPrefix, credPath string) (tabulator.Fixer, error) {
   178  	if projectSub == "" {
   179  		return nil, nil
   180  	}
   181  	parts := strings.SplitN(projectSub, "/", 2)
   182  	if len(parts) != 2 {
   183  		return nil, errors.New("malformed project/subscription")
   184  	}
   185  	projID, subID := parts[0], parts[1]
   186  	pubsubClient, err := gpubsub.NewClient(ctx, "", option.WithCredentialsFile(credPath))
   187  	if err != nil {
   188  		logrus.WithError(err).Fatal("Failed to create pubsub client")
   189  	}
   190  	client := pubsub.NewClient(pubsubClient)
   191  	return tabulator.FixGCS(client, logrus.StandardLogger(), projID, subID, configPath, gridPrefix)
   192  }