blob: 6bd062040a621711b236350aa5e25229a8599334 [file] [log] [blame]
// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Package site contains site local constants for the shivas
package site
import (
"flag"
"fmt"
"os"
"path/filepath"
"slices"
"strings"
"go.chromium.org/luci/auth"
"go.chromium.org/luci/auth/scopes"
"go.chromium.org/luci/common/errors"
"go.chromium.org/luci/common/tsmon"
"go.chromium.org/luci/grpc/prpc"
"go.chromium.org/luci/hardcoded/chromeinfra"
ufsUtil "go.chromium.org/infra/unifiedfleet/app/util"
)
// Environment contains environment specific values.
type Environment struct {
AdminService string
InventoryService string
UnifiedFleetService string
SwarmingService string
SwarmingServiceAccount string
LogdogService string
QueenService string
}
var TSMonFlags = tsmon.NewFlags()
// prod is the environment for prod.
func prod(isLocal bool, instance string) Environment {
if isLocal {
return localDevEnv
}
env := prodEnv
if instance != "" {
env.UnifiedFleetService = fmt.Sprintf("%s.%s", instance, env.UnifiedFleetService)
}
return env
}
// dev is the environment for dev.
func dev(isLocal bool, instance string) Environment {
if isLocal {
return localDevEnv
}
env := devEnv
env.UnifiedFleetService = fmt.Sprintf("%s.%s", instance, env.UnifiedFleetService)
return env
}
// CommonFlags controls some commonly-used CLI flags.
type CommonFlags struct {
verbose bool
}
// Register sets up the common flags.
func (f *CommonFlags) Register(fl *flag.FlagSet) {
// Register the tsmon flags and set flush to manual
TSMonFlags.Flush = tsmon.FlushManual
TSMonFlags.Register(fl)
fl.BoolVar(&f.verbose, "verbose", false, "log more details")
}
// Verbose returns if the command is set to verbose mode.
func (f *CommonFlags) Verbose() bool {
return f.verbose
}
// OutputFlags controls output-related CLI flags.
type OutputFlags struct {
json bool
tsv bool
full bool
noemit bool
}
// Register sets up the output flags.
func (f *OutputFlags) Register(fl *flag.FlagSet) {
fl.BoolVar(&f.json, "json", false, "log output in json format")
fl.BoolVar(&f.tsv, "tsv", false, "log output in tsv format (without title)")
fl.BoolVar(&f.full, "full", false, "log full output in specified format, only works for GET command(latency is high). Users can also set os env SHIVAS_FULL_MODE to enable this.")
fl.BoolVar(&f.noemit, "noemit", false, "specifies NOT to emit/print unpopulated fields in json format. Users can also set os env SHIVAS_NO_JSON_EMIT to enable this.")
}
// JSON returns if the output is logged in json format
func (f *OutputFlags) JSON() bool {
return f.json
}
// Tsv returns if the output is logged in tsv format (without title)
func (f *OutputFlags) Tsv() bool {
return f.tsv
}
// Full returns if the full format of output is logged in tsv format (without title)
func (f *OutputFlags) Full() bool {
return f.full
}
// NoEmit returns if output json should NOT print/emit unpopulated fields
func (f *OutputFlags) NoEmit() bool {
return f.noemit
}
// EnvFlags controls selection of the environment: either prod (default) or dev.
type EnvFlags struct {
local bool
dev bool
instance string
namespace string
}
// Register sets up the -dev argument.
func (f *EnvFlags) Register(fl *flag.FlagSet) {
fl.BoolVar(&f.local, "local", localDefault, "Run locally (or use env var SHIVAS_ENV=LOCAL)")
fl.BoolVar(&f.dev, "dev", false, "Run in dev environment (or use env var SHIVAS_ENV=DEV)")
fl.StringVar(&f.instance, "instance", "", "Target a specific instance (eg. canary, stable)")
fl.StringVar(&f.namespace, "namespace", "", fmt.Sprintf("namespace where data resides. Users can also set os env SHIVAS_NAMESPACE. Valid namespaces: [%s]", strings.Join(ufsUtil.ValidClientNamespaceStr(), ", ")))
shivasEnv := strings.ToLower(os.Getenv("SHIVAS_ENV"))
f.local = f.local || (shivasEnv == "local")
f.dev = shivasEnv == "dev"
}
// Function validate checks the flags for correctness.
//
// For example, -local and -dev are mutually exclusive.
func (f *EnvFlags) validate() error {
devlikeFlags := 0
if f.local {
devlikeFlags++
if f.instance != "" {
return errors.New("-local and -instance are not compatible")
}
}
if f.dev {
devlikeFlags++
switch f.instance {
case "stable", "canary":
return errors.New("cannot call prod UFS with -dev flags")
case "":
f.instance = "staging"
}
}
if devlikeFlags > 1 {
return errors.New("exactly one of -dev and -local may be specified")
} else if devlikeFlags == 0 {
switch f.instance {
case "stable", "canary", "":
// okay
default:
return errors.New("instance must be one of 'canary' or 'stable' for prod env")
}
}
return nil
}
// Env returns the environment, either dev or prod.
func (f EnvFlags) Env() Environment {
if err := f.validate(); err != nil {
panic(err.Error())
}
if f.local {
return dev(true, "")
}
if f.dev {
return dev(false, f.instance)
}
return prod(false, f.instance)
}
// Namespace returns the namespace and validates the namespace is:
// 1) nonempty
// 2) a namespace that UFS expects
// 3) a namespace the command expects
//
// Supports default namespaces if defaultNS != "". These will be returned if
// no value is passed via flag or env var
func (f EnvFlags) Namespace(validNSList []string, defaultNS string) (string, error) {
ns := strings.ToLower(f.namespace)
if ns == "" {
ns = strings.ToLower(os.Getenv("SHIVAS_NAMESPACE"))
}
// If ns is empty, user did not pass in namespace via flag or env var.
if ns == "" {
// no default + no input = error
if defaultNS == "" {
return ns, errors.New(fmt.Sprintf("namespace is a required field. Users can also set os env SHIVAS_NAMESPACE. Valid namespaces: [%s]", strings.Join(ufsUtil.ValidClientNamespaceStr(), ", ")))
}
ns = defaultNS
}
// This is a separate check from `IsClientNamespace` to ensure that
// we catch if `validNSList` contains strings UFS does not expect.
// If validNSList is empty, we ignore it
if validNSList != nil && !slices.Contains(validNSList, ns) {
return ns, errors.New(fmt.Sprintf("namespace %s is invalid. Users can also set os env SHIVAS_NAMESPACE. Valid namespaces for this command: [%s]", ns, strings.Join(validNSList, ", ")))
}
// This catches a namespace which the local command thinks is valid, but
// will be rejected by UFS.
if !ufsUtil.IsClientNamespace(ns) {
return ns, errors.New(fmt.Sprintf("namespace %s is invalid. Users can also set os env SHIVAS_NAMESPACE. Valid namespaces: [%s]", ns, strings.Join(ufsUtil.ValidClientNamespaceStr(), ", ")))
}
return ns, nil
}
var (
// OSLikeNamespaces are namespaces that store primarily chromeos data.
// This includes a separate partner namespace which stores chromeos data.
OSLikeNamespaces = []string{ufsUtil.OSNamespace, ufsUtil.OSPartnerNamespace}
// AllNamespaces contain all namespaces UFS considers valid.
AllNamespaces = ufsUtil.ValidClientNamespaceStr()
)
// DefaultAuthScopes is the default scopes for shivas login
var DefaultAuthScopes = append(
scopes.CloudScopeSet(),
"https://www.googleapis.com/auth/spreadsheets",
)
// DefaultAuthOptions is an auth.Options struct prefilled with chrome-infra
// defaults.
var DefaultAuthOptions = chromeinfra.SetDefaultAuthOptions(auth.Options{
Scopes: GetAuthScopes(DefaultAuthScopes),
SecretsDir: SecretsDir(),
})
// GetAuthScopes get environment scopes if set
// Otherwise, return default scopes
func GetAuthScopes(defaultScopes []string) []string {
e := os.Getenv("OAUTH_SCOPES")
if e != "" {
return strings.Split(e, "|")
}
return defaultScopes
}
// SecretsDir customizes the location for auth-related secrets.
func SecretsDir() string {
configDir := os.Getenv("XDG_CACHE_HOME")
if configDir == "" {
configDir = filepath.Join(os.Getenv("HOME"), ".cache")
}
return filepath.Join(configDir, "shivas", "auth")
}
// VersionNumber is the version number for the tool. It follows the Semantic
// Versioning Specification (http://semver.org) and the format is:
// "MAJOR.MINOR.0+BUILD_TIME".
// We can ignore the PATCH part (i.e. it's always 0) to make the maintenance
// work easier.
// We can also print out the build time (e.g. 20060102150405) as the METADATA
// when show version to users.
var VersionNumber = fmt.Sprintf("%d.%d.%d", Major, Minor, Patch)
// Major is the Major version number
const Major = 7
// Minor is the Minor version number
const Minor = 2
// Patch is the PAtch version number
const Patch = 0
// DefaultPRPCOptions is used for PRPC clients. If it is nil, the
// default value is used. See prpc.Options for details.
//
// This is provided so it can be overridden for testing.
func DefaultPRPCOptions(e EnvFlags) *prpc.Options {
if e.local {
return &prpc.Options{
Insecure: true,
UserAgent: fmt.Sprintf("shivas/%s", VersionNumber),
}
}
return ProdDefaultPRPCOptions()
}
// ProdDefaultPRPCOptions returns UFS PRPC client options for the prod project
func ProdDefaultPRPCOptions() *prpc.Options {
return prpcOptionWithUserAgent(fmt.Sprintf("%s/%s", "shivas", VersionNumber))
}
// CipdInstalledPath is the installed path for shivas package.
var CipdInstalledPath = "infra/shivas/"
// prpcOptionWithUserAgent create prpc option with custom UserAgent.
//
// DefaultOptions provides Retry ability in case we have issue with service.
func prpcOptionWithUserAgent(userAgent string) *prpc.Options {
options := *prpc.DefaultOptions()
options.UserAgent = userAgent
return &options
}